Search code examples
androidkotlin-coroutinesandroid-viewmodelkotlin-flowandroid-mvvm

How to get value from ViewModel viewModelScope.launch without observing it in Activity?


I am in the process to re-build my live app to use MVVM and Coroutines and moving most of the code that is not UI related from Activity/Fragment to ViewModel.

Currently, my app uses Java language for Activities and Fragment and Kotlin for everything new. The app minimum Sdk Version is 19.

In some cases, I implemented LiveData and Live Data Observer in my app, however most of the time I need to get the current value from ViewModel inside viewModelScope.launch without using the Observer in the Activity/Fragment.

For Demonstration purposes, I created a small app that shows what I tries so far. I am using a log (Timber.d) to show the result in Activity. Unfortunately, the result shows the default values instead of the new values that I expected (10 and 30).

My goal is to get the current value (10 and 30) in my Activity without using Observer.

ViewModel - Kotlin

class MainActivityViewModel(
    private val mainActivityRepository: MainActivityRepository = MainActivityRepositoryImpl(),
    private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
    private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main
) : ViewModel() {
...
  private val _flowWithReturnAndDispatcher = MutableStateFlow<Int?>(-1)
    val flowWithReturnAndDispatcher = _flowWithReturnAndDispatcher.asStateFlow()
    private suspend fun getFlowWithReturnAndDispatcher():Flow<Int>{
        return flow {
            val data = 10
            emit(data)
        }.flowOn(ioDispatcher)
    }


    private val _mFlowValue = MutableStateFlow<Int?>(-1)
    val mFlowValue = _mFlowValue.asStateFlow()
    private val flowValue: Flow<Int> = flow {
        val data = 30
            emit(data)
    }


fun viewModelScopeCollectFlow() {
        viewModelScope.launch {
            val mFlowWithReturnAndDispatcher =             getFlowWithReturnAndDispatcher()
            val mFlowValue = flowValue

            mFlowWithReturnAndDispatcher.collect { value ->
                _flowWithReturnAndDispatcher.value = value
            }

            flowValue.collect { value ->
                _mFlowValue.value = value
            }

        }
    }
...
}

Activity - Java

public class MainActivity extends AppCompatActivity {
...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
...
    viewModel.viewModelScopeCollectFlow();

  Timber.d("onCreate-> flowWithReturnAndDispatcher: " +
                viewModel.getFlowWithReturnAndDispatcher().getValue()  + "" +
                "\nflowValue: " + viewModel.getMFlowValue().getValue());
...
}
...
}

Solution

  • In your Activity, the problem is that you're trying to get the values instead of observing them. Rarely (or never) should you use the value of a StateFlow outside the ViewModel. Flows are intended for collecting. When you try to immediately get the value instead of observing, you're getting the value before they have had a chance to be updated.

    Since your Activity is in Java, you should convert your flows into LiveData and observe it in the Activity instead of using getValue().

    This:

        private suspend fun getFlowWithReturnAndDispatcher():Flow<Int>{
            return flow {
                val data = 10
                emit(data)
            }.flowOn(ioDispatcher)
        }
    

    is wrong in a few ways. First, you don't need to suspend to create a Flow. Second, it's against Kotlin convention to create a getter function instead of using a property. Third, there's no need to specify a dispatcher if you aren't calling blocking code or code that requires the main thread (like using liveData.value =).

    Your viewModelScopeCollectFlow() function is convoluted design. You shouldn't have to manually tell the ViewModel to start preparing its own stuff. I would recommend changing it to an init block, but even that is really convoluted. You could just be using stateIn to greatly simplify your code.

    class MainActivityViewModel(
        private val mainActivityRepository: MainActivityRepository = MainActivityRepositoryImpl(),
        private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
        private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
        private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main
    ) : ViewModel() {
    ...
    
    
        private val baseFlowWithReturnAndDispatcher: Flow<Int> {
            return flow {
                val data = 10
                emit(data)
            }
        }
        val flowWithReturnAndDispatcher = baseFlowWithReturnAndDispatcher
            .asLiveData()
    
        private val baseFlowValue: Flow<Int> = flow {
            val data = 30
                emit(data)
        }
        val flowValue = baseFlowValue
            .asLiveData()
    }
    

    But since you're collecting from a Java class, you should use LiveData instead of StateFlow:


    Side note, you keep naming things "mSomething" incorrectly. "m" stands for "member" in Hungarian notation (which almost no one uses any more because it's generally considered bad for readability and unnecessary in modern IDEs). "Member" means not a local variable, but a variable that is defined for the whole class scope. If we were to use it in Kotlin, it would only be sensible to use for properties inside classes.