Android Kotlin:合并Flow数据后Google Map仅显示单源标记问题求助
代码托管在GitHub仓库,我正在开发一款地震地图App,该App从两个Web API获取地震信息,并在Google Map上显示其位置。在MainScreen.kt中,我获取数据列表并通过forEach()在地图上添加标记。
MainScreen.kt
@Composable fun MainScreen( viewModel: MainViewModel = hiltViewModel() ) { val items = viewModel.uiState.collectAsStateWithLifecycle() if (items.value is MainActivityUiState.Success) { MainScreen((items.value as MainActivityUiState.Success).earthquakeData) } else { val noItemList = emptyList<EarthquakeInfo>() MainScreen(noItemList) } } @Composable internal fun MainScreen( items: List<EarthquakeInfo> ) { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { Column { val tokyo = LatLng(35.6812, 139.7671) val cameraPositionState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(tokyo, 0f) } GoogleMap( modifier = Modifier.weight(1f), cameraPositionState = cameraPositionState ) { items.forEach { Marker( position = LatLng(it.latitude, it.longitude), title = it.place, snippet = it.place ) } } ... } } }
MainViewModel.kt
@HiltViewModel class MainViewModel @Inject constructor( usgsEarthquakeRepository: UsgsEarthquakeRepository, p2pquakeRepository: P2pquakeRepository, @DefaultDispatcher defaultDispatcher: CoroutineDispatcher, ) : ViewModel() { val uiState: StateFlow<MainActivityUiState> = try { GetLatestEarthquakeInfoUseCase( usgsEarthquakeRepository, p2pquakeRepository, ) .invoke() .flowOn(defaultDispatcher) .map<List<EarthquakeInfo>, MainActivityUiState>(::Success) .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = MainActivityUiState.Loading, ) } catch (e: IOException) { MutableStateFlow(MainActivityUiState.Error).asStateFlow() } } sealed interface MainActivityUiState { object Loading: MainActivityUiState data class Success(val earthquakeData: List<EarthquakeInfo>): MainActivityUiState object Error: MainActivityUiState }
在GetLatestEarthquakeInfoUseCase.kt中,我需要合并来自两个API的数据,但它们的数据结构不同,因此我使用map()创建EarthquakeInfo数据类并提取所需数据,随后使用merge()返回数据列表。
GetLatestEarthquakeInfoUseCase.kt
class GetLatestEarthquakeInfoUseCase @Inject constructor( private val usgsEarthquakeRepository: UsgsEarthquakeRepository, private val p2pquakeRepository: P2pquakeRepository, ) { operator fun invoke(): Flow<List<EarthquakeInfo>> { val usgsEarthquakeFlow = usgsEarthquakeRepository .getInfo("geojson", 20, "time") .map { usgsEarthquakeInfo -> usgsEarthquakeInfo.features .map { feature -> EarthquakeInfo( feature.properties.getDatetime(), feature.properties.place ?: "", feature.geometry.coordinates[1], feature.geometry.coordinates[0], feature.properties.mag, feature.geometry.getDepth(), ) } } val p2pEarthquakeFlow = p2pquakeRepository .getInfo(10, 0) .map { p2pquakeInfo -> p2pquakeInfo.map { data -> EarthquakeInfo( data.earthquake.getDatetime(), data.earthquake.hypocenter.name, data.earthquake.hypocenter.latitude, data.earthquake.hypocenter.longitude, data.earthquake.hypocenter.magnitude, data.earthquake.hypocenter.depth, ) } } return merge(usgsEarthquakeFlow, p2pEarthquakeFlow) } }
运行代码时,p2pEarthquakeFlow的标记会显示约1秒后消失,最终仅显示usgsEarthquakeFlow的标记。我希望同时显示两个来源的标记,请求帮助解决该问题。
我是Android Kotlin新手,英语是我的第二语言,若有表述错误请告知,我会修正。
问题出在merge操作符的使用逻辑上:merge会分别发射两个Flow的结果——先发射p2p的地震列表,再发射usgs的列表,而UI每次只会接收最新的列表并渲染,所以当usgs的数据返回后,UI就会替换成usgs的标记,p2p的标记自然消失。
要解决这个问题,需要把两个Flow的结果合并成一个完整列表后再发射,而非分别发射。可以使用combine操作符实现:
1. 修改GetLatestEarthquakeInfoUseCase的合并逻辑
operator fun invoke(): Flow<List<EarthquakeInfo>> { val usgsEarthquakeFlow = usgsEarthquakeRepository .getInfo("geojson", 20, "time") .map { usgsEarthquakeInfo -> usgsEarthquakeInfo.features .map { feature -> EarthquakeInfo( feature.properties.getDatetime(), feature.properties.place ?: "", feature.geometry.coordinates[1], feature.geometry.coordinates[0], feature.properties.mag, feature.geometry.getDepth(), ) } } val p2pEarthquakeFlow = p2pquakeRepository .getInfo(10, 0) .map { p2pquakeInfo -> p2pquakeInfo.map { data -> EarthquakeInfo( data.earthquake.getDatetime(), data.earthquake.hypocenter.name, data.earthquake.hypocenter.latitude, data.earthquake.hypocenter.longitude, data.earthquake.hypocenter.magnitude, data.earthquake.hypocenter.depth, ) } } // 使用combine合并两个列表,拼接成一个整体列表发射 return combine(usgsEarthquakeFlow, p2pEarthquakeFlow) { usgsList, p2pList -> usgsList + p2pList } }
2. 可选:添加去重逻辑(避免重复标记)
如果两个API可能返回同一地震的重复数据,可以通过distinctBy根据地震的时间、经纬度等唯一标识去重:
return combine(usgsEarthquakeFlow, p2pEarthquakeFlow) { usgsList, p2pList -> (usgsList + p2pList).distinctBy { it.datetime to it.latitude to it.longitude } }
3. 修复ViewModel的异常捕获逻辑
当前的try-catch仅能捕获UseCase创建时的异常,无法处理API请求过程中的异常。需要在Flow链中添加catch操作符:
val uiState: StateFlow<MainActivityUiState> = GetLatestEarthquakeInfoUseCase( usgsEarthquakeRepository, p2pquakeRepository, ) .invoke() .flowOn(defaultDispatcher) .map<List<EarthquakeInfo>, MainActivityUiState>(::Success) .catch { emit(MainActivityUiState.Error) } // 捕获Flow发射过程中的异常 .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = MainActivityUiState.Loading, )
修改完成后,两个API的数据会合并成一个完整列表发射,UI就能同时显示所有来源的标记了。
内容的提问来源于stack exchange,提问作者vegcale

