ActionMode callback never calls onCreateActionMode when started in fragment's OnViewCreated - android

#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?

Related

Problem Implement Update UI with LiveData, Retrofit, Coroutine on Recyclerview : adapter Recyclerview not update

I'm newbie use Kotlin on my dev apps android,
and now, I on step learn to implement Update UI with LiveData, Retrofit, Coroutine on Recyclerview. The My Apps:
MainActivity > MainFragment with 3 Tab fragment > HomeFragment, DashboardFragment, and SettingsFragment
I call function to get data from server on onCreateView HomeFragment, and observe this with show shimmer data on my Recylerview when is loading, try update RecyclerView when success, and show view Failed Load -button refresh when error.
The problem is:
Adapter Recyclerview not Update when success get Data from Server. Adapter still show shimmer data
With case error (no Internet), i show view Failed Load, with button refresh. Tap to refresh, i re-call function to get data server, but fuction not work correct. Recyclerview show last data, not show Failed Load again.
Bellow my code
HomeFragment
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
private lateinit var adapterNews: NewsAdapter
private var shimmerNews: Boolean = false
private var itemsDataNews = ArrayList<NewsModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentHomeBinding.inflate(inflater, container, false)
......
newsViewModel = ViewModelProvider(requireActivity()).get(NewsViewModel::class.java)
//news
binding.frameNews.rvNews.setHasFixedSize(true)
binding.frameNews.rvNews.layoutManager = llmh
adapterNews = NewsAdapter(itemsDataNews, shimmerNews)
binding.frameNews.rvNews.adapter = adapterNews
// Observe
//get News
newsViewModel.refresh()
newsViewModel.newsList.observe(
viewLifecycleOwner,
androidx.lifecycle.Observer { newsList ->
newsList?.let {
binding.frameNews.rvNews.visibility = View.VISIBLE
binding.frameNews.rvNews.isNestedScrollingEnabled = true
binding.frameNews.itemNewsLayoutFailed.visibility = View.GONE
if (it.size == 0)
binding.frameNews.root.visibility = View.GONE
else
getDataNews(it)
}
})
newsViewModel.loading.observe(viewLifecycleOwner) { isLoading ->
isLoading?.let {
binding.frameNews.rvNews.visibility = View.VISIBLE
binding.frameNews.rvNews.isNestedScrollingEnabled = false
binding.frameNews.itemNewsLayoutFailed.visibility = View.GONE
getDataNewsShimmer()
}
}
newsViewModel.loadError.observe(viewLifecycleOwner) { isError ->
isError?.let {
binding.frameNews.rvNews.visibility = View.INVISIBLE
binding.frameNews.itemNewsLayoutFailed.visibility = View.VISIBLE
binding.frameNews.btnNewsFailed.setOnClickListener {
newsViewModel.refresh()
}
}
}
....
return binding.root
}
#SuppressLint("NotifyDataSetChanged")
private fun getDataNewsShimmer() {
shimmerNews = true
itemsDataNews.clear()
itemsDataNews.addAll(NewsData.itemsShimmer)
adapterNews.notifyDataSetChanged()
}
#SuppressLint("NotifyDataSetChanged")
private fun getDataNews(list: List<NewsModel>) {
Toast.makeText(requireContext(), list.size.toString(), Toast.LENGTH_SHORT).show()
shimmerNews = false
itemsDataNews.clear()
itemsDataNews.addAll(list)
adapterNews.notifyDataSetChanged()
}
override fun onDestroyView() {
super.onDestroyView()
_binding=null
}
NewsViewModel
class NewsViewModel: ViewModel() {
val newsService = KopraMobileService().getNewsApi()
var job: Job? = null
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
val newsList = MutableLiveData<List<NewsModel>>()
val loadError = MutableLiveData<String?>()
val loading = MutableLiveData<Boolean>()
fun refresh() {
fetchNews()
}
private fun fetchNews() {
loading.postValue(true)
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = newsService.getNewsList()
withContext(Dispatchers.Main) {
if (response.isSuccessful) {
newsList.postValue(response.body()?.data)
loadError.postValue(null)
loading.postValue(false)
} else {
onError("Error : ${response.message()} ")
}
}
}
loadError.postValue("")
loading.postValue( false)
}
private fun onError(message: String) {
loadError.postValue(message)
loading.postValue( false)
}
override fun onCleared() {
super.onCleared()
job?.cancel()
}
}
NewsAdapter
NewsAdapter(
var itemsCells: List<NewsModel?> = emptyList(),
var shimmer: Boolean ,
) :
RecyclerView.Adapter<ViewHolder>() {
private lateinit var context: Context
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsHomeViewHolder {
context = parent.context
return NewsHomeViewHolder(
NewsItemHomeBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
override fun onBindViewHolder(holder: NewsHomeViewHolder, position: Int) {
holder.bind(itemsCells[position]!!)
}
inner class NewsHomeViewHolder(private val binding: NewsItemHomeBinding) :
ViewHolder(binding.root) {
fun bind(newsItem: NewsModel) {
binding.newsItemFlat.newsTitle.text = newsItem.name
binding.newsItemFlatShimmer.newsTitle.text = newsItem.name
binding.newsItemFlat.newsSummary.text = newsItem.name
binding.newsItemFlatShimmer.newsSummary.text = newsItem.name
if (shimmer) {
binding.layoutNewsItemFlat.visibility = View.INVISIBLE
binding.layoutNewsItemFlatShimmer.visibility = View.VISIBLE
binding.layoutNewsItemFlatShimmer.startShimmer()
} else {
binding.layoutNewsItemFlat.visibility = View.VISIBLE
binding.layoutNewsItemFlatShimmer.visibility = View.INVISIBLE
}
}
}
override fun getItemCount(): Int {
return itemsCells.size
}
I hope someone can help me to solve the problem. thanks, sorry for my English.
You have 3 different LiveDatas, right? One with a list of news data, one with a loading error message, and one with a loading state.
You set up an Observer for each of these, and that observer function gets called whenever the LiveData's value updates. That's important, because here's what happens when your request succeeds, and you get some new data:
if (response.isSuccessful) {
newsList.postValue(response.body()?.data)
loadError.postValue(null)
loading.postValue(false)
}
which means newsList updates, then loadError updates, then loading updates.
So your observer functions for each of those LiveDatas run, in that order. But you're not testing the new values (except for null checks), so the code in each one always runs when the value updates. So when your response is successful, this happens:
newsList observer runs, displays as successful, calls getDataNews
loadError observer runs, value is null so nothing happens
loading observer runs, value is false but isn't checked, displays as loading, calls getDataNewsShimmer
So even when the response is successful, the last thing you do is display the loading state
You need to check the state (like loading) before you try to display it. But if you check that loading is true, you'll have a bug in fetchNews - that sets loading = true, starts a coroutine that finishes later, and then immediately sets loading = false.
I'd recommend trying to create a class that represents different states, with a single LiveData that represents the current state. Something like this:
// a class representing the different states, and any data they need
sealed class State {
object Loading : State()
data class Success(val newsList: List<NewsModel>?) : State()
data class Error(val message: String) : State()
}
// this is just a way to keep the mutable LiveData private, so it can't be updated
private val _state = MutableLiveData<State>()
val state: LiveData<State> get() = _state
private fun fetchNews() {
// initial state is Loading, until we get a response
_state.value = State.Loading
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = newsService.getNewsList()
// if you're using postValue I don't think you need to switch to Dispatchers.Main?
_state.postValue(
// when you get a response, the state is now either Success or Error
if (response.isSuccessful) State.Success(response.body()?.data)
else State.Error("Error : ${response.message()} ")
)
}
}
And then you just need to observe that state:
// you don't need to create an Observer object, you can use a lambda!
newsViewModel.state.observe(viewLifecycleOwner) { state ->
// Handle the different possible states, and display the current one
// this lets us avoid repeating 'binding.frameNews' before everything
with(binding.frameNews) {
// You could use a when block, and describe each state explicitly,
// like your current setup:
when(state) {
// just checking equality because Loading is a -singleton object instance-
State.Loading -> {
rvNews.visibility = View.VISIBLE
rvNews.isNestedScrollingEnabled = false
itemNewsLayoutFailed.visibility = View.GONE
getDataNewsShimmer()
}
// Error and Success are both -classes- so we need to check their type with 'is'
is State.Error -> {
rvNews.visibility = View.INVISIBLE
itemNewsLayoutFailed.visibility = View.VISIBLE
btnNewsFailed.setOnClickListener {
newsViewModel.refresh()
}
}
is State.Success -> {
rvNews.visibility = View.VISIBLE
rvNews.isNestedScrollingEnabled = true
itemNewsLayoutFailed.visibility = View.GONE
// Because we know state is a Success, we can access newsList on it
// newsList can be null - I don't know how you want to handle that,
// I'm just treating it as defaulting to size == 0
// (make sure you make this visible when necessary too)
if (state.newsList?.size ?: 0 == 0) root.visibility = View.GONE
else getDataNews(state.newsList)
}
}
// or, if you like, you could do this kind of thing instead:
itemNewsLayoutFailed.visibility = if (state is Error) VISIBLE else GONE
}
}
You also might want to break that display code out into separate functions (like showError(), showList(state.newsList) etc) and call those from the branches of the when, if that makes it more readable
I hope that makes sense! When you have a single value representing a state, it's a lot easier to work with - set the current state as things change, and make your observer handle each possible UI state by updating the display. When it's Loading, make it look like this. When there's an Error, make it look like this
That should help avoid bugs where you update multiple times for multiple things, trying to coordinate everything. I'm not sure why you're seeing that problem when you reload after an error, but doing this might help fix it (or make it easier to see what's causing it)

Data disappears when scrolling in recycler view

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()
}
}
}
}

ArrayList automatically duplicates items in it

I have created a variable private var deals=ArrayList<Deals>() in a fragment and I have set a click listener in the onCerateView() like the following.
binding.tvAllDeals.setOnClickListener {
viewAllDeals()
}
So that it will trigger the following method
private fun viewAllDeals(){
val intent = Intent(context,ViewAllDealsActivity::class.java)
intent.putExtra("details",deals)
Log.d("Tag2", "Size is ${deals.size}")
startActivity(intent)
}
I have the following function to get the data from the firestore and then I save the result in the variable 'deals'. However, whenever I click the 'tvAllDeals' it shows many images, when I check the size of the 'deals' using Log.d 'Tag1' always shows the correct size, which is 3, whereas 'Tag2' show some random numbers like 6, 9, 24. I try to find out why this is happening but I didn't get any idea. The variable 'deals' is not used anywhere else other than declaring and initializing, to assign the value and to pass it in the 'viewAllDeals()'
private fun getDeals() {
FirestoreClass().getDeals(
onSuccess = { list ->
Result.success(list)
successDeals(list) ///// THIS FUNCTION WILL SHOW THE IMAGES IN A VIEWPAGER
deals.clear()
deals=list
Log.d("Tag1", "Size is ${deals.size}")
},
onFailure = {
}
)
}
Edit:
NOTE: 'Tag3' also shows correct array size like 'Tag1'. However,
private fun successDeals(list: ArrayList<Deals>) {
Log.d("Tag3", "Size is ${deals.size}")
if (list.size > 0) {
binding.vpDeals.visibility = View.VISIBLE
val adapter = DealsAdapter(binding.vpDeals,requireContext(), list)
binding.vpDeals.adapter = adapter
binding.vpDeals.orientation = ViewPager2.ORIENTATION_HORIZONTAL
sliderHandle= Handler()
sliderRun= Runnable {
binding.vpDeals.currentItem=binding.vpDeals.currentItem+1
}
binding.vpDeals.registerOnPageChangeCallback(
object :ViewPager2.OnPageChangeCallback(){
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
sliderHandle.removeCallbacks(sliderRun)
sliderHandle.postDelayed(sliderRun,4000)
}
}
)
} else {
binding.vpDeals.visibility = View.GONE
}
}

Android | Update RecyclerView Adapter on ViewHolder onClick

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

How to make RecyclerView scroll to last position in onCreate?

I was trying to just make the RecyclerView move to last position when the fragment is started, but cant find a solution. The code below works just fine when a new "Pagina" in inserted in database, it moves to last position. But, if I close and reopen it goes back to the top. So, how to make the smoothscroll work when the fragment is created?
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
appDatabase = AppDatabase.getDatabase(requireContext())
val smoothScroller = object : LinearSmoothScroller(context) {
override fun getVerticalSnapPreference(): Int = SNAP_TO_START
}
viewModelo = ViewModelProviders.of(this).get(ViewModelo::class.java)
// Create the observer which updates the UI.
val listPaginas = Observer <List<Pagina>> {
// Update the UI, in this case, a TextView.
recyclerViewAdapter?.addItens(it)
if (it.size < 1) {
smoothScroller.targetPosition = it.size
} else {
smoothScroller.targetPosition = it.size-1
}
recyclerView?.layoutManager?.startSmoothScroll(smoothScroller)
}
viewModelo?.todasPaginas()?.observe(this, listPaginas)
}
Try below changes, it worked!
LinearLayoutManager manager = new LinearLayoutManager(Application.Context);
manager.setStackFromEnd(true);
mRecyclerView.SetLayoutManager(manager);

Categories

Resources