Search code examples
androidkotlinscopeviewmodelcoroutine

What is an efficient way of returning values in viewmodel scopes / function scopes in kotlin


I have a QuestDetailScreen in which I call getDailyQById(id), which passes to a viewmodel -> repo -> dao -> database. I use LiveData in the viewmodel for the DailyQuest vals. To not have the database call be on the main thread, I surrounded the repo call in a viewModelScope. But when this returns to the view in QuestDetailScreen it also has to be in a scope for me to access it.

I would like to access the DailyQuest that has been returned to be accessible outside of this scope.

QuestDetailScreen:

@Destination
@Composable
fun QuestDetailScreen(
    query: Long
) {
    val viewModel = hiltViewModel<QuestVM>()
    val dailyQuest by viewModel.dailyQues.observeAsState()
    var quest: DailyQuest? = null
    var description = " "
    AppChoreQuestTheme {
        Column{
            TopBar(screen = "Quest Details")
            Spacer(Modifier.height(8.dp))

            LaunchedEffect(query){
                viewModel.getDailyQById(query)
            }

            dailyQuest?.let { q ->
                quest = q
            }

            if(quest != null){
                println(" Name: " + quest!!.name)
                println(" Description: " + quest!!.description)
            }
        }
    }
}

QuestVM:

@HiltViewModel
class QuestVM @Inject constructor(
    private val dailyRepo: DailyQuestRepo,
    private val montlyRepo: MonthlyQuestRepo,
    private val yearlyRepo: YearlyQuestRepo
): ViewModel(){
    **private val _dailyQuest = MutableLiveData<DailyQuest?>(null)
    val dailyQues: LiveData<DailyQuest?> get() = _dailyQuest**

    private val _questType = MutableStateFlow(QuestType.DAILY_QUEST)
    private val type = _questType.value.toString()
    private val _quests = _questType
        .flatMapLatest { questType ->
            when(questType) {
                QuestType.DAILY_QUEST -> dailyRepo.getAllDailyQuests()
                QuestType.MONTHLY_QUEST -> montlyRepo.getAllMonthlyQuests()
                QuestType.YEARLY_QUEST -> yearlyRepo.getAllYearlyQuests()
            }
        }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
    private val _state = MutableStateFlow(QuestState())
    val state = combine(_state, _questType, _quests) { state, questType, quests ->
        state.copy(
            quests = quests,
            questType = questType
        )
    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), QuestState())


    **fun getDailyQById(id: Long) {
        println(" Help ")
        viewModelScope.launch(Dispatchers.IO) {
            _dailyQuest.postValue(dailyRepo.getQuestById(id))
                val quest = dailyRepo.getQuestById(id)
        }
    }**

At first I just made a bunch of variables for the different parameters of the DailyQuest object, and then assigned the returned values to them in the let-scope.

Then I tried what is in the code above, creating a DailyQuest val outside the scope, and then basically copying it, but then when I use it in the if(quest != null){ I have to put 2 ' !!' behind the quest val, which I don't know is best practice.

So basically, it works but I would like to know what is most efficient way to return a whole object from the database based on an id.


Solution

  • In your Composable, you don’t need that quest variable at all. Just use your dailyQuest variable. It is an observed LiveData of a nullable, so it will switch from null to not-null when the value has been retrieved. Your Composable function will be called again when the value arrives. Remember that Composables are functions that are called over again each time state changes. It doesn’t make sense to create a var in your Composable for this purpose. It rarely if ever makes sense to use a var inside a Composable unless it's for a remembered and delegated MutableState, e.g. var x by remember { mutableStateOf("something") }.

    And this is not critical, but I would also move the LaunchedEffect to where its relevant (next to where the live data is being observed) for code organization purposes and make it less likely you introduce a bug later. For example, if the Column exited the composable but you were using dailyQuest somewhere else in this screen, you'd wonder why the value never arrives.

    @Destination
    @Composable
    fun QuestDetailScreen(
        query: Long
    ) {
        val viewModel = hiltViewModel<QuestVM>()
        LaunchedEffect(query){
            viewModel.getDailyQById(query)
        }
        val dailyQuest by viewModel.dailyQues.observeAsState()
        AppChoreQuestTheme {
            Column{
                TopBar(screen = "Quest Details")
                Spacer(Modifier.height(8.dp))
    
                if (dailyQuest != null) {
                    println(" Name: " + dailyQuest.name)
                    println(" Description: " + dailyQuest.description)
                } else {
                    // It's not finished loading yet. Could show progress 
                    // indicator here.
                }
            }
        }
    }