如何更新 MutableStateFlow 内的元素并动态更改 UI?

问题描述 投票:0回答:1

我对 Android 开发还比较陌生。在设计应用程序时,我遇到了一些问题。我发现的一切并不能完全满足我的需求。我想听听您的建议。所以,好吧,我们走吧!

主屏幕显示特定类别的菜肴列表。我希望更新添加到数据库的项目上的图标。

型号:

data class MealModel(
    val id: String,
    val name: String,
    val image: String,
    val isSaved: Boolean = false,
)

通过状态获取资源:

sealed class Resource<T>(
    val data: T? = null,
    val message: String? = null,
) {
    class Success<T>(data: T) : Resource<T>(data)
    class Error<T>(message: String?, data: T? = null) : Resource<T>(data, message)
    class Loading<T> : Resource<T>()
}

视图模型:

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val getMealsUseCase: GetMealsUseCase,
    private val isMealInSavedUseCase: IsMealInSavedUseCase,
    private val addOrRemoveMealUseCase: AddOrRemoveMealUseCase,
    private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : ViewModel() {
    private val _meals = MutableStateFlow<Resource<List<MealModel>>>(Resource.Loading())
    val meals: StateFlow<Resource<List<MealModel>>> = _meals.asStateFlow()

    fun getMeals() {
        getMealsUseCase.invoke(category = category)
            .onEach { resource ->
                when (resource) {
                    is Resource.Loading -> {
                        _meals.value = Resource.Loading()
                    }

                    is Resource.Success -> {
                        if (resource.data != null) {
                            _meals.value = Resource.Success(resource.data)
                        }
                    }

                    is Resource.Error -> {
                        _meals.value = Resource.Error(resource.message)
                    }
                }
            }
            .launchIn(viewModelScope)
    }

    fun savedIconClicked(meal: MealModel) {
        viewModelScope.launch(dispatcher) {
            addOrRemoveMealUseCase.addOrRemoveMeal(meal)
        }
    }
}

在寻找答案后,我意识到我可以做这样的事情:

val index = _meals.value.data!!.indexOf(item)
val items = _meals.value.data!!.toMutableList()
items[index] = items[index].copy(isSaved = item.isSaved.not())
_meals.value.data = items

事实是问题出现在

Resource
data
val
,因此我无法分配新列表。如果您创建了
var
一切都会根据日志进行。

现在在函数内部

getMeals()
,当状态成功时,它看起来像这样:

is Resource.Success -> {
    if (resource.data != null) {
       _meals.value = Resource.Success(resource.data!!)
    }
    for (item in resource.data!!) {
        if (isMealInSavedUseCase.isMealInSaved(item.id)) {
           val index = _meals.value.data!!.indexOf(item)
           val items = _meals.value.data!!.toMutableList()
           items[index] = items[index].copy(isSaved = item.isSaved.not())
           _meals.value.data = items
        }
    }
}

但是,首先,更改

data
中的
Resource
可能不是特别可取。其次,当再次请求时,列表将再次更新为带有
MealModel
isSaved = false
元素。也就是说,我们不会注意到任何更改添加元素的尝试。第三,我不太明白添加图标应该如何动态更新。使用列表元素是在适配器中,但我们不能在那里添加
LiveData
,它将存储特定的图标。

最后,为了便于理解,我将向您展示列表元素的样子: List item

android kotlin kotlin-flow kotlin-stateflow
1个回答
0
投票

我在您的代码中看到两个相关问题。

第一个是在视图模型中收集流(使用

launchIn
)。不要这样做,流应该只在视图模型中进行“转换”,而不是收集。相反,将 getMeals 和属性替换为如下内容:
private val category: MutableStateFlow<String?> = MutableStateFlow(null)

val meals: StateFlow<Resource<List<MealModel>>> = category
    .flatMapLatest { category ->
        if (category != null) getMealsUseCase.invoke(category = category)
        else flowOf()
    }
    .map(::processResource)
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = Resource.Loading(),
    )

fun selectCategory(category: String) {
    this.category.value = category
}

private fun processResource(resource: Resource<List<MealModel>>): Resource<List<MealModel>> {
    // ...
}

重要的是,我将 
getMealsUseCase

流所依赖的所有内容都变成了流本身。这只是类别,对于我的示例,我假设它是一个字符串。不过,您可以将其类型替换为您想要的任何类型。现在可以使用

getMealsUseCase
在其基础上构建
flatMapLatest
流程。这将用新流替换底层流。新流可以在此处访问基础流的值(类别)。然后通过调用
map
来转换新流的内容。我只是将占位符函数
processResource
放在这里,我们稍后再讨论。最后,将生成的流转换为 StateFlow。从外面看,
meals
属性现在看起来和以前一模一样。
现在不再从 UI 调用 

getMeals

,而是调用

selectCategory
。这将更新视图模型的
category
流,进而更新 UI 收集的
meals
流。
您可能已经意识到,旧 

getMeals

功能的大部分部分现在都丢失了。接下来我们进入第二期。您之前所做的是尝试改变以前的值而不是创建新值。如果 StateFlow 仅包含不可变类型,则效果最佳。您不愿意将

data
属性设置为
var
是完全正确的,您应该将其改回
val
。使用上面的新代码,不需要改变
data
属性,而我们只需要填充我之前留空的
processResource
函数。正如您所看到的,它接收一个资源参数并期望另一个资源作为返回值。请注意,不涉及任何流:此函数通过流转换被
称为
,但它本身独立于任何流。 现在,我不完全确定这种转变到底应该做什么。我最好的猜测是这样的:

private fun processResource(resource: Resource<List<MealModel>>): Resource<List<MealModel>> = when (resource) { is Resource.Loading -> Resource.Loading() is Resource.Success -> { if (resource.data != null) { val data = resource.data .map { item -> if (isMealInSavedUseCase.isMealInSaved(item.id)) item.copy(isSaved = item.isSaved.not()) else item } Resource.Success(data) } else TODO("implement here whatever the result should be when resource.data == null") } is Resource.Error -> Resource.Error(resource.message) }

Success

情况下,数据列表将 map 转换为一个新列表,其中包含当

isSaved
为 true 时其
isMealInSaved
属性反转的所有项目。这可能不完全是您想要的;正如我已经提到的,我并不真正理解您的原始代码试图做什么,特别是当数据为空时。您可以以此为例并从这里开始。
作为结束语,请不要使用 

!!

运算符,

它会使您的应用程序崩溃
。如果您认为有必要这样做,那么您的代码中很可能存在其他问题。

© www.soinside.com 2019 - 2024. All rights reserved.