Android Kotlin ViewModel数据过滤失效问题求助
Android Kotlin ViewModel数据过滤失效问题
通过CharacterViewModel调用getCharactersUseCase从API获取分页数据后,尝试根据角色ID过滤数据并在CharacterDetailScreen中观察结果,但始终无法得到预期的过滤后数据。
CharacterViewModel代码
@HiltViewModel class CharacterViewModel @Inject constructor( private val getCharactersUseCase: GetCharactersUseCase ) : ViewModel() { private val _characterState: MutableStateFlow<PagingData<Character>> = MutableStateFlow(value = PagingData.empty()) private val _characterDetailsState: MutableStateFlow<Character?> = MutableStateFlow(null) val charactersState: MutableStateFlow<PagingData<Character>> get() = _characterState val characterDetailsState: MutableStateFlow<Character?> get() = _characterDetailsState init { getCharacters() } private fun getCharacters() { viewModelScope.launch { getCharactersUseCase.execute(Unit) .distinctUntilChanged() .cachedIn(viewModelScope) .collect { _characterState.value = it } } } fun getCharacterDetails(characterID: String) { Log.i("character"," character from viewmodel received id= ${characterID}") // Collect the latest charactersState and then filter the character viewModelScope.launch { charactersState.collect { characters -> characters .filter { it.id == characterID } .map { character -> Log.i("character"," character from viewmodel id= ${character.id}") _characterDetailsState.value = character } } } } }
CharacterDetailScreen代码
@Composable fun CharacterDetailScreen( navController: NavController, viewModel: CharacterViewModel = hiltViewModel(), characterId: String ) { // Collect character details state val characterDetailsState by viewModel.characterDetailsState.collectAsState() LaunchedEffect(characterDetailsState) { viewModel.getCharacterDetails(characterId) } // Use LaunchedEffect to trigger the getCharacterDetails call when the Composable is first launched CharacterDetailsItem(characterDetailsState, characterId) }
CharacterDetailsItem代码
@Composable fun CharacterDetailsItem(character_: Character?, characterId: String){ Log.i("character"," character from details item ${character_?.name}") val character by remember { mutableStateOf(character_) } Box(modifier = Modifier.padding(top = 8.dp)) { Card( colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceVariant, ), modifier = Modifier .fillMaxWidth() ) { Column { Text( text = characterId, modifier = Modifier .padding(16.dp), textAlign = TextAlign.Center, ) character?.name?.let { Text( text = it, modifier = Modifier .padding(16.dp), textAlign = TextAlign.Center, ) } character?.gender?.let { Text( text = it, modifier = Modifier .padding(16.dp), textAlign = TextAlign.Center, ) } } } } }
Logcat日志
2023-11-17 12:01:19.709 5486-5486 character com.starwars.app I character from details item null 2023-11-17 12:01:19.740 5486-5486 character com.starwars.app I character from viewmodel received id= 631a02dc-ecf2-4636-8e59-d180766e52cf 2023-11-17 12:01:19.760 5486-5486 character com.starwars.app I character from details item null 2023-11-17 12:01:20.472 5486-5486 character com.starwars.app I character from details item null 2023-11-17 12:01:20.524 5486-5486 character com.starwars.app I character from details item null
已尝试方案
- 直接给
_characterDetailsState赋值测试,能正常显示,但从charactersState过滤无法生效:
fun getCharacterDetails(characterID: String) { Log.i("character", " character from viewmodel received id= ${characterID}") // Collect the latest charactersState and then filter the character viewModelScope.launch { _characterDetailsState.value = Character("213","bday","e","g","h","hi","name","skin") } }
- 在Composable中通过
LazyPagingItems获取数据并过滤,仍无法正常显示:
@Composable fun CharacterDetailScreen( navController: NavController, viewModel: CharacterViewModel = hiltViewModel(), characterId: String ) { // Collect character details state val characterPagingItems: LazyPagingItems<Character> = viewModel.charactersState.collectAsLazyPagingItems() val selectedCharacter = characterPagingItems.itemSnapshotList .find { it?.id == characterId } // Call CharacterDetailsItem when selectedCharacter is not null selectedCharacter?.let { character -> CharacterDetailsItem(character, characterId) //this composable gets recomposed with null values } Text(text = "first") //this text stays }
DI模块代码
@Module @InstallIn(ViewModelComponent::class) object CharacterModule { @Provides @ViewModelScoped fun providesCharacterRemoteDataSource( api: StarWarsApi ): CharacterRemoteDataSource { return CharacterRemoteDataSourceImpl(api) } @Provides @ViewModelScoped fun providesCharacterRepository( characterRemoteDataSource: CharacterRemoteDataSource, starWarsDatabase: StarWarsDatabase ): CharacterRepository { return CharacterRepositoryImpl(characterRemoteDataSource, starWarsDatabase) } @Provides @ViewModelScoped fun providesGetCharactersUseCase( characterRepository: CharacterRepository ): GetCharactersUseCase { return GetCharactersUseCase(characterRepository) } @Provides @ViewModelScoped fun providesGetCharacterDetailsUseCase( characterRepository: CharacterRepository ): GetCharacterDetailsUseCase { return GetCharacterDetailsUseCase(characterRepository) } @Provides @ViewModelScoped fun provideCharacterDetailsViewModel( getCharactersUseCase: GetCharactersUseCase ): CharacterViewModel { return CharacterViewModel(getCharactersUseCase) } }
问题排查分析
- PagingData操作逻辑错误:
PagingData是分页专用数据流,无法直接用filter/map进行数据过滤,这些操作不会对分页数据生效,只会返回空的PagingData实例。 - LaunchedEffect触发条件错误:用
characterDetailsState作为触发条件,会导致每次状态变化就重复调用getCharacterDetails,且初始状态为null时触发调用,此时分页数据可能还未加载完成。 - CharacterDetailsItem缓存错误:
val character by remember { mutableStateOf(character_) }会缓存初始的null值,后续character_更新时不会同步更新该变量。 - DI配置冗余:手动提供
CharacterViewModel的代码与@HiltViewModel注解冲突,Hilt会自动处理ViewModel的实例化,无需手动提供。
解决方案
方案一:使用详情接口(推荐)
直接利用已有的GetCharacterDetailsUseCase获取单个角色详情,比从分页数据中过滤更高效,还能避免分页数据不全的问题:
@HiltViewModel class CharacterViewModel @Inject constructor( private val getCharactersUseCase: GetCharactersUseCase, private val getCharacterDetailsUseCase: GetCharacterDetailsUseCase // 注入详情用例 ) : ViewModel() { // 原有代码不变 fun getCharacterDetails(characterID: String) { Log.i("character"," character from viewmodel received id= ${characterID}") viewModelScope.launch { // 假设execute返回Result<Character>,根据实际业务逻辑调整 val result = getCharacterDetailsUseCase.execute(characterID) result.onSuccess { character -> _characterDetailsState.value = character Log.i("character"," character from viewmodel id= ${character.id}") } } } }
同时修改CharacterDetailScreen的触发逻辑:
@Composable fun CharacterDetailScreen( navController: NavController, viewModel: CharacterViewModel = hiltViewModel(), characterId: String ) { val characterDetailsState by viewModel.characterDetailsState.collectAsState() // 仅在characterId变化时调用一次详情接口 LaunchedEffect(characterId) { viewModel.getCharacterDetails(characterId) } CharacterDetailsItem(characterDetailsState, characterId) }
方案二:修复分页数据过滤逻辑
如果必须从分页数据中过滤,需调整LazyPagingItems的使用方式:
- 修改
CharacterDetailScreen:
@Composable fun CharacterDetailScreen( navController: NavController, viewModel: CharacterViewModel = hiltViewModel(), characterId: String ) { val characterPagingItems = viewModel.charactersState.collectAsLazyPagingItems() // 根据分页快照和characterId实时查找目标角色 val selectedCharacter = remember(characterPagingItems.itemSnapshotList, characterId) { characterPagingItems.itemSnapshotList.find { it?.id == characterId } } // 仅首次进入时加载分页数据 LaunchedEffect(Unit) { if (characterPagingItems.itemCount == 0) { viewModel.getCharacters() } } CharacterDetailsItem(selectedCharacter, characterId) }
- 修复
CharacterDetailsItem的缓存问题:
@Composable fun CharacterDetailsItem(character_: Character?, characterId: String){ Log.i("character"," character from details item ${character_?.name}") // 移除错误的remember缓存,直接使用传入的character_ Box(modifier = Modifier.padding(top = 8.dp)) { Card( colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceVariant, ), modifier = Modifier.fillMaxWidth() ) { Column { Text( text = characterId, modifier = Modifier.padding(16.dp), textAlign = TextAlign.Center, ) character_?.name?.let { Text( text = it, modifier = Modifier.padding(16.dp), textAlign = TextAlign.Center, ) } character_?.gender?.let { Text( text = it, modifier = Modifier.padding(16.dp), textAlign = TextAlign.Center, ) } } } } }
- 移除DI模块中手动提供
CharacterViewModel的代码:
// 删除这段冗余代码 @Provides @ViewModelScoped fun provideCharacterDetailsViewModel( getCharactersUseCase: GetCharactersUseCase ): CharacterViewModel { return CharacterViewModel(getCharactersUseCase) }
内容的提问来源于stack exchange,提问作者Zobaer Hossain
相关产品推荐
相关产品推荐

