将参数从Fragment传递到ViewModel函数

6

你能告诉我我的方法是否正确吗?它能工作但我不确定它的架构是否正确。我在某处读到我们应该避免在负责创建片段/活动的函数中调用viewmodel函数,主要是因为屏幕方向更改会重新调用网络请求,但我真的需要从一个viewmodel传递参数到另一个viewmodel。重要的是,我正在使用Dagger Hilt依赖注入,因此为每个viewmodel创建工厂并不合理?

假设我有一个项目的RecyclerView,点击后我想启动一个显示详细信息的新片段 - 这是常见的事情。由于这些屏幕的逻辑很复杂,我决定将单个viewmodel分成两个 - 一个用于列表片段,一个用于详细信息片段。

items structure

ItemsFragment 有监听器,并使用以下代码启动详细信息片段:

    fun onItemSelected(item: Item) {
        val args = Bundle().apply {
            putInt(KEY_ITEM_ID, item.id)
        }
        findNavController().navigate(R.id.action_listFragment_to_detailsFragment, args)
    }

然后在ItemDetailsFragment类中的onViewCreated函数中,我接收传递的参数,将其保存在ItemDetailsViewModelitemId变量中,然后启动requestItemDetails()函数进行api调用,结果保存到被ItemDetailsFragment观察的LiveData中。

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        //...
        val itemId = arguments?.getInt(KEY_ITEM_ID, -1) ?: -1
        viewModel.itemId = itemId
        viewModel.requestItemDetails()
        //...
    }

项目详情视图模型
class ItemDetailsViewModel @ViewModelInject constructor(val repository: Repository) : ViewModel() {

    var itemId: Int = -1

    private val _item = MutableLiveData<Item>()
    val item: LiveData<Item> = _item

    fun requestItemDetails() {
        if (itemId == -1) {
            // return error state
            return
        }

        viewModelScope.launch {
            val response = repository.getItemDetails(itemId)
            //...
            _item.postValue(response.data)
        }
    }
}
2个回答

16

好消息是,这正是SavedStateHandle的用途,它会自动将参数作为其初始映射接收。

@HiltViewModel
class ItemDetailsViewModel @Inject constructor(
    private val repository: Repository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val itemId = savedStateHandle.getLiveData(KEY_ITEM_ID)

    val item: LiveData<Item> = itemId.switchMap { itemId ->
        liveData(viewModelScope.coroutineContext) {
            emit(repository.getItemDetails(itemId).data)
        }
    }

你如何从片段传递参数? - Volodymyr Zakharov
savedStateHandle为空! - Hussien Fahmy
@HussienFahmy 如果你正确配置了事情,它就不是空的。 - EpicPandaForce
2
这就是我正在寻找的东西。为什么文档中没有提到 SavedStateHandle 也处理 fragment.arguments? - fikr4n
1
@VolodymyrZakharov 在导航调用期间传递的键值束可以在savedStateHandle中检索。例如:findNavController().navigate(R.id.destination, Bundle().apply { putString("key", "value") }) - Sai
显示剩余2条评论

0

我们应该避免在负责创建片段/活动的函数上调用viewmodel函数,主要是因为屏幕方向改变会重新调用网络请求。

是的,在您的示例中,每当创建ItemDetailsFragment视图时都会执行一次请求。

请查看此GitHub问题 有关Hilt的辅助注入支持。 辅助注入的重点在于在对象创建时传递附加依赖项。

这将使您能够通过构造函数传递itemId,然后您就可以在ViewModelinit块中访问它。

class ItemDetailsViewModel @HiltViewModel constructor(
    private val repository: Repository,
    @Assisted private val itemId: Int
) : ViewModel() {

    init {
        requestItemDetails()
    }

    private fun requestItemDetails() {
        // Do stuff with itemId.
    }
}

这样,当创建ItemDetailsViewModel时,网络请求只会执行一次。

当该功能可用时,您可以尝试GitHub问题中建议的解决方法或使用标志模拟init块:

class ItemDetailsViewModel @ViewModelInject constructor(
    private val repository: Repository
) : ViewModel() {

    private var isInitialized = false

    fun initialize(itemId: Int) {
        if (isInitialized) return
        isInitialized = true

        requestItemDetails(itemId)
    }

    private fun requestItemDetails(itemId: Int) {
        // Do stuff with itemId.
    }
}

谢谢您的建议。我正在考虑使用某种标志。同时,我意识到我想要实现不可能的事情,因为我想在不传递参数的情况下将参数传递给函数... 我理解在onCreateView中传递参数的部分是正确的吗?我只需要注意在viewmodel类中避免多次不必要的API调用即可。 - Remzo
一般来说,那是正确的。虽然我猜想有一些特定的用例需要在配置更改时重新加载,例如为横向模式下载更详细的图像。您可以将我的建议视为“经验法则”。 - Alex Krupa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接