I have a MutableLiveData list in my repository as follows :
class AnswerRepository {
private var _answerList = mutableListOf<Answer>()
var answerList = MutableLiveData<MutableList<Answer>>()
fun addAnswerInList(answer: Answer) {
_answerList.add(answer)
answerList.value = _answerList
Log.e("AnswerRepository", "Answer List size : ${answerList.value?.size}")
Log.e("AnswerRepository", "_Answer List Size : ${_answerList.size}")
}
fun returnAnswerList(): MutableLiveData<MutableList<Answer>> {
return answerList
}
}
An item is added in 'answerList' (the MutableLiveData List) in a service as given below :
class FloatingWidgetService : Service(), View.OnClickListener{
private val answerRepository = AnswerRepository()
#SuppressLint("InflateParams")
override fun onCreate() {
super.onCreate()
//Getting the widget layout from xml using layout inflater
mFloatingView = LayoutInflater.from(this).inflate(R.layout.floating_widget, null)
initialiseViews()
setListeners()
}
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onClick(p0: View?) {
when (p0?.id) {
R.id.next -> {
addItemInList()
}
}
private fun addItemInList(){
val answer = Answer(questionNumber, selectedOption, questionStatus)
answerRepository.addAnswerInList(answer)
}
Then this MutableLiveData List (answersList) is being observed in the fragment using viewmodel between repository and the fragment as follows :
ViewModel :
class SendAnswerToCloudViewModel : ViewModel() {
private val answerRepository = AnswerRepository()
val answerList = answerRepository.returnAnswerList()
}
Fragment :
class SendAnswerToCloud : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentSendDataToCloudBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_send_data_to_cloud,
container,
false
)
binding.lifecycleOwner = this
viewModel.answerList.observe(viewLifecycleOwner, Observer {
Log.e("SendAnswerToCloud", "isChangeTrigerred")
val answer = viewModel.answerList.value?.last()
Log.e(
"SendAnswerToCloud",
"QuestionNumber : ${answer?.questionNumber}, SelectedOption : ${answer?.selectedOption}, QuestionStatus : ${answer?.questionStatus}"
)
})
return binding.root
}
}
The list is successfully updated in the repository when addAnswerInListis called in the service. However nothing happens in the fragment (as the logs don't appear in the logcat).
So, what am I doing wrong ? Any kind of help would be highly appreciated. Thanks in Advance!!
The problem in your implementation is that you instantiate two AnswerRepository objects instead of one. Thus, you get two var answerList = MutableLiveData<MutableList<Answer>>() instead of one. While your SendAnswerToCloud to cloud listening for changes on the first answerList your service edits the other answerList. That is the reason you do not see any changes.
Make sure you create only one AnswerRepository object.
In programming people use dependency injection and optionally in combination with singleton pattern. Sometimes you can get away using only singleton pattern, but this is a less flexible solution and not so easy to test.
Detailed reponse
So the problem occurs because you have first object instantiation in FloatingWidgetService class:
class FloatingWidgetService : Service(), View.OnClickListener{
private val answerRepository = AnswerRepository()
...
and the second instantiation in SendAnswerToCloudViewModel class:
class SendAnswerToCloudViewModel : ViewModel() {
private val answerRepository = AnswerRepository()
...
}
This way you create two absolutely separate objects. Each one of them occupies different address in memory, and all of the objects you create inside AnswerRepository are also different between these two instances.
Imagine placing those declarations one after the other like this:
class SendAnswerToCloudViewModel : ViewModel() {
private val answerRepository = AnswerRepository()
private val answerRepository_second = AnswerRepository()
...
}
If you later compare them by equals method or by == operator you will get result false because they are two different objects. Thus, if you set a new value to answerList of answerRepository subscribers of answerList stored in answerRepository_second will not receive any updates.
Solution
Add companion object and make the primary constructor private.
class AnswerRepository private constructor() {
companion object {
private var INSTANCE: AnswerRepository? = null
fun getInstance(): AnswerRepository {
if (INSTANCE == null) {
INSTANCE = AnswerRepository()
}
return INSTANCE!!
}
}
private var _answerList = mutableListOf<Answer>()
var answerList = MutableLiveData<MutableList<Answer>>()
fun addAnswerInList(answer: Answer) {
_answerList.add(answer)
answerList.value = _answerList
Log.e("AnswerRepository", "Answer List size : ${answerList.value?.size}")
Log.e("AnswerRepository", "_Answer List Size : ${_answerList.size}")
}
fun returnAnswerList(): MutableLiveData<MutableList<Answer>> {
return answerList
}
}
Now instead of writing declarations with constructor invocation:
private val answerRepository = AnswerRepository()
You will call getInstance() method to get AnswerRepository.
private val answerRepository = AnswerRepository.getInstance()
This pattern is called singleton. When you ensure that your program has only one instance of a specific class.
Strongly recommend you to complete Essentials and Kotlin maps here.
Related
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)
}
I have two recycler views. My view is not updated until I used notifyDataSetChanged. I asked for a similar type of issue, but this time I have Github Link. So please have a look and explain to me what I am doing wrong. Thanks
MainActivity.kt
package com.example.diffutilexample
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.example.diffutilexample.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<ActivityViewModel>()
private lateinit var binding: ActivityMainBinding
private var groupAdapter: GroupAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupViewModel()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel.fetchData()
binding.button.setOnClickListener {
viewModel.addData()
}
}
private fun setupViewModel() {
viewModel.groupListLiveData.observe(this) {
if (groupAdapter == null) {
groupAdapter = GroupAdapter()
binding.recyclerview.adapter = groupAdapter
}
groupAdapter?.submitList(viewModel.groupList?.toMutableList())
binding.recyclerview.post {
groupAdapter?.notifyDataSetChanged()
}
}
}
}
ActivityViewModel.kt
package com.example.diffutilexample
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
var groupListLiveData: MutableLiveData<Boolean> = MutableLiveData()
var groupList: ArrayDeque<Group>? = null
set(value) {
field = value
groupListLiveData.postValue(true)
}
var value = 0
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
groupList = groupByData(response.abc)
}
}
private fun groupByData(abc: List<Abc>?): ArrayDeque<Group> {
val result: ArrayDeque<Group> = groupList ?: ArrayDeque()
abc?.iterator()?.forEach { item ->
val key = GroupKey(item.qwe)
result.addFirst(Group(key, mutableListOf(item)))
}
return result
}
fun addData() {
groupList?.let { lastList ->
val qwe = Qwe("Vivek ${value++}", "Modi")
val item = Abc(type = "Type 1", "Adding Message", qwe)
val lastGroup = lastList[0]
lastGroup.list.add(item)
groupList = lastList
}
}
}
Please find the whole code in Github Link. I attached in above
I haven't debugged this, but if you remove your overuse of MutableLists and vars, and simplify your LiveData, you will likely eliminate your bug. At the very least, it will help you track down the problem.
MutableLists and DiffUtil do not play well together!
For example, Group's list should be a read-only List:
data class Group(
val key: GroupKey,
val list: List<Abc?> = emptyList()
)
It's convoluted to have a LiveData that only reports if some other property is usable. Then you're dealing with nullability all over the place here and in the observer, so it becomes hard to tell when some code is going to be skipped or not from a null-safe call. I would change your LiveData to directly publish a read-only List. You can avoid nullable Lists by using emptyList() to also simplify code.
You can avoid publicly showing your interior workings with the ArrayDeque as well. And you are lazy loading the ArrayDeque unnecessarily, which leads to having to deal with nullability unnecessarily.
class ActivityViewModel(app: Application) : AndroidViewModel(app) {
private val _groupList = MutableLiveData<List<Group>>()
val groupList: LiveData<List<Group>> get() = _groupList
private val trackedGroups = ArrayDeque<Group>()
private var counter = 0
fun fetchData() {
viewModelScope.launch {
val response = ApiInterface.create().getResponse()
addFetchedData(response.abc.orEmpty())
_groupList.value = trackedGroups.toList() // new copy for observers
}
}
private fun addFetchedData(abcList: List<Abc>) {
for (item in abcList) {
val key = GroupKey(item.qwe)
trackedGroups.addFirst(Group(key, listOf(item)))
}
}
fun addData() {
if (trackedGroups.isEmpty())
return // Might want to create a default instead of doing nothing?
val qwe = Qwe("Vivek ${counter++}", "Modi")
val item = Abc(type = "Type 1", "Adding Message", qwe)
val group = trackedGroups[0]
trackedGroups[0] = group.copy(list = group.list + item)
_groupList.value = trackedGroups.toList() // new copy for observers
}
}
In your Activity, since your GroupAdapter has no dependencies, you can instantiate it at the call site to avoid dealing with lazy loading it. And you can set it to the RecyclerView in onCreate() immediately.
Because of the changes in ViewModel, observing becomes very simple.
If you do something in setupViewModel() that updates a view immediately, you'll have a crash, so you should move it after calling setContentView().
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<ActivityViewModel>()
private lateinit var binding: ActivityMainBinding
private val groupAdapter = GroupAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater).apply {
setContentView(root)
recyclerview.adapter = groupAdapter
button.setOnClickListener {
viewModel.addData()
}
}
setupViewModel()
viewModel.fetchData()
}
private fun setupViewModel() {
viewModel.groupList.observe(this) {
groupAdapter.submitList(it)
}
}
}
Your DiffUtil.ItemCallback.areItemsTheSame in GroupAdapter is incorrect. You are only supposed to check if they represent the same item, not if their contents are the same, so it should not be comparing lists.
override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean {
return oldItem.key == newItem.key
}
And in GroupViewHolder, you are creating a new adapter for the inner RecyclerView every time it is rebound. That defeats the purpose of using RecyclerView at all. You should only create the adapter once.
I am predicting that the change in the nested list is going to look weird when the view is being recycled rather than just updated, because it will animate the change from what was in the view previously, which could be from a different item. So we should probably track the old item key and avoid the animation if the new key doesn't match. I think this can be done in the submitList() callback parameter to run after the list contents have been updated in the adapter by calling notifyDataSetChanged(), but I haven't tested it.
class GroupViewHolder(val binding: ItemLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
companion object {
//...
}
private val adapter = NestedGroupAdapter().also {
binding.nestedRecyclerview.adapter = it
}
private var previousKey: GroupKey? = null
fun bindItem(item: Group?) {
val skipAnimation = item?.key != previousKey
previousKey = item?.key
adapter.submitList(item?.list.orEmpty()) {
if (skipAnimation) adapter.notifyDataSetChanged()
}
}
}
Side note: your adapters' bindView functions are confusingly named. I would just make those into secondary constructors and you can make the primary constructor private.
class GroupViewHolder private constructor(private val binding: ItemLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
constructor(parent: ViewGroup) : this(
ItemLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
//...
}
I'm not entirely sure, and I admit I haven't extensively studied your code, and this is not a solution, but this might point you in the right direction of how to solve it.
The thing about
groupAdapter?.submitList(viewModel.groupList?.toMutableList())
Is that toMutableList() does indeed make a copy of the list. But each of the objects in the list are not copies. If you add things to an object in the original list, like you do in addData() it in fact is also already added to the copy that is in the adapter. That's why a new submitList doesn't recognize it as a change because it is actually the same as it was before the submitList.
As far as I understand, working with DiffUtil works best if the list you submit only contains objects that are immutable, so mistakes like this can't happen. I have ran into a similar problem before and the solution is also not straightforward. In fact, I don't entirely remember how I solved it back then, but hopefully this pushes you in the right direction.
I'm trying to DRY up my code and I have a couple activities which use the same blocks of code which I want to move into a method in the parent activity. The problem is that this code uses generated ViewBindings which are unique classes, and I can't figure out what the parent class is in order to use it as a method parameter.
For example, this code is in two different activities and the only difference is that in one activity binding = Activity1Binding, in the other one it's Activity2Binding. They share some views with the same IDs.
binding.noteTitleTV.setOnClickListener { changeTitle() }
binding.deleteModalLayout.setOnClickListener { binding.deleteModalLayout.visibility = View.GONE }
binding.cancelDeleteButton.setOnClickListener { binding.deleteModalLayout.visibility = View.GONE }
binding.confirmDeleteButton.setOnClickListener { onDeleteNoteClicked() }
I would like to implement something like this in the parent activity to prevent duplicate code, if that's possible:
fun setUp(binding: [BINDING PARENT CLASS]) {
binding.noteTitleTV.setOnClickListener { changeTitle() }
// etc
}
The generated classes extend the Object class (java.lang.Object)
The binding class inherits from ViewDataBinding, so you could do this (Kotlin code)
fun setUp(binding: ViewDataBinding) {
when(binding){
is Activity1Binding -> { (binding as Activity1Binding).noteTitelTV.setOnClickListner{ changeTitle() } }
is Activity2Binding -> { (binding as Activity2Binding).noteTitelTV.setOnClickListner{ changeTitle() } }
}
// etc
}
I don't know that it could get more "generic" than that as you don't control the generated classes. But that would at least allow you to place all the code in a single class as you suggested. I use a similar approach in that I have an lateinit instance of all of my generated binding classes and just set which is active based on the passed variable and use that instance name so i don't have to keep typing as.
ex:
private lateinit var itemBinding : GroceryItemBinding
private lateinit var maintItemBinding : GroceryItemMaintBinding
private lateinit var compareItemBinding : GroceryItemCompareBinding
private lateinit var priceItemBinding : GroceryItemPriceBinding
private lateinit var adItemBinding : GroceryItemAdBinding
when(viewBinding){
is GroceryItemMaintBinding -> {
maintItemBinding = viewBinding as GroceryItemMaintBinding
maintItemBinding.groceryItem = gi
maintItemBinding.groceryItemImage.setOnClickListener { ... }
......
}
is GroceryItemBinding -> {
itemBinding = viewBinding as GroceryItemBinding
}
......
}
ViewBinding can create by bind(view), so you can create a base class like this:
abstract class BaseActivity : AppCompatActivity() {
private lateinit var binding: Activity1Binding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val view = createContentView()
// create ViewBinding
binding = Activity1Binding.bind(view)
}
// create view by subclass
abstract fun createContentView(): View
fun setTextViewTitle(text: CharSequence) {
binding.tvTitle.text = text
}
}
this is the content of Activity1Binding#bind():
#NonNull
public static ActivityMainBinding bind(#NonNull View rootView) {
// The body of this method is generated in a way you would not otherwise write.
// This is done to optimize the compiled bytecode for size and performance.
int id;
missingId: {
id = R.id.layout;
FinanceLabelLayout layout = rootView.findViewById(id);
if (layout == null) {
break missingId;
}
return new ActivityMainBinding((ConstraintLayout) rootView, layout);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
But this is not recommended.
This is not type safe.
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);
Activity receiving intent
class AddNoteActivity : AppCompatActivity() {
private lateinit var addViewModel: NoteViewModel
private lateinit var titleEditText: TextInputEditText
private lateinit var contentEditText: TextInputEditText
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_note_activty)
setSupportActionBar(toolbar)
addViewModel = ViewModelProviders.of(this).get(NoteViewModel::class.java)
titleEditText = findViewById(R.id.itemTitle)
contentEditText = findViewById(R.id.itemNote)
val extra = intent.extras
if (extra != null) {
val uuid = extra.getLong("UUID")
val note: Notes? = addViewModel.getNote(uuid)
titleEditText.setText(note!!.title)
contentEditText.setText(note.note)
}
}
}
NoteViewModel class
class NoteViewModel(application: Application) : AndroidViewModel(application) {
companion object {
private var note: Notes = Notes(0, "", "test title", "test ontent")
}
fun getNote(uuid: Long?): Notes {
val job = async(CommonPool) {
getNoteAsyncTask(notesDatabase).execute(uuid)
}
runBlocking { job.await() }
return note
}
class getNoteAsyncTask(database: NotesDatabase) : AsyncTask<Long, Unit, Unit>() {
private val db: NotesDatabase = database
override fun doInBackground(vararg params: Long?) {
note = db.notesDataDao().getNote(params[0])
}
}
}
If I pass an intent to get a Note object from the database with a uuid and set that received data in titleEditText and contentEditText, the data set in the Note was from previous intent invoked when we clicked on the Note item in RecyclerView. On clicking the Note item for the first time, I get the default value which I have set "test title" and "test content".
Aforementioned is the behavior most of the time. Sometimes the data set in titleEditText and contentEditText is of the correct Note object.
Can someone please tell me what I have done wrong? How can I correct my apps behavior?
Unfortunately, there is a big mistake in how you use a view model to provide a data to your view(AddNoteActivity).
Basically, your view never has a chance to wait for the data to be fetched as it always receives a default value. This happens because the AsyncTask runs on its own thread pool so the coroutine completes immediately and returns a default value.
You should consider using LiveData to post a new object to your view and refactor your view model.
So, you need to make a query to the database synchronous and observe changes to a note rather than have a getter for it. Of course, in a real life scenario it might be a good idea to have different kind of states to be able to show a spinner while a user is waiting. But this is another big question. So to keep things simple consider changing your view model to something like that:
class NoteViewModel(private val database: NotesDatabase) : ViewModel { // you do not need an application class here
private val _notes = MutableLiveData<Notes>()
val notes: LiveData<Notes> = _notes
fun loadNotes(uuid: Long) {
launch(CommonPool) {
val notes = database.notesDataDao().getNote(uuid)
_notes.setValue(notes)
}
}
}
Then, you can observe changes to the note field in your activity.
class AddNoteActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
val noteViewModel = ViewModelProviders.of(this).get(NoteViewModel::class.java)
noteViewModel.notes.observe(this, Observer {
title.text = it.title
content.text = it.note
})
}
}
Also you need to use a ViewModelProvider.Factory to create your view model and properly inject dependencies into it. Try to avoid having a context there as it makes it much harder to test.