Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Android Jetpack: ViewModel and Testing

1,680 views

Published on

@GDG Korea Android - Google I/O Extended Android

Published in: Software
  • Be the first to comment

Android Jetpack: ViewModel and Testing

  1. 1. KimKevin Android Developer / Kakaobank Android Jetpack ViewModel and Testing
  2. 2. ViewModel
  3. 3. Model Presenter View Repository The past of past
  4. 4. Model ViewModel (Not AAC) Presenter Repository View Databinding The past
  5. 5. Model AAC ViewModel Repository View Databinding The present
  6. 6. ViewModel ! The Lifecycle Library’s ViewModel class ! android.arch.lifecycle -> androidx.lifecycle ! Provides data for UI components and survive configuration changes.
  7. 7. ViewModel can help ! Avoiding memory leaks ! Solving common Android lifecycle challenges
  8. 8. ViewModel can help ! Avoiding memory leaks ! Solving common Android lifecycle challenges ! Share data between fragments
  9. 9. class UserViewModel : ViewModel() { val name = MutableLiveData<String>() } class UserFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { val userViewModel = ViewModelProviders.of(getActivity()).get(UserViewModel::class.java) loginButton.setOnClickListener({ item -> userViewModel.name = item }) } } class UserProfileFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { val userViewModel = ViewModelProviders.of(getActivity()).get(UserViewModel::class.java) userViewModel.name.observe(this, { name -> // Update the UI. }) } }
  10. 10. ViewModel can help ! Avoiding memory leaks ! Solving common Android lifecycle challenges ! Share data between fragments ! Design good software (SRP, testing etc)
  11. 11. Activity Activity Data Loading Drawing UI Components Processing Data Handling all UI Interactions Saving and restoring the UI
  12. 12. Data Loading Drawing UI Components Processing Data Handling all UI Interactions Saving and restoring the UI Activity Activity https://stackify.com/solid-design-principles
  13. 13. Data Loading Drawing UI Components Processing Data Handling all UI Interactions Saving and restoring the UI Activity Activity Single Responsibility Principle https://stackify.com/solid-design-principles
  14. 14. Activity Drawing UI Receiving User Interactions as Android Developer
  15. 15. Activity Drawing UI Receiving User Interactions ViewModel Hold UI Data Repository API for saving and loading app data Presenter Process data for UI User Case full fledged clean architecture And more… as Android Developer as Java Developer
  16. 16. Activity Drawing UI Receiving User Interactions ViewModel Hold UI Data Repository API for saving and loading app data Presenter Process data for UI User Case full fledged clean architecture And more… as Android Developer as Java Developer Provides APIs Testable Code
  17. 17. Reactive UI ! ViewModel ! LiveData ! Data Binding ! Android Studio 3.1 + ! Support for ViewModel and LiveData ! setLifecycleOwner(LifecycleOwner)
  18. 18. ViewModel could do ! Handle configuration changes ! Replace onSaveInstanceState()
  19. 19. Activity Instance Activity UI data
  20. 20. Activity Instance RotationEvent Activity UI data
  21. 21. Activity Instance Recreated Activity Instance RotationEvent Forgotten data that didn’t get passed to next activity Activity UI data
  22. 22. <activity android:name="MainActivity" android:configChanges="screenSize|orientation" /> Activity Fragment public void onCreate(@Nullable Bundle savedInstanceState) { ··· setRetainInstance(true) }
  23. 23. ViewModel Activity Instance Activity UI data
  24. 24. ViewModel Activity UI data RotationEvent Activity Instance
  25. 25. ViewModel Activity UI data Activity Instance Recreated Activity Instance RotationEvent
  26. 26. class UserLoginViewModel : ViewModel() { private val _id = MutableLiveData<String>() val id: LiveData<String> get() = _id private val _password = MutableLiveData<String>() val password: LiveData<String> get() = _password ··· }
  27. 27. public abstract class ViewModel { @SuppressWarnings("WeakerAccess") protected void onCleared() { } }
  28. 28. public abstract class ViewModel { @SuppressWarnings("WeakerAccess") protected void onCleared() { } } @Override protected void onDestroy() { super.onDestroy(); ··· mViewModelStore.clear(); ··· } FragmentActivity & Fragment
  29. 29. public final void clear() { for (ViewModel vm : mMap.values()) { vm.onCleared(); } mMap.clear(); } @Override protected void onDestroy() { super.onDestroy(); ··· mViewModelStore.clear(); ··· } FragmentActivity & Fragment ViewModelStore
  30. 30. WARNING! ! Don’t store Contexts in a ViewModel ! Activities, Fragments, Views, etc
  31. 31. ViewModel Activity UI data Activity Instance If you store an Activity
  32. 32. ViewModel Activity UI data Activity Instance If you store an Activity RotationEvent
  33. 33. ViewModel Activity UI data Activity Instance If you store an Activity RotationEvent Recreated Activity Instance MEMORY LEAK
  34. 34. override fun onCreate(savedInstanceState: Bundle?) { ··· val userLoginViewModel = ViewModelProviders.of(this, factory).get(UserLoginViewModel::class.java) } ViewModelProdivers.of() creates a ViewModelProvider
  35. 35. public static ViewModelProvider of(@NonNull FragmentActivity activity, @Nullable Factory factory) { Application application = checkApplication(activity); if (factory == null) { factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application); } return new ViewModelProvider(activity.getViewModelStore(), factory); } override fun onCreate(savedInstanceState: Bundle?) { ··· val userLoginViewModel = ViewModelProviders.of(this, factory).get(UserLoginViewModel::class.java) } ViewModelProviders
  36. 36. public static ViewModelProvider of(@NonNull FragmentActivity activity, @Nullable Factory factory) { // Lifecycles 2.0.0-alpha1 (AndroidX) return new ViewModelProvider(activity.getViewModelStore(), factory); // Lifecycles 1.1.1 return new ViewModelProvider(ViewModelStores.of(activity), factory); } ViewModelProviders
  37. 37. public static ViewModelStore of(@NonNull FragmentActivity activity) { if (activity instanceof ViewModelStoreOwner) { return ((ViewModelStoreOwner) activity).getViewModelStore(); } return holderFragmentFor(activity).getViewModelStore(); } public HolderFragment() { setRetainInstance(true); } public static ViewModelProvider of(@NonNull FragmentActivity activity, @Nullable Factory factory) { ··· return new ViewModelProvider(ViewModelStores.of(activity), factory); } ViewModelStores (Not included in AndroidX) ViewModelProviders Lifecycles 1.1.1
  38. 38. public static ViewModelProvider of(@NonNull FragmentActivity activity, @Nullable Factory factory) { ··· return new ViewModelProvider(activity.getViewModelStore(), factory); } public static ViewModelProvider of(@NonNull Fragment fragment, @Nullable Factory factory) { ··· return new ViewModelProvider(fragment.getViewModelStore(), factory); } // for FragmentActivity Lifecycles 2.0.0-alpha1 (AndroidX) ViewModelProviders // for Fragment
  39. 39. ViewModelStore ViewModel2 FragmentA ViewModel3 ViewModelStore ViewModel1 ViewModel1 FragmentB ViewModel2 Created FragmentActivity
  40. 40. ViewModelStore ViewModel2 FragmentA ViewModel3 ViewModelStore ViewModel1 FragmentB Recreated FragmentActivity on rotation
  41. 41. ViewModelStore ViewModel2 FragmentA ViewModel3 ViewModelStore ViewModel1 ViewModel1 ViewModelProvider ViewModelProviders FragmentB ViewModel2 Restored ViewModels
  42. 42. ViewModelStore ViewModel2 ViewModel3 ViewModelStore ViewModel1 FragmentA ViewModel1 ViewModelProvider ViewModelProviders FragmentB ViewModel2 Restored ViewModels ViewModelStore ViewModel2 ViewModel3 ViewModelStore ViewModel1
  43. 43. How to restore ViewModelStore
  44. 44. @Override public final Object onRetainNonConfigurationInstance() { ··· Object custom = onRetainCustomNonConfigurationInstance(); FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig(); if (fragments == null && mViewModelStore == null && custom == null) { return null; } NonConfigurationInstances nci = new NonConfigurationInstances(); nci.custom = custom; nci.viewModelStore = mViewModelStore; nci.fragments = fragments; return nci; } FragmentActivity Retain All fragment state
  45. 45. FragmentActivity Restore All fragment state @Override protected void onCreate(@Nullable Bundle savedInstanceState) { ··· NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance(); if (nc != null) { mViewModelStore = nc.viewModelStore; } if (savedInstanceState != null) { Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG); mFragments.restoreAllState(p, nc != null ? nc.fragments : null); ··· }
  46. 46. FragmentActivity Restore All fragment state @Override protected void onCreate(@Nullable Bundle savedInstanceState) { ··· NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance(); if (nc != null) { mViewModelStore = nc.viewModelStore; } if (savedInstanceState != null) { Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG); mFragments.restoreAllState(p, nc != null ? nc.fragments : null); ··· }
  47. 47. Restore All fragment state public void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) { mHost.mFragmentManager.restoreAllState(state, nonConfig); } FragmentController
  48. 48. Restore All fragment state FragmentController public void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) { mHost.mFragmentManager.restoreAllState(state, nonConfig); }
  49. 49. FragmentManager void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) { ··· if (nonConfig != null) { List<Fragment> nonConfigFragments = nonConfig.getFragments(); childNonConfigs = nonConfig.getChildNonConfigs(); viewModelStores = nonConfig.getViewModelStores(); ··· mActive = new SparseArray<>(fms.mActive.length); for (int i=0; i<fms.mActive.length; i++) { FragmentState fs = fms.mActive[i]; if (fs != null) { FragmentManagerNonConfig childNonConfig = null; if (childNonConfigs != null && i < childNonConfigs.size()) { childNonConfig = childNonConfigs.get(i); } ViewModelStore viewModelStore = null; if (viewModelStores != null && i < viewModelStores.size()) { viewModelStore = viewModelStores.get(i); } Fragment f = fs.instantiate(mHost, mContainer, mParent, childNonConfig, viewModelStore); Restore All fragment state
  50. 50. ViewModel could do ! Handle configuration changes?
  51. 51. ViewModel could do ! Handle configuration changes? Yes!
  52. 52. ViewModel could do ! Handle configuration changes? Yes! ! Replace onSaveInstanceState()?
  53. 53. ViewModel could do ! Handle configuration changes? Yes! ! Replace onSaveInstanceState()? No!
  54. 54. Use ViewModel with onSaveInstanceState • Survives configuration changes • Hold lots of data • Survives configuration changes and process death • Requires serialization • Hold small amount of data (less than 50k of data) All data for Activity UI Data to reload Activity data in emergency ViewModel onSaveInstanceState
  55. 55. Use ViewModel with onSaveInstanceState • Survives configuration changes • Hold lots of data • Survives configuration changes and process death • Requires serialization • Hold small amount of data (less than 50k of data) All the user’s data: User id, first name, last name, birthday, address, profile images… User id ViewModel onSaveInstanceState
  56. 56. and Testing
  57. 57. https://github.com/mockito/mockito/issues/1013
  58. 58. Wiki & Docs How to use Search feature • Handling network errors • ERROR CODE : 1003 • Do something! • •
  59. 59. Wiki & Docs How to use Search feature • Handling network errors • ERROR CODE : 1003 • Do something! • • Hard to keep document the latest, whenever requirement changes
  60. 60. Why writing Unit tests ! Think of unit tests as documentation for future developers ! Help developers to refactor safely
  61. 61. https://www.shutterstock.com Testing is not only for you but also for your colleagues in your next seat
  62. 62. Presenter Testing ! Behavior Driven Testing ! Template : Given-When-Then (3A - Arrange-Act-Assert)
  63. 63. @Test fun givenErrorCode1003_whenSearch_shouldHandleError() { val networkError = NetworkException(1003, "Network Error!") doThrow(networkError).when(mockSearchRepository).search(INVALID_QUERY) presenter.search(INVALID_QUERY) verify(view).hideLoading() verify(view).setErrorMessage(networkError.message) }
  64. 64. ViewModel Testing ! Assert data values
  65. 65. @Test fun givenErrorCode1003_whenSearch_shouldHandleError() { val networkError = NetworkException(1003, "Network Error!") doThrow(networkError).when(mockSearchRepository).search(INVALID_QUERY) presenter.search(INVALID_QUERY) verify(view).hideLoading() verify(view).setErrorMessage(networkError.message) } @Test fun givenErrorCode1003_whenSearch_shouldHandleError() { ··· viewModel.search(INVALID_QUERY) assertEquals(viewModel.loading.value, false) assertEquals(viewModel.errorMessage.value, networkError.message) } Presenter ViewModel
  66. 66. ViewModel Testing ! Assert data values ! Verify Observer onChanged() events ! Dependency for testing LiveData ! testImplementation “android.arch.core:core-testing:{version}“ ! @Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
  67. 67. @Test fun givenErrorCode1003_whenSearch_shouldHandleError() { ··· val mockObserver = mock<Observer<Boolean>>() viewModel.loading.observeForever(mockObserver) viewModel.search(INVALID_QUERY) assertEquals(viewModel.loading.value, false) verify(mockObserver).onChanged(false) } class SearchActivity : AppCompatActivity() { viewModel.loading.observe(this, Observer { it?.let { if (it) loading.visibility = View.VISIBLE else loading.visibility = View.GONE } }) } SearchActivity
  68. 68. Mockito features ! verify(), reset(), never(), doThrow() ! verifyNoMoreInteractions() ! verifyZeroInteractions()
  69. 69. @Test fun givenErrorCode1003_whenSearch_shouldHandleError() { ··· val mockObserver = mock<Observer<Boolean>>() viewModel.loading.observeForever(mockObserver) viewModel.search(INVALID_QUERY) verify(mockObserver).onChanged(false) reset(mockObserver) // For making sure interaction(s) never happened on mock verifyNoMoreInteractions(mockObserver) verify(mockObserver, never()).onChanged(any()) // For finding redundant invocations verifyZeroInteractions(mockData1, mockData2) }
  70. 70. Thank you @imkimkevin / imkimkevin@gmail.com Kevin Yongjun Kim
  71. 71. References ! https://www.youtube.com/watch?v=5qlIPTDE274 ! https://medium.com/google-developers/viewmodels-persistence- onsaveinstancestate-restoring-ui-state-and-loaders-fc7cc4a6c090 ! https://developer.android.com/topic/libraries/architecture/viewmodel

×