Unsplash Image by Akin Cakiner
1. Introduction
As you can read in the official documentation of RainbowCake, RainbowCake is an Android architecture framework, providing tools and guidance for building modern Android applications. It builds on top of Jetpack, both in terms of code and ideas.
Some of the main goals of this architecture:
Give guidance on all aspects of the application, covering not just the View architecture,
Clearly separate concerns between different layers and components,
Always keep views in a safe and consistent state with ViewModels,
Handle configuration changes (and even process death) gracefully,
Make offloading work to background threads trivial.
2. MVVM, MVI and RainbowCake
Recently I have published a post about MVI architecture, and this post I have given some benefit of MVI architecture such as solving the State Problem that often occure in the MVVM architecture.
RainbowCake is between the MVVM and MVI architecture because it takes some advantage of both the MVI and MVVM architecture
3. RainbowCake Architecture overview
The RainbowCake architecture looks like this, but this architecture concept and this documentation isn’t gospel, If your specific application’s needs require you to deviate from this, Read more here.
Figure 1, RainbowCake Architecture
Views (Fragments or Activities) represent application screens. They observe immutable state from their respective ViewModels and display it on the UI. They also forward input events to the ViewModel, and may receive state updates or one-time events in return.
ViewModels store the current state of the UI, handle UI related logic, and update the state based on results received from presenters. They start coroutines for every task they have to perform (triggered by input events), and forward calls to their presenters.
Presenters put work on background threads and use interactors (one or more) to access business logic. Then, they transform the results to screen-specific presentation models for the ViewModels to store as state.
Interactors contain the core business logic of the application. They aggregate and manipulate data and perform computations. They are not tied to a single screen, but instead group functionality by the major features of the application.
Data sources provide the interactors with data from various origins — local database and file system, network locations, key-value stores, system APIs, resources, etc. It’s their responsibility to abstract away the underlying implementation from the domain layer, and to keep their stored data in a consistent state (i.e. not expose operations that can lead to inconsistency).
4. RainbowCake in Practice
In this post, will try to make a simple app step by step based on RainbowCake in order to build an Android application based on RainbowCake Architecture you have to add some dependencies in your project.
Step 1: Adding RainbowCake dependencies
RainbowCake is downloadable from MavenCentral
repositories {
mavenCentral()
}
In the build.gradle file of the module, we need to add the core library of RainbowCake, and as in our small application we will use dependency injection, we also need to add another RainbowCake dependency to support dependency injection, in our case we will use Koin, but RainbowCake also supports Dagger. Read more here
dependencies {
// RainbowCake
def rainbow_cake_version = '1.0.0'
implementation "co.zsmb:rainbow-cake-core:$rainbow_cake_version"
implementation "co.zsmb:rainbow-cake-koin:$rainbow_cake_version"
implementation "co.zsmb:rainbow-cake-navigation:$rainbow_cake_version"
implementation "co.zsmb:rainbow-cake-timber:$rainbow_cake_version"
// Coroutines
def coroutines_version = '1.3.7'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
// Koin
def koin_version = '2.1.6'
implementation "org.koin:koin-core:$koin_version"
implementation "org.koin:koin-android:$koin_version"
implementation "org.koin:koin-android-viewmodel:$koin_version"
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
Step 2 : Creating architecture components
As we have added all the RainbowCake dependencies, it is time to create the different elements of our architecture as shown in figure 1, First of all we have to create a RainbowCake Screen, A RainbowCake screen is composition of a fragment, a class that represents the state of the fragment, a ViewModel and the Presenter as shown in the Figure 2.
Each time we need to have a view, we have to create 4 classes, so this operation can become repetitive and a bit boring, to fix it we just have to add to Android Studio the possibility to create for us those 4 classes using the RainbowCake template as shown in the following image (figure 3).
Figure 3
Read the following guide in order to add RainbowCake template.
Step 3 : Components description
The first component that we are going to describe is the state, in our case, the state of the our unique View is the sealed class (UserViewState), and as you can see there are 4 states, Initial, Loading, Error and UserReady.
sealed class UserViewState
object Initial : UserViewState()
object Loading : UserViewState()
class Error(val errorMessage: String?) : UserViewState()
data class UserReady(val data: User) : UserViewState()
The second component is the presenter, all presenter operations are performed in the background as you can see in Figure 1, so we used the “withIOContext” method provided by RainbowCake. The UserPresenter has a UserInteractor as a unique property that we used to obtain the user data.
class UserPresenter(private val userInteractor: UserInteractor) {
suspend fun getData() = withIOContext {
userInteractor.getUserInfo()
}
}
The next component is the UserViewModel, that handle UI related logic, and update the state based on results received from presenters. They start coroutines for every task they have to perform (triggered by input events), and forward calls to their presenters.
class UserViewModel(
private val userPresenter: UserPresenter
) : RainbowCakeViewModel<UserViewState>(Initial) {
fun load() = execute {
try {
viewState = Loading
viewState = UserReady(userPresenter.getData())
} catch (e: Exception) {
viewState = Error(e.message)
}
}
}
As you can see, instead of using the ViewModel class we used the RainbowCakeViewModel class, which takes as parameter the initial state which must be one of the subclasses of the UserViewState class we used as type parameter of the RainbowCakeViewModel.
The execute
method from RainbowCakeViewModel
is used to launch a coroutine on the UI thread with the appropriate CoroutineScope
in ViewModels.
The viewState property provided by the RainbowCakeViewModel class allows to modify the state
The last component is the Fragment, the UserFragment inherited from the RainbowCakeFragment class provided by the RainbowCake framework, and to use the RainbowCakeFragment class you have to redefine 3 methods
The getViewResource method: Allows to specify the layout attached to the fragment.
The
provideViewModel
method: Allows to retrieve the ViewModel of the same type as the one specified as type parameter of the RainbowCakeFragment class. The ViewModel is returned by thegetViewModelFromFactory()
methodThe render method: This method is called each time the state is modified in the ViewModel, the only parameter of this method has the same type as the one specified as type parameter of the RainbowCakeFragment class.
class UserFragment : RainbowCakeFragment<UserViewState, UserViewModel>() {
override fun provideViewModel() = getViewModelFromFactory()
override fun getViewResource() = R.layout.fragment_user
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
override fun onStart() {
super.onStart()
viewModel.load()
}
override fun render(viewState: UserViewState) {
when (viewState) {
is Loading -> {
viewFlipper.displayedChild = 0
}
is UserReady -> {
viewFlipper.displayedChild = 1
tvUserLogin.text = viewState.data.login
ivUserProfile.load(viewState.data.avatar_url) {
crossfade(true)
placeholder(android.R.color.darker_gray)
transformations(CircleCropTransformation())
}
}
is Error -> {
Toast.makeText(requireContext(), viewState.errorMessage, Toast.LENGTH_SHORT).show()
}
}
}
}
In layout of the UserFragment I used the ViewFlipper as ViewGroup which contains two children, a ProgressBar and a LinearLayout, The ViewFlipper has a particular behavior in the measure it displays only one of its children at a time according to the index provided as in the render method of the UserFragment.
<?xml version="1.0" encoding="utf-8"?>
<ViewFlipper xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/viewFlipper"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center" />
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:layout_margin="10dp"
android:id="@+id/ivUserProfile"
android:layout_width="150dp"
android:layout_height="150dp" />
<TextView
android:id="@+id/tvUserLogin"
android:textColor="@android:color/black"
android:textStyle="bold"
android:textSize="18sp"
tools:text="Eric Ampire"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</ViewFlipper>
The final application looks like this.
The Result
For the getViewModelFromFactory() method to be able to return the ViewModel you must be sure that you have declared it in the corresponding Koin module.
val mainModule = module {
factory { UserPresenter(get()) }
factory { UserViewModel(get()) }
factory { UserInteractor(get()) }
single {
Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
single {
get<Retrofit>().create(UserApi::class.java)
}
}
5. Conclusion
RainbowCake is production ready! It has been in production for nearly two years now, in several applications, in the hands of (at least) tens of thousands of users. So don’t hesitate to simplify yourself by using it in your projects.
The complete code of the application is available here
6. References
rainbowcake/rainbowcake-templates This repository contains templates to generate boilerplate code for an Android application. Note: these templates are…github.com