0

Scenario

  1. List<Message> in a composable
  2. messages = mutableStateListOf<Message> in viewModel
  3. messages depends on two actions getAll or search
  4. In both cases we observe SQLite, but only one flow should be active at a single instant (user is either viewing all or is searching)
  5. Flow is used with SQLite because new messages can come from the network.

Questions

  1. How to switch between getAll and search flow
  2. When a new search happens the old search flow should be cancelled and a new one should start. How to do this?

Current implementation with two flows

@Composable
fun ListOfMessages() {
    val messages = viewModel.messages
}

// -----------------------------
// ViewModel
// -----------------------------
@ExperimentalCoroutinesApi
class MessageViewModel(application: Application) {
    val messages = mutableStateListOf<Message>()
    val isLoading = mutableStateOf(false)

    init {
        fetchMessages()
    }

    fun fetchMessages() {
        MessageUseCase(messagesDB).getAll().onEach { dataState ->
            isLoading.value = dataState.loading
            
            dataState.data?.let { data ->
                messages.clear()
                messages.addAll(data)
            }

            dataState.error?.let { error ->
                // UIState -> error message
            }
        }.launchIn(viewModelScope)
    }

    fun SearchWithInMessage(q: String) {
        MessageUseCase(messagesDB).search(q).onEach { dataState ->
            isLoading.value = dataState.loading
            
            dataState.data?.let { data ->
                messages.clear()
                messages.addAll(data)
            }

            dataState.error?.let { error ->
                // UIState -> error message
            }
        }
    }
}

// -----------------------------
// Use Cases
// -----------------------------
class MessageUseCase(messagesDB: messageDao) {
    @ExperimentalCoroutinesApi
    fun getAll(): Flow<DataState<List<Message>>> = channelFlow { {
        send(DataState.loading())

        try {
            fetchAndSaveLatestMessagesFromRemote()
            val messages = messagesDB.getAllStream()
            messages.collectLatest { list ->
                // business logic
                send(DataState.success(list))
            }
        } catch (e: Exception){
            send(DataState.error<List<Message>>(e.message?: "Unknown Error"))
        }
    }

    @ExperimentalCoroutinesApi
    fun search(q: String): Flow<DataState<List<Message>>> = channelFlow { {
        send(DataState.loading())

        try {
            fetchAndSaveSearchedMessageFromRemote(q)
            val messages = messagesDB.searchStream(q)
            messages.collectLatest { list ->
                // business logic
                send(DataState.success(list))
            }
        } catch (e: Exception){
            send(DataState.error<List<Message>>(e.message?: "Unknown Error"))
        }
    }
}

// -----------------------------
// DB and Network calls
// -----------------------------
clamentjohn
  • 3,417
  • 2
  • 18
  • 42
  • For the second part, you need not worry about cancelling flow. If you stop collecting from a (cold) flow, it consumes no resources. For your first question, add some code in the question showing how the `getAll` and `search` flows are being generated. – Arpit Shukla Nov 06 '21 at 11:42
  • @ArpitShukla Added some bare minimum code – clamentjohn Nov 06 '21 at 12:00
  • Btw, if you search for an empty string, does it give a different result than fetching all data? Because if it doesn't you can convert your `messagesDB.getAllStream()` to `messagesDB.searchStream("")`, which will simplify the process. – Arpit Shukla Nov 06 '21 at 12:54

1 Answers1

2

You can use flatMapLatest(). It receives a flow of flows and always replicate items of the latest flow sent to it, cancelling previous flow. As a result, it works as the flow that can switch between other flows.

Depending on how your getAll() and search() are declared, it could be something like this:

private val state = MutableStateFlow<String?>(null)
private val messages = state.flatMapLatest {
    if (it == null) getAll() else search(it)
}

suspend fun requestSearch(search: String) = state.emit(search)
suspend fun requestAll() = state.emit(null)

fun getAll(): Flow<List<Message>> = TODO()
fun search(search: String): Flow<List<Message>> = TODO()

state represents our currently required search state. Whenever we emit to it, it requests either getAll() or search() flow and replicates it as messages flow.

Then, to use it with Compose, you need to convert it to State:

messages.collectAsState(emptyList())
broot
  • 21,588
  • 3
  • 30
  • 35
  • Kotlin Flow APIs are so hard to be searched on the internet. How did you learn these? By facing a need and exploring or reading the docs even before a need occured. – clamentjohn Nov 06 '21 at 12:12
  • 1
    I don't really remember, but once I went through all functions in the left sidebar on the website linked by me above - just for curiosity. It seems like a lot of stuff, but it could be done in an hour or so and then we have at least an insight on what is possible. Also, `flatMapLatest()` is a very specific function, it is very often compared to `switchMap` operator of RxJava. – broot Nov 06 '21 at 12:50
  • @clmno `flatMap` is a pretty classic name for this kind of operation on collections/sequences/streams in general, only the details of cancellation are particular to `Flow` here. So in my case the first time I needed it I just tried `flatMap` and read the docs of whatever function name was auto-completed here :D But I also believe I did go through the whole list once, just out of curiosity like broot – Joffrey Nov 06 '21 at 18:00