diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3561ec66a..1a7e0593e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: - name: Build run: > ./gradlew - build + buildNonMkdocs projectHealth mergeLintSarif mergeDetektSarif @@ -143,6 +143,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '11' + - uses: gradle/wrapper-validation-action@v1 + - uses: gradle/gradle-build-action@v2 + with: + cache-read-only: ${{ env.MAIN_BRANCH != 'true' }} + - name: Generate distributions + run: ./gradlew jsBrowserDistributionMkdocs --continue - uses: actions/setup-python@v4 with: python-version: '3.x' diff --git a/appyx-navigation/common/src/commonMain/kotlin/com/bumble/appyx/navigation/store/RetainedInstanceStore.kt b/appyx-navigation/common/src/commonMain/kotlin/com/bumble/appyx/navigation/store/RetainedInstanceStore.kt index 76112c21a..e690796b7 100644 --- a/appyx-navigation/common/src/commonMain/kotlin/com/bumble/appyx/navigation/store/RetainedInstanceStore.kt +++ b/appyx-navigation/common/src/commonMain/kotlin/com/bumble/appyx/navigation/store/RetainedInstanceStore.kt @@ -26,7 +26,12 @@ package com.bumble.appyx.navigation.store */ interface RetainedInstanceStore { - fun get(storeId: String, key: String, disposer: (T) -> Unit = {}, factory: () -> T): T + fun get( + storeId: String, + key: String, + disposer: (T) -> Unit = {}, + factory: () -> T + ): T fun isRetainedByStoreId(storeId: String, value: Any): Boolean diff --git a/build.gradle.kts b/build.gradle.kts index fc4ceaba1..e46023140 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -73,9 +73,33 @@ allprojects { } } +val buildNonMkdocsTask = tasks.register("buildNonMkdocs") +val jsBrowserDistributionMkdocsTask = tasks.register("jsBrowserDistributionMkdocs") + subprojects { plugins.apply("release-dependencies-diff-create") + // Allows avoiding building these modules as part of CI as they are also built for mkdocs. + if (!path.startsWith(":demos:mkdocs:")) { + plugins.withId("com.android.application") { + buildNonMkdocsTask.configure { dependsOn(tasks.named("build")) } + } + plugins.withId("com.android.library") { + buildNonMkdocsTask.configure { dependsOn(tasks.named("build")) } + } + plugins.withId("org.jetbrains.kotlin.multiplatform") { + buildNonMkdocsTask.configure { dependsOn(tasks.named("build")) } + } + plugins.withId("java-library") { + buildNonMkdocsTask.configure { dependsOn(tasks.named("build")) } + } + } else { + plugins.withId("org.jetbrains.kotlin.multiplatform") { + jsBrowserDistributionMkdocsTask + .configure { dependsOn(tasks.named("jsBrowserDistribution")) } + } + } + tasks.withType().configureEach { kotlinOptions { if (project.findProperty("enableComposeCompilerReports") == "true") { diff --git a/documentation/2.x/migrationguide.md b/documentation/2.x/migrationguide.md index 528551197..fb2b1ac38 100644 --- a/documentation/2.x/migrationguide.md +++ b/documentation/2.x/migrationguide.md @@ -1,12 +1,12 @@ # Appyx -## 2.x vs 1.x project organisation +## Project organisation -### Appyx 1.0 +### 1.x Packaged as a single library, implementing Model-driven navigation with transitions together. -### Appyx 2.0 +### 2.x The library is packaged as multiple artifacts. #### :appyx-navigation @@ -31,13 +31,14 @@ The library is packaged as multiple artifacts. - Compose multiplatform -## 1.x ~ 2.x rough equivalents +## Rough equivalents -- `NavModel` -> `AppyxComponent` -- `TransitionHandler` -> `MotionController` +- 1.x → 2.x +- `NavModel` → `AppyxComponent` +- `TransitionHandler` → `MotionController` -## 1.x → 2.x Migration guide +## Migration guide ### Gradle @@ -48,10 +49,10 @@ The library is packaged as multiple artifacts. Note that [BackStack](../components/backstack.md) and [Spotlight](../components/spotlight.md) are now standalone artifacts. Check your usage, you might only need `backstack`: ```diff -- implementation("com.bumble.appyx:core:1.x.x") -+ implementation("com.bumble.appyx:appyx-navigation:2.0.0-alpha01") -+ implementation("com.bumble.appyx:backstack-android:2.0.0-alpha01") -+ implementation("com.bumble.appyx:spotlight-android:2.0.0-alpha01") +-implementation("com.bumble.appyx:core:1.x.x") ++implementation("com.bumble.appyx:appyx-navigation:2.0.0-alpha01") ++implementation("com.bumble.appyx:backstack-android:2.0.0-alpha01") ++implementation("com.bumble.appyx:spotlight-android:2.0.0-alpha01") ``` @@ -61,15 +62,15 @@ Note that [BackStack](../components/backstack.md) and [Spotlight](../components/ Artifacts have a `utils-` prefix: ```diff --"com.bumble.appyx:testing-ui" --"com.bumble.appyx:testing-unit-common" --"com.bumble.appyx:testing-junit4" --"com.bumble.appyx:testing-junit5" - -+"com.bumble.appyx:utils-testing-ui" -+"com.bumble.appyx:utils-testing-unit-common" -+"com.bumble.appyx:utils-testing-junit4" -+"com.bumble.appyx:utils-testing-junit5" +-implementation("com.bumble.appyx:testing-ui") +-implementation("com.bumble.appyx:testing-unit-common") +-implementation("com.bumble.appyx:testing-junit4") +-implementation("com.bumble.appyx:testing-junit5") + ++implementation("com.bumble.appyx:utils-testing-ui") ++implementation("com.bumble.appyx:utils-testing-unit-common") ++implementation("com.bumble.appyx:utils-testing-junit4") ++implementation("com.bumble.appyx:utils-testing-junit5") ``` @@ -116,7 +117,7 @@ Artifacts have a `utils-` prefix: class RootNode( buildContext: BuildContext, - private val backStack: BackStack = BackStack( + private val backStack: BackStack = BackStack( - initialElement = Child1, - savedStateMap = buildContext.savedStateMap + model = BackStackModel( diff --git a/documentation/components/backstack.md b/documentation/components/backstack.md index a0b1ace46..b6ce084a1 100644 --- a/documentation/components/backstack.md +++ b/documentation/components/backstack.md @@ -79,7 +79,7 @@ Class: `BackStackFader` ### Custom -You can always create your own visualisations for Appyx components. Find more info in [UI representation](../interactions/uirepresentation.md). +You can always create your own visualisations for Appyx components. Find more info in [UI representation](../interactions/ui-representation.md). diff --git a/documentation/components/spotlight.md b/documentation/components/spotlight.md index 0dc9512e5..09b246088 100644 --- a/documentation/components/spotlight.md +++ b/documentation/components/spotlight.md @@ -90,7 +90,7 @@ Class: `SpotlightFader` ### Custom -You can always create your own visualisations for Appyx components. Find more info in [UI representation](../interactions/uirepresentation.md). +You can always create your own visualisations for Appyx components. Find more info in [UI representation](../interactions/ui-representation.md). diff --git a/documentation/faq.md b/documentation/faq.md new file mode 100644 index 000000000..3dfde5774 --- /dev/null +++ b/documentation/faq.md @@ -0,0 +1,110 @@ +# FAQ + + +## Navigation-related + +#### **Q: How does Appyx Navigation relate to Jetpack Compose Navigation?** + +We wrote an article on this in the context of Appyx 1.x: [Appyx vs Jetpack Compose Navigation](https://medium.com/bumble-tech/appyx-vs-jetpack-compose-navigation-b91bd23369f2). + +Most of the same arguments apply to Appyx 2.x too. + +While Appyx represents a different paradigm, it can also co-exist with Jetpack Compose Navigation. This can be helpful if you want to use Appyx for in-screen mechanisms only, or if you plan to migrate gradually. + +See [Appyx + Compose Navigation](navigation/integrations/compose-navigation.md) for more details. + +--- + +#### **Q: How does Appyx Navigation compare against other solutions?** + +The core concepts of navigation in Appyx differ from most navigation libraries: + +1. You don't have a concept of the "screen" present in the model +2. You can define your own navigation models using [Appyx Components](components/index.md) +3. On the UI level you can transform what feels like the "screen" itself + +See [Model-driven navigation](navigation/concepts/model-driven-navigation.md) for more details. + +--- + + +#### **Q: How can I navigate to a specific part of my Appyx tree?** + +In most cases [Implicit navigation](navigation/concepts/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/concepts/explicit-navigation.md) and [Deep linking](navigation/features/deep-linking.md) covers you. + +--- + + +#### **Q: What about dialogs & bottom sheets?** + +You can use Appyx in conjunction with Accompanist or any other Compose mechanism. + +If you wish, you can model your own Modal with Appyx too. We'll add an example soon. + + +--- + + +## Using Appyx in an app + + +#### **Q: Is it an all or nothing approach?** + +No, you can adopt Appyx gradually: + +- Plug an [Appyx Components](components/index.md) in to any screen and just use it as a UI component. +- Plug it in to a few screens and substitute another navigation mechanism with it, such as [Jetpack Compose Navigation](navigation/integrations/compose-navigation.md) + +--- + +#### **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. + +--- + +#### **Q: Can I use it with ViewModel?** + +Please see [Appyx + ViewModel](navigation/integrations/viewmodel.md). + +--- + + +#### **Q: Can I use it with Hilt?** + +Please see [Appyx + DI frameworks](navigation/integrations/di-frameworks.md). + +--- + +## 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](navigation/features/lifecycle.md#on-screen-off-screen) for more details. + +--- + + +## On the project itself + +#### **Q: Is it production ready?** + +Appyx matured to its stable version in the `1.x` branch. + +The `2.x` branch is currently in alpha. + +We use Appyx at Bumble in production, and as such, we're committed to maintaining and improving it. + +--- + +## Other + +Have a question? Come over to **#appyx** on Kotlinlang Slack! diff --git a/documentation/index.md b/documentation/index.md index 0f886b968..d9f22a71c 100644 --- a/documentation/index.md +++ b/documentation/index.md @@ -7,6 +7,9 @@ Model-driven navigation + UI components with gesture control for Compose Multipl Find us on Kotlinlang Slack: **#appyx** +## Setup + +See [Downloads](releases/downloads.md) and [Navigation quick start guide](navigation/quick-start.md). ## Overview @@ -17,11 +20,11 @@ Appyx is a collection of libraries: ## Appyx Navigation -**Type-safe navigation directly from code.** +**Type-safe navigation for Compose directly from code.** - Tree-based, composable -- Leverages the transitions and gesture-based capabilities to **Appyx Interactions** to build beautiful, custom navigation. -- Use any component for navigation, whether pre-built (see: [Appyx Components](components/index.md)), or custom-built by you (see: [Appyx Interactions](interactions/index.md)). +- Leverages the transitions and gesture-based capabilities of [Appyx Interactions](interactions/index.md) to build beautiful, custom navigation. +- Use any component for navigation, whether pre-built ([Appyx Components](components/index.md)), or custom-built by you ([Appyx Interactions](interactions/index.md)). [» More details](navigation/index.md) @@ -65,7 +68,7 @@ Appyx is a collection of libraries: **Component gallery.** -Back stack, Spotlight (pager), and other UI components built using Appyx Interactions. +Back stack, Spotlight (pager), and other UI components built using [Appyx Interactions](interactions/index.md). [» More details](components/index.md) diff --git a/documentation/interactions/ksp.md b/documentation/interactions/ksp.md new file mode 100644 index 000000000..19011f970 --- /dev/null +++ b/documentation/interactions/ksp.md @@ -0,0 +1,50 @@ +# Appyx Interactions – KSP setup + +Defining `TargetUiStates` is easy, as demonstrated in [UI representation](ui-representation.md) + +```kotlin +@MutableUiStateSpecs +class TargetUiState( + val position: Position.Target, + val rotation: Rotation.Target, + val backgroundColor: BackgroundColor.Target, +) +``` + +However, for every `TargetUiState`, there needs to exist a corresponding `MutableUiState` class containing the animation code. + +By adding the `@MutableUiStateSpecs` annotation, if you follow the below setup guide, you can have the Appyx KSP mutable ui state processor generate this class for you automatically. + + +## Setup + +Works with **Kotlin 1.8.10**. For our migration to **Kotlin 1.9** please check: + +- [#547](https://github.com/bumble-tech/appyx/issues/547) - Upgrade Compose Multiplatform / Kotlin + + +### Main `build.gradle` + +```kotlin +plugins { + id("com.google.devtools.ksp") version libs.versions.ksp.get() apply false + // Alternatively: + // id("com.google.devtools.ksp") version '1.8.0-1.0.8' apply false +} +``` + +### App `build.gradle` + +```kotlin +plugins { + id("com.google.devtools.ksp") +} + +composeOptions { + kotlinCompilerExtensionVersion = '1.4.3' +} + +dependencies { + ksp("com.bumble.appyx:mutable-ui-processor:{latest version}") +} +``` diff --git a/documentation/interactions/uirepresentation.md b/documentation/interactions/ui-representation.md similarity index 93% rename from documentation/interactions/uirepresentation.md rename to documentation/interactions/ui-representation.md index b7102c47d..8c9bff76d 100644 --- a/documentation/interactions/uirepresentation.md +++ b/documentation/interactions/ui-representation.md @@ -166,21 +166,8 @@ class TargetUiState( You might have noticed that the above classes are annotated with `@MutableUiStateSpecs`. Appyx comes with a KSP processor that will generate the corresponding `MutableUiState` class for you, so that you don't have to. -You can configure the processor in your `build.gradle.kts` such as: +Please refer to the [KSP setup](ksp.md) guide. -```kotlin -plugins { - id("com.google.devtools.ksp") -} - -dependencies { - add("kspCommonMainMetadata", project(":ksp:mutable-ui-processor")) - // Add for each of your target platforms: - add("kspAndroid", project(":ksp:mutable-ui-processor")) - add("kspDesktop", project(":ksp:mutable-ui-processor")) - add("kspJs", project(":ksp:mutable-ui-processor")) -} -``` ## Observing MotionProperty in children UI diff --git a/documentation/navigation/apps/configuration.md b/documentation/navigation/apps/configuration.md deleted file mode 100644 index 3f84d4e33..000000000 --- a/documentation/navigation/apps/configuration.md +++ /dev/null @@ -1,49 +0,0 @@ -# Configuration change - -To retain objects during configuration change you can use the `RetainedInstanceStore` class. - -## How does it work? - -The `RetainedInstanceStore` stores the objects within a singleton. The node manages whether the content should be removed by checking whether the `Activity` is being recreated due to a configuration change or not. - -These are the following scenarios: -- If the `Activity` is recreated: the retained instance is returned instead of a new instance. -- If the `Activity` is destroyed: the retained instance is removed and disposed. - -## Example - -Here is an example of how you can use the `RetainedInstanceStore`: - -```kotlin -import com.bumble.appyx.core.builder.Builder -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.store.getRetainedInstance -import com.bumble.appyx.interop.rx2.store.getRetainedDisposable - -class RetainedInstancesBuilder : Builder() { - - override fun build(buildContext: BuildContext, payload: String): Node { - val retainedNonDisposable = buildContext.getRetainedInstance( - factory = { NonDisposableClass(payload) }, - disposer = { feature.cleanUp() } - ) - val retainedFeature = buildContext.getRetainedDisposable { - RetainedInstancesFeature(payload) - } - - val view = RetainedInstancesViewImpl() - val interactor = RetainedInstancesInteractor( - feature = retainedFeature, - nonDisposable = retainedNonDisposable, - view = view - ) - - return RetainedInstancesNode( - buildContext = buildContext, - view = view, - plugins = listOf(interactor) - ) - } -} -``` diff --git a/documentation/navigation/apps/structure.md b/documentation/navigation/apps/structure.md deleted file mode 100644 index 463e9f27f..000000000 --- a/documentation/navigation/apps/structure.md +++ /dev/null @@ -1,67 +0,0 @@ -# Structuring your app navigation - -As seen in [Composable navigation](../navigation/composable-navigation.md), you can make `AppyxComponents` composable. - -To achieve this, Appyx offers the `Node` class as the structural element. - - -## Node illustration - -In many of the examples you'll see this panel as an illustration of a very simple `Node` – it has some local state (id, colour, and a counter). - - - -If you launch the sample app in the `:app` module, you can also change its state (colour) by tapping it. Its counter is stepped automatically. This is to illustrate that it has its own state, persisted and restored. - - -## Node overview - -You can think of a `Node` as a standalone component with: - -- Its own simplified lifecycle -- State restoration -- A `@Composable` view -- Business logic that's kept alive even when the view isn't added to the composition -- The ability to host generic [Plugins](../apps/plugins.md) to extract extra concerns without enforcing any particular architectural pattern - - -## Parent nodes, child nodes - -`ParentNodes` can have other `Nodes` as children. This means you can represent your whole application as a tree of Appyx nodes. - - - -You can go as granular or as high-level as it fits you. This allows to keep the complexity low in individual `Nodes` by extracting responsibilities to children, as well as composing other components to build more complex functionality. - - -## Composable navigation - - - -`Nodes` offer the structure – `AppyxComponents` add dynamism to it. - -Read more in [Composable navigation](../navigation/composable-navigation.md) - - -## Lifecycle - -Nodes have their own lifecycles, directly using the related classes of `androidx.lifecycle`. - -Read more in [Lifecycle](../apps/lifecycle.md) - - -## ChildAware API - -React to dynamically added child nodes in the tree: [ChildAware API](childaware.md) - - -## Summary - -A summary of Appyx's approach to structuring applications: - -- Compose your app out of `Nodes` with their own lifecycles and state -- Navigation is local, composed of individual pieces of `AppyxComponents` -- Navigation is stateful -- Navigation is unit-testable -- You're free to implement your own navigable components by utilising `AppyxComponents` -- Avoid global navigation concerns, like shared modules needing to know about the application, or the application needing to know about all its possible modules diff --git a/documentation/navigation/concepts/composable-navigation.md b/documentation/navigation/concepts/composable-navigation.md new file mode 100644 index 000000000..cc4969dcc --- /dev/null +++ b/documentation/navigation/concepts/composable-navigation.md @@ -0,0 +1,84 @@ +# Composable navigation + +[AppyxComponents](../../components/index.md) in Appyx are composable. + +As a single `AppyxComponent` won't be enough for the whole of your whole app, you can use many in a composable way. That is, any managed element of a `AppyxComponent` can also host its own `AppyxComponent`. + + +## Nodes – structural elements for composing navigation + +```Nodes``` are the main structural element in Appyx. They can form a tree. + +You can think of a `Node` as a standalone unit of your app with: + +- Its own `AppyxComponent` +- Its own [Lifecycle](/lifecycle.md) +- State restoration +- A `@Composable` view +- Business logic that's kept alive even when the view isn't added to the composition +- The ability to host generic [Plugins](/plugins.md) to extract extra concerns without enforcing any particular architectural pattern + +This allows you to make your app's business logic also composable by leveraging `Nodes` as lifecycled components. + + +## Parent nodes, child nodes + +`ParentNodes` can have other `Nodes` as children. This means you can represent your whole application as a tree of Appyx nodes. + + + +You can go as granular or as high-level as it fits you. This allows to keep the complexity low in individual `Nodes` by extracting responsibilities to children, as well as composing other components to build more complex functionality. + + +## ChildAware API + +A `ParentNode` can react to dynamically added child `Nodes` in the tree: [ChildAware API](../features/childaware.md) + + +## Navigation in the tree + + + +Once you've structured your navigation in a composable way, you can add an `AppyxComponent` to a `Node` of this tree and make it dynamic: + +- Some parts in this tree are active while others ore not +- 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 an `AppyxComponent` 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 AppyxComponents affect Nodes + +`AppyxComponent` operations will typically result in: + +- Adding or removing child `Nodes` of a `ParentNode` +- Move them on and off the screen +- Change their states + +As an illustration: + + + +Here: + +- `Back stack` illustrates adding and removing child `Nodes` +- `Tiles` illustrates changing the state of children and removing them from the `ParentNode` + +These are just two examples, you're of course not limited to using them. + + + +## Summary + +A summary of Appyx's approach to structuring applications: + +- Compose your app out of `Nodes` with their own lifecycles and state +- Navigation is local, composed of individual pieces of `AppyxComponents` +- Navigation is stateful +- Navigation is unit-testable +- Nested navigation and multi-module navigation works as a default +- You're free to implement your own navigable components by utilising `AppyxComponents` +- Avoid global navigation concerns, like shared modules needing to know about the application, or the application needing to know about all its possible modules diff --git a/documentation/navigation/navigation/explicit-navigation.md b/documentation/navigation/concepts/explicit-navigation.md similarity index 100% rename from documentation/navigation/navigation/explicit-navigation.md rename to documentation/navigation/concepts/explicit-navigation.md diff --git a/documentation/navigation/navigation/implicit-navigation.md b/documentation/navigation/concepts/implicit-navigation.md similarity index 100% rename from documentation/navigation/navigation/implicit-navigation.md rename to documentation/navigation/concepts/implicit-navigation.md diff --git a/documentation/navigation/navigation/model-driven-navigation.md b/documentation/navigation/concepts/model-driven-navigation.md similarity index 100% rename from documentation/navigation/navigation/model-driven-navigation.md rename to documentation/navigation/concepts/model-driven-navigation.md diff --git a/documentation/navigation/apps/childaware.md b/documentation/navigation/features/childaware.md similarity index 92% rename from documentation/navigation/apps/childaware.md rename to documentation/navigation/features/childaware.md index 1b5e1b2b8..46ab1609e 100644 --- a/documentation/navigation/apps/childaware.md +++ b/documentation/navigation/features/childaware.md @@ -1,8 +1,6 @@ # ChildAware API -The framework includes the `ChildAware` interface which comes with a powerful API. - -It allows you to scope communication with (or between) dynamically available child nodes easily. +The `ChildAware` interface allows you to scope communication with (or between) dynamically available child nodes easily. ## Baseline diff --git a/documentation/navigation/features/configuration.md b/documentation/navigation/features/configuration.md new file mode 100644 index 000000000..b8913aeba --- /dev/null +++ b/documentation/navigation/features/configuration.md @@ -0,0 +1,67 @@ +# Configuration change + +To retain objects during configuration change you can use the `RetainedInstanceStore` class. + +## How does it work? + +{== + +The `RetainedInstanceStore` stores objects within a singleton. + +Every `Node` has access to the `RetainedInstanceStore` and manages these cases automatically: + +- The `Activity` is recreated: the retained instance is returned instead of a new instance. +- The `Activity` is destroyed: the retained instance is removed and disposed. + +==} + +## Example + +Appyx provides extension methods on the `BuildContext` class (an instance which is passed to every `Node` upon creation) to access the `RetainedInstanceStore`. You can use these to: + +- Create your retained objects (`factory`) +- Define cleanup mechanisms (`disposer`) to be run when the retained object will be removed on `Activity` destroy. + +You can opt to use the `Builder` pattern to provide dependencies to your `Node` and separate this logic: + +{== + +Note: to use the rx2/rx3 `getRetainedDisposable` extension methods you see below, you need to add the relevant gradle dependencies. Please refer to [Downloads](../../releases/downloads.md). + +==} + +```kotlin +import com.bumble.appyx.navigation.builder.Builder +import com.bumble.appyx.navigation.modality.BuildContext +import com.bumble.appyx.navigation.node.Node +import com.bumble.appyx.navigation.store.getRetainedInstance +import com.bumble.appyx.utils.interop.rx2.store.getRetainedDisposable + +class YourNodeBuilder : Builder() { + + override fun build(buildContext: BuildContext, payload: YourPayload): Node { + // Case 1: + // If your type implements an rx2/rx3 Disposable, + // you don't need to pass a disposer: + val retainedFoo = buildContext.getRetainedDisposable { + Foo(payload) + } + + // Case 2: + // If your type doesn't implement an rx2/rx3 Disposable, + // you can define a custom cleanup mechanism: + val retainedFoo = buildContext.getRetainedInstance( + factory = { Foo(payload) }, + disposer = { it.cleanup() } // it: Foo + ) + + val view = YourNodeViewImpl() + + return YourNode( + buildContext = buildContext, + foo = retainedFoo, + view = view, + ) + } +} +``` diff --git a/documentation/navigation/navigation/deep-linking.md b/documentation/navigation/features/deep-linking.md similarity index 90% rename from documentation/navigation/navigation/deep-linking.md rename to documentation/navigation/features/deep-linking.md index 339bcce20..40ce7f8c9 100644 --- a/documentation/navigation/navigation/deep-linking.md +++ b/documentation/navigation/features/deep-linking.md @@ -1,6 +1,6 @@ # Deep linking -Building on top of [explicit navigation](explicit-navigation.md), implementing deep links is straightforward: +Building on top of [explicit navigation](../concepts/explicit-navigation.md), implementing deep links is straightforward: ```kotlin class ExplicitNavigationExampleActivity : NodeActivity(), Navigator { diff --git a/documentation/navigation/apps/lifecycle.md b/documentation/navigation/features/lifecycle.md similarity index 100% rename from documentation/navigation/apps/lifecycle.md rename to documentation/navigation/features/lifecycle.md diff --git a/documentation/navigation/apps/plugins.md b/documentation/navigation/features/plugins.md similarity index 100% rename from documentation/navigation/apps/plugins.md rename to documentation/navigation/features/plugins.md diff --git a/documentation/navigation/features/scoped-di.md b/documentation/navigation/features/scoped-di.md new file mode 100644 index 000000000..450fcc301 --- /dev/null +++ b/documentation/navigation/features/scoped-di.md @@ -0,0 +1,38 @@ +# Scoped DI + +Once you represent your navigation in a [Composable way](../concepts/composable-navigation.md), you will get powerful DI scoping as a pleasant side-effect: + + + +Appyx gives every single `Node` its own DI scope for free, with no extra effort required to clean up these scopes other than navigating away from them. + + +## How does this work? + +1. Imagine you create an object in the `Node` related to `Onboarding`, and make it available to all of its children. +2. While navigation is advancing between the individual screens of Onboarding, `O1` – `O2` – `O3`, this object will be the same instance. +3. As soon as the navigation switches to `Main`, the entire subtree of `Onboarding` is destroyed and all held objects are released. +4. Should the navigation ever go back to `Onboarding`, said object would be created from scratch. + +This of course applies to every other `Node` in the tree. + +## Scoping in practice + +Imagine in a larger tree: + + + +1. A logout action is represented as switching the navigation back to the `Logged out` node +2. This will destroy the entire `Logged in` scope automatically +3. All objects held in the scope of an authenticated state are released without any special effort. + +You could also use this for DI scoping flows (e.g. a cart object during a multi-screen product checkout). + +With a regular approach these cases could be more difficult to represent: + +- Screen-bound scopes wouldn't allow multi-screen lifetime of the objects. +- Application-scoped singletons would require extra attention of cleaning up once the flow ends. + +With Appyx you get best of both worlds for free. + + diff --git a/documentation/navigation/index.md b/documentation/navigation/index.md index 16b541958..e5b113fd1 100644 --- a/documentation/navigation/index.md +++ b/documentation/navigation/index.md @@ -1,13 +1,65 @@ # Appyx Navigation +## Type-safe navigation for Compose directly from code -An Android navigation library for Jetpack Compose that uses **Appyx Interactions** for its transitions and gesture-based navigation. +- Tree-based, composable +- Leverages the transitions and gesture-based capabilities of [Appyx Interactions](../interactions/index.md) to build beautiful, custom navigation. +- Use any component for navigation, whether pre-built ([Appyx Components](../components/index.md)), or custom-built by you ([Appyx Interactions](../interactions/index.md)). + -## Multiplatform status & WIP notice -Appyx navigation is currently being converted to a multiplatform project. Most concepts will not change, however new platform-agnostic parts are not documented as of yet. +## Quick start -Some of the documentation might also contain outdated terminology from **1.x**. +[Quick start guide](quick-start.md) -If you find any issues, please file an issue/PR. + +## Concepts + +- [Model-driven navigation](concepts/model-driven-navigation.md) +- [Composable navigation](concepts/composable-navigation.md) +- [Implicit navigation](concepts/implicit-navigation.md) +- [Explicit navigation](concepts/explicit-navigation.md) + +## Features + +- [ChildAware](features/childaware.md) +- [Deep link navigation](features/deep-linking.md) +- [Lifecycle](features/lifecycle.md) +- [Plugins](features/plugins.md) +- [Scoped DI](features/scoped-di.md) +- [Surviving configuration changes](features/configuration.md) + +## Integrations + +- [Compose Navigation](integrations/compose-navigation.md) +- [DI frameworks](integrations/di-frameworks.md) +- [RIBs](integrations/ribs.md) +- [RxJava](integrations/rx.md) +- [ViewModel](integrations/viewmodel.md) + + +## Components + +{{ + compose_mpp_sample( + project_output_directory="demos/mkdocs/appyx-components/backstack/parallax/web/build/distributions", + compile_task=":demos:mkdocs:appyx-components:backstack:parallax:web:jsBrowserDistribution", + width=512, + height=384, + target_directory="samples/documentation-components-backstack-parallax", + html_file_name="index.html", + classname="compose_mpp_sample", + ) +}} + +See [Appyx Components](../components/index.md) for Back stack, Spotlight (pager) and other components you can use in navigation. + +See [Appyx Interactions](../interactions/index.md) on how to build your own components with state-based transitions and easy-to-create gesture control. + + +## Multiplatform status + +**Appyx Interactions** and **Appyx Components** are multiplatform. + +**Appyx Navigation** is currently being converted to a multiplatform project and is expected to be merged soon (ETA Aug 2023). diff --git a/documentation/navigation/integrations/compose-navigation.md b/documentation/navigation/integrations/compose-navigation.md new file mode 100644 index 000000000..d4e3c0a35 --- /dev/null +++ b/documentation/navigation/integrations/compose-navigation.md @@ -0,0 +1,62 @@ +# Appyx + Jetpack Compose Navigation + +You can easily make Appyx co-exist with Jetpack Compose Navigation. This might be useful if: + +- You want to to use some [AppyxComponents](../../components/index.md) for navigation in your non-Appyx app. +- You want to migrate to Appyx gradually. + +No special downloads are required. + +## Sample + +```kotlin +/** + * This Composable demonstrates how to add Appyx into Jetpack Compose Navigation. + */ +@Composable +fun ComposeNavigationRoot(modifier: Modifier = Modifier) { + val googleNavController = rememberNavController() + NavHost( + navController = googleNavController, + startDestination = "google-route" + ) { + composable("google-route") { + GoogleRoute { googleNavController.navigate("appyx-route") } + } + composable("appyx-route") { + AppyxRoute { googleNavController.navigate("google-route") } + } + } +} + +@Composable +internal fun GoogleRoute( + modifier: Modifier = Modifier, + onNavigateToAppyxRoute: () -> Unit +) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Compose Navigation screen") + Button(onClick = { onNavigateToAppyxRoute() }) { + Text("Navigate to Appyx") + } + } +} + + +@Composable +internal fun AppyxRoute( + onNavigateToGoogleRoute: () -> Unit +) { + NodeHost(integrationPoint = LocalIntegrationPoint.current) { + RootNode( + buildContext = it, + // use this in the Node / some child Node, + // in business logic or in the view on some button: + onNavigateToGoogleRoute = onNavigateToGoogleRoute + ) + } +} +``` diff --git a/documentation/navigation/integrations/di-frameworks.md b/documentation/navigation/integrations/di-frameworks.md new file mode 100644 index 000000000..387af7a32 --- /dev/null +++ b/documentation/navigation/integrations/di-frameworks.md @@ -0,0 +1,39 @@ +# Appyx + DI frameworks + +## Appyx + Hilt + +We've experimented with adding Hilt support for Appyx Nodes, however the solution is not yet finalised. + +You can track and vote on the issue here: + +- [#552](https://github.com/bumble-tech/appyx/issues/552) - Integration: Appyx + Hilt + + +## Appyx + Kodein + +If you'd like us to prioritise it, you can vote on the issue here: + +- [#555](https://github.com/bumble-tech/appyx/issues/555) - Integration: Appyx + Kodein + + +## Appyx + Koin + +If you'd like us to prioritise it, you can vote on the issue here: + +- [#554](https://github.com/bumble-tech/appyx/issues/554) - Integration: Appyx + Koin + + +## Appyx + Dagger + +If you're not too bothered to create the extra classes for Dagger yourself, you can make it work pretty nicely with a tree-based approach like Appyx. We have detailed this in this project's predecessor, [badoo/RIBs](https://github.com/badoo/RIBs/blob/master/documentation/tree-structure-101/providing-dependencies.md#dagger) – however we moved away from it and didn't implement a sample in the scope Appyx. + + +## Appyx + Manual DI + +It's worth mentioning that while manual DI in an unstructured project sounds like a bad idea, with a tree-scoped project structure it can be viable. + +Benefits: + +- [Scoped DI for free](../features/scoped-di.md). +- The tree provides a good enough structure to make it understandable. +- Less boilerplate than with Dagger. diff --git a/documentation/navigation/integrations/ribs.md b/documentation/navigation/integrations/ribs.md new file mode 100644 index 000000000..3e4448998 --- /dev/null +++ b/documentation/navigation/integrations/ribs.md @@ -0,0 +1,12 @@ +# Appyx + RIBs + +Interop classes for a gradual migration from [badoo/RIBs](https://github.com/badoo/RIBs) to Appyx. Please refer to [Downloads](../../releases/downloads.md) for gradle artifacts. + + +## Provided classes + +- `InteropActivity` +- `InteropBackPressHandler` +- `InteropBuilder` +- `InteropNode` +- `InteropView` diff --git a/documentation/navigation/integrations/rx.md b/documentation/navigation/integrations/rx.md new file mode 100644 index 000000000..0e26153d6 --- /dev/null +++ b/documentation/navigation/integrations/rx.md @@ -0,0 +1,44 @@ +# Appyx + RxJava + +Rx2 and Rx3 implementations are provided for the functionality below. Please refer to [Downloads](../../releases/downloads.md) for gradle artifacts. + + +## Connectable + +You can use this together with the [ChildAware API](../features/childaware.md) to set up communication between Nodes in a decoupled way: + +```kotlin +interface Connectable : NodeLifecycleAware { + val input: Relay + val output: Relay +} +``` + +## RetainedInstanceStore + +Provides a singleton store to survive configuration changes. The rx2/rx3 helpers add automatic disposal on objects implementing `Disposable`. + +You can find more details here: [Configuration change](../features/configuration.md) + +```kotlin +import com.bumble.appyx.utils.interop.rx2.store.getRetainedDisposable + +class YourNodeBuilder : Builder() { + + override fun build(buildContext: BuildContext, payload: YourPayload): Node { + + // If your type implements an rx2/rx3 Disposable, + // you don't need to pass a disposer: + val retainedFoo = buildContext.getRetainedDisposable { + Foo(payload) + } + + return YourNode( + buildContext = buildContext, + foo = retainedFoo, + view = view, + ) + } +} +``` + diff --git a/documentation/navigation/integrations/viewmodel.md b/documentation/navigation/integrations/viewmodel.md new file mode 100644 index 000000000..0bc23e8d7 --- /dev/null +++ b/documentation/navigation/integrations/viewmodel.md @@ -0,0 +1,12 @@ +# Appyx + ViewModel + +We've experimented with adding ViewModel support for Appyx Nodes, however the solution is not finalised. + +You can track and vote on the issue here: + +- [#553](https://github.com/bumble-tech/appyx/issues/553) - Integration: Appyx + ViewModel + + +## Alternative: `RetainedInstanceStore` + +Even without a direct ViewModel support, you can pretty much achieve the same by using the [RetainedInstanceStore](../features/configuration.md) Appyx provides. diff --git a/documentation/navigation/navigation/composable-navigation.md b/documentation/navigation/navigation/composable-navigation.md deleted file mode 100644 index 48425cb58..000000000 --- a/documentation/navigation/navigation/composable-navigation.md +++ /dev/null @@ -1,49 +0,0 @@ -# Composable navigation - -[AppyxComponents](../../components/index.md) in Appyx are composable. - -As a single `AppyxComponent` won't be enough for the whole of your whole app, you can use many in a composable way. That is, any managed element of a `AppyxComponent` can also host its own `AppyxComponent`. - - -## Structural element for composing navigation - -```Nodes``` are the main structural element in Appyx. They can host `AppyxComponents`, and they form a tree. - -This allows you to make your app's business logic also composable by leveraging `Nodes` as lifecycled components. - -Read more in [Structuring your app navigation](../apps/structure.md) - - -## Navigation in the tree - - - -Once you've structured your navigation in a composable way, you can add an `AppyxComponent` to a `Node` of this tree and make it dynamic: - -- Some parts in this tree are active while others ore not -- 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 an `AppyxComponent` 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 AppyxComponents affect Nodes - -`AppyxComponent` operations will typically result in: - -- Adding or removing child `Nodes` of a `ParentNode` -- Move them on and off the screen -- Change their states - -As an illustration: - - - -Here: - -- `Back stack` illustrates adding and removing child `Nodes` -- `Tiles` illustrates changing the state of children and removing them from the `ParentNode` - -These are just two examples, you're of course not limited to using them. diff --git a/documentation/navigation/quick-start.md b/documentation/navigation/quick-start.md index f6adc62d5..efcca9442 100644 --- a/documentation/navigation/quick-start.md +++ b/documentation/navigation/quick-start.md @@ -1,6 +1,6 @@ # Quick start guide -You can check out [App structure](apps/structure.md), which explains the concepts you'll encounter in this guide. +You can check out [Composable navigation](concepts/composable-navigation.md), which explains the concepts you'll encounter in this guide. ## The scope of this guide @@ -183,7 +183,7 @@ motionController = { BackStackSlider(it) } Need something more custom? -1. You can check out some other visualisations in the [Back stack documentation](../components/backstack.md), or [create your own](../interactions/uirepresentation.md). +1. You can check out some other visualisations in the [Back stack documentation](../components/backstack.md), or [create your own](../interactions/ui-representation.md). 2. Instead of a back stack, you can also find other [Components](../components/index.md) in the library, or you can [create your own](../interactions/appyxcomponent.md). @@ -231,5 +231,5 @@ You can repeat the same pattern and make any embedded children also a `ParentNod ## Further reading -- Check out [Model-driven navigation](navigation/model-driven-navigation.md) how to take your navigation to the next level -- You can (and probably should) also extract local business logic, the view, any any other components into separate classes and [Plugins](apps/plugins.md). +- Check out [Model-driven navigation](concepts/model-driven-navigation.md) how to take your navigation to the next level +- You can (and probably should) also extract local business logic, the view, any any other components into separate classes and [Plugins](features/plugins.md). diff --git a/documentation/releases/downloads.md b/documentation/releases/downloads.md index 8dce7c707..1056f9436 100644 --- a/documentation/releases/downloads.md +++ b/documentation/releases/downloads.md @@ -60,3 +60,54 @@ dependencies { implementation "com.bumble.appyx:spotlight-js:$version" } ``` + +## Utils and interop with other libraries + +### RxJava 2 + +```groovy +dependencies { + // Optional support for RxJava 2/3 + implementation "com.bumble.appyx:utils-interop-rx2:$version" +} +``` + +### RxJava 3 + +```groovy +dependencies { + implementation "com.bumble.appyx:utils-interop-rx3:$version" +} +``` + +### badoo/RIBs + +```groovy +repositories { + // Don't forget to add this, since badoo/RIBs is hosted on jitpack: + maven(url = "https://jitpack.io") +} + +dependencies { + implementation "com.bumble.appyx:utils-interop-ribs:$version" + +} +``` + + +### Testing + +```groovy + // Test rules and utility classes for testing on Android + debugImplementation "com.bumble.appyx:utils-testing-ui-activity:$version" + androidTestImplementation "com.bumble.appyx:utils-testing-ui:$version" + + // Utility classes for unit testing + testImplementation "com.bumble.appyx:utils-testing-unit-common:$version" + + // Test rules and utility classes for unit testing using JUnit4 + testImplementation "com.bumble.appyx:utils-testing-junit4:$version" + + // Test extensions and utility classes for unit testing using JUnit5 + testImplementation "com.bumble.appyx:utils-testing-junit5:$version" +``` diff --git a/mkdocs.yml b/mkdocs.yml index 0eba58621..62c563f8e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,18 +20,24 @@ nav: - Appyx Navigation: - Overview: navigation/index.md - Quick start guide: navigation/quick-start.md - - Navigation: - - Model-driven navigation: navigation/navigation/model-driven-navigation.md - - Composable navigation: navigation/navigation/composable-navigation.md - - Implicit navigation: navigation/navigation/implicit-navigation.md - - Explicit navigation: navigation/navigation/explicit-navigation.md - - Deep linking: navigation/navigation/deep-linking.md - - App: - - Structuring your app navigation: navigation/apps/structure.md - - Lifecycle: navigation/apps/lifecycle.md - - Plugins: navigation/apps/plugins.md - - ChildAware API: navigation/apps/childaware.md - - Configuration changes: navigation/apps/configuration.md + - Concepts: + - Model-driven navigation: navigation/concepts/model-driven-navigation.md + - Composable navigation: navigation/concepts/composable-navigation.md + - Implicit navigation: navigation/concepts/implicit-navigation.md + - Explicit navigation: navigation/concepts/explicit-navigation.md + - Features: + - Deep linking: navigation/features/deep-linking.md + - Scoped DI: navigation/features/scoped-di.md + - Lifecycle: navigation/features/lifecycle.md + - Plugins: navigation/features/plugins.md + - ChildAware API: navigation/features/childaware.md + - Configuration changes: navigation/features/configuration.md + - Integrations: + - Compose Navigation: navigation/integrations/compose-navigation.md + - DI frameworks: navigation/integrations/di-frameworks.md + - RIBs: navigation/integrations/ribs.md + - RxJava: navigation/integrations/rx.md + - ViewModel: navigation/integrations/viewmodel.md - Appyx Interactions: - Overview: interactions/index.md - Using components: interactions/usage.md @@ -39,7 +45,8 @@ nav: - Component overview: interactions/appyxcomponent.md - Transition model: interactions/transitionmodel.md - Operations: interactions/operations.md - - UI representation: interactions/uirepresentation.md + - UI representation: interactions/ui-representation.md + - KSP setup: interactions/ksp.md - Gestures: interactions/gestures.md - Appyx Components: - Overview: components/index.md @@ -86,6 +93,7 @@ nav: - ChildAware API: 1.x/apps/childaware.md - Configuration changes: 1.x/apps/configuration.md - FAQ: 1.x/faq.md + - FAQ: faq.md # theme configuration @@ -125,7 +133,7 @@ plugins: 'how-to-use-appyx/codelabs.md': '1.x/how-to-use-appyx/codelabs.md' 'how-to-use-appyx/coding-challenges.md': '1.x/how-to-use-appyx/coding-challenges.md' 'how-to-use-appyx/sample-apps.md': '1.x/how-to-use-appyx/sample-apps.md' - 'navigation/model-driven-navigation.md': 'navigation/navigation/model-driven-navigation.md' + 'navigation/model-driven-navigation.md': 'navigation/concepts/model-driven-navigation.md' 'navmodel/index.md': 'components/index.md' 'navmodel/backstack.md': 'components/backstack.md' 'navmodel/spotlight.md': 'components/spotlight.md' @@ -133,19 +141,28 @@ plugins: 'navmodel/tiles.md': '1.x/navmodel/tiles.md' 'navmodel/promoter.md': '1.x/navmodel/promoter.md' 'navmodel/custom.md': 'interactions/appyxcomponent.md' - 'navigation/composable-navigation.md': 'navigation/navigation/composable-navigation.md' - 'navigation/implicit-navigation.md': 'navigation/navigation/implicit-navigation.md' - 'navigation/explicit-navigation.md': 'navigation/navigation/explicit-navigation.md' - 'navigation/deep-linking.md': 'navigation/navigation/deep-linking.md' + 'navigation/composable-navigation.md': 'navigation/concepts/composable-navigation.md' + 'navigation/implicit-navigation.md': 'navigation/concepts/implicit-navigation.md' + 'navigation/explicit-navigation.md': 'navigation/concepts/explicit-navigation.md' + 'navigation/deep-linking.md': 'navigation/features/deep-linking.md' 'ui/children-view.md': '1.x/ui/children-view.md' 'ui/transitions.md': '1.x/ui/transitions.md' - 'apps/structure.md': 'navigation/apps/structure.md' - 'apps/lifecycle.md': 'navigation/apps/lifecycle.md' - 'apps/plugins.md': 'navigation/apps/plugins.md' - 'apps/childaware.md': 'navigation/apps/childaware.md' - 'apps/configuration.md': 'navigation/apps/configuration.md' - 'faq.md': '1.x/faq.md' + 'apps/structure.md': 'navigation/concepts/composable-navigation.md' + 'apps/lifecycle.md': 'navigation/features/lifecycle.md' + 'apps/plugins.md': 'navigation/features/plugins.md' + 'apps/childaware.md': 'navigation/features/childaware.md' + 'apps/configuration.md': 'navigation/features/configuration.md' 'news.md': 'index.md' + 'interactions/uirepresentation.md': 'interactions/ui-representation.md' + 'navigation/navigation/composable-navigation.md': 'navigation/concepts/composable-navigation.md' + 'navigation/navigation/implicit-navigation.md': 'navigation/concepts/implicit-navigation.md' + 'navigation/navigation/explicit-navigation.md': 'navigation/concepts/explicit-navigation.md' + 'navigation/navigation/deep-linking.md': 'navigation/features/deep-linking.md' + 'navigation/apps/structure.md': 'navigation/concepts/composable-navigation.md' + 'navigation/apps/lifecycle.md': 'navigation/features/lifecycle.md' + 'navigation/apps/plugins.md': 'navigation/features/plugins.md' + 'navigation/apps/childaware.md': 'navigation/features/childaware.md' + 'navigation/apps/configuration.md': 'navigation/features/configuration.md' # extensions markdown_extensions: diff --git a/plugins/build.gradle.kts b/plugins/build.gradle.kts index e5bc70668..50f4aa10d 100644 --- a/plugins/build.gradle.kts +++ b/plugins/build.gradle.kts @@ -2,4 +2,4 @@ val buildTask = tasks.register("buildPlugins") subprojects { buildTask.configure { dependsOn(tasks.named("build")) } -} \ No newline at end of file +}