Skip to content

Deprecation warning

This page in the documentation is about Appyx 1.x.

Appyx is now in its 2.x iteration.

To access the 2.x-related pages please check the sidebar or go to:

Documentation root


Implementing your own navigation models

A step-by-step guide. You can also take a look at other existing examples to see these in practice.

Step 1

Create the class; define your possible states; define your initial state.

class Foo<NavTarget : Any>(
    initialItems: List<NavTarget> = listOf(),
    savedStateMap: SavedStateMap?
) : BaseNavModel<NavTarget, Foo.State>(
    screenResolver = FooOnScreenResolver, // We'll see about this shortly
    finalState = DESTROYED, // Anything transitioning towards this state will be discarded eventually
    savedStateMap = savedStateMap // It's nullable if you don't need state restoration
) {

    // Your possible states for any single navigation target
    enum class State {
        CREATED, FOO, BAR, BAZ, DESTROYED;
    }

    // You can go about it any other way.
    // Back stack for example defines only a single element.
    // Here we take all the <NavTarget> elements and make them transition CREATED -> FOO immediately.
    override val initialElements = initialItems.map {
        FooElement(
            key = NavKey(it),
            fromState = State.CREATED,
            targetState = State.FOO,
            operation = Operation.Noop()
        )
    }
}

(optional) Step 2

Add some convenience aliases:

typealias FooElement<NavTarget> = NavElement<NavTarget, Foo.State>

typealias FooElements<NavTarget> = NavElements<NavTarget, Foo.State>

sealed interface FooOperation<NavTarget> : Operation<NavTarget, Foo.State>

Step 3

Define one or more operations.

@Parcelize
class SomeOperation<NavTarget : Any> : FooOperation<NavTarget> {

    override fun isApplicable(elements: FooElements<NavTarget>): Boolean =
        TODO("Define whether this operation is applicable given the current state")

    override fun invoke(
        elements: FooElements<NavTarget>,
    ): NavElements<NavTarget, Foo.State> =
        // TODO: Mutate elements however you please. Add, remove, change.
        //  In this example we're changing all elements to transition to BAR.
        //  You can also use helper methods elements.transitionTo & elements.transitionToIndexed 
        elements.map {
            it.transitionTo(
                newTargetState = BAR,
                operation = this
            )
        }
}

// You can add an extension method for a leaner API
fun <NavTarget : Any> Foo<NavTarget>.someOperation() {
    accept(FooOperation())
}

Step 4

Add the screen resolver to define which states should be / should not be part of the composition in the end:

object FooOnScreenResolver : OnScreenStateResolver<State> {
    override fun isOnScreen(state: State): Boolean =
        when (state) {
            Foo.State.CREATED,
            Foo.State.DESTROYED -> false
            Foo.State.FOO,
            Foo.State.BAR,
            Foo.State.BAZ, -> true
        }
}

Step 5

Add one or more transition handlers to interpret different states and translate them to Jetpack Compose Modifiers.

class FooTransitionHandler<NavTarget>(
    private val transitionSpec: TransitionSpec<Foo.State, Float> = { spring() }
) : ModifierTransitionHandler<NavTarget, Foo.State>() {

    // TODO define a Modifier depending on the state.
    //  Here we'll just mutate scaling: 
    override fun createModifier(
        modifier: Modifier,
        transition: Transition<Foo.State>,
        descriptor: TransitionDescriptor<NavTarget, Foo.State>
    ): Modifier = modifier.composed {
        val scale = transition.animateFloat(
            transitionSpec = transitionSpec,
            targetValueByState = {
                when (it) {
                    Foo.State.CREATED -> 0f
                    Foo.State.FOO -> 0.33f
                    Foo.State.BAR -> 0.66f
                    Foo.State.BAZ -> 1.0f
                    Foo.State.DESTROYED -> 0f
                }
            })

        scale(scale.value)
    }
}

// TODO remember to add:
@Composable
fun <NavTarget> rememberFooTransitionHandler(
    transitionSpec: TransitionSpec<Foo.State, Float> = { spring() }
): ModifierTransitionHandler<NavTarget, Foo.State> = remember {
    FooTransitionHandler(transitionSpec)
}

Test it

Add Children to your Node. Pass your NavModel and the transition handler:

@Composable
override fun View(modifier: Modifier) {
    Children(
        modifier = Modifier.fillMaxSize(),
        navModel = foo,
        transitionHandler = rememberFooTransitionHandler()
    )
}

Somewhere else in your business logic trigger the operations you defined. Make sure they're called on the same foo instance that you pass to the Children composable:

foo.someOperation()

As soon as this is triggered, elements should transition to the BAR state in this example, and you should see them scale up defined by the transition handler.

Created something cool?

Let us know!