DEV Community

loading...
Cover image for Android app with MVVM + Room + Paging + Koin + Coroutine + Live Data + Retrofit

Android app with MVVM + Room + Paging + Koin + Coroutine + Live Data + Retrofit

mohitrajput987 profile image Mohit Rajput Updated on ・5 min read

Earlier, we didn't have architecture components officially from Android. A few years ago, Android introduced a set of components to develop Android applications with proper architecture under "Android Jetpack".

Jetpack is a suite of libraries to help developers follow best practices, reduce boilerplate code, and write code that works consistently across Android versions and devices so that developers can focus on the code they care about.

Components Used

In this article, we will develop a small Android application to demonstrate jetpack as well as a few more components which are as follows:

  • ViewModel- A jetpack component to architect app with MVVM. Here we will use data binding as well.
  • Room Database- It's a wrapper over the SQLite database to give developers an ORM-like feel.
  • Paging Library - The Paging Library helps you load and display a small amount of data at a time. Loading partial data on-demand reduces the usage of network bandwidth and system resources.
  • Coroutine- Coroutine is a framework in Kotlin to make asynchronous calls in a more readable fashion.
  • Koin- It's a dependency injection library that is very easy to use as compare to dagger or hilt.
  • Live Data- It's a life cycle aware observable data holder.
  • Retrofit- It's the most famous web service calling library which we will use in the app to fetch data from web API.

Repositories

I have developed the following apps to demonstrate these components-

Feel free to fork these repositories, add new features, and raise pull requests.

Coding Starts

Without too much description, I will add code snippets from GitHub Issues project here which are self-explanatory. For any query, you can use the comment section.

Here is the contract for github pull request list feature:

interface IssuesContract {
    interface ViewModel {
        fun getIssues(): LiveData<PagedList<IssuesModels.Issue>>
        fun getProgressStatus(): LiveData<ProgressStatus>
        fun searchIssues(githubOwner: String, repoName: String, state: String)
    }

    interface Repository {
        suspend fun searchIssues(githubOwner: String, repoName: String, state: String, pageNumber: Int): DataResult<List<IssuesModels.Issue>>
    }
}
Enter fullscreen mode Exit fullscreen mode

Here is the implementation of our data layer:

class IssuesRepository(private val gitHubApiService: GitHubApiService, private val issuesDao: IssuesDao) : IssuesContract.Repository {
    companion object {
        const val PAGE_SIZE = 20
    }

    override suspend fun searchIssues(githubOwner: String, repoName: String, state: String, pageNumber: Int): DataResult<List<IssuesModels.Issue>> {
        val storedIssues = issuesDao.getIssues(githubOwner, repoName, state, ((pageNumber - 1) * PAGE_SIZE), PAGE_SIZE)
        if (storedIssues.isNotEmpty()) {
            val refreshInterval = 30 * 60 * 1000
            val oldestIssue = storedIssues.minBy { it.storedAt }
            if (System.currentTimeMillis() - oldestIssue!!.storedAt < refreshInterval) {
                return DataResult.DataSuccess(IssuesResponseToIssuesMapper().mapFromDatabase(storedIssues))
            } else {
                issuesDao.deleteIssues(githubOwner, repoName, state)
            }
        }

        val response = gitHubApiService.fetchIssues(githubOwner, repoName, state, pageNumber, PAGE_SIZE)
        return if (response.isSuccessful) {
            val issuesResponse = response.body()!!
            val issues = IssuesResponseToIssuesMapper().map(issuesResponse)
            issuesDao.insertAll(issues.map { IssuesModels.IssueEntity(it.id, it.patchUrl, it.title, it.number, it.userName, it.state, githubOwner, repoName, System.currentTimeMillis()) })
            DataResult.DataSuccess(issues)
        } else {
            DataResult.DataError(ErrorHandler.getError(response))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Here is the implementation of ViewModel:

class IssuesViewModel(private val repository: IssuesContract.Repository) : ViewModel(), IssuesContract.ViewModel {
    private val issuesDataSourceFactory: IssuesDataSourceFactory = IssuesDataSourceFactory(repository, viewModelScope)
    private val progressLoadStatus: LiveData<ProgressStatus>
    private val issues: LiveData<PagedList<IssuesModels.Issue>>

    init {
        val pagedListConfig = PagedList.Config.Builder()
                .setEnablePlaceholders(true)
                .setInitialLoadSizeHint(20)
                .setPageSize(20)
                .build()

        issues = LivePagedListBuilder<Int, IssuesModels.Issue>(issuesDataSourceFactory, pagedListConfig)
                .build()

        progressLoadStatus = Transformations.switchMap(issuesDataSourceFactory.liveData, IssuesDataSource::getProgressLiveStatus)
    }

    override fun getIssues() = issues

    override fun getProgressStatus() = progressLoadStatus

    override fun searchIssues(githubOwner: String, repoName: String, state: String) {
        issuesDataSourceFactory.githubOwner = githubOwner
        issuesDataSourceFactory.repoName = repoName
        issuesDataSourceFactory.state = state
        issues.value?.dataSource?.invalidate()
    }
}
Enter fullscreen mode Exit fullscreen mode

Here you can see, we have used live data of PagedList. We have initialized paging related instanced i.e. paging config, data source etc. Now we will create data source factory:

class IssuesDataSourceFactory(private val repository: IssuesContract.Repository, private val scope: CoroutineScope) : DataSource.Factory<Int, IssuesModels.Issue>() {
    var githubOwner: String = ""
    var repoName: String = ""
    var state: String = ""

    val liveData = MutableLiveData<IssuesDataSource>()
    lateinit var issuesDataSource: IssuesDataSource

    override fun create(): DataSource<Int, IssuesModels.Issue> {
        issuesDataSource = IssuesDataSource(repository, scope, githubOwner, repoName, state)
        liveData.postValue(issuesDataSource)
        return issuesDataSource
    }
}
Enter fullscreen mode Exit fullscreen mode

and here is the implementation of data source:

class IssuesDataSource(private val repository: IssuesContract.Repository, private val scope: CoroutineScope, private val githubOwner: String, private val repoName: String, private val state: String) : PageKeyedDataSource<Int, IssuesModels.Issue>() {
    private val progressLiveStatus = MutableLiveData<ProgressStatus>()

    fun getProgressLiveStatus() = progressLiveStatus

    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, IssuesModels.Issue>) {
        scope.launch {
            progressLiveStatus.postValue(ProgressStatus.Loading)
            when (val dataResult = repository.searchIssues(githubOwner, repoName, state, 1)) {
                is DataResult.DataSuccess -> {
                    progressLiveStatus.postValue(ProgressStatus.Success)
                    callback.onResult(dataResult.data, null, 2)
                }
                is DataResult.DataError -> progressLiveStatus.postValue(ProgressStatus.Error(dataResult.errorMessage))
            }
        }
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, IssuesModels.Issue>) {
        scope.launch {
            progressLiveStatus.postValue(ProgressStatus.Loading)
            when (val dataResult = repository.searchIssues(githubOwner, repoName, state, params.key)) {
                is DataResult.DataSuccess -> {
                    progressLiveStatus.postValue(ProgressStatus.Success)
                    callback.onResult(dataResult.data, params.key + 1)
                }
                is DataResult.DataError -> progressLiveStatus.postValue(ProgressStatus.Error(dataResult.errorMessage))
            }
        }
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, IssuesModels.Issue>) {

    }
}
Enter fullscreen mode Exit fullscreen mode

Now it's time to create adapter for RecyclerView according to paging library:

class IssueAdapter : PagedListAdapter<IssuesModels.Issue, IssueAdapter.IssueViewHolder>(DiffUtilCallBack()) {

    inner class IssueViewHolder(private val binding: ItemIssueBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(issue: IssuesModels.Issue) {
            binding.issue = issue
            binding.executePendingBindings()
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IssueViewHolder {
        val itemBinding: ItemIssueBinding = ItemIssueBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return IssueViewHolder(itemBinding)
    }

    override fun onBindViewHolder(holder: IssueViewHolder, position: Int) {
        holder.bind(getItem(position)!!)
    }
}

class DiffUtilCallBack : DiffUtil.ItemCallback<IssuesModels.Issue>() {
    override fun areItemsTheSame(oldItem: IssuesModels.Issue, newItem: IssuesModels.Issue) = oldItem.id == newItem.id

    override fun areContentsTheSame(oldItem: IssuesModels.Issue, newItem: IssuesModels.Issue): Boolean {
        return oldItem.id == newItem.id
                && oldItem.title == newItem.title
                && oldItem.number == newItem.number
    }
}
Enter fullscreen mode Exit fullscreen mode

In the source code of GitHub Issues, IssuesFragment has complete code for initializing the ViewModel. Here is a snippet how can we observe data and set it to adapter:

private fun setupViewModel() {
        issuesViewModel.getIssues().observe(this, Observer {
            (rvIssues.adapter as IssueAdapter).submitList(it)
        })

        issuesViewModel.getProgressStatus().observe(this, Observer {
            when (it) {
                is ProgressStatus.Loading -> displayLoading()
                is ProgressStatus.Success -> hideLoading()
                is ProgressStatus.Error -> displayError(it.errorMessage)
            }
        })
        issuesViewModel.searchIssues(githubOwner, repoName, issueState)
    }
Enter fullscreen mode Exit fullscreen mode

In the GitHubApp application class, you can see the setup of Koin and its global modules

class GitHubApp : Application() {
    override fun onCreate() {
        super.onCreate()
        initKoin()
    }

    private fun initKoin() {
        startKoin {
            androidLogger(Level.DEBUG)
            androidContext(this@GitHubApp)
            androidFileProperties()
            modules(provideModules())
        }
    }

    private fun provideModules() = listOf(retrofitModule, apiModule, databaseModule)
}
Enter fullscreen mode Exit fullscreen mode

To see the complete working source code, fork

If you have any doubt or query or need a detailed explanation of any component, drop in the comment box.

Discussion

pic
Editor guide