Cannot reassign variable inside observer viewmodel - android

So I created MVVM app in kotlin to fetch movies from TMDB api, using injections and coroutines.
My problem is that I cannot copy the list of returned movies into a new list I created or reassign any variables inside the livedata observer from the MainActivity the values of variables stays the same as they were after exit the scope.
MainActivity class:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding:ActivityMainBinding
private val viewModel:MoviesViewModel by lazy {
ViewModelProvider(this)[MoviesViewModel::class.java]
}
private lateinit var list: MutableList<Movies>
private var number:Int=1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding=ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
getData()
list
number
}
private fun getData(){
list= mutableListOf()
viewModel.getData(page = "1").observe(this#MainActivity,Observer{ item ->
item?.let { res ->
when (res.status) {
Status.SUCCESS -> {
var tmpList = item.data?.body()?.results
list= tmpList!!.toMutableList()
number+=1
}
Status.ERROR -> {
res.message?.let { Log.e("Error", it) }
}}}
})}}
ViewModel class:
class MoviesViewModel #ViewModelInject constructor(var repository: MoviesRepository): ViewModel() {
fun getData(page:String)= liveData(Dispatchers.IO){
emit(Resource.loading(data = null))
try {
emit(Resource.success(data=repository.getMovies(api_key = Constants.API_KEY,
start_year=Constants.START_YEAR, end_year = Constants.END_YEAR,page = page)))
}catch (e:Exception){
emit(e.message?.let { Resource.error(message = it, data = null) })
}
}
}
As you can see I tried to change the value of number and load the list into my new list but outside the scope the values returned to be what they were before.
Very thankful for anyone who can assist.
Update:
So I tried to initialized all the items inside the success case and it worked I guess there is no other way to change the values outside the scope.

Related

ViewModel not initializing or problem design with my viewModel

I've been reading some questions, answers and blogs about MVVM pattern in Android, and I've implemented it in my application.
My application consists of a MainActivity with 3 Tabs. Content of each tab is a fragment.
One of these fragments, is a List of Users stored on Room DB, which is where I've implemented the MVVM (implementing User object, ViewModel, Repository and Adapter with RecycleView).
In this same fragment, I have an "add User" button at the end that leads to a new activity where a formulary is presented to add a new user. In this activity I want to be sure that the full name of user not exists in my DB before saving it.
I was trying to use the same ViewModel to get full UserNames full name, but it seems that ViewModel is never initialized and I dont' know why.
I've read some questions about that viewmodel can't be used in different activities (I use it in MainActivity also in AddUser activity
This is my ViewModel:
class UserViewModel : ViewModel() {
val allUsersLiveData: LiveData<List<User>>
private val repository: UserRepository
init {
Timber.i("Initializing UserViewModel")
repository = UserRepository(UserTrackerApplication.database!!.databaseDao())
allUsersLiveData = repository.getAllUsers()
}
fun getAllUsersFullName(): List<String> {
return allUsersLiveData.value!!.map { it.fullname}
}
And my AddUser activity:
class AddUser : AppCompatActivity() {
private lateinit var userList:List<String>
private lateinit var binding: ActivityAddUserBinding
private val userViewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_user)
Timber.i("Add User OnCreate")
binding = ActivityAddUserBinding.inflate(layoutInflater)
setContentView(binding.root)
}
fun addUserClick(v : View){
//someCode
val userName = binding.constraintLayoutAddUser.etUserName!!.text.toString()
if(checkUserExistance(userName)) {
val text: String = String.format(
resources.getString(R.string.repeated_user_name),
userName
Snackbar.make(v, text, Snackbar.LENGTH_LONG).show()
{
else
{
lifecycleScope.launch {
UserTrackerApplication.database!!.databaseDao()
.insertUser(user)
Timber.i("User added!")
}
finish()
}
}
Debugging, I see the log "Initializing UserViewModel" when the fragment of MainActivity is started, but I can't see it when AddUser activity is called. So it seems it's not initializing correctly.
So the questions:
Is this a good approach? I'm making some design mistake?
Why the VM isn't initializing?
EDIT
I forgot to add this function. Calling userViewModel here is where I get the error:
private fun checkUserExistance(userName: String): Boolean {
var result = false
userList = userViewModel.getAllUsersNames()
for (usr in userList)
{
if(usr.uppercase() == userName.uppercase())
{
result = true
break
}
}
return result
}
EDIT 2
I added this on my "onCreate" function and started to work:
userViewModel.allUsersLiveData.observe(this, Observer<List<User>>{
it?.let {
// updates the list.
Timber.i("Updating User Names")
userList =userViewModel.getAllUsersNames()
}
})
if you take a look at by viewModels delegate you will see it's lazy it means it will initialize when it is first time accessed
#MainThread
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}

Property value changes in ViewModel before it is updated in RecyclerViewAdapter

What I have is a ViewModel, a Fragment and a RecyclerViewAdapter.
The use case is as follows:
User wants to change the name of one item in the RecyclerView.
His command is sent to the ViewModel.
ViewModel updates the item and postValue using LiveData
Fragment observes on the live property and sends a command to the RecyclerViewAdapter to update the list.
List is updated.
Snippet containing only methods that I checked are valid for this problem.
data class Name(
var firstName: String = "",
var lastName: String = "",
)
class NameListRecyclerViewAdapter : RecyclerView.Adapter<NameListRecyclerViewAdapter.ListViewHolder>() {
private var names: List<Name> = listOf()
fun setData(newList: List<Name>) {
names = newList
notifyDataSetChanged()
}
}
class NameListViewModel : ViewModel() {
private var names: MutableList<Name>? = null
private val _nameList = MutableLiveData<MutableList<Name>>()
val nameList: LiveData<MutableList<Name>>
get() = _nameList
fun changeFirstName(index: Int, name: String) {
names?.get(0)?.firstName = name // in this very moment the names property in NameListRecyclerViewAdapter is being changed
_nameList.postValue(names)
}
}
class NamesListFragment : Fragment() {
private lateinit var viewModel: NameListViewModel
private lateinit var recyclerViewAdapter: NameListRecyclerViewAdapter
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
viewModel = ViewModelProvider(
requireActivity(),
NameListViewModelFactory(requireActivity().applicationContext)
).get(NameListViewModel::class.java)
recyclerViewAdapter = NameListRecyclerViewAdapter()
recycler_view_layout.apply {
setHasFixedSize(true)
layoutManager =
LinearLayoutManager(context).apply {
orientation = LinearLayoutManager.VERTICAL
}
adapter = recyclerViewAdapter
}
viewModel.nameList.observe(
viewLifecycleOwner,
Observer {
val result = it ?: return#Observer
recyclerViewAdapter.setData(result)
}
)
}
}
For the first time, the name is changing as it should, but the second one is very peculiar.
The name (the property in the RecyclerViewAdapter) is updated not when it should (in step 5) but in step 3 - even before the new value is posted using liveData.
My theory is that its the same list in the ViewModel and the RecyclerViewAdapter, but I have no idea why is that, why one is not a copy of the other??
Update The problem seems to be solved when in the ViewModel i add .toMutableList() as follows:
class NameListViewModel : ViewModel() {
//...
fun changeFirstName(index: Int, name: String) {
names?.get(0)?.firstName = name
_nameList.postValue(names.toMutableList()) // instead of simple _nameList.postValue(names)
}
}
Does that mean that live data property has the exact same list and I have to make sure to copy it always?

LiveData Value only observes the last added value but using delay makes it working didn't understand why?

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

MutableLiveData ArrayList is empty even after postValue() Kotlin

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

Hold data from EventListener in MutableLiveData

I'm trying to retrive data from firestore and want to keep it in shared view model. Basically I have a main activity and 2 fragments that need to retrieve data from shared view model of main activity. My current method is :
class SharedViewModel: ViewModel() {
private val firebaseUtils = FirebaseUtils()
fun getTempWords(localeLearn: String): LiveData<DocumentSnapshot> {
val document = firebaseUtils.getTempWordsLocaleRef(localeLearn)
return FirebaseDocumentLiveData(document)
}}
What i want is that just retrive data once and keep it in MutableLiveData and pass to fragmetns.
Edit:
What I'm done is:
var tempWords : MutableLiveData<DocumentSnapshot> = MutableLiveData()
fun getTemp(localeLearn: String): LiveData<DocumentSnapshot> {
if (tempWords.value == null) {
val document = firebaseUtils.getTempWordsLocaleRef(localeLearn)
tempWords = FirebaseDocumentLiveData(document)
}
return tempWords
}
But if i kill the fragment and recreate it again, it calls EventListener in FirebaseDocumentLiveData(document) class again.
Edit 2:
My Fragment
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
//
model = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
}
//
model.getTemp(mLocaleLearn!!).observe(this, Observer {...}
What is the question? it looks like you successfully made it.
You ask how you can access the data from your fragments?
in view model class
private final MutableLiveData<Item> data = new MutableLiveData<Item>();
in your fragments:
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);

Categories

Resources