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

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

已尝试方案

  1. 直接给_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")
    }
}
  1. 在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)
    }
}

问题排查分析

  1. PagingData操作逻辑错误:PagingData是分页专用数据流,无法直接用filter/map进行数据过滤,这些操作不会对分页数据生效,只会返回空的PagingData实例。
  2. LaunchedEffect触发条件错误:用characterDetailsState作为触发条件,会导致每次状态变化就重复调用getCharacterDetails,且初始状态为null时触发调用,此时分页数据可能还未加载完成。
  3. CharacterDetailsItem缓存错误:val character by remember { mutableStateOf(character_) }会缓存初始的null值,后续character_更新时不会同步更新该变量。
  4. 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的使用方式:

  1. 修改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)
}
  1. 修复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,
                    )
                }
            }
        }
    }
}
  1. 移除DI模块中手动提供CharacterViewModel的代码:
// 删除这段冗余代码
@Provides
@ViewModelScoped
fun provideCharacterDetailsViewModel(
    getCharactersUseCase: GetCharactersUseCase
): CharacterViewModel {
    return CharacterViewModel(getCharactersUseCase)
}

内容的提问来源于stack exchange,提问作者Zobaer Hossain

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 01:25:54