如何在Compose+Dagger-Hilt的MVVM架构中向ViewModel注入FusedLocationProviderClient
解决方案:在Compose+Hilt的MVVM架构中注入FusedLocationProviderClient到ViewModel
核心思路是通过Dagger-Hilt封装位置服务的创建逻辑,让ViewModel通过构造注入获取依赖,完全避免持有Activity上下文或使用AndroidViewModel,同时遵循MVVM的职责分离原则,方便单元测试。
步骤1:创建Hilt模块提供FusedLocationProviderClient
在Hilt的Activity级别组件中提供FusedLocationProviderClient实例,安全获取Activity上下文,同时不让ViewModel直接持有上下文:
@Module @InstallIn(ActivityComponent::class) object LocationModule { @Provides fun provideFusedLocationProviderClient(@ActivityContext context: Context): FusedLocationProviderClient { return LocationServices.getFusedLocationProviderClient(context) } }
步骤2:ViewModel构造注入依赖(无需AndroidViewModel)
用@HiltViewModel标记ViewModel,通过构造函数直接注入FusedLocationProviderClient,ViewModel只负责业务逻辑,不依赖Android上下文:
@HiltViewModel class MapViewModel @Inject constructor( private val fusedLocationClient: FusedLocationProviderClient ) : ViewModel() { private val _currentLocation = MutableStateFlow<Location?>(null) val currentLocation = _currentLocation.asStateFlow() fun fetchCurrentLocation() { viewModelScope.launch { try { // 使用kotlinx-coroutines-play-services的await()将Task转为挂起函数 val location = fusedLocationClient.lastLocation.await() _currentLocation.value = location } catch (e: Exception) { // 处理权限异常、位置不可用等情况 _currentLocation.value = null } } } }
注意:需添加
kotlinx-coroutines-play-services依赖支持Task的挂起函数转换。
步骤3:在Composable中使用ViewModel更新GoogleMap
通过hiltViewModel()获取ViewModel实例,收集位置状态并更新地图:
@Composable fun MapScreen( viewModel: MapViewModel = hiltViewModel() ) { val currentLocation by viewModel.currentLocation.collectAsState() GoogleMap( cameraPositionState = rememberCameraPositionState { currentLocation?.let { position = CameraPosition.fromLatLngZoom( LatLng(it.latitude, it.longitude), 15f ) } } ) { currentLocation?.let { Marker( position = LatLng(it.latitude, it.longitude), title = "当前位置" ) } } // 屏幕初始化时请求位置 LaunchedEffect(Unit) { viewModel.fetchCurrentLocation() } }
进阶优化:封装位置服务接口(更易测试)
为完全解耦ViewModel与Android特定类,建议封装位置服务接口,让ViewModel依赖抽象而非具体实现:
定义接口
interface LocationService { suspend fun getCurrentLocation(): Location? }
实现接口
class FusedLocationServiceImpl @Inject constructor( @ActivityContext private val context: Context ) : LocationService { private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context) override suspend fun getCurrentLocation(): Location? { return try { fusedLocationClient.lastLocation.await() } catch (e: Exception) { null } } }
更新Hilt模块
@Module @InstallIn(ActivityComponent::class) object LocationModule { @Provides fun provideLocationService(@ActivityContext context: Context): LocationService { return FusedLocationServiceImpl(context) } }
更新ViewModel
@HiltViewModel class MapViewModel @Inject constructor( private val locationService: LocationService ) : ViewModel() { private val _currentLocation = MutableStateFlow<Location?>(null) val currentLocation = _currentLocation.asStateFlow() fun fetchCurrentLocation() { viewModelScope.launch { _currentLocation.value = locationService.getCurrentLocation() } } }
单元测试示例
ViewModel依赖抽象接口,单元测试无需依赖Android框架,直接mock接口即可:
class MapViewModelTest { private lateinit var viewModel: MapViewModel private val mockLocationService = mockk<LocationService>() @Before fun setup() { viewModel = MapViewModel(mockLocationService) } @Test fun `fetchCurrentLocation updates state when location is available`() = runTest { val mockLocation = mockk<Location>() coEvery { mockLocationService.getCurrentLocation() } returns mockLocation viewModel.fetchCurrentLocation() assertEquals(mockLocation, viewModel.currentLocation.value) } @Test fun `fetchCurrentLocation sets null when location fails`() = runTest { coEvery { mockLocationService.getCurrentLocation() } returns null viewModel.fetchCurrentLocation() assertNull(viewModel.currentLocation.value) } }
关键优势
- 完全遵循MVVM:ViewModel专注业务逻辑,不持有Android上下文,避免内存泄漏
- 依赖注入解耦:通过Hilt管理依赖,无需在Activity中手动创建ViewModel或位置客户端
- 单元测试友好:无需AndroidViewModel,mock抽象接口即可完成纯粹的单元测试
内容的提问来源于stack exchange,提问作者Kyle Angelo Gonzales
相关产品推荐
相关产品推荐

