From fcbf21ecb9b5468d4ed705b238e7f6c2575d9d55 Mon Sep 17 00:00:00 2001 From: Zsolt Kocsi Date: Mon, 21 Nov 2022 16:23:31 +0000 Subject: [PATCH 1/4] Add section on implicit navigation --- .../navigation/composable-navigation.md | 5 +- .../navigation/implicit-navigation.md | 61 +++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 documentation/navigation/implicit-navigation.md diff --git a/documentation/navigation/composable-navigation.md b/documentation/navigation/composable-navigation.md index e5b957eed..3dd483d72 100644 --- a/documentation/navigation/composable-navigation.md +++ b/documentation/navigation/composable-navigation.md @@ -21,10 +21,13 @@ Read more in [Structuring your app navigation](../apps/structure.md) Once you've structured your navigation in a composable way, you can add `NavModels` to `Node` of this tree and make it dynamic: - Some parts in this tree are active while others ore not -- The activate parts define what state the application is in, and what the user sees on the screen +- The active parts define what state the application is in, and what the user sees on the screen - We can change what's active by using `NavModels` on each level of the tree - Changes will feel like navigation to the user +See [Implicit navigation](implicit-navigation.md) and [Explicit navigation](explicit-navigation.md) for building complex navigation behaviours with this approach. + + ## How NavModels affect Nodes diff --git a/documentation/navigation/implicit-navigation.md b/documentation/navigation/implicit-navigation.md new file mode 100644 index 000000000..f10f38892 --- /dev/null +++ b/documentation/navigation/implicit-navigation.md @@ -0,0 +1,61 @@ +# Implicit navigation + + + +How can we go from one part of the tree to another? In almost all cases navigation can be implicit instead of explicit. We don't need to specify the target – navigation will happen as a consequence of individual pieces of the puzzle. + +!!! info "Relevant methods" + + - `ParentNode.onChildFinished(child: Node)` can be overridden by client code to handle a child finishing + - `Node.finish()` invokes the above method on its parent + + +## Use-case 1 + + + +### Requirement + +After onboarding finishes, the user should land in the message list screen. + +### Solution + +1. `O3` calls its `finish()` method +2. `Onboarding` notices `O3` finished; if it had more children, it could switch to another; now it calls `finish()` too +3. `Logged in` notices `Onboarding` finished, and switches its navigation to `Main` +4. `Main` is initialised, and loads its default navigation target (based on product requirements) to be `Messages` +5. `Messages` is initialised, and loads its default navigation target to be `List` + +!!! success "Bonus" + + Every `Node` in the above sequence only needed to care about its own local concern. + + +## Use-case 2 + + + +### Requirement + +Pressing the logout button on the profile screen should land us back to the login screen. + +### Solution + +1. `Root` either implements a `logout` callback, or subscribes to the changes of a user repository; in both cases, either the callback or the repository is passed down the tree as a dependency +2. `Profile` invokes the callback or a `logout` method on the repository +3. `Root` notices the state change, and switches its navigation to the `Logged out` scope +4. `Logged out` loads its initial navigation target, `Login` + +!!! success "Bonus" + + Note how the entire `Logged in` scope is destroyed without any extra effort. The next time a login happens, all state is created anew. + + +## Summary + +Implicit navigation allows you to implement navigation without introducing unnecessary coupling in the tree, and successfully covers the majority of navigation scenarios. + +In case it's not enough to meet your needs, see the next chapter, [Explicit navigation](explicit-navigation.md) + + + diff --git a/mkdocs.yml b/mkdocs.yml index 58d322610..413f34bc8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,6 +35,7 @@ nav: - Promoter: navmodel/promoter.md - Writing your own: navmodel/custom.md - Composable navigation: navigation/composable-navigation.md + - Implicit navigation: navigation/implicit-navigation.md - Explicit navigation: navigation/explicit-navigation.md - Deep linking: navigation/deep-linking.md - UI: From c4dfc220b36b9a4f26c1f722276a6d1486d66528 Mon Sep 17 00:00:00 2001 From: Zsolt Kocsi Date: Mon, 21 Nov 2022 16:24:26 +0000 Subject: [PATCH 2/4] Rework explicit navigation --- .../navigation/explicit-navigation.md | 153 +++++++++--------- 1 file changed, 79 insertions(+), 74 deletions(-) diff --git a/documentation/navigation/explicit-navigation.md b/documentation/navigation/explicit-navigation.md index a9456f9cc..7f1e40880 100644 --- a/documentation/navigation/explicit-navigation.md +++ b/documentation/navigation/explicit-navigation.md @@ -1,27 +1,37 @@ # Explicit navigation -[In this section](composable-navigation.md#navigation-in-the-tree) we covered how changing `NavModel` will result in manipulating -the Appyx tree on the local level. +When [Implicit navigation](implicit-navigation.md) doesn't fit your use case, you can try an explicit approach. -But there are use cases when instead of the local changes on any level of the Appyx tree we want to switch the navigation state globally. +!!! info "Relevant methods" -For instance, user wants to navigate from `Chat` + - ParentNode.attachChild() + - ParentNode.waitForChildAttached() + +Using these methods we can chain together a path which leads from the root of the tree to a specific `Node`. + +## Use case + +We want to navigate from `Chat` -to onboarding `O1` node explicitly by calling a function: +to onboarding's first screen `O1`: -To explicitly navigate to any given `Node`, we need to determine the path which leads from the root of the tree to that `Node`. Once we've done that, -starting from the top of the tree we attach the next `Node` from the determined path. Repeat this step until we reach the desired `Node`. +This time we'll want to do this explicitly by calling a function. + -In our example, we start from the `Root` node, attach `Onboarding` Node to `Root`, and then we attach `O1` to `Onboarding`. -To iteratively perform `Node` attachment Appyx provides `attachChild` API. +## The plan -## Attach child API +1. Create a public method on `Root` that attaches `Onboarding` +2. Create a public method on `Onboarding` that attaches the first onboarding screen +3. Create a `Navigator`, that starting from an instance of `Root`, can chain these public methods together into a single action: `navigateToO1()` +4. Capture an instance of `Root` to use with `Navigator` +5. Call `navigateToO1()` on our `Navigator` instance -Let's take a look at our example and implement navigation to `O1` using `attachChild` API. + +## Step 1 – `Root` → `Onboarding` First, we need to define how to programmatically attach `Onboarding` to the `Root`: @@ -44,24 +54,32 @@ class RootNode( Let's break down what happens here: -1. `attachChild` is provided with a lambda where we add `NavTarget.Onboarding` to a `BackStack`. -2. `attachChild` internally executes this lambda and waits for the provided `OnboardingNode` node type to appear in the children of `Root` node after. -3. Once the desired `Node` appeared in the children list `attachChild` returns it. +1. Since `attachChild` has a generic `` return type, it will conform to the defined `OnboardingNode` type +2. However, `attachChild` doesn't know how to create navigation to `OnboardingNode` – that's something only we can do with the provided lambda +3. We replace `NavTarget.Onboarding` into the back stack +4. Doing this _should_ result in `OnboardingNode` being created and added to `RootNode` as a child +5. `attachChild` expects an instance of `OnboardingNode` to appear as a child of `Root` as a consequence of executing our lambda +6. Once it appears, `attachChild` returns it -In the case when you provide an action which will not result in appearing the desired `Node` in the children list, for instance: -```kotlin -suspend fun attachOnboarding(): OnboardingNode { - return attachChild { - backStack.replace(NavTarget.Main) +!!! info "Important" + + It's our responsibility to make sure that the provided lambda actually results in the expected child being added. If we accidentally do something else instead, for example: + + ```kotlin + suspend fun attachOnboarding(): OnboardingNode { + return attachChild { + backStack.replace(NavTarget.Main) // Wrong NavTarget + } } -} -``` + ``` + + Then an exception will be thrown after a timeout. -exception will be thrown after a timeout. +## Step 2 – `Onboarding` → `O1` -Unlike `Root`, `Onboarding` uses [Spotlight](../navmodel/spotlight.md) instead of [BackStack](../navmodel/backstack.md) as a `NavModel`, so navigation to `O1` will be slightly different: +Unlike `Root`, `Onboarding` uses [Spotlight](../navmodel/spotlight.md) instead of [BackStack](../navmodel/backstack.md) as a `NavModel`, so navigation to the first screen is slightly different: ```kotlin class OnboardingNode( @@ -80,39 +98,23 @@ class OnboardingNode( } ``` -The next step is to obtain the reference to a root of the Appyx tree in the hosting `Actvity` using `NodeReadyObserver` plugin: +## Step 3 – Our `Navigator` ```kotlin -class ExplicitNavigationExampleActivity : NodeActivity() { - - lateinit var rootNode: RootNode - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - NodeHost(integrationPoint = appyxIntegrationPoint) { - RootNode( - buildContext = it, - plugins = listOf(object : NodeReadyObserver { - override fun init(node: RootNode) { - rootNode = node - } - }) - ) - } - } - } +interface Navigator { + fun navigateToO1() } ``` -Once we implemented navigation to `OnboardingNode` and `O1`, and grabbed the reference to the root of a tree, -we can execute the chain of actions which will result in navigation to `O1`: +In this case we'll implement it directly with our activity: ```kotlin -class ExplicitNavigationExampleActivity : NodeActivity() { +class ExplicitNavigationExampleActivity : NodeActivity(), Navigator { + + lateinit var rootNode: RootNode // See the next step - fun navigateToO1() { + override fun navigateToO1() { lifecycleScope.launch { rootNode .attachOnboarding() @@ -122,18 +124,18 @@ class ExplicitNavigationExampleActivity : NodeActivity() { } ``` -As the last step, we can define our navigation method in the interface `Navigator`, let the `Activity` implement it, -and provide the instance of a `Navigator` down the Appyx tree: +## Step 4 – An instance of `RootNode` + +As the last piece of the puzzle, we'll also need to capture the instance of `RootNode` to make it all work. We can do that by a `NodeReadyObserver` plugin when setting up our tree: -```kotlin -interface Navigator { - fun navigateToO1() -} -``` ```kotlin class ExplicitNavigationExampleActivity : NodeActivity(), Navigator { + lateinit var rootNode: RootNode + + override fun navigateToO1() { /*...*/ } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { @@ -153,24 +155,26 @@ class ExplicitNavigationExampleActivity : NodeActivity(), Navigator { } ``` +## Step 5 – Using the `Navigator` -Calling `Navigator` methods explicitly from inside of the Appyx tree will change the global navigation state. +See how in the previous snippet `RootNode` receives a `navigator` dependency. -## Wait for child attached API +It can pass it further down the tree as a dependency to other nodes. Those nodes can call the methods of the `Navigator`, which will change the global navigation state directly. -In the example above we learned how to navigate explicitly in Appyx. -But there are cases when we want to wait for a certain action to be performed by a user, and only then -execute logic. +--- -Let's imagine the following example: +## Bonus: Wait for a child to be attached - +There might be cases when we want to wait for a certain action to be _performed by the user_, rather than us, to result in a child being attached. -1. User is logged in and uses the application. -2. Once user is logged out (`LoggedOutNode` is attached to `RootNode`) we need to show a `PromoNode`. +In these cases we can use `ParentNode.waitForChildAttached()` instead. + + +### Use case – Wait for login + +A typical case building an explicit navigation chain that relies on `Logged in` being attached. Most probably `Logged in` has a dependency on some kind of a `User` object. Here we want to wait for the user to authenticate themselves, rather than creating a dummy user object ourselves. -To implement that behaviour we need to use `waitForChildAttached` API: ```kotlin class RootNode( @@ -179,26 +183,27 @@ class RootNode( buildContext = buildContext ) { - suspend fun waitForLoggedOutAttached() = waitForChildAttached() + suspend fun waitForLoggedIn(): LoggedInNode = + waitForChildAttached() } ``` -This method will wait for `LoggedOutNode` to appear in the child list of `RootNode` and return `LoggedOutNode`. -In the section above we covered how we can explicitly navigate to `PromoNode` from `LoggedOutNode`, -so the final solution to implement the desired behaviour could look like: +This method will wait for `LoggedInNode` to appear in the child list of `RootNode` and return with it. If it's already there, it returns immediately. + +A navigation chain using it could look like: ```kotlin -class Activity : NodeActivity() { +class ExplicitNavigationExampleActivity : NodeActivity(), Navigator { - fun showPromoWhenLoggedOut() { + override fun navigateToProfile() { lifecycleScope.launch { rootNode - .waitForLoggedOutAttached() - .attachPromo() + .waitForLoggedIn() + .attachMain() + .attachProfile() } } } ``` - -To check the actual code please visit `ExplicitNavigationExampleActivity` in our samples. +You can find related code examples in `ExplicitNavigationExampleActivity` in our samples. From 9262803500b66c2ae308ff16c6d565ddb734c9f9 Mon Sep 17 00:00:00 2001 From: Zsolt Kocsi Date: Mon, 21 Nov 2022 16:24:36 +0000 Subject: [PATCH 3/4] Simplify intro text on deep linking --- documentation/navigation/deep-linking.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/documentation/navigation/deep-linking.md b/documentation/navigation/deep-linking.md index 99d7a3d53..339bcce20 100644 --- a/documentation/navigation/deep-linking.md +++ b/documentation/navigation/deep-linking.md @@ -1,7 +1,6 @@ # Deep linking -In the [explicit navigation](explicit-navigation.md) section we covered -how we can implement explicit navigation. Once we've done that, implementing deep links is straightforward: +Building on top of [explicit navigation](explicit-navigation.md), implementing deep links is straightforward: ```kotlin class ExplicitNavigationExampleActivity : NodeActivity(), Navigator { From 853002a103c9dffd63d14f2053495726a56fe0f3 Mon Sep 17 00:00:00 2001 From: Zsolt Kocsi Date: Mon, 21 Nov 2022 17:00:14 +0000 Subject: [PATCH 4/4] Update FAQ --- documentation/faq.md | 45 +++++++++++++++++-- documentation/how-to-use-appyx/sample-apps.md | 5 +++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/documentation/faq.md b/documentation/faq.md index 496bd420c..265806d5b 100644 --- a/documentation/faq.md +++ b/documentation/faq.md @@ -26,6 +26,15 @@ See [Model-driven navigation](navigation/model-driven-navigation.md) for more de --- +#### **Q: How can I navigate to a specific part of my Appyx tree?** + +In most cases [Implicit navigation](navigation/implicit-navigation.md) can be your primary choice, and you don't need to explicitly specify a remote point in the tree. This is helpful to avoid coupling. + +For those cases when you can't avoid it, [Explicit navigation](navigation/explicit-navigation.md) and [Deep linking](navigation/deep-linking.md) covers you. + +--- + + #### **Q: What about dialogs & bottom sheets?** You can use Appyx in conjunction with Accompanist or any other Compose mechanism. @@ -34,9 +43,24 @@ If you wish, you can model your own Modal with Appyx too. We'll add an example s --- +#### **Q: Can I have a bottom sheet conditionally?** + +You could use a similar approach as we do with back buttons in `SamplesContainerNode` you can find in the `:app` module: store a flag in the `NavTarget` that can be different per instance. + +--- + ## Using Appyx in an app +#### **Q: Is it an all or nothing approach?** + +No, you can adopt Appyx gradually: + +- Plug it in to one screen and just utilise its screen transformation capabilities (e.g. [Cards](navmodel/cards.md)) +- Plug it in to a few screens and substitute another navigation mechanism with it, such as [Jetpack Compose Navigation](how-to-use-appyx/sample-apps.md#appyx-jetpack-compose-navigation-example) + +--- + #### **Q: What architectural patterns can I use?** Appyx is agnostic of architectural patterns. You can use any architectural pattern in the `Nodes` you'd like. You can even use a different one in each. @@ -52,7 +76,22 @@ Yes, we'll add an example soon. #### **Q: Can I use it with Hilt?** -Yes, we'll add an example soon. +- Our draft PR: [#115](https://github.com/bumble-tech/appyx/pull/115) (Feel free to provide feedback!) +- [https://github.com/jbreitfeller-sfix/appyx-playground](https://github.com/jbreitfeller-sfix/appyx-playground) another approach on this topic + +--- + +## Performance-related + +#### **Q: Are `Nodes` kept alive?** + +In short: you can decide whether a `Node`: + +- is on-screen +- is off-screen but kept alive +- is off-screen and becomes destroyed + +Check the [Lifecycle](apps/lifecycle.md#on-screen-off-screen) for more details. --- @@ -61,9 +100,7 @@ Yes, we'll add an example soon. #### **Q: Is it production ready?** -We do use it at Bumble in production, and as such, we're committed to maintaining and improving it. - -The project is currently in an alpha stage only to allow API changes for now. However, we commit ourselves to communicating all such changes in the [Changelog](releases/changelog.md). +Yes, Appyx matured to its stable version. We also use it at Bumble in production, and as such, we're committed to maintaining and improving it. --- diff --git a/documentation/how-to-use-appyx/sample-apps.md b/documentation/how-to-use-appyx/sample-apps.md index cf3859163..3b7725bf0 100644 --- a/documentation/how-to-use-appyx/sample-apps.md +++ b/documentation/how-to-use-appyx/sample-apps.md @@ -23,3 +23,8 @@ This example may be useful if you need to migrate to Appyx gradually. ## Appyx + Hilt example Coming soon! + +Meanwhile: + +- Our draft PR: [#115](https://github.com/bumble-tech/appyx/pull/115) (Feel free to provide feedback!) +- [https://github.com/jbreitfeller-sfix/appyx-playground](https://github.com/jbreitfeller-sfix/appyx-playground) another approach on this topic