First question in the community :)
I am building an app in which I have a MainActivity and fragments everywhere.
Inside a fragment I have another fragment which has a RecyclerView.
I load the elements of the recyclerview from Cloud Firebase and it works perfectly!
The thing I have a problem with is the time it takes to load every time I close and enter the same fragment.
For example: When I first load this fragment, it takes a second or two to get the data and parse it into objects etc etc... to finally show the recyclerView as I mean to.
BUT when I press the back button and enter the fragment again, it takes 2 seconds (aprox) again.
Is there a way to make the request only one time, save the Adapter (or the response) and then set it up again?
I have read about onSaveInstanceState(), I have read about SavedInstanceState, I tried setting a counter, setting a boolean, but I still can't find a way to STORE the information in a way that it is still alive when I shut down the fragment.
Hope this makes sense,
Thanks in advance!
EDIT:
fun getObjects(): FirestoreRecyclerAdapter<Object, ObjectViewHolder> {
val firestore = FirebaseFirestore.getInstance()
val query = firestore.collection("collectionPath")
val response = FirestoreRecyclerOptions.Builder<Object>()
.setQuery(query, Object::class.java).build()
val adapter = object : FirestoreRecyclerAdapter<Object, ObjectViewHolder>(response) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ObjectViewHolder {
val view = LayoutInflater.from(parent.context).inflate(
R.layout.element_object,
parent, false
)
return ObjectViewHolder(view)
}
override fun onBindViewHolder(holder: ObjectViewHolder, position: Int, object: Object) {
val title = (position + 1).toString() + ". " + object.title
holder.itemView.element_object_title.text = title
val image = holder.itemView.element_object_image
Picasso.with(holder.itemView.context).load(object.image).into(image)
holder.object = object
}
}
return adapter
}
Related
In my case I am trying to get a list of String from my Adapter and use it in my Fragment but upon debugging using Logs I found that the list is getting updated inside the onBindViewHolder but not outside it. So when I try to access the list from my Fragment I am getting an empty list of String.
I have spent few hours trying to figure this but can't find a feasible solution.
My Approach: I am thinking of an approach to save this list in a room table and then query it back in the Fragment. Though it may solve the issue but is it the only way? Are there any other ways to achieve this result?
My Adapter
class FloorProfileDialogAdapter() : RecyclerView.Adapter<FloorProfileDialogAdapter.MyViewHolder>() {
var floors = emptyList<String>()
inner class MyViewHolder(val binding: ScheduleFloorDialogItemBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ScheduleFloorDialogItemBinding.inflate(inflater, parent, false)
return MyViewHolder(binding)
}
private val checkedFloors: MutableList<String> = mutableListOf()
//List of uniquely selected checkbox to be observed from New Schedule Floor Fragment
var unique: List<String> = mutableListOf()
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val currentFloor = floors[position]
Timber.d("Current floor: $currentFloor")
holder.binding.floorCheckBox.text = "Floor $currentFloor"
//Checks the checked boxes and updates the list
holder.binding.floorCheckBox.setOnCheckedChangeListener { buttonView, isChecked ->
if (buttonView.isChecked) {
Timber.d("${buttonView.text} checked")
checkedFloors.add(buttonView.text.toString())
} else if (!buttonView.isChecked) {
Timber.d("${buttonView.text} unchecked")
checkedFloors.remove(buttonView.text)
}
unique = checkedFloors.distinct().sorted()
Timber.d("List: $unique")
}
}
fun returnList(): List<String> {
Timber.d("$unique")
return unique
}
override fun getItemCount(): Int {
return floors.size
}
#SuppressLint("NotifyDataSetChanged")
fun getAllFloors(floorsReceived: List<String>) {
Timber.d("Floors received : $floorsReceived")
this.floors = floorsReceived
notifyDataSetChanged()
}
}
Fragment code where I am trying to read it
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Chosen Floors
val chosenFloors = floorProfileDialogAdapter.returnList()
Timber.d("Chosen floors : $chosenFloors")
}
Note: The list I am trying to receive is var unique: List<String> = mutableListOf. I tried to get it using the returnList() but the log in that function shows that list is empty. Similarly the Log in fragment shows that it received an empty list.
Edit 1 :
Class to fill the Adapter Floors using getAllFloors()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val floorList: MutableList<String> = mutableListOf()
var profileName: String? = ""
profileName = args.profileName
//Profile name received
Timber.d("Profile name : $profileName")
//Getting list of all floors
createProfileViewModel.totalFloors.observe(viewLifecycleOwner) {
Timber.d("List of floors received : $it")
val intList = it.map(String::toInt)
val maxFloorValue = intList.last()
var count = 0
try {
while (count <= maxFloorValue) {
floorList.add(count.toString())
count++
}
} catch (e: Exception) {
Timber.d("Exception: $e")
}
floorProfileDialogAdapter.getAllFloors(floorList)
Timber.d("Floor List : $floorList")
}
When you first set up your Fragment and create your Adapter, unique is empty:
var unique: List<String> = mutableListOf()
(if you have some checked state you want to save and restore, you'll have to initialise this with your checked data)
In onViewCreated, during Fragment setup, you get a reference to this (empty) list:
// Fragment onViewCreated
val chosenFloors = floorProfileDialogAdapter.returnList()
// Adapter
fun returnList(): List<String> {
return unique
}
So chosenFloors is a reference to this initial entry list. But when you actually update unique in onBindViewHolder
unique = checkedFloors.distinct().sorted()
you're replacing the current list with a new list object. You're not updating the existing list (even though you made it a MutableList). So you never actually add anything to that empty list you started with, and chosenFloors is left pointing at a list that contains nothing, while the Adapter has discarded it and unique holds a completely different object.
The solution there is to make unique a val (so you can't replace it) and just change its contents, e.g.
unique.clear()
unique += checkedFloors.distinct().sorted()
But I don't feel like that's your problem. Like I pointed out, that list is initially empty anyway, and you're grabbing it in your Fragment during initialisation just so you can print out its contents, as though you expect it to contain something at that point. Unless you initialise it with some values, it's gonna be empty.
If you're not already storing/restoring them, you'll need to handle that! I posted some code to do that on another answer so I'll just link that instead of repeating myself. That code is storing indices though, not text labels like you're doing. Indices are much cleaner and avoid errors - the text is more of a display thing, a property of the item the specific (and unique) index refers to. (But you can store a string array in SharedPreferences if you really want to.)
Also you're not actually updating your ViewHolder to display the checked state for the current item in onBindViewHolder. So whatever ViewHolder you happen to have been given (there's only a few of them for the list, they get reused) it's just showing whatever its checkbox was last set to, by you poking at it. Check an item, then scroll the list and see what happens!
So you need to check or uncheck the box so it's correct for the item you're displaying. This is pretty easy if you're storing the checked items by indices:
// explicitly set the checked state, depending on whether the item at 'position' is checked
holder.binding.floorCheckBox.checked = checkedItems[position]
You can work out something similar for your text label approach, but again I wouldn't recommend doing things that way.
I'm currently experimenting with a RecyclerViewer, but stumbled upon a problem: When I update the data and call notifyDataSetChanged, the RecyclerViewer updates it's view, but not with the new data, but rather with the old data.
I've searched through Stackoverflow for the problem, but in most cases the problem is that they either created two instances of the adapter (reference) or that they don't have a layout manager, and I believe that neither of those is my problem.
Here is my code for creating and updating the RecyclerViewer in the fragment in which it's hosted:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_player_list_list, container, false)
data = getPlayersAsList(requireContext(), gameUUID)
if(data.isEmpty()){
data = listOf(SharedPreferencesManager.Companion.Player("John", false))
}
// Set the adapter
if(view is LinearLayout){
view.children.forEach {
if (it is RecyclerView) {
with(it) {
layoutManager = when {
columnCount <= 1 -> LinearLayoutManager(context)
else -> GridLayoutManager(context, columnCount)
}
Log.i("DEBUG", "The first adapter was called")
adapter = MyItemRecyclerViewAdapter(data)
}
}
}
}
fun notifyDataUpdate(position: Int? = null) {
if(view is LinearLayout){
(view as LinearLayout).children.forEach {
if(it is RecyclerView){
data = getPlayersAsList(requireContext(), gameUUID)
Log.i("DATA UPDATE", "Player list is now $data")
it.adapter?.notifyDataSetChanged()
}
}
}
}
And here is the code of the adapter:
import android.util.Log
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import com.chuaat.hideandseek.databinding.FragmentPlayerListBinding
import com.google.android.material.button.MaterialButton
import java.util.*
/**
* [RecyclerView.Adapter] that can display a [PlaceholderItem].
* TODO: Replace the implementation with code for your data type.
*/
class MyItemRecyclerViewAdapter(
private val values: List<SharedPreferencesManager.Companion.Player>
) : RecyclerView.Adapter<MyItemRecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
FragmentPlayerListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = values[position]
Log.i("RECYCLER VIEWER", "SETTING VALUE OF $item")
if(item.isSeeker){
holder.buttonView.setIconResource(R.drawable.ic_baseline_search_24)
}
else{
holder.buttonView.setIconResource(R.drawable.ic_outline_visibility_off_24)
}
holder.contentView.text = item.name
}
override fun getItemCount(): Int = values.size
inner class ViewHolder(binding: FragmentPlayerListBinding) :
RecyclerView.ViewHolder(binding.root) {
val buttonView: MaterialButton = binding.toggleSeekerButton as MaterialButton
val contentView: TextView = binding.content
override fun toString(): String {
return super.toString() + " '" + contentView.text + "'"
}
}
}
The log output I get when calling notifiyDataUpdate is:
I/DATA UPDATE: Player list is now [Player(name=Joe, isSeeker=false)]
I/RECYCLER VIEWER: SETTING VALUE OF Player(name=John, isSeeker=false)
As you can see the updated data is with a Player named Joe, but in onBindViewHolder the only value is the default Player ("John").
What is the problem I'm missing?
You're not actually updating the data in your adapter
Here's how you initialise it:
data = getPlayersAsList(requireContext(), gameUUID)
if(data.isEmpty()){
data = listOf(SharedPreferencesManager.Companion.Player("John", false))
}
...
adapter = MyItemRecyclerViewAdapter(data)
and that parameter in your adapter's constructor is your data source for the RecyclerView
class MyItemRecyclerViewAdapter(
private val values: List<SharedPreferencesManager.Companion.Player>
At this point, your Fragment has a list called data which contains your current data, and the Adapter has a reference to that same list. They're both looking at the same object - let's call it list 1.
Then you update your data in the Fragment, and notify the Adapter:
data = getPlayersAsList(requireContext(), gameUUID)
it.adapter?.notifyDataSetChanged()
But what you've done is create a new list, list 2, with that getPlayersAsList call. You assign that to data. So data points to list 2, the new data - but values in your adapter still points to list 1, the old list. So for the adapter, nothing's changed! It can't see the new data, so even though you notify it, it will still look the same.
You have two options here. Firstly, since you're already using this shared list that the Fragment and Adapter are both looking at, you can just update that list - which is what you should be doing if they're both sharing it, right?
// clear the old data
data.clear()
// replace it with the new items
// this is the same as data.addAll(getPlayersAsList(...))
data += getPlayersAsList(requireContext(), gameUUID)
// now the list the adapter is using has been updated, you can notify it
it.adapter?.notifyDataSetChanged()
This way you're updating the actual list the adapter uses as its dataset, so you'll see the changes when it refreshes.
The second way, and the one I'd recommend, is to completely separate the Adapter's data from anything the Fragment is holding onto. Having a shared list like this can be a source of bugs, where one component changes it in the background, affecting the state of another component. If you ever use a DiffUtil in a RecyclerView for example, mutating the current list will stop it from working, because it won't be able to compare for changes.
You could make the values property a public var and update that externally, then notify the adapter - but honestly, it's better to let the Adapter handle those details internally. A setter function is a lot cleaner to me:
// in the Adapter
private var values = emptyList<SharedPreferencesManager.Companion.Player>()
fun setData(items: List<SharedPreferencesManager.Companion.Player>) {
// as a safety measure, creating a new list like this *ensures* that if the one
// that was passed in is mutated, this internal one won't change. (The items
// in the list can still be mutated of course!)
values = items.toList()
// now the -adapter- can decide on how/if it should update, based on its own
// internal state and the new data. The Fragment shouldn't be concerned with those details
notifyDataSetChanged()
}
Then when you want to update the adapter, just pass it the new data list:
// assuming you still want to keep a local reference to this data (if you don't need it, don't!)
data = getPlayersAsList(requireContext(), gameUUID)
// I'd really recommend just keeping a reference to your adapter when you create it,
// so you don't need to go searching for it and casting it like this
(it.adapter as? MyItemRecyclerViewAdapter)?.setData(data)
To initialise the adapter, you can either use this method:
// seriously, just store this in a `lateinit var adapter: MyItemRecyclerViewAdapter`
adapter = MyItemRecyclerViewAdapter()
adapter.setData(data)
recyclerView.adapter = adapter
Or you could keep the constructor parameter (which we're not using as a property to store the data, remember!) and use it to call the setData function:
class MyItemRecyclerViewAdapter(
data: List<SharedPreferencesManager.Companion.Player> // no val
) : RecyclerView.Adapter<MyItemRecyclerViewAdapter.ViewHolder>() {
init {
setData(data)
}
And just as a hint - the way you're accessing your RecyclerView is complicated and not how you generally do things in Android. Give it an id in your layout XML file (R.layout.fragment_player_list_list) and then just do
val recyclerView = view.findViewById<RecyclerView>(R.id.whatever)
That's it! No need to loop through the hierarchy searching for it. If you store your Adapter in a variable, you probably won't need to touch the RV itself after setting it up - just call setData on your adapter reference
I asked a similar question here. But I feel I didn't express myself vividly. So I'm elaborating.
I want to load textView and imageView (from strings & drawables) on the fragment that is navigated to, from recyclerView.
Please note I am using navigation component.
I am kinda new to this, but I'm done some reach and find out that I could used intent, and just navigate to an activity and use intent putExtra to load whatever I want to load, but I am using single activity and mutiple fragment model. So I can't do this.
I have been at this for over a week, so I'd really appreciate any help. Even if it's just the name of the tool I need to use.. But I wouldn't mind some code, thanks a lot for your help, in advance.
I don't know what info you will need, so I'll just add what I feel is important. I will provide any info if requested thanks.
Here's the recyclerView Adapter, that navigate into the fragment:
class StoriesRecyclerAdapter(private val storiesList: ArrayList<StoriesDataClass>) : RecyclerView.Adapter<StoriesRecyclerAdapter.ViewHolder>() {
// create new views
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
...
return ViewHolder(view)
}
// binds the list items to a view
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
...
// navigates to stories content destination
holder.storiesCardView.setOnClickListener {
val action = StoriesFragmentDirections.actionNavigationStoriesToStoriesContentFragment()
holder.itemView.findNavController().navigate(action)
}
}
// return the number of the items in the list
override fun getItemCount(): Int {
...
}
// Holds the views for adding it to image and text
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val storiesCardView : CardView = itemView.findViewById(R.id.stories_cardView)
...
}
}
Please inform me if you need any more info, I sinerely thank you for your time and feedback.
you should use safe args navigaiton component
check this link out its pretty easy to work
https://developer.android.com/guide/navigation/navigation-pass-data
you can use intent.extra to pass data
findNavController(R.id.main_content).setGraph(R.navigation.<you_nav_graph_xml>, intent.extras)
or bundle to pass data
val bundle = Bundle()
bundle.putString("some_argument", "some_value")
navController.setGraph(R.navigation.<you_nav_graph_xml>, bundle)
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.
I am building an app where user is required to fill some data in order to post something, so a fragment consists of EditText, radio buttons and Spinner along with RecyclerView which dynamically renders a number of child layout containing TextView and EditText.
So when user select category from Spinner, some properties which are related to that category are displayed in RecyclerView and user can optionally fill some of them.
I have tried to implement this functionality using callback and TextWatcher but I don't get the values I want.
CallBack
interface PropertiesCallback {
fun addProp(position: Int, title: String, value: String)
}
Adapter
class PropertiesAdapter(private val propertiesCallback: PropertiesCallback)
: RecyclerView.Adapter<PropertiesAdapter.ViewHolder>() {
private var list = listOf<CategoriesAndSubcategoriesQuery.Property>()
fun setData(listOfProps: List<CategoriesAndSubcategoriesQuery.Property>) {
this.list = listOfProps
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.z_property_input, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int = list.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(list[position], position)
}
inner class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
private val label: TextView = view.findViewById(R.id.label)
private val input: EditText = view.findViewById(R.id.input)
fun bind(prop: CategoriesAndSubcategoriesQuery.Property, position: Int) {
label.text = prop.title()
prop.hint()?.let { input.hint = prop.hint() }
input.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
propertiesCallback.addProp(position, prop.title(), input.text.toString())
}
})
}
}
}
In Fragment
private var propertiesList = mutableListOf<CategoriesAndSubcategoriesQuery.Property>()
private var propertiesInputList = mutableListOf<ProductPropertiesInput>()
private fun setUpSubcategorySpinner() {
subcategoriesAdapter = ArrayAdapter(
this#AddProductFragment.context!!,
android.R.layout.simple_spinner_item,
subcategoriesList
)
//Subcategories
subcategoriesAdapter.setDropDownViewResource(android.R.layout.simple_dropdown_item_1line)
subcategory_spinner.adapter = subcategoriesAdapter
subcategory_spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
subcategoryId = subcategoriesList[position].id()
//Adding properties
subcategoriesList[position].properties()?.let {
//Clear previous properties data of another subcategory.
propertiesInputList.clear()
propertiesList.clear()
propertiesList.addAll(it)
propertiesAdapter.setData(propertiesList)
propertiesAdapter.notifyDataSetChanged()
}
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
}
overide
override fun addProp(position: Int, title: String, value: String) {
val prop = ProductPropertiesInput
.builder()
.title(title)
.value(value)
.build()
propertiesInputList.add(prop)
//Log.d(TAG, "prop: ${prop.title()} : ${prop.value()}")
}
submit fun
private fun submitProduct() {
//Initializing properties.
val properties: Any
//The keys needed in final list.
val propertyKeys = propertiesList.map { it.title() }
//Removing objects which keys are not needed.
propertiesInputList.removeAll { it.title() !in propertyKeys }
Log.d(TAG, "propertiesInputList: $propertiesInputList")
//Removing duplicate and assign result in properties var.
properties = propertiesInputList
.distinctBy { it.title() }
Log.d(TAG, "properties: $properties")
for (prop in properties) {
Log.d(TAG, "properties , title: ${prop.title()}, value: ${prop.value()} ")
}
}
Above codes is intended to work as. When user types a value in one of the EditText in RecyclerView the value will be taken to fragment and added to an object which takes title and value and then added to propertiesInputList.
Problem 1: propertiesInputList will have so many duplicates objects with the same title and I thought the best solution was using distinctBy.
Problem 2: When user fills a number of EditText which are related to let's say category1 and changes his mind and select another category from Spinner. The previous values which are not part of new chosen category remain in propertiesInputList list. So I thought the best solution was to clear propertiesInputList and using removeAll with the titles related to category to filter unwanted objects.
But now I get only the first letter user types. If user types shoes I get s. So it seems distinctBy returns the first object but I want to get exactly last word user typed and if the user typed and erased everything I want blank.
Is there a better solution to handle this? Like looping recyclerView only when user press submit instead of TextWatcher? Or which part should I fix to make this work?
I don't completely understand what you are trying to achieve here. EditTexts inside a RecyclerView is generally not a good idea for following reasons.
When the recyclerView is scrolled, you would want to preserve the
text added by the user for that particular field/item and show it
correctly when the user scrolls back.
When you add a TextWatcher to an EditText, you also need to remove it when the view is recycled or the view holder is bound again. Otherwise, you will end up with multiple listeners and things will go wrong.
For the other question that you have,
But now I get only the first letter user types. If user types shoes I get s
That's by design. TextWatcher would emit event every time a character is entered. So you would get s, sh, sho, shoe, shoes. So you can not take an action on this data because the user is still adding something to that field.
So,
You don't know when the user has stopped adding the text to the EditText (or whether user is done). You could use something like debounce but that is complicated. You should give a button to the user. Take the value when the user taps the button.
I am assuming you have multiple edittexts in the RecyclerView. So you would need to store the values for each edittext because the recyclerview will re-use the views and you'll lose the data. You could do that in your adapter's onViewRecycled callback. Keep a map of id -> string where you store this data and retrieve when the view holder is bound.
You could also use a TextWatcher but you would have detach it before attaching a new one or in onViewRecycled.
Update:
If I had something like this, I would use a ScrollView with a vertical LinearLayout (for simplicity) and add EditText based on the requirements. If you want to add TextWatcher, you'd need some kind of stable id.
class EditTextContainer : LinearLayout {
private val views = mutableListOf<EditText>()
private val textWatchers = hashMapOf<Int, TextWatcher>()
... constructor and bunch of stuff
fun updateViews(items: List<Item>, textCallback: (id, text) -> Unit) {
// Remove text watchers
views.forEach { view ->
view.removeTextWatcher(textWatchers[view.id])
}
// More views than required
while (views.size > items.size) {
val view = views.removeAt(views.size-1)
removeView(view)
}
// Less views than required
while (view.size < items.size) {
val view = createView()
view.id = View.generateViewId()
addView(view, createParams()) // Add this view to the container
views.add(view)
}
// Update the views
items.forEachIndexed { index, item ->
val editText = views[item]
// Update your edittext.
addTextWatcher(editText, item.id, textCallback)
}
}
private fun createView(): EditText {
// Create new view using inflater or just constructor and return
}
private fun createParams(): LayoutParams {
// Create layout params for the new view
}
private fun addTextWatcher(view, itemId, textCallback) {
val watcher = create text watcher where it invokes textCallback with itemId
view.addTextWatcher(watcher)
textWatchers[view.id] = watcher
}
}
Your inputs are less to identify the issue. I guess you are making some data collection application with the list of edit text.
There is a an issue when you were using the edit text in recycler list.
When you scroll down the bottom edit text in the recycler view will be filled with already filled edit text value, even though you user is not filled.
As a work around You can create some sparse array any data structure which will best suitable for you, that can map you position and value
like
mPropertyValue[] = new String [LIST_SIZE]. , assuming that position of ur list item matches with index of array.
Try updating the index with the value of text watcher
mPropertyValue[POSITION] = YOUR_EDIT_TEXT_VALUE
When you want to initialize your edit text use the value by mPropertyValue[POSITION]
You can always make sure that your edit text will be having the right value by this .
i face like this problem in my java code and that was the solution
for (int i = 0; i < list.size(); i++) {
(put her the getter and setter class) mylist = list.get(i);
//use the getter class to get values and save them or do what ever you want
}