I expect my 'heart' icon to change when my ViewHolder item is clicked.
Fortunately, it does this. However, an issue arises as multiple items seems to replicate the button click.
What I mean is:
If I tap the heart on item number 1. Other items throughout the list replicate also change the heart.
Why is this happening and what is a potential fix? I am confused why this issue is occuring as I am referencing the ViewHolder item. Thus, shouldn't it only affect the item I am clicking?
View Holder
fun bind(item: Location) {
heart.setOnClickListener {
item.fav = item.fav != true
heart.setImageDrawable(
when (item.fav) {
false -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_border_heart))
else -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_whole_heart))
})
}
}
onBindViewHolder you need to save list of fave in change item image base on that list otherwise it changes randomly as view recreates
fun bind(item: Location) {
heart.setImageDrawable(
when (item.fav) {
false -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_border_heart))
else -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_whole_heart))
})
heart.setOnClickListener {
item.fav = item.fav != true
heart.setImageDrawable(
when (item.fav) {
false -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_border_heart))
else -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_whole_heart))
})
}
}
You didn't check the view ID in the onClick method. You can set onClick directly on the views as below.
class LocationViewHolder(v: View): RecyclerView.ViewHolder(v), View.OnClickListener {
private val actLoc: TextView = v.findViewById(R.id.location_main)
private val genLoc: TextView = v.findViewById(R.id.location_subtitle)
private val heart: ImageView = v.findViewById(R.id.heart)
private lateinit var item: Location
fun bind(item: Location) {
this.item = item
actLoc.setText(item.actualLocation)
actLoc.setOnClickListener {
Toast.makeText(itemView.context, "${item.cords}", Toast.LENGTH_SHORT).show()
}
genLoc.setText(item.genLocation)
genLoc.setOnClickListener {
Toast.makeText(itemView.context, "${item.cords}", Toast.LENGTH_SHORT).show()
}
heart.setOnClickListener {
item.fav = item.fav != true
heart.setImageDrawable(
when (item.fav) {
false -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_border_heart))
else -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_whole_heart))
})
}
}
However, an issue arises as multiple items seems to replicate the
button click.
it is because of the cell recycling mechanism
heart.setImageDrawable(
when (item.fav) {
false -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_border_heart))
else -> (ContextCompat.getDrawable(itemView.context, R.drawable.ic_whole_heart))
})
should be part of the bind function in the viewholder and not part of the onClick function. What I would expect is
Click informs the viewmodel
Viewmodel update the dataset
Viewmodel informs the recyclerview
Related
Good day. So I currently have data in my recycler view. It is for now only static data. I still have to do the code where I import. My problem however is I have a button that changes the background of a text view. This happens in my adapter. And when I scroll through my list the bg color change gets reverted back to what it was before the button click. I have read a lot of similar problems but could not really find one that explains clearly or work for me. From what I read the data gets reset to the static data because it is currently happening in my onBindViewHolder and I think this changes the data on every new data read(scrolling). I read that I should create a link or a listener and then call it. But It does not make sense to me because if a link is called the same amount of times as the code is executed then it will be the same will it not. Maybe having a condition listener but not sure if this is the way to go.
I am somewhat new to android and kotlin. Have been working with it for a month now. I dont know everything I am doing but I got given a deadline. So sadly there was no time to go and learn the basics. Thank you for any and all help. Please let me know if you need any additional code/information
my adapter
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RowViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.table_list_item, parent, false)
return RowViewHolder(itemView)
}
private fun setHeaderBg(view: View) {
view.setBackgroundResource(R.drawable.table_header_cell_bg)
}
private fun setContentBg(view: View) {
view.setBackgroundResource(R.drawable.table_content_cell_bg)
}
override fun onBindViewHolder(holder: RowViewHolder, position: Int) {
// (TableViewAdapter.DataviewHolder) .bind()
val rowPos = holder.adapterPosition
if (rowPos == 0) {
// Header Cells. Main Headings appear here
holder.itemView.apply {
setHeaderBg(txtWOrder)
setHeaderBg(txtDElNote)
setHeaderBg(txtCompany)
// setHeaderBg(txtAddress)
setHeaderBg(txtWeight)
setHeaderBg(txtbutton1)
setHeaderBg(txtbutton2)
setHeaderBg(txttvdone)
txtWOrder.text = "WOrder"
txtDElNote.text = "DElNote"
txtCompany.text = "Company"
// txtAddress.text = "Address"
txtWeight.text = "Weight"
txtbutton1.text = "Delivered"
txtbutton2.text = "Exception"
txttvdone.text = ""
}
} else {
val modal = Tripsheetlist[rowPos - 1]
holder.itemView.apply {
setContentBg(txtWOrder)
setContentBg(txtDElNote)
setContentBg(txtCompany)
// setContentBg(txtAddress)
setContentBg(txtWeight)
setContentBg(txtbutton1)
setContentBg(txtbutton2)
setContentBg(txttvdone)
txtWOrder.text = modal.WOrder.toString()
txtDElNote.text = modal.DElNote.toString()
txtCompany.text = modal.Company.toString()
// txtAddress.text = modal.Address.toString()
txtWeight.text = modal.Weight.toString()
txtbutton1.text = modal.Button1.toString()
txtbutton2.text = modal.Button2.toString()
txttvdone.text = modal.tvdone.toString()
}
}
holder.apply {
txtbutton1.setOnClickListener {
Log.e("Clicked", "Successful delivery")
txttvdone.setBackgroundResource(R.color.green)
txttvdone.setText("✓")
}
txtbutton2.setOnClickListener {
Log.e("Clicked", "Exception on delivery")
txttvdone.setBackgroundResource(R.color.orange)
txttvdone.setText("x")
}
}
}
class RowViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val txttvdone:TextView = itemView.findViewById<TextView>(R.id.txttvdone)
val txtbutton1:Button = itemView.findViewById<Button>(R.id.txtbutton1)
val txtbutton2:Button = itemView.findViewById<Button>(R.id.txtbutton2)
} class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view){
var txtbutton1 = view.findViewById<Button>(R.id.txtbutton1)
val txtbutton2:Button = itemView.findViewById<Button>(R.id.txtbutton2)
var txttvdone = view.findViewById<TextView>(R.id.txttvdone)
}
I tried (TableViewAdapter.DataviewHolder) .bind() doing this and creating another class as I saw that was done in another thread(Why do values disappear after scrolling in Recycler View?) Its a lot like my problem. I just can't seem to implement his solution to make mine work. ( don't understand his solution fully)
//I am also aware that I am using android extensions which will expire at the end of the year. But for now it works and once I have the code up and running I will start to move over to the newer versions of kotlin.
A RecyclerView, as its name implies, will recycle the views when they go off screen. This means that when the view for an item comes into view, it gets recreated and the onBindViewHolder() is called to fill in the details.
Your onClickListener inside your adapter changes the background of one of the subviews for your cell view. However, that cell will be redrawn if it leaves the screen and comes back.
To get around this, your onClickListener should be changing a property on the data item, and your onBindViewHolder should check that property to determine what background color to display for the subview:
enum class DataState {
Unselected,
Success,
Failure
}
data class DataItem(var state: DataState = DataState.Unselected)
class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
var dataItems: List<DataItem> = emptyList()
fun updateData(data: List<DataItem>) {
dataItems = data
notifyDataSetChanged()
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val dataItem = dataItems[position]
holder.txttvdone.apply {
setBackgroundResource(when (dataItem.state) {
DataState.Unselected -> android.R.color.transparent
DataState.Success -> R.color.green
DataState.Failure -> R.color.orange
})
text = when (dataItem.state) {
DataState.Unselected -> ""
DataState.Success -> "✓"
DataState.Failure -> "x"
}
}
holder.apply {
txtbutton1.setOnClickListener {
Log.e("Clicked", "Successful delivery")
dataItem.state = DataState.Success
notifyDataSetChanged()
}
txtbutton2.setOnClickListener {
Log.e("Clicked", "Exception on delivery")
dataItem.state = DataState.Failure
notifyDataSetChanged()
}
}
}
}
My problem is that live data observer is triggered Observer<T> { state.value = it } with the correct data but compose doesn't kick on recompose. Only when I add an item all changes are propagated. There must some checking on the list itself if it has changed. I guess it doens't compare list items.
#Composable
fun <R, T : R> LiveData<T>.observeAsState(initial: R): State<R> {
val lifecycleOwner = LifecycleOwnerAmbient.current
val state = remember { mutableStateOf(initial) }
onCommit(this, lifecycleOwner) {
val observer = Observer<T> { state.value = it }
observe(lifecycleOwner, observer)
onDispose { removeObserver(observer) }
}
return state
}
val items: List<TrackedActivityWithMetric> by vm.activities.observeAsState(mutableListOf())
LazyColumnForIndexed(
items = items,
Modifier.padding(8.dp)
) { index, item ->
....
MetricBlock(item.past[1], item.activity.id )
}
So behind the scenes there must be some kind hash comparing mechanism preventing rendering same item twice (More elabored answer wanted). The incorrect rendering was caused by property which was not in TrackedActivityWithMetric data class constructor.
Jetpack Compose does not work well with MutableList, you need to use a List and do something like this:
var myList: List<MyItem> by mutableStateOf(listOf())
private set
for adding an item:
fun addItem(item: MyItem) {
myList = myList + listOf(myItem)
}
for editing an item:
fun editItem(item: MyItem) {
val index = myList.indexOf(myItem)
myList = myList.toMutableList().also {
it[index] = myItem
}
}
I am using the latest Paging3 library for my app, which has a gallery screen displaying a list of photos, and a details screen showing more options and info on a photo. I have setup the gallery to fetch a list of photos in my Fragment's onCreate:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// display all photos, sorted by latest
viewModel.getAllPhotos()
}
If successful, the photos are passed to the adapter via submitList, and if the user pulls down the gallery screen, it should trigger a refresh, so I've setup a refreshListener accordingly. I do this on onViewCreated(note that I use ViewBinding):
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentGalleryBinding.bind(view)
viewLifecycleOwner.lifecycle.addObserver(viewModel)
setupGallery()
setupRetryButton()
}
private fun setupGallery() {
// Add a click listener for each list item
adapter = GalleryAdapter{ photo ->
photo.id.let {
findNavController().navigate(GalleryFragmentDirections.detailsAction(it))
}
}
viewModel.uiState?.observe(viewLifecycleOwner, {
binding?.swipeLayout?.isRefreshing = false
adapter.submitData(lifecycle, it)
})
binding?.apply {
// Apply the following settings to our recyclerview
list.adapter = adapter.withLoadStateHeaderAndFooter(
header = RetryAdapter {
adapter.retry()
},
footer = RetryAdapter {
adapter.retry()
}
)
// Add a listener for the current state of paging
adapter.addLoadStateListener { loadState ->
Log.d("GalleryFragment", "LoadState: " + loadState.source.refresh.toString())
// Only show the list if refresh succeeds.
list.isVisible = loadState.source.refresh is LoadState.NotLoading
// do not show SwipeRefreshLayout's progress indicator if LoadState is NotLoading
swipeLayout.isRefreshing = loadState.source.refresh !is LoadState.NotLoading
// Show loading spinner during initial load or refresh.
progressBar.isVisible = loadState.source.refresh is LoadState.Loading && !swipeLayout.isRefreshing
// Show the retry state if initial load or refresh fails.
retryButton.isVisible = loadState.source.refresh is LoadState.Error
val errorState = loadState.source.append as? LoadState.Error
?: loadState.source.prepend as? LoadState.Error
?: loadState.append as? LoadState.Error
?: loadState.prepend as? LoadState.Error
errorState?.let {
swipeLayout.isRefreshing = false
Snackbar.make(requireView(),
"\uD83D\uDE28 Wooops ${it.error}",
Snackbar.LENGTH_LONG).show()
}
}
swipeLayout.apply {
setOnRefreshListener {
isRefreshing = true
adapter.refresh()
}
}
}
On first load, the pulling down the layout triggers the refresh successfully. However a problem arises after I navigate to the details screen. From the details screen, pressing the back button returns the user to the gallery. If the users pulls the layout, the progress indicator appears but adapter.refresh() does not happen. I am at a loss as to how to debug this.
For reference, here is how my ViewModel that's in charge of fetching photos looks like:
class GalleryViewModel(private val getAllPhotosUseCase: GetAllPhotosUseCase): BaseViewModel() {
private val _uiState = MutableLiveData<PagingData<UnsplashPhoto>>()
val uiState: LiveData<PagingData<UnsplashPhoto>>? get() = _uiState
fun getAllPhotos() {
compositeDisposable += getAllPhotosUseCase.getAllPhotos()
.cachedIn(viewModelScope)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onNext = { _uiState.value = it },
onError = {
it.printStackTrace()
}
)
}
}
The GetAllPhotosUseCase forwards the getAllPhotos call to a Repository implementation that contains the following:
class UnsplashRepoImpl(private val unsplashApi: UnsplashApi): UnsplashRepo {
override fun getAllPhotos(): Observable<PagingData<UnsplashPhoto>> = Pager(
config = PagingConfig(Const.PAGE_SIZE),
remoteMediator = null,
// Always create a new UnsplashPagingSource object. Failure to do so would result in a
// IllegalStateException when adapter.refresh() is called--
// Exception message states that the same PagingSource was used as the prev request,
// and a new PagingSource is required
pagingSourceFactory = { UnsplashPagingSource(unsplashApi) }
).observable
....
}
My RxPagingSource is setup like this:
class UnsplashPagingSource (private val unsplashApi: UnsplashApi)
: RxPagingSource<Int, UnsplashPhoto>(){
override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, UnsplashPhoto>> {
val id = params.key ?: Const.PAGE_NUM
return unsplashApi.getAllPhotos(id, Const.PAGE_SIZE, "latest")
.subscribeOn(Schedulers.io())
.map { response ->
response.map { it.toUnsplashPhoto() }
}
.map<LoadResult<Int, UnsplashPhoto>> { item ->
LoadResult.Page(
data = item,
prevKey = if (id == Const.PAGE_NUM) null else id - 1,
nextKey = if (item.isEmpty()) null else id + 1
)
}
.onErrorReturn { e -> LoadResult.Error(e) }
}
}
Can anyone point me in the right direction with this?
EDIT: As Jay Dangar has said, moving viewModel.getAllPhotos() to onResume would make the call to adapter.refresh() trigger successfully. However, I do not want to fetch all photos every time I navigate from the details screen to the gallery. To avoid this, instead of calling adapter.refresh() when the layout is pulled, I just call viewModel.getAllPhotos() instead.
I still don't understand why the accepted answer works, but I assume that adapter.refresh() only works when a new PagingSource is created or something.
put your refersh logic in onResume() instead of onCreate(), it's an issue of lifecycle management.
I'm creating a method to recursively search for a View inside an ArrayList. It will loop through this ArrayList and, if it contains an ArrayList, it will be searched for Views too, and so on, until finding a View to return. This is so I can make whatever View is inside there invisible.
fun searchForView(arrayList: ArrayList<*>): View {
arrayList.forEach { item ->
if (item is View) {
return item
} else if (item is ArrayList<*>) {
item.forEach {
searchForView(it as ArrayList<*>)
}
}
}
} // Error here, needs to return a View
So I will use it like this:
someArrayList.forEach {
searchForView(someArrayList).visibility = View.INVISIBLE
}
However it is giving me an error because there needs to be a return someView statement near the end of the method. Whenever I call it, the ArrayList being searched will always have a View. So what should I be returning here at the end, knowing that whatever View found will already be returned?
You can set inside function and don't return anything
fun searchForView(arrayList: ArrayList<*>){
arrayList.forEach { item ->
if (item is View) {
item.visibility = View.INVISIBLE // set here
} else if (item is ArrayList<*>) {
item.forEach {
searchForView(it as ArrayList<*>)
}
}
}
}
You should use searchForView(item) instead of item.forEach { searchForView(it as ArrayList<*>) } as #IR42 suggested since you don't know each item in arraylist is an arraylist or not.
Your function is not compileable because it's supposed to return a View, but you aren't returning a View in the else branch or if you reach the end of the input list without finding a View.
However, if all this function does is return a View, then it is not usable for your requirement to set all views' visibility. It would only return a single View.
Instead, you can pass a function argument for what to do to each view it finds. There's no need to return anything.
fun ArrayList<*>.forEachViewDeep(block: (View) -> Unit) {
for (item in this) when (item) {
is View -> block(item)
is ArrayList<*> -> item.forEachViewDeep(block)
}
}
And use it like:
someArrayList.forEachViewDeep {
it.visibility = View.INVISIBLE
}
If it's very deeply nested, you might want to rearrange this function to be tail-recursive like this:
tailrec fun List<*>.forEachViewDeep(block: (View) -> Unit) {
for (item in this) {
if (item is View)
block(item)
}
filterIsInstance<ArrayList<*>>().flatten().forEachViewDeep(block)
}
I was trying to do the same thing like you before
and this is what I've made
class VisibilitySwitcher(private val mutableViewSet: MutableSet<View?>, private val onCondition: Boolean = true){
fun betweenVisibleOrGone(){
if(onCondition)
mutableViewSet.forEach {
when (it?.visibility) {
View.VISIBLE -> {it.visibility = View.GONE}
View.GONE -> {it.visibility = View.VISIBLE}
}
}
}
fun betweenVisibleOrInvisible(){
if(onCondition)
mutableViewSet.forEach {
when (it?.visibility) {
View.VISIBLE -> {it.visibility = View.INVISIBLE}
View.INVISIBLE -> {it.visibility = View.VISIBLE}
}
}
}
fun betweenInVisibleOrGone(){
if(onCondition)
mutableViewSet.forEach {
when (it?.visibility) {
View.INVISIBLE -> {it.visibility = View.GONE}
View.GONE -> {it.visibility = View.INVISIBLE}
}
}
}
}
Usage Example
class LoginActivity : BaseActivity() {
#Inject
#ViewModelInjection
lateinit var viewModel: LoginVM
private lateinit var mutableViewSet: MutableSet<View?>
override fun layoutRes() = R.layout.activity_login
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
facebookBtn.setOnClickListener { handleClickEvent(it) }
googleBtn.setOnClickListener { handleClickEvent(it) }
}
private fun handleClickEvent(view: View) {
when (view) {
facebookBtn -> { viewModel.smartLoginManager.onFacebookLoginClick() }
googleBtn -> { viewModel.smartLoginManager.onGoogleLoginClick() }
}
mutableViewSet = mutableSetOf(facebookBtn, googleBtn, progressBar)
VisibilitySwitcher(mutableViewSet).betweenVisibleOrGone() // <----- Use without Condition
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
VisibilitySwitcher(mutableViewSet, resultCode != -1).betweenVisibleOrGone() //<-- Use with Conditions
viewModel.smartLoginManager.onActivityResultCallBack(requestCode, resultCode, data)
super.onActivityResult(requestCode, resultCode, data)
}
}
The point is whenever you click login from facebook or google button
It will set visibility for facebook and google to be gone and set progressbar(the default of progressbar is View.GONE) to be visible
At override fun onActivityResult()
if the resultcode is not -1 it means that it got some error or cancel
so it will switch back the progressbar to be gone and change facebook and google button to be visible again
If you want to fix your own code I would do this
fun searchForView(mutableViewSet: MutableSet<View?>){
mutableViewSet.forEach {
when (it?.visibility) {
View.VISIBLE -> {it.visibility = View.INVISIBLE}
View.INVISIBLE -> {it.visibility = View.VISIBLE} //<-- you can delete this if you don't want
}
}
}
Or very short form
fun searchForView(mutableViewSet: MutableSet<View?>) = mutableViewSet.forEach { when (it?.visibility) {View.VISIBLE -> it.visibility = View.INVISIBLE } }
Usage
val mutableViewSet = mutableSetOf(your view1,2,3....)
searchForView(mutableViewSet)
if it has to use arrayList: ArrayList<*> Then
fun searchForView(arrayList: ArrayList<*>) = arrayList.forEach{ if (it is View) it.visibility = View.INVISIBLE
#ExperimentalCoroutinesApi
class WeekdayTimesFragment #Inject constructor(viewModelFactory: SavedStateViewModelFactory.Factory)
:Fragment(R.layout.fragment_weekday_times), WeekdayAlarmTimesAdapter.OnWeekdayAlarmTimePressed, ActionModeListener {
private lateinit var weekdayAlarmTimesAdapter: WeekdayAlarmTimesAdapter
private val weekdayTimesViewModel
by viewModels<WeekdayTimesViewModel> { viewModelFactory.create(this) }
override val actionCallback = PrimaryActionCallback(this)
private var weekday: Int = 0
private val isActionModeActive: Boolean
get() = weekdayTimesViewModel.isActionModeActive
private val numSelected: Int
get() = weekdayTimesViewModel.selectedTimePositions.size
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
weekday = requireArguments().get("WEEKDAY") as Int
if(isActionModeActive) { //action mode may be active prior to rotation
startActionMode() //for some reason, starting action mode here does not work....
}
initRecyclerView() //will determine what recyclerview items are selected via the viewmodel
weekdayTimesViewModel.getTimesAndAlarmsForSelectedWeekday(Weekday[weekday])
.observe(viewLifecycleOwner) { times->
weekdayAlarmTimesAdapter.submitWeekdayTimes(times)
}
}
override fun alarmTimePressed(wasSelected: Boolean, position: Int, time: WeekdayTime) {
if(wasSelected) { //this item was selected prior to being selected, effectively unselecting it
weekdayTimesViewModel.selectedTimePositions.remove(position)
} else {
weekdayTimesViewModel.selectedTimePositions[position] =
Time(time.hour, time.minute, Weekday[weekday], time.alarm.alarmId, time.isDisabled, time.id)
}
if(numSelected == 0 && isActionModeActive) { //no more times are selected, turn off action mode
destroyActionMode() //need to de-select recyclerview items
} else {
if (!isActionModeActive) {
startActionMode()
} else {
actionCallback.changeTitle("$numSelected selected")
}
}
}
private fun startActionMode() {
actionCallback
.startActionMode(
requireView(),
R.menu.toolbar_contextual_menu,
"$numSelected selected")
//fragment hosting the viewpager
(requireParentFragment() as ActionModeListenerController).currentActionModeListener = this
weekdayTimesViewModel.isActionModeActive = true
}
override fun onActionItemClick(item: MenuItem?) {
item?.let {
when(it.itemId){
R.id.toolbar_disable_alarm-> {
requireContext().createGenericAlertDialog(
message = if(numSelected == 1) "Are you sure you want to disable this alarm?"
else "Are you sure you want to disable these alarms?",
positiveListener = { _, _ ->
weekdayTimesViewModel.disableSelectedTimes()
showUndoSnackbar(true)
}
).show()
}
R.id.toolbar_delete_alarms-> {
requireContext().createGenericAlertDialog(
message = if(numSelected == 1) "Are you sure you want to delete this alarm?"
else "Are you sure you want to delete these alarms?",
positiveListener = { _, _ ->
weekdayTimesViewModel.deleteSelectedTimes()
showUndoSnackbar(false)
}
).show()
}
}
}
}
//when we aren't destroying the view but need to end action mode (changing tab or after we confirm an action)
//we may want to do specific things specific to certain listeners, e.g. de-select certain recyclerview items.
override fun destroyActionMode() {
actionCallback.finishActionMode()
weekdayTimesViewModel.isActionModeActive = false
//also un-select all of the elements that have been selected
weekdayTimesViewModel.selectedTimePositions.clear()
//redraw recyclerview as well
rv_weekday_times.resetAdapterState()
}
private fun showUndoSnackbar(disablingTimes: Boolean){
val message =
if(disablingTimes) "Your alarm times have been disabled." else "Your alarm times have been deleted."
Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG).setAction("UNDO"){
if(disablingTimes)
weekdayTimesViewModel.restoreDisabledTimes()
else
weekdayTimesViewModel.restoreDeletedTimes()
}.setActionTextColor(ContextCompat.getColor(requireContext(), R.color.colorPrimary))
.setAnchorView(requireActivity().bottom_nav_view).show()
}
//when rotating screen or navigating we can just end action mode. The state of whether we are
//in action mode either doesn't matter (navigation) or is already persisted, along with the selected
//items in the viewmodel(rotation)
override fun onDestroyView() {
actionCallback.finishActionMode()
super.onDestroyView()
}
private fun initRecyclerView() {
weekdayAlarmTimesAdapter = WeekdayAlarmTimesAdapter(this, weekdayTimesViewModel.selectedTimePositions.keys)
rv_weekday_times.apply {
adapter = weekdayAlarmTimesAdapter
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
}
weekdayAlarmTimesAdapter.selectedItems.clear()
//clears selected items as these items no longer remain consistently for the lifecycle of the view
}
}
So my ActionMode works great when I get a callback (alarmTimePressed) which is through a click listener set in the recyclerview's OnBindViewHolder. I start my ActionMode and it works great, I save whether ActionMode is active in my viewmodel, which works well upon rotation, as if it is active prior to rotation, it still remains active. As you can see in my OnViewCreated, I call startActionMode() when ActionMode is meant to be active. The problem is that when I start it from there, the onCreateActionMode callback is never called. This is strange because even after rotating, if I select an item in the RecyclerView it correctly launches ActionMode. The view I am passing to startActionMode is the same in both cases. If you need the code for PrimaryActionCallback, it's here: https://hastebin.com/vepujuhefe.m. Didn't think it was very necessary but in case you felt like it was necessary to see :). Anyone know what's going on here?