LiveData not able to observe the changes

Viewed 6108

I am updating a LiveData value from a DialogFragment in the ViewModel, but not able to get the value in Fragment.

The ViewModel:

class OtpViewModel(private val otpUseCase: OtpUseCase, analyticsModel: IAnalyticsModel) : BaseViewModel(analyticsModel) {
    override val globalNavModel = GlobalNavModel(titleId = R.string.otp_contact_title, hasGlobalNavBar = false)

    private val _contactListLiveData = MutableLiveData<List<Contact>>()
    val contactListLiveData: LiveData<List<Contact>>
        get() = _contactListLiveData

    private lateinit var cachedContactList: LiveData<List<Contact>>
    private val contactListObserver = Observer<List<Contact>> {
        _contactListLiveData.value = it
    }



    private lateinit var cachedResendOtpResponse: LiveData<LogonModel>
    private val resendOTPResponseObserver = Observer<LogonModel> {
        _resendOTPResponse.value = it
    }

    private var _resendOTPResponse = MutableLiveData<LogonModel>()
    val resendOTPResponseLiveData: LiveData<LogonModel>
        get() = _resendOTPResponse

    var userSelectedIndex : Int = 0 //First otp contact selected by default

    val selectedContact : LiveData<Contact>
        get() = MutableLiveData(contactListLiveData.value?.get(userSelectedIndex))

    override fun onCleared() {
        if (::cachedContactList.isInitialized) {
            cachedContactList.removeObserver(contactListObserver)
        }

        if (::cachedOtpResponse.isInitialized) {
            cachedOtpResponse.removeObserver(otpResponseObserver)
        }

        super.onCleared()
    }

    fun updateIndex(pos: Int){
        userSelectedIndex = pos
    }

    fun onChangeDeliveryMethod() {
        navigate(
            OtpVerificationHelpCodeSentBottomSheetFragmentDirections
                .actionOtpContactVerificationBottomSheetToOtpChooseContactFragment()
        )
    }

    fun onClickContactCancel() {
        navigateBackTo(R.id.logonFragment, true)
    }

    fun retrieveContactList() {
        cachedContactList = otpUseCase.fetchContactList()
        cachedContactList.observeForever(contactListObserver)
    }



    fun resendOTP(contactId : String){
        navigateBack()
        cachedResendOtpResponse = otpUseCase.resendOTP(contactId)
        cachedResendOtpResponse.observeForever(resendOTPResponseObserver)

    }
}

The BaseViewModel:

abstract class BaseViewModel(val analyticsModel: IAnalyticsModel) : ViewModel() {
    protected val _navigationCommands: SingleLiveEvent<NavigationCommand> = SingleLiveEvent()
    val navigationCommands: LiveData<NavigationCommand> = _navigationCommands

    abstract val globalNavModel: GlobalNavModel


    /**
     * Posts a navigation event to the navigationsCommands LiveData observable for retrieval by the view
     */
    fun navigate(directions: NavDirections) {
        _navigationCommands.postValue(NavigationCommand.ToDirections(directions))
    }

    fun navigate(destinationId: Int) {
        _navigationCommands.postValue(NavigationCommand.ToDestinationId(destinationId))
    }

    fun navigateBack() {
        _navigationCommands.postValue(NavigationCommand.Back)
    }

    fun navigateBackTo(destinationId: Int, isInclusive: Boolean) {
        _navigationCommands.postValue(NavigationCommand.BackTo(destinationId, isInclusive))
    }

    open fun init() {
        // DEFAULT IMPLEMENTATION - override to initialize your view model
    }


    /**
     * Called from base fragment when the view has been created.
     */
    fun onViewCreated() {
        analyticsModel.onNewState(getAnalyticsPathCrumb())
    }

    /**
     * gets the Path for the current page to be used for the trackstate call
     *
     * Override this method if you need to modify the path
     *
     * the page id for the track state call will be calculated in the following manner
     * 1) analyticsPageId
     * 2) titleId
     * 3) the page title string
     */
    protected fun getAnalyticsPathCrumb() : AnalyticsBreadCrumb {

        return analyticsBreadCrumb {
            pathElements {
                if (globalNavModel.analyticsPageId != null) {
                    waPath {
                        path = PathElement(globalNavModel.analyticsPageId as Int)
                    }
                } else if (globalNavModel.titleId != null) {
                    waPath {
                        path = PathElement(globalNavModel.titleId as Int)
                    }
                } else {
                    waPath {
                        path = PathElement(globalNavModel.title ?: "")
                    }
                }
            }
        }
    }
}

The DialogFragment:

class OtpVerificationHelpCodeSentBottomSheetFragment : BaseBottomSheetDialogFragment(){

    private lateinit var rootView: View
    lateinit var binding: BottomSheetFragmentOtpVerificationHelpCodeSentBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

        viewModel = getViewModel<OtpViewModel>()

        binding = DataBindingUtil.inflate(inflater, R.layout.bottom_sheet_fragment_otp_verification_help_code_sent, container, false)

        rootView = binding.root

        return rootView
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)


        val otpViewModel = (viewModel as OtpViewModel)
        binding.viewmodel = otpViewModel

        otpViewModel.resendOTPResponseLiveData.observe(viewLifecycleOwner, Observer {

            it?.let { resendOtpResponse ->
                if(resendOtpResponse.statusCode.equals("000")){
                    //valid status code
                    requireActivity().toastMessageOtp(getString(R.string.otp_code_verification_sent))
                }else{
                    //show the error model
                    //it?.errorModel?.let { it1 -> handleDiasNetworkError(it1) }
                }
            }

        })
    }
}

I am calling the resendOTP(contactId : String) method of the viewmodel from the xml file of the DialogFragment:

 <TextView
            android:id="@+id/verification_help_code_sent_resend_code"
            style="@style/TruTextView.SubText2.BottomActions"
            android:layout_height="@dimen/spaceXl"
            android:gravity="center_vertical"
            android:text="@string/verification_help_resend_code"
            android:onClick="@{() -> viewmodel.resendOTP(Integer.toString(viewmodel.userSelectedIndex))}"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/top_guideline" />

Now whenever I try to call resendOTPResponseLiveData from the Fragment it does not gets called:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Log.d("OtpVerify" , "OnViewCreatedCalled")
        viewModel.onViewCreated()
        val otpViewModel = (viewModel as OtpViewModel)

        binding.lifecycleOwner = this
        binding.viewmodel = otpViewModel
        binding.toAuthenticated = OtpVerifyFragmentDirections.actionOtpVerifyFragmentToAuthenticatedActivity()
        binding.toVerificationBtmSheet = OtpVerifyFragmentDirections.actionOtpVerifyFragmentToOtpContactVerificationCodeSentBottomSheet()


        otpViewModel.resendOTPResponseLiveData.observe(viewLifecycleOwner, Observer {
            if(it?.statusCode.equals("000")){
                //valid status code
                requireActivity().toastMessageOtp(getString(R.string.otp_code_verification_sent))
            }else{
                //show the error model
                it?.errorModel?.let { it1 -> handleDiasNetworkError(it1) }
            }
        })

    }

So what wrong I am doing here.

EDIT

Basically I need clicklistener(resend button click) in dialogfragment, and need to read it in the fragment. So I used the concept of SharedViewModel.

So I make necessary changes in the ViewModel:

private val selected = MutableLiveData<LogonModel>()

 fun select(logonModel: LogonModel) {
        selected.value = logonModel
    }

    fun getSelected(): LiveData<LogonModel> {
        return selected
    }

In the DialogFragment:

 otpViewModel.resendOTPResponseLiveData.observe(viewLifecycleOwner, Observer{

           otpViewModel.select(it);

        })

And in the fragment where I want to read the value:

otpViewModel.getSelected().observe(viewLifecycleOwner, Observer {

            Log.d("OtpVerify" , "ResendCalled")
            // Update the UI.
            if(it?.statusCode.equals("000")){
                //valid status code
                requireActivity().toastMessageOtp(getString(R.string.otp_code_verification_sent))
            }else{
                //show the error model
                it?.errorModel?.let { it1 -> handleDiasNetworkError(it1) }
            }
        })

But it is still not working.

Edit:

ViewModel Source for fragment:

viewModel = getSharedViewModel<OtpViewModel>(from = {
            Navigation.findNavController(container as View).getViewModelStoreOwner(R.id.two_step_authentication_graph)
        })

ViewModel Source for dialogfragment:

viewModel = getViewModel<OtpViewModel>()
2 Answers

Being new-ish to the Jetpack library and Kotlin a few months back I ran into a similar issue, if I understand you correctly.

I think the issue here is that you are retrieving you ViewModel using the by viewModels which means the ViewModel you get back will only be scoped to the current fragments context... If you would like to share a view model across multiple parts of your application they have to be activity scoped.

So for example:

//this will only work for the current fragment, using this declaration here and anywhere else and observing changes wont work, the observer will never fire, except if the method is called within the same fragment that this is declared
private val viewModel: AddPatientViewModel by viewModels {
    InjectorUtils.provideAddPatientViewModelFactory(requireContext())
}

//this will work for the ANY fragment in the current activies scope, using this code and observing anywhere else should work, the observer will fire, except if the method is called fro another activity
private val patientViewModel: PatientViewModel by activityViewModels {
    InjectorUtils.providePatientViewModelFactory(requireContext())
}

Notice my viewModel of type AddPatientViewModel is scoped to the current fragments context only via viewModel: XXX by viewModels, any changes etc made to that particular ViewModel will only be propagated in my current fragment.

Where as patientViewModel of type PatientViewModel is scoped to the activities context via patientViewModel: XXX by activityViewModels. This means that as long as both fragments belong to the same activity, and you get the ViewModel via ... by activityViewModels you should be able to observe any changes made to the ViewModel on a global scope (global meaning any fragment within the same activity where it was declared).

With all the above in mind if your viewModel is correctly scoped to your activity and in both fragments you retrieve the viewModel using the by activityViewModels and updating the value being observed via XXX.postValue(YYY) or XXX.value = YYY you should be able to observe any changes made to the ViewModel from anywhere within the same activity context.

Hope that makes sense, it's late here, and I saw this question just before I hit the sack!

The problem is that you are actually not sharing the ViewModel between the Fragment and the Dialog. To share instances of a ViewModel they must be retrieved from the same ViewModelStore.

The syntax you are using to retrieve the ViewModels seems to be from a third party framework. I feel like probably Koin.

If that is the case, note that in Koin, getViewModel retrieves the ViewModel from the Fragment's own ViewModelStore. So, you are retrieving the ViewModel in your DialogFragment from its own ViewModelStore. On the other hand, in your Fragment, you are retrieving it using getSharedViewModel, in which you can specify which ViewModelStore it should retrieve the ViewModel from. So you are retrieving the ViewModel from two different ViewModelStores, and so, getting two different ViewModel. Interacting with one of those does not affect the other, as they are not the same instance.

To solve it, you should retrieve the ViewModel in both your Fragment and DialogFragment from the same ViewModelStore. For example, you could use getSharedViewModel in both, maybe specifying the same ViewModelStore manually at each, or even, without even specifying, which Koin will default to their Activity's one.

You could also even just use getViewModel in your Fragment, then pass its own specific ViewModelStore to the DialogFragment, in which you could then use getSharedViewModel, specifying the passed Fragment's ViewModelStore.

Related