com.shipdream.lib.android.mvc.MvcFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.shipdream.lib.android.mvc.MvcFragment.java

Source

/*
 * Copyright 2016 Kejun Xia
 *
 * 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.shipdream.lib.android.mvc;

import android.content.res.Configuration;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.shipdream.lib.poke.Graph;
import com.shipdream.lib.poke.exception.PokeException;
import com.shipdream.lib.poke.exception.ProviderMissingException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.CopyOnWriteArrayList;

import javax.inject.Inject;

/**
 * Fragment to help utilize Mvc pattern. {@link #setRetainInstance(boolean)} will be set true by
 * default. Don't set it false which will result unexpected behaviour and life cycles. Controllers
 * and other dependencies can be injected by fields annotated by @{@link Inject}.
 * <p>
 * <p>
 * This fragment uses life cycles slightly different from original Android Fragment.
 * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} is sealed to be overridden which will
 * use the layout provided by {@link #getLayoutResId()} to inflate the view of the fragment.
 * {@link #onViewCreated(View, Bundle)} is sealed too. Instead use {@link #onViewReady(View, Bundle, Reason)}
 * with an extra flag to indicate the {@link Reason}  why the onViewReady is called. Override
 * {@link #onViewReady(View, Bundle, Reason)} to setup views and bind data where all dependencies
 * and restored state will be guaranteed ready.
 * </p>
 * <p>
 * <p>
 * If some actions need to be delayed to invoke after the fragment is ready, use {@link #registerOnViewReadyListener(Runnable)}
 * For example, when the fragment is just instantiated before it's inflated and added to view
 * hierarchy, the fragment is not in ready state to be interacted. In this case, register to run the
 * action after the first {@link #onViewReady(View, Bundle, Reason)} lifecycle of the fragment
 * </p>
 */
public abstract class MvcFragment<CONTROLLER extends FragmentController> extends Fragment implements UiView {
    private final static String STATE_LAST_ORIENTATION = MvcActivity.STATE_PREFIX + "LastOrientation--__";
    private EventRegister eventRegister;
    private CopyOnWriteArrayList<Runnable> onViewReadyListeners;
    private boolean fragmentComesBackFromBackground = false;
    private int lastOrientation;
    private boolean dependenciesInjected = false;
    private Object newInstanceChecker;
    boolean isStateManagedByRootDelegateFragment;
    protected CONTROLLER controller;
    private Graph.Monitor graphMonitor;

    /**
     *
     * Specify the class type of the {@link FragmentController} for this fragment. It's recommended
     * every fragment has a paired controller to deal with the fragment's model and business logic.
     * If the fragment doesn't need a controller simply returns null and {@link MvcFragment#controller}
     * will be null. So it this method returns null, be cautious not to use {@link MvcFragment#controller}.
     *
     * <p/>
     * The fragment will instantiate the {@link FragmentController} by this class type. The
     * instantiated controller will get its fragment lifecycle called automatically by this fragment.
     * @return The class type of the controller
     *
     */
    protected abstract Class<CONTROLLER> getControllerClass();

    /**
     * @return orientation before last orientation change.
     */
    protected int getLastOrientation() {
        return lastOrientation;
    }

    /**
     * @return current orientation
     */
    protected int getCurrentOrientation() {
        return getResources().getConfiguration().orientation;
    }

    /**
     * Assign the layout xml of the <strong>Root View</strong> for this fragment. The layout will be
     * inflated in {@link #onCreateView(android.view.LayoutInflater, ViewGroup, android.os.Bundle)}..
     * <p><strong>Also see</strong> {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}
     * </p>
     *
     * @return The id of the fragment layout
     */
    protected abstract int getLayoutResId();

    private void injectDependencies() {
        if (!dependenciesInjected) {
            if (getControllerClass() != null) {
                try {
                    controller = Mvc.graph().reference(getControllerClass(), null);
                } catch (PokeException e) {
                    throw new IllegalStateException(
                            "Unable to inject " + getControllerClass().getName() + ".\n" + e.getMessage(), e);
                }
            }

            Mvc.graph().inject(this);
            dependenciesInjected = true;
        }
    }

    private void releaseDependencies() {
        if (dependenciesInjected) {
            if (getControllerClass() != null) {
                try {
                    Mvc.graph().dereference(controller, getControllerClass(), null);
                } catch (ProviderMissingException e) {
                    //should never happen
                    Logger logger = LoggerFactory.getLogger(getClass());
                    logger.warn("Failed to dereference controller " + getControllerClass().getName(), e);
                }
            }

            Mvc.graph().release(this);
            dependenciesInjected = false;
        }
    }

    /**
     * Called when the fragment is about to create. Fields annotated by {@link Inject} will be
     * injected in this method.
     * </p>
     * Event2C bus will be registered in this method
     * <p/>
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        graphMonitor = new Graph.Monitor() {
            @Override
            public void onInject(Object target) {
                if (controller != null && target == MvcFragment.this) {
                    controller.view = MvcFragment.this;
                }
            }

            @Override
            public void onRelease(Object target) {
            }
        };
        Mvc.graph().registerMonitor(graphMonitor);

        eventRegister = new EventRegister(this);

        if (savedInstanceState == null) {
            lastOrientation = getResources().getConfiguration().orientation;
        } else {
            lastOrientation = savedInstanceState.getInt(STATE_LAST_ORIENTATION);
        }

        if (getParentFragment() == null) {
            setRetainInstance(true);
        }

        injectDependencies();
    }

    /**
     * This Android lifecycle callback is sealed. {@link MvcFragment} will always use the
     * layout returned by {@link #getLayoutResId()} to inflate the view. Instead, do actions to
     * prepare views in {@link #onViewReady(View, Bundle, Reason)} where all injected dependencies
     * and all restored state will be ready to use.
     *
     * @param inflater           The inflater
     * @param container          The container
     * @param savedInstanceState The savedInstanceState
     * @return The view for the fragment
     */
    @Override
    final public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        injectDependencies();
        return inflater.inflate(getLayoutResId(), container, false);
    }

    /**
     * Called when view is created by before {@link #onViewReady(View, Bundle, Reason)} is called
     */
    void onPreViewReady(View view, Bundle savedInstanceState) {
    }

    /**
     * This Android lifecycle callback is sealed. Use {@link #onViewReady(View, Bundle, Reason)}
     * instead, which provides a flag to indicate why the view is created.
     *
     * @param view               View of this fragment
     * @param savedInstanceState The savedInstanceState: Null when the view is newly created,
     *                           otherwise the state to restore and recreate the view
     */
    @Override
    final public void onViewCreated(final View view, final Bundle savedInstanceState) {
        fragmentComesBackFromBackground = false;
        eventRegister.registerEventBuses();

        onPreViewReady(view, savedInstanceState);

        final boolean restoring = savedInstanceState != null;
        if (restoring && isStateManagedByRootDelegateFragment) {
            ((MvcActivity) getActivity()).addPendingOnViewReadyActions(new Runnable() {
                @Override
                public void run() {
                    doOnViewCreatedCallBack(view, savedInstanceState, restoring);
                }
            });
        } else {
            doOnViewCreatedCallBack(view, savedInstanceState, restoring);
        }
    }

    private void doOnViewCreatedCallBack(View view, Bundle savedInstanceState, boolean restoring) {
        int currentOrientation = getResources().getConfiguration().orientation;
        if (controller != null) {
            controller.orientation = parseOrientation(currentOrientation);
        }

        boolean orientationChanged = currentOrientation != lastOrientation;
        Reason reason = new Reason();

        if (newInstanceChecker == null) {
            newInstanceChecker = new Object();
            reason.isNewInstance = true;
        } else {
            reason.isNewInstance = false;
        }

        if (orientationChanged) {
            reason.isRotated = true;
        }

        if (restoring) {
            reason.isRestored = !reason.isRotated;
        } else if (!orientationChanged && !aboutToPopOut) {
            //When the view is created not by orientation change nor popping out from back stack
            reason.isFirstTime = !reason.isRotated;
        }

        if (aboutToPopOut) {
            reason.isPoppedOut = true;
            aboutToPopOut = false;
        }

        onViewReady(view, savedInstanceState, reason);

        if (reason.isPoppedOut()) {
            onPoppedOutToFront();
        }

        if (orientationChanged) {
            onOrientationChanged(lastOrientation, getResources().getConfiguration().orientation);
        }

        lastOrientation = currentOrientation;

        if (onViewReadyListeners != null) {
            for (Runnable r : onViewReadyListeners) {
                r.run();
            }
        }

        if (controller != null) {
            controller.onViewReady(reason);
        }

        update();
    }

    /**
     * Called when the view of the fragment is ready to use. This also replaces Android lifecycle -
     * {@link #onViewCreated(View, Bundle)} and provide an extra flag to indicate the {@link Reason}
     * why this callback is invoked. This callback also ensure all injected instanced are fully
     * injected which means their own injectable fields are injected as well.
     *
     * @param view               The root view of the fragment
     * @param savedInstanceState The savedInstanceState when the fragment is being recreated after
     *                           its enclosing activity is killed by OS, otherwise null including on
     *                           rotation
     * @param reason             Indicates the {@link Reason} why the onViewReady is called.
     */
    protected void onViewReady(View view, Bundle savedInstanceState, Reason reason) {
    }

    @Override
    public void onResume() {
        super.onResume();
        checkWhetherReturnFromForeground();
        if (controller != null) {
            controller.onResume();
        }
    }

    private void checkWhetherReturnFromForeground() {
        if (fragmentComesBackFromBackground) {
            onReturnForeground();
        }
        fragmentComesBackFromBackground = false;
    }

    /**
     * Called when the fragment resumes without a new view being created. For example, press home
     * button and then bring the app back foreground without rotation or being killed by OS.
     * This method is called after {@link #onResume}
     */
    protected void onReturnForeground() {
        if (controller != null) {
            controller.onReturnForeground();
        }
    }

    /**
     * Called before this fragment is about to be replaced by new fragment and being pushed to fragment
     * back stack.
     */
    protected void onPushToBackStack() {
        if (controller != null) {
            controller.onPushToBackStack();
        }
    }

    /**
     * <p>
     * Called when this fragment is popped out from fragment back stack and will become the top most
     * fragment and present to user. This callback will be invoked after {@link #onViewReady(View, Bundle, Reason)}.
     * </p>
     *
     * <p>
     * For example, current navigation history is A->B->C, when navigate back. The C will be popped
     * out. At this moment,
     * <ul>
     *     <li>C.onPopAway() will be called</li>
     *     <li>B.onPoppedOutToFront will be called</li>
     * </ul>
     * </p>
     */
    protected void onPoppedOutToFront() {
        if (controller != null) {
            controller.onPoppedOutToFront();
        }
    }

    /**
     *
     * <p>
     * Called when the fragment was the top most presenting fragment and will be removed from the
     * fragment back stack and replaced by the fragment under it from the back stack.
     * </p>
     *
     * <p>
     * For example, current navigation history is A->B->C, when navigate back. The C will be popped
     * out. At this moment,
     * <ul>
     *     <li>C.onPopAway() will be called</li>
     *     <li>B.onPoppedOutToFront will be called</li>
     * </ul>
     * </p>
     */
    protected void onPopAway() {
        if (controller != null) {
            controller.onPopAway();
        }
    }

    /**
     * Called when {@link NavigationManager#navigate(Object)} is invoked and this fragment is acting
     * as a navigable page. This fragment will be replaced by the nextFragment. This method is called
     * right before the transaction is committed. This is the ideal place to
     * {@link FragmentTransaction#addSharedElement(View, String)}.
     *
     * @param transaction  The transaction being committing
     * @param nextFragment Next fragment is going to
     */
    protected void onPreNavigationTransaction(FragmentTransaction transaction, MvcFragment nextFragment) {
    }

    boolean aboutToPopOut = false;

    /**
     * Called after {@link #onViewReady(View, Bundle, Reason)} when orientation changed.
     *
     * @param lastOrientation    Orientation before rotation
     * @param currentOrientation Current orientation
     */
    protected void onOrientationChanged(int lastOrientation, int currentOrientation) {
        if (controller != null) {
            controller.onOrientationChanged(parseOrientation(lastOrientation),
                    parseOrientation(currentOrientation));
        }
    }

    private static Orientation parseOrientation(int androidOrientation) {
        if (androidOrientation == Configuration.ORIENTATION_PORTRAIT) {
            return Orientation.PORTRAIT;
        } else if (androidOrientation == Configuration.ORIENTATION_LANDSCAPE) {
            return Orientation.LANDSCAPE;
        } else {
            return Orientation.UNSPECIFIED;
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        fragmentComesBackFromBackground = true;
        if (controller != null) {
            controller.onPause();
        }
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        eventRegister.unregisterEventBuses();
    }

    /**
     * Called when the fragment is no longer in use. This is called after onStop() and before onDetach().
     * Event2C bus will be unregistered in the method.
     * <p>
     * <p><b>Note that, when a new fragment to create and pushes this fragment to back stack,
     * onDestroy of this fragment will NOT be called. This method will be called until this fragment
     * is removed completely.</b></p>
     */
    @Override
    public void onDestroy() {
        super.onDestroy();

        releaseDependencies();

        Mvc.graph().unregisterMonitor(graphMonitor);
        eventRegister = null;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt(STATE_LAST_ORIENTATION, lastOrientation);
    }

    /**
     * Register a callback after {@link #onViewReady(View, Bundle, Reason)} is called. This is useful when an action that
     * can't be executed after the fragment is instantiated but before the fragment has gone through
     * life cycles and gets created and ready to use. If this is one time action, use
     * {@link #unregisterOnViewReadyListener(Runnable)} (Runnable)}  unregister itself in the given onCreateViewAction.
     *
     * @param action The action to registered to be run after view is ready
     */
    public void registerOnViewReadyListener(Runnable action) {
        if (onViewReadyListeners == null) {
            onViewReadyListeners = new CopyOnWriteArrayList<>();
        }
        onViewReadyListeners.add(action);
    }

    /**
     * Unregister the callback that to be called in {@link #onViewReady(View, Bundle, Reason)}
     *
     * @param action The action to unregistered
     */
    public void unregisterOnViewReadyListener(Runnable action) {
        if (onViewReadyListeners != null) {
            onViewReadyListeners.remove(action);
        }
    }

    /**
     * Unregister all actions to be called after {@link #onViewReady(View, Bundle, Reason)}
     */
    public void clearOnViewReadyListener() {
        if (onViewReadyListeners != null) {
            onViewReadyListeners.clear();
        }
    }

    /**
     * Overrides this method when this fragment needs to handle its own business on back button
     * pressed. If this fragment wants to intercept and swallow the back button pressed event, return
     * true, otherwise return false. In other words, when false is returned, the fragment will be
     * dismissed, otherwise only the logic in this method will be executed but the fragment remains
     * in the front.
     *
     * <p>By default, if this fragment has a corresponding controller it delegates the call to
     * {@link FragmentController#onBackButtonPressed()} otherwise returns false.</p>
     *
     * @return True to consume the back button pressed event, otherwise returns false which will
     * forward the back button pressed event to other views
     */
    public boolean onBackButtonPressed() {
        if (controller != null) {
            return controller.onBackButtonPressed();
        } else {
            return false;
        }
    }

    /**
     * Handy method to post an event to other views directly. However, when possible, it's
     * recommended to post events from controllers to views.
     *
     * @param event The event send to other views
     */
    protected void postEvent2V(Object event) {
        eventRegister.postEvent2V(event);
    }

}