如何通过Dagger Hilt实现跨模块全局单例注入DataStore实例
解决方案
根因说明
你出现该问题的核心原因是手动构造了多个CheckLocationIsSetDataStore实例,多个DataStore实例操作同一个本地存储文件会触发并发冲突,导致读写异常。你已经配置了Hilt的Singleton级别的依赖提供逻辑,只要通过注入方式获取实例,Hilt会自动保证全局唯一。
改造步骤
1. 为ViewModel注入DataStore实例
不要在Composable视图层直接操作DataStore,将数据操作逻辑放到ViewModel层,通过构造函数注入全局唯一的DataStore实例:
PickLocationViewModel 改造
@HiltViewModel class PickLocationViewModel @Inject constructor( private val checkLocationIsSetDataStore: CheckLocationIsSetDataStore, // 你的其他原有依赖,比如导航器等 ) : ViewModel() { // 保留你原有的location状态逻辑 val location = MutableStateFlow<Location?>(null) // 可以封装保存位置的方法,也可以直接在Composable中调用注入的dataStore fun saveSelectedLocation() = viewModelScope.launch { location.value?.let { checkLocationIsSetDataStore.set(it) } } }
SplashViewModel 改造
@HiltViewModel class SplashViewModel @Inject constructor( private val checkLocationIsSetDataStore: CheckLocationIsSetDataStore, // 你的其他原有依赖 ) : ViewModel() { // 暴露位置状态流给视图层 val locationStatusFlow = checkLocationIsSetDataStore.get // 你的原有导航逻辑保留 }
2. 改造Composable代码,删除手动构造DataStore的逻辑
PickLocation模块MapScreen改造
删除手动构造DataStore的两行代码,直接调用ViewModel的能力:
@Composable fun MapScreen(navigatorViewModel: PickLocationViewModel = hiltViewModel()) { // 删除这两行代码: // val context = LocalContext.current // val dataStore = CheckLocationIsSetDataStore(context = context) Surface(color = AppColor.ThemeColor.BACKGROUND) { val mapView = rememberMapViewWithLifecycle() Column(Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) { MapViewContainer(mapView, navigatorViewModel) MapPinOverlay() Column(modifier = Modifier.align(Alignment.BottomCenter)) { Button( colors = ButtonDefaults.buttonColors( backgroundColor = AppColor.brandColor.BLUE_DE_FRANCE, contentColor = AppColor.neutralColor.DOCTOR ), onClick = { navigatorViewModel.apply { navigate(HomeDestination.route()) viewModelScope.launch { // 直接使用ViewModel中注入的DataStore checkLocationIsSetDataStore.set(location.value) // 也可以调用你封装好的saveSelectedLocation()方法 } } }) { Text( text = stringResource(R.string.confirm_address), style = AppFont.PoppinsTypography.button ) } Spacer(modifier = Modifier.size(16.dp)) } } } } }
Splash模块SplashView改造
同样删除手动构造DataStore的代码,使用ViewModel暴露的状态流:
@OptIn(ExperimentalPermissionsApi::class) @Composable private fun SplashView( modifier: Modifier, multiplePermissionsState: MultiplePermissionsState, navigator: SplashViewModel = hiltViewModel() ) { // 删除这两行代码: // val context = LocalContext.current // val dataStore = CheckLocationIsSetDataStore(context = context) Box( modifier = modifier .fillMaxSize() .background(color = AppColor.brandColor.BLUE_DE_FRANCE) .padding(start = 64.dp, end = 64.dp, bottom = 16.dp) ) { Column(modifier = modifier.align(Alignment.Center)) { Image( painter = painterResource(id = R.drawable.mykuyatm), contentDescription = "mykuya tm image" ) Image( painter = painterResource(id = R.drawable.mykuya_powered_by), contentDescription = "mykuya powered by image" ) } Loading(modifier = modifier.align(Alignment.BottomCenter)) FeatureThatRequiresPermission( multiplePermissionsState = multiplePermissionsState, permissionsState = { if (it) { navigator.apply { viewModelScope.launch { delay(Constants.SPLASH_DELAY) // 使用ViewModel中注入的DataStore暴露的流 locationStatusFlow.collect { model -> model.let { if (model.latitude == Constants.IF_LOCATION_LAT_NOT_SET && model.longitude == Constants.IF_LOCATION_LNG_NOT_SET) { navigate(PickLocationDestination.route()) }else{ navigate(HomeDestination.route()) } } } } } } }) } }
注意事项
- 所有需要使用
CheckLocationIsSetDataStore的模块,都要在gradle中添加对preferences模块的依赖,否则无法访问对应的类 - 全程不要手动调用
CheckLocationIsSetDataStore(context)构造实例,所有实例都通过Hilt注入获取,才能保证全局唯一 - 如果特殊场景需要在Composable中直接获取DataStore实例,可以通过Hilt EntryPoint获取,但是优先推荐将数据操作放到ViewModel层处理,更符合MVVM架构规范
内容的提问来源于stack exchange,提问作者Saeed Noshadi
相关产品推荐
相关产品推荐

