Jetpack Compose Load Data (Initial on Show) using Flow or Coroutines

December 14, 2021

Assuming you need to load data when a screen is shown. There ara a few ways

  • Assign value to viewModel variable on init via flow
  • Write a function to load data, and call the function in LaunchedEffect

I usually use a suspend function or flow (using collectAsState()).

This is the data class to be used for data loading from firestore db.

data class Card() {
    id: String? = null,
    name: String? = null
} {
    companion object {
        fun toObject(doc: DocumentSnapshot): Card? {
            val item = doc.toObject<Card>()
            item?.id = doc.id
            return item
        }
    }
}

ViewModel

Option 1: initialize variable using flow

class CardViewModel : ViewModel() {
    private val db = Firebase.firestore

    val itemsFlow = flow {
        val docs = db.collection("card").get().await()
        val items = docs.map { doc ->
            Card.toObject(doc)
        }
        emit(items)
    }
}
fun CardScreen(viewModel: CardViewModel = viewModel()) {
    val items by viewModel.itemsFlow.collectAsState(initial = emptyList())
}

Option 2: using init

You can also store items in ViewModel and fetch data via init.

class CardViewModel : ViewModel() {
    private val db = Firebase.firestore

    val itemsFlow = flow {
        val docs = db.collection("card").get().await()
        val items = docs.map { doc ->
            Card.toObject(doc)
        }
        emit(items)
    }

    var items: List<Card> = emptyList()

    init {
        viewModelScope.launch {
            itemsFlow.collect { it
                items = it
            }
        }
    }

}
fun CardScreen(viewModel: CardViewModel = viewModel()) {
    val items = viewModel.items
}

If the items could mutate (value change and display on ui) during the lifetime in this screen, you need to use mutableStateOf (overwrite entire list) or mutableStateListOf (modify individual item in list).

class CardViewModel : ViewModel() {
    var items by mutableStateOf<List<Card>>(emptyList())

    viewModelScope.launch {
        itemsFlow.collect { it
            items = it
        }
    }
}
class CardViewModel : ViewModel() {
    var items = mutableStateListOf<Card>()

    viewModelScope.launch {
        itemsFlow.collect { it
            items.addAll(it)
        }
    }
}

You can also use suspend function instead of flow in init.

class CardViewModel : ViewModel() {
    suspend fun getItems(): List<Card> {
        val docs = db.collection("card").get().await()
        val items = docs.map { doc ->
            Card.toObject(doc)!!
        }
        return items
    }

    var items: List<Card> = emptyList()

    init {
        viewModelScope.launch {
            items = getItems()
        }
    }
}

Or just plain init.

class CardViewModel : ViewModel() {
    private val db = Firebase.firestore

    var card by mutableStateOf(Card())

    init {
        card = Card(id ="new", name = "New Card")
    }
}

init based on parameter

Assuming we need to fetch a card by id (pass in arguments). I am using Compose Navigation, and the parameter could be passed in to SavedStateHandle in viewModel.

class CardViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    private val cardId = savedStateHandle.get<String>("cardId")

    val itemFlow = flow {
        val doc = db.collection("card").document(cardId).get().await()
        if (doc.exists()) {
            val item = Card.toObject(doc)
            emit(item)
        }
    }
}

Compose

As shown in the example above, you can create a flow and use collectAsState to access the data.

class CardViewModel : ViewModel() {
    private val db = Firebase.firestore

    val itemsFlow = flow {
        val docs = db.collection("card").get().await()
        val items = docs.map { doc ->
            Card.toObject(doc)
        }
        emit(items)
    }
}
fun CardScreen(viewModel: CardViewModel = viewModel()) {
    val items by viewModel.itemsFlow.collectAsState(initial = emptyList())
}

Notice that I use viewModel.itemsFlow rather than a function like viewModel.getItemsFlow(). If you use a function (or function which accept a parameter), the flow will be recreated everytime there is a value changes in compose, which might cause infinite compose loop.

If you need to use a function, you need to use LaunchedEffect to only execute once, or until a key parameter change.

class CardViewModel : ViewModel() {
    private val db = Firebase.firestore

    fun getItem(id: String) = flow {
        val doc = db.collection("card").document(id).get().await()
        val item = if (doc.exists()) {
            Card.toObject(doc)!!
        }
        else null
        emit(item)
    }
}
fun CardScreen(viewModel: CardViewModel = viewModel(), cardId: String) {
    var item by remember { mutableStateOf<Card?>(null) }

    LaunchedEffect(cardId) { // only launch once whenever cardId change
        viewModel.getItem(cardId).collect { 
            item = it
        }
    }
}

You can also use suspend function instead of flow.

class CardViewModel : ViewModel() {
    private val db = Firebase.firestore

    suspend fun getItem(id: String): Card? {
        val doc = db.collection("card").document(id).get().await()
        return if (doc.exists()) {
            Card.toObject(doc)!!
        }
        else null
    }
}
fun CardScreen(viewModel: CardViewModel = viewModel(), cardId: String) {

    // val coroutineScope = rememberCoroutineScope()
    var item by remember { mutableStateOf<Card?>(null) }

    LaunchedEffect(cardId) {
        // coroutineScope.launch {
        item = viewModel.getItem(cardId)
        // }
    }
}

Conclusion

You can perform the data fetch in ViewModel via variable initialization or constructor init. You can access arguments via SavedStateHandle. If the data is mutable (reflect value change on ui compose) during the lifetime of the screen, you need to use mutableStateOf or mutableStateListOf.

You can also perform data fetch in Composable, using collectAsState or LaunchedEffect. If you use collectAsState, don’t use it to collect state from function as it will cause infinite compose loop. If you use LaunchedEffect and the data is mutable, use remember with mutableStateOf or mutableStateListOf. If the value need to survive screen destruction (configuration change), use rememberSaveable. ViewModel doesn’t require remmeber or rememberSaveable and it can survice screen destruction (configuration change).

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.