Java tutorial
/* * Copyright (C) 2017 grandcentrix GmbH * 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 net.grandcentrix.thirtyinch; import net.grandcentrix.thirtyinch.internal.OneTimeRemovable; import android.app.Activity; import android.content.Intent; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v4.app.Fragment; import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; /** * Represents the Presenter of the popular Model-View-Presenter design pattern. If used with {@link * TiActivity} or {@link TiFragment} this presenter survives configuration changes. * * @see TiConfiguration */ public abstract class TiPresenter<V extends TiView> { /** * The LifecycleState of a {@link TiPresenter} */ public enum State { /** * Initial state of the presenter before {@link #create()} got called */ INITIALIZED, /** * presenter is running fine but has no attached view. Either it gets a view and * transitions to {@link #VIEW_ATTACHED} or the presenter gets destroyed -> * {@link * #DESTROYED} */ VIEW_DETACHED, /** * the view is attached. In any case, the next step will be {@link * #VIEW_DETACHED} */ VIEW_ATTACHED, /** * termination state. It will never change again. */ DESTROYED } private static TiConfiguration sDefaultConfig = TiConfiguration.DEFAULT; /** * list of the added observers */ @VisibleForTesting final List<TiLifecycleObserver> mLifecycleObservers = new ArrayList<>(); private final String TAG = this.getClass().getSimpleName() + ":" + TiPresenter.class.getSimpleName() + "@" + Integer.toHexString(this.hashCode()); /** * used to check that lifecycle methods (starting with on..) cannot be called directly. i.e. * {@link #onCreate()} cannot be called. Instead use {@link #create()} which calls {@link * #onCreate()} and makes sure the Presenter has the correct state. */ private boolean mCalled = true; private final TiConfiguration mConfig; private LinkedBlockingQueue<ViewAction<V>> mPostponedViewActions = new LinkedBlockingQueue<>(); private State mState = State.INITIALIZED; /** * Executor for UI operations, must be set by the view implementation */ @Nullable private Executor mUiThreadExecutor; private V mView; public static void setDefaultConfig(final TiConfiguration config) { sDefaultConfig = config; } public TiPresenter() { this(sDefaultConfig); } /** * Constructs a presenter with a different configuration then the default one. Change the * default configuration with {@link #setDefaultConfig(TiConfiguration)} */ public TiPresenter(final TiConfiguration config) { mConfig = config; } /** * Observes the lifecycle state of this presenter. Observers get called in order they are * added for constructive events and in reversed order for destructive events. First in, last * out. * * @param observer called when lifecycle state changes after the lifecycle method such as * {@link * #onCreate()} got called * @return a {@link Removable} allowing to remove the {@link TiLifecycleObserver} from the * {@link TiPresenter} before it reaches its termination state */ public Removable addLifecycleObserver(final TiLifecycleObserver observer) { if (mState == State.DESTROYED) { throw new IllegalStateException("Don't add observers " + "when the presenter reached the DESTROYED state. " + "They wont get any new events anyways."); } mLifecycleObservers.add(observer); return new OneTimeRemovable() { @Override public void onRemove() { mLifecycleObservers.remove(observer); } }; } /** * bind a new view to this presenter. * * @param view the new view, can't be null. To set the view to {@code null} call {@link * #detachView()} */ public void attachView(@NonNull final V view) { //noinspection ConstantConditions if (view == null) { throw new IllegalStateException("the view cannot be set to null. Call #detachView() instead"); } if (isDestroyed()) { throw new IllegalStateException( "The presenter is already in it's terminal state and waits for garbage collection. " + "Binding a view is not allowed"); } if (isViewAttached()) { if (view.equals(mView)) { TiLog.v(TAG, "not calling onAttachView(), view already attached"); return; } else { throw new IllegalStateException("a view is already attached, call #detachView first"); } } if (!isInitialized()) { throw new IllegalStateException("Presenter is not created, call #create() first"); } mView = view; moveToState(State.VIEW_ATTACHED, false); mCalled = false; TiLog.v(TAG, "onAttachView(TiView)"); onAttachView(view); if (!mCalled) { throw new SuperNotCalledException( "Presenter " + this + " did not call through to super.onAttachView(TiView)"); } mCalled = false; TiLog.v(TAG, "deprecated onWakeUp()"); onWakeUp(); if (!mCalled) { throw new SuperNotCalledException("Presenter " + this + " did not call through to super.onWakeUp()"); } moveToState(State.VIEW_ATTACHED, true); sendPostponedActionsToView(view); } /** * Initializes the presenter. This is like the constructor. Keeping things separate allows * manually injecting fields in test cases after initializing the presenter and then start the * work with {@link #create()} * * @see #onCreate() */ public final void create() { if (isInitialized()) { TiLog.w(TAG, "not calling onCreate(), it was already called"); return; } moveToState(State.VIEW_DETACHED, false); mCalled = false; TiLog.v(TAG, "onCreate()"); onCreate(); if (!mCalled) { throw new SuperNotCalledException("Presenter " + this + " did not call through to super.onCreate()"); } moveToState(State.VIEW_DETACHED, true); } /** * Should be called when the view is about to die and will never come back. * <p/> * call this in {@link Fragment#onDestroy()} or {@link Activity#onDestroy()} * * @see #onDestroy() */ public final void destroy() { if (!isInitialized() || isDestroyed()) { TiLog.v(TAG, "not calling onDestroy(), destroy was already called"); return; } moveToState(State.DESTROYED, false); mCalled = false; TiLog.v(TAG, "onDestroy()"); onDestroy(); if (!mCalled) { throw new SuperNotCalledException("Presenter " + this + " did not call through to super.onDestroy()"); } moveToState(State.DESTROYED, true); // release everything, no new states will be posted mLifecycleObservers.clear(); } /** * call detachView as the opposite of {@link #attachView(TiView)}, when the view is not * available anymore. * Calling detachView in {@code Fragment#onDestroyView()} makes sense because observing a * discarded view does not. * * @see #onSleep() */ public final void detachView() { if (!isViewAttached()) { TiLog.v(TAG, "not calling onDetachView(), not woken up"); return; } moveToState(State.VIEW_DETACHED, false); mCalled = false; TiLog.v(TAG, "deprecated onSleep()"); onSleep(); if (!mCalled) { throw new SuperNotCalledException("Presenter " + this + " did not call through to super.onSleep()"); } mCalled = false; TiLog.v(TAG, "onDetachView()"); onDetachView(); if (!mCalled) { throw new SuperNotCalledException( "Presenter " + this + " did not call through to super.onDetachView()"); } moveToState(State.VIEW_DETACHED, true); mView = null; } /** * @return the presenter configuration */ @NonNull public TiConfiguration getConfig() { return mConfig; } /** * @return the current lifecycle state */ @NonNull public State getState() { return mState; } /** * Gets the currently attached view. The view is attached between the lifecycle callbacks * {@link #onAttachView(TiView)} and {@link #onSleep()}. * <p> * If you don't care about the view being attached or detached you should either rethink your * architecture or use {@link #sendToView(ViewAction)} where the action will be executed when * the view is attached. * * @return the currently attached view of this presenter, {@code null} when no view is attached. */ @Nullable public V getView() { return mView; } /** * Gets the currently attached view or throws an {@link IllegalStateException} if the view * is not attached. Use this method if you are sure that a view is currently attached to the * presenter. If you're not sure you should better use {@link #sendToView(ViewAction)} where the * action will be executed when the view is attached. * * @return the currently attached view of this presenter */ @NonNull public V getViewOrThrow() { final V view = getView(); if (view == null) { throw new IllegalStateException( "The view is currently not attached. Use 'sendToView(ViewAction)' instead."); } return view; } public boolean isDestroyed() { return mState == State.DESTROYED; } public boolean isInitialized() { return mState == State.VIEW_DETACHED; } public boolean isViewAttached() { return mState == State.VIEW_ATTACHED; } /** * Runs the specified action on the UI thread. It only works when a view is attached * <p> * When you are looking for a way to execute code when the view got available in the future * have a look at {@link #sendToView(ViewAction)} * * @param action the action to run on the UI thread * @throws IllegalStateException when the executor is not available (most likely because the * view is not attached) */ public void runOnUiThread(@NonNull final Runnable action) { if (mUiThreadExecutor != null) { mUiThreadExecutor.execute(action); } else { if (getView() == null) { throw new IllegalStateException( "view is not attached, " + "no executor available to run ui interactions on"); } else { throw new IllegalStateException("no ui thread executor available"); } } } /** * sets the Executor used for the {@link #runOnUiThread(Runnable)} method. * <p> * This Executor is most likely the {@link net.grandcentrix.thirtyinch.internal.UiThreadExecutor} * posting the work on the Android Main Thread. * When using the {@code TiPresenterInstructor} in your tests an {@link Executor} for the * current {@link Thread} is used, therefore all executed actions run synchronous. * * @param uiThreadExecutor executor for view interactions */ public void setUiThreadExecutor(@Nullable final Executor uiThreadExecutor) { mUiThreadExecutor = uiThreadExecutor; } @Override public String toString() { final String viewName; if (getView() != null) { viewName = getView().toString(); } else { viewName = "null"; } return getClass().getSimpleName() + ":" + TiPresenter.class.getSimpleName() + "@" + Integer.toHexString(hashCode()) + "{view = " + viewName + "}"; } /** * Gives access to the postponed actions while the view is not attached. * * @return the queued actions */ protected Queue<ViewAction<V>> getQueuedViewActions() { return mPostponedViewActions; } /** * The view is now attached and ready to receive events. * * @see #onDetachView() * @see #attachView(TiView) */ protected void onAttachView(@NonNull V view) { if (mCalled) { throw new IllegalAccessError("don't call #onAttachView(TiView) directly, call #attachView(TiView)"); } mCalled = true; } /** * the first lifecycle method after the presenter was created. This will be called only once! * The view is not attached at this state. But doing network requests is possible at this * state. * * @see #create() * @see #onDestroy() */ protected void onCreate() { if (mCalled) { throw new IllegalAccessError("don't call #onCreate() directly, call #create()"); } mCalled = true; } /** * this Presenter is about to die. make a complete cleanup and don't leak anything. i.e. * complete Subjects * * @see #destroy() * @see #onCreate() */ protected void onDestroy() { if (mCalled) { throw new IllegalAccessError("don't call #onDestroy() directly, call #destroy()"); } mCalled = true; } /** * Right after this method the view will be detached. {@link #getView()} will return * <code>null</code> afterwards. * * @see #onAttachView(TiView) * @see #detachView() */ protected void onDetachView() { if (mCalled) { throw new IllegalAccessError("don't call #onDetachView() directly, call #detachView()"); } mCalled = true; } /** * @deprecated use {@link #onDetachView()} instead */ @Deprecated protected void onSleep() { if (mCalled) { throw new IllegalAccessError("don't call #onSleep() directly, call #detachView()"); } mCalled = true; } /** * @deprecated use {@link #onAttachView(TiView)} instead */ @Deprecated protected void onWakeUp() { if (mCalled) { throw new IllegalAccessError("don't call #onWakeUp() directly, call #attachView(TiView)"); } mCalled = true; } /** * Executes the {@link ViewAction} when the view is available on the UI thread. * Once a view is attached the actions get called in the same order they have been added. * When the view is already attached the action will be executed immediately. * <p> * This method might be very useful for single actions which invoke function like {@link * Activity#finish()}, {@link Activity#startActivity(Intent)} or showing a {@link * android.widget.Toast} in the view. * <p> * <b>But don't overuse it.</b> * The action will only be called <b>once</b>. * When a new view attaches (after a configuration change) it doesn't know about the previously * sent actions. * If your using this method too often you should rethink your architecture. * A model which can be bound to the view in {@link #onAttachView(TiView)} and when changes * happen might be a better solution. * See the <a href="https://github.com/passsy/thirtyinch-sample">thirtyinch-sample</a> project * for ideas. * * @see #sendPostponedActionsToView * @see #onAttachView(TiView) */ protected void sendToView(final ViewAction<V> action) { final V view = getView(); if (view != null) { runOnUiThread(new Runnable() { @Override public void run() { action.call(view); } }); } else { mPostponedViewActions.add(action); } } /** * moves the presenter to the new state and validates the correctness of the transition * * @param newState the new state to set */ private void moveToState(final State newState, final boolean hasLifecycleMethodBeenCalled) { final State oldState = mState; if (hasLifecycleMethodBeenCalled) { if (newState != oldState) { throw new IllegalStateException("first call moveToState(<state>, false);"); } } if (newState != oldState) { switch (oldState) { case INITIALIZED: if (newState == State.VIEW_DETACHED) { // move allowed break; } else { throw new IllegalStateException("Can't move to state " + newState + ", the next state after INITIALIZED has to be VIEW_DETACHED"); } case VIEW_DETACHED: if (newState == State.VIEW_ATTACHED) { // move allowed break; } else if (newState == State.DESTROYED) { // move allowed break; } else { throw new IllegalStateException("Can't move to state " + newState + ", the allowed states after VIEW_DETACHED are VIEW_ATTACHED or DESTROYED"); } case VIEW_ATTACHED: // directly moving to DESTROYED is not possible, first detach the view if (newState == State.VIEW_DETACHED) { // move allowed break; } else { throw new IllegalStateException("Can't move to state " + newState + ", the next state after VIEW_ATTACHED has to be VIEW_DETACHED"); } case DESTROYED: throw new IllegalStateException("once destroyed the presenter can't be moved to a different state"); } mState = newState; } switch (newState) { case INITIALIZED: case VIEW_ATTACHED: for (int i = 0; i < mLifecycleObservers.size(); i++) { mLifecycleObservers.get(i).onChange(newState, hasLifecycleMethodBeenCalled); } break; case VIEW_DETACHED: case DESTROYED: // reverse observer order for teardown events; first in, last out for (int i = mLifecycleObservers.size() - 1; i >= 0; i--) { mLifecycleObservers.get(i).onChange(newState, hasLifecycleMethodBeenCalled); } } } /** * Executes all postponed view actions * * @param view where the actions will be sent to */ private void sendPostponedActionsToView(@NonNull final V view) { while (!mPostponedViewActions.isEmpty()) { mPostponedViewActions.poll().call(view); } } }