Skip to content

Commit

Permalink
Recreate views on config changes
Browse files Browse the repository at this point in the history
This is typically necessary when the Activity has specified in the manifest that it will handle configuration changes such as an orientation change.

If the top Controller.setRecreateViewOnConfigChange(true) has been called before a configuration change happens, that Controller's current view will be removed and a recreated view will be added to the parent.

For Controllers that are lower in the stack, those Controllers' view will only be recreated if the Controller also has a RetainViewMode of RetainViewMode#RETAIN_DETACH.
  • Loading branch information
Chris Hanson committed Jan 27, 2017
1 parent 44bcd0f commit 90b7add
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 0 deletions.
43 changes: 43 additions & 0 deletions conductor/src/main/java/com/bluelinelabs/conductor/Controller.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public abstract class Controller {
private final ArrayList<RouterRequiringFunc> onRouterSetListeners = new ArrayList<>();
private final List<Controller> childBackstack = new LinkedList<>();
private WeakReference<View> destroyedView;
private boolean recreateViewOnConfigChange;

private final OnControllerPushedListener onControllerPushedListener = new OnControllerPushedListener() {
@Override
Expand Down Expand Up @@ -601,6 +602,33 @@ public void setRetainViewMode(@NonNull RetainViewMode retainViewMode) {
}
}

/**
* Returns whether this controller should recreate its view when configuration changes.
*
* @see #setRecreateViewOnConfigChange(boolean)
*/
public boolean isRecreateViewOnConfigChange() {
return recreateViewOnConfigChange;
}

/**
* Sets whether this Controller should recreate its view when configuration changes. This is
* typically necessary when the Activity has specified in the manifest that it will handle
* configuration changes such as an orientation change.
*
* For this method to have any effect, the Activity should set a value for {@code android:configChanges}
* in the manifest. Otherwise, the Activity will be destroyed and recreated, and there will be
* no need to explicitly recreate the view outside of that.
*
* If the top Controller has set this to {@code true} when a configuration change happens, that
* Controller's current view will be removed and a recreated view will be added to the parent.
* For Controllers that are lower in the stack, those Controllers' view will only be recreated
* if the Controller also has a {@link RetainViewMode} of {@link RetainViewMode#RETAIN_DETACH}.
*/
public void setRecreateViewOnConfigChange(boolean recreateViewOnConfigChange) {
this.recreateViewOnConfigChange = recreateViewOnConfigChange;
}

/**
* Returns the {@link ControllerChangeHandler} that should be used for pushing this Controller, or null
* if the handler from the {@link RouterTransaction} should be used instead.
Expand Down Expand Up @@ -878,6 +906,21 @@ final View inflate(@NonNull ViewGroup parent) {
removeViewReference();
}

return inflateAfterCheckingView(parent);
}

final View reinflate(@NonNull ViewGroup parent) {
if (view != null) {
detach(view, true);
removeViewReference();
}

view = null;

return inflateAfterCheckingView(parent);
}

private View inflateAfterCheckingView(@NonNull ViewGroup parent) {
if (view == null) {
List<LifecycleListener> listeners = new ArrayList<>(lifecycleListeners);
for (LifecycleListener lifecycleListener : listeners) {
Expand Down
35 changes: 35 additions & 0 deletions conductor/src/main/java/com/bluelinelabs/conductor/Router.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
Expand Down Expand Up @@ -564,6 +565,40 @@ public final boolean onOptionsItemSelected(@NonNull MenuItem item) {
return false;
}

/**
* @see Controller#setRecreateViewOnConfigChange(boolean)
*/
public final void onConfigurationChanged(Configuration newConfig) {
if (backstack.isEmpty()) {
return;
}

RouterTransaction topTransaction = backstack.peek();
Controller topController = topTransaction.controller;
if (topController.isRecreateViewOnConfigChange()) {
View currentView = topController.getView();
if (currentView != null) {
container.removeView(currentView);
}

View newView = topController.reinflate(container);
container.addView(newView);
}

for (RouterTransaction transaction : backstack) {
if (transaction == topTransaction) {
continue;
}

Controller controller = transaction.controller;
if (controller.isRecreateViewOnConfigChange()
&& controller.getRetainViewMode() == Controller.RetainViewMode.RETAIN_DETACH) {

controller.reinflate(container);
}
}
}

private void popToTransaction(@NonNull RouterTransaction transaction, @Nullable ControllerChangeHandler changeHandler) {
RouterTransaction topTransaction = backstack.peek();
List<RouterTransaction> poppedTransactions = backstack.popTo(transaction);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import android.app.Fragment;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
Expand Down Expand Up @@ -261,6 +262,15 @@ public boolean onOptionsItemSelected(MenuItem item) {
return super.onOptionsItemSelected(item);
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);

for (Router router : routerMap.values()) {
router.onConfigurationChanged(newConfig);
}
}

public void registerForActivityResult(@NonNull String instanceId, int requestCode) {
activityRequestMap.put(requestCode, instanceId);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.bluelinelabs.conductor;

import android.view.View;

import com.bluelinelabs.conductor.util.ActivityProxy;
import com.bluelinelabs.conductor.util.ListUtils;
import com.bluelinelabs.conductor.util.MockChangeHandler;
Expand All @@ -11,10 +13,13 @@
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

Expand Down Expand Up @@ -312,4 +317,87 @@ public void testReplaceTopControllerWithNoRemoveViewOnPush() {
assertTrue(newTopTransaction.controller.isAttached());
}

@Test
public void testRecreateViewsOnConfigChange_notRecreatesTopControllerView() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());

List<RouterTransaction> backstack = new ArrayList<>();
backstack.add(rootTransaction);
backstack.add(topTransaction);

router.setBackstack(backstack, null);

View originalTopView = topTransaction.controller.getView();
router.onConfigurationChanged(null);
View newTopView = topTransaction.controller.getView();

assertNotNull(originalTopView);
assertNotNull(newTopView);
assertEquals(originalTopView, newTopView);
}

@Test
public void testRecreateViewsOnConfigChange_recreatesTopControllerView() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
topTransaction.controller.setRecreateViewOnConfigChange(true);

List<RouterTransaction> backstack = new ArrayList<>();
backstack.add(rootTransaction);
backstack.add(topTransaction);

router.setBackstack(backstack, null);

View originalTopView = topTransaction.controller.getView();
router.onConfigurationChanged(null);
View newTopView = topTransaction.controller.getView();

assertNotNull(originalTopView);
assertNotNull(newTopView);
assertNotEquals(originalTopView, newTopView);
}

@Test
public void testRecreateViewsOnConfigChange_notRecreatesLowerControllerView() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());

List<RouterTransaction> backstack = new ArrayList<>();
backstack.add(rootTransaction);
backstack.add(topTransaction);

router.setBackstack(backstack, null);

View originalRootView = rootTransaction.controller.getView();
router.onConfigurationChanged(null);
View newRootView = rootTransaction.controller.getView();

assertNotNull(originalRootView);
assertNotNull(newRootView);
assertEquals(originalRootView, newRootView);
}

@Test
public void testRecreateViewsOnConfigChange_recreatesLowerControllerView() {
RouterTransaction rootTransaction = RouterTransaction.with(new TestController());
RouterTransaction topTransaction = RouterTransaction.with(new TestController()).pushChangeHandler(MockChangeHandler.noRemoveViewOnPushHandler());
rootTransaction.controller.setRecreateViewOnConfigChange(true);
rootTransaction.controller.setRetainViewMode(Controller.RetainViewMode.RETAIN_DETACH);

List<RouterTransaction> backstack = new ArrayList<>();
backstack.add(rootTransaction);
backstack.add(topTransaction);

router.setBackstack(backstack, null);

View originalRootView = rootTransaction.controller.getView();
router.onConfigurationChanged(null);
View newRootView = rootTransaction.controller.getView();

assertNotNull(originalRootView);
assertNotNull(newRootView);
assertNotEquals(originalRootView, newRootView);
}

}

0 comments on commit 90b7add

Please sign in to comment.