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

Architecture Components: MVVM for Android

GDG SPb
September 12, 2017

Architecture Components: MVVM for Android

На Google I/O 17 были представлены Android Architecture Components – библиотеки для построения некоторых элементов архитектуры приложения и решения различных проблем жизненного цикла. В рамках доклада мы рассмотрим, что получилось сделать у разработчиков Google. Мы разберем, как правильно использовать архитектурные компоненты в ваших приложениях, как с помощью них решать часто встречающиеся задачи, в чем архитектурные компоненты и MVVM отличаются от привычных подходов и как тестировать написанный код. Кроме того, мы заглянем внутрь библиотек и проанализируем наиболее интересные моменты в их реализации и узнаем, что еще из этих библиотек можно использовать в своих приложениях.

GDG SPb

September 12, 2017
Tweet

More Decks by GDG SPb

Other Decks in Programming

Transcript

  1. Presentation layer components › Model › View › Some magical

    thing › Connections between these components 8
  2. Bindings › DataBinding (true way for MVVM – framework handles

    binding) › Observer pattern (less true way) 10
  3. MVVM vs MVP › Neither better nor worse › MVVM

    is less coupled (ViewModel doesn’t have a reference to View, but Presenter does) › Observing changes could be less pleasant than direct methods call › MVP has lots of implementation variants and MVVM does not 11
  4. Observe LiveData ExampleLiveData liveData = new ExampleLiveData(); liveData.observe(this, value ->

    { Log.i(TAG, String.format("Received value %d", value)); }); liveData.calculateValue();
  5. Observe LiveData ExampleLiveData liveData = new ExampleLiveData(); liveData.observe(LifecycleOwner, value ->

    { Log.i(TAG, String.format("Received value %d", value)); }); liveData.calculateValue();
  6. LifecycleOwner public interface LifecycleOwner { /** * Returns the Lifecycle

    of the provider. * * @return The lifecycle of the provider. */ Lifecycle getLifecycle(); }
  7. Lifecycle 1. Observe lifecycle changes – this allows to remove

    observer when LifecycleOwner (Activity / Fragment / Service / …) is destroyed 2. SupportActivity / Fragment (support library 26.1.0+) or LifecycleActivity / LifecycleFragment 20
  8. ViewModel 1. Usual ViewModel from MVVM 2. Stored in retain

    fragment (survives configuration changes) 3. Holds all important LiveData objects 4. No reference for View (View observe LiveData objects) 21
  9. ViewModel example public class SimpleViewModel extends ViewModel { @NonNull private

    final MutableLiveData<Integer> valueLiveData = new MutableLiveData<>(); public void init() { valueLiveData.setValue(42); } @NonNull public LiveData<Integer> observeValueChanges() { return valueLiveData; } }
  10. ViewModel example public class SimpleViewModel extends ViewModel { @NonNull private

    final MutableLiveData<Integer> valueLiveData = new MutableLiveData<>(); public void init() { valueLiveData.setValue(42); } @NonNull public LiveData<Integer> observeValueChanges() { return valueLiveData; } }
  11. ViewModel example public class SimpleViewModel extends ViewModel { @NonNull private

    final MutableLiveData<Integer> valueLiveData = new MutableLiveData<>(); public void init() { valueLiveData.setValue(42); } @NonNull public LiveData<Integer> observeValueChanges() { return valueLiveData; } }
  12. ViewModel with network call public class MoviesViewModel extends ViewModel {

    //... @MainThread @NonNull LiveData<List<Movie>> getMoviesList() { if (moviesLiveData == null) { moviesLiveData = new MutableLiveData<>(); moviesRepository.popularMovies() .subscribeOn(schedulersProvider.io()) .observeOn(schedulersProvider.mainThread()) .subscribe(movies -> moviesLiveData.setValue(movies)); } return moviesLiveData; } }
  13. ViewModel with network call public class MoviesViewModel extends ViewModel {

    //... @MainThread @NonNull LiveData<List<Movie>> getMoviesList() { if (moviesLiveData == null) { moviesLiveData = new MutableLiveData<>(); moviesRepository.popularMovies() .subscribeOn(schedulersProvider.io()) .observeOn(schedulersProvider.mainThread()) .subscribe(movies -> moviesLiveData.setValue(movies)); } return moviesLiveData; } }
  14. ViewModel with network call public class MoviesViewModel extends ViewModel {

    //... @MainThread @NonNull LiveData<List<Movie>> getMoviesList() { if (moviesLiveData == null) { moviesLiveData = new MutableLiveData<>(); moviesRepository.popularMovies() .subscribeOn(schedulersProvider.io()) .observeOn(schedulersProvider.mainThread()) .subscribe(movies -> moviesLiveData.setValue(movies)); } return moviesLiveData; } }
  15. ViewModel with network call public class MoviesViewModel extends ViewModel {

    //... @MainThread @NonNull LiveData<List<Movie>> getMoviesList() { if (moviesLiveData == null) { moviesLiveData = new MutableLiveData<>(); moviesRepository.popularMovies() .subscribeOn(schedulersProvider.io()) .observeOn(schedulersProvider.mainThread()) .subscribe(movies -> moviesLiveData.setValue(movies)); } return moviesLiveData; } }
  16. ViewModel.Factory public ViewModelProvider(ViewModelStore store, Factory factory) { mFactory = factory;

    this.mViewModelStore = store; } public interface Factory { /** * Creates a new instance of the given {@code Class}. * <p> * * @param modelClass a {@code Class} whose instance is requested * @param <T> The type parameter for the ViewModel. * @return a newly created ViewModel */ <T extends ViewModel> T create(Class<T> modelClass); }
  17. Custom factory @Inject ViewModelProvider.Factory viewModelFactory; @NonNull private MoviesViewModel getViewModel() {

    return ViewModelProviders.of(this, viewModelFactory).get(MoviesViewModel.class); }
  18. Handling errors 1. Wrap error in custom object 2. Use

    special LiveData for delivering errors 36
  19. Handling errors @NonNull private final MutableLiveData<Throwable> errorsLiveData = new MutableLiveData<>();

    @NonNull LiveData<Throwable> observeErrors() { return errorsLiveData; }
  20. Handling errors @MainThread @NonNull LiveData<List<Movie>> getMoviesList() { if (moviesLiveData ==

    null) { moviesLiveData = new MutableLiveData<>(); moviesRepository.popularMovies() .subscribeOn(schedulersProvider.io()) .observeOn(schedulersProvider.mainThread()) .subscribe( movies -> moviesLiveData.setValue(movies), throwable -> errorsLiveData.setValue(throwable) ); } return moviesLiveData; }
  21. Testing 1. ViewModel is the only new thing to test

    2. Testing is discussed many times 3. JUnit is enough 4. ViewModel testing only differs a little from Presenter testing 39
  22. Unit-testing @RunWith(JUnit4.class) public class MoviesViewModelTest { private MoviesViewModel viewModel; private

    MoviesRepository repository; @Before public void setUp() throws Exception { repository = Mockito.mock(MoviesRepository.class); Mockito.when(repository.popularMovies()).thenReturn(Observable.just(new ArrayList<>())); viewModel = new MoviesViewModel(schedulersProvider, repository); } }
  23. Unit-testing @RunWith(JUnit4.class) public class MoviesViewModelTest { @Test public void testLoadingMovies()

    throws Exception { Observer observer = Mockito.mock(Observer.class); viewModel.getMoviesList().observeForever(observer); Mockito.verify(repository).popularMovies(); Mockito.verify(observer).onChanged(anyList()); } }
  24. Unit-testing @RunWith(JUnit4.class) public class MoviesViewModelTest { @Test public void testLoadingMovies()

    throws Exception { Observer observer = Mockito.mock(Observer.class); viewModel.getMoviesList().observeForever(observer); Mockito.verify(repository).popularMovies(); Mockito.verify(observer).onChanged(anyList()); } }
  25. Unit-testing @RunWith(JUnit4.class) public class MoviesViewModelTest { @Test public void testLoadingMovies()

    throws Exception { Observer observer = Mockito.mock(Observer.class); viewModel.getMoviesList().observeForever(observer); Mockito.verify(repository).popularMovies(); Mockito.verify(observer).onChanged(anyList()); } }
  26. Mock threads in LiveData @RunWith(JUnit4.class) public class MoviesViewModelTest { @Rule

    public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule(); @Test public void testLoadingMovies() throws Exception { Observer observer = Mockito.mock(Observer.class); viewModel.getMoviesList().observeForever(observer); Mockito.verify(repository).popularMovies(); Mockito.verify(observer).onChanged(anyList()); } }
  27. LifecycleObserver We often have onStart / onStop and onResume /

    onPause method with lots of attaching / detaching operations public class LifecycleEventsObserver implements LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_START) void onStart() { Log.i(TAG, "onStart"); } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) void onStop() { Log.i(TAG, "onStop"); } } С н цвет не ав л н
  28. Use LifecycleObserver public class LifecycleEventsObserver implements LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_START) void

    onStart() { Log.i(TAG, "onStart"); } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) void onStop() { Log.i(TAG, "onStop"); } } getLifecycle().addObserver(new LifecycleEventsObserver());
  29. LocationLiveData public class LocationLiveData extends LiveData<Location> implements LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_START)

    void startLocationUpdates() { mFusedLocationClient.requestLocationUpdates(locationRequest, callback, locationLooper); } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) void stopLocationUpdates() { mFusedLocationClient.removeLocationUpdates(callback); } }
  30. LocationLiveData public class LocationLiveData extends LiveData<Location> implements LifecycleObserver { @NonNull

    private final LocationCallback callback = new LocationCallback() { @Override public void onLocationResult(LocationResult locationResult) { if (locationResult.getLastLocation() != null) { setValue(locationResult.getLastLocation()); } } }; }
  31. MVVM problems with DataBinding 1. Toast, dialogs, pagination and etc.

    2. With Architecture Components each action is a LiveData (LiveData is like direct View method in MVP) 51
  32. Send toast public class ToastViewModel extends ViewModel { @NonNull private

    final MutableLiveData<String> toastMessageLiveData = new MutableLiveData<>(); @NonNull public LiveData<String> toasts() { return toastMessageLiveData; } public void doWork() { // do some work toastMessageLiveData.setValue("Hello from ViewModel"); } }
  33. Receive Toast @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState);

    toastViewModel = ViewModelProviders.of(this).get(ToastViewModel.class); toastViewModel.toasts().observe(this, message -> { Toast.makeText(ToastActivity.this, message, Toast.LENGTH_SHORT).show(); }); if (savedInstanceState == null) { toastViewModel.doWork(); } }
  34. SingleEventLiveData public class SingleEventLiveData<T> extends MutableLiveData<T> { @NonNull private final

    AtomicBoolean delivered = new AtomicBoolean(false); @Override public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) { super.observe(owner, value -> { if (delivered.compareAndSet(true, false)) { observer.onChanged(value); } }); } @Override public void setValue(@Nullable T value) { delivered.set(true); super.setValue(value); } }
  35. Problems 1. Duplicate events 2. When multiple parameters are required

    we have to create custom objects 56 source: https://www.drupal.org/u/shalimanov
  36. List pagination 1. Store all items in ViewModel, join new

    items and deliver all 2. Store all items in Adapter, deliver only new items 3. Paging library? 57
  37. DataBinding › Google sample › Android architecture components + DataBinding

    https://habrahabr.ru/company/touchinstinct/blog/330830/ 58 source: https://www.drupal.org/u/shalimanov
  38. ComputableLiveData 1. A LiveData class that can be invalidated &

    computed on demand 2. Thread-safe (computes in background) 3. Internal class (but you can copy and use it) 61
  39. ComputableLiveData ComputableLiveData<BigInteger> liveData = new ComputableLiveData<BigInteger>() { @Override protected BigInteger

    compute() { return BigInteger.probablePrime(2048, new SecureRandom()); } }; liveData.getLiveData().observe(this, prime -> { Log.i(TAG, "Probable prime is " + prime); }); // call this to recompute value // liveData.invalidate();
  40. Transformations 1. Map 2. SwitchMap public static <X, Y> LiveData<Y>

    map(LiveData<X> source, final Function<X, Y> func) { final MediatorLiveData<Y> result = new MediatorLiveData<>(); result.addSource(source, new Observer<X>() { @Override public void onChanged(@Nullable X x) { result.setValue(func.apply(x)); } }); return result; } С н цвет не ав л н
  41. Custom transformations @NonNull public static <X> LiveData<X> filter(@NonNull LiveData<X> source,

    @NonNull Function<X, Boolean> filterFunction) { MediatorLiveData<X> result = new MediatorLiveData<>(); result.addSource(source, input -> { if (filterFunction.apply(input)) { result.setValue(input); } }); return result; }
  42. No leaks for LiveData LiveData.LifecycleBoundObserver @OnLifecycleEvent(Lifecycle.Event.ON_ANY) void onStateChange() { if

    (owner.getLifecycle().getCurrentState() == DESTROYED) { removeObserver(observer); return; } // immediately set active state, so we'd never dispatch anything to inactive owner activeStateChanged(isActiveState(owner.getLifecycle().getCurrentState())) } С н цвет не ав л н
  43. ViewModelStore public class ViewModelStore { private final HashMap<String, ViewModel> mMap

    = new HashMap<>(); final void put(String key, ViewModel viewModel) { ViewModel oldViewModel = mMap.get(key); if (oldViewModel != null) { oldViewModel.onCleared(); } mMap.put(key, viewModel); } final ViewModel get(String key) { return mMap.get(key); } }
  44. HolderFragment public class HolderFragment extends Fragment { private ViewModelStore mViewModelStore

    = new ViewModelStore(); public HolderFragment() { setRetainInstance(true); } //... }
  45. HolderFragment HolderFragment holderFragmentFor(Fragment parentFragment) { FragmentManager fm = parentFragment.getChildFragmentManager(); HolderFragment

    holder = findHolderFragment(fm); if (holder != null) { return holder; } holder = mNotCommittedFragmentHolders.get(parentFragment); if (holder != null) { return holder; } parentFragment.getFragmentManager() .registerFragmentLifecycleCallbacks(mParentDestroyedCallback, false); holder = createHolderFragment(fm); mNotCommittedFragmentHolders.put(parentFragment, holder); return holder; }
  46. LifecycleDispatcher Global handler for lifecycle events which dispatches them to

    the observers class LifecycleDispatcher { static void init(Context context) { if (sInitialized.getAndSet(true)) { return; } ((Application) context.getApplicationContext()) .registerActivityLifecycleCallbacks(new DispatcherActivityCallback()); } //... } С н цвет не ав л н
  47. LifecycleRuntimeTrojanProvider public class LifecycleRuntimeTrojanProvider extends ContentProvider { @Override public boolean

    onCreate() { LifecycleDispatcher.init(getContext()); ProcessLifecycleOwner.init(getContext()); return true; } //... } source: https://www.drupal.org/u/shalimanov
  48. LifecycleObserver under the hood 1. Code generation 2. Reflection public

    class LifecycleEventsObserver implements LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_START) void onStart() { Log.i(TAG, "onStart"); } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) void onStop() { Log.i(TAG, "onStop"); } }
  49. Summary 1. Pretty-good designed and working library 2. It’s still

    alpha version 3. Good choice for newbies, pet-projects or for experiments lovers 4. Don’t change your project architecture just for Architecture Components 5. No one forces you to use MVVM (LiveData and ViewModelProviders are good enough in themselves) 77
  50. +7 917 873-48-37 [email protected] Architecture Components – MVVM for Android

    Artur Vasilov Android developer ArturVasilov ArturVasilov