I have a custom adapter, and filter that I am currently implementing to filter a recycler view based on a simple substring search on my recycler view entries. Here is my adapter NotifyChanged() function, which updates the RecylerView, and my custom filter() function. Everything works great, except for the auto scrolling afterwards.
private fun notifyChanged() {
val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return objects.size
}
override fun getNewListSize(): Int {
return temp.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return this#DiffRecyclerViewAdapter.areItemsTheSame(objects[oldItemPosition], temp[newItemPosition])
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return this#DiffRecyclerViewAdapter.areContentsTheSame(objects[oldItemPosition], temp[newItemPosition])
}
})
objects.clear()
objects.addAll(temp)
result.dispatchUpdatesTo(this)
}
fun filter(text : String){
val ob = original_objects as ArrayList<Category>
val filtered_categories = ArrayList<T>() as ArrayList<Category>
for (category in ob){
//val temp_category = category
val list_of_subcategories = ArrayList<T>() as ArrayList<Category>
for (subcategory in category.categories){
val name_of_category = subcategory.name.toLowerCase()
if (name_of_category.contains(text)){
list_of_subcategories?.add(subcategory)
}
}
if (list_of_subcategories.size > 0){
val newCategory = Category(category.id,category.name,category.description,category.videos,list_of_subcategories)
filtered_categories.add(newCategory)
}
}
temp = filtered_categories as MutableList<T>
notifyChanged()
}
In my SearchActivity.kt I have the following listener:
searchEditText.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) {
adapter.filter(s.toString())
recyclerView.scrollToPosition(0)
}
})
I was looking through the source code of DiffUtil and notifyDataSetChanged() to see how scrolling after filtering works. But didn't have much luck. The whole problem is that after I search text the RecyclerView is filtered fine. But will scroll to inconsistent locations. I want it to scroll back to the top every single time, but this isn't happening. EVEN WITH scrollToPosition(0) it will USUALLY scroll to the top, but not always.
I thought scrolling to the top was typically automatic in this case. I'm curious as to what the best practice is in updating and scrolling.
It needs some time to update the data on recyclerview. Mean while you are trying to scroll which doesn't work in most of the cases. PReferably use a postDelayed with 200 or 300 milliseconds before scrolling
Ex:
new Handler.postDelayed(new Runnable(){
#Override
public void run(){
recyclerView.scrollToPosition(0)
}
}, 300);
Related
I am new to kotlin-android and I am developing a stock keeping unit, where in one fragment i need to load the data into recycler view using arraylist but also, i need one blank item, where user can enter his own data and once he fills that blank item, a new blank item should be created.
class RecyclerViewAdapterU (val dataList:ArrayList<ModelClass>): RecyclerView.Adapter<RecyclerViewAdapterU.ViewHolder>() {
var _binding: UploadItemViewBinding? = null
val binding get() = _binding!!
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerViewAdapterU.ViewHolder {
val v =
LayoutInflater.from(parent.context).inflate(R.layout.upload_item_view, parent, false)
_binding = UploadItemViewBinding.bind(v)
return ViewHolder(binding.root)
}
fun getUpdatedDetails(skucode:String,pos:Int){
// val skuDetails:ArrayList<ModelClass>
val call: Call<List<ModelClass>>? =
ApiClient.instance?.myApi?.getfromsku(skucode)!!
call!!.enqueue(object : Callback<List<ModelClass>?> {
override fun onResponse(
call: Call<List<ModelClass>?>,
response: Response<List<ModelClass>?>
) {
if (!response.body().isNullOrEmpty()) {
val skuDetails: ArrayList<ModelClass> = response.body() as ArrayList<ModelClass>
//
// if (!skuDetails.isNullOrEmpty()) {
val x = dataList[pos].sku_code
if (skuDetails[pos].sku_code != x) {
//for (i in skuDetails.indices){
println(skuDetails[pos].sku_code)
println(".........$pos")
dataList.removeAt(pos)
dataList.add(pos, skuDetails[0])
skuDetails.clear()
notifyItemChanged(pos)
//}
}
}
}
override fun onFailure(call: Call<List<ModelClass>?>, t: Throwable) {
}
})
}
override fun onBindViewHolder(holder: ViewHolder, #SuppressLint("RecyclerView") position: Int) {
bindItems(dataList[position])
holder.getStock()
holder.updateStockDetail()
}
fun bindItems(data: ModelClass) {
binding.apply {
itemquant.text=data.item_quant
uploadItemName.text = data.item_name
uploadMfg.text = data.mfg
skuStock.setText(data.item_stock.toString())
skuCode.setText(data.sku_code)
}
}
override fun getItemCount(): Int {
return dataList.size
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun getStock() {
binding.skuStock.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
if (binding.skuStock.isFocused){
for (i in 0 until RecyclerViewAdapter.ob.dataSelected.size){
if (editable.toString().trim()!=""){
var x= editable.toString().trim().toInt()
RecyclerViewAdapter.ob.dataSelected[adapterPosition].item_stock=x
//getting current itemstock before pushing update.
//assigning latest itemstock to the data for the update
}
}
}}
})
}
fun updateStockDetail(){
binding.skuCode.addTextChangedListener(object : TextWatcher{
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun afterTextChanged(editable: Editable) {
//binding.skuCode.removeTextChangedListener(this)
var pos:Int=adapterPosition
var x= editable.toString().trim()
//RecyclerViewAdapter.ob.dataSelected[adapterPosition].sku_code=x
println("$x in textwatcher and $pos")
//getting edited text and calling the function to get updated details.
getUpdatedDetails(x,pos)
// binding.skuCode.addTextChangedListener(this)
}
})
}
}
}
I have couple of bugs in the adapter too, for example, i cant change textwatcher more than once.
If somebody could help me with this, would be very glad.
ob.data.selected comes from another recyclerview adapter.
One way you can do this is by using a different view type for the blank item that will always stay at the very top (or bottom if you prefer). Once the blank item is filled up then a new non-blank item with a different view type will be added to your list.
More info about view types here: Android and Kotlin: RecyclerView with multiple view types
I have been working on a stock keeping application as a demo to learn kotlin and android studio, I have added textchangedlistener in a recycler view item, which everytime it text is changed, gets information from Api and displays new item on textview. This works good for only one time, After that it keeps infinitely changing. please check video for better understanding : ScreenRecording .
Here is my rcv adapter :
class RecyclerViewAdapterU (val dataList:ArrayList<ModelClass>): RecyclerView.Adapter<RecyclerViewAdapterU.ViewHolder>() {
var _binding: UploadItemViewBinding? = null
val binding get() = _binding!!
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerViewAdapterU.ViewHolder {
val v =
LayoutInflater.from(parent.context).inflate(R.layout.upload_item_view, parent, false)
_binding = UploadItemViewBinding.bind(v)
return ViewHolder(binding.root)
}
override fun onBindViewHolder(holder: ViewHolder, #SuppressLint("RecyclerView") position: Int) {
bindItems(dataList[position])
holder.getStock()
holder.updateStockDetail()
}
fun bindItems(data: ModelClass) {
binding.apply {
itemquant.text=data.item_quant
uploadItemName.text = data.item_name
uploadMfg.text = data.mfg
skuStock.setText(data.item_stock.toString())
skuCode.setText(data.sku_code)
}
}
fun getUpdatedDetails(skucode:String,pos:Int){
val call: Call<List<ModelClass>>? =
ApiClient.instance?.myApi?.getfromsku(skucode)!!
call!!.enqueue(object : Callback<List<ModelClass>?> {
override fun onResponse(
call: Call<List<ModelClass>?>,
response: Response<List<ModelClass>?>
) {
val skuDetails=response.body()
if (skuDetails != null) {
dataList.removeAt(pos)
for (i in skuDetails){
println(i.item_name)
dataList.add(pos,i)
}
notifyItemChanged(pos)
}
}
override fun onFailure(call: Call<List<ModelClass>?>, t: Throwable) {
}
})
}
override fun getItemCount(): Int {
return dataList.size
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun getStock() {
binding.skuStock.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
for (i in 0 until RecyclerViewAdapter.ob.dataSelected.size){
if (editable.toString().trim()!=""){
var x= editable.toString().trim().toInt()
RecyclerViewAdapter.ob.dataSelected[adapterPosition].item_stock=x
//getting current itemstock before pushing update.
//assigning latest itemstock to the data for the update
}
}
}
})
}
fun updateStockDetail(){
binding.skuCode.addTextChangedListener(object : TextWatcher{
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun afterTextChanged(editable: Editable) {
var x:String=""
var pos:Int=adapterPosition
for (i in 0 until RecyclerViewAdapter.ob.dataSelected.size){
if (editable.toString().trim()!=""){
x=editable.toString().trim()
//RecyclerViewAdapter.ob.dataSelected[adapterPosition].sku_code=x
println("$x in if")
}
}
//println(RecyclerViewAdapter.ob.dataSelected[adapterPosition].sku_code)
//getting edited text and calling the function to get updated details.
getUpdatedDetails(x,pos)
binding.skuStock.removeTextChangedListener(this)
}
})
}
}
}
If possible please review my code and let me know what are things i need to work on.
Note: ob.dataselected is a global variable from another recyclerview adapter.
Textchangedlistener i am talking about is in the fun updateStockDetail()
In the documentation for afterTextChanged we see the following:
This method is called to notify you that, somewhere within s, the text has been changed. It is legitimate to make further changes to s from this callback, but be careful not to get yourself into an infinite loop, because any changes you make will cause this method to be called again recursively.
(The emphasis is mine.) I don't see where you remove the text watcher before changing the text, so you may be in an infinite loop.
To remove the text watcher use removeTextChangedListener. You can replace the text watcher after making changes.
I'm having this problem and spent hours exploring different solutions found here but couldn't figure it out. I have a RecyclerView with a RadioGroup (with two RadioButton) and an EditText. As expected, the text keeps getting duplicated on scroll and the "original" gets deleted. The same happens with the radio buttons. I've tried to save on another array backup the values when the view is recycled but couldn't solve the duplicating issue.
Here's my adapter
class ServicesCheckoutAdapter(var context: Context,
var servicesList: List<Service>) : RecyclerView.Adapter<ServicesCheckoutAdapter.ViewHolder>() {
private lateinit var onRadioGroupClickListener: OnRadioGroupClickListener
private lateinit var onTextChangedListener: OnTextChangedListener
private lateinit var onServiceClickListener: OnServiceClickListener
private var externalArray = mutableListOf<String>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.services_list_item,
parent, false)
val viewHolder = ViewHolder(view)
val position = viewHolder.adapterPosition
view.setOnClickListener {
if (onServiceClickListener != null) {
onServiceClickListener.onServiceClick(view, servicesList[position].id, position)
}
}
return viewHolder
}
override fun getItemId(position: Int): Long {
return super.getItemId(position)
}
override fun getItemViewType(position: Int): Int {
return super.getItemViewType(position)
}
interface OnServiceClickListener {
fun onServiceClick(view: View, serviceId: Int, position: Int)
}
fun setOnServiceClickListener(listener: OnServiceClickListener)
{
onServiceClickListener = listener
}
interface OnRadioGroupClickListener {
fun onRadioGroupClick(buttonId: Int, serviceId: Int, position: Int) {}
}
fun setOnRadioButtonClickListener(listener: OnRadioGroupClickListener) {
onRadioGroupClickListener = listener
}
interface OnTextChangedListener{
fun onTextChanged(position: Int, text: String)
}
fun setOnTextChangedListener(listener: OnTextChangedListener){
onTextChangedListener = listener
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
Log.d("recycler", "lista: ${servicesList[position].serviceSolution}")
holder.edtSolution.removeTextChangedListener(holder.watcher)
holder.bind(context,
servicesList[position].id,
servicesList[position].name,
servicesList[position].serviceSolved,
servicesList[position].serviceSolution,
onRadioGroupClickListener,
onTextChangedListener,
position)
}
override fun getItemCount(): Int {
return servicesList.size
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var radioGroup = itemView.findViewById<RadioGroup>(R.id.radioGroupService)
val edtSolution = itemView.findViewById<EditText>(R.id.editTextCheckoutDesc)
var watcher: TextWatcher? = null
fun bind(context: Context,
serviceId: Int,
serviceName: String,
serviceSolved: Boolean,
serviceSolution: String,
onRadioGroupClickListener: OnRadioGroupClickListener,
onTextChangedListener: OnTextChangedListener,
position: Int
) {
itemView.findViewById<TextView>(R.id.serviceTitle)
.text = context.resources
.getString(R.string.service_title_comma, serviceName)
itemView.findViewById<RadioGroup>(R.id.radioGroupService)
.setOnClickListener {
onRadioGroupClickListener
.onRadioGroupClick(
(it as RadioGroup).checkedRadioButtonId, serviceId, adapterPosition)
}
if (serviceSolved) {
radioGroup.find<RadioButton>(R.id.radioBtnYes).isChecked = true
radioGroup.find<RadioButton>(R.id.radioBtnNo).isChecked = false
} else {
radioGroup.find<RadioButton>(R.id.radioBtnYes).isChecked = false
radioGroup.find<RadioButton>(R.id.radioBtnNo).isChecked = true
}
edtSolution.addTextChangedListener(object: TextWatcher{
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
onTextChangedListener.onTextChanged(position, s.toString())
}
override fun afterTextChanged(s: Editable?) {
}
})
}
}
}
And here's the adapter initialization on the activity
serviceList = occurrence.services
servicesAdapter = ServicesCheckoutAdapter(this, serviceList)
recyclerViewServices.adapter = servicesAdapter
recyclerViewServices.layoutManager = LinearLayoutManager(this)
servicesAdapter.setOnRadioButtonClickListener(object : ServicesCheckoutAdapter.OnRadioGroupClickListener {
override fun onRadioGroupClick(buttonId: Int, serviceId: Int, position: Int) {
super.onRadioGroupClick(buttonId, serviceId, position)
when (buttonId) {
R.id.radioBtnYes -> {
serviceList[position].serviceSolved = true
servicesAdapter.notifyDataSetChanged()
}
R.id.radioBtnNo -> {
serviceList[position].serviceSolved = false
servicesAdapter.notifyDataSetChanged()
}
}
}
})
servicesAdapter.setOnTextChangedListener(object : ServicesCheckoutAdapter.OnTextChangedListener{
override fun onTextChanged(position: Int, text: String) {
serviceList[position].serviceSolution = text
}
})
RecyclerView isn't usually made for Input views. As Android saves the input of a view depending on its ID, your RecyclerView has X number of views that all have the same ID, hence they all use the same state.
A solution to this would be saving your input state manually in recycle method, and restoring it in bind method.
So if you have 20 items, you have 20 states. Initially they are all empty or defaulted, and change when needed to save.
A more simpler approach would be using a LinearLayout or similar Layouts to accomplish your goal, but that depends on how many items you have and how many views that will contain.
You still need to be careful for the View ID part, though
I've tried to use a ListView but the keyboard went crazy changing focus. Decided to keep the RecyclerView and since my dynamic list isn't so large, I used the
holder.setIsRecyclable(false);
on the OnBindViewHolder() method and it solved my issue. Thanks everyone for the help
I'm trying to implement search functionality in my recyclerview using room and a viewmodel.
The functionality works just fine while searching. However if I empty the search string the recyclerview does not scroll to the top of the complete list.
Below the code:
ViewModel
#HiltViewModel
class MyViewModel #Inject constructor(repository: MyRepository) :
ViewModel() {
private var nameFilter = MutableLiveData<String>("")
val allPosts : LiveData<List<Post>> = Transformations.switchMap(nameFilter) { name ->
if (name.isEmpty()) {
repository.getAllPosts()
} else {
repository.getPostByName(name)
}
}
fun getPostByName(name: String) {
nameFilter.postValue(name)
}
}
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val listAdapter = MyListAdapter()
binding.RV.apply {
adapter = listAdapter
layoutManager = GridLayoutManager(context, 2)
// Just for spacing
addItemDecoration(RecyclerViewCustomItemDecorator(8, 8))
}
binding.searchRV.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(editable: Editable?) {
viewModel.getPostByName(editable.toString())
}
})
viewModel.allPosts.observe(viewLifecycleOwner) {
listAdapter.submitList(it)
}
}
The Recyclerview adapter implements the diffUtil logic.
Hope anyone can help! Thanks
In recycler view there is a property to scroll on top when user is bottom of scroll
mRecyclerView.smoothScrollToPosition(0);
SmoothScrollToPosition help in smoothly scroll on top you can also use mRecyclerView.layoutManager?.scrollToPosition(0)
If we use scroll view then use
scrollView.fullScroll(ScrollView.FOCUS_UP);.
In My code I am getting total 214 countries from api which I have displayed on Recycler View. When I click on the country flag I am getting a Toast with country position (Getting correct position without searching). But when I search any country from search option and then I click on the flag I am getting wrong position [When the country is searched the Searched country comes on the top and it takes that position]. I don't know why it is so and what mistake I am doing in code??
For Example, Let assume 4 countries. At pos 0 - India, pos 1 - Germany, pos 2 - Pakistan, pos 3 - France.
Now this four countries has been displayed on Recycler View then onClick of India it displays position 0 and when I click on Germany it will display position 1 (without searching). But when I search France from the list and I click on France after searching it should display position 3 but it is not.
My CustomAdapter.kt for RecyclerView .
class CustomAdapter(public val context: Context, private val countriesResponse: CountriesResponse) :
RecyclerView.Adapter<CustomAdapter.MyViewHolder>(), Filterable {
var country : ArrayList<CountriesResponseItem>
var list_country: ArrayList<CountriesResponseItem>
internal var mFilter: NewFilter
override fun getFilter(): Filter {
return mFilter
}
init {
list_country = getCountries()
country = ArrayList()
country.addAll(list_country)
mFilter = NewFilter(this#CustomAdapter)
}
fun getCountries(): ArrayList<CountriesResponseItem> {
val list_country = arrayListOf<CountriesResponseItem>()
for (list1 in countriesResponse)
list_country.add(list1)
return list_country
}
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
var image: ImageView
var cv_tv_country_name: TextView
init {
image = itemView.cv_iv_country_flag
cv_tv_country_name = itemView.cv_tv_country_name
image.setOnClickListener(this)
}
override fun onClick(v: View?) {
val intent =
Intent(v?.context, CountryWiseDataActivity::class.java).putExtra("position", position)
v?.context?.startActivity(intent)
//Toast when clicked on Item
Toast.makeText(v?.context,"Item Clicked at " + getPosition(),Toast.LENGTH_SHORT).show()
}
}
val VIEW_TYPE = 1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
var itemView = LayoutInflater.from(context).inflate(R.layout.flag_list, parent, false)
return MyViewHolder(itemView)
}
override fun getItemCount(): Int {
return country.size
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
Glide.with(context).load(country[position].countryInfo.flag).into(holder.image)
holder.cv_tv_country_name.text = country[position].country
}
inner class NewFilter(var customAdapter: CustomAdapter) : Filter() { <-- Search Filter
override fun performFiltering(constraint: CharSequence?): FilterResults {
country.clear()
val results = FilterResults()
if (constraint!!.isEmpty()) {
country.addAll(list_country)
} else {
val filterPattern = constraint.toString().toLowerCase().trim() { it <= ' ' }
for (list_country1 in 0..list_country.size) {
if (list_country[list_country1].country.toLowerCase()
.contains(filterPattern)
) {
country.add(list_country[list_country1])
}
}
}
results.values = country
results.count = country.size
return results
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
notifyDataSetChanged()
}
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
}
In Activity I have done as below for search filter
search_countries.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) {
countryAdapter.getFilter().filter(s.toString())
}
})
I think you should properly learn the recycler view core concept first. Here Recyclerview is not creating 214 view holders.
If the view is displaying list positions 0 through 3, the RecyclerView creates and binds those view holders, and might also create and bind the view holder for position 4. That way, if the user scrolls the list, the next element is ready to display.
So the main concept here is recycling previous views. After searching for any item it again reorganizes its items & gives you its current position. That's why you are getting a random position.
To learn visit here