Part 2, UI Widgets from scratch in Jetpack Compose.
Tutorial, Part 2 of 3 (The Deck of Cards)
In this trilogy of tutorials, I will show how I build Compound UI Widgets for my pet project (Study Cards app — my version of the flash/mem cards).
In Part One of this tutorial, I showed how to build an iOS-styled ListView widget with the rows (list items) that have a different shape depending on the number of rows and the row location and that look different in edit or delete mode.
In the second part, we will build another UI Widget — the Deck of Cards (Figure 1).
I like this widget because it mimics an object from the physical world and reminds the paper stickie notes. And who doesn’t like sticky notes, especially the digital stickies for which you don’t need to kill a tree?
I was using similar widgets in my previous projects, but I used third-party open-source libraries until now. This time, we will build the Deck of Cards from scratch, and in the following article we will add the swipe and flip animation and the drag gesture recognizer. Moreover, we will refactor the code so it will follow some of the SOLID principles.
A single card first (Surface, Scaffold)
First, let’s create a
CardView.kt file inside the deck package, and let’s add some constants and enum that define which side of the card is visible. Now, we can create the top level compostable function —
StudyCardView. It accepts the current side and current background color and an external modifier. To keep this function simple, the content of the card and the bottom bar we will define in separate composable functions, then we will pass them as parameters.
In order to add a shadow below the card and create rounded corners, we need to use a composable Surface as a root element. To arrange the buttons at the bottom and a content (Text) in the center of the card, we shall use a Scaffold.
StudyCardView is straightforward. We define the
shape and the
elevation of the card. Then, we determine the card’s background
color depending on the side of the card — yellow for the backside and some unique color for the front. The Scaffold takes the full size of the card and accepts the bottom bar and
Content and Bottom Bar (Column, Row, Text, Spacer)
By extracting content and bottom bar from the top level function, we get a few benefits:
1. The flexibility to have more than one implementation. For example - in production, I use an Android
WebView to render simple HTML inside the card. But for this tutorial, we will use plain text.
2. Our top level function remains short. Jetpack Compose functions tend to grow and become cluttered, so we need to try our best to keep them short to follow the common sense or general good practice recommendations!
StudyCardContent() function, we place just a Column that takes up all available space and embeds the center-aligned Text.
StudyCardsBottomBar() composable emits a Row that contains two Buttons, two Spacers, and the Text. We have to add Spacers to keep the text in the middle of the card between two buttons. The backside of the card is always yellow, but the buttons turn to the front side cards color. Also, this function accepts the action handlers. The title for the right button is always
“Say”, but for the left button, it depends on the side — it is
“Back”. And finally, the Text will show the number of the current card and the total number of cards in the deck.
That’s it for the single card!
To see the front and the back sides in preview, all you need to do is add those
@Composable functions with the
@Preview annotation. As you can see, the only difference between those two functions is the side parameter. In the first function, we pass FRONT_FACE and in the second BACK_FACE. The result is in Figure 2.
Now, we’re ready to collect a few of those cards into the Deck.
StudyCardDeck (Box, zIndex, Scale, Offset)
For the new widget, create a new file,
DeckOfCards.kt, under the same package — deck. Before making a composable function, let’s prepare a demo data model class and a few helper methods. The
StudyCard objects keep an index, front side text message, backside text message, and both messages’ languages — very simple.
In the array of colors, I keep only three colors for cards in the deck. Two methods calculate the scale and offset for each card. The top card will have a scale 1 and the base
paddingOffset. Each next card will be smaller and will stick down because of the
paddingOffset multiplied by index plus one.
The deck itself is a Box that takes the full available size. And, it contains inside the composable element
visibleCards times. In other words, we take 3 cards from the data source, emit the card with the index 0 with the
zIndex 100, the next card’s
zIndex is 99, and the third card is 98. Such a high number guarantees that it will go above all elements on the screen when we drag the card.
StudyCardDeck() composable accepts the
current index in the data source, the number of
visible cards, and the collection of data models, a
dataSource. Take a note that the
current is supposed to be defined as a state so that Jetpack Compose will recompose the widget for the next current card. Inside, we determine the number of visible cards for the current index. We calculate the
colorIndex in the array,
offsetY. We create the
cardModifier and pass all those parameters to
To preview and test
StudyCardDeck(), we need to make a stub data collection and the
@Preview function. Let’s remember the current and the visible parameters as states. Then, let’s place the deck widget in the content of another Scaffold, and to the top bar, add two buttons, Next and Add. The Next button will increment the
current state, and the Add button will increment the
visible so that we could test the widget behavior with the different amount of cards.
The Live Preview, you can find in the middle of Figure 3 at the bottom.
That’s it for now.
Four composable functions and ~150 lines of code to build such a widget is not bad at all! Of course, it’s going to become more complex when we add animations and the gesture handler. However, you can clearly see that Jetpack Compose’s declarative approach is way concise! Usually, the only Adapter class for the RecycleView based List/Grid/Deck widget is much bigger than 150 lines.
On the right of the Figure 3 is the preview from the upcoming article. We will add the swipe and flip animation and the drag gesture recognizer, and we will follow some of the SOLID principles to separate concerns.
Please stay tuned, and checkout the Part One of this tutorial!
UI Widgets from scratch in Jetpack Compose
Thinking in Compose:
StudyCards app helps to memorize things (my version of the flashcards/memory cards):
Google Play(Beta, work in progress):