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

Reactive View Models

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

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