All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hannesdorfmann.mosby3.mvi.MviBasePresenter Maven / Gradle / Ivy

/*
 * Copyright 2017 Hannes Dorfmann.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.hannesdorfmann.mosby3.mvi;

import android.support.annotation.CallSuper;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import com.hannesdorfmann.mosby3.mvp.MvpView;
import io.reactivex.Observable;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.subjects.BehaviorSubject;
import io.reactivex.subjects.PublishSubject;
import java.util.ArrayList;
import java.util.List;

/**
 * This type of presenter is responsible to interact with the viewState in a Model-View-Intent way.
 * A {@link MviBasePresenter} is the bridge that is repsonsible to setup the reactive flow between
 * "view" and "model".
 *
 * 

* Thee methods {@link #bindIntents()} and {@link #unbindIntents()} are kind of representing the * lifecycle of this Presenter. *

    *
  • {@link #bindIntents()} is called the first time the view is attached
  • *
  • {@link #unbindIntents()} is called once the view is detached permanently because the view * has * been destroyed and hence this presenter is not needed anymore and will also be destroyed * afterwards too.
  • *
*

* *

* This means that a presenter can survive orientation changes. During orientation changes (or when * the view is put on the back stack because the user navigated to another view) the view * will be detached temporarily and reattached to the presenter afterwards. To avoid memory leaks * this Presenter class offers two methods: *

    *
  • {@link #intent(ViewIntentBinder)}
  • : Use this to bind an Observable intent from the view *
  • {@link #subscribeViewState(Observable, ViewStateConsumer)}: Use this to bind the ViewState * (a * viewState is a object (typically a POJO) that holds all the data the view needs to display
  • *
* * By using {@link #intent(ViewIntentBinder)} and {@link #subscribeViewState(Observable, * ViewStateConsumer)} * a relay will be established between the view and this presenter that allows the view to be * temporarily detached, without unsubscribing the underlying reactive business logic workflow and * without causing memory leaks (caused by recreation of the view). *

* *

* Please note that the methods {@link #attachView(MvpView)} and {@link #detachView(boolean)} * should * not be overridden unless you have a really good reason to do so. Usually {@link #bindIntents()} * and {@link #unbindIntents()} should be enough. *

* *

* In very rare cases you could also use {@link #getViewStateObservable()} to offer an observable * to other components you can make this method public. *

* *

* Please note that you should not reuse a MviBasePresenter once the View who originally has * instantiated this Presenter has been destroyed permanently. App wide singletons for * Presenters is not a good idea in Model-View-Intent. Reusing singleton scoped Presenters for * different view instances may cause emitting the previous state of the previous attached view * (which already has been destroyed permanently). *

* * @param The type of the viewState this presenter responds to * @param The type of the viewState state * @author Hannes Dorfmann * @since 3.0 */ public abstract class MviBasePresenter implements MviPresenter { /** * The binder is responsible to bind a single view intent. * Typlically you use that in {@link #bindIntents()} in combination with the {@link * #intent(ViewIntentBinder)} function like this: *

   *   Observable loadIntent = intent(new ViewIntentBinder() {
   *      @Override
   *      public Observable bind(MyView view){
   *         return view.loadIntent();
   *      }
   *   }
   * 
* * @param The View type * @param The type of the Intent */ protected interface ViewIntentBinder { @NonNull public Observable bind(@NonNull V view); } /** * This "binder" is responsible to bind the view state to the currently attached view. * This typically "renders" the view. * * Typically this is used in {@link #bindIntents()} with {@link MviBasePresenter#subscribeViewState(Observable, * ViewStateConsumer)} * like this: *

   *   Observable viewState =  ... ;
   *   subscribeViewStateConsumerActually(viewState, new ViewStateConsumer() {
   *      @Override
   *      public void accept(MyView view, MyViewState viewState){
   *         view.render(viewState);
   *      }
   *   }
   * 
* * @param The view Type * @param The ViewState type */ protected interface ViewStateConsumer { public void accept(@NonNull V view, @NonNull VS viewState); } /** * A simple class that holds a pair of the intent relay and the binder to bind the actual Intent * Observable. * * @param The Intent type */ private class IntentRelayBinderPair { private final PublishSubject intentRelaySubject; private final ViewIntentBinder intentBinder; public IntentRelayBinderPair(PublishSubject intentRelaySubject, ViewIntentBinder intentBinder) { this.intentRelaySubject = intentRelaySubject; this.intentBinder = intentBinder; } } /** * This relay is the bridge to the viewState (UI). Whenever the viewState get's reattached, the * latest * state will be reemitted. */ private final BehaviorSubject viewStateBehaviorSubject; /** * We only allow to cal {@link #subscribeViewState(Observable, ViewStateConsumer)} method once */ private boolean subscribeViewStateMethodCalled = false; /** * List of internal relays, bridging the gap between intents coming from the viewState (will be * unsubscribed temporarily when viewState is detached i.e. during config changes) */ private List> intentRelaysBinders = new ArrayList<>(4); /** * Composite Disposals holding subscriptions to all intents observable offered by the viewState. */ private CompositeDisposable intentDisposals; /** * Disposal to unsubscribe from the viewState when the viewState is detached (i.e. during screen * orientation * changes) */ private Disposable viewRelayConsumerDisposable; /** * Disposable between the viewState observable returned from {@link #intent(ViewIntentBinder)} * and * {@link #viewStateBehaviorSubject} */ private Disposable viewStateDisposable; /** * Will be used to determine whether or not a View has been attached for the first time. * This is used to determine whether or not the intents should be bound via {@link * #bindIntents()} * or rebound internally. */ private boolean viewAttachedFirstTime = true; /** * This binder is used to subscribe the view's render method to render the ViewState in the view. */ private ViewStateConsumer viewStateConsumer; /** * Creates a new Presenter without an initial view state */ public MviBasePresenter() { viewStateBehaviorSubject = BehaviorSubject.create(); reset(); } /** * Creaes a new Presenter with the initial view state * * @param initialViewState initial view state (must be not null) */ public MviBasePresenter(@NonNull VS initialViewState) { if (initialViewState == null) { throw new NullPointerException("Initial ViewState == null"); } viewStateBehaviorSubject = BehaviorSubject.createDefault(initialViewState); reset(); } /** * Get the view state observable. *

* Most likely you will use this method for unit testing your presenter. *

* *

* In some very rare case it could be useful to provide other * components, like other presenters, * access to the state. This observable contains the same value as got from {@link * #subscribeViewState(Observable, ViewStateConsumer)} which is also used to render the view. * In other words, this Observable also represents the state of the View, so you could subscribe * via this observable to the view's state. *

* * @return Observable */ protected Observable getViewStateObservable() { return viewStateBehaviorSubject; } @CallSuper @Override public void attachView(@NonNull V view) { if (viewAttachedFirstTime) { bindIntents(); } int intentsSize = intentRelaysBinders.size(); for (int i = 0; i < intentsSize; i++) { IntentRelayBinderPair intentRelayBinderPair = intentRelaysBinders.get(i); bindIntentActually(view, intentRelayBinderPair); } if (viewStateConsumer != null) { subscribeViewStateConsumerActually(view); } viewAttachedFirstTime = false; } @Override @CallSuper public void detachView(boolean retainInstance) { if (!retainInstance) { if (viewStateDisposable != null) { // Cancel the overall observable stream viewStateDisposable.dispose(); } unbindIntents(); reset(); // TODO should we re emit the inital state? What if no initial state has been set. // TODO should we rather throw an exception if presenter is reused after view has been detached permanently } if (viewRelayConsumerDisposable != null) { // Cancel subscription from View to viewState Relay viewRelayConsumerDisposable.dispose(); viewRelayConsumerDisposable = null; } if (intentDisposals != null) { // Cancel subscriptons from view intents to intent Relays intentDisposals.dispose(); intentDisposals = null; } } /** * This is called when the View has been detached permantently (view is destroyed permanently) * to reset the internal state of this Presenter to be ready for being reused (even thought * reusing presenters after their view has been destroy is BAD) */ private void reset() { viewAttachedFirstTime = true; intentRelaysBinders.clear(); subscribeViewStateMethodCalled = false; } /** * This method subscribes the Observable emitting {@code ViewState} over time to the passed * consumer. * Do only invoke this method once! Typically in {@link #bindIntents()} *

* Internally Mosby will hold some relays to ensure that no items emitted from the ViewState * Observable will be lost while viewState is not attached nor that the subscriptions to * viewState * intents will cause memory leaks while viewState detached. *

* * Typically this method is used in {@link #bindIntents()} like this: *

   *   Observable viewState =  ... ;
   *   subscribeViewStateConsumerActually(viewState, new ViewStateConsumer() {
   *      @Override
   *      public void accept(MyView view, MyViewState viewState){
   *         view.render(viewState);
   *      }
   *   }
   * 
* * @param viewStateObservable The Observable emitting new ViewState. Typically an intent {@link * #intent(ViewIntentBinder)} causes the underlying business logic to do a change and eventually * create a new ViewState. * @param consumer {@link ViewStateConsumer} The consumer that will update ("render") the view. */ @MainThread protected void subscribeViewState(@NonNull Observable viewStateObservable, @NonNull ViewStateConsumer consumer) { if (subscribeViewStateMethodCalled) { throw new IllegalStateException( "subscribeViewState() method is only allowed to be called once"); } subscribeViewStateMethodCalled = true; if (viewStateObservable == null) { throw new NullPointerException("ViewState Observable is null"); } if (consumer == null) { throw new NullPointerException("ViewStateBinder is null"); } this.viewStateConsumer = consumer; viewStateDisposable = viewStateObservable.subscribeWith( new DisposableViewStateObserver<>(viewStateBehaviorSubject)); } /** * Actually subscribes the view as consumer to the internally view relay. * * @param view The mvp view */ @MainThread private void subscribeViewStateConsumerActually(@NonNull final V view) { if (view == null) { throw new NullPointerException("View is null"); } if (viewStateConsumer == null) { throw new NullPointerException(ViewStateConsumer.class.getSimpleName() + " is null. This is a mosby internal bug. Please file an issue at https://github.com/sockeqwe/mosby/issues"); } viewRelayConsumerDisposable = viewStateBehaviorSubject.subscribe(new Consumer() { @Override public void accept(VS vs) throws Exception { viewStateConsumer.accept(view, vs); } }); } /** * This method is called one the view is attached for the very first time to this presenter. * It will not called again for instance during screen orientation changes when the view will be * detached temporarily. * *

* The counter part of this method is {@link #unbindIntents()}. * This {@link #bindIntents()} and {@link #unbindIntents()} are kind of representing the * lifecycle of this Presenter. * {@link #bindIntents()} is called the first time the view is attached * and {@link #unbindIntents()} is called once the view is detached permanently because it has * been destroyed and hence this presenter is not needed anymore and will also be destroyed * afterwards *

*/ @MainThread abstract protected void bindIntents(); /** * This method will be called once the view has been detached permanently and hence the presenter * will be "destroyed" too. This is the correct time for doing some cleanup like unsubscribe from * some RxSubscriptions etc. * * *

* The counter part of this method is {@link #bindIntents()} ()}. * This {@link #bindIntents()} and {@link #unbindIntents()} are kind of representing the * lifecycle of this Presenter. * {@link #bindIntents()} is called the first time the view is attached * and {@link #unbindIntents()} is called once the view is detached permanently because it has * been destroyed and hence this presenter is not needed anymore and will also be destroyed * afterwards *

*/ protected void unbindIntents() { } /** * This method creates a decorator around the original view's "intent". This method ensures that * no * memoryleak by using a {@link ViewIntentBinder} is caused by the subscription to the original * view's intent when the view gets * detached. * * Typically this method is used in {@link #bindIntents()} like this: *

   *   Observable loadIntent = intent(new ViewIntentBinder() {
   *      @Override
   *      public Observable bind(MyView view){
   *         return view.loadIntent();
   *      }
   *   }
   * 
* * @param binder The {@link ViewIntentBinder} from where the the real view's intent will be * bound * @param The type of the intent * @return The decorated intent Observable emitting the intent */ @MainThread protected Observable intent(ViewIntentBinder binder) { PublishSubject intentRelay = PublishSubject.create(); intentRelaysBinders.add(new IntentRelayBinderPair(intentRelay, binder)); return intentRelay; } @MainThread private Observable bindIntentActually(@NonNull V view, @NonNull IntentRelayBinderPair relayBinderPair) { if (view == null) { throw new NullPointerException( "View is null. This is a Mosby internal bug. Please file an issue at https://github.com/sockeqwe/mosby/issues"); } if (relayBinderPair == null) { throw new NullPointerException( "IntentRelayBinderPair is null. This is a Mosby internal bug. Please file an issue at https://github.com/sockeqwe/mosby/issues"); } PublishSubject intentRelay = (PublishSubject) relayBinderPair.intentRelaySubject; if (intentRelay == null) { throw new NullPointerException( "IntentRelay from binderPair is null. This is a Mosby internal bug. Please file an issue at https://github.com/sockeqwe/mosby/issues"); } ViewIntentBinder intentBinder = (ViewIntentBinder) relayBinderPair.intentBinder; if (intentBinder == null) { throw new NullPointerException(ViewIntentBinder.class.getSimpleName() + " is null. This is a Mosby internal bug. Please file an issue at https://github.com/sockeqwe/mosby/issues"); } Observable intent = intentBinder.bind(view); if (intent == null) { throw new NullPointerException( "Intent Observable returned from Binder " + intentBinder + " is null"); } if (intentDisposals == null) { intentDisposals = new CompositeDisposable(); } intentDisposals.add(intent.subscribeWith(new DisposableIntentObserver(intentRelay))); return intentRelay; } }