Issue
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 .
Solution
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). If you return the data you need to post it.
Asynchronous loader
If your routine to get the data is asynchronous (e.g. runs in a coroutine or calls some API like Firebase) you can use the pattern from the example like this, where you call something.postValue()
from within the loading callback, and do not return the data:
// 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)
}
}
Synchronous loader
If you want to use the lazy
approach, but your getSomething()
method is synchronous (returns the data) you can use an approach like this, where you post the data right away inside also
. Make sure not to try to call something.postValue()
inside getSomethingSync
though.
private val something: MutableLiveData<List<String>> by lazy {
MutableLiveData<List<String>>().also {
it.postValue(getSomethingSync())
}
}
fun load(): LiveData<List<String>> {
return something
}
private fun getSomethingSync(): List<String> {
val data = listOf("hello","world") // get your List<Something> here
return data
}
Without using lazy
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")
}
}
}
Answered By - Tyler V
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.