Saving checkbox states in RecyclerView inside Fragment using SharedPreferences or any other method - kotlin

I'm trying to save checkbox states in RecyclerView inside Fragment to restore these preferences after exit from the app and loading it again.
I have a ConfigActivity for AppWidget in which there are fragments.
Inside of one of the fragments I have a RecyclerView which loads calendars available for the user from Calendar Provider. Based on selected calendars the appwidget will be loading the events from them. Selected calendars should be passed into the appwidget.
I've made saving states of the checkboxes while scrolling of the RecyclerView.
But I don't know how to save selected checkboxes in RecyclerView inside Fragment using SharedPreferences (saving for relaunching of the app).
My data class for calendar items:
data class CalendarItem(
val idCalendar: Long,
val displayNameCalendar: String?,
val accountNameCalendar: String?,
val colorCalendar: Int?
)
Item with checkbox in xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp">
<ImageView
android:id="#+id/calendar_color"
android:layout_width="10dp"
android:layout_height="10dp"
android:src="#drawable/color_label_circle"
app:tint="#color/accent_color"
android:layout_alignParentStart="true"
android:layout_alignTop="#+id/text_display_name_calendar"
android:layout_alignBottom="#+id/text_display_name_calendar"/>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="#+id/text_display_name_calendar"
style="#style/basicText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginStart="24dp"
android:layout_marginEnd="4dp"
android:maxLines="1"
android:ellipsize="end"
android:gravity="start|center_vertical"
android:layoutDirection="rtl"
android:text="Display Name" />
<TextView
android:id="#+id/text_account_name"
style="#style/commentText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Account Name"
android:layout_marginEnd="4dp"
android:maxLines="1"
android:ellipsize="end"
android:layout_alignStart="#+id/text_display_name_calendar"
android:layout_below="#+id/text_display_name_calendar" />
</RelativeLayout>
My Fragment getting calendars:
class CalendarsEventsFragment : Fragment() {
// For permissions
private val PERMISSION_REQUEST_CODE = 101
// For RecyclerView - Calendars
private lateinit var calendarItemAdapter: CalendarItemAdapter
private lateinit var recyclerViewCalendars: RecyclerView
// Values for the calendars from the calendar content provider
private val EVENT_PROJECTION = arrayOf(
CalendarContract.Calendars._ID,
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
CalendarContract.Calendars.ACCOUNT_NAME,
CalendarContract.Calendars.CALENDAR_COLOR
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_calendars_events, container, false)
recyclerViewCalendars = view.findViewById(R.id.recyclerview_calendars)
// Setup permissions + start getCalendars
setupPermissionsGetCalendars()
return view
}
// Function to get and show calendars
private fun getCalendars() {
// Getting calendars from CalendarProvider
// In practice, should be done in an asynchronous thread instead of on the main thread
calendarItemAdapter = CalendarItemAdapter()
calendarItemAdapter.clearData()
val uri = CalendarContract.Calendars.CONTENT_URI
val cur: Cursor? = context?.contentResolver?.query(
uri,
EVENT_PROJECTION,
null,
null,
null
)
while (cur?.moveToNext() == true) {
val calId = cur.getLong(PROJECTION_ID_INDEX)
val displayName = cur.getString(PROJECTION_DISPLAY_NAME_INDEX)
val accountName = cur.getString(PROJECTION_ACCOUNT_NAME_INDEX)
val color = cur.getInt(PROJECTION_CALENDAR_COLOR_INDEX)
calendarItemAdapter.pushData(
CalendarItem(
idCalendar = calId,
displayNameCalendar = displayName,
accountNameCalendar = accountName,
colorCalendar = color
)
)
}
cur?.close()
// Setup RecyclerView adapter
recyclerViewCalendars.let {
it.layoutManager = LinearLayoutManager(context)
it.adapter = calendarItemAdapter
}
}
// Function to check permission and make request for permission + start getCalendars
private fun setupPermissionsGetCalendars() {
if (checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) !=
PackageManager.PERMISSION_GRANTED
) {
requestPermissions(
arrayOf(Manifest.permission.READ_CALENDAR),
PERMISSION_REQUEST_CODE
)
} else {
getCalendars()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
PERMISSION_REQUEST_CODE -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(
requireActivity(),
getText((R.string.toast_permission_granted)),
Toast.LENGTH_SHORT
).show()
getCalendars()
} else {
if (shouldShowRequestPermissionRationale(Manifest.permission.READ_CALENDAR)) {
Toast.makeText(
requireActivity(),
getText((R.string.toast_permission_denied)),
Toast.LENGTH_SHORT
).show()
showUserRationale()
} else {
askUserOpenAppInfo()
}
}
}
}
}
private fun showUserRationale() {
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.request_permission_rationale_title))
.setMessage(getString(R.string.request_permission_rationale_message))
.setPositiveButton("OK") { dialog, id ->
requestPermissions(
arrayOf(Manifest.permission.READ_CALENDAR),
PERMISSION_REQUEST_CODE
)
}
.create()
.show()
}
private fun askUserOpenAppInfo() {
val appSettingsIntent = Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", activity?.packageName, null)
)
if (activity?.packageManager?.resolveActivity(
appSettingsIntent,
PackageManager.MATCH_DEFAULT_ONLY
) == null
) {
Toast.makeText(
requireContext(),
getText(R.string.toast_permission_denied_forever),
Toast.LENGTH_SHORT
).show()
} else {
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.request_permission_denied_forever_title))
.setMessage(getString(R.string.request_permission_denied_forever_message))
.setPositiveButton(getString(R.string.open_app_info_dialog_positive_button_text)) { dialog, id ->
startActivity(appSettingsIntent)
requireActivity().finish()
}
.setNegativeButton(getString(R.string.open_app_info_dialog_negative_button_text)) { dialog, id ->
requireActivity().finish()
}
.create()
.show()
}
}
}
My RecyclerView Adapter:
class CalendarItemAdapter() : RecyclerView.Adapter<CalendarItemAdapter.ViewHolder>() {
var data: MutableList<CalendarItem> = mutableListOf()
var checkedCalendarItems = SparseBooleanArray()
fun clearData() {
data.clear()
notifyDataSetChanged()
}
fun pushData(calendarItem: CalendarItem) {
data.add(calendarItem)
notifyDataSetChanged()
}
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val imageViewColor: ImageView = view.findViewById(R.id.calendar_color)
val displayNameOfCalendar: CheckBox = view.findViewById(R.id.text_display_name_calendar)
val accountName: TextView = view.findViewById(R.id.text_account_name)
init {
displayNameOfCalendar.setOnClickListener {
if(!checkedCalendarItems.get(adapterPosition, false)) {
displayNameOfCalendar.isChecked = true
checkedCalendarItems.put(adapterPosition, true)
} else {
displayNameOfCalendar.isChecked = false
checkedCalendarItems.put(adapterPosition, false)
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_calendar, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val datum = data[position]
datum.colorCalendar?.let {
holder.imageViewColor.setColorFilter(it)
}
holder.displayNameOfCalendar.text = datum.displayNameCalendar
holder.displayNameOfCalendar.isChecked = checkedCalendarItems.get(position, false)
holder.accountName.text = datum.accountNameCalendar
}
override fun getItemCount(): Int {
return data.size
}
}
Could you help me, please?

SharedPreferences can only store primitives and String arrays so you'll have to serialise your array somehow. Probably the easiest way is to just get all the indices of the checked items and throw them in a string. And when you pull that back out, split them up and set those to true.
You should probably handle this in the adapter, since really it's an internal implementation detail that only the adapter needs to know about. Something like this maybe:
class CalendarItemAdapter() : RecyclerView.Adapter<CalendarItemAdapter.ViewHolder>() {
var checkedCalendarItems = SparseBooleanArray()
fun saveState(prefs: SharedPreferences) {
// make a list of all the indices that are set to true, join them as a string
val checkedIndices = checkedCalendarItems
.mapIndexedNotNull {index, checked -> if (checked) index else null }
.joinToString(SEPARATOR)
prefs.edit { putString(KEY_CHECKED_INDICES, checkedIndices) }
}
fun restoreState(prefs: SharedPreferences) {
// reset the array - we're clearing the current state
// whether there's anything stored or not
checkedCalendarItems = SparseBooleanArray()
// grab the checked indices and set them - using null as a "do nothing" fallback
val checkedIndices = prefs.getString(KEY_CHECKED_INDICES, null)
?.split(SEPARATOR)
?.map(String::toInt) // or mapNotNull(String::toIntOrNull) to be super safe
?.forEach { checkedCalendarItems[it] = true }
// update the display - onBindViewHolder should be setting/clearing checkboxes
// by referring to the checked array
notifyDataSetChanged()
}
...
companion object {
// making these constants that both functions refer to avoids future bugs
// e.g. someone changing the separator in one function but not the other
const val SEPARATOR = ","
const val KEY_CHECKED_INDICES = "checked indices"
}
}
Then you can call these save/restore state functions on the adapter as appropriate, e.g. in onStop and onStart, passing in your SharedPreferences state object

Related

Change API param based on Dialog Fragment input with MVVM in Kotlin

i'm a beginner in android & kotlin and i'm having an issue i been trying to figure out all day...
I have an app that fetches data from NewsApi and displays it in a recycler view , i am using Retrofit library and Room (to save favorite articles) with MVVM architecture. I want to add an option so that the user can select the country of the news from a dialog that pops up by clicking on a icon on the toolbar menu.
I have created a custom DialogFragment and have it show up, the dialog contains a spinner with a list of countries and i'm using FragmentResult and FragmentResultListener to pass the country value between dialog fragment and news fragment.
DialogFragment
class CountrySelectDialog : DialogFragment(R.layout.country_selection_dialog) {
private lateinit var binding: CountrySelectionDialogBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = CountrySelectionDialogBinding.bind(view)
binding.spCountrySelection.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
adapterView: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
Toast.makeText(
context,
"you selected ${adapterView?.getItemAtPosition(position).toString()}",
Toast.LENGTH_SHORT
).show()
}
override fun onNothingSelected(adapterView: AdapterView<*>?) {
}
}
binding.btnCancel.setOnClickListener {
this.dismiss()
}
binding.btnConfirm.setOnClickListener {
val result = binding.spCountrySelection.selectedItem.toString()
setFragmentResult("countryCode", bundleOf("bundleKey" to result))
this.dismiss()
}
}
}
The news Fragment is observing data from the View Model
class BreakingNewsFragment : Fragment(R.layout.fragment_breaking_news) {
lateinit var viewModel: NewsViewModel
lateinit var newsAdapter: NewsAdapter
private lateinit var binding: FragmentBreakingNewsBinding
val TAG = "BreakingNewsFragment"
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentBreakingNewsBinding.bind(view)
viewModel = (activity as NewsActivity).viewModel
setUpRecyclerView()
setFragmentResultListener("countryCode") { countryCode, bundle ->
val result = bundle.getString("countryCode")
viewModel.countryCode = result!!}
viewModel.breakingNews.observe(viewLifecycleOwner, Observer {
when (it) {
is Resource.Success -> {
hideProgressBar()
it.data?.let {
newsAdapter.differ.submitList(it.articles.toList())
val totalPages = it.totalResults / QUERY_PAGE_SIZE + 2
isLastPage = viewModel.breakingNewsPage == totalPages
}
}
is Resource.Error -> {
hideProgressBar()
it.message?.let {
Log.e(TAG, "An error occurred: $it")
}
}
is Resource.Loading -> {
showProgressBar()
}
}
})
newsAdapter.setOnItemClickListener {
val bundle = Bundle().apply {
putSerializable("article", it)
}
findNavController().navigate(
R.id.action_breakingNewsFragment_to_articleFragment, bundle
)
}
}
ViewModel:
class NewsViewModel(val newsRepository: NewsRepository, val app: Application) : AndroidViewModel(app) {
val breakingNews: MutableLiveData<Resource<NewsResponse>> = MutableLiveData()
var breakingNewsPage = 1
var breakingNewsResponse: NewsResponse? = null
val searchNews: MutableLiveData<Resource<NewsResponse>> = MutableLiveData()
var searchNewsPage = 1
var searchNewsResponse: NewsResponse? = null
var countryCode :String = "it"
init {
getBreakingNews(countryCode)
}
fun getBreakingNews(countryCode: String) {
viewModelScope.launch {
breakingNews.postValue(Resource.Loading())
val response = newsRepository.getBreakingNews(countryCode, breakingNewsPage)
breakingNews.postValue(handleBreakingNewsResponse(response))
}
}
fun handleBreakingNewsResponse(response: Response<NewsResponse>): Resource<NewsResponse> {
if (response.isSuccessful) {
response.body()?.let { resultResponse ->
breakingNewsPage++
if (breakingNewsResponse == null) {
breakingNewsResponse = resultResponse
} else {
val oldArticles = breakingNewsResponse?.articles
val newArticles = resultResponse.articles
oldArticles?.addAll(newArticles)
}
return Resource.Success(breakingNewsResponse ?: resultResponse)
}
}
return Resource.Error(response.message())
}
fun searchNews(searchQuery: String) {
viewModelScope.launch {
searchNews.postValue(Resource.Loading())
val response = newsRepository.searchNews(searchQuery, searchNewsPage)
searchNews.postValue((handleSearchNewsResponse(response)))
}
}
fun handleSearchNewsResponse(response: Response<NewsResponse>): Resource<NewsResponse> {
if (response.isSuccessful) {
response.body()?.let { resultResponse ->
searchNewsPage++
if (searchNewsResponse == null) {
searchNewsResponse = resultResponse
} else {
val oldArticles = searchNewsResponse?.articles
val newArticles = resultResponse.articles
oldArticles?.addAll(newArticles)
}
return Resource.Success(searchNewsResponse ?: resultResponse)
}
}
return Resource.Error(response.message())
}
}
When i click on the icon on the toolbar menu the dialog appears and works fine but i can't seem to find a way to have the recycler view update with new data using given value for country
I searched everywhere and couldn't find a solution (or probably didn't understand it :S) can someone guide me into the right direction? I'm so lost...
When I click on the icon on the toolbar menu the dialog appears and works fine but I can't seem to find a way to have the recycler view update with new data using given value for country.

RecyclerView adapter inside a fragment is not updating when data changes

I have an activity that contains 2 tabs and each tab has a fragment inside ViewPager. Each fragment have a RecyclerView.
When I navigate to another activity the data inside the Fragments should be updated. Although the data is being sent correctly to the fragment, the original data is displayed.
I tried using notifyDataSetChanged() method inside the fragment but it didn't work.
I also tried calling it from the activity like:
if (!pickedItemsList.isNullOrEmpty() && notScannedItemsFragment != null && notScannedItemsFragment.isAdded)
{
notScannedItemsFragment.notScannedItemsAdapter.notifyDataSetChanged()
}
However, it didn't work too.
That's how I am initiating the fragment:
override fun initFragments(savedInstanceState: Bundle?, pickedItemsList: ArrayList<OrderDetail>, remainigItemsList: ArrayList<OrderDetail>) {
val listener: ItemsInteractionListener = object : ItemsInteractionListener {
override fun onSwipeToRefresh() {
presenter.onSwipeToRefresh()
}
}
if (!pickedItemsList.isNullOrEmpty() && notScannedItemsFragment != null && notScannedItemsFragment.isAdded) {
notScannedItemsFragment.notScannedItemsAdapter.notifyDataSetChanged()
scannedItemsFragment = ScannedItemsFragment().newInstance(remainingItemsList)
notScannedItemsFragment = NotScannedItemsFragment().newInstance(pickedItemsList)!!
} else {
scannedItemsFragment = ScannedItemsFragment().newInstance(arrayListOf())
notScannedItemsFragment = NotScannedItemsFragment().newInstance(allItemsList)!!
}
scannedItemsFragment.setListener(listener)
notScannedItemsFragment.setListener(listener)
}
allItemList is the original list and pickedItemsList and remainingItemsList are the lists after the changes (that I got from the other activity)
This is one of the fragments classes:
class NotScannedItemsFragment : BaseFragment() {
private var listener: ItemsInteractionListener? = null
lateinit var notScannedItemsAdapter: OrderItemListingAdapter
private var itemRemainingCount: Int = 0
lateinit var notScannedItems: ArrayList<OrderDetail>
lateinit var recyclerView: RecyclerView
lateinit var fragmentView: View
fun newInstance(notScannedItems: ArrayList<OrderDetail>): NotScannedItemsFragment? {
val notScannedItemsFragment = NotScannedItemsFragment()
val args = Bundle()
val order = Gson().toJson(notScannedItems)
args.putString(IntentConstants.EXTRA_NOT_SCANNED_ITEM_LIST, order)
notScannedItemsFragment.setArguments(args)
return notScannedItemsFragment
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val str = arguments?.getString(IntentConstants.EXTRA_NOT_SCANNED_ITEM_LIST)
notScannedItems = Gson().fromJson(
str,
object : TypeToken<List<OrderDetail?>?>() {}.type
) as ArrayList<OrderDetail>
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
fragmentView = inflater.inflate(R.layout.fragment_not_scanned_items, container, false)
recyclerView = fragmentView.notScannedItemListing
return fragmentView
}
fun setListener(listener: ItemsInteractionListener) {
this.listener = listener
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setUpRecycler(view)
super.onViewCreated(view, savedInstanceState)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
}
private fun setUpRecycler(view: View) {
imageLoader = ImageLoader(context)
notScannedItemsAdapter = OrderItemListingAdapter(
false,
imageLoader,
object : ImageClickListener {
override fun onImageClick(
itemName: String,
itemQuantity: Int,
url: String,
barcodes: List<String>?
) {
startImageFullViewActivity(itemName, itemQuantity, url, barcodes)
}
})
notScannedItemsAdapter.addItem(notScannedItems)
notScannedItemsAdapter.notifyDataSetChanged()
view.notScannedItemListing.apply {
view.notScannedItemListing.layoutManager = LinearLayoutManager(context)
view.notScannedItemListing.setHasFixedSize(true)
view.notScannedItemListing.isNestedScrollingEnabled = false
adapter = notScannedItemsAdapter
}
notScannedItemsAdapter.printList()
}
fun showOrderItemListing(notScannedItems: ArrayList<OrderDetail>) {
this.notScannedItems = notScannedItems
itemRemainingCount = notScannedItems.size
}
fun getItemsRemainingCount(): Int{
return notScannedItems.size
}
fun clearItems() {
notScannedItemsAdapter.clearItems()
}
fun updateAdapterContent(pickedItemsList: ArrayList<OrderDetail>) {
if(this::notScannedItemsAdapter.isInitialized ) {
notScannedItemsAdapter.clearItems()
notScannedItemsAdapter.addItem(notScannedItems)
notScannedItemsAdapter.notifyDataSetChanged()
}
}
}
It turns out since I'm getting the list from arguments it's not updating with the new list. As stated here: Anything initialized in onCreate() is preserved if the Fragment is paused and resumed.
So I added a boolean variable loadListFromArgs and I only loaded the list from args if it's true and when I call updateAdapterContent I set it to false.

Kotlin RecyclerView not updating after data changes

I am using RecyclerView to display a dynamic list of data and after I call an api I need to update my RecyclerView UI but the items in my RecyclerView does not change...
Below is my how I init my RecyclerView in my Fragment:-
forwardedList.layoutManager = LinearLayoutManager(context!!, RecyclerView.VERTICAL, false)
adapter = ForwardListAdapter(SmsHelper.getForwardedSms(context!!))
forwardedList.adapter = adapter
Below is my custom RecyclerView Adapter:-
class ForwardListAdapter(val forwardedList: List<SmsData>) : RecyclerView.Adapter<ForwardListAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ForwardListAdapter.ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.forwarded_item, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ForwardListAdapter.ViewHolder, position: Int) {
holder.bindItems(forwardedList[position])
}
override fun getItemCount(): Int {
return forwardedList.size
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindItems(sms: SmsData) {
val simSlotText: TextView = itemView.findViewById(R.id.simSlot)
val senderText: TextView = itemView.findViewById(R.id.sender)
simSlotText.text = "[SIM ${sms.simSlot}] "
senderText.text = sms.sender
}
}
}
I am currently updating my RecyclerView from SmsHelper class as below:-
val fragments = mainActivity!!.supportFragmentManager.fragments
for (f in fragments) {
if (f.isVisible) {
if (f.javaClass.simpleName.equals("ForwardedFragment")) {
val fg = f as ForwardedFragment
fg.adapter.notifyDataSetChanged() <----- HERE
} else if (f.javaClass.simpleName.equals("FailedFragment")) {
val fg = f as FailedFragment
fg.adapter.notifyDataSetChanged()
}
}
}
As I observed, you did not really change the adapter's data but only called notifyDataSetChanged. You cannot just expect the data to be changed automatically like that since notifyDataSetChanged only:
Notifies the attached observers that the underlying data has been changed and any View reflecting the data set should refresh itself.
You need to change the data by yourself first, then call notifyDataSetChanged.
class ForwardListAdapter(private val forwardedList: MutableList<SmsData>) : RecyclerView.Adapter<ForwardListAdapter.ViewHolder>() {
// ...
fun setData(data: List<SmsData>) {
forwardedList.run {
clear()
addAll(data)
}
}
// ...
}
Then do it like this:
adapter.run {
setData(...) // Set the new data
notifyDataSetChanged(); // notify changed
}

How to search a diffutil filter out results from existing list

I'm using DiffUtil in my RecyclerView to displays a list from a database using the Room component. I would like to add a search function in the Appbar, that will filter out the existing items in the list as the user is typing.
My app currently has a search icon in the action bar, when you click the search icon it will expand across the Appbar and allow the user to search the database and return a new list. This mehtod involves querying the database each time.
Search Menu, This is where the parameters for the search widget are set.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="#+id/my_search"
android:title="Search"
android:icon="#drawable/ic_search"
app:showAsAction="collapseActionView|ifRoom"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
</menu>
RecyclerViewFragment
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.recycler_view_menu, menu)
val searchItem = menu.findItem(R.id.my_search)
val searchView: SearchView = searchItem.actionView as SearchView
searchView.imeOptions = EditorInfo.IME_ACTION_DONE
searchView.setIconifiedByDefault(false)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
// This approach queries my database for a new list.
viewModel.searchTopic("%$query%")
submitList()
return false
}
override fun onQueryTextChange(newText: String): Boolean {
// I would like to use the onTextChange() to filter out results from the list instead of querying a new list from the database.
return true
}
})
}
private fun submitList() {
viewModel.listDevTopics.observe(viewLifecycleOwner, Observer {
it?.let {
rvAdapter.submitList(it)
}
})
}
My RecyclerViewAdapter
class RecyclerViewAdapter() : androidx.recyclerview.widget.ListAdapter<Dev,
RecyclerViewAdapter.ItemViewHolder>(MyDiffCallback()) {
lateinit var searchList: List<Dev>
class MyDiffCallback : DiffUtil.ItemCallback<Dev>() {
override fun areItemsTheSame(oldItem: Dev, newItem: Dev): Boolean {
return oldItem.topic == newItem.topic
}
override fun areContentsTheSame(oldItem: Dev, newItem: Dev): Boolean {
return oldItem == newItem
}
}
class ItemViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
...
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
...
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
...
}
I would like to avoid querying the database every time for a search result, I want to use onQueryTextChange so it goes through the existing list and updates the list as the user is entering their query.
just implement Filterable and override getFilter Method
and make your filter object then return this object at getFilter Method
class JobOrderAdapter(val clickListener: JobOrderListener) : ListAdapter<CJO,
ViewHolder>(JobOrderDiffCallback()), Filterable {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder.from(parent)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item, clickListener)
}
private var list = listOf<CJO>()
fun setData(list: List<CJO>){
this.list = list
submitList(list)
}
override fun getFilter(): Filter = customFilter
private val customFilter = object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val filteredList = mutableListOf<CJO>()
if (constraint == null || constraint.isEmpty()) {
filteredList.addAll(list)
} else {
val filterPattern = constraint.toString().toLowerCase().trim()
for (item in list) {
// here i am searching at custom obj by managerName
if (item.managerName.toLowerCase().contains(filterPattern)) {
filteredList.add(item)
}
}
}
val results = FilterResults()
results.values = filteredList
return results
}
override fun publishResults(constraint: CharSequence?, filterResults: FilterResults?) {
submitList(filterResults?.values as MutableList<CJO>?)
}
}}
and from your fragmnet or activity just call adapter.filter.filter(yourQueryText)
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.home_menu, menu)
val searchByContract = menu.findItem(R.id.search_by_name)
val searchContractView = searchByContract.actionView as SearchView
searchContractView.queryHint = "البحث باسم مدير البيع"
searchContractView.inputType = InputType.TYPE_CLASS_TEXT
searchContractView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
adapter.filter.filter(newText)
return false
}
})
super.onCreateOptionsMenu(menu, inflater)
}

How to change recyclerview item and sub items when clicked and also get the data in the position clicked from a fragment?

I have 2 Recyclerviews in a fragment. Each item consists of 2 textview. When clicked, i want to change the color of item background and the 2 tvs and get the listofData(position) clicked also then send these s pieces of data to an activity.
The problem is 2 rvs each have its own adapter so i can't call the activity and check if data is selected from both adapters. And when i try it from the fragment i get the adapter position right but the view colors are not being changed correctly.
My RV element
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="2dp"
android:paddingBottom="10dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:background="#drawable/date_time_background"
android:elevation="0dp"
android:gravity="center"
android:layout_marginEnd="5dp"
android:orientation="vertical"
android:id="#+id/item_linear_layout">
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/dateNameTV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Sat"
android:textColor="#color/black_65"
android:textSize="16sp" />
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/dateNumTV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="23/5"
android:textColor="#color/black_65"
android:textSize="16sp" />
</LinearLayout>
My RV adapter
class TimesAdapter(private var availableTimes: List<String>?, private val onTimeListener: OnTimeListener) :
RecyclerView.Adapter<TimesAdapter.TimeViewHolder>() {
private var itemIndex = -1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TimeViewHolder {
val v = LayoutInflater.from(parent.context)
.inflate(R.layout.date_time_element, parent, false)
return TimeViewHolder(v, onTimeListener)
}
override fun getItemCount(): Int = availableTimes?.size!!
#SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: TimeViewHolder, position: Int) {
val currentDate: String? = availableTimes?.get(position) // i.e Sun 23/5
val parts = currentDate?.split(" ")
try {
val part1 = parts?.get(0)
holder.dateNameTV.text = part1
val part2 = parts?.get(1)
holder.dateNumTV.text = part2
} catch (e: IndexOutOfBoundsException) {
e.printStackTrace()
}
holder.itemLinearLayout.setOnClickListener {
itemIndex = position
notifyItemChanged(position)
}
val ctx = holder.itemLinearLayout.context
if (itemIndex == position) {
holder.itemLinearLayout.background = (loadDrawable(ctx, R.drawable.date_time_background_selected))
holder.dateNameTV.setTextColor(loadColor(ctx, android.R.color.white))
holder.dateNumTV.setTextColor(loadColor(ctx, android.R.color.white))
} else {
holder.itemLinearLayout.background = (loadDrawable(ctx, R.drawable.date_time_background))
holder.dateNameTV.setTextColor(loadColor(ctx, R.color.black_65))
holder.dateNumTV.setTextColor(loadColor(ctx, R.color.black_65))
}
}
class TimeViewHolder(itemView: View, onTimeListener: OnTimeListener) :
RecyclerView.ViewHolder(itemView), View.OnClickListener{
var dateNameTV: TextView = itemView.findViewById(R.id.dateNameTV)
var dateNumTV: TextView = itemView.findViewById(R.id.dateNumTV)
var itemLinearLayout: LinearLayout = itemView.findViewById(R.id.item_linear_layout)
private var onTimeListener: OnTimeListener? = null
init {
itemView.setOnClickListener(this)
this.onTimeListener = onTimeListener
}
override fun onClick(v: View?) {
onTimeListener?.onTimeClick(adapterPosition, itemView)
}
}
interface OnTimeListener{
fun onTimeClick(position: Int, itemView: View)
}
}
My fragment
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [DoctorAppointmentsFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class DoctorAppointmentsFragment : Fragment(),
DatesAdapter.OnDateListener, TimesAdapter.OnTimeListener {
private lateinit var datesRV: RecyclerView
private lateinit var timesRV: RecyclerView
private lateinit var linearLayoutManager: LinearLayoutManager
private lateinit var datesAdapter: DatesAdapter
private lateinit var timesAdapter: TimesAdapter
private lateinit var dateNameTV: TextView
private lateinit var dateNumTV: TextView
private lateinit var dateLinearLayout: LinearLayout
private var dates: List<String>? = null
private var times: List<String>? = null
private var isDateSelected = false
private var isTimeSelected = false
private var selectedDate: String? = null
private var selectedTime: String? = null
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_doctor_appointments, container, false)
datesRV = view.findViewById(R.id.datesRV)
datesRV.setHasFixedSize(true)
linearLayoutManager = LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false)
linearLayoutManager.isAutoMeasureEnabled = false
datesRV.layoutManager = linearLayoutManager
timesRV = view.findViewById(R.id.timesRV)
timesRV.setHasFixedSize(true)
linearLayoutManager = LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false)
linearLayoutManager.isAutoMeasureEnabled = false
timesRV.layoutManager = linearLayoutManager
val bookNowBT = view.findViewById<Button>(R.id.bookNowBT)
bookNowBT.setOnClickListener {
if (isDateSelected && isTimeSelected) {
val i = Intent(activity, ConfirmPaymentActivity::class.java)
i.putExtra(SELECTED_DATE, selectedDate)
i.putExtra(SELECTED_TIME, selectedTime)
startActivity(i)
}
}
getDoctorAvailableAppointments("7ab63fd2461bfb0008b72f5d8c0033fs", "basic")
return view
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* #param param1 Parameter 1.
* #param param2 Parameter 2.
* #return A new instance of fragment DoctorAppointmentsFragment.
*/
// TODO: Rename and change types and number of parameters
#JvmStatic
fun newInstance(param1: String, param2: String) =
DoctorAppointmentsFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
private fun getDoctorAvailableAppointments(doctorID: String, type: String) {
setProgressDialog(this.requireActivity())
if (!InternetConnection.isInternetAvailable(this.requireActivity())) {
alertError(this.requireActivity(),
R.string.no_internet_connection,
R.string.check_internet_connection)
} else {
showProgressDialog()
val builder = ServiceBuilder()
val appointments = builder.getDoctorAvailableReservations()
val call = appointments.getDoctorAvailableReservations(
RequestAvailableReservations(doctorID,type))
call?.enqueue(object : Callback<ResponseAvailableReservations?> {
override fun onResponse(call: Call<ResponseAvailableReservations?>, response: Response<ResponseAvailableReservations?>) {
dismissProgressDialog()
if (!response.isSuccessful) {
alertError(requireActivity(),
R.string.code_not_200,
R.string.try_later)
return
}
val body: ResponseAvailableReservations? = response.body()
if (body == null) {
alertError(requireActivity(),
R.string.null_body,
R.string.try_later)
return
}
val status = body.status
val message = body.message
val data = body.data
if (status == null || message == null || data == null) {
alertError(requireActivity(),
R.string.null_body,
R.string.try_later)
return
}
if (status == "error") {
alertError(requireActivity(), R.string.error, message)
} else if (status == "success") {
dates = data.availableDatesList
datesAdapter = DatesAdapter(dates, this#DoctorAppointmentsFragment)
datesRV.adapter = datesAdapter
times = data.availableTimesList
timesAdapter = TimesAdapter(times, this#DoctorAppointmentsFragment)
timesRV.adapter = timesAdapter
}
}
override fun onFailure(call: Call<ResponseAvailableReservations?>, t: Throwable) {
t.printStackTrace()
dismissProgressDialog()
alertError(requireActivity(),
R.string.fail,
R.string.login_fail)
}
})
}
}
private var dateIndex = -1
override fun onDateClick(position: Int) {
selectedDate = dates?.get(position)
isDateSelected = true
Log.e("selectedDate", "selectedDate")
dateIndex = position
datesAdapter.notifyDataSetChanged()
}
private var timeIndex = -1
override fun onTimeClick(position: Int, itemView: View) {
// selectedTime = times?.get(position)
// isTimeSelected = true
// Log.e("isSelectedTime", "true")
// timeIndex = position
// timesAdapter.notifyItemChanged(position)
// Log.e("timeIndex", timeIndex.toString())
//
// val ctx = itemView.context
// if (timeIndex == position) {
// Log.e("timeIndex", "timeIndex == position")
//
// itemView.background = loadDrawable(ctx, R.drawable.date_time_background_selected)
// itemView.dateNameTV?.setTextColor(loadColor(ctx, android.R.color.white))
// itemView.dateNumTV?.setTextColor(loadColor(ctx, android.R.color.white))
// } else {
// itemView.background = (loadDrawable(ctx, R.drawable.date_time_background))
// itemView.dateNameTV.setTextColor(loadColor(ctx, R.color.black_65))
// itemView.dateNumTV.setTextColor(loadColor(ctx, R.color.black_65))
// }
}
}
Solved
For anyone interested i ended up using a very simple idea. Instead of accessing the RecyclerView selected element from outside the adapter, i did all i wanted to do inside the adapter and used 4 static variables in the Fragment. 2 booleans to check
if date and time are selected or not and 2 Strings having the date and time actually selected.
Inside the Fragment:
companion object {
#JvmStatic var isTimeSelected = false
#JvmStatic var selectedTime = ""
#JvmStatic var isDateSelected = false
#JvmStatic var selectedDate = ""
....
}
Inside the Adapter's onBindViewHolder()
holder.itemLinearLayout.setOnClickListener {
selectedDate = dates?.get(position)!!
isDateSelected = true
listener?.onDateClick(position)
selectedIndex = position
notifyDataSetChanged()
}
val ctx = holder.itemLinearLayout.context
if (selectedIndex == position) {
holder.itemLinearLayout.background = (loadDrawable(ctx, R.drawable.date_time_background_selected))
holder.dateNameTV.setTextColor(loadColor(ctx, android.R.color.white))
holder.dateNumTV.setTextColor(loadColor(ctx, android.R.color.white))
} else {
holder.itemLinearLayout.background = (loadDrawable(ctx, R.drawable.date_time_background))
holder.dateNameTV.setTextColor(loadColor(ctx, R.color.black_65))
holder.dateNumTV.setTextColor(loadColor(ctx, R.color.black_65))
}
And i made sure static variables don't have old values using this
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
isTimeSelected = false
selectedTime = ""
isDateSelected = false
selectedDate = ""
}
I know it's bad practice to put onClickListener inside onBindViewHolder but i don't konw if the static variables thing is good or bad practice. Either way it's working fine for now.
There is many ways to do this, And I recommend the first one.
Using shared ViewModels for communication between fragments in the same activity. The idea is that you will tie up the ViewModel with activity and every fragment can access this ViewModel and in this ViewModel you will have a LiveData object that holds the data and observing it from both fragments and every change in the value of LiveData will affect each fragment. Shared ViewModel
Using interfaces for communication between fragments and it's a hard thing to do these days. Basic communication between fragments
Using EventBus it's easy but not recommended while you can use ViewModel at the first solution. EventBus
Try this
TimesAdapter
class TimesAdapter(
availableTimes: List<String>
) : RecyclerView.Adapter<TimesAdapter.TimeViewHolder>() {
var availableTimes:List<String> = availableTimes
set(value) {
field = value
notifyDataSetChanged()
}
var listener:TimeSelectedListener?=null
var selectedIndex:Int = -1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TimeViewHolder {
return TimeViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.date_time_element, parent, false)
)
}
override fun getItemCount(): Int = availableTimes.count()
override fun onBindViewHolder(holder: TimeViewHolder, position: Int) {
val currentDate: String? = availableTimes[position] // i.e Sun 23/5
val parts = currentDate?.split(" ")
try {
val part1 = parts?.get(0)
val part2 = parts?.get(1)
holder.dateNameTV.text = part2
holder.dateNumTV.text = part1
} catch (e: IndexOutOfBoundsException) {
e.printStackTrace()
}
holder.itemLinearLayout.setOnClickListener {
listener?.onTimeClick(position)
selectedIndex = position
}
if (selectedIndex == position) {
// Selection Code
} else {
// De selection code
}
}
class TimeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var dateNameTV: TextView = itemView.findViewById(R.id.dateNameTV)
var dateNumTV: TextView = itemView.findViewById(R.id.dateNumTV)
var itemLinearLayout: LinearLayout = itemView.findViewById(R.id.item_linear_layout)
}
interface TimeSelectedListener {
fun onTimeClick(position: Int)
}
}
Usage from fragment
times = data.availableTimesList
timesAdapter = TimesAdapter(times).apply {
listener = object :TimesAdapter.TimeSelectedListener{
override fun onTimeClick(position: Int) {
// TODO here is where you should implement when an item is selected
}
}
}
timesRV.adapter = timesAdapter