際際滷

際際滷Share a Scribd company logo
D└bora Gomez
@dgomezdebora
22 noviembre 2019
CLEANING YOUR
ARCHITECTURE
WITH ANDROID
ARCHITECTURE
COMPONENTS
Who am I?
Team & Technical Lead
myToys Group GmbH
https://github.com/dgomez-developer
dgomez.developer@gmail.com
@dgomezdebora
www.linkedin.com/in/deboragomezbertoli
01
02
03
04
Before
Architecture
components
Presentation
Domain
Data
Disclaimer
This talk is not about explaining
Clean Architecture.
Disclaimer!
@dgomezdebora#commitconf
Disclaimer
RxJava is out of scope
Disclaimer!
#commitconf @dgomezdebora
Before Architecture
components
01
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) })
}
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}
Implementation
https://github.com/dgomez-developer/qa-c
lient/tree/feature/example-with-no-archite
cture-components
#commitconf @dgomezdebora
Presentation Layer02
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.
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.
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.
Implementation - Dependencies
#commitconf @dgomezdebora
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
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
}
}
}
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()
}
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
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
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
Easy right?
#commitconf @dgomezdebora
Domain Layer03
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
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()
}
}
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)
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)
}
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
04 Data Layer
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
Implementation - Dependencies
#commitconf @dgomezdebora
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
Implementation - Entity
#commitconf @dgomezdebora
@Entity(tableName = "question")
class QuestionEntity (
@PrimaryKey
val id: String,
val question: String,
val contact: String?
)
Implementation - DAO
#commitconf @dgomezdebora
@Dao
interface QuestionsDao {
@Query("SELECT * from question")
fun getAll(): LiveData<List<QuestionEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(map: List<QuestionEntity>)
}
Implementation - RoomDatabase
#commitconf @dgomezdebora
@Database(entities = arrayOf(QuestionEntity::class), version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun questionsDao(): QuestionsDao
}
Implementation - Datasource
#commitconf @dgomezdebora
class QuestionsLocalDataSource(val db: AppDatabase) {
fun getAllQuestions() = Transformations.map(db.questionsDao().getAll()) {
input -> input.map{ dbquestion ->
Question(dbquestion.id, dbquestion.question, dbquestion.contact) }
}
fun updateQuestions(questions: List<Question>) {
db.questionsDao().insertAll(questions.map {
QuestionEntity(it.id, it.question, it.contact) })
}
}
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
Are you still there??
Show me the code
#commitconf @dgomezdebora
https://github.com/dgomez-developer/qa-clien
t
Data Layer04
Pagination use case
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.
Paging Library DataSource
#commitconf @dgomezdebora
class QuestionsPagedDataSource(
private val api: QuestionsApi,
private val requestParams: QuestionsRequestParams,
private val errorLD: MutableLiveData<Throwable>) :
PageKeyedDataSource<Int, Question>() {
Paging Library DataSource
#commitconf @dgomezdebora
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int,
Question>) {
val questionsCall = api.getQuestions(requestParams.page, requestParams.pageSize)
val response = questionsCall.execute()
return if (response.isSuccessful) {
callback.onResult(
response.body()?.toMutableList() ?: mutableListOf(), null, requestParams.page + 1)
} else {
errorLD.postValue(Throwable())
}
}
Paging Library DataSource
#commitconf @dgomezdebora
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Question>) {
val questionsCall = api.getQuestions(params.key, params.requestedLoadSize)
val response = questionsCall.execute()
return if (response.isSuccessful) {
if (params.requestedLoadSize >= (params.key + 1)) {
callback.onResult(response.body()?.toMutableList() ?: mutableListOf(), params.key + 1)
} else {
callback.onResult(response.body()?.toMutableList() ?: mutableListOf(), null)
}
} else {errorLD.postValue(Throwable())}
}
Paging Library Factory
#commitconf @dgomezdebora
class QuestionsPagedFactory(
private val api: QuestionsApi,
val threadExecutor: ThreadExecutor) : DataSource.Factory<Int, Question>() {
private val mutableErrorLD by lazy { MutableLiveData<Throwable>() }
override fun create(): DataSource<Int, Question> {
return QuestionsPagedDataSource(api, QuestionsRequestParams(),
mutableErrorLD)
}
}
Paging Library Repository
#commitconf @dgomezdebora
class QuestionsRepositoryImpl(...) : QuestionsRepository {
override fun getQuestionsFromServer(): LiveData<PagedList<Question>> {
val config: PagedList.Config = PagedList.Config.Builder()
.setInitialLoadSizeHint(6)
.setPageSize(6)
.setEnablePlaceholders(false)
.setPrefetchDistance(6)
.build()
return LivePagedListBuilder(questionsNetworkDataSource, config)
.setFetchExecutor(questionsNetworkDataSource.threadExecutor.getThreadExecutor())
.build()
}
}
Paging Library 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 = QuestionsPagedAdapter()
viewModel.showQuestions().observe(this, Observer {
(list_of_questions.adapter as QuestionsPagedAdapter).submitList(it)
})
viewModel.showMessage().observe(this, Observer {
Snackbar.make(list_of_questions_container, it, Snackbar.LENGTH_LONG).show()
})
viewModel.init()
}
Wrap Up!
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
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
Thanks!
#commitconf
https://github.com/dgomez-developer/qa-client
dgomez.developer@gmail.com
@dgomezdebora
www.linkedin.com/in/deboragomezbertoli

More Related Content

Cleaning your architecture with android architecture components

  • 1. D└bora Gomez @dgomezdebora 22 noviembre 2019 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
  • 4. Disclaimer This talk is not about explaining Clean Architecture. Disclaimer! @dgomezdebora#commitconf
  • 5. Disclaimer RxJava is out of scope Disclaimer! #commitconf @dgomezdebora
  • 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.
  • 14. Implementation - Dependencies #commitconf @dgomezdebora implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
  • 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
  • 29. Implementation - Dependencies #commitconf @dgomezdebora implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version"
  • 30. Implementation - Entity #commitconf @dgomezdebora @Entity(tableName = "question") class QuestionEntity ( @PrimaryKey val id: String, val question: String, val contact: String? )
  • 31. Implementation - DAO #commitconf @dgomezdebora @Dao interface QuestionsDao { @Query("SELECT * from question") fun getAll(): LiveData<List<QuestionEntity>> @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(map: List<QuestionEntity>) }
  • 32. Implementation - RoomDatabase #commitconf @dgomezdebora @Database(entities = arrayOf(QuestionEntity::class), version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun questionsDao(): QuestionsDao }
  • 33. Implementation - Datasource #commitconf @dgomezdebora class QuestionsLocalDataSource(val db: AppDatabase) { fun getAllQuestions() = Transformations.map(db.questionsDao().getAll()) { input -> input.map{ dbquestion -> Question(dbquestion.id, dbquestion.question, dbquestion.contact) } } fun updateQuestions(questions: List<Question>) { db.questionsDao().insertAll(questions.map { QuestionEntity(it.id, it.question, it.contact) }) } }
  • 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
  • 35. Are you still there??
  • 36. Show me the code #commitconf @dgomezdebora https://github.com/dgomez-developer/qa-clien t
  • 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>() {
  • 40. Paging Library DataSource #commitconf @dgomezdebora override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Question>) { val questionsCall = api.getQuestions(requestParams.page, requestParams.pageSize) val response = questionsCall.execute() return if (response.isSuccessful) { callback.onResult( response.body()?.toMutableList() ?: mutableListOf(), null, requestParams.page + 1) } else { errorLD.postValue(Throwable()) } }
  • 41. Paging Library DataSource #commitconf @dgomezdebora override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Question>) { val questionsCall = api.getQuestions(params.key, params.requestedLoadSize) val response = questionsCall.execute() return if (response.isSuccessful) { if (params.requestedLoadSize >= (params.key + 1)) { callback.onResult(response.body()?.toMutableList() ?: mutableListOf(), params.key + 1) } else { callback.onResult(response.body()?.toMutableList() ?: mutableListOf(), null) } } else {errorLD.postValue(Throwable())} }
  • 42. Paging Library Factory #commitconf @dgomezdebora class QuestionsPagedFactory( private val api: QuestionsApi, val threadExecutor: ThreadExecutor) : DataSource.Factory<Int, Question>() { private val mutableErrorLD by lazy { MutableLiveData<Throwable>() } override fun create(): DataSource<Int, Question> { return QuestionsPagedDataSource(api, QuestionsRequestParams(), mutableErrorLD) } }
  • 43. Paging Library Repository #commitconf @dgomezdebora class QuestionsRepositoryImpl(...) : QuestionsRepository { override fun getQuestionsFromServer(): LiveData<PagedList<Question>> { val config: PagedList.Config = PagedList.Config.Builder() .setInitialLoadSizeHint(6) .setPageSize(6) .setEnablePlaceholders(false) .setPrefetchDistance(6) .build() return LivePagedListBuilder(questionsNetworkDataSource, config) .setFetchExecutor(questionsNetworkDataSource.threadExecutor.getThreadExecutor()) .build() } }
  • 44. Paging Library 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 = QuestionsPagedAdapter() viewModel.showQuestions().observe(this, Observer { (list_of_questions.adapter as QuestionsPagedAdapter).submitList(it) }) viewModel.showMessage().observe(this, Observer { Snackbar.make(list_of_questions_container, it, Snackbar.LENGTH_LONG).show() }) viewModel.init() }
  • 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