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. Phil Shadlyn
    @physphil
    I Wrote an App with Architecture
    Components

    View Slide

  2. Architecture Components

    View Slide

  3. - Google
    “A collection of libraries that help
    you design robust, testable, and
    maintainable apps.”

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  9. Don’t listen to me.

    View Slide

  10. Don’t only listen to me.

    View Slide

  11. A Simple Reminder App

    View Slide

  12. ViewModel

    - Hold view data
    - React to user input
    Room

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

    View Slide

  13. ViewModel

    - Hold view data
    - React to user input
    Room

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

    View Slide

  14. LiveData

    View Slide

  15. LiveData

    View Slide

  16. • Subscribe for data updates
    LiveData

    View Slide

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

    View Slide

  18. • Subscribe for data updates
    • Lifecycle aware - no more unsubscribe(this)
    • MutableLiveData, SingleLiveEvent are useful subclasses
    LiveData

    View Slide

  19. val reminderList: LiveData> = repo.getReminderList()
    MainActivityViewModel.kt

    View Slide

  20. val reminderList: LiveData> = 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

    View Slide

  21. val reminderList: LiveData> = 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

    View Slide

  22. MutableLiveData

    View Slide

  23. private val spinnerVisibility = MutableLiveData()
    private val emptyVisibility = MutableLiveData()
    MainActivityViewModel.kt

    View Slide

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

    View Slide

  25. private val spinnerVisibility = MutableLiveData()
    private val emptyVisibility = MutableLiveData()
    MainActivityViewModel.kt
    // Mutable LiveData accessor methods - everyone loves ensuring immutability!
    fun getSpinnerVisibility(): LiveData = spinnerVisibility
    fun getEmptyVisibility(): LiveData = emptyVisibility
    fun reminderListUpdated(reminders: List) {
    spinnerVisibility.value = false
    emptyVisibility.value = reminders.isEmpty()
    }

    View Slide

  26. SingleLiveEvent

    View Slide

  27. // Single events - subscribe for UI updates
    val clearNotificationEvent = SingleLiveEvent()
    val showDeleteConfirmationEvent = SingleLiveEvent()
    val showDeleteAllConfirmationEvent = SingleLiveEvent()
    MainActivityViewModel.kt

    View Slide

  28. // Single events - subscribe for UI updates
    val clearNotificationEvent = SingleLiveEvent()
    val showDeleteConfirmationEvent = SingleLiveEvent()
    val showDeleteAllConfirmationEvent = SingleLiveEvent()
    MainActivityViewModel.kt
    fun deleteAllReminders() {
    scheduler.cancelAllJobs()
    repo.deleteAllReminders()
    clearNotificationEvent.postValue(notificationId)
    }

    View Slide

  29. Testing?

    View Slide

  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

    View Slide

  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

    View Slide

  32. Pros

    View Slide

  33. • Simple to implement
    Pros

    View Slide

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

    View Slide

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

    View Slide

  36. Gotchas

    View Slide

  37. • Create getters for MutableLiveData
    Gotchas

    View Slide

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

    View Slide

  39. • Create getters for MutableLiveData
    • SingleLiveEvent for one-time updates
    • Limited compared to RxJava
    Gotchas

    View Slide

  40. Should I use LiveData?

    View Slide

  41. Should I use LiveData?

    View Slide

  42. ViewModel

    View Slide

  43. ViewModel

    View Slide

  44. • Contains LiveData and logic required for view
    ViewModel

    View Slide

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

    View Slide

  46. • Contains LiveData and logic required for view
    • React to user input
    • Survive configuration changes!
    ViewModel

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  50. class MainActivityViewModel(private val repo: ReminderRepo, private val scheduler: JobRequestScheduler) : ViewModel() {
    // LiveData objects - subscribe for UI updates
    val reminderList: LiveData> = repo.getActiveReminders()
    private val spinnerVisibility = MutableLiveData()
    private val emptyVisibility = MutableLiveData()
    // Single events - subscribe for UI updates
    val clearNotificationEvent = SingleLiveEvent()
    val showDeleteConfirmationEvent = SingleLiveEvent()
    val showDeleteAllConfirmationEvent = SingleLiveEvent()
    // Mutable LiveData accessor methods - everyone loves ensuring immutability!
    fun getSpinnerVisibility(): LiveData = spinnerVisibility
    fun getEmptyVisibility(): LiveData = emptyVisibility
    // Public user-driven methods
    fun reminderListUpdated(reminders: List) {
    spinnerVisibility.value = false
    emptyVisibility.value = reminders.isEmpty()
    }
    fun confirmDeleteAllReminders() {
    showDeleteAllConfirmationEvent.call()
    }
    fun deleteAllReminders() {
    scheduler.cancelAllJobs()
    repo.deleteAllReminders()
    clearNotificationEvent.call()
    }

    View Slide

  51. override fun onCreate(savedInstanceState: Bundle?) {
    viewModel = ViewModelProviders.of(this, MainActivityViewModelFactory(repo, scheduler))
    .get(MainActivityViewModel::class.java)
    }
    MainActivity.kt

    View Slide

  52. class MainActivityViewModelFactory(private val repo: ReminderRepo, private val scheduler: JobRequestScheduler) :
    ViewModelProvider.Factory {
    override fun create(modelClass: Class): 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

    View Slide

  53. class MainActivityViewModelFactory(private val repo: ReminderRepo, private val scheduler: JobRequestScheduler) :
    ViewModelProvider.Factory {
    override fun create(modelClass: Class): 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

    View Slide

  54. … and, Testing?

    View Slide

  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

    View Slide

  56. Pros

    View Slide

  57. • Android supported architecture
    Pros

    View Slide

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

    View Slide

  59. • Android supported architecture
    • Persist config changes!!
    • Easy JVM Tests
    Pros

    View Slide

  60. Gotchas

    View Slide

  61. Gotchas
    • Don’t instantiate directly

    View Slide

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

    View Slide

  63. Gotchas
    • Don’t instantiate directly
    • Implement ViewModelProvider.Factory for constructor
    parameters
    • Inherit from AndroidViewModel if you need (Application) context

    View Slide

  64. Should I use ViewModels?

    View Slide

  65. Should I use ViewModels?

    View Slide

  66. Room

    View Slide

  67. Room

    View Slide

  68. • Communicates with SQLite database
    Room

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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
    }
    ReminderDao.kt

    View Slide

  74. How about testing?

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  78. Pros

    View Slide

  79. • No more SQLite APIs
    Pros

    View Slide

  80. • No more SQLite APIs
    • Easy testing
    Pros

    View Slide

  81. • No more SQLite APIs
    • Easy testing
    • Autoupdates with LiveData or RxJava
    Pros

    View Slide

  82. Gotchas

    View Slide

  83. • Require Android device/emulator for testing
    Gotchas

    View Slide

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

    View Slide

  85. • Require Android device/emulator for testing
    • Room entities require public getters/setters
    • Handle threading yourself (unless subscribing to LiveData)
    Gotchas

    View Slide

  86. Should I use Room?

    View Slide

  87. Should I use Room?

    View Slide

  88. LiveData

    View Slide

  89. LiveData
    RxJava2

    View Slide

  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>
    }
    ReminderDao.kt

    View Slide

  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>
    }
    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>
    }

    View Slide

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

    View Slide

  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
    })

    View Slide

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

    View Slide

  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()
    }

    View Slide

  96. And of course, testing…

    View Slide

  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

    View Slide

  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 })
    }

    View Slide

  99. LiveData with RxJava

    View Slide

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

    View Slide

  101. Summary

    View Slide

  102. Pros

    View Slide

  103. • Lifecycle-aware, officially supported architecture
    Pros

    View Slide

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

    View Slide

  105. • Lifecycle-aware, officially supported architecture
    • LiveData autoupdating!
    • Can use individually if desired
    Pros

    View Slide

  106. Gotchas

    View Slide

  107. • SingleLiveEvent, LiveDataTestUtil not included
    Gotchas

    View Slide

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

    View Slide

  109. • SingleLiveEvent, LiveDataTestUtil not included
    • ViewModelProvider.Factory documentation not great
    • Implement threading for Room
    Gotchas

    View Slide

  110. - Google
    “A collection of libraries that help
    you design robust, testable, and
    maintainable apps.”

    View Slide

  111. Should I use Architecture
    Components?

    View Slide

  112. Should I use Architecture
    Components?

    View Slide

  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/

    View Slide

  114. Thanks!
    Phil Shadlyn
    @physphil

    View Slide