Introduction
MVVM facilitates a separation of development of the graphical user interface be it via a markup language or GUI code from the development of the business logic or back-end logic (the data model). The view model of MVVM is a value converter, meaning the view model is responsible for exposing (converting) the data objects from the model in such a way that objects are easily managed and presented. In this respect, the view model is more model than a view, and handles most if not all of the view’s display logic. The view model may implement a mediator pattern, organizing access to the back-end logic around the set of use cases supported by the view.
In this tutorial, we will try to define each component of the MVVM pattern in order to create a small android app that respects this pattern.
The image below show different element that we are going to create by using Architecture component and Koin as Dependency Injection library.
The architecture below can be divided into 3 different part
View
It contains the structural definition of what users will have on the screen. You can put static and dynamic content (animations and change states). It must not contain any application logic in our case the view can be an activity or a fragment.
View Model
This component links the model and the view. He is in charge of managing data links and possible conversions. This is where binding comes in. In Android, we don’t worry about it because we can directly use AndroidViewModel class or ViewModel.
Model
It is the business data layer and is not linked to any specific graphical representation. In Android, according to the clean architecture, the model can contain the room database, the repository and all other Business Logic Class.
The picture below can summarize the interaction between different component
How to implement the MVVM pattern
In order to implement the MVVM pattern, it’s very important to begin with the components that no need another component to work.
Since the arrival of the architecture component, it is common to implement Android applications by following the model in the image below, as you can see the arrows are pointing from the view (activity/fragment) to the model.
This means that the View knows the View-Model but the View-Model has no reference to the View, the View Model knows the Model but the Model has no reference to the View-Model, In the nutshell the view will have a reference to the view-model but not the other way around and the view-model will have a reference to the model but not the other way around.
This architecture makes the application maintainable and testable.
To develop quickly and efficiently you have to start with the model because the model will not need the other components to work.
Application scenario and Model implementation
To understand the functioning of the MVVM pattern we will develop a small application that will contain all the components to be used in the previous image. We will create an application that will display the data coming from this link, the application will save the data locally to allow the application to run on offline mode.
User Model
@Entity(tableName = "users")
data class GithubUser(
@PrimaryKey val id: Long,
val login: String,
val avatar_url: String
)
The application will handle data with this structure and for simplicity’s sake I only chose some properties
The GithubUser class has Room annotation since the data in the local database will have the same structure as the data from the API
The room DAO has just two methods, one to add the information to the database and another to retrieve it
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun findAll(): LiveData<List<GithubUser>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun add(users: List<GithubUser>)
}
The room database looks like this
@Database(entities = [GithubUser::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract val userDao: UserDao
}
In second position we will implement the Webservice that has the responsibility to recover the data online, for that we will use retrofit+coroutines.
interface UserApi {
@GET("users")
suspend fun getAllUser(): List<GithubUser>
}
If you want to know how to use Retrofit with coroutine see here
In third position we will implement the repository, this class will be responsible for determining the source of the data, in our case we have two data sources, here the repository will just retrieve the data online to store them in the local database.
class UserRepository(private val userApi: UserApi, private val userDao: UserDao) {
val data = userDao.findAll()
suspend fun refresh() {
withContext(Dispatchers.IO) {
val users = userApi.getAllUser()
userDao.add(users)
}
}
}
As you can see the repository has a constructor with two parameters, the first one class that represent online data and the second represent offline data.
The View-Model
After having defined the model and all its different parts, it is high time to implement the view-model, for that we will use a class that came with Android Jetpack the ViewModel class.
class UserViewModel(private val userRepository: UserRepository) : ViewModel() {
private val _loadingState = MutableLiveData<LoadingState>()
val loadingState: LiveData<LoadingState>
get() = _loadingState
val data = userRepository.data
init {
fetchData()
}
private fun fetchData() {
viewModelScope.launch {
try {
_loadingState.value = LoadingState.LOADING
userRepository.refresh()
_loadingState.value = LoadingState.LOADED
} catch (e: Exception) {
_loadingState.value = LoadingState.error(e.message)
}
}
}
}
The
[ViewModel
](developer.android.com/reference/androidx/li..) class is designed to store and manage UI-related data in a lifecycle conscious way. The[ViewModel
](developer.android.com/reference/androidx/li..) class allows data to survive configuration changes such as screen rotations.
As you can notice, the view-model takes as parameter the repository, this class knows all the data sources of our application, in the init block of the view-model we refresh the data of the database by calling the refresh method of the repository, but the view-model also has a data property that directly retrieves the data locally, this guarantees that the user will have on his interface some data even if the device is not connected.
Note: I use a helper class that allow me to manage the state of the loading
data class LoadingState private constructor(val status: Status, val msg: String? = null) {
companion object {
val LOADED = LoadingState(Status.SUCCESS)
val LOADING = LoadingState(Status.RUNNING)
fun error(msg: String?) = LoadingState(Status.FAILED, msg)
}
enum class Status {
RUNNING,
SUCCESS,
FAILED
}
}
The View
The view is the last component of the architecture, this component will communicate directly with the view-model to retrieve the data and populate a recycler-view for example, in our case the view is just a simple activity.
class MainActivity : AppCompatActivity() {
private val userViewModel by viewModel<UserViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
userViewModel.data.observe(this, Observer {
// Todo: Populate the recyclerView here
it.forEach { githubUser ->
Log.i("MainActivity", githubUser.login)
}
})
userViewModel.loadingState.observe(this, Observer {
when (it.status) {
LoadingState.Status.FAILED -> Toast.makeText(baseContext, it.msg, Toast.LENGTH_SHORT).show()
LoadingState.Status.RUNNING -> Toast.makeText(baseContext, "Loading", Toast.LENGTH_SHORT).show()
LoadingState.Status.SUCCESS -> Toast.makeText(baseContext, "Success", Toast.LENGTH_SHORT).show()
}
})
}
}
The view only observes the change of the data to automatically update the data at the interface level, in our case the view also observes the state of loading operations in the background using the loadingState property previously defined in the view-model.
As you can see, I have retrieved an instance of the view-model by using inject function, we will see how it’s work in the next part
Objects instantiation and dependency injection
If you are vigilant you will notice that until then I have not yet created a repository object let alone the parameters that the repository takes, this is precisely what we will do by using dependency injection, for that we will use a library that I personally appreciate Koin.
By using Koin, we will create the important objects that our application will have to use at the same place and we will only have to call its objects at different places in our application using the magic of Koin.
If you wanted to learn how to configure and use Koin in an android application I advise you to read my post Dependency Injection with Koin
val viewModelModule = module {
viewModel { UserViewModel(get()) }
}
val apiModule = module {
fun provideUserApi(retrofit: Retrofit): UserApi {
return retrofit.create(UserApi::class.java)
}
single { provideUserApi(get()) }
}
val netModule = module {
fun provideCache(application: Application): Cache {
val cacheSize = 10 * 1024 * 1024
return Cache(application.cacheDir, cacheSize.toLong())
}
fun provideHttpClient(cache: Cache): OkHttpClient {
val okHttpClientBuilder = OkHttpClient.Builder()
.cache(cache)
return okHttpClientBuilder.build()
}
fun provideGson(): Gson {
return GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.IDENTITY).create()
}
fun provideRetrofit(factory: Gson, client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create(factory))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.client(client)
.build()
}
single { provideCache(androidApplication()) }
single { provideHttpClient(get()) }
single { provideGson() }
single { provideRetrofit(get(), get()) }
}
val databaseModule = module {
fun provideDatabase(application: Application): AppDatabase {
return Room.databaseBuilder(application, AppDatabase::class.java, "eds.database")
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
}
fun provideDao(database: AppDatabase): UserDao {
return database.userDao
}
single { provideDatabase(androidApplication()) }
single { provideDao(get()) }
}
val repositoryModule = module {
fun provideUserRepository(api: UserApi, dao: UserDao): UserRepository {
return UserRepository(api, dao)
}
single { provideUserRepository(get(), get()) }
}
The Module.kt contain the declaration of object that the application is going to use, in the view, we are using inject that tell to Koin that we need a view-model object, Koin will try to found this object in the module previously defined and will assign the property userViewModel with this instance, if there isn’t the corresponding object in the module Koin will throw an exception.
In our case, the code will compile properly because we have created an instance of the view-model in the module with the appropriate parameter
The same scenario will be applied to the repository inside the view-model, this instance will be retrieved from the Koin module because we already created an instance of the repository with the appropriate parameter inside the Koin module.
You want to learn more about dependency injection with koin see Dependency Injection with Koin
Conclusion
The biggest job of a software engineer is not the development but the maintenance, the more the code is based on a good architecture the easier the maintenance and the testability of the application will be, that’s why it’s often important to use patterns to avoid creating a bomb instead of software.
You can find the complete code of the application on my GitHub with this link
If you have any kind of feedback, feel free to connect with me on Twitter.
Reference
Le pattern Modèle - Vue - Vue Modèle (MVVM) Je travaille actuellement sur le développement d'une application Windows 8 (C# et XAML) en utilisant une architecture…nordinemhoumadi.wordpress.com eric-ampire/android-clean-architecture You can't perform that action at this time. You signed in with another tab or window. You signed out in another tab or…github.com