This document discusses cleaning up Android architecture using Architecture Components. It begins with an overview of issues with previous approaches like callbacks between layers. It then covers how each component addresses these issues: ViewModel/LiveData for presentation, MediatorLiveData for merging repository calls in the domain layer, and Room/Paging Library for the data layer. Considerations are provided for each component regarding threading, lifecycles, and other best practices. Code examples demonstrate implementations across the layers following these guidelines.
1 of 48
Download to read offline
More Related Content
Cleaning your architecture with android architecture components
2. Who am I?
Team & Technical Lead
myToys Group GmbH
https://github.com/dgomez-developer
dgomez.developer@gmail.com
@dgomezdebora
www.linkedin.com/in/deboragomezbertoli
7. It is impossible for this to happen
Trying to update an Activity / Fragment that is not there anymore ´
#commitconf @dgomezdebora
if((view as? Activity)?.isFinishing == false){
view?.showQuestions(result.map { question ->
QuestionViewItem(question.id, question.question, question.contact) })
}
8. This is a callback nightmare
Callback in datasource, that calls a callback in the repository, that calls a callback in the
interactor, that calls a callback in the presenter, that calls the view.
#commitconf @dgomezdebora
getQuestionsUseCase.invoke(object : Callback<List<Question>, Throwable>
{..}
questionsRepository.getQuestions(object : Callback<List<Question>,
Throwable> {..}
override fun setView(view: QuestionsView?) {
this.view = view}
11. ViewModel as Presenter
Is designed to store and manage UI-related data in a lifecycle conscious way.
#commitconf @dgomezdebora
Is automatically retained during configuration changes.
Remains in memory until the Activity finishes or the Fragment is detached.
Includes support for Kotlin coroutines.
12. LiveData as callbacks
#commitconf @dgomezdebora
Is an observable data holder class.
Is lifecycle-aware, only updates observers that are in an active lifecycle state
(STARTED, RESUMED).
Observers are bound to lifecycle so they are clean up when their associated lifecycle is
destroyed.
13. Transformations as mapper
#commitconf @dgomezdebora
Transformations.map() - It observes the LiveData. Whenever a
new value is available it takes the value, applies the Function on in, and
sets the Function¨s output as a value on the LiveData it returns.
Transformations.switchmap() - It observes the LiveData and
returns a new LiveData with the value mapped according to the Function
applied.
15. Implementation - ViewModel
#commitconf @dgomezdebora
class QuestionsListViewModel (private val getQuestionsUseCase: GetQuestionsUseCase) :
ViewModel() {
[...]
fun init() {
loaderLD.value = true
val listOfQuestionsLD = getQuestionsUseCase.invoke(Unit)
questionsLD.removeSource(listOfQuestionsLD)
questionsLD.addSource(listOfQuestionsLD) {
when (it) {
is Either.Success -> questionsLD.value =
it.value.map { question -> QuestionViewItem(question.id, question.question,
question.contact) }
is Either.Failure -> messageLD.value = R.string.error_getting_questions
}
loaderLD.value = false
}
}
}
16. Implementation - Activity
#commitconf @dgomezdebora
private val viewModel by viewModel<QuestionsListViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
list_of_questions.layoutManager = LinearLayoutManager(this)
list_of_questions.adapter = QuestionsListAdapter()
viewModel.showQuestions().observe(this, Observer {
adapter.questionsList = it
})
viewModel.showMessage().observe(this, Observer {
Snackbar.make(list_of_questions_container, it, Snackbar.LENGTH_LONG).show()
})
viewModel.init()
}
17. Considerations - ViewModel
ViewModel shouldn¨t be referenced in any object that can outlive the Activity,
so the ViewModel can be garbage collected.
A ViewModel must never reference a view, Lifecycle, or any class that may hold a
reference to the Activity context.
ViewModel is not an eternal thing, they also get killed when the OS is low on
resources and kills our process.
.... don¨t panic! Google is already working on a safe state module for
ViewModel. (still in alpha)
#commitconf @dgomezdebora
18. Considerations - LiveData
If the code is executed in a worker thread, use postValue(T).
setValue(T) should be called only from the main thread.
If you use this instead of viewLifeCycleOwner, LiveData won?t remove
observers every time the Fragment?s view is destroyed.
Views should not be able of updating LiveData, this is ViewModel¨s
reponsibility. Do not expose mutable LiveData to the views.
When using observeForever(Observer), you should manually call
removeObserver(Observer)
#commitconf @dgomezdebora
19. Considerations - Transformations
Solution is to use Events to trigger a new request and update the
LiveData.
Transformations create a new LiveData when called (both map and switchmap).
It is very common to miss that the observer will only receive updates to the
LiveData assigned to the var in the moment of the subscription!!
#commitconf @dgomezdebora
22. MediatorLiveData as merger of
repository calls
Scenario: we have 2 instances of different LiveData (liveDataA, liveDataB), we
want to merge their emissions in one LiveData (liveDatasMerger).
Then, liveDataA and liveDataB will become sources of liveDatasMerger.
Each time onChanged is called for either of them, we will set a new value
in liveDatasMerger.
#commitconf @dgomezdebora
23. Implementation - Use Case
#commitconf @dgomezdebora
class GetQuestionsUseCase(
private val questionsRepository: QuestionsRepository,
threadExecutor: ThreadExecutor,
postExecutionThread: PostExecutionThread):
BaseBackgroundLiveDataInteractor<Unit, List<Question>>(
threadExecutor, postExecutionThread) {
override fun run(inputParams: Unit): LiveData<List<Question>> {
return questionsRepository.getQuestions()
}
}
24. Implementation - Interactor
#commitconf @dgomezdebora
abstract class BaseBackgroundLiveDataInteractor<I, O>(
private val backgroundThread: ThreadExecutor,
private val postExecutionThread: PostExecutionThread) {
internal abstract fun run(inputParams: I): LiveData<O>
operator fun invoke(inputParams: I): LiveData<Either<O, Throwable>> =
buildLiveData(inputParams)
25. Implementation - Interactor
#commitconf @dgomezdebora
private fun buildLiveData(inputParams: I): LiveData<Either<O, Throwable>> =
MediatorLiveData<Either<O, Throwable>>().also {
backgroundThread.execute(Runnable {
val result = execute(inputParams)
postExecutionThread.post(Runnable {
when (result) {
is Either.Success -> it.addSource(result.value) { t -> it.postValue(Either.Success(t)) }
is Either.Failure -> it.postValue(Either.Failure(result.error))
}
})
})
}
private fun execute(inputParams: I): Either<LiveData<O>, Throwable> = try {
Either.Success(run(inputParams))
} catch (throwable: Throwable) {
Either.Failure(throwable)
}
26. Considerations - MediatorLiveData
It does not combine data. In case we want to combine data, we will have to do it a
separate method that receives all the LiveDatas and merges their values.
If a method where an addSource is executing is called several times, we will leak all the
previous LiveData.
#commitconf @dgomezdebora
28. Room as persistence data source
Provides an abstraction layer over SQLite.
It is highly recommended by Google using Room instead of SQLite.
We can use LiveData with Room to observe changes in the database.
In Room the intensive use of annotations let us write less code.
Provides integration with RxJava out of the box.
There is no compile time verification of raw SQLite queries.
To convert SQLite queries into data objects, you need to write a lot of boilerplate
code.
#commitconf @dgomezdebora
34. Considerations - Room
Room does not support to be called on the MainThread unless you set
allowMainThreadQueries().
Room is NOT a relational database.
You could simulate a relational database using:
@ForeignKey to define one-to-many relationships.
@Embedded to create nested objects.
Intermediate class operating as Join query to define many-to-many relationships.
If your app runs in a single process, you should follow the singleton pattern when
instantiating an AppDatabase object.
Otherwise ´. You will get crashes like SQLiteException when migrating different
instances of the AppDatabase object.
#commitconf @dgomezdebora
38. Paging Library
#commitconf @dgomezdebora
Helps loading displaying small chunks of data at a time.
The key component is the PagedList.
If any loaded data changes, a new instance of the PagedList is emitted to the
observable data holder from a LiveData.
39. Paging Library DataSource
#commitconf @dgomezdebora
class QuestionsPagedDataSource(
private val api: QuestionsApi,
private val requestParams: QuestionsRequestParams,
private val errorLD: MutableLiveData<Throwable>) :
PageKeyedDataSource<Int, Question>() {
46. This is a win!
No memory leaks.
Ensures our UI matches our data state.
No crashes due to stopped Activities.
No more manual lifecycle handling.
Proper configuration changes.
#commitconf @dgomezdebora
47. What about RxJava?
If you already use Rx for this, you can connect both using
LiveDataReactiveStreams.
LiveData was designed to allow the View observe the ViewModel.
If you want to use LiveData beyond presentation layer, you might find that
MediatorLiveData is not as powerful when combining and operating on streams
as RXJava.
However with some magic using Kotlin Extensions it might be more than
enough for your use case.
#commitconf @dgomezdebora