I have a RecyclerView that has many ViewHolder types. One of those ViewHolder (GoalCardViewHolder) holds a View that is just a container for displaying a Fragment. In my chat app this ViewHolder is used by two different ViewTypes, this is for UserInput and ChatItem view types.
The UserInput type represents when the user needs to act.
User Input
The ChatItemType represent any of the other elements in the Chat, when the user inputs a new goal, a ChatItemType.GoalCard is created: ChatItem.GoalCard
Note that both types use the same ViewHolder.
The problem
When the user is trying to create a new Goal , I expect a new UserInput to be created at the bottom of the chat. However, the previous GoalCardViewHolder is being re-used(this happens a few times, until the list grows long enough that the ViewHolder gets recycled, when that happens the new view is added at the bottom as expected).
The source code
Apologies for the lengthy pasting but the Adapter and MainChatFragment are very complex (please do let me know if you need anything else):
MainChatFragment.kt
class MainChatFragment(
private val viewModel: MainChatViewModel,
private val chatAdapter: ChatAdapter,
private val chatToolbar: ChatToolbar,
private val chatDaySelector: ChatDaySelector,
private val dayOfStudyProvider: DayOfStudyProvider,
private val subjectMenuFragment: SubjectMenuFragment,
private val chatSpeedHelper: ChatSpeedHelper,
) : Fragment() {
private lateinit var dayOfStudySelectorView: DayOfStudySelectorView
private lateinit var binding: FragmentChatBinding
private var scrollDistance: Int = 0
private val cancelOnPauseScope = CancelOnPauseCoroutineScope(this)
private val bufferedChatItems = mutableListOf<IChatItem>()
private val arguments: MainChatFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentChatBinding.inflate(inflater)
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
dayOfStudySelectorView = binding.dashboardView
dayOfStudySelectorView.init(dayOfStudyProvider, chatDaySelector)
setupChatItemList()
subjectMenuFragment.closeMenuCallback = { shouldDiscardItems ->
if (!shouldDiscardItems) {
bufferedChatItems.forEach {
addChatItem(it)
}
}
bufferedChatItems.clear()
}
chatAdapter.removeLastUserInput = this::removeLastUserInput
chatAdapter.subjectMenuButtonOnClick = this::openSubjectMenuFragment
chatToolbar.setMenuButtonOnClickListener {
openSubjectMenuFragment()
}
}
private fun removeLastUserInput() {
viewModel.removeLastUserInputAndLogMessage()
}
private fun openSubjectMenuFragment() {
if (!subjectMenuFragment.isAdded) {
subjectMenuFragment.show(requireActivity().supportFragmentManager, "subjectMenu")
}
}
override fun onResume() {
super.onResume()
observeChatItemActions() // Should be last entry in here
}
#Suppress("IMPLICIT_CAST_TO_ANY")
private fun observeChatItemActions() {
cancelOnPauseScope.launch {
viewModel.chatItemActionFlow()
.onStart { viewModel.onUserEnteredChat(arguments.hasJustActivated) }
.transform {
if (it is ChatItemAction.AddChatItemAction) {
val delayMs = chatSpeedHelper.itemDelay(it.chatItem)
if (delayMs > 0) {
delay(delayMs)
}
if (subjectMenuFragment.isVisible) {
bufferedChatItems.add(it.chatItem)
return#transform
}
}
emit(it)
}
.collect {
if (!isVisible) {
return#collect
}
when (it) {
is ChatItemAction.AddChatItemAction -> {
addChatItem(it.chatItem)
}
is ChatItemAction.SetChatItemsAction -> {
chatAdapter.showWaitingBubble = false
binding.emptyChatText.isVisible =
it.chatItems.isEmpty() && !it.chatEnabled
chatAdapter.items = it.chatItems.map { item -> ChatViewItem(item) }
}
is ChatItemAction.RemoveAllItemsAction -> {
chatAdapter.removeAllItems()
}
is ChatItemAction.RemoveLastItemAction -> {
chatAdapter.conditionallyRemoveLastChatItem(it.predicate)
}
}.let { } // Make when exhaustive
}
}
}
private fun addChatItem(chatItem: IChatItem) {
if (chatItem is ChatItem) {
chatItem.chatBackgroundAction?.let { chatBackgroundAction ->
viewModel.launchChatBackgroundAction(chatBackgroundAction)
}
}
val chatViewItem = ChatViewItem(chatItem)
chatAdapter.showWaitingBubble = chatViewItem.shouldShowPendingBubble()
chatAdapter.addChatItem(chatViewItem)
binding.emptyChatText.isVisible = false
}
private fun setupChatItemList() {
val linearLayoutManager = LinearLayoutManager(requireView().context)
linearLayoutManager.stackFromEnd = true
with(binding.chatList) {
layoutManager = linearLayoutManager
adapter = chatAdapter
addItemDecoration(
DividerItemDecoration(
requireView().context,
linearLayoutManager.orientation
).apply {
setDrawable(
ContextCompat.getDrawable(
requireView().context,
R.drawable.divider_chat
)!!
)
})
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
scrollDistance += dy
if (dy < 0 &&
(scrollDistance.absoluteValue >= measuredHeight || !binding.chatList.canScrollVertically(
-1
))
) {
dayOfStudySelectorView.revealDaySelector()
} else if (dy > 0) {
dayOfStudySelectorView.hideDaySelector()
}
}
})
}
}
}
ChatAdapter.kt
class ChatAdapter(
private val widgetHandler: WidgetHandler,
private val coroutineScope: CoroutineScope,
) : RecyclerView.Adapter<BaseChatItemViewHolder>() {
private lateinit var context: Context
private lateinit var layoutInflater: LayoutInflater
private var recyclerView: RecyclerView? = null
private val lastDivider = ChatViewItem(ChatItemType.LastDivider)
private val pendingMessage = ChatViewItem(ChatItemType.PendingMessage)
private val staticPendingView = false
var showWaitingBubble: Boolean = true
set(value) {
if (value != showWaitingBubble) {
setShowingWaitingBubble(value)
}
field = value
}
var removeLastUserInput: () -> Unit = {}
var subjectMenuButtonOnClick: () -> Unit = {}
lateinit var onGoalSettingStarted: (GoalCardProgressListener) -> Unit
private fun setShowingWaitingBubble(value: Boolean): Int {
var updatedIndex = -1
if (_items.isNotEmpty()) {
if (!value) {
_items.lastIndexOf(pendingMessage)
.takeIf { it >= 0 }
?.let {
_items.removeAt(it)
updatedIndex = it
}
} else {
if (!hasPendingBubble() && shouldShowPendingBubble(getLastRealChatItem().second)) {
val divider = hasLastDivider()
updatedIndex = Math.max(0, _items.size - divider.toBitInteger())
_items.add(updatedIndex, pendingMessage)
}
}
}
if (updatedIndex != -1) {
scrollToBottom()
}
return updatedIndex
}
private var _items = ArrayList<ChatViewItem>()
var items: List<ChatViewItem>
get() = _items
set(value) {
_items.clear()
_items.addAll(value)
if (value.isNotEmpty()) {
setShowingWaitingBubble(showWaitingBubble)
}
_items.add(lastDivider)
notifyDataSetChanged()
scrollToBottom()
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
this.recyclerView = recyclerView
context = recyclerView.context
layoutInflater = LayoutInflater.from(recyclerView.context)
recyclerView.setItemViewCacheSize(20)
}
override fun getItemCount(): Int = items.size
override fun getItemViewType(position: Int): Int {
val chatViewItem = items[position]
return when (chatViewItem.type) {
ChatItemType.UserInput, ChatItemType.DisabledUserInput -> {
val inputType = chatViewItem.inputType ?: npe("chatViewItem.inputType")
when (inputType) {
is InputType.GoalSettingCard -> R.layout.goal_card
else -> -1 // deleted other viewTypes for stackOverflow
}
}
ChatItemType.GoalCard -> R.layout.goal_card
else -> -1 // deleted other viewTypes for stackOverflow
}
}
#SuppressLint("SetJavaScriptEnabled")
override fun onBindViewHolder(holder: BaseChatItemViewHolder, position: Int) {
val chatViewItem = items[position]
when (chatViewItem.type) {
ChatItemType.UserInput, ChatItemType.DisabledUserInput -> {
val inputType = chatViewItem.inputType ?: npe("chatViewItem.inputType")
when (inputType) {
is InputType.GoalSettingCard -> {
val goalCardFragment = GoalCardFragment()
goalCardFragment.initialProgress =
GoalBuilder() // Fixme use goal builder from topic interactor
onGoalSettingStarted.invoke(goalCardFragment)
goalCardFragment.arguments =
bundleOf(GoalCardFragment.FLOW_TYPE_EXTRA to inputType.flowType)
goalCardFragment.onCanceledCallback =
(holder as GoalCardViewHolder).canceledListener
goalCardFragment.onCompletedCallback = holder.completedListener
if (inputType.flowType == FlowType.COMPLETE_GOAL) {
context.activity().supportFragmentManager
.beginTransaction()
.replace(
holder.binding.goalCardContainer.id,
goalCardFragment,
GOAL_CARD_FRAGMENT_AS_USER_INPUT
)
.commit()
} else {
TODO("Need implementation")
}
}
}
}
ChatItemType.GoalCard -> {
gson.fromJson(chatViewItem.text, GoalCardParameters::class.java)
GoalCardFragment().let {
if (chatViewItem.data is GoalSettingData) {
it.initialProgress = (chatViewItem.data as GoalSettingData).goalBuilder
}
context.activity().supportFragmentManager.beginTransaction()
.replace(
(holder as GoalCardViewHolder).binding.goalCardContainer.id,
it as GoalCardFragment,
GOAL_CARD_FRAGMENT_AS_CHAT_ITEM
)
.commit()
}
}
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
#LayoutRes viewType: Int
): BaseChatItemViewHolder {
val itemView = layoutInflater.inflate(viewType, parent, false)
return when (viewType) {
R.layout.goal_card -> {
GoalCardViewHolder(itemView).apply {
setOnCanceledListener {
coroutineScope.launch {
removeLastUserInput()
removeFragmentByTag(GOAL_CARD_FRAGMENT_AS_USER_INPUT)
widgetHandler.onWidgetDone(
userResponse = UserResponse(
WIDGET_DISMISSED_WITH_VALUE + "cancelled"
)
)
}
}
setOnCompletedListener {
coroutineScope.launch {
val goalData = UserResponseData.GoalData(it)
removeLastUserInput()
removeFragmentByTag(GOAL_CARD_FRAGMENT_AS_USER_INPUT)
widgetHandler.onWidgetDone(
userResponse = UserResponse(
WIDGET_DISMISSED_WITH_VALUE + "completed", data = goalData
)
)
// TODO api
}
}
}
}
else -> throw IllegalStateException("View type not mapped to a view holder")
}
}
private fun removeFragmentByTag(tag: String) {
context.activity().supportFragmentManager.apply {
findFragmentByTag(tag)?.let {
beginTransaction()
.remove(it)
.commit()
}
}
}
fun addChatItem(chatItemView: ChatViewItem) {
val shouldShowPendingBubble = shouldShowPendingBubble(chatItemView)
setShowingWaitingBubble(shouldShowPendingBubble)
val hasPendingBubble = hasPendingBubble()
val insertIndex = Math.max(
0,
itemCount - hasLastDivider().toBitInteger() - hasPendingBubble.toBitInteger()
)
_items.add(insertIndex, chatItemView)
// sending notifyItemRangeChanged instead of notifyItemInserted is for a feeling of
// scrolling also the pendingBubble, otherwise it looks like it's static view and it's not part of scrolling content
if (staticPendingView && shouldShowPendingBubble) {
notifyItemInserted(insertIndex)
} else {
notifyItemRangeChanged(insertIndex, 1 + hasPendingBubble.toBitInteger())
}
when (chatItemView.inputType) {
is InputType.FreeText,
is InputType.FreeTextWithLimit ->
// for some reason RecyclerView scrolls up little bit when EditText is shown
// basically aligns to bottom of line, not bottom of EditText =>
// let RV do this nonsense and scroll down little bit later
// however we have to do it sooner, we try to show the keyboard then
recyclerView?.postDelayed(UiConst.keyboardHandlingDelay / 2) {
scrollToBottom()
}
else -> scrollToBottom()
}
}
override fun onViewAttachedToWindow(holder: BaseChatItemViewHolder) {
super.onViewAttachedToWindow(holder)
holder.onAttached()
}
override fun onViewDetachedFromWindow(holder: BaseChatItemViewHolder) {
super.onViewDetachedFromWindow(holder)
holder.onDetached()
}
fun removeAllItems() {
_items.clear()
notifyDataSetChanged()
}
fun notifyItemChanged(chatViewItem: ChatViewItem) {
notifyItemChanged(items.indexOf(chatViewItem))
}
fun conditionallyRemoveLastChatItem(predicate: (ChatViewItem) -> Boolean) {
val (index, item) = getLastRealChatItem()
if (item != null && predicate(item)) {
_items.removeAt(index)
notifyItemRemoved(index)
}
}
private fun hasLastDivider(): Boolean = _items.lastOrNull()?.type == ChatItemType.LastDivider
private fun hasPendingBubble(): Boolean {
return (items.lastOrNull(1)?.type == ChatItemType.PendingMessage) ||
(items.lastOrNull(0)?.type == ChatItemType.PendingMessage)
}
private fun getLastRealChatItem(): Pair<Int, ChatViewItem?> {
if (!items.isEmpty()) {
(items.size - 1 downTo 0).forEach { i ->
val item = items[i]
when (item.type) {
ChatItemType.LastDivider,
ChatItemType.PendingMessage -> {
/*ignore*/
}
else -> return Pair(i, item)
}
}
}
return Pair(-1, null)
}
private val skipPendingBubbleFor = listOf(
ChatItemType.UserInput,
ChatItemType.LastBotMessage
)
private fun shouldShowPendingBubble(chatItemView: ChatViewItem?): Boolean {
return showWaitingBubble && !skipPendingBubbleFor.contains(chatItemView?.type)
}
private fun scrollToBottom() {
(recyclerView?.layoutManager as? LinearLayoutManager)?.scrollToPosition(itemCount - 1)
}
companion object {
const val GOAL_CARD_FRAGMENT_AS_USER_INPUT = "goal-card-fragment-user-input"
const val GOAL_CARD_FRAGMENT_AS_CHAT_ITEM = "goal-card-fragment-chat-item"
}
}
abstract class BaseChatItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
open fun onAttached() {}
open fun onDetached() {}
open fun bind(chatViewItem: ChatViewItem) {}
abstract val binding: ViewBinding?
}
abstract class BaseTextChatItemViewHolder(itemView: View) : BaseChatItemViewHolder(itemView) {
abstract val chatText: TextView
}
class GoalCardViewHolder(itemView: View) : BaseChatItemViewHolder(itemView) {
lateinit var canceledListener: (() -> Unit)
lateinit var completedListener: ((GoalBuilder) -> Unit)
fun setOnCanceledListener(canceledListener: () -> Unit) {
this.canceledListener = canceledListener
}
fun setOnCompletedListener(completedListener: (GoalBuilder) -> Unit) {
this.completedListener = completedListener
}
override val binding: GoalCardBinding = GoalCardBinding.bind(itemView)
}
goal_card.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/goal_card_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
The issue and lesson learned was 'never use a Fragment in a RecyclerView.ViewHolders
I converted my fragment to a custom view and the problem is no longer happening.
I think it must have been an issue with the Fragment's lifecycle.
Related
I want to navigate 2 different fragments using same adapter according to the type of my model. But my problem is I can't handle onItemClicked with two different models. As you can see:
private val onItemClicked: (WordsWithMeanings) -> Unit
FavoriteFragment
class FavoriteFragment : Fragment(R.layout.fragment_favorite) {
private var _binding: FragmentFavoriteBinding? = null
private val binding get() = _binding!!
private val viewModel: WordViewModel by activityViewModels {
WordViewModelFactory(
(activity?.application as WordApplication).database.wordDao()
)
}
private lateinit var adapter: FavoriteListAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentFavoriteBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = FavoriteListAdapter(viewModel) {
val action = FavoriteFragmentDirections.actionFavouriteFragmentToWordDetailFragment(it).
this.findNavController().navigate(action)
}
binding.recyclerView.adapter = adapter
viewModel.allWordsWithMeanings.observe(this.viewLifecycleOwner) { items ->
items.let {
for (i in items) {
if (i.word.isFavorite == true) {
adapter.mData.add(i)
}
}
}
favoriteFragmentFavoriteStatus()
}
viewModel.allProverbs.observe(this.viewLifecycleOwner) { items ->
items.let {
for (i in items) {
if (i.isFavorite == true) {
adapter.mData.add(i)
}
}
}
}
binding.recyclerView.layoutManager = LinearLayoutManager(this.context)
}
}`
```
FavoriteListAdapter
private const val WORD_VIEW_TYPE = 0
private const val PROVERB_VIEW_TYPE = 1
class FavoriteListAdapter(
private val viewModel: WordViewModel,
private val onItemClicked: (WordsWithMeanings) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val mData = mutableListOf<Any>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
WORD_VIEW_TYPE -> WordItemViewHolder(
FavoriteItemsBinding.inflate(
inflater,
parent,
false
)
)
PROVERB_VIEW_TYPE -> ProverbItemViewHolder(
FavoriteItemsBinding.inflate(
inflater,
parent,
false
)
)
else -> throw IllegalArgumentException("Invalid view type")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val element = mData[position]
when (getItemViewType(position)) {
WORD_VIEW_TYPE -> {
holder.itemView.setOnClickListener {
onItemClicked(element as WordsWithMeanings)
}
(holder as WordItemViewHolder).bind(
element as WordsWithMeanings,
viewModel
)
}
PROVERB_VIEW_TYPE -> {
(holder as ProverbItemViewHolder).bind(element as Proverb, viewModel)
}
}
}
class WordItemViewHolder(private var binding: FavoriteItemsBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(favoriteWordsWithMeanings: WordsWithMeanings, viewModel: WordViewModel) {
binding.apply {
title.text = favoriteWordsWithMeanings.word.word
content.text = favoriteWordsWithMeanings.means[0].wordMean
favoriteButton.isChecked =
favoriteWordsWithMeanings.word.isFavorite == true
favoriteButton.setOnClickListener {
if (favoriteButton.isChecked) {
favoriteButton.isChecked = true
viewModel.updateFavoriteWord(favoriteWordsWithMeanings, true)
} else {
favoriteButton.isChecked = false
viewModel.updateFavoriteWord(favoriteWordsWithMeanings, false)
}
}
}
}
}
class ProverbItemViewHolder(private var binding: FavoriteItemsBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(favoriteProverb: Proverb, viewModel: WordViewModel) {
binding.apply {
title.text = favoriteProverb.proverb
content.text = favoriteProverb.proverbMean
favoriteButton.isChecked =
favoriteProverb.isFavorite == true
favoriteButton.setOnClickListener {
if (favoriteButton.isChecked) {
favoriteButton.isChecked = true
viewModel.updateFavoriteProverb(favoriteProverb, true)
} else {
favoriteButton.isChecked = false
viewModel.updateFavoriteProverb(favoriteProverb, false)
}
}
}
}
}
override fun getItemCount(): Int {
return mData.size
}
override fun getItemViewType(position: Int): Int {
return when (mData[position]) {
is WordsWithMeanings -> WORD_VIEW_TYPE
is Proverb -> PROVERB_VIEW_TYPE
else -> throw IllegalArgumentException("Unsupported type")
}
}
}
So I only navigate to one fragment. What I want to do is if clicked item is 'WordsWithMeanings' navigate to WordDetailFragment, if clicked item is 'Proverb' navigate to ProverbDetailFragment. Is there proper way to do this?
SOLUTION:
Firstly I changed this
private val onItemClicked: (WordsWithMeanings) -> Unit
to this
private val onItemClicked: (Any) -> Unit
After that using action like this solved my problem.
action = if (it is WordsWithMeanings) {
FavoriteFragmentDirections.actionFavouriteFragmentToWordDetailFragment(it)
} else {
FavoriteFragmentDirections.actionFavouriteFragmentToProverbDetailFragment(it as Proverb)
}
this.findNavController().navigate(action as NavDirections)
You are passing a WordsWithMeaning item in your adapter onItemClicked callback.
You can check if the item passed to your callback is of WordsWithMeaning type or Proverb type and then act accordingly:
adapter = FavoriteListAdapter(viewModel) {
val action = if (it is Proverb) {
// navigate to proverb details
} else {
// navigate to word details
}
}
I have a list.I am sorting that list based on the selected chip.Here is my sorting code:
SortingUtil
enum class SortingUtil {
DESCBYTOTALCASES,
ASCBYTOTALCASES,
DESCBYDEATHCASES,
ASCBYDEATHCASES,
DESCBYALPHA,
ASCBYALPHA
}
CountriesFragment
#Inject
lateinit var mAdapter: CountriesFragmentAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initRecycler()
observeViewModel()
initChips()
}
private fun initChips() {
binding.chipGroup.setOnCheckedChangeListener { _, checkedId ->
when (checkedId) {
R.id.totalDesc -> {
mViewModel.rearangeCountries(SortingUtil.DESCBYTOTALCASES)
}
R.id.totalAsc -> {
mViewModel.rearangeCountries(SortingUtil.ASCBYTOTALCASES)
}
R.id.deathDesc -> {
mViewModel.rearangeCountries(SortingUtil.DESCBYDEATHCASES)
}
R.id.deathAsc -> {
mViewModel.rearangeCountries(SortingUtil.ASCBYDEATHCASES)
}
R.id.alphaDesc -> {
mViewModel.rearangeCountries(SortingUtil.DESCBYALPHA)
}
R.id.alphaAsc -> {
mViewModel.rearangeCountries(SortingUtil.ASCBYALPHA)
}
}
}
}
private fun observeViewModel() {
mViewModel.countries.observe(viewLifecycleOwner) { resource ->
when (resource) {
is Resource.Loading -> showProgress()
is Resource.Error -> {
hideProgress()
SnackUtil.showSnackbar(
requireContext(),
requireView(),
resource.message.toString(),
R.color.color_danger
)
}
is Resource.Success -> {
hideProgress()
mViewModel.setCountryList(resource.data!!)
}
}
}
mViewModel.mediatorLiveData.observe(viewLifecycleOwner) { countryList ->
mAdapter.diff.submitList(countryList)
}
}
CountriesFragmentViewModel
#HiltViewModel
class CountriesFragmentViewModel #Inject constructor(
private val repo: Repo
) : ViewModel() {
private val _countries = MutableLiveData<Resource<List<CountryResponse>>>()
val countries: LiveData<Resource<List<CountryResponse>>> get() = _countries
val mediatorLiveData = MediatorLiveData<List<CountryResponse>>()
private val countryList = MutableLiveData<List<CountryResponse>>()
private var currentOrder = SortingUtil.DESCBYTOTALCASES
init {
getCountries()
mediatorLiveData.addSource(countryList) { countries ->
countries.let {
mediatorLiveData.value = sortList(it, currentOrder)
}
}
}
fun rearangeCountries(order: SortingUtil) = countryList.value.let {
mediatorLiveData.value = sortList(it!!, order)
}.also {
currentOrder = order
}
fun getCountries() {
viewModelScope.launch {
_countries.value = repo.getCountries()
}
}
fun setCountryList(list: List<CountryResponse>) {
countryList.value = list
}
private fun sortList(
countryList: List<CountryResponse>,
order: SortingUtil
): List<CountryResponse> {
when (order) {
SortingUtil.DESCBYTOTALCASES -> {
return countryList.sortedByDescending {
it.cases
}
}
SortingUtil.ASCBYTOTALCASES -> {
return countryList.sortedBy {
it.cases
}
}
SortingUtil.DESCBYDEATHCASES -> {
return countryList.sortedByDescending {
it.deaths
}
}
SortingUtil.ASCBYDEATHCASES -> {
return countryList.sortedBy {
it.deaths
}
}
SortingUtil.DESCBYALPHA -> {
return countryList.sortedByDescending {
it.country
}
}
SortingUtil.ASCBYALPHA -> {
return countryList.sortedBy {
it.country
}
}
}
}
}
CountriesFragmentAdapter
#FragmentScoped
class CountriesFragmentAdapter #Inject constructor() :
RecyclerView.Adapter<CountriesFragmentAdapter.ViewHolder>() {
private val differCallback = object : DiffUtil.ItemCallback<CountryResponse>() {
override fun areItemsTheSame(oldItem: CountryResponse, newItem: CountryResponse): Boolean =
oldItem.countryInfo.id == newItem.countryInfo.id
override fun areContentsTheSame(
oldItem: CountryResponse,
newItem: CountryResponse
): Boolean = oldItem == newItem
}
val diff = AsyncListDiffer(this, differCallback)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): CountriesFragmentAdapter.ViewHolder {
return ViewHolder(
CountryItemLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: CountriesFragmentAdapter.ViewHolder, position: Int) {
val currentItem = diff.currentList[position]
holder.bind(currentItem)
}
override fun getItemCount(): Int = diff.currentList.size
inner class ViewHolder(private val binding: CountryItemLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(countryResponse: CountryResponse) {
binding.apply {
Glide.with(itemView.context)
.load(countryResponse.countryInfo.flag)
.into(binding.countryFlagIv)
countryNameTv.text = countryResponse.country
totalCasesTv.text = formatStat(countryResponse.cases)
totalDeathsTv.text = formatStat(countryResponse.deaths)
}
}
private fun formatStat(stat: Long): String = "%,d".format(stat)
}
}
My sorting logic is working as expected.My problem is when my items rearranged, my recyclerview is getting just some random position.I want to scroll my recyclerview to the top when items rearranged.I have also tried binding.countryRv.scrollToPosition(0) but it did not work.
I have an application with 2 fragments, both have lists that are being filled from the same adapter.
The first one works correctly, but the second one -
class CountryBordersFragment : Fragment(R.layout.fragment_country_borders) {
private lateinit var selectedCountry: String
private lateinit var countriesViewModel: CountriesListViewModel
private lateinit var countriesAdapter: CountriesListAdapter
private var countriesList = mutableListOf<CountryEntity>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
countriesViewModel = ViewModelProvider(this).get(CountriesListViewModel::class.java)
initData()
initClickListener()
}
private fun initClickListener() {
backButton.setOnClickListener {
requireActivity().onBackPressed()
}
}
private fun initData() {
countriesAdapter = CountriesListAdapter(null)
countriesAdapter.submitList(countriesList)
countriesRecyclerView.setHasFixedSize(true)
countriesRecyclerView.layoutManager = LinearLayoutManager(context)
countriesRecyclerView.adapter = countriesAdapter
arguments?.let {
selectedCountry = it.getString(getString(R.string.countries_list_fragment_selected_country))!!
}
countryName.text = selectedCountry
countriesViewModel.getCountryBorders(selectedCountry).observeOnce(requireActivity(), Observer { countryBorder ->
if (countryBorder.neighborCountries.isEmpty()) {
bordersWith.text = getString(R.string.country_border_fragment_country_does_not_have_borders)
return#Observer
}
countriesViewModel.getCountryByCioc(countryBorder.neighborCountries).observe(requireActivity(), Observer { countryEntityList ->
countriesAdapter.submitList(countryEntityList)
})
})
}
}
Does not fill the adapter at all. It just does not display any list whatsoever.
For sure something is missing because I am able to fill my adapter correctly at the first fragment but coming to this one the list does not pop up.
Here is my ListAdapter implementation -
class CountriesListAdapter(private val callback: CountryViewHolder.OnCountryClickListener?)
: ListAdapter<CountryEntity, CountryViewHolder>(CountriesDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CountryViewHolder {
val view = LayoutInflater.from(App.context!!).inflate(R.layout.country_viewholder, parent, false)
return CountryViewHolder(view)
}
override fun onBindViewHolder(holder: CountryViewHolder, position: Int) {
holder.bind(getItem(position), callback)
}
class CountriesDiffCallback : DiffUtil.ItemCallback<CountryEntity>() {
override fun areItemsTheSame(oldItem: CountryEntity, newItem: CountryEntity): Boolean {
return oldItem.countryName == newItem.countryName
}
override fun areContentsTheSame(oldItem: CountryEntity, newItem: CountryEntity): Boolean {
return oldItem == newItem
}
}
}
and my ViewHolder and model -
class CountryViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val rootLayout: LinearLayout = view.country_viewholder_root_layout
private val nativeName: TextView = view.country_viewholder_native_name
private val countryName: TextView = view.country_viewholder_country_name
private val area: TextView = view.country_viewholder_area
private val countryImage: ImageView = view.country_viewholder_country_image
fun bind(model: CountryEntity, callback: OnCountryClickListener?) {
nativeName.text = App.context!!.getString(R.string.country_view_holder_native_name).plus(" ${model.countryName}")
countryName.text = App.context!!.getString(R.string.country_view_holder_country_name).plus(" ${model.nativeName}")
area.text = App.context!!.getString(R.string.country_view_holder_country_area).plus(" ${model.area}")
// Glide.with(App.context!!).load("https://www.talkwalker.com/images/2020/blog-headers/image-analysis.png").into(countryImage)
Picasso.get().load(model.imageUri).into(countryImage)
rootLayout.setOnClickListener {
callback?.onCountryClicked(model.countryName)
}
}
interface OnCountryClickListener {
fun onCountryClicked(countryName: String)
}
}
#Entity(tableName = countriesTable, primaryKeys = ["countryName"])
class CountryEntity(
val countryName: String,
val nativeName: String,
val area: Double,
val cioc: String? = null,
val imageUri : String? = null
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CountryEntity
if (countryName != other.countryName) return false
if (nativeName != other.nativeName) return false
if (area != other.area) return false
if (cioc != other.cioc) return false
if (imageUri != other.imageUri) return false
return true
}
override fun hashCode(): Int {
var result = countryName.hashCode()
result = 31 * result + nativeName.hashCode()
result = 31 * result + area.hashCode()
result = 31 * result + (cioc?.hashCode() ?: 0)
result = 31 * result + (imageUri?.hashCode() ?: 0)
return result
}
}
class CountryViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val rootLayout: LinearLayout = view.country_viewholder_root_layout
private val nativeName: TextView = view.country_viewholder_native_name
private val countryName: TextView = view.country_viewholder_country_name
private val area: TextView = view.country_viewholder_area
private val countryImage: ImageView = view.country_viewholder_country_image
fun bind(model: CountryEntity, callback: OnCountryClickListener?) {
nativeName.text = App.context!!.getString(R.string.country_view_holder_native_name).plus(" ${model.countryName}")
countryName.text = App.context!!.getString(R.string.country_view_holder_country_name).plus(" ${model.nativeName}")
area.text = App.context!!.getString(R.string.country_view_holder_country_area).plus(" ${model.area}")
// Glide.with(App.context!!).load("https://www.talkwalker.com/images/2020/blog-headers/image-analysis.png").into(countryImage)
Picasso.get().load(model.imageUri).into(countryImage)
rootLayout.setOnClickListener {
callback?.onCountryClicked(model.countryName)
}
}
interface OnCountryClickListener {
fun onCountryClicked(countryName: String)
}
}
What is it that I am missing? Just started working with ListAdapter from normal RecyclerView.Adapter.
Looks like you are missing - notifyDataSetChanged()
Just After -
countriesAdapter.submitList(countryEntityList)
Add -
countriesAdapter.notifyDataSetChanged()
I've got a Popup menu that should launch a Maps intent whenever a Popup menu item is clicked. In popupMenu.setOnMenuItemClickListener, does anyone know how I can pass the String of the clicked Popup menu item (from the arrayAMap array) and use it for an intent? I've already got the Arrays but I can't seem to figure out that correct way to implement this function.
class MyAdapter(
private val mCtx: Context,
var myList: MutableList<ItemRV>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), PopupMenu.OnMenuItemClickListener {
private var mClickListener: ItemClickListener? = null
lateinit var mAdView : AdView
private val itemRV = 1
private val itemAD = 2
override fun getItemViewType(position: Int): Int {
return if (position % 4 == 0) {
itemAD
} else {
itemRV
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == itemAD) {
val v = LayoutInflater.from(mCtx).inflate(R.layout.item_ad, parent, false)
AdViewHolder(v)
} else {
val v = LayoutInflater.from(mCtx).inflate(R.layout.item_rv, parent, false)
AreaViewHolder(v)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when(holder) {
is AdViewHolder -> {
MobileAds.initialize(mCtx) {}
mAdView = holder.itemView.findViewById(R.id.adView)
val adRequest = AdRequest.Builder().build()
mAdView.loadAd(adRequest)
}
is AreaViewHolder -> {
val positionToBind = position - position / 4 - 1
val product = myList[positionToBind]
holder.tvTitle.text = product.itemTitle
}
}
}
override fun getItemCount(): Int {
return myList.size
}
inner class AdViewHolder(itemView: View) : androidx.recyclerview.widget.RecyclerView
.ViewHolder(itemView), View.OnClickListener {
override fun onClick(v: View?) {
}
}
inner class AreaViewHolder(itemView: View) : androidx.recyclerview.widget.RecyclerView
.ViewHolder(itemView), View.OnClickListener {
var tvTitle: TextView = itemView.tvtitle
// Use package name which we want to check
private val isAppInstalled = appInstalledOrNot("com.google.android.apps.maps")
private val isLiteAppInstalled = appInstalledOrNot("com.google.android.apps.mapslite")
fun launchMapIntent(nameLocation: String) {
val mapPkg = when {
isAppInstalled -> "com.google.android.apps.maps"
isLiteAppInstalled -> "com.google.android.apps.mapslite"
else -> null
}
val mapIntent = if(mapPkg != null) {
val gmmIntentUri = Uri.parse("geo:0,0?q=$nameLocation")
Intent(Intent.ACTION_VIEW, gmmIntentUri).setPackage(mapPkg)
} else {
val encLoc = Uri.encode(nameLocation)
val str = "https://www.google.com/maps/place/$encLoc/"
Intent(Intent.ACTION_VIEW, Uri.parse(str))
}
mCtx.startActivity(mapIntent)
}
val arrayA = arrayOf(view.resources.getString(R.string.stockholm),
view.resources.getString(R.string.copenhagen))
val arrayAMap = arrayOf("Stockholm, Sweden", "Copenhagen, Denmark")
fun launchPopupMenu(namePopupItemLocation: Array<String>, nameLocation: Array<String>){
val popupMenu = PopupMenu(ibMap.context, ibMap)
for (item in namePopupItemLocation) {
popupMenu.menu.add(item)
}
popupMenu.setOnMenuItemClickListener {
launchMapIntent(nameLocation.get())
true
}
popupMenu.show()
}
ibMap.setOnClickListener {
when(tvTitle.text.toString()) {
"A" -> launchMapIntent("Paris, France")
"B" -> launchPopupMenu(arrayA, arrayAMap)
else -> return#setOnClickListener
}
}
}
private fun appInstalledOrNot(uri: String): Boolean {
val pm = mCtx.packageManager
try {
pm.getPackageInfo(uri, PackageManager.GET_ACTIVITIES)
return true
} catch (e: PackageManager.NameNotFoundException) {
}
return false
}
}
// Parent activity will implement this method to respond to click events
interface ItemClickListener {
fun onItemClick(view: View, position: Int)
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
}
}
You can get clicked menu item in setOnMenuItemClickListener and from that menu item you can get title of that menu
popupMenu.setOnMenuItemClickListener { item ->
nameLocation.forEach {
if (it.toLowerCase().startsWith(item.title.toLowerCase())) {
launchMapIntent(it)
}
}
true
}
Hope this will help!!
I am making a contexual action bar by modifying the toolbar on long click of recyclerView which contains edit and delete. It works okay if I select / deselect the views, however if I click on edit, the menu doesn't appear until I focus on one of the edit texts. When I edit any entry, it works again. the toolbar textView is being updated but menu is not inflating
//Activity Expense_list
class Expenses_list : AppCompatActivity(), View.OnLongClickListener{
lateinit var expenses_recycler : RecyclerView
var expense_list_items :MutableList<expense_input> = ArrayList()
var selection_list_items :MutableList<expense_input> = ArrayList()
var db = DataBaseHandler(this)
var categ:String = ""
var is_in_action_mode = false
var edit_mode = false
private var counter = 0
lateinit var toolbar: android.support.v7.widget.Toolbar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_expenses_list)
toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
categ= intent.getStringExtra("cat")
textView.text = categ.capitalize()
loadData()
expenses_recycler = findViewById(R.id.expense_list_item)
expenses_recycler.layoutManager = LinearLayoutManager(this)
expenses_recycler.adapter =
Expense_Adapter(expense_list_items,this,this#Expenses_list)
}
fun addObject(i:Int) {
selection_list_items.add(expense_list_items[i])
counter=counter+1
changeMenu()
updateCounter(counter)
}
private fun updateCounter(counter:Int) {
if(counter==0)
{
textToolbar.text = "0 item selected"
}
else
{
textToolbar.text = "" + counter + "item selected"
}
}
private fun removeObject(adapterPosition: Int) {
selection_list_items.remove(expense_list_items[adapterPosition])
counter=counter-1
changeMenu()
updateCounter(counter)
}
fun loadData() {
expense_list_items.clear()
var data = db.readDataExpense(categ)
for (i in 0..(data.size - 1))
{
expense_list_items.add(data[i])
}
}
override fun onLongClick(p0: View?): Boolean {
toolbar.menu.clear()
textToolbar.visibility = View.VISIBLE
is_in_action_mode = true
expenses_recycler.adapter.notifyDataSetChanged()
return true
}
fun changeMenu() {
when (counter) {
0 -> {
Toast.makeText(this,"counter=0",Toast.LENGTH_SHORT).show()
toolbar.menu.clear()
}
1 -> {
Toast.makeText(this,"counter=1",Toast.LENGTH_SHORT).show()
toolbar.menu.clear()
toolbar.inflateMenu(R.menu.menu_toolbar_edit)
}
else -> {
toolbar.menu.clear()
toolbar.inflateMenu(R.menu.menu_toolbar)
}
}
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when(item?.itemId)
{
R.id.edit->{
edit_mode=true
expenses_recycler.adapter.notifyDataSetChanged()
}
R.id.delete->{
db.deleteExpense(selection_list_items)
loadData()
clearActionMode()
}
}
return super.onOptionsItemSelected(item)
}
private fun clearActionMode() {
is_in_action_mode=false
edit_mode=false
selection_list_items.clear()
toolbar.menu.clear()
textToolbar.visibility = View.GONE
textToolbar.text = "0 item selected"
expenses_recycler.adapter.notifyDataSetChanged()
}
override fun onBackPressed() {
if(is_in_action_mode==true)
{
clearActionMode()
}
else
super.onBackPressed()
}
}
//Adapter
class Expense_Adapter(items
:List<expense_input>,ctx:Context,el:Expenses_list) :
RecyclerView.Adapter<Expense_Adapter.ViewHolder>() {
var expl = el
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(context,expl,LayoutInflater.from(context).inflate(R.layout.expense_list,parent,false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindTo(list[position])
}
var list = items
var context = ctx
override fun getItemCount(): Int {
return list.size
}
class ViewHolder(v:View) :RecyclerView.ViewHolder(v)
{
var mex :Expenses_list? = null
var context : Context?= null
var exp :expense_input ?= null
constructor(ctx: Context,ex: Expenses_list,v: View):this(v){
mex = ex
context = ctx
bl.setOnLongClickListener(mex)
cb?.setOnCheckedChangeListener { buttonView, isChecked ->
if (isChecked) {
mex!!.addObject(adapterPosition)
}
else {
mex!!.removeObject(adapterPosition)
}
}
}
fun bindTo(l: expense_input)
{
exp = l
name?.setText(l.item)
//name?.text = l.item
am.setText(l.amount.toString())
//am.text = l.amount.toString()
d.text = l.date
if(mex?.is_in_action_mode==false) {
cb.visibility = View.GONE
}
else {
cb.visibility = View.VISIBLE
cb.isChecked = false
}
if(mex?.edit_mode==false)
{
if(vs.displayedChild == 1)
{
Log.d("action","here")
im.setImageResource(R.drawable.ic_expnote)
vs.showPrevious()
}
}
if(mex!!.edit_mode)
{
Log.d("action","Edit")
vs.showNext()
cb.visibility = View.GONE
ename.setText(exp?.item)
eam.setText(exp?.amount.toString())
im.setImageResource(R.drawable.ic_list)
}
im.setOnClickListener {
val db = DataBaseHandler(context!!)
if(vs.displayedChild==1)
{
if(eam.text.toString().isNotEmpty() && ename.text.toString().isNotEmpty())
{
var ex = l
ex.amount = eam.text.toString().toFloat()
ex.item = ename.text.toString()
db.UpdateDataExpense(ex)
mex?.clearActionMode()
}
else
{
Toast.makeText(context,"Item is invalid",Toast.LENGTH_SHORT).show()
}
}
else
{
val detailIntent = Intent(context, Exp_note::class.java)
detailIntent.putExtra("Note",l.id)
context!!.startActivity(detailIntent)
}
}
}
val name = v.textItem
val am = v.textAmount
val d = v.textDate
val im = v.openNote
val cb = v.cb
val bl = v.biglinear
val vs = v.viewSwitcher
val ename = v.editItem
val eam = v.EditAmount
}
}
Instead of trying to inflate different menus in one Activity did you try to create one menu that contains all items and set them to invisible by default? You could then override onPrepareOptionsMenu, for example like below, to change what items are to be displayed based on your defined conditions:
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
invalidateOptionsMenu()
when {
// more than one item selected
mSelectedItems.isNotEmpty() && mSelectedItems.size > 1 -> {
menu.findItem(R.id.action_edit).isVisible = false
menu.findItem(R.id.action_delete).isVisible = true
}
// one item selected
mSelectedItems.isNotEmpty() -> {
menu.findItem(R.id.action_edit).isVisible = true
menu.findItem(R.id.action_delete).isVisible = true
}
// none selected
else -> {
menu.findItem(R.id.action_edit).isVisible = false
menu.findItem(R.id.action_delete).isVisible = false
}
}
return super.onPrepareOptionsMenu(menu)
}
obove code produces following result