Blog Infos
Author
Published
Topics
, ,
Published

This article answers:

The proposed approach is universal and can be applied to other similar cases.

Let’s dive into it. Here is the main utility class:

/**
 * This class helps to unitize back navigation. There can be several folded BackNavElements.
 *
 * Each time user presses the Back button, the whole chain is asked one-by-one starting from the most child node
 * whether any handler needs to process itself. In case it needs - it processes and returns the Result.CANNOT_GO_BACK.
 * Whole chain do not proceed if ANY of the elements returns the Result.CANNOT_GO_BACK.
 *
 * So, the chain is processed only if ALL of its sub handles returned the Result.CAN_GO_BACK.
 *
 * This utilize the cases when user has several dialogs on the screen that needs to be closed
 * one-by-one.
 */
class BackNavElement private constructor(
    private var child: BackNavElement? = null,
    private val handler: () -> Result
) {

    enum class Result {
        CANNOT_GO_BACK,
        CAN_GO_BACK
    }

    /**
     * Adds element to the END of the chain.
     */
    fun add(element: BackNavElement?) {
        this.child?.let {
            it.add(element)
        } ?: run {
            this.child = element
        }
    }

    fun tryGoBack(): Result {
        if (child?.tryGoBack() == Result.CANNOT_GO_BACK) {
            return Result.CANNOT_GO_BACK
        }
        return handler()
    }

    companion object {

        fun default(child: BackNavElement? = null, handler: () -> Unit) =
            BackNavElement(
                child = child,
                handler = {
                    handler()
                    BackNavElement.Result.CAN_GO_BACK
                })

        fun needsProcessing(child: BackNavElement? = null, handler: () -> Result) =
            BackNavElement(child = child, handler = handler)
    }
}

Main takeaway points:

Now let’s look at the example to understand how we can take advantage of it.

Confirmation dialog

Use case:

ExitConfirmDialog.kt

@Composable
fun ExitDialog(@StringRes bodyResId: Int, onNavIconClicked: () -> Unit): BackNavElement {
    val showDialog = rememberMutableStateOf(value = false)

    val defaultBackHandler = BackNavElement.needsProcessing {
        if (!showDialog.value) {
            showDialog.value = true
            BackNavElement.Result.CANNOT_GO_BACK
        } else {
            BackNavElement.Result.CAN_GO_BACK
        }
    }

    if (showDialog.value) {
        DefaultAlertDialog(
            onDismiss = { showDialog.value = false },
            titleResId = R.string.exit_confirmation_title,
            bodyResId = bodyResId,
            positiveButtonResId = R.string.confirm,
            onPositiveClick = {
                showDialog.value = false
                onNavIconClicked()
            },
            negativeButtonResId = R.string.cancel
        )
    }

    return defaultBackHandler
}

Client Composablecontent:

val rootBackHandler = ExitDialog(
    bodyResId = R.string.exit_confirmation_body,
    onNavIconClicked = onNavIconClicked
)
DefaultBackHandler(rootBackHandler)

// other views such as Text, Icon, etc.

Here onNavIconClicked is a callback that does navController.popBackStack() on a higher level.

DefaultBackHandler:

@Composable
fun DefaultBackHandler(backNavElement: BackNavElement) = BackHandler { backNavElement.tryGoBack() }

Where BackHandler is the default Composable provided by the framework.

What happens here?

So the only way to go back is to press theConfirm button on AlertDialog.

Let’s look at another example.

ModalBottomSheet dialog

Use case:

There is a ModalBottomSheet dialog on the screen. It may be hidden or shown.

To archive it the only thing that needs to be added to the client code is:

DefaultBackHandler(
    BackNavElement.default(
        child = modalBackNavElement(modalState, coroutineScope),
        handler = onNavIconClicked
    )
)

Where modalBackNavElement() is a helper method:

@OptIn(ExperimentalMaterialApi::class)
fun modalBackNavElement(
    state: ModalBottomSheetState,
    coroutineScope: CoroutineScope,
    callback: (() -> Unit)? = null
) = BackNavElement.needsProcessing {
    if (state.isVisible) {
        state.hideAnd(coroutineScope = coroutineScope, thenCallback = { callback?.invoke() })
        BackNavElement.Result.CANNOT_GO_BACK
    } else {
        BackNavElement.Result.CAN_GO_BACK
    }
}

hideAnd():

@OptIn(ExperimentalMaterialApi::class)
fun ModalBottomSheetState.hideAnd(
    coroutineScope: CoroutineScope,
    thenCallback: () -> Unit
) {
    coroutineScope.launch {
        if (isVisible) {
            hide()
            thenCallback()
        }
    }
}

That’s it! The next example shows how to build chains.

Chains

Imagine that the screen has several states. They are represented as separate Composable functions. The appropriate Composable function is shown depending on the view state.

The Composable ChildComposable2 has 2 modals. They need to be correctly handled when the user presses the Back button.

So the screen root Composable contains:

val rootBackHandler = ExitDialog(
    bodyResId = R.string.exit_confirmation_body,
    onNavIconClicked = onNavIconClicked
)
DefaultBackHandler(rootBackHandler)

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

Then depending on the state ChildComposable2 may be added into the composition.

@Composable
fun ChildComposable2(

    rootBackHandler: BackNavElement,
    // other parameters
) {
     rootBackHandler.apply {
        add(modalBackNavElement(addWalletModalState, coroutineScope))
        add(modalBackNavElement(walletsListModalState, coroutineScope))
    // other views such as Text, Icon, etc.
    }
}

As you see, the only thing that is needed is to pass rootBackHandler to the child Composable. This Composable adds its handlers via add() method.

The whole chain will be processed in the next priority — walletsListModalState, addWalletModalState, rootBackHandler .

This can be scaled to any number of child Composables or modal windows.

This is an example how several modals on the same screen can be properly handled

Conclusion

Although you may think that the solution is complex actually it’s already simplified. Previously I had children instead of child in the BackNavElementclass. However, with some time passed I realized that it was overkill for my app. But if you have heavy and complex screens — you may find it useful for your use cases.

I think that maybe some libraries exist to utilize all the work. If not — write a comment, perhaps I will do some.

If you know simpler (but still flexible) solutions — let me know.

Happy coding! Follow me on Twitter.

This article was originally published on proandroiddev.com on March 10, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Compose is a relatively young technology for writing declarative UI. Many developers don’t even…
READ MORE
blog
When it comes to the contentDescription-attribute, I’ve noticed a couple of things Android devs…
READ MORE
blog
In this article we’ll go through how to own a legacy code that is…
READ MORE
blog
Compose is part of the Jetpack Library released by Android last spring. Create Android…
READ MORE

1 Comment. Leave new

  • Partysan
    23.10.2022 20:42

    Please share a very simple example of the full code. I’m a beginner and can’t understand the code. If I would have a working example I’m sure I could understand it.

    Thanks in adcance! 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu