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


    Senior Android Engineer

    Busuu
    Madrid
    • Full Time
    apply now

    Reverse Engineer-Andriod

    Sauce Labs
    Anywhere
    • Full Time
    apply now

    Distinguished Android Engineer

    Expedia Group
    Chicago, London, San Francisco, Austin, Gurgaon, Seattle or Remote
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

, ,

The Evolution of Android Graphics in Android 12/13

Android 12 and 13 both added significant new capabilities to Android platform graphics, including RenderEffect, RuntimeShader, and more. At the same time, RenderScript has been deprecated and we’ve introduced the RenderScript Intrinsics Replacement Toolkit. This…
Watch Video

The Evolution of Android Graphics in Android 12/13

Daniel Galpin
Android Developer Advocate and Fast Talking YouTuber
Google

The Evolution of Android Graphics in Android 12/13

Daniel Galpin
Android Developer Ad ...
Google

The Evolution of Android Graphics in Android 12/13

Daniel Galpin
Android Developer Advocat ...
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 part of the Jetpack Library released by Android last spring. Create Android…
READ MORE
blog
The reason for writing this article is that Text composable function does not support…
READ MORE
blog
RecyclerView is a really cool and powerful tool to display list(s) of content on Android.…
READ MORE
blog
I have been playing around with Compose and recently implemented video playback in a…
READ MORE

Leave a Reply

Your email address will not be published.

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

Menu