Pass arguments from fragment to viewmodel function

Viewed 6288

Can you tell me if my approach is right? It works but I don't know if it's correct architecture. I read somewhere that we should avoid calling viewmodel function on function responsible for creating fragments/activities mainly because of screen orientation change which recall network request but I really need to pass arguments from one viewmodel to another one. Important thing is I'm using Dagger Hilt dependency injection so creating factory for each viewmodel isn't reasonable?

Assume I have RecyclerView of items and on click I want to launch new fragment with details - common thing. Because logic of these screens is complicated I decided to separate single viewmodel to two - one for list fragment, one for details fragment.

items structure

ItemsFragment has listener and launches details fragment using following code:

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

Then in ItemDetailsFragment class in onViewCreated function I receive passed argument, saves it in ItemDetailsViewModel itemId variable and then launch requestItemDetails() function to make api call which result is saved to LiveData which is observed by ItemDetailsFragment

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

ItemDetailsViewModel

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 Answers

Good news is that this is what SavedStateHandle is for, which automatically receives the arguments as its initial map.

@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)
        }
    }

we should avoid calling viewmodel function on function responsible for creating fragments/activities mainly because of screen orientation change which recall network request

Yes, in your example a request will be executed whenever ItemDetailsFragment's view is created.

Take a look at this GitHub issue about assisted injection support in Hilt. The point of assisted injection is to pass additional dependencies at object's creation time.

This will allow you to pass itemId through the constructor, which then will allow you to access it in ViewModel's init block.

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

    init {
        requestItemDetails()
    }

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

This way the network request will be executed just once when ItemDetailsViewModel is created.


By the time the feature is available you can either try workarounds suggested in the GitHub issue or simulate the init block with a flag:

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.
    }
}
Related