SlideShare 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

What's hot

Intro to React
Intro to ReactIntro to React
Intro to React
Justin Reock
 
Introduction to ReactJS
Introduction to ReactJSIntroduction to ReactJS
Introduction to ReactJS
Knoldus Inc.
 
React Js Simplified
React Js SimplifiedReact Js Simplified
React Js Simplified
Sunil Yadav
 
[Final] ReactJS presentation
[Final] ReactJS presentation[Final] ReactJS presentation
[Final] ReactJS presentation洪 鹏发
 
React – Structure Container Component In Meteor
 React – Structure Container Component In Meteor React – Structure Container Component In Meteor
React – Structure Container Component In Meteor
Designveloper
 
React JS: A Secret Preview
React JS: A Secret PreviewReact JS: A Secret Preview
React JS: A Secret Preview
valuebound
 
Introduction to React JS for beginners
Introduction to React JS for beginners Introduction to React JS for beginners
Introduction to React JS for beginners
Varun Raj
 
React workshop
React workshopReact workshop
React workshop
Imran Sayed
 
React js
React jsReact js
React js
Rajesh Kolla
 
ReactJs
ReactJsReactJs
ReactJs
LearningTech
 
Introduction to React JS
Introduction to React JSIntroduction to React JS
Introduction to React JS
Arno Lordkronos
 
Its time to React.js
Its time to React.jsIts time to React.js
Its time to React.js
Ritesh Mehrotra
 
React-js
React-jsReact-js
React-js
Avi Kedar
 
ReactJS
ReactJSReactJS
Reactjs
Reactjs Reactjs
Reactjs
Neha Sharma
 
React JS .NET
React JS .NETReact JS .NET
React JS .NET
Jennifer Estrada
 
React js for beginners
React js for beginnersReact js for beginners
React js for beginners
Alessandro Valenti
 
React.js
React.jsReact.js
Next stop: Spring 4
Next stop: Spring 4Next stop: Spring 4
Next stop: Spring 4
Oleg Tsal-Tsalko
 

What's hot (20)

Intro to React
Intro to ReactIntro to React
Intro to React
 
Introduction to ReactJS
Introduction to ReactJSIntroduction to ReactJS
Introduction to ReactJS
 
React Js Simplified
React Js SimplifiedReact Js Simplified
React Js Simplified
 
[Final] ReactJS presentation
[Final] ReactJS presentation[Final] ReactJS presentation
[Final] ReactJS presentation
 
React – Structure Container Component In Meteor
 React – Structure Container Component In Meteor React – Structure Container Component In Meteor
React – Structure Container Component In Meteor
 
React JS: A Secret Preview
React JS: A Secret PreviewReact JS: A Secret Preview
React JS: A Secret Preview
 
Introduction to React JS for beginners
Introduction to React JS for beginners Introduction to React JS for beginners
Introduction to React JS for beginners
 
React workshop
React workshopReact workshop
React workshop
 
React js
React jsReact js
React js
 
Intro react js
Intro react jsIntro react js
Intro react js
 
ReactJs
ReactJsReactJs
ReactJs
 
Introduction to React JS
Introduction to React JSIntroduction to React JS
Introduction to React JS
 
Its time to React.js
Its time to React.jsIts time to React.js
Its time to React.js
 
React-js
React-jsReact-js
React-js
 
ReactJS
ReactJSReactJS
ReactJS
 
Reactjs
Reactjs Reactjs
Reactjs
 
React JS .NET
React JS .NETReact JS .NET
React JS .NET
 
React js for beginners
React js for beginnersReact js for beginners
React js for beginners
 
React.js
React.jsReact.js
React.js
 
Next stop: Spring 4
Next stop: Spring 4Next stop: Spring 4
Next stop: Spring 4
 

Similar to Cleaning your architecture with android architecture components

Building Modern Apps using Android Architecture Components
Building Modern Apps using Android Architecture ComponentsBuilding Modern Apps using Android Architecture Components
Building Modern Apps using Android Architecture Components
Hassan Abid
 
Android and the Seven Dwarfs from Devox'15
Android and the Seven Dwarfs from Devox'15Android and the Seven Dwarfs from Devox'15
Android and the Seven Dwarfs from Devox'15
Murat Yener
 
My way to clean android - Android day salamanca edition
My way to clean android - Android day salamanca editionMy way to clean android - Android day salamanca edition
My way to clean android - Android day salamanca edition
Christian Panadero
 
Home Improvement: Architecture & Kotlin
Home Improvement: Architecture & KotlinHome Improvement: Architecture & Kotlin
Home Improvement: Architecture & Kotlin
Jorge Ortiz
 
Automatically Documenting Program Changes
Automatically Documenting Program ChangesAutomatically Documenting Program Changes
Automatically Documenting Program Changes
Ray Buse
 
Native Java with GraalVM
Native Java with GraalVMNative Java with GraalVM
Native Java with GraalVM
Sylvain Wallez
 
From Legacy to Hexagonal (An Unexpected Android Journey)
From Legacy to Hexagonal (An Unexpected Android Journey)From Legacy to Hexagonal (An Unexpected Android Journey)
From Legacy to Hexagonal (An Unexpected Android Journey)
Jose Manuel Pereira Garcia
 
Petcube epic battle: architecture vs product. UA Mobile 2017.
Petcube epic battle: architecture vs product. UA Mobile 2017.Petcube epic battle: architecture vs product. UA Mobile 2017.
Petcube epic battle: architecture vs product. UA Mobile 2017.
UA Mobile
 
MobiConf 2018 | Room: an SQLite object mapping library
MobiConf 2018 | Room: an SQLite object mapping library MobiConf 2018 | Room: an SQLite object mapping library
MobiConf 2018 | Room: an SQLite object mapping library
Magda Miu
 
Middy.js - A powerful Node.js middleware framework for your lambdas​
Middy.js - A powerful Node.js middleware framework for your lambdas​ Middy.js - A powerful Node.js middleware framework for your lambdas​
Middy.js - A powerful Node.js middleware framework for your lambdas​
Luciano Mammino
 
Clojure And Swing
Clojure And SwingClojure And Swing
Clojure And Swing
Skills Matter
 
GWT Extreme!
GWT Extreme!GWT Extreme!
GWT Extreme!
cromwellian
 
Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023
Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023
Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023
Nicolas HAAN
 
Droidcon ES '16 - How to fail going offline
Droidcon ES '16 - How to fail going offlineDroidcon ES '16 - How to fail going offline
Droidcon ES '16 - How to fail going offline
Javier de Pedro López
 
Building Web Apps Sanely - EclipseCon 2010
Building Web Apps Sanely - EclipseCon 2010Building Web Apps Sanely - EclipseCon 2010
Building Web Apps Sanely - EclipseCon 2010
Chris Ramsdale
 
10 ways to make your code rock
10 ways to make your code rock10 ways to make your code rock
10 ways to make your code rockmartincronje
 
Getting start Java EE Action-Based MVC with Thymeleaf
Getting start Java EE Action-Based MVC with ThymeleafGetting start Java EE Action-Based MVC with Thymeleaf
Getting start Java EE Action-Based MVC with Thymeleaf
Masatoshi Tada
 
當ZK遇見Front-End
當ZK遇見Front-End當ZK遇見Front-End
當ZK遇見Front-End
祁源 朱
 

Similar to Cleaning your architecture with android architecture components (20)

Building Modern Apps using Android Architecture Components
Building Modern Apps using Android Architecture ComponentsBuilding Modern Apps using Android Architecture Components
Building Modern Apps using Android Architecture Components
 
Android and the Seven Dwarfs from Devox'15
Android and the Seven Dwarfs from Devox'15Android and the Seven Dwarfs from Devox'15
Android and the Seven Dwarfs from Devox'15
 
Green dao
Green daoGreen dao
Green dao
 
My way to clean android - Android day salamanca edition
My way to clean android - Android day salamanca editionMy way to clean android - Android day salamanca edition
My way to clean android - Android day salamanca edition
 
Home Improvement: Architecture & Kotlin
Home Improvement: Architecture & KotlinHome Improvement: Architecture & Kotlin
Home Improvement: Architecture & Kotlin
 
Automatically Documenting Program Changes
Automatically Documenting Program ChangesAutomatically Documenting Program Changes
Automatically Documenting Program Changes
 
Native Java with GraalVM
Native Java with GraalVMNative Java with GraalVM
Native Java with GraalVM
 
From Legacy to Hexagonal (An Unexpected Android Journey)
From Legacy to Hexagonal (An Unexpected Android Journey)From Legacy to Hexagonal (An Unexpected Android Journey)
From Legacy to Hexagonal (An Unexpected Android Journey)
 
Petcube epic battle: architecture vs product. UA Mobile 2017.
Petcube epic battle: architecture vs product. UA Mobile 2017.Petcube epic battle: architecture vs product. UA Mobile 2017.
Petcube epic battle: architecture vs product. UA Mobile 2017.
 
MobiConf 2018 | Room: an SQLite object mapping library
MobiConf 2018 | Room: an SQLite object mapping library MobiConf 2018 | Room: an SQLite object mapping library
MobiConf 2018 | Room: an SQLite object mapping library
 
Middy.js - A powerful Node.js middleware framework for your lambdas​
Middy.js - A powerful Node.js middleware framework for your lambdas​ Middy.js - A powerful Node.js middleware framework for your lambdas​
Middy.js - A powerful Node.js middleware framework for your lambdas​
 
Clojure And Swing
Clojure And SwingClojure And Swing
Clojure And Swing
 
GWT Extreme!
GWT Extreme!GWT Extreme!
GWT Extreme!
 
Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023
Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023
Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023
 
Droidcon ES '16 - How to fail going offline
Droidcon ES '16 - How to fail going offlineDroidcon ES '16 - How to fail going offline
Droidcon ES '16 - How to fail going offline
 
Building Web Apps Sanely - EclipseCon 2010
Building Web Apps Sanely - EclipseCon 2010Building Web Apps Sanely - EclipseCon 2010
Building Web Apps Sanely - EclipseCon 2010
 
10 ways to make your code rock
10 ways to make your code rock10 ways to make your code rock
10 ways to make your code rock
 
Getting start Java EE Action-Based MVC with Thymeleaf
Getting start Java EE Action-Based MVC with ThymeleafGetting start Java EE Action-Based MVC with Thymeleaf
Getting start Java EE Action-Based MVC with Thymeleaf
 
From dot net_to_rails
From dot net_to_railsFrom dot net_to_rails
From dot net_to_rails
 
當ZK遇見Front-End
當ZK遇見Front-End當ZK遇見Front-End
當ZK遇見Front-End
 

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