I have view model like this:
class SimpleViewModel : ViewModel() {
private val _state = MutableStateFlow(false)
val state: StateFlow<Boolean> = _state
}
How can I collect this state's values and call methods from another class like this:
class AnotherClass {
fun doWhenViewModelStateUpdateToTrue()
fun doWhenViewModelStateUpdateToFalse()
}
Your other class needs a reference to the state flow and to a CoroutineScope to run the collection in.
The CoroutineScope should have a lifecycle matching that of this class. So if it's a class you create in an Activity, for example, you would pass lifecycleScope.
class AnotherClass(
private val coroutineScope: CoroutineScope,
private val flowToCollect: Flow<Boolean>
) {
init {
coroutineScope.launch {
flowToCollect.collect {
if (it) doWhenViewModelStateUpdateToTrue()
else doWhenViewModelStateUpdateToFalse()
}
}
}
fun doWhenViewModelStateUpdateToTrue() {
//...
}
fun doWhenViewModelStateUpdateToFalse() {
//...
}
}
Related
I run Code A, and get the error Result A. It seems that isRecordingState has't been initialized.
So I modify Code A as Code B, and Code B can run correctly.
In my mind, I can place different functions in any order in a class of Kotlin.
I think init{ } of a Class will be launched after the object has been initialized, so I think I can place init{ } in any place of a class.
What's wrong with my ideas?
Code A
#HiltViewModel
class SoundViewModel #Inject constructor(
#ApplicationContext private val appContext: Context,
...
): ViewModel() {
init { beginSoundDensity() }
private val _timeXState = MutableStateFlow(0)
var isRecordingState by mutableStateOf(false)
private set
private var myJob: Job?=null
fun beginSoundDensity() {
if (isRecordingState == false) {
isRecordingState = true
myJob?.cancel()
...
}
}
}
Result A
java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object androidx.compose.runtime.State.getValue()' on a null object reference
at info.dodata.soundmeter.presentation.viewmodel.SoundViewModel.isRecordingState(SoundViewModel.kt:245)
at info.dodata.soundmeter.presentation.viewmodel.SoundViewModel.beginSoundDensity(SoundViewModel.kt:81)
at info.dodata.soundmeter.presentation.viewmodel.SoundViewModel.<init>(SoundViewModel.kt:39)
Code B
#HiltViewModel
class SoundViewModel #Inject constructor(
#ApplicationContext private val appContext: Context,
...
): ViewModel() {
private val _timeXState = MutableStateFlow(0)
var isRecordingState by mutableStateOf(false)
private set
private var myJob: Job?=null
init { beginSoundDensity() }
fun beginSoundDensity() {
if (isRecordingState == false) {
isRecordingState = true
myJob?.cancel()
...
}
}
}
The code just runs from top to bottom, so this code for example prints "12345"
fun main() {
A()
}
class A {
init {
print("1")
}
val s2 = printAndReturn("2")
init {
print("3")
}
val s4 = printAndReturn("4")
init {
print("5")
}
private fun printAndReturn(s: String): String {
print(s)
return s
}
}
I am discovering Kotlin and android app dev. I fail to get data from my room database (because of Cannot access database on the main thread). So I try with lifecyclescope.
The concerned code, in Fragment onViewCreated function, is :
lifecycleScope.launch {
withContext(Dispatchers.Default) {
val accountConfiguration = viewModel.get();
println("{${accountConfiguration}}")
}
}
The called function (in viewModel) is :
fun get() = viewModelScope.launch {
repository.get()
}
There is the "full" code (simplified), Entity & DAO :
#Entity
data class AccountConfiguration(
#PrimaryKey val server_address: String,
#ColumnInfo(name = "user_name") val user_name: String,
// [...]
)
#Dao
interface AccountConfigurationDao {
#Query("SELECT * FROM accountconfiguration LIMIT 1")
fun flow(): Flow<AccountConfiguration?>
#Query("SELECT * FROM accountconfiguration LIMIT 1")
suspend fun get(): AccountConfiguration?
// [...]
}
Repository :
package fr.bux.rollingdashboard
import androidx.annotation.WorkerThread
import kotlinx.coroutines.flow.Flow
class AccountConfigurationRepository(private val accountConfigurationDao: AccountConfigurationDao) {
val accountConfiguration: Flow<AccountConfiguration?> = accountConfigurationDao.flow()
// [...]
#Suppress("RedundantSuspendModifier")
#WorkerThread
suspend fun get() : AccountConfiguration? {
return accountConfigurationDao.get()
}
}
ViewModel & Factory :
class AccountConfigurationViewModel(private val repository: AccountConfigurationRepository) : ViewModel() {
val accountConfiguration: LiveData<AccountConfiguration?> = repository.accountConfiguration.asLiveData()
// [...]
fun get() = viewModelScope.launch {
repository.get()
}
// [...]
}
class AccountConfigurationViewModelFactory(private val repository: AccountConfigurationRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(AccountConfigurationViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return AccountConfigurationViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Fragment :
class AccountConfigurationFragment : Fragment() {
private var _binding: AccountConfigurationFragmentBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private val viewModel: AccountConfigurationViewModel by activityViewModels {
AccountConfigurationViewModelFactory(
(activity?.application as RollingDashboardApplication).account_configuration_repository
)
}
lateinit var accountConfiguration: AccountConfiguration
// [...]
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonGoBackMain.setOnClickListener {
findNavController().navigate(R.id.action_AccountConfigurationFragment_to_DashboardFragment)
}
lifecycleScope.launch {
withContext(Dispatchers.Default) {
val accountConfiguration = viewModel.get();
println("{${accountConfiguration}}")
}
}
binding.buttonSave.setOnClickListener {
save()
}
}
// [...]
}
In your current code,
lifecycleScope.launch {
withContext(Dispatchers.Default) {
val accountConfiguration = viewModel.get();
println("{${accountConfiguration}}")
}
}
viewModel.get() is not a suspend function, so it returns immediately and proceeds to the next line. It actually returns the Job created by viewModelScope.launch().
If you want your coroutine to wait for the result before continuing you should make the get() function suspend and return the AccountConfiguration?
suspend fun get(): AccountConfiguration? {
return repository.get()
}
You need not change dispatchers to Dispatchers.Default because Room itself will switch to a background thread before executing any database operation.
Right now if there is a configuration change while coroutines inside lifecyclerScope are running, everything will get cancelled and restarted.
A better way would have been to put the suspending calls inside the ViewModel and expose a LiveData/Flow to the UI.
The problem is the viewModel function :
fun get() = viewModelScope.launch {
repository.get()
}
This function must be the coroutine instead launch the coroutine itself. Correct code is :
suspend fun get(): AccountConfiguration? {
return repository.get()
}
I am trying to listen to my ViewModels MutableStateFlow from my FlutterSceneView. But I get the following error when trying to set the listener from the views init:
Suspend function 'listenToBackgroundColor' should be called only from a coroutine or another suspend function
class FlutterSceneView(context: Context, private val viewModel: FlutterSceneViewModelType): PlatformView {
private val context = context
private val sceneView = SceneView(context)
init {
listenToBackgroundColor() // Error here
}
private suspend fun listenToBackgroundColor() {
viewModel.colorFlow.collect {
val newColor = Color.parseColor(it)
sceneView.setBackgroundColor(newColor)
}
}
}
My ViewModel:
interface FlutterSceneViewModelType {
var colorFlow: MutableStateFlow<String>
}
class FlutterSceneViewModel(private val database: Database): FlutterSceneViewModelType, ViewModel() {
override var colorFlow = MutableStateFlow<String>("#FFFFFF")
init {
listenToBackgroundColorFlow()
}
private fun listenToBackgroundColorFlow() {
database.backgroundColorFlow.watch {
colorFlow.value = it.hex
}
}
}
the .watch call is a helper I have added so that this can be exposed to iOS using Kotlin multi-platform, it looks as follows but I can use collect instead if necessary:
fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun watch(block: (T) -> Unit): Closeable {
val job = Job()
onEach {
block(it)
}.launchIn(CoroutineScope(Dispatchers.Main + job))
return object : Closeable {
override fun close() {
job.cancel()
}
}
}
}
I resolved this by using viewModel context:
private fun listenToBackgroundColor() {
viewModel.colorFlow.onEach {
val newColor = Color.parseColor(it)
sceneView.setBackgroundColor(newColor)
}.launchIn(viewModel.viewModelScope)
}
I had to import the following into my ViewModel:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
from:
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0")
I've been trying to test my view model for several days without success.
This is my view model :
class AdvertViewModel : ViewModel() {
private val parentJob = Job()
private val coroutineContext: CoroutineContext
get() = parentJob + Dispatchers.Default
private val scope = CoroutineScope(coroutineContext)
private val repository : AdvertRepository = AdvertRepository(ApiFactory.Apifactory.advertService)
val advertContactLiveData = MutableLiveData<String>()
fun fetchRequestContact(requestContact: RequestContact) {
scope.launch {
val advertContact = repository.requestContact(requestContact)
advertContactLiveData.postValue(advertContact)
}
}
}
This is my repository :
class AdvertRepository (private val api : AdvertService) : BaseRepository() {
suspend fun requestContact(requestContact: RequestContact) : String? {
val advertResponse = safeApiCall(
call = {api.requestContact(requestContact).await()},
errorMessage = "Error Request Contact"
)
return advertResponse
}
}
This is my view model test :
#RunWith(JUnit4::class)
class AdvertViewModelTest {
private val goodContact = RequestContact(...)
private lateinit var advertViewModel: AdvertViewModel
private var observer: Observer<String> = mock()
#get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
#Before
fun setUp() {
advertViewModel = AdvertViewModel()
advertViewModel.advertContactLiveData.observeForever(observer)
}
#Test
fun fetchRequestContact_goodResponse() {
advertViewModel.fetchRequestContact(goodContact)
val captor = ArgumentCaptor.forClass(String::class.java)
captor.run {
verify(observer, times(1)).onChanged(capture())
assertEquals("someValue", value)
}
}
}
The method mock() :
inline fun <reified T> mock(): T = Mockito.mock(T::class.java)
I got this error :
Wanted but not invoked: observer.onChanged();
-> at com.vizzit.AdvertViewModelTest.fetchRequestContact_goodResponse(AdvertViewModelTest.kt:52)
Actually, there were zero interactions with this mock.
I don't understand how to retrieve the result of my query.
You would need to write a OneTimeObserver to observe livedata from the ViewModel
class OneTimeObserver<T>(private val handler: (T) -> Unit) : Observer<T>, LifecycleOwner {
private val lifecycle = LifecycleRegistry(this)
init {
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
override fun getLifecycle(): Lifecycle = lifecycle
override fun onChanged(t: T) {
handler(t)
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
}
After that you can write an extension function:
fun <T> LiveData<T>.observeOnce(onChangeHandler: (T) -> Unit) {
val observer = OneTimeObserver(handler = onChangeHandler)
observe(observer, observer)
}
Than you can check this ViewModel class class that I have from a project to check what's going on with your LiveData after you act (when) with invoking a method.
As for your error, it just says that the onChanged() method is not being called ever.
I request data from server by bunches and store it in the array.To track fetching of the next bunch of the data I have this class.In the addItems method I notify diffObservers and pass list of new items:
class PackItems:MutableLiveData<ArrayList<GetPacksResponse.PackData>>() {
private var diffObservers=ArrayList<Observer<List<GetPacksResponse.PackData>>>()
private var active=false
fun observeItems(owner: LifecycleOwner, valueObserver:Observer<List<GetPacksResponse.PackData>>,diffObserver:Observer<List<GetPacksResponse.PackData>>) {
super.observe(owner,valueObserver)
diffObservers.add(diffObserver)
}
override fun removeObservers(owner: LifecycleOwner) {
super.removeObservers(owner)
diffObservers= ArrayList()
}
fun addItems(toAdd:List<GetPacksResponse.PackData>) {
value?.addAll(toAdd)
if (active)
for (observer in diffObservers)
observer.onChanged(toAdd)
}
override fun onActive() {
super.onActive()
active=true
}
override fun onInactive() {
super.onInactive()
active=false
}
}
The problem is PackItems is MutableLiveData and it's not good practice to expose it.Is there way to cast it to LiveData?Like usually we do:
private val _items = MutableLiveData<List<Int>>()
val items: LiveData<List<Int>> = _items
UPD:Ideally would be if I could expose completely immutable LiveData.But I can't just write
private val _packs:PackItems=PackItems()
val packs:LiveData<ArrayList<GetPacksResponse.PackData>>
get()=_packs
Because in this case packs won't contain observeItems method.Therefore there must be custom class derived from LiveData like:
open class PackItems: LiveData<ArrayList<GetPacksResponse.PackData>>() {
protected var active=false
protected var diffObservers = ArrayList<Observer<List<GetPacksResponse.PackData>>>()
fun observeItems(owner: LifecycleOwner, valueObserver: Observer<List<GetPacksResponse.PackData>>, diffObserver: Observer<List<GetPacksResponse.PackData>>) {
super.observe(owner,valueObserver)
diffObservers.add(diffObserver)
}
//...
}
class MutablePackItems: PackItems() {
fun addItems(toAdd:List<GetPacksResponse.PackData>) {
value?.addAll(toAdd)
if (active)
for (observer in diffObservers)
observer.onChanged(toAdd)
}
}
But in this case I won't be able to set data because now MutablePackItems is LiveData(immutable) :)
I'd consider using composition instead of inheritance:
class PackItems() {
private val mutableData = MutableLiveData<ArrayList<GetPacksResponse.PackData>>()
val asLiveData: LiveData<ArrayList<GetPacksResponse.PackData>> get() = mutableData
...
fun observeItems(owner: LifecycleOwner, valueObserver:Observer<List<GetPacksResponse.PackData>>,diffObserver:Observer<List<GetPacksResponse.PackData>>) {
mutableData.observe(owner,valueObserver)
diffObservers.add(diffObserver)
}
fun removeObservers(owner: LifecycleOwner) {
mutableData.removeObservers(owner)
diffObservers = ArrayList()
}
// etc
}
EDIT: to set active as in your original code, may be a bit nastier:
private val mutableData = object : MutableLiveData<ArrayList<GetPacksResponse.PackData>>() {
override fun onActive() {
super.onActive()
active = true
}
override fun onInactive() {
super.onInactive()
active = false
}
}
EDIT 2:
but the main problem is I need to return custom LiveData class with custom observeItems method
The point is that you don't necessarily. Whenever you'd call LiveData's method (e.g. observe), just call items.asLiveData.observe(...) instead. If you want to pass it to another method foo accepting LiveData, call foo(items.asLiveData).
In principle, you could modify this approach by extending LiveData and delegating all calls to mutableData:
class PackItems(): LiveData<ArrayList<GetPacksResponse.PackData>>() {
private val mutableData = MutableLiveData<ArrayList<GetPacksResponse.PackData>>()
...
fun observeItems(owner: LifecycleOwner, valueObserver:Observer<List<GetPacksResponse.PackData>>,diffObserver:Observer<List<GetPacksResponse.PackData>>) {
mutableData.observe(owner,valueObserver)
diffObservers.add(diffObserver)
}
override fun observe(owner: LifecycleOwner, observer: ArrayList<GetPacksResponse.PackData>) {
mutableData.observe(owner, observer)
}
override fun removeObservers(owner: LifecycleOwner) {
mutableData.removeObservers(owner) // not super!
diffObservers = ArrayList()
}
// etc
}
but I don't think it's a good idea.