I Wrote an App with Architecture Components

I Wrote an App with Architecture Components

Architecture Components were announced to much fanfare at Google I/O 2017, as it finally gave Android developers some official guidance on how to architect their applications. It also gave us tools to make managing lifecycle changes much easier, and to persist data without writing lots of annoying SQLite boilerplate code. By now we’ve all read the documentation and tutorials, but how easy is it to use these intriguing new tools in a production application? (Spoiler alert: they’re pretty helpful)

I developed an app from scratch using all of the new Architecture Components – ViewModels and an MVVM architecture, Room to persist all the local data, and LiveData objects to easily and safely update the UI with automatic lifecycle handling. Join us as we discuss in detail what it’s like to use these new Architecture Components in a real world situation – we’ll talk about the problems they solved, the problems they created, and whether or not they’re the right choice for your next project.

8b0334cfb942bef4267f86844fd941e5?s=128

Phil Shadlyn

March 21, 2018
Tweet

Transcript

  1. Phil Shadlyn @physphil I Wrote an App with Architecture Components

  2. Architecture Components

  3. - Google “A collection of libraries that help you design

    robust, testable, and maintainable apps.”
  4. https://developer.android.com/topic/libraries/architecture/guide.html

  5. LiveData https://developer.android.com/topic/libraries/architecture/guide.html

  6. LiveData ViewModels https://developer.android.com/topic/libraries/architecture/guide.html

  7. LiveData ViewModels Room https://developer.android.com/topic/libraries/architecture/guide.html

  8. LiveData ViewModels Room Paging Library https://developer.android.com/topic/libraries/architecture/guide.html

  9. Don’t listen to me.

  10. Don’t only listen to me.

  11. A Simple Reminder App

  12. ViewModel
 - Hold view data - React to user input

    Room
 - Retrieves data from SQLite - Push updates to LiveData ReminderRepo.kt LiveData Exposed methods
  13. ViewModel
 - Hold view data - React to user input

    Room
 - Retrieves data from SQLite - Push updates to LiveData ReminderRepo.kt LiveData Exposed methods
  14. LiveData

  15. LiveData

  16. • Subscribe for data updates LiveData

  17. • Subscribe for data updates • Lifecycle aware - no

    more unsubscribe(this) LiveData
  18. • Subscribe for data updates • Lifecycle aware - no

    more unsubscribe(this) • MutableLiveData, SingleLiveEvent are useful subclasses LiveData
  19. val reminderList: LiveData<List<Reminder>> = repo.getReminderList() MainActivityViewModel.kt

  20. val reminderList: LiveData<List<Reminder>> = repo.getReminderList() override fun onCreate(savedInstanceState: Bundle?) {

    viewModel.reminderList.observe(this, Observer { reminders -> reminders?.let { adapter.setReminderList(it) viewModel.reminderListUpdated(it) fab.show() } }) } MainActivity.kt MainActivityViewModel.kt
  21. val reminderList: LiveData<List<Reminder>> = repo.getReminderList() override fun onCreate(savedInstanceState: Bundle?) {

    viewModel.reminderList.observe(this, Observer { reminders -> reminders?.let { adapter.setReminderList(it) viewModel.reminderListUpdated(it) fab.show() } }) } MainActivity.kt MainActivityViewModel.kt
  22. MutableLiveData

  23. private val spinnerVisibility = MutableLiveData<Boolean>() private val emptyVisibility = MutableLiveData<Boolean>()

    MainActivityViewModel.kt
  24. private val spinnerVisibility = MutableLiveData<Boolean>() private val emptyVisibility = MutableLiveData<Boolean>()

    MainActivityViewModel.kt // Mutable LiveData accessor methods - everyone loves ensuring immutability! fun getSpinnerVisibility(): LiveData<Boolean> = spinnerVisibility fun getEmptyVisibility(): LiveData<Boolean> = emptyVisibility
  25. private val spinnerVisibility = MutableLiveData<Boolean>() private val emptyVisibility = MutableLiveData<Boolean>()

    MainActivityViewModel.kt // Mutable LiveData accessor methods - everyone loves ensuring immutability! fun getSpinnerVisibility(): LiveData<Boolean> = spinnerVisibility fun getEmptyVisibility(): LiveData<Boolean> = emptyVisibility fun reminderListUpdated(reminders: List<Reminder>) { spinnerVisibility.value = false emptyVisibility.value = reminders.isEmpty() }
  26. SingleLiveEvent

  27. // Single events - subscribe for UI updates val clearNotificationEvent

    = SingleLiveEvent<Int?>() val showDeleteConfirmationEvent = SingleLiveEvent<Unit>() val showDeleteAllConfirmationEvent = SingleLiveEvent<Unit>() MainActivityViewModel.kt
  28. // Single events - subscribe for UI updates val clearNotificationEvent

    = SingleLiveEvent<Int?>() val showDeleteConfirmationEvent = SingleLiveEvent<Unit>() val showDeleteAllConfirmationEvent = SingleLiveEvent<Unit>() MainActivityViewModel.kt fun deleteAllReminders() { scheduler.cancelAllJobs() repo.deleteAllReminders() clearNotificationEvent.postValue(notificationId) }
  29. Testing?

  30. @Test fun testGetReminderById() { val reminder = Reminder(title = TITLE)

    val id = reminder.id dao.insertReminder(reminder) dao.insertReminder(Reminder()) dao.insertReminder(Reminder()) val result = LiveDataTestUtil.getValue(dao.getReminderById(id)) assertTrue(result.id == id) assertTrue(result.title == TITLE) } ReminderDaoTest.kt
  31. @Test fun testGetReminderById() { val reminder = Reminder(title = TITLE)

    val id = reminder.id dao.insertReminder(reminder) dao.insertReminder(Reminder()) dao.insertReminder(Reminder()) val result = LiveDataTestUtil.getValue(dao.getReminderById(id)) assertTrue(result.id == id) assertTrue(result.title == TITLE) } ReminderDaoTest.kt
  32. Pros

  33. • Simple to implement Pros

  34. • Simple to implement • Automatically updated with backing data

    change (from Room) Pros
  35. • Simple to implement • Automatically updated with backing data

    change (from Room) • Lifecycle aware Pros
  36. Gotchas

  37. • Create getters for MutableLiveData Gotchas

  38. • Create getters for MutableLiveData • SingleLiveEvent for one-time updates

    Gotchas
  39. • Create getters for MutableLiveData • SingleLiveEvent for one-time updates

    • Limited compared to RxJava Gotchas
  40. Should I use LiveData?

  41. Should I use LiveData?

  42. ViewModel

  43. ViewModel

  44. • Contains LiveData and logic required for view ViewModel

  45. • Contains LiveData and logic required for view • React

    to user input ViewModel
  46. • Contains LiveData and logic required for view • React

    to user input • Survive configuration changes! ViewModel
  47. class MainActivityViewModel(private val repo: ReminderRepo, private val scheduler: JobRequestScheduler) :

    ViewModel() { // LiveData objects - subscribe for UI updates val reminderList: LiveData<List<Reminder>> = repo.getActiveReminders() private val spinnerVisibility = MutableLiveData<Boolean>() private val emptyVisibility = MutableLiveData<Boolean>()
  48. class MainActivityViewModel(private val repo: ReminderRepo, private val scheduler: JobRequestScheduler) :

    ViewModel() { // LiveData objects - subscribe for UI updates val reminderList: LiveData<List<Reminder>> = repo.getActiveReminders() private val spinnerVisibility = MutableLiveData<Boolean>() private val emptyVisibility = MutableLiveData<Boolean>() // Single events - subscribe for UI updates val clearNotificationEvent = SingleLiveEvent<Int?>() val showDeleteConfirmationEvent = SingleLiveEvent<Unit>() val showDeleteAllConfirmationEvent = SingleLiveEvent<Unit>()
  49. class MainActivityViewModel(private val repo: ReminderRepo, private val scheduler: JobRequestScheduler) :

    ViewModel() { // LiveData objects - subscribe for UI updates val reminderList: LiveData<List<Reminder>> = repo.getActiveReminders() private val spinnerVisibility = MutableLiveData<Boolean>() private val emptyVisibility = MutableLiveData<Boolean>() // Single events - subscribe for UI updates val clearNotificationEvent = SingleLiveEvent<Int?>() val showDeleteConfirmationEvent = SingleLiveEvent<Unit>() val showDeleteAllConfirmationEvent = SingleLiveEvent<Unit>() // Mutable LiveData accessor methods - everyone loves ensuring immutability! fun getSpinnerVisibility(): LiveData<Boolean> = spinnerVisibility fun getEmptyVisibility(): LiveData<Boolean> = emptyVisibility
  50. class MainActivityViewModel(private val repo: ReminderRepo, private val scheduler: JobRequestScheduler) :

    ViewModel() { // LiveData objects - subscribe for UI updates val reminderList: LiveData<List<Reminder>> = repo.getActiveReminders() private val spinnerVisibility = MutableLiveData<Boolean>() private val emptyVisibility = MutableLiveData<Boolean>() // Single events - subscribe for UI updates val clearNotificationEvent = SingleLiveEvent<Int?>() val showDeleteConfirmationEvent = SingleLiveEvent<Unit>() val showDeleteAllConfirmationEvent = SingleLiveEvent<Unit>() // Mutable LiveData accessor methods - everyone loves ensuring immutability! fun getSpinnerVisibility(): LiveData<Boolean> = spinnerVisibility fun getEmptyVisibility(): LiveData<Boolean> = emptyVisibility // Public user-driven methods fun reminderListUpdated(reminders: List<Reminder>) { spinnerVisibility.value = false emptyVisibility.value = reminders.isEmpty() } fun confirmDeleteAllReminders() { showDeleteAllConfirmationEvent.call() } fun deleteAllReminders() { scheduler.cancelAllJobs() repo.deleteAllReminders() clearNotificationEvent.call() }
  51. override fun onCreate(savedInstanceState: Bundle?) { viewModel = ViewModelProviders.of(this, MainActivityViewModelFactory(repo, scheduler))

    .get(MainActivityViewModel::class.java) } MainActivity.kt
  52. class MainActivityViewModelFactory(private val repo: ReminderRepo, private val scheduler: JobRequestScheduler) :

    ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(MainActivityViewModel::class.java)) { return MainActivityViewModel(repo, scheduler) as T } throw IllegalArgumentException("Cannot instantiate ViewModel class with those arguments") } } MainActivityViewModelFactory.kt override fun onCreate(savedInstanceState: Bundle?) { viewModel = ViewModelProviders.of(this, MainActivityViewModelFactory(repo, scheduler)) .get(MainActivityViewModel::class.java) } MainActivity.kt
  53. class MainActivityViewModelFactory(private val repo: ReminderRepo, private val scheduler: JobRequestScheduler) :

    ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(MainActivityViewModel::class.java)) { return MainActivityViewModel(repo, scheduler) as T } throw IllegalArgumentException("Cannot instantiate ViewModel class with those arguments") } } MainActivityViewModelFactory.kt override fun onCreate(savedInstanceState: Bundle?) { viewModel = ViewModelProviders.of(this, MainActivityViewModelFactory(repo, scheduler)) .get(MainActivityViewModel::class.java) } MainActivity.kt
  54. … and, Testing?

  55. @Test fun testReminderListUpdated() { val reminders = listOf(Reminder(), Reminder()) `when`(repo.getActiveReminders()).thenReturn(Flowable.just(reminders))

    viewModel = MainActivityViewModel(repo, scheduler) viewModel.reminderListUpdated(reminders) viewModel.getSpinnerVisibility().observeForever({ assertEquals(it, false) }) viewModel.getEmptyVisibility().observeForever({ assertEquals(it, false) }) } @Test fun testDeleteReminder() { val reminder = Reminder() viewModel.clearNotificationEvent.observeForever({ id -> assertEquals(id, reminder.notificationId) }) viewModel.confirmDeleteReminder(reminder) viewModel.deleteReminder() verify(scheduler).cancelJob(reminder.externalId) verify(repo).deleteReminder(reminder) } MainActivityViewModelTest.kt
  56. Pros

  57. • Android supported architecture Pros

  58. • Android supported architecture • Persist config changes!! Pros

  59. • Android supported architecture • Persist config changes!! • Easy

    JVM Tests Pros
  60. Gotchas

  61. Gotchas • Don’t instantiate directly

  62. Gotchas • Don’t instantiate directly • Implement ViewModelProvider.Factory for constructor

    parameters
  63. Gotchas • Don’t instantiate directly • Implement ViewModelProvider.Factory for constructor

    parameters • Inherit from AndroidViewModel if you need (Application) context
  64. Should I use ViewModels?

  65. Should I use ViewModels?

  66. Room

  67. Room

  68. • Communicates with SQLite database Room

  69. • Communicates with SQLite database • Pushes updates with LiveData

    or RxJava Room
  70. • Communicates with SQLite database • Pushes updates with LiveData

    or RxJava • Access through repository Room
  71. @Entity(tableName = TABLE_REMINDERS) data class Reminder(@ColumnInfo(name = REMINDER_COLUMN_TITLE) var title:

    String = "", @ColumnInfo(name = REMINDER_COLUMN_TEXT) var body: String = "", @ColumnInfo(name = REMINDER_COLUMN_TIME) var time: Calendar = Calendar.getInstance(), @ColumnInfo(name = REMINDER_COLUMN_RECURRENCE) var recurrence: Recurrence = Recurrence.NONE, @ColumnInfo(name = REMINDER_COLUMN_EXTERNAL_ID) var externalId: Int = 0, @ColumnInfo(name = REMINDER_COLUMN_NOTIFICATION_ID) var notificationId: Int = 0) { @PrimaryKey @ColumnInfo(name = REMINDER_COLUMN_ID) var id: String = UUID.randomUUID().toString() } Reminder.kt
  72. @Entity(tableName = TABLE_REMINDERS) data class Reminder(@ColumnInfo(name = REMINDER_COLUMN_TITLE) var title:

    String = "", @ColumnInfo(name = REMINDER_COLUMN_TEXT) var body: String = "", @ColumnInfo(name = REMINDER_COLUMN_TIME) var time: Calendar = Calendar.getInstance(), @ColumnInfo(name = REMINDER_COLUMN_RECURRENCE) var recurrence: Recurrence = Recurrence.NONE, @ColumnInfo(name = REMINDER_COLUMN_EXTERNAL_ID) var externalId: Int = 0, @ColumnInfo(name = REMINDER_COLUMN_NOTIFICATION_ID) var notificationId: Int = 0) { @PrimaryKey @ColumnInfo(name = REMINDER_COLUMN_ID) var id: String = UUID.randomUUID().toString() } Reminder.kt
  73. @Dao interface ReminderDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertReminder(reminder: Reminder)

    @Update fun updateReminder(reminder: Reminder) @Delete fun deleteReminder(reminder: Reminder) @Query("SELECT * FROM $TABLE_REMINDERS " + "WHERE $REMINDER_COLUMN_ID = :id") fun getReminderById(id: String): LiveData<Reminder> } ReminderDao.kt
  74. How about testing?

  75. @RunWith(AndroidJUnit4::class) class ReminderDaoTest { @Before fun createDb() { db =

    Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(), AppDatabase::class.java) .allowMainThreadQueries() .build() dao = db.reminderDao() } @Test fun testGetReminderById() { val reminder = Reminder(title = TITLE) val id = reminder.id dao.insertReminder(reminder) dao.insertReminder(Reminder()) dao.insertReminder(Reminder()) val result = LiveDataTestUtil.getValue(dao.getReminderById(id)) assertTrue(result.id == id) assertTrue(result.title == TITLE) } ReminderDaoTest.kt
  76. @RunWith(AndroidJUnit4::class) class ReminderDaoTest { @Before fun createDb() { db =

    Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(), AppDatabase::class.java) .allowMainThreadQueries() .build() dao = db.reminderDao() } @Test fun testGetReminderById() { val reminder = Reminder(title = TITLE) val id = reminder.id dao.insertReminder(reminder) dao.insertReminder(Reminder()) dao.insertReminder(Reminder()) val result = LiveDataTestUtil.getValue(dao.getReminderById(id)) assertTrue(result.id == id) assertTrue(result.title == TITLE) } ReminderDaoTest.kt
  77. @RunWith(AndroidJUnit4::class) class ReminderDaoTest { @Before fun createDb() { db =

    Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(), AppDatabase::class.java) .allowMainThreadQueries() .build() dao = db.reminderDao() } @Test fun testGetReminderById() { val reminder = Reminder(title = TITLE) val id = reminder.id dao.insertReminder(reminder) dao.insertReminder(Reminder()) dao.insertReminder(Reminder()) val result = LiveDataTestUtil.getValue(dao.getReminderById(id)) assertTrue(result.id == id) assertTrue(result.title == TITLE) } ReminderDaoTest.kt
  78. Pros

  79. • No more SQLite APIs Pros

  80. • No more SQLite APIs • Easy testing Pros

  81. • No more SQLite APIs • Easy testing • Autoupdates

    with LiveData or RxJava Pros
  82. Gotchas

  83. • Require Android device/emulator for testing Gotchas

  84. • Require Android device/emulator for testing • Room entities require

    public getters/setters Gotchas
  85. • Require Android device/emulator for testing • Room entities require

    public getters/setters • Handle threading yourself (unless subscribing to LiveData) Gotchas
  86. Should I use Room?

  87. Should I use Room?

  88. LiveData

  89. LiveData RxJava2

  90. @Dao interface ReminderDao { @Query("SELECT * FROM $TABLE_REMINDERS " +

    "WHERE $REMINDER_COLUMN_TIME > :time " + "ORDER BY $REMINDER_COLUMN_TIME ASC") fun getAllReminders(time: Long = System.currentTimeMillis()): LiveData<List<Reminder>> } ReminderDao.kt
  91. @Dao interface ReminderDao { @Query("SELECT * FROM $TABLE_REMINDERS " +

    "WHERE $REMINDER_COLUMN_TIME > :time " + "ORDER BY $REMINDER_COLUMN_TIME ASC") fun getAllReminders(time: Long = System.currentTimeMillis()): LiveData<List<Reminder>> } ReminderDao.kt @Dao interface ReminderDao { @Query("SELECT * FROM $TABLE_REMINDERS " + "WHERE $REMINDER_COLUMN_TIME > :time " + "ORDER BY $REMINDER_COLUMN_TIME ASC") fun getAllReminders(time: Long = System.currentTimeMillis()): Flowable<List<Reminder>> }
  92. viewModel.reminderList.observe(this, Observer { reminders -> reminders?.let { adapter.setReminderList(it) viewModel.reminderListUpdated(it) fab.show()

    } }) MainActivity.kt
  93. viewModel.reminderList.observe(this, Observer { reminders -> reminders?.let { adapter.setReminderList(it) viewModel.reminderListUpdated(it) fab.show()

    } }) MainActivity.kt viewModel.reminderList.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ // onNext adapter.setReminderList(it) viewModel.reminderListUpdated(it) fab.show() }, { // onError })
  94. fun insertReminder(reminder: Reminder) { Thread(Runnable { dao.insertReminder(reminder) }).start() } ReminderRepo.kt

  95. fun insertReminder(reminder: Reminder) { Thread(Runnable { dao.insertReminder(reminder) }).start() } ReminderRepo.kt

    fun insertReminder(reminder: Reminder): Disposable { return Completable.fromAction { dao.insertReminder(reminder) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe() }
  96. And of course, testing…

  97. @Test fun testGetReminderList() { val time = Calendar.getInstance() time.timeInMillis =

    System.currentTimeMillis() + 60000 * 5 // set each reminder 5 minutes in future dao.insertReminder(Reminder(time = time)) dao.insertReminder(Reminder(time = time)) val reminders = LiveDataTestUtil.getValue(dao.getAllReminders()) assertTrue(reminders.size == 2) } ReminderDaoTest.kt
  98. @Test fun testGetReminderList() { val time = Calendar.getInstance() time.timeInMillis =

    System.currentTimeMillis() + 60000 * 5 // set each reminder 5 minutes in future dao.insertReminder(Reminder(time = time)) dao.insertReminder(Reminder(time = time)) val reminders = LiveDataTestUtil.getValue(dao.getAllReminders()) assertTrue(reminders.size == 2) } ReminderDaoTest.kt @Test fun testGetReminderList() { val time = Calendar.getInstance() time.timeInMillis = System.currentTimeMillis() + 60000 * 5 // set each reminder 5 minutes in future dao.insertReminder(Reminder(time = time)) dao.insertReminder(Reminder(time = time)) dao.getAllReminders() .test() .assertValue({ it.size == 2 }) }
  99. LiveData with RxJava

  100. • RxJava for database operations • LiveData for data controlled

    locally LiveData with RxJava
  101. Summary

  102. Pros

  103. • Lifecycle-aware, officially supported architecture Pros

  104. • Lifecycle-aware, officially supported architecture • LiveData autoupdating! Pros

  105. • Lifecycle-aware, officially supported architecture • LiveData autoupdating! • Can

    use individually if desired Pros
  106. Gotchas

  107. • SingleLiveEvent, LiveDataTestUtil not included Gotchas

  108. • SingleLiveEvent, LiveDataTestUtil not included • ViewModelProvider.Factory documentation not great

    Gotchas
  109. • SingleLiveEvent, LiveDataTestUtil not included • ViewModelProvider.Factory documentation not great

    • Implement threading for Room Gotchas
  110. - Google “A collection of libraries that help you design

    robust, testable, and maintainable apps.”
  111. Should I use Architecture Components?

  112. Should I use Architecture Components?

  113. Want to help? https://github.com/physphil/RemindMe/ Need more info? https://developer.android.com/topic/libraries/architecture/index.html https://github.com/googlesamples/android-architecture-components http://fragmentedpodcast.com/episodes/115/

  114. Thanks! Phil Shadlyn @physphil