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).