/* * 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); } }