Priyank Jain
Priyank Jain

Reputation: 93

Live data giving old value again while changing fragment

I am using live data from a shared ViewModel across multiple fragments. I have a sign-in fragment which takes user's phone number and password and then the user presses sign in button I am calling the API for that, now if the sign-in fails I am showing a toast "Sign In failed", now if the user goes to "ForgotPassword" screen which also uses the same view model as "SignInFragment" and presses back from the forgot password screen, it comes to sign-in fragment, but it again shows the toast "Sign In failed" but the API is not called, it gets data from the previously registered observer, so is there any way to fix this?

SignInFragment.kt

class SignInFragment : Fragment() {

    private lateinit var binding: FragmentSignInBinding

    //Shared view model across two fragments
    private val onBoardViewModel by activityViewModels<OnBoardViewModel>()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(
            inflater,
            R.layout.fragment_sign_in,
            container,
            false
        )
        return binding.root
    }

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

        onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
            //This is calling again after coming back from new fragment it.
            showToast("Sign In Failed")
        }
    }

    override fun onClick(v: View?) {
        when (v?.id!!) {
            R.id.forgotPasswordTV -> {
                findNavController().navigate(SignInFragmentDirections.actionSignInFragmentToForgotPasswordFragment())
            }
            R.id.signInTV -> {
                val phoneNumber = binding.phoneNumberET.text
                val password = binding.passwordET.text
                val signInRequestModel = SignInRequestModel(
                    phoneNumber.toString(),
                    password.toString(),
                    ""
                )

                //Calling API for the sign-in
                onBoardViewModel.callSignInAPI(signInRequestModel)
            }
        }
    }
}

ForgotPasswordFragment

class ForgotPasswordFragment : Fragment() {

    private lateinit var binding: FragmentForgotPasswordBinding

    //Shared view model across two fragments
    private val onBoardViewModel by activityViewModels<OnBoardViewModel>()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(
            inflater,
            R.layout.fragment_forgot_password,
            container,
            false
        )
        return binding.root
    }
}

OnBoardViewModel

class OnBoardViewModel : ViewModel() {

    private var repository: OnBoardRepository = OnBoardRepository.getInstance()

    private val signInRequestLiveData = MutableLiveData<SignInRequestModel>()

    //Observing this data in sign in fragment
    val signInResponse: LiveData<APIResource<SignInResponse>> =
        signInRequestLiveData.switchMap {
            repository.callSignInAPI(it)
        }

    //Calling this function from sign in fragment
    fun callSignInAPI(signInRequestModel: SignInRequestModel) {
        signInRequestLiveData.value = signInRequestModel
    }

    override fun onCleared() {
        super.onCleared()
        repository.clearRepo()
    }
}

I have tried to move this code inside onActivityCreated but it's still getting called after coming back from new fragment.

 onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
            showToast("Sign In Failed")
        }

Upvotes: 6

Views: 6659

Answers (3)

Darotudeen
Darotudeen

Reputation: 2508

For Kotlin users @Sergey answer can also be implemented using delegates like below

class SingleLiveEvent<T> : MutableLiveData<T>() {
    
    var curUser: Boolean by Delegates.vetoable(false) { property, oldValue, newValue ->
        newValue != oldValue
    }

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        super.observe(owner, Observer<T> { t ->
            if (curUser) {
                observer.onChanged(t)
                curUser = false
            }
        })
    }

    override fun setValue(t: T?) {
      
        curUser = true
        super.setValue(t)
    }

    fun call() {
        postValue(null)
    }

}

Upvotes: 0

Tenfour04
Tenfour04

Reputation: 93531

I would provide a way to reset your live data. Give it a nullable type. Your observers can ignore it when they get a null value. Call this function when you receive login data, so you also won't be repeating messages on a screen rotation.

class OnBoardViewModel : ViewModel() {

    // ...

    fun consumeSignInResponse() {
        signInRequestLiveData.value = null
    }

}
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
    if (response != null) {
        showToast("Sign In Failed")
        onBoardViewModel.consumeSignInResponse()
    }
}

Upvotes: 1

Sergio
Sergio

Reputation: 30595

Using SingleLiveEvent class instead of LiveData in OnBoardViewModel class will solve your problem:

val signInResponse: SingleLiveEvent <APIResource<SignInResponse>>.

class SingleLiveEvent<T> : MutableLiveData<T>() {

    private val pending = AtomicBoolean(false)

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        super.observe(owner, Observer<T> { t ->
            if (pending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        })
    }

    override fun setValue(t: T?) {
        pending.set(true)
        super.setValue(t)
    }

    fun call() {
        postValue(null)
    }

}

This is a lifecycle-aware observable that sends only new updates after subscription. This LiveData only calls the observable if there's an explicit call to setValue() or call().

Upvotes: 7

Related Questions