Clean Architecture and MVVM Architecture are two popular architectural patterns for building robust, maintainable, and scalable Android applications. In this article, we will discuss how to implement Clean Architecture and MVVM Architecture in an Android application using Kotlin. We will cover all aspects of both architectures in-depth and explain how they work together to create a robust application.
Clean Architecture
Clean Architecture is a software design pattern that emphasizes separation of concerns and the use of dependency injection. It divides an application into layers, with each layer having a specific responsibility. The layers include:
- Presentation Layer
- Domain Layer
- Data Layer
The Presentation Layer is responsible for the user interface and interacts with the user. The Domain Layer contains business logic and rules. The Data Layer interacts with external sources of data.
The Clean Architecture pattern is designed to promote testability, maintainability, and scalability. It reduces coupling between different parts of an application, making it easier to modify or update them without affecting other parts of the application.
MVVM Architecture
MVVM stands for Model-View-ViewModel. It is a software design pattern that separates an application into three layers: Model, View, and ViewModel. The Model represents the data and business logic. The View represents the user interface. The ViewModel acts as a mediator between the Model and the View. It exposes data from the Model to the View and handles user input from the View.
MVVM Architecture promotes separation of concerns, testability, and maintainability. It is designed to work with data binding and makes it easy to update the user interface when data changes.
Combining Clean and MVVM Architecture
Clean Architecture and MVVM Architecture can be used together to create a robust, maintainable, and scalable Android application. The Presentation Layer in Clean Architecture corresponds to the View and ViewModel in MVVM Architecture. The Domain Layer in Clean Architecture corresponds to the Model in MVVM Architecture. The Data Layer in Clean Architecture corresponds to the Data Layer in MVVM Architecture.
Implement Clean and MVVM Architecture
Let’s build one demo app to implement Clean and MVVM Architecture. we will create a simple app that displays a list of movies and allows the user to view the details of each movie. We will use the Movie Database API as our data source.
Building this MVVM demo app using Clean Architecture, MVVM, Kotlin, Coroutines, Room, Hilt, Retrofit, Moshi, Flow, and Jetpack Compose.
Set up the project
Create a new project in Android Studio and add the necessary dependencies for MVVM Architecture, such as room, hilt, and ViewModel.
build.gradle (Module:app)
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
namespace 'com.softaai.mvvmdemo'
compileSdk 33
defaultConfig {
applicationId "com.softaai.mvvmdemo"
minSdk 24
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.4.2'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.core:core-ktx:1.10.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.compose.material3:material3:1.1.0-beta02'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// activity
implementation 'androidx.activity:activity-ktx:1.7.0'
implementation 'androidx.activity:activity-compose:1.7.0'
// Lifecycle
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
implementation "androidx.lifecycle:lifecycle-common:2.6.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
//compose
implementation 'androidx.compose.ui:ui:1.5.0-alpha02'
implementation 'androidx.compose.material:material:1.5.0-alpha02'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
debugImplementation "androidx.compose.ui:ui-tooling:1.5.0-alpha02"
implementation "androidx.compose.ui:ui-tooling-preview:1.5.0-alpha02"
implementation "androidx.compose.runtime:runtime-livedata:1.5.0-alpha02"
//compose navigation
implementation "androidx.navigation:navigation-compose:2.5.3"
implementation "androidx.hilt:hilt-navigation-compose:1.0.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1"
// Dagger hilt
implementation 'com.google.dagger:hilt-android:2.45'
kapt 'com.google.dagger:hilt-compiler:2.45'
// Networking
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.2'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.2'
// Moshi
implementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.14.0'
// Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"
// Room and room pagination
implementation "androidx.room:room-runtime:2.5.1"
kapt "androidx.room:room-compiler:2.5.1"
implementation "androidx.room:room-ktx:2.5.1"
implementation "androidx.room:room-paging:2.5.1"
// coil image loading
implementation 'io.coil-kt:coil-compose:2.3.0'
// multidex
implementation 'androidx.multidex:multidex:2.0.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.5.0-alpha02'
debugImplementation 'androidx.compose.ui:ui-tooling:1.5.0-alpha02'
debugImplementation 'androidx.compose.ui:ui-test-manifest:1.5.0-alpha02'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10"
//if update warning comes then go to settings and install latest plugin and restart
}
Define Initial Packages
Create initial packages for each layer of Clean Architecture: Presentation, Domain, and Data. Inside each package, create sub-packages for specific functionalities of the layer.
├── data │ ├── repository │ └── source │ ├── local │ │ ├── datastore │ │ └── roomdb │ └── remote ├── di │ ├── movies │ └── moviedetails ├── domain │ ├── model │ ├── repository │ └── usecase └── presentation ├── ui └── viewmodel
In this hierarchy, we have:
data
package which contains therepository
andsource
packages.- The
repository
package contains classes responsible for fetching data fromsource
and returning it todomain
. - The
source
package containslocal
andremote
packages. - The
local
package contains classes responsible for accessing data from local data storage, such asdatastore
androomdb
. - The
remote
package contains classes responsible for accessing data from remote data storage, such as APIs. di
package which contains themovies
andmoviedetails
packages.- These packages contain classes responsible for dependency injection related to
movies
andmoviedetails
modules. domain
package which contains themodel
,repository
, andusecase
packages.- The
model
package contains classes representing the data model of the application. - The
repository
package contains interfaces defining the methods that therepository
classes indata
package must implement. - The
usecase
package contains classes responsible for defining the use cases of the application, by usingrepository
interfaces and returning the result to thepresentation
layer. presentation
package which contains theui
andviewmodel
packages.- The
ui
package contains classes responsible for the user interface of the application, such as activities, fragments, and views. - The
viewmodel
package contains classes responsible for implementing the ViewModel layer of the application, which holds data related to the UI and communicates with theusecase
layer.
Identify JSON Response
Identify the JSON response from the URL, Before making a network request to the URL, please use your own API Key as mine is an invalid key. Then examine the JSON response using an Online JSON Viewer to identify its structure. Once you have identified the structure of the response, create Kotlin DTOs for the response and place them in the remote package.
DTOs, Entities, and Domain Models:
In our Android application, we will have different types of data models. These models include DTOs, Entities, and Domain Models.
DTOs (Data Transfer Objects) are used to transfer data between different parts of the application. They are typically used to communicate with a remote server or API.
Entities represent the data models in our local database. They are used to persist data in our application.
Domain Models represent the business logic in our application. They contain the logic and rules that govern how data is processed in the application.
By using these different types of models, we can separate our concerns and ensure that each model is responsible for its own functionality. This makes our code more modular and easier to maintain.
Mapper Functions:
In our Android application, we will often need to convert between different types of models. For example, we might need to convert a DTO to an Entity or an Entity to a Domain Model. To do this, we can use Mapper Functions.
Mapper Functions are used to convert data between different models. They take an input model and convert it to an output model. By using Mapper Functions, we can ensure that our code is organized and maintainable, and we can easily convert between different models as needed.
Define DTOs
We can create Kotlin data transfer objects (DTOs) to represent the data and place them into the remote
package, as it represents data fetched from a remote data source
package com.softaai.mvvmdemo.data.source.remote.dto
import com.softaai.mvvmdemo.data.source.local.roomdb.entity.PopularMoviesEntity
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class PopularMoviesDto(
@Json(name = "page")
val page: Int,
@Json(name = "results")
val results: List<MovieDto>,
@Json(name = "total_pages")
val totalPages: Int,
@Json(name = "total_results")
val totalResults: Int
) {
fun toPopularMoviesEntity(): PopularMoviesEntity {
return PopularMoviesEntity(
page = page,
results = results.map { it.toMovieEntity() }
)
}
}
package com.softaai.mvvmdemo.data.source.remote.dto
import com.softaai.mvvmdemo.data.source.local.roomdb.entity.MovieEntity
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class MovieDto(
@Json(name = "adult")
val adult: Boolean,
@Json(name = "backdrop_path")
val backdropPath: String,
@Json(name = "genre_ids")
val genreIds: List<Int>,
@Json(name = "id")
val id: Int,
@Json(name = "original_language")
val originalLanguage: String,
@Json(name = "original_title")
val originalTitle: String,
@Json(name = "overview")
val overview: String,
@Json(name = "popularity")
val popularity: Double,
@Json(name = "poster_path")
val posterPath: String,
@Json(name = "release_date")
val releaseDate: String,
@Json(name = "title")
val title: String,
@Json(name = "video")
val video: Boolean,
@Json(name = "vote_average")
val voteAverage: Double,
@Json(name = "vote_count")
val voteCount: Int
) {
fun toMovieEntity(): MovieEntity {
return MovieEntity(
id = id,
title = title,
overview = overview,
posterUrl = posterPath,
releaseDate = releaseDate
)
}
}
Define the data source
To fetch movies from the Movie Database API, we will use Retrofit to define an interface that defines the API endpoints. We will also use Moshi to deserialize the JSON responses into our Movie
data class. Here\’s an example of how to define the API interface:
package com.softaai.mvvmdemo.data.source.remote
import com.softaai.mvvmdemo.data.source.remote.dto.PopularMoviesDto
import retrofit2.http.GET
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
interface MovieApiService {
@GET("movie/popular")
suspend fun getPopularMovies(): PopularMoviesDto
companion object {
const val BASE_URL: String = "https://api.themoviedb.org/3/"
}
}
Here, we are using the @GET
annotation to define the API endpoint, and the suspend
keyword to indicate that this function should be called from a coroutine. We are also using the deserialized PopularMovieDto
data class.
Note → We used
PopularMoviesDto
data class directly instead of wrapping it in aResponse
orResource
class. This is because it is assumed that the API response will always contain the expected data structure and any errors in the API call will be handled by catching exceptions, another reason is we are not tightly coupling our app to the API response structure and can modify the response format without affecting the rest of the app.
Define Resource Sealed Class
Resource Sealed Classes are used to represent the state of a request or operation that can either succeed or fail. They allow us to handle different states of an operation, such as loading, success, or error, in a more organized way. Typically, a Resource Sealed Class contains three states:
- Loading: When the operation is in progress.
- Success: When the operation is successful and data is available.
- Error: When the operation fails.
package com.softaai.mvvmdemo.data.source.remote
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
class Loading<T>(data: T? = null) : Resource<T>(data)
class Success<T>(data: T?) : Resource<T>(data)
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
}
By using Resource Sealed Classes, we can easily handle different states of an operation in our ViewModel without writing lots of boilerplate code.
Implement Interceptor for network requests
The purpose of the interceptor is to add an API key query parameter to every outgoing network request.
package com.softaai.mvvmdemo.data.source.remote
import okhttp3.Interceptor
import okhttp3.Response
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
class RequestInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val newUrl = originalRequest.url
.newBuilder()
.addQueryParameter(
"api_key",
"04a03ff73803441c785b1ae76dbdab9c" //TODO Use your api key this one invalid
)
.build()
val request = originalRequest.newBuilder()
.url(newUrl)
.build()
return chain.proceed(request)
}
}
The RequestInterceptor
class implements the Interceptor
interface provided by the OkHttp library, which allows it to intercept and modify HTTP requests and responses.
In the intercept
method, the incoming chain
parameter represents the chain of interceptors and the final network call to be executed. The method first retrieves the original request from the chain
using chain.request()
. It then creates a new URL builder from the original request URL and adds a query parameter with the key \”api_key\” and a specific value to it. Here use your own api_key as existing key is invalid
Next, it creates a new request by calling originalRequest.newBuilder()
and setting the new URL with the added query parameter using .url(newUrl)
. Finally, it calls chain.proceed(request)
to execute the modified request and return the response.
Overall, this interceptor helps to ensure that every network request made by the app includes a valid API key, which is required for authentication and authorization purposes.
Implementation of Room
Room is a powerful ORM (Object-Relational Mapping) library that makes it easy to work with a SQLite database in Android. It provides a high-level API for working with database tables, queries, and transactions, as well as support for Flow, LiveData, and RxJava for reactive programming.
Define the Entity
First, we’ll define the MovieEntity
class, which represents the movies
table in our local database. We annotate the class with @Entity
and specify the table name and primary key. We also define the columns using public properties.
package com.softaai.mvvmdemo.data.source.local.roomdb.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.softaai.mvvmdemo.domain.model.Movie
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
@Entity(tableName = MovieEntity.TABLE_NAME)
data class MovieEntity(
@PrimaryKey val id: Int,
val title: String,
val overview: String,
@ColumnInfo(name = "poster_url") val posterUrl: String,
@ColumnInfo(name = "release_date") val releaseDate: String
) {
fun toMovie(): Movie {
return Movie(
title = title,
overview = overview,
posterUrl = posterUrl,
releaseDate = releaseDate
)
}
companion object {
const val TABLE_NAME = "movie"
}
}
We have another entity for API response, which contains a movie entity list
package com.softaai.mvvmdemo.data.source.local.roomdb.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.softaai.mvvmdemo.domain.model.PopularMovies
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
@Entity(tableName = PopularMoviesEntity.TABLE_NAME)
class PopularMoviesEntity(
@PrimaryKey(autoGenerate = true)
val primaryKeyId: Int? = null,
val page: Int,
val results: List<MovieEntity>
) {
fun toPopularMovies(): PopularMovies {
return PopularMovies(
page = page,
results = results.map { it.toMovie() }
)
}
companion object {
const val TABLE_NAME = "popular_movies"
}
}
Define the DAO
Next, we’ll define the MovieDao
interface, which provides the methods to interact with the movies
table. We annotate the interface with @Dao
and define the query methods using annotations such as @Query
, @Insert
, and @Delete
.
package com.softaai.mvvmdemo.data.source.local.roomdb.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.softaai.mvvmdemo.data.source.local.roomdb.entity.MovieEntity
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
@Dao
interface MovieDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMovieList(movies: List<MovieEntity>)
@Query("SELECT * FROM ${MovieEntity.TABLE_NAME}")
suspend fun getMovieList(): List<MovieEntity>
@Query("DELETE FROM ${MovieEntity.TABLE_NAME}")
suspend fun deleteAll()
}
Note ->Here also, we are not using any wrappers like Flow, Response, or Resource. The reason behind this is that we are keeping the repository layer decoupled from the data sources (local or remote) and allowing for easier testing and evolution. In this specific case, it is a simple synchronous database operation, as the data is being retrieved from the local database using Room. Room already provides the functionality to perform asynchronous database operations in the background, so we do not need to use any additional wrappers like Flow or Resource. We can simply call the getMovieList() method from a coroutine and retrieve the list of MovieEntity objects.
Define Type Converter
In Room, a type converter is a way to convert non-primitive types (such as Date or custom objects in our case List<MovieEntity>) to primitive types that can be stored in the SQLite database.
To use a type converter in Room, you need to create a class that implements the TypeConverter
interface, which has two methods: toType()
and fromType()
. The toType()
method converts a non-primitive type to a primitive type, while the fromType()
method converts the primitive type back to the non-primitive type.
package com.softaai.mvvmdemo.data.source.local.roomdb.converter
import androidx.room.TypeConverter
import com.softaai.mvvmdemo.data.source.local.roomdb.entity.MovieEntity
import com.softaai.mvvmdemo.domain.model.Movie
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
class PopularMoviesEntityConverter {
@TypeConverter
fun fromStringToMovieList(value: String): List<Movie>? =
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
.adapter<List<Movie>>(Types.newParameterizedType(List::class.java, Movie::class.java))
.fromJson(value)
@TypeConverter
fun fromMovieListTypeToString(movieListType: List<Movie>?): String =
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
.adapter<List<Movie>>(Types.newParameterizedType(List::class.java, Movie::class.java))
.toJson(movieListType)
@TypeConverter
fun fromStringToMovieEntityList(value: String): List<MovieEntity>? =
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build().adapter<List<MovieEntity>>(
Types.newParameterizedType(
List::class.java,
MovieEntity::class.java
)
).fromJson(value)
@TypeConverter
fun fromMovieEntityListTypeToString(movieEntityListType: List<MovieEntity>?): String =
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build().adapter<List<MovieEntity>>(
Types.newParameterizedType(
List::class.java,
MovieEntity::class.java
)
).toJson(movieEntityListType)
}
To use this TypeConverter
, you need to annotate the field or property that needs to be converted with the @TypeConverters
annotation, specifying the converter class.
Define the Database
Finally, we’ll define the MovieDatabase
class, which represents the entire local database. We annotate the class with @Database
and specify the list of entities and version number. We also define a singleton instance of the database using the Room.databaseBuilder
method.
package com.softaai.mvvmdemo.data.source.local.roomdb
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.softaai.mvvmdemo.data.source.local.roomdb.converter.PopularMoviesEntityConverter
import com.softaai.mvvmdemo.data.source.local.roomdb.dao.MovieDao
import com.softaai.mvvmdemo.data.source.local.roomdb.dao.PopularMoviesDao
import com.softaai.mvvmdemo.data.source.local.roomdb.entity.MovieEntity
import com.softaai.mvvmdemo.data.source.local.roomdb.entity.PopularMoviesEntity
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
@Database(
entities = [PopularMoviesEntity::class, MovieEntity::class],
version = 1,
exportSchema = false
)
@TypeConverters(PopularMoviesEntityConverter::class)
abstract class MovieDatabase : RoomDatabase() {
abstract fun getMovieDao(): MovieDao
abstract fun getPopularMoviesDao(): PopularMoviesDao
companion object {
@Volatile
private var INSTANCE: MovieDatabase? = null
fun getDatabase(context: Context): MovieDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
MovieDatabase::class.java,
"movie_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
Define Repository interface in Domain Layer
Usually In the Domain Layer, we define the interfaces for the Repository and Use Case (In our case, skipped for the Use Case). These interfaces define the methods that will be used to interact with the data layer. The Repository interface defines the methods that will be used to retrieve and save data, while the Use Case interface defines the business logic that will be performed on the data.
We will create a MovieRepository
interface that defines the methods for fetching movies:
package com.softaai.mvvmdemo.domain.repository
import com.softaai.mvvmdemo.data.source.remote.Resource
import com.softaai.mvvmdemo.domain.model.Movie
import kotlinx.coroutines.flow.Flow
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
interface MovieRepository {
fun getPopularMovies(): Flow<Resource<List<Movie>>>
}
We are returning a Flow<Resource<List<Movie>>>
from the getPopularMovies()
function. The Flow
will emit the result of the API call asynchronously, and the Resource
class will hold either the list of movies or an error.
Implement a Repository interface in the Data Layer
We define interfaces for the Repository and Use Case in the Domain Layer and these interfaces will be implemented in the Data Layer. By separating the interfaces from their implementations, we can easily swap out the data layer implementation if needed. This allows us to easily switch between different data sources, such as a local database or a remote API, without having to modify the business logic layer.
In our example, we will create an implementation of the MovieRepository
interface that uses Retrofit and Moshi to fetch the popular movies:
package com.softaai.mvvmdemo.data.repository
import com.softaai.mvvmdemo.data.source.local.roomdb.dao.MovieDao
import com.softaai.mvvmdemo.data.source.local.roomdb.dao.PopularMoviesDao
import com.softaai.mvvmdemo.data.source.remote.MovieApiService
import com.softaai.mvvmdemo.data.source.remote.Resource
import com.softaai.mvvmdemo.domain.model.Movie
import com.softaai.mvvmdemo.domain.repository.MovieRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.HttpException
import java.io.IOException
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
class MovieRepositoryImpl constructor(
private val movieApiService: MovieApiService,
private val popularMoviesDao: PopularMoviesDao,
private val movieDao: MovieDao
) : MovieRepository {
override fun getPopularMovies(): Flow<Resource<List<Movie>>> = flow {
emit(Resource.Loading())
try {
fetchAndInsertPopularMovies(movieApiService, popularMoviesDao, movieDao)
} catch (e: HttpException) {
emit(
Resource.Error(
message = "Oops, something went wrong!"
)
)
} catch (e: IOException) {
emit(
Resource.Error(
message = "Couldn't reach server, check your internet connection."
)
)
}
// single source of truth we will emit data from db only and not directly from remote
emit(Resource.Success(getPopularMoviesFromDb(movieDao)))
}
private suspend fun fetchAndInsertPopularMovies(
movieApiService: MovieApiService,
popularMoviesDao: PopularMoviesDao,
movieDao: MovieDao
) {
val remotePopularMovies = movieApiService.getPopularMovies()
popularMoviesDao.insertPopularMovies(remotePopularMovies.toPopularMoviesEntity())
movieDao.insertMovieList(remotePopularMovies.results.map { it.toMovieEntity() }) //now insert newly fetched data to db
}
private suspend fun getPopularMoviesFromDb(movieDao: MovieDao): List<Movie> {
val newPopularMovies = movieDao.getMovieList().map { it.toMovie() }
return newPopularMovies
}
}
Here, we are using the flow
builder from the Kotlin coroutines library to emit the result of the API call asynchronously. We are also using the catch
operator to catch any exceptions that might occur during the API call. If there is an error, we emit the error wrapped in the Resource.Error
class.
Implement Use Case
I skipped implementing the Use Case in the Data Layer, such as the Repository, and instead implemented it directly in the Domain Layer for this small assignment. However, in a bigger project, it is important to implement it properly in the Data Layer.
package com.softaai.mvvmdemo.domain.usecase
import com.softaai.mvvmdemo.data.source.remote.Resource
import com.softaai.mvvmdemo.domain.model.Movie
import com.softaai.mvvmdemo.domain.repository.MovieRepository
import kotlinx.coroutines.flow.Flow
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
class GetPopularMovies(
private val movieRepository: MovieRepository
) {
operator fun invoke(): Flow<Resource<List<Movie>>> {
return movieRepository.getPopularMovies()
}
}
The GetPopularMovies
class is a use case class in the domain layer that provides a way to retrieve a list of popular movies from the MovieRepository
. By using this class, we can easily retrieve the list of popular movies by calling its invoke()
method an operator function, which returns a Flow
. We can then collect the items emitted by the Flow
and handle the different states of the data using the Resource
class.
Add Hilt Modules for Dependency Injection
Hilt is a dependency injection framework that makes it easy to manage dependencies in Android apps. It is built on top of Dagger, a popular dependency injection library, and provides a simpler, more streamlined API for configuring and injecting dependencies.
To inject dependencies into our ViewModel and Repository, we’ll use Hilt for Dependency Injection. Since we have already added the Hilt dependency in the gradle file, we can now directly annotate our Application class with @HiltAndroidApp
:
package com.softaai.mvvmdemo
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
/**
* Created by amoljp19 on 4/19/2023.
* softAai Apps.
*/
@HiltAndroidApp
class MvvmDemoApp : Application() {
}
Define Hilt modules
Create a Kotlin object for each module and annotate it with @Module
. In each module, define one or more provider methods that create instances of your dependencies and annotate them with @Provides
.
MoviesNetworkModule
package com.softaai.mvvmdemo.di.moviesmodule
import com.softaai.mvvmdemo.data.source.remote.MovieApiService
import com.softaai.mvvmdemo.data.source.remote.RequestInterceptor
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Singleton
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
@Module
@InstallIn(SingletonComponent::class)
class MoviesNetworkModule {
private val interceptor = run {
val httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.apply {
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
}
}
@Provides
@Singleton
fun provideOkHttpClient() =
OkHttpClient.Builder().addInterceptor(RequestInterceptor()).addInterceptor(interceptor)
.build()
@Singleton
@Provides
fun provideRetrofitService(okHttpClient: OkHttpClient): MovieApiService =
Retrofit.Builder()
.baseUrl(MovieApiService.BASE_URL)
.addConverterFactory(
MoshiConverterFactory.create(
Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
)
)
.client(okHttpClient)
.build()
.create(MovieApiService::class.java)
}
This is a Hilt module called MoviesNetworkModule, which is used for providing dependencies related to network communication with the MovieApiService. The module is annotated with @Module and @InstallIn(SingletonComponent::class), which means that it will be installed in the SingletonComponent and has the scope of the entire application.
The module provides the following dependencies:
- OkHttpClient: This dependency is provided by a method called provideOkHttpClient, which returns an instance of OkHttpClient that is built with RequestInterceptor and HttpLoggingInterceptor.
- MovieApiService: This dependency is provided by a method called provideRetrofitService, which takes an instance of OkHttpClient as a parameter and returns an instance of MovieApiService. This method builds a Retrofit instance using MoshiConverterFactory for JSON parsing and the provided OkHttpClient, and creates a MovieApiService instance using the Retrofit.create method.
The @Singleton annotation is used on both provideOkHttpClient and provideRetrofitService methods, which means that Hilt will only create one instance of each dependency and provide it whenever it is needed.
By using these @Provides methods, we can provide these dependencies to any component in our app by simply annotating the constructor of that component with @Inject.
MoviesDatabaseModule
package com.softaai.mvvmdemo.di.moviesmodule
import android.app.Application
import com.softaai.mvvmdemo.data.source.local.roomdb.MovieDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
@Module
@InstallIn(SingletonComponent::class)
class MoviesDatabaseModule {
@Singleton
@Provides
fun provideDatabase(application: Application) = MovieDatabase.getDatabase(application)
@Singleton
@Provides
fun providePopularMoviesDao(database: MovieDatabase) =
database.getPopularMoviesDao()
@Singleton
@Provides
fun provideMovieDao(database: MovieDatabase) =
database.getMovieDao()
}
Here we have defined a Hilt module called MoviesDatabaseModule
which is annotated with @Module
and @InstallIn(SingletonComponent::class)
. This means that this module will be installed in the SingletonComponent
which has the scope of the entire application. By using these @Provides
methods, we can provide these dependencies to any component in our app by simply annotating the constructor of that component with @Inject
.
For example, if we want to use PopularMoviesDao
in our MovieRepository
, we can simply annotate the constructor of MovieRepository
with @Inject
and pass PopularMoviesDao
as a parameter:
class MovieRepository @Inject constructor(
private val movieApiService: MovieApiService,
private val popularMoviesDao: PopularMoviesDao,
private val movieDao: MovieDao
) {
...
}
By doing this, Hilt will automatically provide the PopularMoviesDao
, MovieDao
, and MovieApiService
objects to our MovieRepository
whenever it is needed.
MoviesRepositoryModule
package com.softaai.mvvmdemo.di.moviesmodule
import com.softaai.mvvmdemo.data.repository.MovieRepositoryImpl
import com.softaai.mvvmdemo.data.source.local.roomdb.dao.MovieDao
import com.softaai.mvvmdemo.data.source.local.roomdb.dao.PopularMoviesDao
import com.softaai.mvvmdemo.data.source.remote.MovieApiService
import com.softaai.mvvmdemo.domain.repository.MovieRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
@Module
@InstallIn(SingletonComponent::class)
class MoviesRepositoryModule {
@Provides
@Singleton
fun provideMovieRepositoryImpl(
movieApiService: MovieApiService,
popularMoviesDao: PopularMoviesDao,
movieDao: MovieDao
): MovieRepository = MovieRepositoryImpl(movieApiService, popularMoviesDao, movieDao)
}
This is a Dagger Hilt module for providing the MovieRepository
implementation to the app. The module is annotated with @Module
and @InstallIn(SingletonComponent::class)
which means that the MovieRepository
will have a singleton scope throughout the app.
The @Provides
method is defined to provide the MovieRepositoryImpl
instance. This method takes three parameters: movieApiService
of type MovieApiService
, popularMoviesDao
of type PopularMoviesDao
, and movieDao
of type MovieDao
. These dependencies are injected into the constructor of MovieRepositoryImpl
to create its instance.
MoviesUseCaseModule
package com.softaai.mvvmdemo.di.moviesmodule
import com.softaai.mvvmdemo.domain.repository.MovieRepository
import com.softaai.mvvmdemo.domain.usecase.GetPopularMovies
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
@Module
@InstallIn(SingletonComponent::class)
class MoviesUsecaseModule {
@Provides
@Singleton
fun provideGetPopularMoviesUseCase(repository: MovieRepository): GetPopularMovies =
GetPopularMovies(repository)
}
The GetPopularMovies
use case by injecting the MovieRepository
. The module is annotated with @InstallIn(SingletonComponent::class)
which means it will be installed in the SingletonComponent
of the application.
The provideGetPopularMoviesUseCase
method is annotated with @Provides
and @Singleton
, indicating that it provides a singleton instance of the GetPopularMovies
use case.
The repository
parameter of the method is injected via constructor injection, as it is declared as a dependency of the GetPopularMovies
constructor. The MovieRepository
is provided by the MoviesRepositoryModule
which is also installed in the SingletonComponent
.
Define the ViewModel
Now, we can define the ViewModel that will be used to expose the movie data to the UI. We will create a MoviesViewModel
class that extends the ViewModel
class from the Android Architecture Components library:
package com.softaai.mvvmdemo.presentation.viewmodel
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.softaai.mvvmdemo.data.source.remote.Resource
import com.softaai.mvvmdemo.domain.usecase.GetPopularMovies
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
@HiltViewModel
class MoviesViewModel @Inject constructor(
private val getPopularMovies: GetPopularMovies
) : ViewModel() {
private val _state = mutableStateOf(MovieUiState())
val state: State<MovieUiState> = _state
init {
getMovies()
}
fun getMovies() {
viewModelScope.launch {
getPopularMovies().onEach { result ->
when (result) {
is Resource.Loading -> {
_state.value = state.value.copy(
moviesList = result.data ?: emptyList(),
isLoading = true
)
}
is Resource.Success -> {
_state.value = state.value.copy(
moviesList = result.data ?: emptyList(),
isLoading = false
)
}
is Resource.Error -> {
_state.value = state.value.copy(
moviesList = result.data ?: emptyList(),
isLoading = false
)
}
}
}.launchIn(this)
}
}
}
This is the implementation of the MoviesViewModel, which is responsible for fetching and providing the list of popular movies to the UI layer. It uses the GetPopularMovies use case to fetch the data from the repository and updates the UI state based on the result of the operation.
package com.softaai.mvvmdemo.presentation.viewmodel
import com.softaai.mvvmdemo.domain.model.Movie
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
data class MovieUiState(
val moviesList: List<Movie> = emptyList(),
val isLoading: Boolean = false
)
The
@HiltViewModel
annotation is used to inject dependencies into aViewModel
using Hilt. When aViewModel
is annotated with@HiltViewModel
, Hilt generates a factory for theViewModel
and provides dependencies to theViewModel
via this factory. This way, theViewModel
can easily access dependencies, such as use cases or repositories, without the need to manually create and inject them.
The ViewModel uses a mutableStateOf() function to create a state object that can be updated from anywhere in the ViewModel. The state object is exposed as an immutable State object to the UI layer, which can observe it and update the UI accordingly.
The ViewModel also uses the viewModelScope to launch a coroutine that executes the use case, and observes the result of the operation using the onEach operator. Based on the result, the ViewModel updates the UI state accordingly, indicating whether the data is being loaded, whether it has been loaded successfully, or whether an error has occurred.
Define the Compose UI
First we define a MovieItem
composable that displays a single movie item in a row. We are using CoilImage
from the Coil library to display the movie poster image, and Row
and Column
composable functions to create the layout.
package com.softaai.mvvmdemo.presentation.ui.compose
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.softaai.mvvmdemo.domain.model.Movie
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
@Composable
fun MovieItem(
movie: Movie,
onItemClick: (Movie) -> Unit
) {
Row(
modifier = Modifier
.clickable { onItemClick(movie) }
.padding(vertical = 16.dp, horizontal = 16.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
CoilImage(imageUrl = movie.posterUrl)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = movie.title,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = movie.releaseDate,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
CoilImage Composable Function
package com.softaai.mvvmdemo.presentation.ui.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import coil.compose.rememberImagePainter
import com.softaai.mvvmdemo.R
/**
* Created by amoljp19 on 4/19/2023.
* softAai Apps.
*/
@Composable
fun CoilImage(imageUrl: String) {
Image(
painter = rememberImagePainter(
data = "https://image.tmdb.org/t/p/w600_and_h900_bestv2${imageUrl}",
builder = {
// Optional: Add image transformations
placeholder(R.drawable.ic_launcher_foreground)
}
),
contentDescription = "Coil Image",
modifier = Modifier
.size(80.dp)
.clip(RoundedCornerShape(4.dp))
)
}
A composable function called CoilImage
, displays an image using Coil library in Jetpack Compose. The function takes a String
parameter called imageUrl
which is the URL of the image to be displayed.
Finally, we will create a MoviesListScreen
composable function that displays a list of popular movies using a LazyColumn
.
package com.softaai.mvvmdemo.presentation.ui.compose
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.softaai.mvvmdemo.presentation.viewmodel.MoviesViewModel
/**
* Created by amoljp19 on 4/18/2023.
* softAai Apps.
*/
@Composable
fun MovieListScreen(moviesViewModel: MoviesViewModel = hiltViewModel()) {
val state = moviesViewModel.state.value
LazyColumn(
Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 16.dp)
) {
items(state.moviesList.size) { i ->
MovieItem(movie = state.moviesList[i], onItemClick = {})
}
}
}
The Composable function MovieListScreen
which takes a MoviesViewModel
as a parameter and sets its default value using the hiltViewModel()
function provided by the Hilt library that allows you to retrieve a ViewModel
instance that is scoped to the current Compose
component. This is useful because it allows you to inject dependencies directly into your ViewModel
using the Hilt dependency injection system.
By using hiltViewModel()
instead of creating a new instance of the MoviesViewModel
class manually, you ensure that the instance of the MoviesViewModel
used in the MovieListScreen
composable is the same instance that is injected by Hilt into the ViewModel
.
Inside the function, it gets the current state of the ViewModel using moviesViewModel.state.value
and stores it in a variable called state
.
It then creates a LazyColumn
with Modifier.fillMaxSize()
and a content padding of PaddingValues(bottom = 16.dp)
. Inside the LazyColumn
, it creates a list of items using the items
function, which iterates over the state.moviesList
and creates a MovieItem
for each movie.
The MovieItem
composable is passed the movie
object from the current iteration, and an empty lambda function onItemClick
(which could be used to handle clicks on the item).
Putting it all together
Now, we can put all the pieces together in our MainActivity
, which is annotated with the @AndroidEntryPoint
annotation. This annotation is part of the Hilt library, and it allows Hilt to generate a component for the activity and provide dependencies to its fields and methods.
package com.softaai.mvvmdemo
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.softaai.mvvmdemo.presentation.ui.compose.MovieListScreen
import com.softaai.mvvmdemo.presentation.ui.theme.MVVMDemoTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MVVMDemoTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
MovieListScreen()
}
}
}
}
}
Inside the onCreate
method, the setContent
method is used to set the main content of the activity. In this case, the content is the MovieListScreen
composable function, which displays a list of movies.
Note — I have provided proper guidance on how to display the list of movies. Now, you can continue building the movie details screen by following similar patterns as discussed earlier. If you face any issues or need any further assistance, feel free to ask me.
Conclusion
In this article, we have demonstrated how to build an Android app using Clean Architecture, MVVM, Kotlin, Room, Hilt, Retrofit, Moshi, Flow, and Jetpack Compose. We have covered all aspects of the app development process, including defining the data model, implementing the repository layer, defining the ViewModel, and defining the UI. By following these best practices, we can create robust and maintainable Android apps that are easy to test and evolve over time.
Note — I have provided proper guidance on how to display the list of movies. I now expect you to complete the remaining work by following the guidelines.