Search code examples
androidandroid-fragmentsandroid-viewmodel

Is it ok for a viewmodel function to have a parameter that receives a fragment?


I have a fragment for user input and a viewmodel to handle the validation and IO. Upon insertion of the info I have the UI navigate to a 'success' dialog. Right now this navigation happens from the viewmodel's insert method using a reference to the fragment. It all seems to work but then I remembered a viewmodel shouldn't hold a reference to a fragment or activity. My question is does that apply to a local parameter for a function of the viewmodel? Will it somehow hold on to that reference?

ViewModel code

suspend fun insertNewCustomer(name: String?,address: String?, city: String?, state: String?,
                      phoneNumber: String?, emailAddress: String?, frag: ManualAddCustomerFrag
 ){
    if (    (validateName(name)
            && validateAddress(address)
            && validateCity(city)
            && validateState()
            && validateServiceDays())
        && isAddressTaken(address) == false

    ){

        val customer = CustomerEntity(address!!,name,city,state,phoneNumber, emailAddress)
        viewModelScope.launch {
            customerRepo.insertCustomer(customer)

            val days = convertSelectedDaysToStrings()
            for (i in days.indices){
                val serviceDay = ServiceDayEntity(0,customer.address,days[i])
                customerRepo.addServiceDay(serviceDay)

            }
            withContext(Dispatchers.Main){
                val dialogFragment = SuccessfullyAddedDialog()
                val action = ManualAddCustomerFragDirections.
                actionManualAddCustomerFragToSuccessfullyAddedDialog()
                frag.findNavController().navigate(action)
                }

        }
    }
}

Solution

  • The ViewModel and Fragment should be separated and one should never pass a Fragment as a parameter to a ViewModel. If you want to trigger a Fragment action by a ViewModel, you should use an Observer in the Fragment. I'll post some snippets of my own code to explain what I mean. It should be noted that I'm using an Activity here instead of a Fragment, but the concept remains the same.

    In the ViewModel I created these variables:

    val closeInCall: LiveData<EmptyEvent>
        get() = _closeInCall
    
    private val _closeInCall = MutableLiveData<EmptyEvent>()
    

    When a certain method in the ViewModel gets triggered, this happens:

    _closeInCall.postValue(EmptyEvent())
    

    In my Activity I observe closeInCall like this:

    viewModel.closeInCall.observe(this, EventObserver {
        // Do stuff
    })
    

    The EmptyEvent and EventObserver that I am using are MVVM helper classes I like to use when I don't need to receive any values in the Fragment/Activity, you don't necessarily have to use them.

    EmptyEvent:

    /**
     * An [Event] without content.
     */
    class EmptyEvent : Event<Unit>(Unit)
    

    EventObserver:

    import androidx.lifecycle.Observer
    
    /**
     * An [Observer] for [Event]s, simplifying the pattern of checking if 
    the [Event]'s content has
     * already been handled.
     *
     * [onEventUnhandledContent] is *only* called if the [Event]'s 
    contents has not been handled.
     */
    internal class EventObserver<T>(
        private val onEventUnhandledContent: (T) -> Unit
    ) : Observer<Event<T>> {
        override fun onChanged(event: Event<T>?) {
            event?.getContentIfNotHandled()?.let { value ->
                onEventUnhandledContent(value)
            }
        }
    }