Step by Step guide to create basic MVVM Application in Android

Step by Step guide to create basic MVVM Application in Android

Machine Coding LLD Android - MVVM-Hilt-Retrofit

In this blog we will create a basic mvvm application where user will fetch some data from the api and show it to the UI as a list.

For the sake of this article, we will use OMDb (The Open Movie Database) API to fetch the title and thumbnail image of the movie.

Purpose of this blog is to get started with creating MVVM project following proper architecture which can be used as a base to create any project.

This blog will help in machine coding round in android interviews.

Out of Scope for this blog:

  • Pagination

  • Testing

  • Compose

  • Offline Caching

  • Multiple Screen / Search section

Check out the NewsApp that covers all the above out of scope points.

1. Understanding the API

{
  "Search": [
    {
      "Title": "Hellboy II: The Golden Army",
      "Year": "2008",
      "imdbID": "tt0411477",
      "Type": "movie",
      "Poster": "https://m.media-amazon.com/images/M/MV5BMjA5NzgyMjc2Nl5BMl5BanBnXkFtZTcwOTU3MDI3MQ@@._V1_SX300.jpg"
    },
    {
      "Title": "Army of Darkness",
      "Year": "1992",
      "imdbID": "tt0106308",
      "Type": "movie",
      "Poster": "https://m.media-amazon.com/images/M/MV5BODcyYzM4YTAtNGM5MS00NjU4LTk2Y2ItZjI5NjkzZTk0MmQ1XkEyXkFqcGdeQXVyNjU0OTQ0OTY@._V1_SX300.jpg"
    }, 
    ..
    ..
   ],
  "totalResults": "703",
  "Response": "True"
}
  • For this project we show the list showing image and title of the movie.

2. Add dependencies

  • Create the new android project in Android Studio and add the following dependency in app level build.gradle.kts or build.gradle file.
//build.gradle.kts

dependencies {

    //Dagger for dependency injection
    implementation("com.google.dagger:hilt-android:2.51")
    kapt("com.google.dagger:hilt-compiler:2.51")

    //Glide for image loading
    implementation("com.github.bumptech.glide:glide:4.16.0")

    //Retrofit for networking
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")

    //ViewModel scope
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.2")
}
  • Additionally for Hilt add the following plugins in app level build.gradle.kts or build.gradle file.
//build.gradle.kts

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("kotlin-kapt") //<-- this one
    id("com.google.dagger.hilt.android") // <-- this one
}
  • Also add following at root level build.gradle.kts or build.gradle file.
//build.gradle.kts

plugins {
    id("com.android.application") version "8.1.4" apply false
    id("org.jetbrains.kotlin.android") version "1.9.0" apply false
    id("com.google.dagger.hilt.android") version "2.51" apply false //<-- this one
}
  • Since we will be using view binding for our xml layouts, add the following in app level build.gradle.kts or build.gradle file.
//build.gradle.kts

android {
..
..
  buildFeatures {
      viewBinding = true
  }
}
  • Sync the gradle and run your project.

3. Create Application class and Add necessary permissions in Manifest file

  • Create Application class and add @HiltAndroidApp annotation.
//MainApplication.kt

@HiltAndroidApp
class MainApplication: Application()
  • Add the name in manifest file.
<!--AndroidManifest.xml-->

<application
  android:name=".MainApplication">
</application>
  • Add Internet permission in manifest file.
<!--AndroidManifest.xml-->

<uses-permission android:name="android.permission.INTERNET"/>

4. Create Folder structure

  • Create the following folder structure, with empty folders (“common”, “data/model”, “data/network”, “data/repository”, “di/module”, “ui”, “ui/viewmodel”).

  • Check the below final project structure added for reference only, we will create each file step by step.

|── MainApplication.kt
├── common
│   ├── Const.kt
│   └── UIState.kt
├── data
│   ├── model
│   │   ├── MainData.kt
│   ├── network
│   │   ├── ApiInterface.kt
│   └── repository
│       └── MainRepository.kt
├── di
│   ├── module
│   │   └── ApplicationModule.kt
├── ui
    ├── MainActivity.kt
    ├── MainAdapter.kt
    └── viewmodel
        ├── MainViewModel.kt

5. Create Model class (Data Layer)

  • Create class MainData.kt inside “data/model” folder and add the following data class based on the JSON, we are using only title and image.
//MainData.kt

data class ApiResponse(
    @SerializedName("Search")
    val dataList: List<MainData>?
)

data class MainData(
    @SerializedName("Title")
    val title: String,

    @SerializedName("Poster")
    val poster: String
)

6. Create Networking interface (Data Layer)

  • First create object class Const.kt inside “common” folder and add Base url and Api Key.
//Const.kt

object Const {
    const val BASE_URL = "https://www.omdbapi.com/"
    const val API_KEY = "your api key"
}
  • Then create ApiInterface.kt class inside “data/network” folder with following function.
//ApiInterface.kt

interface ApiInterface {
    @GET(".")
    suspend fun getMoviesData(
        @Query("s") s: String = "army",
        @Query("apikey") apikey: String = Const.API_KEY
    ): ApiResponse
}
  • We are not using OkHttp interceptor for adding api key for of this article but it is recommended to add api key via interceptors.

7. Create ApplicationModule (DI Layer)

  • Create ApplicationModule.kt class inside “di/module” folder for providing Retrofit instance.
//ApplicationModule.kt

@Module
@InstallIn(SingletonComponent::class)
class ApplicationModule {

    @Singleton
    @Provides
    fun provideNetworkService(): ApiInterface {
        return Retrofit
            .Builder()
            .baseUrl(Const.BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiInterface::class.java)
    }
}

8. Create Repository class (Data Layer)

  • Create MainRepository.kt class inside “data/repository” folder that will return flow of list which will be collected by view model.
//MainRepository.kt

@Singleton
class MainRepository @Inject constructor(
    private val apiInterface: ApiInterface
) {

    fun getMainData(): Flow<List<MainData>> {
        return flow {
            emit(
                apiInterface.getMoviesData().dataList ?: emptyList()
            )
        }
    }
}

9. Create ViewModel and UIState (UI Layer)

  • First create UIState.kt inside “common” folder which is a sealed interface representing the state of UI that will emit and collect between ViewModel and UI component.
//UIState.kt

sealed interface UIState<out T> {
    data class Success<T>(val data: T) : UIState<T>
    data class Failure<T>(val throwable: Throwable, val data: T? = null) : UIState<T>
    data object Loading : UIState<Nothing>
}
  • Then create MainViewModel.kt inside “ui/viewmodel” folder which extends ViewModel() and inject MainRepository.

  • We use StateFlow to emit data from ViewModel and then collect by UI.

  • We use IO dispatcher for network call (For this article we are not injecting dispatcher from outside, but it is recommended to provide from outside).

//MainViewModel.kt

@HiltViewModel
class MainViewModel @Inject constructor(
    private val mainRepository: MainRepository
) : ViewModel() {

    private val _mainItem = MutableStateFlow<UIState<List<MainData>>>(UIState.Loading)
    val mainItem: StateFlow<UIState<List<MainData>>> = _mainItem

    init {
        fetchItems()
    }

    private fun fetchItems() {
        viewModelScope.launch {
            _mainItem.emit(UIState.Loading)
            mainRepository
                .getMainData()
                .flowOn(Dispatchers.IO)
                .catch {
                    _mainItem.emit(UIState.Failure(it))
                }
                .collect {
                    _mainItem.emit(UIState.Success(it))
                }
        }
    }

}

10. Create Layouts (UI Layer)

  • Edit activity_main.xml with RecyclerView for showing the list, progress bar and error message.
<!--activity_main.xml-->

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ProgressBar
        android:id="@+id/progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/error"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="Something went wrong" />

</androidx.constraintlayout.widget.ConstraintLayout>
  • Create item_main.xml layout and put inside “res” folder for single item visible on UI that will be used by Recycler View Adapter.
<!--item_main.xml-->

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="110dp">

    <ImageView
        android:id="@+id/iv_item"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_margin="5dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:background="@color/design_default_color_secondary" />

    <TextView
        android:id="@+id/tv_item"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@+id/iv_item"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="Hey there" />

</androidx.constraintlayout.widget.ConstraintLayout>

11. Create Adapter class (UI Layer)

  • Create MainAdapter.kt inside “ui” folder that extends RecyclerView Adapter.

  • We use DiffUtil for calculating data diff and bind data.

  • We use Glide for loading image into image view.

//MainAdapter.kt

class MainAdapter : RecyclerView.Adapter<MainAdapter.MainViewHolder>() {

    private val items = ArrayList<MainData>()

    fun setItems(items: List<MainData>) {
        val diffResult = DiffUtil.calculateDiff(MainDiffCallBack(this.items, items))
        this.items.clear()
        this.items.addAll(items)
        diffResult.dispatchUpdatesTo(this)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
        return MainViewHolder(
            ItemMainBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        )
    }

    override fun getItemCount(): Int {
        return items.size
    }

    override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
        holder.bind(items[position])
    }

    inner class MainViewHolder(private val binding: ItemMainBinding) : ViewHolder(binding.root) {

        fun bind(mainData: MainData) {
            binding.tvItem.text = mainData.title
            Glide.with(itemView.context)
                .load(mainData.poster)
                .placeholder(com.google.android.material.R.color.design_default_color_secondary)
                .into(binding.ivItem)
        }

    }
}

class MainDiffCallBack(private val oldList: List<MainData>, private val newList: List<MainData>) :
    DiffUtil.Callback() {
    override fun getOldListSize(): Int {
        return oldList.size
    }

    override fun getNewListSize(): Int {
        return newList.size
    }

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return (oldList[oldItemPosition].title == newList[newItemPosition].title)

    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition] == newList[newItemPosition]
    }
}

12. Observe UIState inside MainActivity (UI Layer)

  • Drag MainActivity.kt file inside “ui” folder.

  • We collect Flow from viewmodel and update UI.

//MainActivity.kt

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    private lateinit var adapter: MainAdapter

    private val mainViewModel: MainViewModel by lazy {
        ViewModelProvider(this)[MainViewModel::class.java]
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        adapter = MainAdapter()
        collector()
        setupUI()
    }

    private fun setupUI() {
        binding.rv.adapter = adapter
        binding.rv.layoutManager =
            LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        binding.rv.addItemDecoration(
            DividerItemDecoration(
                this,
                DividerItemDecoration.VERTICAL
            )
        )
    }

    private fun collector() {
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mainViewModel.mainItem.collect {
                    when (it) {
                        is UIState.Success -> {
                            binding.progress.visibility = View.GONE
                            binding.error.visibility = View.GONE
                            binding.rv.visibility = View.VISIBLE
                            adapter.setItems(it.data)
                        }

                        is UIState.Failure -> {
                            binding.progress.visibility = View.GONE
                            binding.error.visibility = View.VISIBLE
                            binding.rv.visibility = View.GONE
                            binding.error.text = it.throwable.toString()
                        }

                        is UIState.Loading -> {
                            binding.progress.visibility = View.VISIBLE
                            binding.error.visibility = View.GONE
                            binding.rv.visibility = View.GONE
                        }
                    }
                }
            }
        }
    }

}

Run your application after each step to check everything is working fine.

High level flow

  • Check the below high level flow of how data coming from API and is shown to UI.

  • Using the above step by step approach help making a base for any MVVM application making it scalable.

Final Result

Source Code: GitHub

Contact Me:

LinkedIn, Twitter

Happy Coding ✌️