Compose全局处理Snackbar展示的实现方式是否合理?
全局Snackbar实现方式是否正确?
我是Jetpack Compose新手,参考各类视频与博客后编写了以下代码,用于全局处理Snackbar的展示,想请教该实现方式是否正确?
MainActivity 代码
class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { SunnyTheme { val snackBarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() Scaffold( snackbarHost = { SnackbarHost(snackBarHostState) }, ) { Box(modifier = Modifier.padding(it)) { val showSnackBar: ( message: String?, actionLabel: String, actionPerformed: () -> Unit, dismissed: () -> Unit ) -> Unit = { message, actionLabel, actionPerformed, dismissed -> coroutineScope.launch { val result = message?.let { snackBarHostState.showSnackbar( message = it, actionLabel = actionLabel, ) } when (result) { ActionPerformed -> { actionPerformed.invoke() } Dismissed -> { dismissed.invoke() } else -> { Timber.e("WTf.......$#$%") } } } } SunnyApp(showSnackBar) } } } } } } @Composable fun SunnyApp( showSnackBar: ( message: String?, actionLabel: String, actionPerformed: () -> Unit, dismissed: () -> Unit ) -> Unit, ) { val navController = rememberNavController() SetupNavGraph(navHostController = navController, showSnackBar = showSnackBar) }
导航图代码
@Composable fun SetupNavGraph( navHostController: NavHostController, showSnackBar: ( message: String?, actionLabel: String, actionPerformed: () -> Unit, dismissed: () -> Unit ) -> Unit, ) { NavHost( navController = navHostController, startDestination = Screen.Splash.route, ) { composable(route = Screen.Splash.route) { SplashScreen { navHostController.popBackStack() navHostController.navigate(Screen.Home.route) } } composable(route = Screen.Home.route) { HomeScreen( showSnackBar = showSnackBar, navigateToSelectLocationScreen = { navHostController.navigate(Screen.SelectLocation.route) }, ) } composable(route = Screen.SelectLocation.route) { SelectLocationScreen() } } }
HomeScreen 调用代码
@Composable fun HomeScreen( homeViewModel: HomeViewModel = hiltViewModel(), showSnackBar: ( message: String?, actionLabel: String, actionPerformed: () -> Unit, dismissed: () -> Unit ) -> Unit, navigateToSelectLocationScreen: () -> Unit, ) { val homeUiState by homeViewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current if (homeUiState.lat == 0.0) { LoadingUi() LocationPermissionUI( context = context, permissionCallback = { permissionAction -> when (permissionAction) { is PermissionAction.OnPermissionGranted -> { Timber.e("Location permission granted") getUserCurrentLocation(context, permissionAction.precise) { Timber.e("Lat : ${it.result.latitude}") Timber.e("Lng : ${it.result.longitude}") homeViewModel.setLocation(it.result.latitude, it.result.longitude) } } is PermissionAction.OnPermissionDenied -> { Timber.e("Location permission denied") } } }, ) } else { when (homeUiState.networkResult) { is NetworkResult.Loading -> { LoadingUi() } is NetworkResult.Success -> { } is NetworkResult.Error -> { } is NetworkResult.Exception -> { var exceptionMessage = (homeUiState.networkResult as NetworkResult.Exception).throwable.localizedMessage Timber.e("Exception message: $exceptionMessage") showSnackBar.invoke( exceptionMessage, "Retry", { homeViewModel.setLocation(homeUiState.lat, homeUiState.lon) }, { //do nothing } ) } } } }
实现思路的合理性
整体思路是正确的:将SnackbarHost放在顶层Scaffold中,通过函数类型参数将展示Snackbar的能力向下传递给各个页面,这种方式能保证全局只有一个Snackbar宿主,避免多个页面各自创建Snackbar导致的冲突,是Compose中实现全局Snackbar的常规方案之一。
可优化的点
1. 参数优化,简化调用
message设为可空,但实际调用时如果传null,showSnackbar不会执行,建议要么处理null情况(比如设置默认提示文本),要么将message改为非空类型,避免无意义的调用。- 给
actionLabel、actionPerformed、dismissed添加默认值,减少调用时的冗余代码:
这样在不需要action或者dismiss回调时,直接调用val showSnackBar: ( message: String, actionLabel: String = "", actionPerformed: () -> Unit = {}, dismissed: () -> Unit = {} ) -> Unit = { message, actionLabel, actionPerformed, dismissed -> coroutineScope.launch { val result = snackBarHostState.showSnackbar( message = message, actionLabel = actionLabel, ) when (result) { ActionPerformed -> actionPerformed() Dismissed -> dismissed() else -> Timber.e("Snackbar result unexpected: $result") } } }showSnackBar("提示文本")即可。
2. 避免重组导致重复弹出Snackbar
当前在NetworkResult.Exception分支中直接调用showSnackBar,由于Compose的重组特性,当homeUiState变化时,只要分支条件满足,就会重复执行调用,导致Snackbar多次弹出。需要用LaunchedEffect包裹调用,依赖对应的状态,确保只执行一次:
is NetworkResult.Exception -> { val exceptionMessage = it.throwable.localizedMessage ?: "未知异常" Timber.e("Exception message: $exceptionMessage") LaunchedEffect(homeUiState.networkResult) { showSnackBar( exceptionMessage, "Retry", { homeViewModel.setLocation(homeUiState.lat, homeUiState.lon) } ) } }
这里利用了when的智能转换,直接用it访问NetworkResult.Exception的属性,不需要强制类型转换。
3. 代码结构优化
可以将showSnackBar的逻辑提取为单独的Composable函数,让MainActivity的代码更简洁:
@Composable fun ProvideSnackbar( content: @Composable (showSnackBar: (String, String, () -> Unit, () -> Unit) -> Unit) -> Unit ) { val snackBarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() val showSnackBar: (String, String, () -> Unit, () -> Unit) -> Unit = { message, actionLabel, actionPerformed, dismissed -> coroutineScope.launch { val result = snackBarHostState.showSnackbar(message, actionLabel) when (result) { ActionPerformed -> actionPerformed() Dismissed -> dismissed() else -> Timber.e("Snackbar result unexpected: $result") } } } Scaffold(snackbarHost = { SnackbarHost(snackBarHostState) }) { paddingValues -> Box(modifier = Modifier.padding(paddingValues)) { content(showSnackBar) } } }
然后在MainActivity中使用:
setContent { SunnyTheme { ProvideSnackbar { showSnackBar -> SunnyApp(showSnackBar) } } }
4. 异常消息的安全处理
localizedMessage可能为null,建议添加默认值,避免传递null给Snackbar:
val exceptionMessage = it.throwable.localizedMessage ?: context.getString(R.string.unknown_error)
内容的提问来源于stack exchange,提问作者Vicky
相关产品推荐
相关产品推荐

