为了账号安全,请及时绑定邮箱和手机立即绑定

Jetpack Compose Navigation中的软导航请求实现 —— 第一部分

你有没有因为误触而丢失过数据?你误触后可能被带到应用的其他地方,这种经历真的很让人沮丧,可能导致用户不满和对应用的信任度下降。防止这些问题对于保持良好的用户体验至关重要。这就是为什么正确的导航处理在使用 Jetpack Compose Navigation 进行现代应用开发时显得尤为重要。

Jetpack Compose 是 Google 的现代化工具包,用于构建原生的 Android UI,提供了声明式方式来简化 UI 开发。它的导航组件提供了一种直观的管理方式来处理应用内的导航,但如果处理不当,用户在屏幕切换时仍可能遇到意外的数据丢失。

引入软导航请求可以缓解这一问题。软导航允许应用程序在进入新页面之前检查是否有未保存的数据或未完成的任务。通过这样做,开发者可以让用户在离开之前确认,避免数据丢失。

Part 1 没有提到的是回退栈的操作。经过一段时间的写作后,我觉得这应该单独写。

一个了解软导航请求的方式
柔和的导航请求是什么?

在移动应用中,导航涉及在不同的屏幕或组件之间切换。软导航是一种导航尝试,它会在继续之前检查是否有未完成的任务或未保存的更改,而与硬导航不同,硬导航会直接转到新页面,软导航会先暂停一下,看看用户是否会因此丢失重要数据或打断正在进行的工作。

用户确认在这一步骤中起着关键作用。通过提示确认,应用程序确保用户在继续之前了解可能的后果,如数据丢失。这种方法不仅保护了用户的劳动成果,还增强了用户对软件的信任。

轻量导航的用例

带有未保存数据的表格

一个典型的情形是用户填了一个表单但没提交。如果这时离开,可能会弄丢填的资料,可能会让用户感到很挫败。

正在处理的任务或操作

用户可能需要执行诸如编辑内容、上传文件或处理数据等任务。用警告中断这些操作可能会导致进度丢失,从而影响用户的使用体验。

设置更改还未生效

小贴士:当你调整需要保存或应用的设置和偏好时,如果提前退出设置页面,可能让这些更改无法生效,这会让你感到困惑。

用户体验考虑

平衡中断与数据丢失的预防至关重要。虽然防止意外丢失很重要,过度或烦人的提示会打扰到用户。设计直观的确认对话框可以助于这种平衡的实现。这些对话框应该清晰简洁,提供简单直接的选项,如“保存更改”、“放弃”或“取消”。

确认对话能增强易用性。

  • 提供上下文:清楚地说明如果用户继续可能会失去什么。
  • 提供选择:让用户决定是否保存修改或继续不保存修改。
  • 减少干扰:将提示无缝地融入工作流程中,以避免打断用户的专注状态。
实现软导航请求功能

在 Jetpack Compose Navigation 中实现软导航请求涉及创建一个机制,允许应用在导航前检查是否有待处理的操作或未保存的更改。本部分将指导您如何设置导航事件拦截系统,显示确认对话框,并根据用户响应进行处理,以增强用户体验,使交互更加顺畅。

捕获导航过程中的操作

我们将创建一个自定义导航界面,使用请求-响应机制来拦截导航操作。这样可以让应用在离开当前页面之前确认是否安全离开,特别是在当存在未保存的数据时。

导航界面的设计

我们从定义一个 ApplicationNavigation 接口开始,这个接口描述了导航行为。

    /**  

* 定义使用Android Navigation组件的应用程序导航行为的接口。  

* 它提供了一个通用的[navigate]函数用于导航到指定目标,一个弹出返回栈的功能,  

* 以及一个[StateFlow]来跟踪当前的返回栈。此外,它支持一个请求-响应机制,  

* 通过[navigationRequest]来执行预导航检查,如在导航前确认未保存的数据。  
     */  
    interface ApplicationNavigation {  

        /**  

* 导航到指定的[route],可选的[NavOptions]和[Navigator.Extras]。  

* 参数[force]用于确定是否立即导航还是等待导航请求确认,例如检查当前屏幕ViewModel中的未保存数据。  

*  

* @param route 要导航到的目的地路由。  

* @param navOptions 可选的导航选项,用于自定义导航行为。  

* @param navigatorExtras 可选的额外参数,用于传递更多导航细节。  

* @param force 如果为true,则强制立即导航,不等待任何预导航检查。  

* @param onSuccess 成功导航后的执行函数。  

*  

* @return 如果导航成功则返回true,如果被取消则返回false。  
         */  
        fun <T : Any> navigate(  
            route: T,  
            navOptions: NavOptions? = null,  
            navigatorExtras: Navigator.Extras? = null,  
            force: Boolean = false,  
            onSuccess: () -> Unit = {},  
        )  

        /**  

* 弹出返回栈以返回至上一个目的地。如果没有目的地在返回栈上,应用程序可能会退出或导航到定义的根目的地,这取决于具体配置。  
         */  
        fun popBackStack()  

        /**  

* 提供一个表示由[NavHostController]管理的当前返回栈的[StateFlow]。  

* 订阅者可以观察此流以反应式地跟踪导航状态的变化。  
         */  
        val currentBackStack: StateFlow<List<NavBackStackEntry>>  

        /**  

* 一个用于处理预导航检查的[SharedFlow]的[NavigationRequest]s,例如确认当前屏幕是否有未保存的更改  

* 需要在继续之前解决。订阅者(通常是ViewModel)可以监听此流并对此类导航请求作出响应。  
         */  
        val navigationRequest: SharedFlow<NavigationRequest>  

        /**  

* 表示应用程序导航结构的导航图。  
         */  
        val graph: NavGraph  

        /**  

* 表示一个导航请求,包含一个[responseChannel]用于通信导航是否应继续或取消。  

* 这允许实现确认对话框,例如在允许导航前警告用户未保存的更改。  

*  

* @property responseChannel 一个通过发送(true/false)来指示导航是否应该继续(true)或被取消(false)的[SendChannel]。  
         */  
        data class NavigationRequest(val responseChannel: SendChannel<Boolean>)  
    }

这篇文章的关键要素:

  • navigate: 导航到指定的路线。force 参数决定是否绕过预导航检查。
  • navigationRequest: 一个 SharedFlow,用于向感兴趣的监听器发出 NavigationRequest 对象,以便处理导航前的逻辑操作。
  • NavigationRequest:封装了一个用于向导航控制器发送响应的通信通道。
实现导航

接下来,我们将使用 NavHostController 来管理应用内的实际导航。

    /**  

* 这是 [ApplicationNavigation] 接口的一个实现,使用 [NavHostController] 来管理应用程序中的目的地导航,并与 ViewModel 或其他组件或服务协调导航逻辑。  

*  

* 此类支持带有可选 'force' 参数的导航流程,该参数可强制执行导航而不考虑任何待处理的导航请求。它还集成了基于协程的机制来处理导航请求和响应,允许在实际导航之前显示确认提示,例如未保存数据警告。  

*  

* @param navHostController 负责在应用内控制导航的 NavHostController。  

* @param dispatcher 用于管理协程执行的调度程序提供者。  
     */  
    internal class AppNavigationImpl(  
        private val navHostController: NavHostController,  
        private val dispatcher: IDispatcherProvider,  
    ) : ApplicationNavigation {  

        override fun <T : Any> navigate(  
            route: T,  
            navOptions: NavOptions?,  
            navigatorExtras: Navigator.Extras?,  
            force: Boolean,  
            onSuccess: () -> Unit,  
        ) {  
            if (force) {  
                // 立即导航,不执行任何预检查  
                navHostController.navigate(route.toString(), navOptions, navigatorExtras)  
                onSuccess()  
            } else {  
                // 处理带有预导航检查的导航请求  
                MainScope().launch(dispatcher.default()) {  
                    if (_navigationRequest.subscriptionCount.value > 0) {  
                        val channel = Channel<Boolean>(capacity = Channel.UNLIMITED)  
                        val canNavigate = suspendCoroutine<Boolean> { cont ->  
                            CoroutineScope(dispatcher.default()).launch {  
                                channel.consumeAsFlow().collect {  
                                    cont.resume(it)  
                                    cancel()  
                                }  
                            }  

                            CoroutineScope(dispatcher.default()).launch {  
                                _navigationRequest.emit(ApplicationNavigation.NavigationRequest(channel))  
                            }  
                        }  

                        if (canNavigate) {  
                            withContext(dispatcher.ui()) {  
                                navHostController.navigate(route.toString(), navOptions, navigatorExtras)  
                                onSuccess()  
                            }  
                        }  
                    } else {  
                        withContext(dispatcher.ui()) {  
                            navHostController.navigate(route.toString(), navOptions, navigatorExtras)  
                            onSuccess()  
                        }  
                    }  
                }  
            }  
        }  

        override fun popBackStack() {  
            navHostController.popBackStack()  
        }  

        override val currentBackStack: StateFlow<List<NavBackStackEntry>>  
            get() = navHostController.currentBackStack  

        private val _navigationRequest = MutableSharedFlow<ApplicationNavigation.NavigationRequest>()  

        override val navigationRequest: SharedFlow<ApplicationNavigation.NavigationRequest> =  
            _navigationRequest.asSharedFlow()  

        override val graph: NavGraph  
            get() = navHostController.graph  
    }

解释:

  • 强制导航: 如果 forcetrue,导航将立即发生,而不会进行任何检查。
  • 导航请求:forcefalse 时,我们会检查是否有订阅 _navigationRequest 的订阅者。如果有,我们会发出一个 NavigationRequest 并等待相应的回应。
  • 协程机制: 我们利用协程来处理导航控制器与订阅者(例如视图模型)之间的异步通信。
  • 响应处理: 只有在收到的响应为 true 时,才会继续导航。
检测未保存的修改

我们需要检测未保存的更改,并响应导航请求。这通常在一个 视图模型 中处理。

视图模型的实现

在该屏幕上的视图模型中,该屏幕可能包含未保存的数据。

    class FormViewModel(  
        private val applicationNavigation: ApplicationNavigation  
    ) : ViewModel() {  

        private val _hasUnsavedChanges = MutableStateFlow(false)  
        val hasUnsavedChanges: StateFlow<Boolean> = _hasUnsavedChanges.asStateFlow()  

        init {  
            viewModelScope.launch {  
                applicationNavigation.navigationRequest.collect { request ->  
                    if (_hasUnsavedChanges.value) {  
                        // 显示确认对话框给用户  
                        val userConfirmed = showUnsavedChangesDialog()  
                        request.responseChannel.send(userConfirmed)  
                    } else {  
                        // 没有未保存的更改,继续导航进程  
                        request.responseChannel.send(true)  
                    }  
                }  
            }  
        }  

        // 更新未保存更改状态的方法  
        fun onDataChanged() {  
            _hasUnsavedChanges.value = true  
        }  

        // 在保存后重置未保存更改状态的方法  
        fun onDataSaved() {  
            _hasUnsavedChanges.value = false  
        }  

        fun confirmNavigation() {  
            // 假设这是当前请求通道的引用  
            navigationRequestChannel?.trySend(true)  
            navigationRequestChannel = null  
        }  

        fun cancelNavigation() {  
            navigationRequestChannel?.trySend(false)  
            navigationRequestChannel = null  
        }  

        // 显示确认对话框的函数(实现方式可能有所不同)  
        private suspend fun showUnsavedChangesDialog(): Boolean {  
            // 实现可能通过 MutableStateFlow 或 LiveData 与 UI 层通信  
            // 为了简单起见,假设它在用户确认后返回 true 确认继续  
            return true  
        }  
    }
显示确认框

我们将创建一个用于确认对话框的可组合函数,用于提示用户,并根据 ViewModel 的状态来管理其显示状态。

    @Composable  
    fun UnsavedChangesDialog(  
        onConfirm: () -> Unit,  
        onDismiss: () -> Unit  
    ) {  
        AlertDialog(  
            onDismissRequest = onDismiss,  
            title = { Text("有未保存的更改") },  
            text = { Text("您愿意放弃这些更改并离开页面吗?") },  
            confirmButton = {  
                TextButton(onClick = onConfirm) {  
                    Text("丢弃")  
                }  
            },  
            dismissButton = {  
                TextButton(onClick = onDismiss) {  
                    Text("算了")  
                }  
            }  
        )  
    }
    @Composable  
    fun FormScreen(viewModel: FormViewModel) {  
        val hasUnsavedChanges by viewModel.hasUnsavedChanges.collectAsState()  

        if (viewModel.showDialog) {  
            UnsavedChangesDialog(  
                onConfirm = {  
                    // 关闭对话框  
                    viewModel.confirmNavigation()  
                },  
                onDismiss = {  
                    // 关闭对话框  
                    viewModel.cancelNavigation()  
                }  
            )  
        }  

        // 其他UI元素...  
    }
测试流动情况

测试一下这两种场景中的导航流程的体验。

有未保存的更改:

  • 尝试导航到其他页面。
  • 弹出了一个确认框。
  • 如果用户点了确认,就继续导航。
  • 如果用户取消了,导航就会停止。

没有未保存的修改:

  • 试着离开这个页面,导航直接进行,没有提示。
实用小窍门和最佳实践

使用挂起协程实现软导航请求需要谨慎管理,以确保良好的用户体验和可维护的代码库。以下是一些实用的方法和技巧,可帮助您更好地管理和使用挂起协程,并跟踪它们的状态。

管理暂停的协程
确保正确重启
  • 一次恢复: 确保每个挂起的协程只恢复一次。多次恢复会导致 IllegalStateException,而未能恢复则可能导致挂起的协程无限期挂起。
    suspendCoroutine<Boolean> { continuation ->  
        // 做一些异步操作  
        asyncOperation { result ->  
            continuation.resume(result)  
        }  
    }
  • 应对异常情况: 用 try-catch 块来包裹 resume 的调用,以免产生异常导致协程崩溃。

尝试执行代码块。如果成功,continuation.resume(result) 会将结果传递给下一个函数。如果失败,continuation.resumeWithException(e) 会将异常传递给下一个函数。

  • 设置超时: 设置超时,以避免函数被永久挂起。
withTimeoutOrNull(5000) {  
    suspendCoroutine { /* ... */ }  
}
防止内存泄漏
  • 生命周期感知: 在适当的范围内启动协程,比如 viewModelScopelifecycleScope,以将其生命周期绑定到组件上,并防止内存泄漏。
  • 取消处理: 务必正确处理协程取消,尤其是当 UI 组件可能被销毁时。
高级用例场景
如何应对深层链接(deep links)和嵌套导航(嵌套式导航)

当您的应用支持深度链接时,用户可以直接跳转到特定屏幕,从而绕过包含未保存更改的中间页面。虽然我认为大多数情况下这会是一个“强制”情形,但也可能需要采取不同的策略来检查未保存的更改。

嵌套的导航图

应用程序常常使用嵌套导航图来管理复杂的导航路径。确保软导航操作在嵌套图中顺利执行至关重要。

实施策略如下:

  • 确保导航请求在嵌套图中正确传播,每个嵌套的NavHost需要订阅navigationRequest流。
  • 嵌套图中的本地ViewModel应该感知导航请求并进行相应处理。
最后的感想

在 Jetpack Compose Navigation 中实现软导航请求有助于提高用户体验并确保数据的完整性。意外的导航可能导致数据丢失,这可能会显著影响用户的满意度和信任感。通过检测未保存更改、提示用户确认并优雅处理导航事件,这样可以创建一个更用户友好的应用程序。

在本文中,我们谈到了:

  • 软导航请求的概念:了解硬导航和软导航之间的区别,并识别需要用户确认以防止数据丢失的场景。
  • 实施策略:利用 Jetpack Compose 的功能拦截导航事件,使用 ViewModels 管理状态,显示确认对话,并有效处理用户响应。
  • 最佳实践:专注于协程管理、用户为中心的设计理念、代码的可维护性以及彻底测试,以创建可靠和可维护的代码库。
  • 高级用例:为了构建全面的导航系统,我们应对复杂场景,如深层链接、嵌套导航图、状态恢复、性能优化以及可访问性问题。

通过细心处理挂起的协程并跟踪它们的恢复点,你可以确保应用程序的导航逻辑既高效又不出错。这种对细节的关注可以防止诸如内存泄漏或协程挂起等潜在问题,并从而提供更流畅的用户体验。

继续往前走

在您的项目中集成软导航功能时,请参考以下步骤:

  • 小规模开始: 为了熟悉概念和机制,在一个有未保存数据的屏幕上实现软导航。
  • 迭代和完善: 逐步将其实现扩展到应用程序的其他部分,完善用户提示并处理各种边缘情况。
  • 保持更新: Jetpack Compose及其导航组件在不断更新。关注官方文档和社区资源,获取最新最佳实践和更新。
最后的鼓励.

软导航请求的实现可能最初看起来很复杂,但对提升用户满意度和确保数据完整性的好处非常明显。花时间去理解并应用这些技术,这可以提升你所开发应用的质量,并成长为能够应对各种高级挑战的开发者。

请记住,目标是开发让用户喜爱和信任的应用程序。通过细心的导航设计防止数据意外丢失是朝着这一目标前进了一步。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消