I do not have very much experience with Kotlin, been doing most of my development in Java. I have converted my ArtistFragment to Kotlin in order to solve another issue. But after converting it I am getting to errors that I do not know how to resolve.
The first one is Smart cast to 'RecyclerView!' is impossible, because 'recyclerView' is a mutable property that could have been changed by this time and the second one is Cannot create an instance of an abstract class
I searched through Stackoverflow, but since I don't really understand the what the problem is I am not sure if they are relevant to my problems.
Here is my converted ArtistFragment:
class ArtistFragment : Fragment() {
var spanCount = 3 // 2 columns
var spacing = 20 // 20px
var includeEdge = true
private var recyclerView: RecyclerView? = null
private var adapter: ArtistAdapter? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_artist, container, false)
recyclerView = view.findViewById(R.id.artistFragment)
recyclerView.setLayoutManager(GridLayoutManager(activity, 3))
LoadData().execute("")
return view
}
abstract inner class LoadData : AsyncTask<String?, Void?, String>() {
protected fun doInBackground(vararg strings: String): String {
if (activity != null) {
adapter = ArtistAdapter(activity, ArtistLoader().artistList(activity))
}
return "Executed"
}
override fun onPostExecute(s: String) {
recyclerView!!.adapter = adapter
if (activity != null) {
recyclerView!!.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, includeEdge))
}
}
override fun onPreExecute() {
super.onPreExecute()
}
}
}
I am seeing Smart cast to 'RecyclerView!' is impossible, because 'recyclerView' is a mutable property that could have been changed by this time here in this section: recyclerView.setLayoutManager(GridLayoutManager(activity, 3))
I am seeing Cannot create an instance of an abstract class here in this section LoadData().execute("")
I hoping someone can explain these error and how to fix them.
Thanks
As your recyclerview is a nullable property, You need to modify your code as below -
recyclerView?.setLayoutManager(GridLayoutManager(activity, 3))
Remove abstract from class
inner class LoadData : AsyncTask<String?, Void?, String>()
Smart cast to 'RecyclerView!' is impossible, because 'recyclerView' is a mutable property that could have been changed by this time is because recyclerView is a var. Kotlin has the concept of mutable (var) and immutable (val) variables, where a val is a fixed value, but a var can be reassigned.
So the issue is that recyclerView is a nullable type (RecyclerView?) and it could be null - so you need to make sure it's not null before you access it, right? findViewById returns a non-null RecyclerView, which gets assigned to recyclerView, so the compiler knows it can't be null.
But then it gets to the next line - what if the value of recyclerView has changed since the previous line? What if something has modified it, like on another thread? It's a var so that's possible, and the compiler can't make any guarantees about whether it's null or not anymore. So it can't "smart cast to RecyclerView!" (! denotes non-null) and let you just treat it as a non-null type in your code. You have to null check it again.
What Priyanka's code (recyclerView?.setLayoutManager(GridLayoutManager(activity, 3))) does is null-checks recyclerView, and only makes the call if it's non-null. Under the hood, it's copying recyclerView to a temporary val (which isn't going to change) and checks that, and then executes the call on that.
This is a pretty standard thing with Kotlin, and not just for nullable types - any var could possibly change at any moment, so it's typical to assign it to a temporary variable so you can do the stuff on that stable instance. Kotlin has a bunch of scope functions which are convenient for doing things on an object, and one of the benefits is that you know the reference you're acting on isn't going to change halfway through
Related
I want to add the random generated integer into my MutableList in Player class when I use the random integer generator method located in Player class in my fragment then I want to pass this MutableList to Fragment with using Livedata(I'm not sure if i'm doing right with using livedata).Then show the MutableList in TextView.But MutableList returns the default value not after adding.
So what am i doing wrong ? What should i do ?
Thank you
MY CLASS
open class Player {
//property
private var playerName : String
private var playerHealth : Int
var playerIsDead : Boolean = false
//constructor
constructor(playerName:String,playerHealth:Int){
this.playerName = playerName
this.playerHealth = playerHealth
}
var numberss: MutableList<Int> = mutableListOf()
fun attack(){
//Create a random number between 1 and 10
var damage = (1..10).random()
//Subtract health points from the opponent
Log.d("TAG-DAMAGE-WRITE","$damage")
numberss.add(damage)
Log.d("TAG-DAMAGE-ADD-TO-LIST","$numberss")
Log.d("TAG-NUMBER-LIST-LATEST-VERSION","$numberss")
}
}
MY FRAGMENT
class ScreenFragment : Fragment() {
var nickname : String? = null
private lateinit var viewModel : DenemeViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_screen, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(DenemeViewModel::class.java)
arguments?.let {
nickname = ScreenFragmentArgs.fromBundle(it).nickName
}
sFtextView.text = "Player : $nickname"
action()
}
private fun action(){
val health1 = (1..50).random()
val health2 = (1..50).random()
val superMan = Superman("Dusman",health1,)
while(superMan.playerIsDead == false){
//attack each other
superMan.attack()
sFtextsonuc.text = "Superman oldu"
viewModel.setData()
observeLiveData()
superMan.playerIsDead = true
}
}
fun observeLiveData(){
viewModel.damageList.observe(viewLifecycleOwner, Observer { dmgList ->
dmgList?.let {
sFtextsonuc.text = it.toString()
Log.d("TAG-THE-LIST-WE'VE-SENT-TO-FRAGMENT","$it")
}
})
}
}
MY VIEWMODEL
class DenemeViewModel : ViewModel() {
val damageList:MutableLiveData<MutableList<Int>> = MutableLiveData()
fun setData(){
damageList.value = Superman("",2).numberss
Log.d("TAG-VIEWMODEL","${damageList.value}")
}
}
MY LOG
PHOTO OF THE LOGS
Your Superman is evidently part of some ongoing game, not something that should be created and then destroyed inside the action function. So you need to store it in a property. This kind of state is usually stored in a ViewModel on Android so it can outlive the Fragment.
Currently, you are creating a Superman in your action function, but anything created in a function is automatically sent do the garbage collector (destroyed) if you don't store the reference in a property outside the function.
Every time you call the Superman constructor, such as in your line damageList.value = Superman("",2).numberss, you are creating a new instance of a Superman that has no relation to the one you were working with in the action() function.
Also, I recommend that you do not use LiveData until you fully grasp the basics of OOP: what object references are, how to pass them around and store them, and when they get sent to the garbage collector.
So, I would change your ViewModel to this. Notice we create a property and initialize it with a new Superman. Now this Superman instance will exist for as long as the ViewModel does, instead of only inside some function.
class DenemeViewModel : ViewModel() {
val superman = Superman("Dusman", (1..50).random())
}
Then in your frgament, you can get this same Superman instance anywhere you need to use it, whether it be to deal some damage to it or get the current value of its numberss to put in an EditText.
Also, I noticed in your action function that you have a while loop that repeatedly deals damage until Superman is dead. (It also incorrectly observes live data over and over in the loop, but we'll ignore that.) The problem with this is that the while loop is processed to completion immediately, so you won't ever see any of the intermediate text. You will only immediately see the final text. You probably want to put some delays inside the loop to sort of animate the series of events that are happening. You can only delay easily inside a coroutine, so you'll need to wrap the while loop in a coroutine launch block. In a fragment when working with views, you should do this with viewLifecycleOwner.lifecycleScope.launch.
Finally, if you set playerIsDead to true on the first iteration of the loop, you might as well remove the whole loop. I'm guessing you want to wrap an if-condition around that line of code. But since your code above doesn't modify player health yet, there's no sensible way to determine when a player should be dead, so I've commented out the while loop
private fun action() = viewLifecycleOwner.lifecycleScope.launch {
val superMan = viewModel.superman
//while(superMan.playerIsDead == false){
//attack each other
superMan.attack()
sFtextsonuc.text = "Superman oldu"
delay(300) // give user a chance to read the text before changing it
sFtextsonuc.text = superMan.numberss.joinToString()
delay(300) // give user a chance to read the text before changing it
// TODO superMan.playerIsDead = true
//}
}
I'm using room query and return LiveData to display elements on UI. The problem is the entity changes most of the properties very often which isn't relative to the UI and due to the UI refreshed many times brings no good.
What I want is like swift combine #Published.
Here is the code:
#Entity
#Parcelize
data class Foo(#PrimaryKey var code: String,
var p1: Double,
var p2: Int? = null,
var p3: Int? = null,
var p4: Double? = null,
var p5: Int? = null,
var p6: Double? = null,
var p7: Int? = null
): Parcelable
Actually I only care about code property changes insert/delete.
#Query("SELECT * FROM Foo WHERE code IN (:fooIds)")
fun getLiveDataListBy(fooIds`: List<String?>): LiveData<List<Foo>?>?
I have the property in ViewModel and observe it in fragment.
var foosLiveData: LiveData<List<Fool>>? = null
viewModel.foosLiveData?.observe(viewLifecycleOwner, {
adapter.foos = it
adapter.notifyDataSetChanged()
})
The p1 to p7 properties are keep changing. Due to the list refresh all the time.
Right now, I could improve it by checking
if (adapter.foos != it) {
adapter.foos = it
adapter.notifyDataSetChanged()
}
But this almost no improvement.
then if might be improved by this: (I haven't tested)
adapter.foos = it
adapter.notifyDataSetChanged()
}
this could be works, but it will keep check the map, only might take adapter.foos.map { a -> a.code } out to save a bit.
This might another workaround.
I also thought take the code out and use a new variable var codeObserver: MutableLiveData(List<String>) = MutableLiveData()
Then
viewModel.foosLiveData?.observe(viewLifecycleOwner, {
viewModel.codeObserver.value = it.foo.map { it.code}
})
viewModel.codeObserver.observe(viewLifecycleOwner, {
adapter.foos = viewModel.foosLiveData?.value
adapter.notifyDataSetChanged()
}
Well, I haven't test the above code, but looks like not right direction.
So any better or right way to achieve observe only one or few properties?
You can avoid reloading the recycler view adapter by using AsyncListDiffer. Refer to Google's Documentation for further information. In simple words, it can consume the values from a LiveData of List and present the data simply for an adapter. It computes differences in list contents via DiffUtil on a background thread as new Lists are received.
it is a known issue that ListAdapter (actually the AsyncListDiffer from its implementation) does not update the list if the new list only has modified items but has the same instance. The updates do not work on new instance list either if you use the same objects inside.
For all of this to work, you have to create a hard copy of the entire list and objects inside.
Easiest way to achieve this:
items.toMutableList().map { it.copy() }
But I am facing a rather weird issue. I have a parse function in my ViewModel that finally posts the items.toMutableList().map { it.copy() } to the LiveData and gets observes in the fragment. Even with the hard copy, DiffUtil does not work. If I move the hard copy inside the fragment, then it works.
To get this easier, if I do this:
IN VIEW MODEL:
[ ... ] parse stuff here
items.toMutableList().map { it.copy() }
restaurants.postValue(items)
IN FRAGMENT:
restaurants.observe(viewLifecycleOwner, Observer { items ->
adapter.submitList(items)
... then, it doesn't work. But if I do this:
IN VIEW MODEL:
[ ... ] parse stuff here
restaurants.postValue(items)
IN FRAGMENT:
restaurants.observe(viewLifecycleOwner, Observer { items ->
adapter.submitList(items.toMutableList().map { it.copy() })
... then it works.
Can anybody explain why this doesn't work?
In the mean time, I have opened an issue on the Google Issue Tracker because maybe they will fix the AsyncListDiffer not updating same instance lists or items. It defeats the purpose of the new adapter. The AsyncListDiffer SHOULD ALWAYS accept same instance lists or items, and fully update using the diff logic that the user customises in the adapter.
I made a quick sample using DiffUtil.Callback and ListAdapter<T, K> (so I called submitList(...) on the adapter), and had no issues.
Then I modified the adapter to be a normal RecyclerView.Adapter and constructed an AsyncDiffUtil inside of it (using the same DiffUtil.Callback from above).
The architecture is:
Activity -> Fragment (contains RecyclerView).
Adapter
ViewModel
"Fake Repository" that simply holds a val source: MutableList<Thing> = mutableListOf()
Model
I've created a Thing object: data class Thing(val name: String = "", val age: Int = 0).
For readability I added typealias Things = List<Thing> (less typing). ;)
Repository
It's fake in the sense that items are created like:
private fun makeThings(total: Int = 20): List<Thing> {
val things: MutableList<Thing> = mutableListOf()
for (i in 1..total) {
things.add(Thing("Name: $i", age = i + 18))
}
return things
}
But the "source" is a mutableList of (the typealias).
The other thing the repo can do is "simulate" a modification on a random item. I simply create a new data class instance, since it's obviously all immutable data types (as they should be). Remember this is just simulating a real change that may have come from an API or DB.
fun modifyItemAt(pos: Int = 0) {
if (source.isEmpty() || source.size <= pos) return
val thing = source[pos]
val newAge = thing.age + 1
val newThing = Thing("Name: $newAge", newAge)
source.removeAt(pos)
source.add(pos, newThing)
}
ViewModel
Nothing fancy here, it talks and holds the reference to the ThingsRepository, and exposes a LiveData:
private val _state = MutableLiveData<ThingsState>(ThingsState.Empty)
val state: LiveData<ThingsState> = _state
And the "state" is:
sealed class ThingsState {
object Empty : ThingsState()
object Loading : ThingsState()
data class Loaded(val things: Things) : ThingsState()
}
The viewModel has two public methods (Aside from the val state):
fun fetchData() {
viewModelScope.launch(Dispatchers.IO) {
_state.postValue(ThingsState.Loaded(repository.fetchAllTheThings()))
}
}
fun modifyData(atPosition: Int) {
repository.modifyItemAt(atPosition)
fetchData()
}
Nothing special, just a way to modify a random item by position (remember this is just a quick hack to test it).
So FetchData, launches the async code in IO to "fetch" (in reality, if the list is there, the cached list is returned, only the 1st time the data is "made" in the repo).
Modify data is simpler, calls modify on the repo and fetch data to post the new value.
Adapter
Lots of boilerplate... but as discussed, it's just an Adapter:
class ThingAdapter(private val itemClickCallback: ThingClickCallback) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
The ThingClickCallback is just:
interface ThingClickCallback {
fun onThingClicked(atPosition: Int)
}
This Adapter now has an AsyncDiffer...
private val differ = AsyncListDiffer(this, DiffUtilCallback())
this in this context is the actual adapter (needed by the differ) and DiffUtilCallback is just a DiffUtil.Callback implementation:
internal class DiffUtilCallback : DiffUtil.ItemCallback<Thing>() {
override fun areItemsTheSame(oldItem: Thing, newItem: Thing): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: Thing, newItem: Thing): Boolean {
return oldItem.age == newItem.age && oldItem.name == oldItem.name
}
nothing special here.
The only special methods in the adapter (aside from onCreateViewHolder and onBindViewHolder) are these:
fun submitList(list: Things) {
differ.submitList(list)
}
override fun getItemCount(): Int = differ.currentList.size
private fun getItem(position: Int) = differ.currentList[position]
So we ask the differ to do these for us and expose the public method submitList to emulate a listAdapter#submitList(...), except we delegate to the differ.
Because you may be wondering, here's the ViewHolder:
internal class ViewHolder(itemView: View, private val callback: ThingClickCallback) :
RecyclerView.ViewHolder(itemView) {
private val title: TextView = itemView.findViewById(R.id.thingName)
private val age: TextView = itemView.findViewById(R.id.thingAge)
fun bind(data: Thing) {
title.text = data.name
age.text = data.age.toString()
itemView.setOnClickListener { callback.onThingClicked(adapterPosition) }
}
}
Don't be too harsh, I know i passed the click listener directly, I only had about 1 hour to do all this, but nothing special, the layout it's just two text views (age and name) and we set the whole row clickable to pass the position to the callback. Nothing special here either.
Last but not least, the Fragment.
Fragment
class ThingListFragment : Fragment() {
private lateinit var viewModel: ThingsViewModel
private var binding: ThingsListFragmentBinding? = null
private val adapter = ThingAdapter(object : ThingClickCallback {
override fun onThingClicked(atPosition: Int) {
viewModel.modifyData(atPosition)
}
})
...
It has 3 member variables. The ViewModel, the Binding (I used ViewBinding why not it's just 1 liner in gradle), and the Adapter (which takes the Click listener in the ctor for convenience).
In this impl., I simply call the viewmodel with "modify item at position (X)" where X = the position of the item clicked in the adapter. (I know this could be better abstracted but this is irrelevant here).
there's only two other implemented methods in this fragment...
onDestroy:
override fun onDestroy() {
super.onDestroy()
binding = null
}
(I wonder if Google will ever accept their mistake with Fragment's lifecycle that we still have to care for this).
Anyway, the other is unsurprisingly, onCreateView.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.things_list_fragment, container, false)
binding = ThingsListFragmentBinding.bind(root)
viewModel = ViewModelProvider(this).get(ThingsViewModel::class.java)
viewModel.state.observe(viewLifecycleOwner) { state ->
when (state) {
is ThingsState.Empty -> adapter.submitList(emptyList())
is ThingsState.Loaded -> adapter.submitList(state.things)
is ThingsState.Loading -> doNothing // Show Loading? :)
}
}
binding?.thingsRecyclerView?.adapter = adapter
viewModel.fetchData()
return root
}
Bind the thing (root/binding), get the viewModel, observe the "state", set the adapter in the recyclerView, and call the viewModel to start fetching data.
That's all.
How does it work then?
The app starts, the fragment is created, subscribes to the VM state LiveData, and triggers the Fetch of data.
The ViewModel calls the repo, which is empty (new), so makeItems is called the list now has items and cached in the repo's "source" list. The viewModel receives this list asynchronously (in a coroutine) and posts the LiveData state.
The fragment receives the state and posts (submit) to the Adapter to finally show something.
When you "click" on an Item, ViewHolder (which has a click listener) triggers the "call back" towards the fragment which receives a position, this is then passed onto the Viewmodel and here the data is mutated in the Repo, which again, pushes the same list, but with a different reference on the clicked item that was modified. This causes the ViewModel to push a new LIveData state with the same list reference as before, towards the fragment, which -again- receives this, and does adapter.submitList(...).
The Adapter asynchronously calculates this and the UI updates.
It works, I can put all this in GitHub if you want to have fun, but my point is, while the concerns about the AsyncDiffer are valid (and may be or been true), this doesn't seem to be my (super limited) experience.
Are you using this differently?
When I tap on any row, the change is propagated from the Repository
UPDATE: forgot to include the doNothing function:
val doNothing: Unit
get() = Unit
I've used this for a while, I normally use it because it reads better than XXX -> {} to me. :)
While doing
items.toMutableList().map { it.copy() }
restaurants.postValue(items)
you are creating a new list but items remains the same. You have to store that new list into a variable or passing that operation directly as a param to postItem.
savedRecordFileName is a variable of LivaData<String>, I hope to get the value of savedRecordFileName at once in Code A.
You know that LiveData variable is lazy, maybe the value of savedRecordFileName is null in binding.btnStop.setOnClickListener { }, so the code in binding.btnStop.setOnClickListener { } will not be fired when the value of savedRecordFileName is null.
I hope that the code in binding.btnStop.setOnClickListener { } can always be fired, how can I do?
BTW, I think the Code B is not suitable because the value of savedRecordFileName maybe changed by other function.
Code B
binding.btnStop.setOnClickListener {
mHomeViewModel.savedRecordFileName.observe(viewLifecycleOwner){
val aMVoice = getDefaultMVoice(mContext,it)
mHomeViewModel.add(aMVoice)
}
}
Code A
class FragmentHome : Fragment() {
private val mHomeViewModel by lazy {
getViewModel {
HomeViewModel(mActivity.application, provideRepository(mContext))
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
...
binding.btnStop.setOnClickListener {
mHomeViewModel.savedRecordFileName.value?.let{
val aMVoice = getDefaultMVoice(mContext,it)
mHomeViewModel.add(aMVoice)
}
}
...
return binding.root
}
}
class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
val savedRecordFileName: LiveData<String> = mDBVoiceRepository.getTotalOfVoice().map {
mApplication.getString(R.string.defaultName, (it+1).toString())
}
}
class DBVoiceRepository private constructor(private val mDBVoiceDao: DBVoiceDao){
fun getTotalOfVoice() = mDBVoiceDao.getTotalOfVoice()
}
#Dao
interface DBVoiceDao{
#Query("SELECT count(id) FROM voice_table")
fun getTotalOfVoice(): LiveData<Long>
}
Add Content
To Ridcully: Thanks!
I think your way "move all of that into the viewmodel class" is good !
I think it will be OK even if the filename is livedata in your Code C, right?
Code C
viewModelScope.launch(Dispatchers.IO) {
filename = dao.getFilename() // without livedata. I think it will be OK even if the filename is livedata
voice = getDefaultVoice(...) // also do this in background
add(voice)
result.postValue(true)
}
As you said, you should do Room queries in background to avoid blocking ui thread. But you shouldn't stop there. Do as much work as possible in a background thread. In your case, on button click, start running a background thread that does all the work: getting name from database (with direct query, which is possible now as you are running in background), getting the default voice, and adding it, to whatever it is you are adding it to.
You should also move all of that into the viewmodel class.
btnStop.setOnClickListener {
viewmodel.stop()
}
In viewmodel something like this (java / pseudo code like):
void stop() {
runinbackgroundthread {
filename = dao.getFilename() // without livedata
voice = getDefaultVoice(...) // also do this in background
add(voice)
// If you want the action to have a reault, observable by ui
// use a MutableLiveData and set it here, via postValue()
// as we are still in background thread.
result.postValue(true)
}
}
On how to actually implement my pseudo code runinbackgroundthread see some official guide here: https://developer.android.com/guide/background/threading
In short, with Kotlin coroutines it should go like this:
viewModelScope.launch(Dispatchers.IO) {
filename = dao.getFilename() // without livedata
voice = getDefaultVoice(...) // also do this in background
add(voice)
result.postValue(true)
}
Solution 1
A simple/tricky/dirty solution would be an empty observe in onViewCreated like:
mHomeViewModel.savedRecordFileName.observe(viewLifecycleOwner){ }
which causes the LiveData to be observed, so it will be queried by Room because it is active and hopefully when you click on the button, the LiveData is filled.
Solution 2
But my favorite approach would be a reactive one. I don't know if you want to take a reactive look toward your code or not, but you can use MediatorLiveData (or take advantage of a library like this) to solve this situation reactively.
Let there be another LiveData in your VM named btnStopClicked and each click changes the value of that LiveData. So, your code may look like this:
class FragmentHome : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
...
binding.btnStop.setOnClickListener {
mHomeViewModel.btnStopClicked.value = null //we don't care about the value
}
mHomeViewModel.addVoiceEvent.observe(viewLifecycleOwner){
val aMVoice = getDefaultMVoice(mContext,it)
mHomeViewModel.add(aMVoice)
}
...
return binding.root
}
}
class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
val savedRecordFileName: ... //whatever it was
val btnStopClicked = MutableLiveData<Any>()
val addVoiceEvent = savedRecordFileName
.combineLatestWith(btnStopClicked){srfn, _ -> srfn}
.toSingleEvent()
}
Also do not forget toSingleEvent because if you don't turn it into an event, it would get called unintentionally in some situations (look at this).
TL;DR - Code A is better, with extra additions.
As you already know, LiveData is not guaranteed to have a non-null value right after instantiation. Ideally, as for best practice, it is good to always treat LiveData value as ever changing (i.e - assume direct value access twice will produce different values). This is intended behavior as LiveData is designed to be observable instead of you dealing with immediate value.
Hence, listen to the LiveData and update your UI accordingly. For instance, disable or hide the stop button when there's no data inside it.
mHomeViewModel.savedRecordFileName.observe(viewLifecycleOwner) {
binding.btnStop.isEnabled = it != null
}
binding.btnStop.setOnClickListener {
mHomeViewModel.savedRecordFileName.value?.also {
val aMVoice = getDefaultMVoice(mContext,it)
mHomeViewModel.add(aMVoice)
}
}
Little story of this topic : the app just updating clicked row's values with dialog when confirmed. Uses pagination scenario on room database.
When an item added or removed, the latest dataset is fetched and passed to submitList method, then all changes are seen and worked well.
The problem starts there, if an existing item updated, again the latest dataset is fetched properly and passed to submitList, but this time changes didn't seem.
When i debug the DIFF_CALLBACK and caught my item in areItemsTheSame, the newHistory and oldHistory values are same! (How!)
There could be any bug in submitList method ?
Room v. : 2.1.0-alpha02
Paging v. : 2.1.0-beta01
After initializing, observe fetches list from room and passes to mHistoryAdapter.submitList(it). Then if i update an item, observe gets triggered again(and i'm seeing updated value in param it) and passes to submitList.
Unfortunately, adapter wont change...
mResolvedAddressViewModel = ViewModelProviders.of(this).get(ResolvedAddressViewModel::class.java)
mResolvedAddressViewModel.getAddresses(false).observe(this, Observer {
mHistoryAdapter.submitList(it)
})
All the parts
Model
#Parcelize
#Entity
data class ResolvedAddress(
#PrimaryKey var id: String = UUID.randomUUID().toString(),
var requestedLat: Double = 0.0,
var requestedLon: Double = 0.0,
var requestedAddress: String = "",
var lat: Double,
var lon: Double,
var address: String,
var country: String,
var countryCode: String,
var city: String,
var alias: String? = null,
var favorite: Boolean = false,
var provider: String? = null,
var lastUseDate: Long = 0L) : Parcelable
Adapter
class HistoryAdapter(var context: Context)
: PagedListAdapter<ResolvedAddress, HistoryItemHolder>(DIFF_CALLBACK) {
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ResolvedAddress>() {
override fun areItemsTheSame(
oldHistory: ResolvedAddress, newHistory: ResolvedAddress): Boolean {
return oldHistory.id == newHistory.id
}
override fun areContentsTheSame(
oldHistory: ResolvedAddress, newHistory: ResolvedAddress): Boolean {
return oldHistory == newHistory
}
}
}
}
Fragment
class HistoryFragment : Fragment() {
private lateinit var mHistoryAdapter: HistoryAdapter
private lateinit var mResolvedAddressViewModel: ResolvedAddressViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_history, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerViewHistory.setHasFixedSize(true)
recyclerViewHistory.layoutManager = LinearLayoutManager(activity)
recyclerViewHistory.itemAnimator = DefaultItemAnimator()
mHistoryAdapter = HistoryAdapter(context!!)
recyclerViewHistory.adapter = mHistoryAdapter
mResolvedAddressViewModel = ViewModelProviders.of(this)
.get(ResolvedAddressViewModel::class.java)
mResolvedAddressViewModel.getAddresses(false).observe(this, Observer {
mHistoryAdapter.submitList(it)
})
}
}
There's a couple things missing from the question that could help provide a more detailed answer.
Ex. What does your RecyclerView.Adapter look like? Does it extend PagedListAdapter?
What does your model class look like? Is it a Kotlin data class?
For the sake of providing an answer, let's assume those unknowns are what we expect.
If I understand the question, it seems like you're just updating an item and not removing or adding any items.
Therefore, the DiffUtil.ItemCallback's areItemsTheSame will always return true, because the old list and new list has not been modified in terms of their size.
Meaning, if you've updated an item, you've probably updated it's contents and not removed it from the list.
Therefore, areItemsTheSame will return true, because their ids are still the same.
It's more likely that the second method, areContentsTheSame will return false since you've updated the item's content.
If your model class, ResolvedAddress, is a Kotlin data class, then the method areContentsTheSame should return false when comparing the item that was updated from the old list and the new list. This should trigger the onBindViewHolder method in your adapter at this point for you to rebind that item with the updated data.
If that model is not a Kotlin data class, than you must make sure the class implements the compareTo method. If not, you are comparing the object's memory address vs the actual contents of the object. If that is the case, the method areContentsTheSame will always return true, since the object's memory address has not changed.
These are some debugging tips, as it is difficult to provide a clearer answer without more knowledge about how the code has been implemented.
I was having a similar issue but managed to fix it by updating the existing item with a new object rather than directly updating the existing item, as suggested by this answer here:
https://stackoverflow.com/a/54505078/10923311
The issue is with how submitList processes changes. If you are passing a reference to the same list, it will not show updates as it determines it is the same datasource. In Kotlin if you want to update the sourceList and pass it back to submitList, you can do so as follows:
submitList(originalList.toList().toMutableList().let {
it[index] = it[index].copy(property = newvalue) // To update a property on an item
it.add(newItem) // To add a new item
it.removeAt[index] // To remove an item
// and so on....
it
})