diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/Conductor.java b/conductor/src/main/java/com/bluelinelabs/conductor/Conductor.java index f1d0d020..0decbe95 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/Conductor.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/Conductor.java @@ -41,4 +41,7 @@ public static Router attachRouter(@NonNull Activity activity, @NonNull ViewGroup return router; } + public static void setControllerFactory(@NonNull ControllerFactory factory) { + Controller.FACTORY = factory; + } } diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java b/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java index cbb05f10..a5e174ac 100644 --- a/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java +++ b/conductor/src/main/java/com/bluelinelabs/conductor/Controller.java @@ -92,14 +92,13 @@ public abstract class Controller { private boolean isPerformingExitTransition; private boolean isContextAvailable; + static volatile ControllerFactory FACTORY = DefaultControllerFactory.INSTANCE; + @NonNull static Controller newInstance(@NonNull Bundle bundle) { final String className = bundle.getString(KEY_CLASS_NAME); //noinspection ConstantConditions Class cls = ClassUtils.classForName(className, false); - Constructor[] constructors = cls.getConstructors(); - Constructor bundleConstructor = getBundleConstructor(constructors); - Bundle args = bundle.getBundle(KEY_ARGS); if (args != null) { args.setClassLoader(cls.getClassLoader()); @@ -107,15 +106,20 @@ static Controller newInstance(@NonNull Bundle bundle) { Controller controller; try { - if (bundleConstructor != null) { - controller = (Controller)bundleConstructor.newInstance(args); - } else { - //noinspection ConstantConditions - controller = (Controller)getDefaultConstructor(constructors).newInstance(); - - // Restore the args that existed before the last process death - if (args != null) { - controller.args.putAll(args); + controller = FACTORY.create(className, args); + if (controller == null) { + Constructor[] constructors = cls.getConstructors(); + Constructor bundleConstructor = getBundleConstructor(constructors); + if (bundleConstructor != null) { + controller = (Controller) bundleConstructor.newInstance(args); + } else { + //noinspection ConstantConditions + controller = (Controller) getDefaultConstructor(constructors).newInstance(); + + // Restore the args that existed before the last process death + if (args != null) { + controller.args.putAll(args); + } } } } catch (Exception e) { @@ -1321,6 +1325,10 @@ final void setParentController(@Nullable Controller controller) { } private void ensureRequiredConstructor() { + if (FACTORY != DefaultControllerFactory.INSTANCE) { + // We assume you know what you're doing if you've installed a ControllerFactory + return; + } Constructor[] constructors = getClass().getConstructors(); if (getBundleConstructor(constructors) == null && getDefaultConstructor(constructors) == null) { throw new RuntimeException(getClass() + " does not have a constructor that takes a Bundle argument or a default constructor. Controllers must have one of these in order to restore their states."); diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/ControllerFactory.java b/conductor/src/main/java/com/bluelinelabs/conductor/ControllerFactory.java new file mode 100644 index 00000000..71fd42f4 --- /dev/null +++ b/conductor/src/main/java/com/bluelinelabs/conductor/ControllerFactory.java @@ -0,0 +1,10 @@ +package com.bluelinelabs.conductor; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public interface ControllerFactory { + @Nullable + Controller create(@NonNull String controllerName, @Nullable Bundle args); +} diff --git a/conductor/src/main/java/com/bluelinelabs/conductor/DefaultControllerFactory.java b/conductor/src/main/java/com/bluelinelabs/conductor/DefaultControllerFactory.java new file mode 100644 index 00000000..960c3b39 --- /dev/null +++ b/conductor/src/main/java/com/bluelinelabs/conductor/DefaultControllerFactory.java @@ -0,0 +1,16 @@ +package com.bluelinelabs.conductor; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +final class DefaultControllerFactory implements ControllerFactory { + + static final DefaultControllerFactory INSTANCE = new DefaultControllerFactory(); + + @Nullable + @Override + public final Controller create(@NonNull String controllerName, @Nullable Bundle args) { + return null; + } +} diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/ControllerFactoryTests.java b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerFactoryTests.java new file mode 100644 index 00000000..a7106270 --- /dev/null +++ b/conductor/src/test/java/com/bluelinelabs/conductor/ControllerFactoryTests.java @@ -0,0 +1,66 @@ +package com.bluelinelabs.conductor; + +import android.os.Bundle; +import com.bluelinelabs.conductor.util.ActivityProxy; +import com.bluelinelabs.conductor.util.TestController; +import com.bluelinelabs.conductor.util.TestControllerFactory; +import com.bluelinelabs.conductor.util.TestDependenciesController; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertEquals; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class ControllerFactoryTests { + + private Router router; + + private ActivityProxy activityProxy; + + public void createActivityController(Bundle savedInstanceState, boolean includeStartAndResume) { + activityProxy = new ActivityProxy().create(savedInstanceState); + + if (includeStartAndResume) { + activityProxy.start().resume(); + } + router = Conductor.attachRouter(activityProxy.getActivity(), activityProxy.getView(), savedInstanceState); + if (!router.hasRootController()) { + router.setRoot(RouterTransaction.with(new TestController())); + } + } + + @Before + public void setup() { + Conductor.setControllerFactory(new TestControllerFactory()); + createActivityController(null, true); + } + + @Test + public void testControllerWithDependenciesRecreated() { + Bundle args = new Bundle(); + args.putBoolean(TestDependenciesController.WAS_RECREATED_WITH_BUNDLE_ARGS, true); + TestDependenciesController controller = new TestDependenciesController(args, false); + router.pushController(RouterTransaction.with(controller) + .tag("root")); + activityProxy.getActivity().isChangingConfigurations = true; + + assertEquals(false, controller.wasCreatedByFactory); + assertEquals(false, controller.wasInstanceStateRestored); + + Bundle bundle = new Bundle(); + activityProxy.saveInstanceState(bundle); + activityProxy.pause(); + activityProxy.stop(true); + activityProxy.destroy(); + + createActivityController(bundle, false); + controller = (TestDependenciesController)router.getControllerWithTag("root"); + assertEquals(true, controller.wasCreatedByFactory); + assertEquals(true, controller.wasInstanceStateRestored); + assertEquals(true, controller.wasRecreatedWithBundleArgs); + } +} diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/util/TestControllerFactory.java b/conductor/src/test/java/com/bluelinelabs/conductor/util/TestControllerFactory.java new file mode 100644 index 00000000..a12eea41 --- /dev/null +++ b/conductor/src/test/java/com/bluelinelabs/conductor/util/TestControllerFactory.java @@ -0,0 +1,20 @@ +package com.bluelinelabs.conductor.util; + +import android.os.Bundle; + +import com.bluelinelabs.conductor.Controller; +import com.bluelinelabs.conductor.ControllerFactory; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class TestControllerFactory implements ControllerFactory { + @Nullable + @Override + public Controller create(@NonNull String controllerName, @Nullable Bundle args) { + if (controllerName.equals(TestDependenciesController.class.getName())) { + return new TestDependenciesController(args, true); + } + return null; + } +} diff --git a/conductor/src/test/java/com/bluelinelabs/conductor/util/TestDependenciesController.java b/conductor/src/test/java/com/bluelinelabs/conductor/util/TestDependenciesController.java new file mode 100644 index 00000000..acd824a9 --- /dev/null +++ b/conductor/src/test/java/com/bluelinelabs/conductor/util/TestDependenciesController.java @@ -0,0 +1,41 @@ +package com.bluelinelabs.conductor.util; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.bluelinelabs.conductor.Controller; + +import androidx.annotation.NonNull; + +public class TestDependenciesController extends Controller { + + public static final String WAS_RECREATED_WITH_BUNDLE_ARGS = "WAS_RECREATED_WITH_BUNDLE_ARGS"; + private static final String INSTANCE_STATE = "INSTANCE_STATE"; + public final Boolean wasCreatedByFactory; + public Boolean wasInstanceStateRestored = false; + public final Boolean wasRecreatedWithBundleArgs; + + public TestDependenciesController(Bundle args, Boolean wasCreatedByFactory) { + super(args); + this.wasRecreatedWithBundleArgs = args.getBoolean(WAS_RECREATED_WITH_BUNDLE_ARGS, false); + this.wasCreatedByFactory = wasCreatedByFactory; + } + + @NonNull + @Override + protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { + return new AttachFakingFrameLayout(inflater.getContext()); + } + + @Override protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(INSTANCE_STATE, true); + } + + @Override protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + wasInstanceStateRestored = savedInstanceState.getBoolean(INSTANCE_STATE); + } +}