Skip to content

Migrating from Magellan 1.x

Chris Mathew edited this page Nov 5, 2021 · 11 revisions

Welcome to Magellan! We're working hard on an update; this is the work-in-progress documentation for that update. For the old documentation, see our old wiki page, Magellan 1.x Home. If you have questions/comments/suggestions for the new documentation, please submit an issue and we'll explain ourselves better.

Backwards compatibility

We put in a lot of work to make Screens work almost exactly the same as before. The few breaking changes can be found below, but for the most part, most Screen logic will work just fine.

Complete list of breaking changes

Coming soon!

Migrating Screens to Magellan 2.x classes

You cannot use Screens with Journey navigators - you must use Magellan 2.x base classes instead. This means you'll likely need to migrate existing Screens accordingly when linking 1.x and 2.x code together.

The biggest difference between Magellan 1.x's Screens and 2.x's Steps is that Screen classes are bound to a corresponding ScreenView by the framework. In contrast, Steps occupy a 'vertical slice' your application - by default implementing both business and view logic. Given this, there are a few ways to transform an existing Screen into Magellan 2.x paradigms:

The first, and most obvious, is to directly convert the Screen/ScreenView pair into a Step: 1.) merge the view logic found in your ScreenView with your Screen and 2.) replace inline view inflation with View Binding.

For a more flexible approach, change your existing Screens to extend LegacyStep as necessary. This allows you to separate Controllers and Views using familiar 1.x paradigms, while still extending 2.x's base classes.

Interoperation with Magellan 1.x navigation

While adopting Magellan 2.x, you will most likely have a period when some of your codebase is still using Magellan 1.x paradigms. Much of this can be rectified by creating Journeys for new UI, and using LegacyStep as described above.

However, sometimes this isn't feasible, so we must do something to rectify the very different navigation models of 1.x and 2.x. If you're unfamiliar with Magellan 2.x's navigation paradigm, see Thinking in Magellan. To summarize: Magellan 1.x's backstack has a flat, linked-list-like structure, where each screen is responsible for navigating to the next screen; Magellan 2.x has a tree-like, parent-focused structure, where parent Journeys are responsible for navigating between their children.

To rectify these, attach a LegacyExpedition to your Activity. This serves as a root Journey with a 1.x style backstack, allowing navigational interop at the top level.

Magellan 1.x navigation

Magellan 2.x navigation

Navigating from 1.x Screens → 2.x Steps/Journeys

Recall that in idiomatic Magellan 2.x, navigation logic should be confined to navigator-owning objects. Therefore, while each Screen has a navigator reference, it's now considered a bad practice to use it directly. We'll need some creativity to properly scope our 1.x -> 2.x navigation operations.

While your legacy code will lack the tree-like navigational structure of 2.x described above, you can think of your existing navigation graph as living under a 'root' Journey (see LegacyExpedition above). Playing this idea forward, we can achieve 2.x paradigms simply by placing any new navigation logic inside our root Journey class, and then injecting this Journey into Screens which require this navigation logic.

class MyExpedition : LegacyExpedition(...) {
  fun goTo2x() {
    navigator.goTo(Magellan2xJourney())
  }
}

class Magellan1xScreen : Screen<...>(...) {
  @Inject lateinit var expedition: MyExpedition

  fun goTo2x(): {
    expedition.goTo2x()
  }
}

Of course at scale, this root Journey class will become packed with many navigation operations. You can solve this by factoring the Journey's navigation logic into smaller, injectable 'router' classes that rely on the Expedition’s Navigator to navigate between pages.

Warning: avoid injecting these routers into Magellan 2.x components (Journeys or Steps), as this would allow children to navigate away from their parents without the parents' knowledge. While this is not technically wrong, it breaks navigational encapsulation, making it much harder to reason about. This is a similar idea to why all coroutines must live in a CoroutineScope.

Navigating from 2.x Steps/Journeys → 1.x Screens

Navigating from 2.x to 1.x requires less creativity, as there is only one way to do it without breaking encapsulation: pass a navigational lambda down from the Expedition.

While it can be painful if you have many layers between the Expedition and the Journey requesting this navigation, this is actually the same pattern as you would use for any top-level (Expedition-level) navigation. It honors the navigational encapsulation that Journeys provide, i.e. that child must perform navigation operations through their parent.

class MyExpedition : LegacyExpedition(...) {
  fun goTo1x() {
    navigator.goTo(Magellan1xScreen())
  }

  fun goTo2x() {
    navigator.goTo(Magellan2xJourney { goTo1x() })
  }
}

class Magellan2xJourney(
  val goToMagellan1x: () -> Unit
) : Journey<...>(...) {
  fun onCreate(): {
    navigator.goTo(MyStep(goToMagellan1x))
  }
}

class MyStep(
  val goToMagellan1x: () -> Unit
) : Step<...>(...) {
  fun onSomethingClicked() {
    goToMagellan1x()
  }
}