Change my mind or Android development transformation to Jetpack Compose & Coroutines

Introduction
Jetpack Compose is one of the most discussed topics covered in the recent Android 11 video series. It is expected to solve most of the problems with the current Android UI toolkit, which contains lots of legacies. Another promising tool for Android-development is Kotlin Coroutines and especially Flow API which is supposed to help avoid over engineering with RxJava.
In this article I would like to show you a small application built with Jetpack Compose UI and with the use of Coroutines StateFlow as a tool for sharing state between screens. Moreover, MVI architecture will be also used.
I would like to note that the approaches shown may differ a little bit from the last recommended ones. I am still exploring these tools, so if you find anything that can be improved — feel free to mention it in a comment or repository issues.
Let’s start with the application idea. I am a big fan of coffee. And I have noticed that I drink it too much. Therefore, I need an app to track my coffee intake. There are lots of such apps for water and alcoholic beverages, but not for coffee. The first implementation of the app should contain two screens: a month table with the icons of coffee cups and a list of various coffee types with an amount of coffee of each type taken on a particular day.
Jetpack Compose
If you take a look at Jetpack Compose for the first time after layouts in XML, you will probably have a feeling of confusion because you write the UI-code in Kotlin files and should always think about the state.
I had an experience of using Flutter which is built on widgets and their states. This helped me to understand the conception more easily and make the prototype of the app in 6 evenings after work. Also, I will make a comparison between Flutter and Compose in the article.
Another helpful resource here can be a website with a correlation between usual UI components and Compose’s ones and an example of their usage.
As well as in Flutter, in Compose you can use a MainActivity as an entry point to your application, while routing can be made by the composition of views without any other activities or fragments. The entry point can also be put in any new activity of already existing common-UI applications.
I have started with sample Compose project in Android Studio. Here is a code of MainActivity.kt:
If you have already seen a Compose, you can read this part fluently.
Compose is built on the basis of functions marked with @Composable annotation. It allows Kotlin compiler plugin to generate the required code.
Instead of usual setContentView() function called in Activity.onCreate(), a setContent() function with parameter taking Composable function should be called in.
Another new thing here is a @Preview annotation above Composable function. It allows to preview the look of inner components via a new version of Android Studio (I use Android Studio 4.2 Canary 2).
To update the preview you should rebuild your project. It is similar to Flutter’s hot reload, but a bit slower and without a real-time code analyzer showing current compile errors. Thus, you can change UI in one file and you will not be able to preview it with errors in others.
Another problem I had struggled with was the removal of the whole .idea directory from source control and files from it after committing. A Preview was not available anymore and I started the project from scratch again.
Nevertheless, I would recommend to have at least one Preview function in each file with UI-code to have an opportunity to see the changes made in the current file.
Let’s create the first custom view for the application. It will be a list item with a coffee type, an amount of coffee, and buttons to change the number of cups.

This list item is represented by Row widget (analog of ListView with horizontal orientation). There is an image (loaded now from a png in drawable) inside, a spacer making a margin, a text with a coffee name, filling all the available space because of weight(1f) modifier (similar as in ListView) and an inner Row with two buttons and text for count representation.
Android Studio Preview allows to run the widget in Interactive mode. It will make taps and other actions leading to change of the state (the number of cups) available right in Preview. Or it can be launched on an emulator for more complicated cases.
State
The code above is already interactive because of the count wrapped with the state.
val count = state { type.count } is taking the count from the data object as an initial value and represents it as a state. Inner widgets can achieve its current value by count.value. When you assign a new value to it — the subtree starting from the widget, containing state function call, will be redrawn.
Unlike Flutter, Compose has no division between Stateful and Stateless widgets. Each widget with state function inside can be considered as Stateful, while others as Stateless.
Now we can create a list of different coffee types. The most simple way is to make a column of them and to add a scroller if list is to long:
Composable functions can be nested inside Control Flow operators like if, for, when, … Column is an analog of ListView with vertical orientation, and VerticalScroller is analog of ScrollView.
The problem in this code is obvious. The list will lag while scrolling. Where is a RecyclerView? Compose has an AdapterList analog. Now scrollable CoffeeList will be implemented as following:
As for now, there is no RecyclerView analog for GridLayoutManager. However, the app already has one of two desirable screens ready.

Before making a second one, let’s think about navigation.
Material design navigation elements are implemented very similar in Flutter and Compose. The root element should be a Scaffold widget, wrapped with a Theme. It can include TopAppBar, BottomAppBar (maybe with FAB integrated) or Drawer (left). To implement BottomNavigationView I have put Column with BottomNavigation widget inside:
The state of the chosen screen tab is contained in selectedItem. With when operator the content that should be displayed is chosen. BottomNavigation widget on click changes the selectedItem value.
It seems that we do not need fragments or other activities to implements screens anymore.
The implementation of the second screen with a table and phased code of the application can be found in the repository. The interesting thing I have found there is a way of retrieving Context for getting resources, or locale, or anything else. For that you should call ContextAmbient.current.context inside of the Composable widget. And a month table screen looks like this:

I have also switched from using png images for coffee types to vector drawables. For this purpose imageResource from Image widget should be replaced by vectorResource. You may also want to use an Icon widget for that (as I have done first), but it makes an icon monochrome.
StateFlow
Let’s move on to the second part of the article’s title. Coroutines introduced the analog of reactive streams — Flow. It can be considered as a cold sequence of data. It starts to emit data when something subscribes on it (calls terminal function). For passing the state between different components of the application the analog of BehaviorSubject from Rx could be helpful. Since 1.3.6 Coroutines’ team introduced it — StateFlow.
As a BehaviorSubject, it can be observable by several subscribers and has an initial state.
Simple example of it’s usage: in sample above the selectedItem state can be replaced by selectedItemFlow:
The state of selectedItem is retrieved by extension function collectAsState() of Flow. It is still used for determining the need to redraw and can be used for value retrieving.
To change the state we pass an index into selectedItemFlow.value.
As value retrieving can be achieved also by val smth = selectedItemFlow.value, it is important not to forget to call collectAsState() inside the widget. It will not be updated otherwise.
A possible pattern here is to use State for reading and MutableStateFlow for writing.
In prototype of the app that allows to move between months tables, see and change the number of cups taken each day, I used 3 StateFlows:
yearMonthFlow is responsible for the current visible month table;
dateFlow is responsible for the chosen day in table and navigation between screens. If it is -1 — TablePage is showing. Otherwise — CoffeeListPage is showing for a particular date;
daysCoffeesFlow is a stub of a repository, containing all recorded coffee numbers. Its structure is a solution to the following problem.
When user navigates from TablePage to CoffeeListPage, its state should be a subset of the common state represented in daysCoffeesFlow. The state of item inside the CoffeeList should also contain a subset of the whole list’s state. When the number of coffee cups inside changes, the item by itself can not know how to change the parent daysCoffeesFlow. We should help it by some mapping of parent’s Flow into successor’s and vice versa.
My temporal solution here was to introduce some boilerplate types shown in DayCoffee.kt file.
MVI
This additional mapper classes brought lots of hard-readable code into view functions. That is why I have decided to try to use MVI architecture. Existing solutions like MVICore seemed coupled with RxJava or other asynchronous frameworks and, thus, too complicated for this case. My solution is based on Android MVI with Kotlin Coroutines & Flow article. Basics of MVI with diagrams can be also found there. Here I will show the code of the base Store class:
Store works on incoming Intent-s and provides a StateFlow<State> to subscribers. It contains helper functions, therefore it’s inheritors should only implement reducer-like handleIntent() function together with a hierarchy of custom Intents and States.
Users of Store’s inheritors can get a state by state property returning StateFlow or push new Intent with newIntent() function.
Lets look at NavigationStore implementing logic for navigation:
Starting from the bottom. Here there are two sealed classes corresponding to possible Intents and States. Intents represent corresponding UI-actions. And states in navigation — corresponding pages of the application.
Parameter initialState of NavigationStore represents the State, which the user will see first when opening the application.
And function handleIntent() contains a business logic for transformation of Intents to States.
The second store as all other code of the application can be found in the repository:
Despite childhood diseases of Jetpack Compose and the need to change your mind for full understanding, it seems to be ready to be used in non-critical applications and looks like a future of Android development. It allows easily adopt modern techniques such as unidirectional dataflow architectures and Coroutines, while easier solving some tasks such as navigation. In my opinion, developers should start to at least try it, in order not to miss the moment when it (together with Coroutines) will appear as a must requirement in vacancies.
Given the popularity of declarative UI-frameworks, such as Compose, Flutter, and SwiftUI, mobile development becomes more similar to Web-development. It can cause unification of used architectures, as well as sharing code between most client platforms.