Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Phil Shadlyn

March 21, 2018
Tweet

More Decks by Phil Shadlyn

Other Decks in Programming

Transcript

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

    robust, testable, and maintainable apps.”
  2. ViewModel
 - Hold view data - React to user input

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

    Room
 - Retrieves data from SQLite - Push updates to LiveData ReminderRepo.kt LiveData Exposed methods
  4. • Subscribe for data updates • Lifecycle aware - no

    more unsubscribe(this) • MutableLiveData, SingleLiveEvent are useful subclasses LiveData
  5. 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
  6. 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
  7. 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
  8. 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() }
  9. // Single events - subscribe for UI updates val clearNotificationEvent

    = SingleLiveEvent<Int?>() val showDeleteConfirmationEvent = SingleLiveEvent<Unit>() val showDeleteAllConfirmationEvent = SingleLiveEvent<Unit>() MainActivityViewModel.kt
  10. // 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) }
  11. @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
  12. @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
  13. • Simple to implement • Automatically updated with backing data

    change (from Room) • Lifecycle aware Pros
  14. • Contains LiveData and logic required for view • React

    to user input • Survive configuration changes! ViewModel
  15. 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>()
  16. 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>()
  17. 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
  18. 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() }
  19. 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
  20. 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
  21. @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
  22. Gotchas • Don’t instantiate directly • Implement ViewModelProvider.Factory for constructor

    parameters • Inherit from AndroidViewModel if you need (Application) context
  23. • Communicates with SQLite database • Pushes updates with LiveData

    or RxJava • Access through repository Room
  24. @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
  25. @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
  26. @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
  27. @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
  28. @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
  29. @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
  30. • Require Android device/emulator for testing • Room entities require

    public getters/setters • Handle threading yourself (unless subscribing to LiveData) Gotchas
  31. @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
  32. @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>> }
  33. 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 })
  34. 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() }
  35. @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
  36. @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 }) }
  37. - Google “A collection of libraries that help you design

    robust, testable, and maintainable apps.”