You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 00:47:50