Kotlin Flow with Clean Architecture and MVVM Pattern in Android

The main goal when developing an Android application is to write a code easy to test and painless to maintain. We must use an architecture which makes it effortless. To do so, we should follow the Separation of Concerns. It helps us to have an efficient application design by separating logic, data and UI.

Kotlin Flow is used to stream data from API-services to UI-based classes. It can apply any logic before deserving data to the UI components. This library suits to the Clean Architecture’s design.

Clean Architecture 🧞

This architecture comes from the well-known Robert C. Martin, called “Uncle Bob”. In his book, he provided the guidance to write successful architecture by separating our application code in layers.

The objective is to separate the responsibility, making it testable and avoid any strong dependencies to UI, frameworks, databases. We could change a dependency smoothly without affecting the whole structure.

Each circle (or layer) should not be aware of the upper one. The most inner circle takes care of the business logic and API connections. The next one named usecases contains the application logic. Followed by the upper circle which controls the data to pass to the views. And finally, the outer circle holds the UI components.

To learn more, Android developers team released an excellent guide for their eco-system.

MVVM pattern 🧞‍♀️

This architecture’s design can be combined with any pattern out there: MVC, MVP, MVI, MVVM, etc. Each of these MV* patterns results of the wish to separate the responsibilities by specific areas in our applications and to become independent.

In Android, we can implement the MVVM (Model-View-ViewModel) pattern and use the ViewModel class to manage the UI data with lifecycle awareness.

  • The Model is any classes relating to data access layer, either domain data models or layer services.
  • The ViewModel is the entry point of the data access layer. It is used to interact with the Model and bound to the View.
  • The View managers (Activities, Fragments) should not contain any other logic than view logic (showing/hiding elements, component click listeners, etc). They observe the data from the ViewModel and change the components accordingly.

Kotlin Flow integration 🧞‍♂️

A flow should be created in the inner layer, near to the API services, passed into each circle to transform its data and it could be collected (started) from the controllers.

EEServices: This is where the flows are created. These classes send and receive data. They can map the response into a DTO object.
Repositories: They apply the business logic to the Services flows.

UUUseCases: They are the reflection of the user interactions. Each usecase is an action. They use the Repositories flows and defined the application logic.

VVViewModels: These controllers are the entry point of the Android Framework. They collect the data by launching and starting the flows. The VMs are responsible of the flows lifecycle.

AAActivities/Fragments: Managers of the view components, they observe the changes from the ViewModels and update the UI components. They don’t use the flows directly.

A case study 👩‍🔬

Assuming our application should get user data from an id, an external API could return a JSON object containing a name, a date in milliseconds named birth and a city:

{ "name":"John", "birth":1023073980000, "city":"London" }

In this app, we want to display the user’s name, its current city, its birthday and its age. However, we need to calculate its age based on milliseconds and we would like to render its birthday in a human readable way.

Of course, we should simply doing it with suspended functions, but let see how to realize this case study using Kotlin Flow with Clean Architecture and MVVM Pattern in Android.

Service 🤾‍♀️

The Service is responsible of the flow’s creation.

class UserService(val apiService: ApiService, val gson: Gson) { fun fetchUserData(id: String): Flow<UserDto> = flow { val result = apiService.getUserData(id); val user = gson.fromJson(result, UserDto::class.java); emit(user); }}

We create a Flow using the flow builder which allows us to call a suspended function getUserData of our API service to get the data in result variable.

With the Gson library, we parse the data to a DTO object named UserDto where all its fields are optional.

Then, we send this DTO object thanks to emit inside our flow.

Repository 🤼‍♀️

The Repository handles the business logic.

class UserRepository(val userService: UserService) { fun fetchUserData(id: String): Flow<User> = userService.fetchUserData(id).map { val now = LocalDate.now(ZoneId.systemDefault()); val date = Instant.ofEpochMilli(dateMs).atZone(ZoneId.systemDefault()).toLocalDate(); val age = Period.between(date, now).years; return@map User(name = it.name.orEmpty(), age = age, birth = it.birth ?: 0L, city = it.city.orEmpty()) }}

It calls the fetchUserData function from our UserService and converts the received flow of DTO object to a flow of domain object.

In the map operator, we calculate the user’s age from the birth date and we create the domain class User. Because its fields are non-nullable, we check if the ones from UserDto are null or empty and apply a default value if they are.

data class User(val name: String, val age: Int, val birth: Long, val city: String, var birthday: String = “”)

In the domain class, we also have a default birthday value which will be established in the upper layer when dealing with app logic.

Use Case ⛹️‍♀️

In our usecase, we define the application logic.

class FetchUserDataUseCase(val userRepository: UserRepository, val dispatcher: CoroutineDispatcher = Dispatcher.IO) { operator fun invoke(id: String): Flow<User> = userRepository.fetchUserData(id).map { val date = Instant.ofEpochMillis(it.birth).atZone(ZoneId.systemDefault()).toLocalDate(); val birthday = date.format(DateTimeFormatter.ofPattern(“d MMM yyyy”));return@map it.apply { this.birthday = birthday }}.flowOn(context = dispatcher) }

For contract-less usecases, we can use invoke operator to directly perform the action when instantiate. Here, we determine the birthday value and add it to the domain object received from our Repository. It will be displayed like: 3 Jun 2002.

We also change the context of the flow by using flowOn. Note that we inject a default CoroutineDispatcher used by this operator. This allows us to control the context of this flow when writing unit tests.

ViewModel 🚴‍♂️

Then, the ViewModel launches the flow’s coroutine and starts the collecting.

class UserViewModel(val fetchUserDataUseCase: FetchUserDataUseCase) { private val _userData = MutableLiveData<User>(); val userData: LiveData<User> = _userData; fun fetchUserData(id: String) { fetchUserDataUseCase(id).onEach { _userData.value = it }.catch { println(it) }.launchIn(viewModelScope) }}

When calling the UseCase, it updates a LiveData to propagate the new collected value to the observers in onEach. Then, for this example, we add catch operator to print the error if anything happened in the upstream flows. After that, we use launchIn (contraction of launch and collect) to start the flow’s collection in the ViewModel’s scope.

Activity 🤸‍♀️

Finally, our Activity, as UI managers, observes the LiveData and updates the view components.

class UserDataActivity : AppCompatActivity() { private val userViewModel: UserViewModel by viewModels(); …; override fun onCreate(inState: Bundle?) { super.onCreate(inState); …; initObservers(); fetchUserData() }; private fun initObservers() { userViewModel.userData.observe(this) { user -> userNameTextView.text = user.name; userAgeTextView.text = user.age; userCityTextView.text = user.city; userBirthdayTextView.text = user.birthday; }}; private fun fetchUserData() { userViewModel.fetchUserData(u

We observe the VM's LiveData by getting the userData property. Then, we update each dedicated TextViews when receiving the new values. Lastly, we launch the flow when the Activity is creating, after the observer set, with the ViewModel fetchUserData function.

Kotlin Flow - Clean Architecture - MVVM Pattern 🪂

With the separation of responsibilities, it is easy to implement a specific layer’s logic to a flow. Kotlin Flow can be included in this architecture and be modified, transformed and manipulated by each layers. It makes the maintaining and the tests effortless.

If you found this post helpful, feel free to clap! 👏 Thanks for reading.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Florent Blot

Florent Blot

Maker, Mobile developer, Kotlin & Dart enthusiast, IoT & DIY addict. @Geev: https://www.geev.com