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>>
}
}
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))
}
}
}
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()
}
}
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
}
}
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>) {
}
}
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
}
}
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)
}
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)
}
To see the complete working source code, fork
- GitHub Issues or
- Photo Search repository and run on the latest Android Studio.
If you have any doubt or query or need a detailed explanation of any component, drop in the comment box.
Top comments (0)