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