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

Reactive View Models

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

Reactive View Models

Quick look into how Reactive View Models can make managing view state in Android easier.

Example Project: https://github.com/bgogetap/ReactiveViewModels

Avatar for Brandon Gogetap

Brandon Gogetap

February 16, 2017
Tweet

More Decks by Brandon Gogetap

Other Decks in Technology

Transcript

  1. View State - Where to persist? • SavedState subclass •

    Override onSaveInstanceState / onRestoreInstanceState • Must serialize all fields required to restore state • Presenter (scoped Singleton instance) • Easy, but can lead to Presenter performing duties beyond its original purpose • Database or other long-term storage • Great for some cases (messaging app?), but may be overly complex for transient data that doesn’t need to be persisted beyond the session.
  2. Presenter <—> View relationship • Having to use a new

    Presenter class for each view isn’t an issue—but dealing with the interaction between them can be bothersome • Null check on view every time Presenter wants to call a view method • Remembering to detach Presenter* • Separating “first attach” logic from subsequent attaches* *If using “scoped Singleton” Presenters
  3. Traditional Presenter <—> View relationship • Bottom line is, for

    efficiency, several methods are exposed on the view to allow Presenter to update independently. • Each call to one of these methods involves a null check* • Presenter cluttered • View cluttered
  4. Traditional Presenter <—> View relationship • Testing the Presenter’s view-updating

    logic isn’t perfect (must assume view receives call) • Above point not as important if view attached logic is sound, but… • Where do we store the state of each independent piece of the view? • Multiple fields in presenter? • Disassembled from some other larger object?
  5. Ideal View Lifecycle • View should subscribe to something that

    tells it what to display • View actions should be delegated to a class (Presenter still fits) • Methods to update view should not have to be defined or exposed
  6. Using an Observable based View Model • View subscribes to

    relevant Observables and unsubscribes when necessary • View Model can be updated at any time, even if view isn’t ready! • Presenter doesn’t have to know about the view • View doesn’t have to expose any methods to allow other classes to update it • View Model can be unit tested • Any manipulation of data into what the view needs is encapsulated in the View Model
  7. Using an Observable based View Model • The idea doesn’t

    have a strict implementation—can go a few different routes • View Model could talk directly with Repository in some cases • This could mean the Presenter never has to know about the view or View Model • Depending on view complexity—expose Observables for each widget/ property (title, message, background color, etc.) • Very simple cases could even have the Presenter act as the View Model
  8. View Model Structure • Scoped Singleton provided by dependency injection

    framework • Exposes Subjects/Relays as Observables • Exposed Observables may be behind Interface to obfuscate other methods required to update the View Model from the view
  9. View Model Structure final class HomeViewModel implements HomeViewProvider {
 


    private final BehaviorRelay<List<ListItem>> listItemRelay;
 
 HomeViewModel(ListRepository repository) {
 listItemRelay = BehaviorRelay.create();
 repository.getItems().subscribe(listItemRelay);
 }
 
 @Override public Observable<List<ListItem>> listItems() {
 return listItemRelay;
 }
 }
  10. View Model Structure final class HomeViewModel implements HomeViewProvider {
 


    private final BehaviorRelay<List<ListItem>> listItemRelay;
 
 HomeViewModel(ListRepository repository) {
 listItemRelay = BehaviorRelay.create();
 repository.getItems().subscribe(listItemRelay);
 }
 
 @Override public Observable<List<ListItem>> listItems() {
 return listItemRelay;
 }
 }
  11. View Model Structure final class HomeViewModel implements HomeViewProvider {
 


    private final BehaviorRelay<List<ListItem>> listItemRelay;
 
 HomeViewModel(ListRepository repository) {
 listItemRelay = BehaviorRelay.create();
 repository.getItems().subscribe(listItemRelay);
 }
 
 @Override public Observable<List<ListItem>> listItems() {
 return listItemRelay;
 }
 }
  12. View Model Structure final class HomeViewModel implements HomeViewProvider {
 


    private final BehaviorRelay<List<ListItem>> listItemRelay;
 
 HomeViewModel(ListRepository repository) {
 listItemRelay = BehaviorRelay.create();
 repository.getItems().subscribe(listItemRelay);
 }
 
 BehaviorRelay<List<ListItem>> listItemRelay() {
 return listItemRelay;
 }
 
 @Override public Observable<List<ListItem>> listItems() {
 return listItemRelay;
 }
 }
  13. View Model Structure final class HomeViewModel implements HomeViewProvider {
 


    private final BehaviorRelay<List<ListItem>> listItemRelay;
 
 HomeViewModel(ListRepository repository) {
 listItemRelay = BehaviorRelay.create();
 repository.getItems().subscribe(listItemRelay);
 }
 
 void updateListItems(List<ListItem> listItems) {
 listItemRelay.accept(listItems);
 }
 
 @Override public Observable<List<ListItem>> listItems() {
 return listItemRelay;
 }
 }
  14. View Structure @Override protected void onViewBound(View view) {
 component.inject(this);
 recyclerView.setLayoutManager(new

    LinearLayoutManager(view.getContext()));
 disposables.add(viewModel.listItems().subscribe(this::setData));
 }
 
 private void setData(List<ListItem> items) {
 adapter = new HomeAdapter(items);
 recyclerView.setAdapter(adapter);
 } @Override protected void onDestroyView(@NonNull View view) {
 super.onDestroyView(view);
 disposables.clear();
 }
  15. final class DetailViewModel implements DetailViewProvider {
 
 /** Field declarations

    **/
 
 DetailViewModel(DetailRepository repository) {
 titleRelay = BehaviorRelay.create();
 messageRelay = BehaviorRelay.create();
 colorRelay = BehaviorRelay.create();
 idRelay = BehaviorRelay.create();
 repository.getItem().subscribe(listItem -> {
 titleRelay.accept(listItem.title);
 messageRelay.accept(listItem.message);
 colorRelay.accept(listItem.color);
 idRelay.accept(listItem.id);
 });
 }
 
 @Override public Observable<String> title() {
 return titleRelay;
 }
 
 @Override public Observable<String> message() {
 return messageRelay;
 }
 
 @Override public Observable<Integer> id() {
 return idRelay;
 }
 
 @Override public Observable<Integer> color() {
 return colorRelay;
 }
 }
  16. final class DetailViewModel implements DetailViewProvider {
 
 /** Field declarations

    **/
 
 DetailViewModel(DetailRepository repository) {
 titleRelay = BehaviorRelay.create();
 messageRelay = BehaviorRelay.create();
 colorRelay = BehaviorRelay.create();
 idRelay = BehaviorRelay.create();
 repository.getItem().subscribe(listItem -> {
 titleRelay.accept(listItem.title);
 messageRelay.accept(listItem.message);
 colorRelay.accept(listItem.color);
 idRelay.accept(listItem.id);
 });
 }
 
 @Override public Observable<String> title() {
 return titleRelay;
 }
 
 @Override public Observable<String> message() {
 return messageRelay;
 }
 
 @Override public Observable<Integer> id() {
 return idRelay;
 }
 
 @Override public Observable<Integer> color() {
 return colorRelay;
 }
 }
  17. final class DetailViewModel implements DetailViewProvider {
 
 /** Field declarations

    **/
 
 DetailViewModel(DetailRepository repository) {
 titleRelay = BehaviorRelay.create();
 messageRelay = BehaviorRelay.create();
 colorRelay = BehaviorRelay.create();
 idRelay = BehaviorRelay.create();
 repository.getItem().subscribe(listItem -> {
 titleRelay.accept(listItem.title);
 messageRelay.accept(listItem.message);
 colorRelay.accept(listItem.color);
 idRelay.accept(listItem.id);
 });
 }
 
 @Override public Observable<String> title() {
 return titleRelay;
 }
 
 @Override public Observable<String> message() {
 return messageRelay;
 }
 
 @Override public Observable<Integer> id() {
 return idRelay;
 }
 
 @Override public Observable<Integer> color() {
 return colorRelay;
 }
 }
  18. final class DetailViewModel implements DetailViewProvider {
 
 /** Field declarations

    **/
 
 DetailViewModel(DetailRepository repository) {
 titleRelay = BehaviorRelay.create();
 messageRelay = BehaviorRelay.create();
 colorRelay = BehaviorRelay.create();
 idRelay = BehaviorRelay.create();
 repository.getItem().subscribe(listItem -> {
 titleRelay.accept(listItem.title);
 messageRelay.accept(listItem.message);
 colorRelay.accept(listItem.color);
 idRelay.accept(listItem.id);
 });
 }
 
 @Override public Observable<String> title() {
 return titleRelay;
 }
 
 @Override public Observable<String> message() {
 return messageRelay;
 }
 
 @Override public Observable<Integer> id() {
 return idRelay;
 }
 
 @Override public Observable<Integer> color() {
 return colorRelay;
 }
 }
  19. @Override protected void onViewBound(View view) {
 component.inject(this);
 disposables.add(viewModel.title().subscribe(titleTextView::setText));
 disposables.add(viewModel.message().subscribe(messageTextView::setText));
 disposables.add(viewModel.id().subscribe(id

    -> idTextView.setText("ID: " + id)));
 disposables.add(viewModel.color().subscribe(view::setBackgroundColor));
 } @Override protected void onDestroyView(@NonNull View view) {
 super.onDestroyView(view);
 disposables.clear();
 }
  20. View Model Structure • There are many ways to structure

    — do what makes sense for you • Main takeaways of an ideal implementation • All view state is kept in BehaviorSubjects/Relays • All interactions are one-way—no other class cares about the view
  21. View Model Testing @Test public void listItems() {
 TestObserver<List<ListItem>> testObserver

    = new TestObserver<>();
 viewModel.listItems().subscribe(testObserver);
 itemRelay.accept(getListWithCount(2));
 itemRelay.accept(getListWithCount(4));
 testObserver.assertValues(getListWithCount(2), getListWithCount(4));
 }
  22. View Model Testing @Test public void listItems() {
 TestObserver<List<ListItem>> testObserver

    = new TestObserver<>();
 viewModel.listItems().subscribe(testObserver);
 itemRelay.accept(getListWithCount(2));
 itemRelay.accept(getListWithCount(4));
 testObserver.assertValues(getListWithCount(2), getListWithCount(4));
 }
  23. View Model Testing @Test public void listItems() {
 TestObserver<List<ListItem>> testObserver

    = new TestObserver<>();
 viewModel.listItems().subscribe(testObserver);
 itemRelay.accept(getListWithCount(2));
 itemRelay.accept(getListWithCount(4));
 testObserver.assertValues(getListWithCount(2), getListWithCount(4));
 }
  24. View Model Testing @Test public void listItems() {
 TestObserver<List<ListItem>> testObserver

    = new TestObserver<>();
 viewModel.listItems().subscribe(testObserver);
 itemRelay.accept(getListWithCount(2));
 itemRelay.accept(getListWithCount(4));
 testObserver.assertValues(getListWithCount(2), getListWithCount(4));
 }
  25. View Model Testing @Test public void listItem() {
 TestObserver<String> titleTestObserver

    = new TestObserver<>();
 TestObserver<Integer> idTestObserver = new TestObserver<>();
 TestObserver<String> messageTestObserver = new TestObserver<>();
 TestObserver<Integer> colorTestObserver = new TestObserver<>();
 viewModel.title().subscribe(titleTestObserver);
 viewModel.message().subscribe(messageTestObserver);
 viewModel.id().subscribe(idTestObserver);
 viewModel.color().subscribe(colorTestObserver);
 
 itemRelay.accept(FIRST);
 itemRelay.accept(SECOND);
 
 titleTestObserver.assertValues(FIRST.title(), SECOND.title());
 messageTestObserver.assertValues(FIRST.message(), SECOND.message());
 idTestObserver.assertValues(FIRST.id(), SECOND.id());
 colorTestObserver.assertValues(FIRST.color(), SECOND.color());
 }
  26. View Model Testing @Test public void listItem() {
 TestObserver<String> titleTestObserver

    = new TestObserver<>();
 TestObserver<Integer> idTestObserver = new TestObserver<>();
 TestObserver<String> messageTestObserver = new TestObserver<>();
 TestObserver<Integer> colorTestObserver = new TestObserver<>();
 viewModel.title().subscribe(titleTestObserver);
 viewModel.message().subscribe(messageTestObserver);
 viewModel.id().subscribe(idTestObserver);
 viewModel.color().subscribe(colorTestObserver);
 
 itemRelay.accept(FIRST);
 itemRelay.accept(SECOND);
 
 titleTestObserver.assertValues(FIRST.title(), SECOND.title());
 messageTestObserver.assertValues(FIRST.message(), SECOND.message());
 idTestObserver.assertValues(FIRST.id(), SECOND.id());
 colorTestObserver.assertValues(FIRST.color(), SECOND.color());
 }
  27. View Model Testing @Test public void listItem() {
 TestObserver<String> titleTestObserver

    = new TestObserver<>();
 TestObserver<Integer> idTestObserver = new TestObserver<>();
 TestObserver<String> messageTestObserver = new TestObserver<>();
 TestObserver<Integer> colorTestObserver = new TestObserver<>();
 viewModel.title().subscribe(titleTestObserver);
 viewModel.message().subscribe(messageTestObserver);
 viewModel.id().subscribe(idTestObserver);
 viewModel.color().subscribe(colorTestObserver);
 
 itemRelay.accept(FIRST);
 itemRelay.accept(SECOND);
 
 titleTestObserver.assertValues(FIRST.title(), SECOND.title());
 messageTestObserver.assertValues(FIRST.message(), SECOND.message());
 idTestObserver.assertValues(FIRST.id(), SECOND.id());
 colorTestObserver.assertValues(FIRST.color(), SECOND.color());
 }
  28. View Model Testing @Test public void listItem() {
 TestObserver<String> titleTestObserver

    = new TestObserver<>();
 TestObserver<Integer> idTestObserver = new TestObserver<>();
 TestObserver<String> messageTestObserver = new TestObserver<>();
 TestObserver<Integer> colorTestObserver = new TestObserver<>();
 viewModel.title().subscribe(titleTestObserver);
 viewModel.message().subscribe(messageTestObserver);
 viewModel.id().subscribe(idTestObserver);
 viewModel.color().subscribe(colorTestObserver);
 
 itemRelay.accept(FIRST);
 itemRelay.accept(SECOND);
 
 titleTestObserver.assertValues(FIRST.title(), SECOND.title());
 messageTestObserver.assertValues(FIRST.message(), SECOND.message());
 idTestObserver.assertValues(FIRST.id(), SECOND.id());
 colorTestObserver.assertValues(FIRST.color(), SECOND.color());
 }