Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] Replacing one controller with another upon configuration change #636

Open
K1rakishou opened this issue Dec 4, 2020 · 3 comments

Comments

@K1rakishou
Copy link

I have a controller in which I want to insert another controller, conditionally. If the phone is in portrait layout I want to add Portrait controller, if in landscape or if an additional setting is turned on then Landscape controller. I'm doing it the following way:

    // onControllerCreated is basically onCreateView()
    override fun onControllerCreated(savedViewState: Bundle?) {
    super.onControllerCreated(savedViewState)

    // AndroidUtils.isSplitMode() is basically "orientation==LANDSCAPE" but it is also manually configurable
    val controller = if (AndroidUtils.isSplitMode(currentContext())) {
      SplitHomeController()
    } else {
      HomeController()
    }

    val transaction = RouterTransaction.with(controller)
      .tag(controller.getControllerTag().tag)

    getChildRouter(contentContainer).setRoot(transaction)
  }

I'm using the same view (contentContainer) for both controllers and also using RetainViewMode.RETAIN_DETACH for all controllers.

The issue I'm having with this approach is that upon config change the old HomeController is still in the stack, it's getting rebound and then is destroyed (after getChildRouter(contentContainer).setRoot(transaction)) which is expected. I'm getting the following log:

// Normal app start (the phone is in landscape orientation)
2020-12-04 14:46:19.995 D/BaseController: MainController onCreateView()
2020-12-04 14:46:20.005 D/BaseController: SplitHomeController onCreateView()
2020-12-04 14:46:20.216 D/BaseController: HomeController onCreateView()
2020-12-04 14:46:20.436 D/BaseController: SplitBrowseController onCreateView()
2020-12-04 14:46:20.556 D/BaseController: CatalogController onCreateView()
2020-12-04 14:46:20.624 D/BaseController: SplitThreadController onCreateView()
2020-12-04 14:46:20.679 D/BaseController: MainController onAttach()
2020-12-04 14:46:20.680 D/BaseController: SplitHomeController onAttach()
2020-12-04 14:46:20.680 D/BaseController: HomeController onAttach()
2020-12-04 14:46:20.680 D/BaseController: SplitBrowseController onAttach()
2020-12-04 14:46:20.680 D/BaseController: CatalogController onAttach()
2020-12-04 14:46:20.681 D/BaseController: SplitThreadController onAttach()


// The app start with the phone in portrait orientation and then is rotated (logs are taken when rotation occurs)
2020-12-04 14:46:35.528 D/BaseController: MainController onDetach()
2020-12-04 14:46:35.528 D/BaseController: HomeController onDetach()
2020-12-04 14:46:35.528 D/BaseController: SlideBrowseController onDetach()
2020-12-04 14:46:35.528 D/BaseController: CatalogController onDetach()
2020-12-04 14:46:35.528 D/BaseController: ThreadController onDetach()
2020-12-04 14:46:35.531 D/BaseController: MainController onDestroyView()
2020-12-04 14:46:35.531 D/BaseController: HomeController onDestroyView()
2020-12-04 14:46:35.532 D/BaseController: SlideBrowseController onDestroyView()
2020-12-04 14:46:35.532 D/BaseController: CatalogController onDestroyView()
2020-12-04 14:46:35.532 D/BaseController: ThreadController onDestroyView()
2020-12-04 14:46:35.574 D/BaseController: MainController onCreateView()
2020-12-04 14:46:35.614 D/BaseController: HomeController onCreateView()
2020-12-04 14:46:35.620 D/BaseController: SlideBrowseController onCreateView()
2020-12-04 14:46:35.626 D/BaseController: CatalogController onCreateView()
2020-12-04 14:46:35.632 D/BaseController: ThreadController onCreateView()
2020-12-04 14:46:35.670 D/BaseController: MainController onAttach()
2020-12-04 14:46:36.029 D/BaseController: SplitHomeController onCreateView()
2020-12-04 14:46:36.051 D/BaseController: HomeController onCreateView()
2020-12-04 14:46:36.055 D/BaseController: SplitBrowseController onCreateView()
2020-12-04 14:46:36.057 D/BaseController: CatalogController onCreateView()
2020-12-04 14:46:36.065 D/BaseController: SplitThreadController onCreateView()
2020-12-04 14:46:36.068 D/BaseController: HomeController onDestroyView()
2020-12-04 14:46:36.069 D/BaseController: SlideBrowseController onDestroyView()
2020-12-04 14:46:36.069 D/BaseController: CatalogController onDestroyView()
2020-12-04 14:46:36.069 D/BaseController: ThreadController onDestroyView()
2020-12-04 14:46:36.072 D/BaseController: SplitHomeController onAttach()
2020-12-04 14:46:36.072 D/BaseController: HomeController onAttach()
2020-12-04 14:46:36.072 D/BaseController: SplitBrowseController onAttach()
2020-12-04 14:46:36.072 D/BaseController: CatalogController onAttach()
2020-12-04 14:46:36.073 D/BaseController: SplitThreadController onAttach()

While everything seems to be working I wonder if there is an official way to handle this situation (without old controller getting recteated)? Maybe I can somehow remove this controller during the call Conductor.attachRouter(this, rootContainer, savedInstanceState) before setting the root controller onto the main router?

I saw the MasterDetailController demo and that it uses two containers when the phone is in landscape orientation and two child routers but I wonder whether it's possible to only have one container.

@K1rakishou
Copy link
Author

K1rakishou commented Dec 10, 2020

A little update. I figured out that maybe I should use Router.setBackstack() method to replace previous backstack with a new one without the controller that I want to remove but now I'm getting NPEs because the "container" view is null since I'm trying to do this before calling rebindIfNeeded().

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    rootContainer = findViewById(R.id.root_container)
    router = attachRouterHacky(this, rootContainer, savedInstanceState)

    if (!router.hasRootController()) {
      val controller = MainController()
      controller.setControllerPresenterDelegate(this)

      router.setRoot(RouterTransaction.with(controller))
    }
  }

  private fun attachRouterHacky(
    activity: Activity,
    container: ViewGroup,
    savedInstanceState: Bundle?
  ): Router {
    BackgroundUtils.ensureMainThread()
    val isSplitMode = isSplitMode(activity)
    val router = LifecycleHandler.install(activity)
      .getRouter(container, savedInstanceState)

    if (savedInstanceState != null) {
      val controllerTag = if (isSplitMode) {
        SlideUiElementsController.CONTROLLER_TAG
      } else {
        SplitNavController.CONTROLLER_TAG
      }

      val result = router.findRouterWithControllerByTag(controllerTag)
      if (result != null) {
        val (foundRouter, foundController) = result
        val backstackCopy = foundRouter.backstack

        val index = backstackCopy.indexOfFirst { routerTransaction ->
          (routerTransaction.controller as? BaseController)?.getControllerTag() == controllerTag
        }

        if (index >= 0) {
          backstackCopy.removeAt(index)
          foundRouter.setBackstack(backstackCopy, null)
        }
      }
    }

    router.rebindIfNeeded()
    return router
  }
     Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'int android.view.ViewGroup.getChildCount()' on a null object reference
        at com.bluelinelabs.conductor.Router.removeAllExceptVisibleAndUnowned(Router.java:901)
        at com.bluelinelabs.conductor.Router.setBackstack(Router.java:409)
2020-12-10 16:44:24.715 E/KurobaEx:     at com.bluelinelabs.conductor.ControllerHostedRouter.setBackstack(ControllerHostedRouter.java:113)
        at com.github.k1rakishou.kurobanewnavstacktest.activity.MainActivity.attachRouterHacky(MainActivity.kt:119)
        at com.github.k1rakishou.kurobanewnavstacktest.activity.MainActivity.onCreate(MainActivity.kt:41)
        at android.app.Activity.performCreate(Activity.java:8000)
        at android.app.Activity.performCreate(Activity.java:7984)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422)
        	... 13 more

I wonder whether there is a way to make this work.

K1rakishou added a commit to K1rakishou/KurobaExNewNavigationSystemTestProject that referenced this issue Dec 10, 2020
…change from Slide to Split layout

which lead to nothing (couple of other bugs were fixed though). The problem is in inability (at least
I don't know how) to remove controllers from backstack upon config change before
calling router.rebindIfNeeded() (it results in NPEs, maybe it's a bug). Waiting for the devs' response
bluelinelabs/Conductor#636
@EricKuck
Copy link
Member

I suppose there is no ideal way to do what you're looking for. Your original attempt is the best bet given the currently available APIs. If you have a suggestion for an API improvement I would be open to considering it, but I'm not sure of a good way to facilitate something like this.

@K1rakishou
Copy link
Author

I actually managed to find a hacky solution: originally I was trying to replace a child controller in the backstack (SlideUiElementsController and SplitNavController are children of MainController) and it didn't work because at that stage those child controllers do not have the container view (and it crashes), however this works when replacing MainController which is the root in my case. The state of all controllers is lost but that is expected and not a problems in my case since I store everything in ViewModels.

If you have a suggestion for an API improvement I would be open to considering it

I guess the current API is fine, but having an ability to remove/replace child controller somewhere deep in the backstack before router.rebindIfNeeded() is called would be nice, right now it simply crashes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants