androidkotlinandroid-livedataandroid-mvvmandroid-livedata-transformations

Why each activity is getting update to the original live data instead of recent live data?


MeetingViewModel

fun observeAttendeesJoined(): LiveData<Array<AttendeeInfo>>? {
        return Repository.getAttendeesJoined()
    }

There is a singleton repository using kotlin's object declaration. Repository has a live data which is being observed by BaseActivity.

Repository

 fun getAttendeesJoined(): LiveData<Array<AttendeeInfo>>? {
        if (attendeesJoined == null) {
            attendeesJoined = MutableLiveData()
        }
        return attendeesJoined
    }

BaseActivity

private fun observeAttendeesJoined() {
        meetingViewModel.observeAttendeesJoined()?.observe(this) {
            Timber.d(" :$LOG_APP_NAME: BaseActivity: :setObservers: onAttendeesJoined: $it")
            lifecycleScope.launchWhenResumed {
                onAttendeesJoined(it)
            }
        }
    }

A foreground service changes the value of the corresponding mutable live data. BaseActivity receives the update and we are showing the snackbar. Now, when we are changing the activity, the same result is being triggered again even when it is not triggering by the foreground service.

For example, if we are in activity A (that extends the BaseActivity) and the foreground service changes the value of a total numbers of new attendees to 5, we are showing it in the activity A as 5 users have joined the meeting. User spends some time on the activity A. After some time, when the user moves to activity B (that is also extending BaseActivity), without any response from foreground service, the activity B receives the same last update that was received by activity A and hence, activity B also shows the snackbar that 5 users have joined the meeting and this pattern continues for all the activities.

MeetingViewModel

fun onAttendeesJoined(attendeeInfo: Array<AttendeeInfo>) {
        Timber.d(" :$LOG_APP_NAME: MeetingViewModel: :onAttendeesJoined: :size: ${attendeeInfo.size}")
        attendeeInfo.forEach {
            Timber.d(" :$LOG_APP_NAME: MeetingViewModel: :onAttendeesJoined: $attendeeInfo")
        }
        Repository.setAttendeesJoined(attendeeInfo)
    }

Service

 override fun onAttendeesJoined(attendeeInfo: Array<AttendeeInfo>) {
        attendeeInfo.forEach {
            Timber.d(" :$LOG_APP_NAME: ChimeService: :onAttendeesJoined: :id: ${it.attendeeId} :externalId: ${it.externalUserId} :attendeeInfo: $it :responseSize: ${attendeeInfo.size}")
        }
        meetingViewModel.onAttendeesJoined(attendeeInfo)
    }

Whenever the foreground service changes the corresponding mutable live data, new activity (activity B in our example) should get the update only if new data (5) gets changed because meetingViewModel.observeAttendeesJoined() returns new data only.

How to receive the unique update across the activities?

Instance of MeetingViewModel is different for each activity but repository data is a singleton (object declaration of kotlin), isn't it?

I tried to understand Transformations.map and switchMap but could not get any idea how to use it to solve the issue.

Thank you in anticipation.


Solution

  • This is how LiveData is designed to work. It's for getting the latest state of data, and not intended to be a message broadcaster. There are various hacks out there to try to make LiveData work for this purpose. You can do a web search for SingleLiveEvent to find various articles on the topic.

    You might consider using Kotlin's MutableSharedFlow for doing this instead of LiveData. It has a replay parameter that you can set to 0 (which is the default) so your new Activities won't receive updates that have already occurred. In my opinion, this is a much cleaner solution that SingleLiveEvent.

    Your repo can expose the property:

    val attendeesJoined = MutableSharedFlow<Array<AttendeeInfo>>()
    

    and your ViewModel can pass it through:

    val attendeesJoined: SharedFlow<Array<AttendeeInfo>> = Repository.getAttendeesJoined()
    

    You can post values to the MutableSharedFlow with tryEmit().

    Your Activities can collect (observe) the Flow on their lifecycleScopes, which will result in analagous behavior to observing a LiveData, because the lifeCycleScope will automatically cancel collection when the Activity is closed.

    private fun observeAttendeesJoined() {
        lifecycleScope.launchWhenResumed {
            meetingViewModel.attendeesJoined.collect {
                Timber.d(" :$LOG_APP_NAME: BaseActivity: :setObservers: onAttendeesJoined: $it")
                onAttendeesJoined(it)
            }
        }
    }
    

    Also, a suggestion. Arrays should rarely be used. It is cleaner/safer for a repository to expose read-only Lists. Arrays are mutable and fixed size, so they have a narrow set of uses, and are generally for low-level work.