I'm trying to implement instant search by address like in Google or HERE Maps using HERE library.
I need to show suggestions when user types several words sequentially even if user miss commas, dots, etc. Though request with missing punctuation works I can't figure out how could I display suggestions returned by request.
I tried to use AutoCompleteTextView, but it works only for first word, when I enter one more word it stops working.
Also I tried to use floatingsearchview (arimorty) library, but it seems not to be working with androidx. I called swapSuggestions(suggestions) on focus, but it works only once in a fragment, though in an activity it works fine.
I solved it, as suggested, by applying custom adapter. But this adapter is analogy of version from Google for Google Place API. Thanks for this guide
Extension function
First of all you need to add this extension function for TextAutoSuggestionRequest class to convert callbacks into coroutine for using it like syncronized code
suspend fun TextAutoSuggestionRequest.await(): MutableList<AutoSuggest> {
return suspendCoroutine { continuation ->
execute { suggestions, errorCode ->
if (errorCode == ErrorCode.NONE) {
continuation.resume(suggestions)
} else {
continuation.resumeWithException(Exception("Error code: $errorCode"))
}
}
}
}
locationServicesAddress()
Then add this converter. I converted geo coordinates to text via standart location services instead of Here GeoCoder because I don't like how it returns address.
fun locationServiceAddress(context: Context, coordinate: GeoCoordinate): String {
val googleGeoCoder = Geocoder(context)
val addresses = googleGeoCoder.getFromLocation(coordinate.latitude, coordinate.longitude, 1)
return addresses[0].getAddressLine(0)
}
Though you could use Here GeoCoder with another extension function for simplicity sake:
suspend fun ReverseGeocodeRequest.await(): String {
return suspendCoroutine { continuation ->
execute { location, errorCode ->
if (errorCode == ErrorCode.NONE) {
continuation.resume(location.address.text)
} else {
continuation.resumeWithException(Exception("Error code: $errorCode"))
}
}
}
}
SuggestionsAdapter.kt
Add this adapter
Note that if you try to return object : Filter() {} in getFilter() it won't work properly because requests will stack in that object instead of interrupting (recreating the class)
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Filter
import android.widget.TextView
import androidx.lifecycle.MutableLiveData
import com.here.android.mpa.common.GeoCoordinate
import com.here.android.mpa.search.AutoSuggestPlace
import com.here.android.mpa.search.TextAutoSuggestionRequest
import kotlinx.coroutines.*
import timber.log.Timber
data class AddressItem(val coordinate: GeoCoordinate, val addressText: String)
class SuggestionsAdapter(context: Context, private val resourceId: Int, private val coordinate: GeoCoordinate) : ArrayAdapter<AddressItem>(context, resourceId, ArrayList<AddressItem>()) {
companion object {
private val _isFetching = MutableLiveData<Boolean>()
val isFetching: LiveData<Boolean>
get() = _isFetching
}
private var suggestions = ArrayList<AddressItem>()
private val customFilter = CustomFilter()
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var view = convertView
if (view == null) {
view = LayoutInflater.from(parent.context!!).inflate(resourceId, parent, false)
}
val item = getItem(position)
if (item != null) {
val addressText = view!!.findViewById<TextView>(R.id.item_address_text)
addressText.text = item.addressText
}
return view!!
}
override fun getItem(position: Int): AddressItem? {
return try {
suggestions[position]
} catch (e: Exception) {
Timber.d("Item is NULL")
null
}
}
override fun getCount(): Int {
return suggestions.size
}
override fun getItemId(position: Int) = position.toLong()
override fun getFilter(): Filter = customFilter
inner class CustomFilter : Filter() {
override fun convertResultToString(resultValue: Any?): CharSequence {
if (resultValue != null) {
val address = resultValue as AddressItem
return address.addressText
}
return "" // if item is null
}
override fun performFiltering(prefix: CharSequence?): FilterResults {
val results = FilterResults()
val suggestions = ArrayList<AddressItem>()
if (prefix == null || prefix.isEmpty()) {
results.values = ArrayList<AddressItem>()
results.count = 0
} else {
val request = TextAutoSuggestionRequest(prefix.toString()).setSearchCenter(coordinate)
Timber.d("Start perform filtering")
runBlocking {
Timber.d("Blocking coroutine scope started")
withContext(Dispatchers.Main) {
isFetching.value = true
}
// Get places on IO thread
val requestResult = withContext(Dispatchers.IO) {
Timber.d("Getting places on IO thread")
request.await()
}
var i = 0
for (place in requestResult) {
i++
// If there are more than 10 suggestions break the loop because the more addresses found the more time need to process them to a string
if (i == 10) {
break
}
if (place is AutoSuggestPlace) {
val item = withContext(Dispatchers.IO) {
AddressItem(place.position, locationServiceAddress(context, place.position))
}
suggestions.add(item)
}
}
Timber.d("Blocking coroutine scope finished")
withContext(Dispatchers.Main) {
isFetching.value = false
}
results.apply {
values = suggestions
count = suggestions.size
}
Timber.d("Filtered results: ${suggestions}")
}
}
return results
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
try {
if (results?.count!! > 0 && results?.values != null) {
suggestions = results.values as ArrayList<AddressItem>
notifyDataSetChanged()
} else {
suggestions = ArrayList()
notifyDataSetInvalidated()
}
} catch (e: Exception) {
Timber.d("Caught exception: ${e.message}")
}
}
}
}
and set it in Here SupporMapFragment.init() callback (if Error.NONE) like so
val adapter = SuggestionsAdapter(context!!, R.layout.item_address, map.center)
binding.searchBox.setAdapter(adapter)
Then you could observe isFetching to reflect loading state
The Adapter for your AutoCompleteTextView only filters elements that start with the input entered by the user. You need to modify the ArrayAdapter class in order for it to work.
public class AutoSuggestAdapter extends ArrayAdapter {
private Context context;
private int resource;
private List<String> tempItems;
private List<String> suggestions;
public AutoSuggestAdapter(Context context, int resource, int item, List<String> items) {
super(context, resource, item, items);
this.context = context;
this.resource = resource;
tempItems = new ArrayList<>(items);
suggestions = new ArrayList<>();
}
#Override
public Filter getFilter() {
return nameFilter;
}
Filter nameFilter = new Filter() {
#Override
public CharSequence convertResultToString(Object resultValue) {
String str = (String) resultValue;
return str;
}
#Override
protected FilterResults performFiltering(CharSequence constraint) {
if (constraint != null) {
suggestions.clear();
for (String names : tempItems) {
if (names.toLowerCase().contains(constraint.toString().toLowerCase())) {
suggestions.add(names);
}
}
FilterResults filterResults = new FilterResults();
filterResults.values = suggestions;
filterResults.count = suggestions.size();
return filterResults;
} else {
return new FilterResults();
}
}
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
List<String> filterList = (ArrayList<String>) results.values;
if (results != null && results.count > 0) {
clear();
for (String item : filterList) {
add(item);
notifyDataSetChanged();
}
}
}
};
Note that the important line is here:
names.toLowerCase().contains(constraint.toString().toLowerCase())
This way it will search for a string that contains the string in the input. By default it's startsWith() intead of contains()
Related
I started the debugger and my breakpoints show that on the first startup the dataset is being transmitted as the adapter is being initialized, but as soon as I call the filter it doesn't have any data to work with and my recyclerview just stays as it was before.
All this code worked before, I just translated it from Java to Kotlin. I must have messed something up, but I can't find it.
Adapter Class
class ClothingListAdapterKt(
private val dataSetIn: MutableList<Clothing>,
private val listener: ClothingListAdapterKt.OnItemClickListener,
private val context: Context
) :
RecyclerView.Adapter<ClothingListAdapterKt.ViewHolder>(), Filterable {
private var lastPosition = -1
var dataSet = mutableListOf<Clothing>()
var dataSetFiltered = mutableListOf<Clothing>()
init {
dataSet = dataSetIn
dataSetFiltered = dataSet
}
override fun onBindViewHolder(viewHolder: ClothingListAdapterKt.ViewHolder, position: Int) {
val currentClothing: Clothing = dataSetFiltered[position]
// Get element from your dataset at this position and replace the
// contents of the view with that element
//SETTING MY VIEWS, ONLY COMMENTED OUT FOR THIS QUESTION
}
setAnimation(viewHolder.itemView, position);
}
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults? {
val charString: String = constraint.toString()
if (charString.isEmpty()) {
dataSetFiltered.addAll(dataSet)
} else {
val dataSetTemp: MutableList<Clothing> = mutableListOf()
for (row in dataSet) {
if (row.name.lowercase().contains(charString.lowercase())) {
dataSetTemp.add(row)
}
}
dataSetFiltered = dataSetTemp
}
val filterResults = FilterResults()
filterResults.values = dataSetFiltered
return filterResults
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
dataSetFiltered = results?.values as MutableList<Clothing>
notifyDataSetChanged()
}
}
}
Adapter Init
recylcerViewClothing.adapter = ClothingListAdapterKt(clothing, listener, this.requireContext())
This is how I call the filter
adapter.filter.filter(searchTerm) //searchTerm is a String passed by the constructor of the function it sits in
this is my filter and It works properly
private val filter = object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val result = FilterResults()
val suggestions: MutableList<ShopAddressProvince> = mutableListOf()
if (constraint != null) {
suggestions.clear()
val filterPattern = constraint.toString().lowercase()
for (item in list) {
if (item.text.lowercase().contains(filterPattern)) {
suggestions.add(item)
}
}
result.values = suggestions
result.count = suggestions.size
}
return result
}
and check publish results
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
clear()
if (results != null && results.count > 0) {
addAll(results.values as MutableList<ShopAddressProvince>)
} else {
addAll(list)
}
notifyDataSetChanged()
}
I'm trying to add searchview on recyclerview but its not working even not showing any error. I ready many answers on stackoverflow but they are not worked for me.please tell me what is mistake in my code. when i debug my code then searched values are showing in result variable but not showing in recyclerview. i want to search with 1) Country Name 2) capital 3) and id can some help me
my adapter is here
class ProfileListAdapter(var profiles:ArrayList<Profile>):RecyclerView.Adapter<ProfileListAdapter.ProfileViewHolder>(),Filterable {
fun updateProfile(newProfiles:List<Profile>){
profiles.clear()
profiles.addAll(newProfiles)
notifyDataSetChanged()
}
var profileFilterList = ArrayList<Profile>()
init {
profileFilterList = profiles
}
class ProfileViewHolder(view: View):RecyclerView.ViewHolder(view){
private var id = view.findViewById<TextView>(R.id.tv_id)
private var name = view.findViewById<TextView>(R.id.tv_name)
private var fatherName = view.findViewById<TextView>(R.id.tv_father_name)
private var profileImage = view.findViewById<ImageView>(R.id.iv_profile_image)
private var progressDrawable = getProgressDrawable(view.context)
fun bind(profile: Profile){
id.text = profile.id.toString()
name.text = profile.name
fatherName.text = profile.fatherName
profileImage.loadImage(profile.profilePicture,progressDrawable)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ProfileViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.rv_dummy_items,parent,false)
)
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) {
holder.bind(profiles[position])
}
override fun getItemCount() = profiles.size
// Search items
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val charString = constraint?.toString() ?: ""
if (charString.isEmpty()){
profiles.also { profileFilterList = it }
} else {
val filteredList = ArrayList<Profile>()
profiles
.filter {
(it.name.contains(constraint!!)) or (it.fatherName.contains(constraint))
}
.forEach { filteredList.add(it) }
profileFilterList = filteredList
}
return FilterResults().apply { values = profileFilterList }
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
profileFilterList = if (results?.values == null) ArrayList()
else
results.values as ArrayList<Profile>
notifyDataSetChanged()
}
}
}
}
my main activity is here
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
lateinit var viewModel: ListViewModel
var profileAdapter = ProfileListAdapter(arrayListOf())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
//here I'm calling search profile function
searchProfile()
//
viewModel = ViewModelProviders.of(this).get(ListViewModel::class.java)
viewModel.refresh()
binding.rvProfileList.apply {
layoutManager = LinearLayoutManager(context)
adapter = profileAdapter
}
observeViewModel()
filterButtons()
}
fun observeViewModel() {
viewModel.profiles.observe(this, Observer { profiles: List<Profile>? ->
profiles?.let {
binding.rvProfileList.visibility = View.VISIBLE
profileAdapter.updateProfile(it)
}
})
viewModel.profileLoadingError.observe(this, Observer { isError: Boolean? ->
isError?.let {
binding.listError.visibility = if (it) View.VISIBLE else View.GONE
}
})
viewModel.loading.observe(this, Observer { isLoading ->
isLoading?.let {
binding.loadingView.visibility = if (it) View.VISIBLE else View.GONE
}
})
}
//search function
fun searchProfile(){
binding.searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
profileAdapter.filter.filter(query)
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
profileAdapter.filter.filter(newText)
return false
}
})
}
}
Your code quality is really bad. I read repeatly to understand what you mean.
Firstly, you are filtering twice in OnQueryTextListener. Instead you can filter once only. Change your searchProfile function to
fun searchProfile(){
binding.searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
profileAdapter.filter.filter(newText)
return false
}
})
}
Probably, your failure is because of reassigning your profiles and profileFilterList lists.
In few times, you are assingning profiles to profileFilterList, not copying! Both of two lists have same references. So if one of them changes, other list changes directly.
Change your Adapter init block from
init {
profileFilterList = profiles
}
to
init {
profileFilterList = ArrayList(profiles)
}
Change your getFilter function from
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val charString = constraint?.toString() ?: ""
if (charString.isEmpty()){
profiles.also { profileFilterList = it }
} else {
val filteredList = ArrayList<Profile>()
profiles
.filter {
(it.name.contains(constraint!!)) or (it.fatherName.contains(constraint))
}
.forEach { filteredList.add(it) }
profileFilterList = filteredList
}
return FilterResults().apply { values = profileFilterList }
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
profileFilterList = if (results?.values == null) ArrayList()
else
results.values as ArrayList<Profile>
notifyDataSetChanged()
}
}
}
to
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val filteredList = mutableListOf<Profile>()
if (constraint == null || constraint.isEmpty()) {
filteredList.addAll(profiles)
} else {
filteredList.addAll(
profiles.filter { (it.name.contains(constraint!!)) or (it.fatherName.contains(constraint)) } )
}
return FilterResults().apply { values = filteredList }
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
profileFilterList.clear()
results?.values?.let {
profileFilterList.addAll(it as MutableList<Profile>)
}
notifyDataSetChanged()
}
}
}
I edit in your adapter and filter works fine with me my code after modification
class ProfileListAdapter():RecyclerView.Adapter<ProfileListAdapter.ProfileViewHolder>(),Filterable {
var profileList: ArrayList<Profile> = ArrayList()
var profileListFiltered: ArrayList<Profile> = ArrayList()
fun updateProfile(newProfiles:List<Profile>){
profileList = ArrayList(newProfiles)
profileListFiltered = profileList
notifyDataSetChanged()
}
class ProfileViewHolder(view: View):RecyclerView.ViewHolder(view){
private var id = view.findViewById<TextView>(R.id.tv_id)
private var name = view.findViewById<TextView>(R.id.tv_name)
private var fatherName = view.findViewById<TextView>(R.id.tv_father_name)
private var profileImage = view.findViewById<ImageView>(R.id.iv_profile_image)
private var progressDrawable = getProgressDrawable(view.context)
fun bind(profile: Profile){
id.text = profile.id.toString()
name.text = profile.name
fatherName.text = profile.fatherName
profileImage.loadImage(profile.profilePicture,progressDrawable)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ProfileViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.rv_dummy_items,parent,false)
)
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int)
{
holder.bind(profileListFiltered[position])
}
override fun getItemCount() = profileListFiltered.size
// Search items
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults
{
val charString = constraint?.toString() ?: ""
if (charString.isEmpty()){
profileListFiltered = profileList
} else {
val filteredList = ArrayList<Profile>()
profileList
.filter {
(it.name.contains(constraint!!)) or
(it.fatherName.contains(constraint))
}
.forEach { filteredList.add(it) }
profileFilterList = filteredList
}
return FilterResults().apply { values = profileFilterList }
}
override fun publishResults(constraint: CharSequence?, results:
FilterResults?) {
profileFilterList = if (results?.values == null) ArrayList()
else
results.values as ArrayList<Profile>
notifyDataSetChanged()
}
}
}
}
in your activity just declare adapter without parameters constructor after get data using function inside adapter updateProfile then will work fine with you ISA
Edit for your adapter
class ProfileListAdapter(var
profiles:ArrayList<Profile>)
:RecyclerView.Adapter<ProfileListAdapter.Profile
ViewHolder>(),Filterable {
fun updateProfile(newProfiles:List<Profile>){
profiles.clear()
profiles.addAll(newProfiles)
notifyDataSetChanged()
}
var profileFilterList = ArrayList<Profile>()
init {
profileFilterList = profiles
}
class ProfileViewHolder(view: View):RecyclerView.ViewHolder(view){
private var id = view.findViewById<TextView>(R.id.tv_id)
private var name = view.findViewById<TextView>(R.id.tv_name)
private var fatherName = view.findViewById<TextView>
(R.id.tv_father_name)
private var profileImage = view.findViewById<ImageView>
(R.id.iv_profile_image)
private var progressDrawable = getProgressDrawable(view.context)
fun bind(profile: Profile){
id.text = profile.id.toString()
name.text = profile.name
fatherName.text = profile.fatherName
profileImage.loadImage(profile.profilePicture,progressDrawable)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ProfileViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.rv_dummy_items,parent,false)
)
override fun onBindViewHolder(holder: ProfileViewHolder, position:
Int)
{
holder.bind(profileFilterList[position])
}
override fun getItemCount() = profileFilterList.size
// Search items
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?):
FilterResults {
val charString = constraint?.toString() ?: ""
if (charString.isEmpty()){
profileFilterList = profiles
} else {
val filteredList = ArrayList<Profile>()
profiles
.filter {
(it.name.contains(constraint!!)) or
(it.fatherName.contains(constraint))
}
.forEach { filteredList.add(it) }
profileFilterList = filteredList
}
return FilterResults().apply { values = profileFilterList }
}
override fun publishResults(constraint: CharSequence?, results:
FilterResults?) {
profileFilterList = if (results?.values == null) ArrayList()
else
results.values as ArrayList<Profile>
notifyDataSetChanged()
}
}
}
}
I had to remade ArrayAdapter with filter feature to RecyclerView Adapter because of nested scrolling issues. But after I remade it, RecyclerView is empty, but adapter.itemCount is returning right value. Problem is that after I call adapter.notifyDataSetChanged(), it is not calling onCreateViewHolder or onBindViewHolder function.
class ZoneViewHolder(val root: ViewGroup): RecyclerView.ViewHolder(root) {
fun bind(
zone: FlightZone,
manageZoneCheck: (View)-> Unit,
maybeAutoselectZone: (View) -> Unit){
root.setOnClickListener {
manageZoneCheck(root.find(R.id.check))
}
root.find<View>(R.id.check).also { check->
check.setOnClickListener {
manageZoneCheck(check)
}
//autoselect zone if it was previously picked (after search reset)
maybeAutoselectZone(check)
}
root.findText(R.id.zoneNum).text = zone.name
root.findText(R.id.zoneName).apply {
isSelected = true
text = zone.radarInfo
}
root.findText(R.id.separator).setVisibleNotGone(zone.radarInfo != null)
}
}
class ZoneListAdapter(
private val ctx: Context,
private val inflater: LayoutInflater,
private val zoneView: Int,
private val zoneList: List<FlightZone>,
private var list: MutableList<FlightZone>,
private val pickedZones: List<FlightZone>,
private val onZoneChecked: (FlightZone, Boolean) -> Unit,
private val onAllZonesChecked: (Boolean) -> Unit,
private val onFilterDone: (List<FlightZone>) -> Unit
): RecyclerView.Adapter<ZoneViewHolder>(), Filterable{
override fun getItemCount() = list.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ZoneViewHolder {
return ZoneViewHolder(inflater.inflate(zoneView, parent, false) as ViewGroup)
}
override fun onBindViewHolder(vh: ZoneViewHolder, position: Int) {
val zone = list[position]
App.log("ZoneListAdapter: onBindView: ${list.size}")
vh.bind(
zone,
manageZoneCheck = { check->
manageZoneCheck(check, zone)
},
maybeAutoselectZone = {check->
App.log("ZoneListAdapter: alreadyPicked: ${pickedZones.size}")
if (pickedZones.find { it.id == zone.id } != null) check.callOnClick()
}
)
}
/**
* Custom Filter implementation for custom suggestions we provide.
*/
data class ZoneFilterable(val zone: FlightZone, val prefixLength: Int)
internal var tempItems: MutableList<FlightZone> = zoneList.toMutableList()
internal var suggestions: MutableList<ZoneFilterable> = mutableListOf()
internal val sharedCounterLock = Semaphore(1)
private var filter = object : Filter(){
override fun performFiltering(constraint: CharSequence?): FilterResults {
return if (constraint != null) {
sharedCounterLock.acquire()
suggestions.clear()
filterByName(constraint)
suggestions.sortedByDescending { it.prefixLength }
val filterResults = FilterResults()
filterResults.values = suggestions
filterResults.count = suggestions.size
sharedCounterLock.release()
filterResults
} else {
FilterResults()
}
}
private fun filterByName(constraint: CharSequence?){
tempItems.forEach {
val zoneName = it.name.toLowerCase(Locale.getDefault())
val zoneNameNonAccent = zoneName.removeDiacritics()
val query = constraint.toString().toLowerCase(Locale.getDefault())
val queryNonAccent = query.removeDiacritics()
App.log("FlightZonePicker filter zone name non-accent: $zoneNameNonAccent")
if (zoneNameNonAccent.contains(queryNonAccent)) {
val prefix = zoneName.commonPrefixWith(query)
suggestions.add(ZoneFilterable(it, prefix.length))
}
}
}
override fun publishResults(constraint: CharSequence?, results: FilterResults) {
if (results.count > 0) {
list.clear()
val filterList = results.values as? List<Any?>
var resultList = mutableListOf<FlightZone>()
GlobalScope.launch(Dispatchers.Main) {
val resultListAsync = async {
withContext(Dispatchers.Default) {
val list = mutableListOf<FlightZone>()
sharedCounterLock.acquire()
val iter = filterList?.iterator()
iter?.let {
while (iter.hasNext()) {
val item = iter.next()
(item as ZoneFilterable).let { (zoneEntity) -> list.add(zoneEntity) }
}
}
sharedCounterLock.release()
return#withContext list
}
}
resultList = resultListAsync.await().take(10).toMutableList()
App.log("ZoneListAdapter: Results: ${resultList.size}")
list.addAll(resultList)
onFilterDone.invoke(resultList)
}
}
}
}
init {
tempItems = zoneList.toMutableList()
}
override fun getFilter(): Filter {
return filter
}
}
MainClass:
zoneListRecycleView = findViewById(R.id.zoneResults)
zoneListAdapter = ZoneListAdapter(
ctx,
f.layoutInflater,
R.layout.zone_pick_item,
zoneList,
filteredZones,
pickedZones,
onZoneChecked = {
zone, isPicked ->
pickedZones.apply {
if (isPicked){
if (find { it.id == zone.id } == null) add(zone)
} else {
if (find { it.id == zone.id } != null) remove(zone)
}
}
zoneListAdapter.notifyDataSetChanged()
},
onAllZonesChecked = {ischecked->
pickedZones.apply {
clear()
if(ischecked) addAll(zoneList)
}
zoneListAdapter.notifyDataSetChanged()
},
::onFilterResult
)
zoneListRecycleView.adapter = zoneListAdapter
private fun onFilterResult(list: List<FlightZone>){
filteredZones = list.toMutableList()
zoneListAdapter.notifyDataSetChanged()
App.log("ZonelistAdapter: count = ${zoneListAdapter.itemCount}")
}
UPDATE:
I changed Adapter to ListAdapter and I changed publishResults function to
resultList = resultListAsync.await().take(10).toMutableList()
onFilterDone.invoke(resultList)
And inside my parent class I call:
private fun onFilterResult(list: List<FlightZone>){
filteredZones = list.toMutableList()
zoneListAdapter.submitList(filteredZones)
App.log("ZonelistAdapter: filteredList = ${filteredZones.size}")
App.log("ZonelistAdapter: count = ${zoneListAdapter.itemCount}")
}
Log:
app: ZonelistAdapter: filteredList = 10
app: ZonelistAdapter: count = 0
There is still some issue with adapter. So filter feature is working as intended but adapter and recyclerview is not displaying items for some reason.
I'm having two fragments
1.UploadFragment - to upload data to firestore
2.FetchFragemnt - fetch data from firestore and display it in recyclerView
these two fragments are used in a NavigationDrawer.
My problem is in fetchFragment initially recyclerView displays datas that are fetched from firestore.
but when I navigate to UploadFragment and return back to FetchFragment RecyclerView is not loaded.
Also, onCreateViewHolder and onBindViewHolder are not called when I navigate to UploadFragment and return back to FetchFragment.
Please someone help.
FetchFragment code :
class FetchFragment : Fragment(R.layout.fragment_fetch) {
private lateinit var binding : FragmentFetchBinding
private lateinit var adapter: PersonsAdapter
private lateinit var personsList: MutableList<PersonsDb>
private val personCollectionRef = Firebase.firestore.collection("persons")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentFetchBinding.bind(view)
personsList = mutableListOf()
adapter = PersonsAdapter(personsList){item ->
Intent(requireContext(),UpdateActivity::class.java).also{
it.putExtra("EXTRA_DETAILS",item.id)
startActivity(it)
}
}
binding.rvPersons.adapter = adapter
binding.rvPersons.layoutManager = LinearLayoutManager(requireContext())
fetchPPerson()
binding.svFilter.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(p0: String?): Boolean {
//Performs search when user hit the search button on the keyboard
// adapter.getFilter().filter(p0);
return false
}
override fun onQueryTextChange(p0: String?): Boolean {
//Start filtering the list as user start entering the characters
adapter.getFilter().filter(p0);
return false
}
})
}
private fun fetchPPerson() = CoroutineScope(Dispatchers.IO).launch{
try{
val querySnapshot = personCollectionRef.get().await()
personsList.clear()
for(document in querySnapshot.documents){
val person = document.toObject<Person>()
personsList.add(
PersonsDb(document.id,
person?.name.toString(),
person?.age.toString().toInt(),
Date(person?.dob.toString())
))
}
withContext(Dispatchers.Main){
adapter.notifyDataSetChanged()
}
}catch (e:Exception){
withContext(Dispatchers.Main){
Toast.makeText(requireContext(),e.message, Toast.LENGTH_LONG).show()
Log.d("Fetch Error", e.message)
}
}
}
}
My Recycler Adapter :
class PersonsAdapter (
var persons : List<PersonsDb>,
private val listener: (PersonsDb) -> Unit
):RecyclerView.Adapter<PersonsAdapter.PersonsViewHolder>(),Filterable{
var personsList = persons
inner class PersonsViewHolder(val binding : ItemPersonBinding):
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonsViewHolder {
val binding = ItemPersonBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return PersonsViewHolder(binding)
}
override fun getItemCount(): Int {
return persons.size
}
override fun getFilter(): Filter {
return filter
}
override fun onBindViewHolder(holder: PersonsViewHolder, position: Int) {
val item = persons[position]
with(holder){
with(persons[position]){
binding.tvName.text = this.name
binding.tvAge.text = this.age.toString()
// binding.tvDob.text = this.dob.toString()
binding.tvDob.text = updateDateInView(this.dob)
}
}
holder.itemView.setOnClickListener { listener(item) }
}
private fun updateDateInView(cal : Date) : String{
val myFormat = "dd/MM/yyyy" // mention the format you need
val sdf = SimpleDateFormat(myFormat, Locale.US)
// binding.dpDob.setText(sdf.format(cal.getTime()))
return sdf.format(cal.getTime())
}
private val filter: Filter = object : Filter() {
override fun performFiltering(constraint: CharSequence): FilterResults {
var filteredList: MutableList<PersonsDb> = arrayListOf()
if (constraint.isEmpty()) {
filteredList.addAll(personsList)
} else {
val filterPattern = constraint.toString().toLowerCase().trim { it <= ' ' }
for (item in 0..persons.size -1) {
if (persons[item].name.toLowerCase().contains(filterPattern)
|| persons[item].age.toString().contains(filterPattern)) {
filteredList.add(persons[item])
}
}
}
val results = FilterResults()
results.count = filteredList.size
results.values = filteredList
// Log.d("Filter Values", results.values.toString())
return results
}
override fun publishResults(charSequence: CharSequence, filterResults: FilterResults) {
persons = if(filterResults == null || filterResults.values == null){
personsList
}
else
filterResults.values as List<PersonsDb>
notifyDataSetChanged()
}
}
}
Can you check Item count( log item count ) . That way you will know if data exists
for recyclerview when returning to FetchFragment . I suspect empty data could be reason.
I have an AutoCompleteTextView (et_item_name) with data source coming from an endpoint. Here is the code for setting the initial adapter and reloading it once we receive data from the endpoint.
productSuggestions = ArrayList()
mSearchSuggestionsAdapter = ArrayAdapter(context, android.R.layout.simple_list_item_1, productSuggestions)
et_item_name.setAdapter(mSearchSuggestionsAdapter)
et_item_name.threshold = 1
et_item_name.doAfterTextChanged {
if (it.toString().trim().length <= 1) {
productSuggestions.clear()
mSearchSuggestionsAdapter.notifyDataSetChanged()
} else {
mainModel.getProductsAutoCompleteResults(ProductAutoCompleteRequest(10, it.toString(), "SOME_ID")) //this is an endpoint call, which returns fetched results
}
}
//Observer
mainModel.autoCompleteBYOSResult.observe(viewLifecycleOwner, Observer { //autoCompleteBYOSResult is MutableLiveData
productSuggestions.clear()
var temp: ArrayList<String> = ArrayList()
it.success?.forEach { temp.add(it.name) }
productSuggestions.addAll(temp) //this array has all correct values
mSearchSuggestionsAdapter.notifyDataSetChanged() //after this call, this adapter doesn't update, it still shows 0 mObjects when debugging
})
mSearchSuggestionsAdapter.notifyDataSetChanged() does not update the adapter. It still shows 0 mObjects when in debug mode. The drop down below the AutoCompleteTextView does not appear.
What is the correct way of dynamically updating adapter for AutoCompleteTextView?
So, after trying a lot, I ended up following this article:
https://www.truiton.com/2018/06/android-autocompletetextview-suggestions-from-webservice-call/
class AutoSuggestAdapter(context: Context, resource: Int) : ArrayAdapter<String>(context, resource), Filterable {
private val mlistData: MutableList<String>
init {
mlistData = ArrayList()
}
fun setData(list: List<String>) {
mlistData.clear()
mlistData.addAll(list)
}
override fun getCount(): Int {
return mlistData.size
}
override fun getItem(position: Int): String? {
return mlistData[position]
}
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val filterResults = FilterResults()
if (constraint != null) {
filterResults.values = mlistData
filterResults.count = mlistData.size
}
return filterResults
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
if (results != null && results.count > 0) {
notifyDataSetChanged()
} else {
notifyDataSetInvalidated()
}
}
}
}
}
//Observer
mainModel.autoCompleteBYOSResult.observe(viewLifecycleOwner, Observer {
val temp: ArrayList<String> = ArrayList()
it.success?.forEach { temp.add(it.name) }
autoSuggestAdapter.setData(temp);
autoSuggestAdapter.notifyDataSetChanged();
})
//onCreate
autoSuggestAdapter = AutoSuggestAdapter(context!!, android.R.layout.simple_dropdown_item_1line)
et_item_name.threshold = 2
et_item_name.setAdapter(autoSuggestAdapter)
et_item_name.doOnTextChanged { text, start, count, after ->
handler.removeMessages(TRIGGER_AUTO_COMPLETE)
handler.sendEmptyMessageDelayed(TRIGGER_AUTO_COMPLETE, AUTO_COMPLETE_DELAY)
}
handler = object : Handler() {
override fun handleMessage(msg: Message?) {
if (msg?.what == TRIGGER_AUTO_COMPLETE) {
if (et_item_name.text.trim().length > 1) {
mainModel.getProductsAutoCompleteResults(ProductAutoCompleteRequest(10, et_item_name.text.trim().toString(), "SOME_ID"))
}
}
}
}
All working now!!