O slideshow foi denunciado.
Seu SlideShare está sendo baixado. ×

Cleaning your architecture with android architecture components

Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Próximos SlideShares
Android Architecture Components
Android Architecture Components
Carregando em…3
×

Confira estes a seguir

1 de 48 Anúncio
Anúncio

Mais Conteúdo rRelacionado

Diapositivos para si (20)

Semelhante a Cleaning your architecture with android architecture components (20)

Anúncio

Mais recentes (20)

Cleaning your architecture with android architecture components

  1. 1. Débora Gomez @dgomezdebora 22 noviembre 2019 CLEANING YOUR ARCHITECTURE WITH ANDROID ARCHITECTURE COMPONENTS
  2. 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
  3. 3. 01 02 03 04 Before Architecture components Presentation Domain Data
  4. 4. Disclaimer This talk is not about explaining Clean Architecture. Disclaimer! @dgomezdebora#commitconf
  5. 5. Disclaimer RxJava is out of scope Disclaimer! #commitconf @dgomezdebora
  6. 6. Before Architecture components 01
  7. 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. 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}
  9. 9. Implementation https://github.com/dgomez-developer/qa-c lient/tree/feature/example-with-no-archite cture-components #commitconf @dgomezdebora
  10. 10. Presentation Layer02
  11. 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. 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. 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. 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. 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. 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. 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. 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. 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
  20. 20. Easy right? #commitconf @dgomezdebora
  21. 21. Domain Layer03
  22. 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. 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. 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. 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. 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
  27. 27. 04 Data Layer
  28. 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. 29. Implementation - Dependencies #commitconf @dgomezdebora implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version"
  30. 30. Implementation - Entity #commitconf @dgomezdebora @Entity(tableName = "question") class QuestionEntity ( @PrimaryKey val id: String, val question: String, val contact: String? )
  31. 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. 32. Implementation - RoomDatabase #commitconf @dgomezdebora @Database(entities = arrayOf(QuestionEntity::class), version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun questionsDao(): QuestionsDao }
  33. 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. 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. 35. Are you still there??
  36. 36. Show me the code #commitconf @dgomezdebora https://github.com/dgomez-developer/qa-clien t
  37. 37. Data Layer04 Pagination use case
  38. 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. 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. 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. 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. 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. 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. 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() }
  45. 45. Wrap Up!
  46. 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. 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
  48. 48. Thanks! #commitconf https://github.com/dgomez-developer/qa-client dgomez.developer@gmail.com @dgomezdebora www.linkedin.com/in/deboragomezbertoli

×