I’m always looking forward to learning new things, and this time Jetpack Compose was the thing I decided to learn.
I use Google Sheets to keep track of my income/expenses so I decided to build an app using Compose that would do same.
In this article I’ll be showing the mistakes I made and how I built some of the customized components that were used in the project.
Home Background Banner

I was looking for inspiration and came across some other apps that used this curved background, so I decided to use it for the home header.
This component is pretty simple, we basically have the curved background and the rest of the content above it. What makes this a little bit more complicated is the fact the the background height is not static, I made it so it adapts to the content that’s being displayed above it.
First I started by creating a custom shape that has the curvature I want, to do that I extended GenericShape and used cubicTo do draw it. I arrived at these numbers by trial and error, you can change them if you want your shape to look different.
| class SemiOvalShape : Shape by GenericShape(builder = { size, _ -> | |
| lineTo(size.width, 0f) | |
| relativeLineTo(0f, size.height * 0.8f) | |
| cubicTo( | |
| x1 = size.width * .7f, | |
| y1 = size.height, | |
| x2 = size.width * .3f, | |
| y2 = size.height, | |
| x3 = 0f, | |
| y3 = size.height * 0.8f | |
| ) | |
| }) |
After I had the custom shape, I created the header component.
| @Composable | |
| private fun HomeHeader( | |
| ... | |
| ) { | |
| Box { | |
| val density = LocalDensity.current | |
| // How big the curved section of the box is | |
| val backgroundBottomPadding = 96.dp | |
| // How much spacing you have between the content inside your box and the content placed below it | |
| val bottomSpacer = 16.dp | |
| // Set to 264.dp initilly just so the calculatation below doesn't result in a negative number | |
| var boxHeight by remember { mutableStateOf(264.dp) } | |
| val contentTopPadding by remember { | |
| derivedStateOf { boxHeight - backgroundBottomPadding + bottomSpacer } | |
| } | |
| Column( | |
| modifier = Modifier | |
| .fillMaxWidth() | |
| // Code that draws the curved background | |
| .background(remember { | |
| Brush.verticalGradient(listOf(Purple40, Purple20)) | |
| }, remember { | |
| SemiOvalShape() | |
| }) | |
| .padding(horizontal = 16.dp) | |
| .onSizeChanged { | |
| boxHeight = with(density) { it.height.toDp() } | |
| } | |
| ) { | |
| ... // The content that's shown inside the box goes here (Welcome Back, Income/Expenses) | |
| Spacer(modifier = Modifier.height(backgroundBottomPadding)) | |
| } | |
| Box( | |
| modifier = Modifier | |
| .padding(top = contentTopPadding) | |
| .fillMaxWidth() | |
| ) { | |
| ... // The content that's shown below the box goes here (Add Expense, Add Income, ...) | |
| } | |
| } | |
| } |
- I defined some variables to set how big the spacing between components should be, you can tweak them if needed.
- Then I calculated
contentTopPadding, this is what makes the box adaptable to different content sizes. - After that I added the Column that draws the curved background, it’s children are drawn inside the box (Welcome back and Income/Expenses).
- Below that I added the Box that hosts the content shown below the curved background (Grid menu).
In the image below you can see I added another component and the box resized itself correctly.

Selection Button Animation

Job Offers
This is a simple filter that uses a box in the background to indicate which option is currently selected.
There are 3 things happening here:
- The box width changes
- The box position changes
- The corner radius changes (take a look at the inner corners when only 1 option is selected)
We use animateDpState to easy animate dp values, unfortunately that’s not possible for corner shapes so we have to create a new shape every time the radius changes.
Then we just need to define the components and Compose handles all of the animation for us, I liked the way this button turned out given its simplicity.
Vertical/Horizontal Scroll

I have a sheet that shows how much I spent by category by month, that’s pretty easy to visualize when you’re using a big screen but for mobile devices I had to come up with a different approach.
I don’t like landscape mode so I didn’t even consider that as an option here. What I ended up doing is making everything scrollable but the months and categories are fixed.
| val verticalScroll = rememberScrollState() | |
| val horizontalScroll = rememberScrollState() | |
| Row( | |
| modifier = Modifier.horizontalScroll(horizontalScroll) | |
| ) { | |
| ... // Oct/22, Sep/22, ... | |
| } | |
| Column( | |
| modifier = Modifier | |
| .verticalScroll(verticalScroll) | |
| ) { | |
| // Food, Gym, Car, Subscriptions | |
| } | |
| Column( | |
| modifier = Modifier | |
| .verticalScroll(verticalScroll) | |
| .horizontalScroll(horizontalScroll) | |
| ) { | |
| // 563, 212, 525, 222, 662, 12, 661, ... | |
| } |
If you take a look again at the video above you can see that whenever I scroll the main content, the other parts are also scrolled. I discovered that it’s possible to use the same ScrollState multiple times, I had no idea this would work but by using the same ScrollState I can get all components to be synchronized.
If you want to see how I built this whole component, you can check this file.
All the code is available in this repository.
I’m far from being an expert in Compose. This article shows how I built these components using the knowledge I had at the time, meaning that this is not necessarily the most performant code nor the simplest way to do a certain thing. If you know of anything that could be improved, please let me know.
Cover photo by Markus Winkler on Unsplash
Originally published at https://victorbrandalise.com on October 11, 2022.



