Home > database >  Android - LiveData observe not getting update from AndroidViewModel in active Activity
Android - LiveData observe not getting update from AndroidViewModel in active Activity

Time:07-12

I do see quite some questions about this issue but most of them are related to Fragment. I am calling it inside the activity but still not observing any changes.

MainActivity.kt:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val mainViewModel =
            ViewModelProvider(
                this,
                MainViewModelFactory(application)
            )[MainViewModel::class.java]

        val observable = mainViewModel.load()

        // Everything before here is working well
        observable.observe(this) { something ->
            // Unreachable
        }
    }
}

MainViewModel.kt:

class MainViewModel(application: Application) :
    AndroidViewModel(application) {
    private val app = getApplication<Application>()

    private val something: LiveData<List<Something>> by lazy {
        MutableLiveData<List<Something>>().also {
            getSomething()
        }
    }

    fun load(): LiveData<List<Something>> {
        return something
    }

    private fun getSomething(): List<Something> {
        // Readthe data from local assets
    }
}

MainViewModelFactory.kt:

class MainViewModelFactory(
    private val application: Application
) : ViewModelProvider.AndroidViewModelFactory(application) {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            return MainViewModel(application) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

Both getSomething and load work as expected. I can print out the observable before the observe() getting called, but the content inside the observe will never reach .

CodePudding user response:

We don't know what you put into getSomething() method :) This method is assumed to be async. Usually it calls viewmodelscope.launch with IO. As stated, we have no idea what's there and how values are passed to livedata 'something'.

Example of viewModel:

    val errorMessage: MutableLiveData<String> = MutableLiveData()

Fragment:

    viewModel.errorMessage.observe(this.viewLifecycleOwner) {
        Log.d(TAG, "Oh, message have been changed in VM")
    }

Value change from viewModel itself:

errorMessage.postValue("new message has drawbacks")

CodePudding user response:

In the examples you linked (here) the loadUsers() method does not return the data, it calls some method to get it asynchronously, then posts it to the LiveData (but they don't show that part). It should look something like this in your case:

// Note that this should be "MutableLiveData", 
// not "LiveData" as you had it
private val something: MutableLiveData<List<String>> by lazy {
    MutableLiveData<List<String>>().also {
        getSomething()
    }
}

fun load(): LiveData<List<String>> {
    return something
}

private fun getSomething() {
    // If you don't use a coroutine here or some other async API call this can trigger an
    // infinite recursion... Yet another reason not to use this pattern.
    // See below for a better option.
    viewModelScope.launch(Dispatchers.IO) {
        // Read the data from local assets - if this is an IO
        // operation or otherwise slow, you'll want to load it in
        // a coroutine like this.
        val data = listOf("hello","world") // get your List<Something> here
        something.postValue(data)
    }
}

A More Robust Form

I generally prefer a simpler form that decouples observing from loading (having calling an object getter trigger internal side effects like loading data makes for confusing code IMO). Plus if your data loading is not asynchronous posting the value in the lazy initializer can cause the app to hang.

class MainViewModel : ViewModel() {
    
    private val somethingLiveData = MutableLiveData<String>()
    val something: LiveData<String>
        get() = somethingLiveData

    fun load() {
        // Ok to call postValue here either inside or outside a coroutine
        viewModelScope.launch {
            somethingLiveData.postValue("test")
        }
    }
}

where you can call load() separately from setting observers

// Set up your observers first
model.something.observe(this) { s ->
    println("TEST: observer saw $s")
}

// then start the data loading
model.load() // start the loading process deliberately,
             // either in onCreate or onResume as needed

If you want to ensure it only loads once (like the lazy approach does) you can just add a check in load to only load the data if nothing was posted yet

fun load() {
    if( somethingLiveData.value == null ) {
        viewModelScope.launch {
            somethingLiveData.postValue("test")
        }
    }
}
  • Related