我对 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
,它将存储特定的图标。
最后,为了便于理解,我将向您展示列表元素的样子:
我在您的代码中看到两个相关问题。
第一个是在视图模型中收集流(使用
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
属性反转的所有项目。这可能不完全是您想要的;正如我已经提到的,我并不真正理解您的原始代码试图做什么,特别是当数据为空时。您可以以此为例并从这里开始。作为结束语,请不要使用 !!
运算符,
它会使您的应用程序崩溃。如果您认为有必要这样做,那么您的代码中很可能存在其他问题。