FooActivity.kt:
class FooActivity : AppCompatActivity(), LifecycleRegistryOwner {
override fun getLifecycle(): LifecycleRegistry {
return LifecycleRegistry(this)
}
..
// <-- here mViewModel is null
mViewModel.getBar().observe(this, Observer<List<String>> {
override fun onChanged(bar: List<String>) {
// Never triggered
}
})
mViewModel.init()
// <-- here mViewModel has changed
}
The mViewModel is confirmed to change. However the observer's onChanged is never called.
Question: why doesn't it work?
Edit: FooViewModel.kt:
class FooViewModel(application: Application) : AndroidViewModel(application) {
val baz: BazPagerAdapter? = null
..
fun init(fm: FragmentManager) {
mBar = listOf("1", "2", "3")
}
..
fun getBar(): List<String> = mBar
..
fun setBaz(pager: ViewPager, periods: List<BazFragment>) {
pager.adapter = BazPagerAdapter(mFragmentManager!!, periods)
}
}
Edit2:
For got to mentiond, getBar already returns LiveData
fun getBar(): LiveData<List<String>> = mBar
And the onChange still wouldn't trigger.
Edit3:
class FooViewModel(application: Application) : AndroidViewModel(application) {
private var mBar: MutableLiveData<List<String>>? = null
..
fun init(fragmentManager: FragmentManager) {
..
if (mBar == null) {
mBar = MutableLiveData<List<String>>()
}
mBar?.value = periods
}
..
fun getBar(): LiveData<List<String>>? = mBar
There is no observe method for type List.
The ViewModel has nothing to do with observing either, it is there mainly to have state that persists through configuration changes.
For observable data you want (Mutable)LiveData objects. These are lifecycle aware and manage observers for their data.
Please see the code examples here:
public class MyViewModel : ViewModel() {
private val mBar = MutableLiveData<List<String>>()
fun getBar(): LiveData<List<String>> = mBar
fun init() {
mBar.setValue(listOf("1", "2", "3"))
}
}
Ok, so I have managed to find the answer.
If you are going to extend AppCompatActivity and you want to use LiveData, you will have to implement the LifecycleRegistryOwner interface and its only method - getLifecycle.
The problem was that:
override fun getLifecycle(): LifecycleRegistry {
return LifecycleRegistry(this)
}
Had to be:
val mLifecycleRegistry = LifecycleRegistry(this)
..
override fun getLifecycle(): LifecycleRegistry {
return mLifecycleRegistry
}
So the problem is when you call init method, it reassigns new instance of LiveData into mBar. And you have assigned observer to previous instance of mBar because you are calling init method after :
mViewModel.getBar().observe(this, Observer<List<String>> {
override fun onChanged(bar: List<String>) {
// Never triggered
}
})
To solve the problem just initialise mBar with MutableLiveData and then change its value (Do not re-assign mBar with another instance).
Check following code:
class FooViewModel(application: Application) : AndroidViewModel(application) {
private var mBar: MutableLiveData<List<String>> = MutableLiveData()
fun init(fragmentManager: FragmentManager) {
mBar.value = periods // Changing value only, not new instance
}
fun getBar(): LiveData<List<String>> = mBar
}
Related
In my viewmodel class
class ViewModel(application: Application) : AndroidViewModel(application) {
private val repository: Repository by lazy {
Repository.getInstance(getApplication<BaseApplication>().retrofitFactory)
}
private var _liveData = MutableLiveData<ItemState>()
val liveData: LiveData<ItemState> = _liveData
init {
fetchData()
}
private fun fetchData() {
repository.getLiveData().observeForever(liveDataObserver)
}
override fun onCleared() {
super.onCleared()
repository.getLiveData().removeObserver(liveDataObserver)
}
private val liveDataObserver = Observer<User> {
if (it != null) {
setData(it)
}
}
private fun setData(it: User) =viewModelScope.launch {
val list1 = mutableListOf<something1>()
val list2 = mutableListOf<something2>()
list1.add(it.data)
list2.add(it.data)
}
_liveData.value = ItemState.State1(list1)
delay(1)
_liveData.value = ItemState.State2(list2)
}
The ItemState is a sealed class with two data members
sealed class ItemState {
data class State1(val list: List<something1>) : ItemState()
data class State2(val list: List<something2>) : ItemState()
}
Activity Observer Code
viewModel.liveData.observe(this, Observer {
loadDataIntoUi(it)
})
private fun loadDataIntoUi(data: ItemState) {
when (data) {
is ItemState.State1 -> adaptr1.addItems(data.list)
is ItemState.State2 -> adaptr2.addItems(data.list)
}
Now if i don't use delay in my viewModel here like above the livedata first value that is Office doesn't get observed but it works fine with delay
I have done a lot of research didn't understand why this happening also I have many alternate solutions to this but my question is why delay make's it working
I am now stuck and currently wondering why my mutable arraylist returns null even if it is being updated with postvalue(). I tried to display it using Toast and it displayed [] which I think is null. It had no space in between so it looked like a box. I did toString() it as well in order to show the text. How would I be able to solve this problem?
Here is my Main Activity:
val list = ArrayList<String>()
list.add("text1")
list.add("text2")
val viewmodel = ViewModelProviders.of(this).get(viewmodel::class.java)
viewmodel.Testlist.postValue(list)
ViewModel:
class viewmodel: ViewModel() {
val Testlist: MutableLiveData<ArrayList<String>> = MutableLiveData()
init {
Testlist.value = arrayListOf()
}
}
Fragment:
Top area:
activity?.let {
val viewmodel = ViewModelProviders.of(this).get(viewmodel::class.java)
observeInput(viewmodel)
}
Bottom area:
private fun observeInput(viewmodel: viewmodel) {
viewmodel.Testlist.observe(viewLifecycleOwner, Observer {
it?.let {
Toast.makeText(context, it.toString(), Toast.LENGTH_LONG).show()
}
})
}
You post the value to the LiveData object in the activity's viewmodel, which isn't the same instance as the fragment's viewmodel. Let's take look at the way you instantiate the viewmodel in your fragment:
activity?.let {
// activity can be refered by the implicit parameter `it`
// `this` refers to the current fragment hence it's the owner of the view model
val viewmodel = ViewModelProviders.of(this).get(viewmodel::class.java)
observeInput(viewmodel)
}
To get a viewmodel that is shared between your activity and fragment you have to pass the activity as its owner:
activity?.let { val viewmodel = ViewModelProviders.of(it).get(viewmodel::class.java) }
Probably you can see developer guide example to resolve your problem
https://developer.android.com/topic/libraries/architecture/viewmodel.html#kotlin
// shared viewmodel
class SharedViewModel : ViewModel() {
private val usersList: MutableLiveData<List<String>>()
fun getUsers(): LiveData<List<String>> {
return usersList
}
fun setUsers(users: List<String>) {
usersList.value = users
}
}
// Attach ViewModel In Activity onCreate()
val model = ViewModelProviders.of(this)[SharedViewModel::class.java]
val list = arrayListOf<String>()
list.add("user1")
list.add("user2")
model.setUsers(list)
// Get same ViewModel instance In fragment onCreateView()
model = activity?.run {
ViewModelProviders.of(this)[SharedViewModel::class.java]
} ?: throw Exception("Invalid Activity")
model.getUsers().observe(this, Observer<List<User>>{ users ->
// update UI
})
You can use this :
fun <T : Any?> MutableLiveData<ArrayList<T>>.default(initialValue: ArrayList<T>) = apply { setValue(initialValue) }
and then use this function as below:
viewmodel.Testlist.default(ArrayList())
For me, I have a BaseActivity that other activities extend from it :
class UAppCompatActivity : AppCompatActivity() {
protected fun <T : Any?> MutableLiveData<ArrayList<T>>.default(initialValue: ArrayList<T>) = apply { setValue(initialValue) }
protected fun <T> MutableLiveData<ArrayList<T>>.addItem(item: T) {
val updatedItems = this.value as ArrayList
updatedItems.add(item)
this.value = updatedItems
}
protected fun <T> MutableLiveData<ArrayList<T>>.deleteItem(item: T) {
val updatedItems = this.value as ArrayList
updatedItems.remove(item)
this.value = updatedItems
}
...
have you used the same instance of your view model? or have you defined another view model in the fragment class? The issue could be that you're accessing a different instance of the view model and not the one were the MutableLiveData was updated
I have a repository setup like this
class ServerTimeRepo #Inject constructor(private val retrofit: Retrofit){
var liveDataTime = MutableLiveData<TimeResponse>()
fun getServerTime(): LiveData<TimeResponse> {
val serverTimeService:ServerTimeService = retrofit.create(ServerTimeService::class.java)
val obs = serverTimeService.getServerTime()
obs.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).unsubscribeOn(Schedulers.io())
.subscribe(object : Observer<Response<TimeResponse>> {
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
}
override fun onNext(t: Response<TimeResponse>) {
val gson = Gson()
val json: String?
val code = t.code()
val cs = code.toString()
if (!cs.equals("200")) {
json = t.errorBody()!!.string()
val userError = gson.fromJson(json, Error::class.java)
} else {
liveDataTime.value = t.body()
}
}
override fun onError(e: Throwable) {
}
})
return liveDataTime
}
}
Then I have a viewmodel calling this repo like this
class ServerTimeViewModel #Inject constructor(private val serverTimeRepo: ServerTimeRepo):ViewModel() {
fun getServerTime(): LiveData<TimeResponse> {
return serverTimeRepo.getServerTime()
}
}
Then I have an activity where I have an onClickListener where I am observing the livedata, like this
tvPWStart.setOnClickListener {
val stlv= serverTimeViewModel.getServerTime()
stlv.observe(this#HomeScreenActivity, Observer {
//this is getting called multiple times??
})
}
I don't know what's wrong in this. Can anyone point me in the right direction? Thanks.
Issue is that every time your ClickListener gets fired, you observe LiveData again and again. So, you can solve that problem by following solution :
Take a MutableLiveData object inside your ViewModel privately & Observe it as LiveData.
class ServerTimeViewModel #Inject constructor(private val serverTimeRepo: ServerTimeRepo):ViewModel() {
private val serverTimeData = MutableLiveData<TimeResponse>() // We make private variable so that UI/View can't modify directly
fun getServerTime() {
serverTimeData.value = serverTimeRepo.getServerTime().value // Rather than returning LiveData, we set value to our local MutableLiveData
}
fun observeServerTime(): LiveData<TimeResponse> {
return serverTimeData //Here we expose our MutableLiveData as LiveData to avoid modification from UI/View
}
}
Now, we observe this LiveData directly outside of ClickListener and we just call API method from button click like below :
//Assuming that this code is inside onCreate() of your Activity/Fragment
//first we observe our LiveData
serverTimeViewModel.observeServerTime().observe(this#HomeScreenActivity, Observer {
//In such case, we won't observe multiple LiveData but one
})
//Then during our ClickListener, we just do API method call without any callback.
tvPWStart.setOnClickListener {
serverTimeViewModel.getServerTime()
}
I've a WeatherRepository class which calls the WeatherProvider class to start fetching the weather.
After the weather is successfully fetched, I simply post that weather using postValue function but the observer on that livedata in the WeatherRepository class's init block never gets called.
I am confused as what am I missing...
Any insights would be extremely helpful.
Here's my code for Repository and Provider:
class WeatherRepository #Inject constructor(private var weatherDao: WeatherDao, private var weatherProvider: WeatherProvider) {
private fun startFetchWeatherService() {
weatherProvider.startFetchWeatherService()
}
init {
// Control flow always gets to this point
var weather = weatherProvider.getDownloadedWeather()
weather.observeForever { // This observer never gets called
if (it != null) AsyncTask.execute { insertWeather(it) }
}
if (isFetchNeeded()) {
startFetchWeatherService() // Android Studio always execute this line since no data is inserted by observer and fetch is needed
}
}
....
}
class WeatherProvider(private val context: Context) {
private val mDownloadedWeather = MutableLiveData<List<Weather>>()
...
fun getDownloadedWeather(): MutableLiveData<List<Weather>> = mDownloadedWeather
fun getFromInternet() {
...
call.enqueue(object : Callback<WorldWeatherOnline> {
override fun onFailure(call: Call<WorldWeatherOnline>?, t: Throwable?) {} // TODO show error
override fun onResponse(call: Call<WorldWeatherOnline>?, response: Response<WorldWeatherOnline>?) {
if (response != null) {
val weather = response.body()?.data
if (weather != null) {
mDownloadedWeather.postValue(WeatherUtils.extractValues(weather)) // app always gets to this point and WeatherUtils successfully returns the List of weathers full of data
}
}
}
})
}
fun startFetchWeatherService() {
val intentToFetch = Intent(context, WeatherSyncIntentService::class.java)
context.startService(intentToFetch)
}
}
...
// Dependency injection always works
// Here's my dagger2 module (other modules are very simillar to this one)
#Module
class ApplicationModule(private val weatherApplication: WeatherApplication) {
#Provides
internal fun provideWeatherApplication(): WeatherApplication {
return weatherApplication
}
#Provides
internal fun provideApplication(): Application {
return weatherApplication
}
#Provides
#Singleton
internal fun provideWeatherProvider(context: WeatherApplication): WeatherProvider {
return WeatherProvider(context)
}
}
#Singleton
class CustomViewModelFactory constructor(private val weatherRepository: WeatherRepository, private val checklistRepository: ChecklistRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
when {
modelClass.isAssignableFrom(WeatherViewModel::class.java) ->
return WeatherViewModel(weatherRepository) as T
modelClass.isAssignableFrom(ChecklistViewModel::class.java) ->
return ChecklistViewModel(checklistRepository) as T
else ->
throw IllegalArgumentException("ViewModel Not Found")
}
}
}
class WeatherFragment : Fragment() {
private lateinit var mWeatherModel: WeatherViewModel
#Inject
internal lateinit var viewModelFactory: ViewModelProvider.Factory
....
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mWeatherModel = ViewModelProviders.of(this, viewModelFactory)
.get(WeatherViewModel::class.java)
...
}
}
It is not necessary to change your postValue to setValue since it is done in a same Thread. The real issue here is the way how Dagger2 is supposed to be set.
In WeatherFragment.kt use
internal lateinit var viewModelFactory: CustomViewModelFactory
rather than
internal lateinit var viewModelFactory: ViewModelProvider.Factory
It is also necessary to add #Inject annotation in your CustomViewModelFactory.kt's constructor.
class CustomViewModelFactory #Inject constructor(
And lastly your WeatherProvider.kt is not in initialized state at all base on the code you provided. You can do it using this code :
init {
getFromInternet()
}
Try to use
mDownloadedWeather.setValue(WeatherUtils.extractValues(weather))
instead of
mDownloadedWeather.postValue(WeatherUtils.extractValues(weather))
Because postValue() Posts a task to a main thread to set the given value. So if you have a following code executed in the main thread:
liveData.postValue("a");
liveData.setValue("b");
The value "b" would be set at first and later the main thread would override it with the value "a".
If you called this method multiple times before a main thread executed a posted task, only the last value would be dispatched.
Hi I am reading this example of LiveData and Observer https://code.tutsplus.com/tutorials/introduction-to-android-architecture--cms-28749
MainActivityViewModel.kt
class MainActivityViewModel : ViewModel() {
private var notes: MutableLiveData<List<String>>? = null
fun getNotes(): LiveData<List<String>> {
if (notes == null) {
notes = MutableLiveData<List<String>>()
loadNotes()
}
return notes!!
}
private fun loadNotes() {
// do async operation to fetch notes
}
}
MainActivity.kt
class MainActivity : LifecycleActivity(), AnkoLogger {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProviders.of(this)
.get(MainActivityViewModel::class.java)
viewModel.getNotes().observe(
this, Observer {
notes -> info("notes: $notes")
}
)
}
}
How LiveData is sending data to MainActivity if there is any changes in notes (new or delete). I see activity is calling viewModel.getNotes() which may not get called once onCreate method finish.
LiveData isn't sending anything to MainActivity, it's "sending" to the Observer passed to the observe method. This Observer has a reference to MainActivity where it was created and can call its methods.