0

I have an activityUiState (of type ActivityUiState) object which has a list of sub-activities (of type ActivityUiState as well) and a list of voice notes. I'm trying to populate the activityUiState with the list of sub-activities and voice notes using flows. Each of the sub-activities might have its own voice notes saved in the database that I would like to include in the sub-activity objects when I'm creating the sub-activity list. The code so far adds sub-activities and voice notes to the activityUiState successfully. However, when I try to add the voice notes of each of the sub-activities, the whole activityUiState object is not updated with any of the data. What am I doing wrong here?

var subActivitiesUiState = emptyList<ActivityUiState>()
activitiesRepository.getSubActivitiesPerMainActivity(id).collect { subActivities ->
    for (subActivity in subActivities) {
        var voiceNotesUiState = emptyList<VoiceNoteUiState>()
        // This code block results in an empty activityUiState being returned
        /*
        voiceNotesRepository.getAllVoiceNotesPerActivity(subActivity.id).collect { voiceNotes ->
            for (voiceNote in voiceNotes) {
                // Append the mapped voice note to the voice notes list
                voiceNotesUiState = voiceNotesUiState + VoiceNoteUiState(
                    id = voiceNote.id,
                    uri = voiceNote.uri,
                    duration = voiceNote.duration,
                    timestamp = voiceNote.timestamp,
                    activityId = voiceNote.activityId
                )
            }

            subActivitiesUiState = subActivitiesUiState + ActivityUiState(
                id = subActivity.id,
                title = subActivity.title,
                note = subActivity.note,
                startTime = subActivity.startTime,
                endTime = subActivity.endTime,
                voiceNotesUiState = voiceNotesUiState,
                mainActivityId = subActivity.mainActivityId
            )
        }
        */

        subActivitiesUiState = subActivitiesUiState + ActivityUiState(
            id = subActivity.id,
            title = subActivity.title,
            note = subActivity.note,
            startTime = subActivity.startTime,
            endTime = subActivity.endTime,
            voiceNotesUiState = voiceNotesUiState,//TODO: Add the voice notes of the sub-activity
            mainActivityId = subActivity.mainActivityId
        )
    }

    activityUiState.update {
        ActivityUiState(
            id = activity.id,
            title = activity.title,
            note = activity.note,
            startTime = activity.startTime,
            endTime = activity.endTime,
            subActivitiesUiState = subActivitiesUiState
        )
    }

    var voiceNotesUiState = emptyList<VoiceNoteUiState>()
    voiceNotesRepository.getAllVoiceNotesPerActivity(id).collect { voiceNotes ->
        for (voiceNote in voiceNotes) {
            // Append the mapped voice note to the voice notes list
            voiceNotesUiState = voiceNotesUiState + VoiceNoteUiState(
                id = voiceNote.id,
                uri = voiceNote.uri,
                duration = voiceNote.duration,
                timestamp = voiceNote.timestamp,
                activityId = voiceNote.activityId
            )
        }

        activityUiState.update { it ->
            it.copy(
                voiceNotesUiState = voiceNotesUiState
            )
        }
    }
}

UPDATE 25.11.2025

Based on tyg's suggestion I changed my code to the following:

val selectedActivityId = MutableStateFlow(savedStateHandle.get(ACTIVITY_ID_SAVED_STATE_KEY))

val activityUiState = MutableStateFlow(ActivityUiState())
selectedActivityId.flatMapLatest { activityId ->
    @OptIn(ExperimentalCoroutinesApi::class)
    val activityState = selectedActivityId.flatMapLatest { activityId ->
        if (activityId == null) return@flatMapLatest flowOf(ActivityUiState())

        val flowOfActivity = activitiesRepository.getActivityStream(activityId)

        val flowOfSubActivities = activitiesRepository.getSubActivitiesPerMainActivity(activityId).flatMapLatest { subActivities ->
            val subActivitiesUiStates = subActivities.map { subActivity ->
                    voiceNotesRepository.getAllVoiceNotesPerActivity(subActivity.id).map { voiceNotes ->
                ActivityUiState(
                    date = subActivity.date,
                    id = subActivity.id,
                    title = subActivity.title,
                    note = subActivity.note,
                    startTime = subActivity.startTime,
                    endTime = subActivity.endTime,
                    voiceNotesUiState = voiceNotes.map { voiceNote ->
                        VoiceNoteUiState(
                            id = voiceNote.id,
                            uri = voiceNote.uri,
                            duration = voiceNote.duration,
                            timestamp = voiceNote.timestamp,
                            activityId = subActivity.id
                        )
                    },
                    mainActivityId = subActivity.mainActivityId
                )
            }
        }
        combine(subActivitiesUiStates) { it -> it.toList() }
    }

    combine(
       flowOfActivity,
       flowOfSubActivities,
       voiceNotesRepository.getAllVoiceNotesPerActivity(activityId)
    ) { activity, subActivitiesUiStates, voiceNotes ->
        activity?.let {
            activityUiState.update {
                it.copy(
                    date = activity.date,
                    id = activity.id,
                    title = activity.title,
                    note = activity.note,
                    startTime = activity.startTime,
                    endTime = activity.endTime,
                    voiceNotesUiState = voiceNotes.map{ voiceNote ->
                        VoiceNoteUiState(
                            id = voiceNote.id,
                            uri = voiceNote.uri,
                            duration = voiceNote.duration,
                            timestamp = voiceNote.timestamp,
                            activityId = activity.id
                        )
                    },
                    subActivitiesUiState = subActivitiesUiStates
                )
            }

            ActivityUiState(
                date = activity.date,
                id = activity.id,
                title = activity.title,
                note = activity.note,
                startTime = activity.startTime,
                endTime = activity.endTime,
                voiceNotesUiState = voiceNotes.map{ voiceNote ->
                    VoiceNoteUiState(
                        id = voiceNote.id,
                        uri = voiceNote.uri,
                        duration = voiceNote.duration,
                        timestamp = voiceNote.timestamp,
                        activityId = activity.id
                    )
                },
                subActivitiesUiState = subActivitiesUiStates
            )
        } ?:run {
            activityUiState.update {
                ActivityUiState()
            }

            ActivityUiState()
        }
    }
}
.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000L),
    initialValue = ActivityUiState()
)
fun selectActivity(id: UUID?) {
    selectedActivityId.value = id
}

I need the activityUiState to be of type MutableState, so I'm using the activityState as a Flow. The voice notes of subsequent sub-activities are loaded now, thank you

Could you also help me with this little problem: the activityState and activityUiState are not always updated when the selectedActivityId's value changes.

1 Answer 1

0

Problem analysis

Assuming the commented-out code is commented in again, then you would call collect in the middle of your code. collect suspends until the entire Flow is finished (in contrast to first, which only collects the first value and then continues).

A Flow finished when it emitted its last value. The Flow in question seems to be from a database, emitting new values whenever anything is changed in the database. There won't be a last value for such kind of Flows (because the Flow monitors the database for changes indefinitely), and in consequence collect will never return, it suspends indefinitely. You call it in a for loop over all subActivities, but only the first sub activity will ever be collected, and nothing that comes after is ever reached - like the other sub activities or activityUiState.update after the for loop.

You should use an entirely different approach to handle the Flows instead.

You do not want to collect the Flows here at all, you should only transform them. collect is a Terminal flow operator that actually starts the whole Flow machinery. That should be done at the latest possible moment. In Android apps that's usually the UI. When using Jetpack Compose use collectAsStateWithLifecycle in your composable. Everything up until then should only transform the Flow using Intermediate flow operators like map, flatMapLatest and combine. They don't start the collection, they only describe how the Flow's content should be transformed when a new value is emitted.

Your code is quite hard to understand, so let's try to break it down to see how it should look like instead. You left out a lot of context, so I had to make these assumptions:

  • activityUiState is a property defined as MutableStateFlow(ActivityUiState())

  • Your code is placed inside a function declared like this:

    suspend fun selectActivity(id: Int)
    
  • getSubActivitiesPerMainActivity() takes an Int and returns a Flow<List<SubActivity>>

  • getAllVoiceNotesPerActivity() takes an Int and returns a Flow<List<VoiceNote>>

  • The activity variable is defined somewhere else, out of scope of the current question.

These assumptions are probably not off by much. Adjust as needed.

Solution

Now, first you should extract some logic into dedicated functions to make the code easier to follow:

fun ActivityUiState(
    subActivity: SubActivity,
    voiceNotesUiState: List<VoiceNoteUiState>,
    subActivitiesUiState: List<ActivityUiState> = emptyList(),
): ActivityUiState = ActivityUiState(
    id = subActivity.id,
    title = subActivity.title,
    note = subActivity.note,
    startTime = subActivity.startTime,
    endTime = subActivity.endTime,
    subActivitiesUiState = subActivitiesUiState,
    voiceNotesUiState = voiceNotesUiState,
    mainActivityId = subActivity.mainActivityId,
)

fun VoiceNoteUiState(voiceNote: VoiceNote): VoiceNoteUiState = VoiceNoteUiState(
    id = voiceNote.id,
    uri = voiceNote.uri,
    duration = voiceNote.duration,
    timestamp = voiceNote.timestamp,
    activityId = voiceNote.activityId,
)

These are so called constructor-like functions. They look like they are a constructor of ActivityUiState and VoiceNoteUiState respectively, but they are just simple functions.

You eventually want to move the entire Flow logic into a property and base all transformations on the only thing that is variable: The id of the activity that was selected. For that you need to introduce that as a Flow itself:

private val selectedId = MutableStateFlow<Int?>(null)

fun selectActivity(id: Int) {
    selectedId.value = id
}

The only thing selectActivity now has to do is setting this Flow's value. Then you can replace the activityUiState property with this:

val activityUiState: Flow<ActivityUiState?> = selectedId.flatMapLatest { mainActivityId ->
    if (mainActivityId == null) return@flatMapLatest flowOf(null)

    val flowOfSubActivityUiStates = activitiesRepository.getSubActivitiesPerMainActivity(mainActivityId)
        .flatMapLatest { subActivities ->
            val subActivityUiStates = subActivities.map { subActivity ->
                voiceNotesRepository.getAllVoiceNotesPerActivity(subActivity.id)
                    .map { voiceNotes ->
                        ActivityUiState(subActivity, voiceNotes.map(::VoiceNoteUiState))
                    }
            }
            combine(subActivityUiStates) { it.toList() }
        }

    combine(
        flowOfSubActivityUiStates,
        voiceNotesRepository.getAllVoiceNotesPerActivity(mainActivityId),
    ) { subActivitiesUiState, voiceNotes ->
        ActivityUiState(
            subActivity = activity,
            subActivitiesUiState = subActivitiesUiState,
            voiceNotesUiState = voiceNotes.map(::VoiceNoteUiState),
        )
    }
}

Explanation

selectedId.flatMapLatest replaces the Flow of the selected id with another Flow. Since the id can be null (i.e. nothing was selected), the trivial case is handled first: When the id is null, then the transformed Flow will also only contain null.

Otherwise an ActivityUiState is created. First, the list of sub activities is assembled. getSubActivitiesPerMainActivity's content is also transformed using flatMapLatest, because each sub activity has its own Flow of VoiceNoteUiState. Please note here that instead of using a for loop, the List's map function is used to make the code more concise.

The result subActivityUiStates is of type List<Flow<ActivityUiState>> and is converted into a Flow<List<ActivityUiState>> by using combine. This is now a single Flow that contains a list of all sub activities' ActivityUiState, where each contains a List<VoiceNoteUiState>.

To assemble the main activity's ActivityUiState combine is used again to combine the flowOfSubActivityUiStates with the Flow of voice notes. When the ominous activity variable (see the last assumption in the list above) also comes from a Flow you can add that Flow as a third parameter to combine. Otherwise just use it as-is to create the final ActivityUiState that the activityUiState property will now contain.

If you need it to be a StateFlow, simply add .stateIn() at the end.

The resulting Flow will now always emit a new value when anything relevant in the database changes. It will always reflect the current state of the database for the activity with the given id.

Final thoughts

This data flow looks quite complex. And although not explicitly mentioned, it looks a bit like the activity hierarchy could even be recursive.

I think a better approach would be move the entire logic to the database. There you could achieve all of this with simple JOINs. You would only have to handle one or two Flows in the end, instead of having to untangle this whole mess of Flows afterwards.

Sign up to request clarification or add additional context in comments.

3 Comments

Thank you for your answer, @tyg. I really love the idea of writing a sql query that would combine the activity, voice notes, sub-activities (and their voice notes) into a single return object - I'm having trouble seeing how to write such a query, though. I mean, wouldn't that require squeezing in a list into a single row cell?
There are multiple ways to do that, and it depends on the database structure. You can ask that as a new question if you want. This question is about how the Flows can be untangled.
If this answer helped you solve the issue, you can tick the checkmark next to it to accept it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.