Slide 1

Slide 1 text

Phil Shadlyn @physphil I Wrote an App with Architecture Components

Slide 2

Slide 2 text

Architecture Components

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Don’t listen to me.

Slide 10

Slide 10 text

Don’t only listen to me.

Slide 11

Slide 11 text

A Simple Reminder App

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

LiveData

Slide 15

Slide 15 text

LiveData

Slide 16

Slide 16 text

• Subscribe for data updates LiveData

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

MutableLiveData

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

SingleLiveEvent

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Testing?

Slide 30

Slide 30 text

@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

Slide 31

Slide 31 text

@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

Slide 32

Slide 32 text

Pros

Slide 33

Slide 33 text

• Simple to implement Pros

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Gotchas

Slide 37

Slide 37 text

• Create getters for MutableLiveData Gotchas

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Should I use LiveData?

Slide 41

Slide 41 text

Should I use LiveData?

Slide 42

Slide 42 text

ViewModel

Slide 43

Slide 43 text

ViewModel

Slide 44

Slide 44 text

• Contains LiveData and logic required for view ViewModel

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

… and, Testing?

Slide 55

Slide 55 text

@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

Slide 56

Slide 56 text

Pros

Slide 57

Slide 57 text

• Android supported architecture Pros

Slide 58

Slide 58 text

• Android supported architecture • Persist config changes!! Pros

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

Gotchas

Slide 61

Slide 61 text

Gotchas • Don’t instantiate directly

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

Should I use ViewModels?

Slide 65

Slide 65 text

Should I use ViewModels?

Slide 66

Slide 66 text

Room

Slide 67

Slide 67 text

Room

Slide 68

Slide 68 text

• Communicates with SQLite database Room

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

@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

Slide 72

Slide 72 text

@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

Slide 73

Slide 73 text

@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

Slide 74

Slide 74 text

How about testing?

Slide 75

Slide 75 text

@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

Slide 76

Slide 76 text

@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

Slide 77

Slide 77 text

@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

Slide 78

Slide 78 text

Pros

Slide 79

Slide 79 text

• No more SQLite APIs Pros

Slide 80

Slide 80 text

• No more SQLite APIs • Easy testing Pros

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

Gotchas

Slide 83

Slide 83 text

• Require Android device/emulator for testing Gotchas

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

Should I use Room?

Slide 87

Slide 87 text

Should I use Room?

Slide 88

Slide 88 text

LiveData

Slide 89

Slide 89 text

LiveData RxJava2

Slide 90

Slide 90 text

@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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

And of course, testing…

Slide 97

Slide 97 text

@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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

LiveData with RxJava

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

Summary

Slide 102

Slide 102 text

Pros

Slide 103

Slide 103 text

• Lifecycle-aware, officially supported architecture Pros

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

Gotchas

Slide 107

Slide 107 text

• SingleLiveEvent, LiveDataTestUtil not included Gotchas

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

Should I use Architecture Components?

Slide 112

Slide 112 text

Should I use Architecture Components?

Slide 113

Slide 113 text

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/

Slide 114

Slide 114 text

Thanks! Phil Shadlyn @physphil