Recycler view item on click not working properly after searchview kotlin - android

I have implemented recyclerview with searchview in my app. But on clicking the item after search, my app crashes.
My app works fine when item on list is clicked before search.
It only stops working when clicking search result.
App Screen Record
My code for listview
class BookFragment : Fragment() , SearchView.OnQueryTextListener {
private var columnCount = 1
private lateinit var bookViewModel: BookViewModel
private val myAdapter: BookAdapter by lazy { BookAdapter(requireContext()) }
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.main_book_list, container, false)
val recyclerView: RecyclerView = view.findViewById(R.id.mainList)
val fab_add_button: FloatingActionButton = view.findViewById(R.id.FAB_add_new)
setHasOptionsMenu(true)
bookViewModel = ViewModelProvider(this)[BookViewModel::class.java]
bookViewModel.allBooks.observe(viewLifecycleOwner) { books ->
myAdapter.setBooks(books)
}
with(recyclerView) {
adapter = myAdapter
layoutManager = when {
columnCount <= 1 -> LinearLayoutManager(context)
else -> GridLayoutManager(context, columnCount)
}
}
fab_add_button.setOnClickListener {
findNavController().navigate(R.id.action_bookFragment_to_addFragment)
}
postponeEnterTransition()
recyclerView.doOnPreDraw {
startPostponedEnterTransition()
}
return view
}
#Deprecated("Deprecated in Java")
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.list_menu, menu)
val search = menu.findItem(R.id.list_menu_search)
val searchView = search.actionView as SearchView
searchView.isSubmitButtonEnabled = true
searchView.setOnQueryTextListener(this)
}
#Deprecated("Deprecated in Java")
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.list_menu_setting -> findNavController().navigate(R.id.action_bookFragment_to_settingsFragment)
}
return super.onOptionsItemSelected(item)
}
override fun onQueryTextSubmit(query: String?): Boolean {
if (query != null ) searchDB(query)
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
if (newText != null ) searchDB(newText)
return true
}
private fun searchDB(string: String) {
val query = "%$string%"
bookViewModel.fetchSearchedBook(query).observe(viewLifecycleOwner) { resp ->
myAdapter.setBooks(resp)
}
}
}
My Adapter Code
class BookAdapter(context: Context) : RecyclerView.Adapter<BookAdapter.ViewHolder>() {
private var books = emptyList<LightNovel>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
SingleBookBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = books[position]
holder.idView.text = item.id.toString()
holder.contentView.text = item.title
Glide.with(holder.itemView.context)
.load(item.coverRemote)
.error(R.drawable.ic_image_error)
.into(holder.imageView)
holder.itemView.setOnClickListener {
holder.itemView.findNavController().navigate(R.id.action_bookFragment_to_detailFragment)
val action = BookFragmentDirections.actionBookFragmentToDetailFragment(item.id)
holder.itemView.findNavController().navigate(action)
}
}
override fun getItemCount(): Int = books.size
inner class ViewHolder(binding: SingleBookBinding) : RecyclerView.ViewHolder(binding.root) {
val idView: TextView = binding.itemNumber
val contentView: TextView = binding.content
val imageView: ImageView = binding.listCover
override fun toString(): String {
return super.toString() + " '" + contentView.text + "'"
}
}
#SuppressLint("NotifyDataSetChanged")
fun setBooks(lightNovels: List<LightNovel>) {
this.books = lightNovels
notifyDataSetChanged()
}
}
And Detail Fragment
class DetailFragment : Fragment() {
private val args: DetailFragmentArgs by navArgs()
private var _binding: FragmentDetailBinding? = null
private val binding get() = _binding!!
private lateinit var bookViewModel: BookViewModel
private lateinit var bookLN: LightNovel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentDetailBinding.inflate(inflater, container, false)
val view = binding.root
val bookid = args.bookID
setHasOptionsMenu(true)
bookViewModel = ViewModelProvider(this)[BookViewModel::class.java]
bookViewModel
.fetchLiveBook(bookid)
.observe(viewLifecycleOwner) {
book ->
bookLN = book
binding.detailTitle.text = book.title
val image = binding.detailCover
val imageUrl = bookLN.coverRemote
val valid = URLUtil.isValidUrl(imageUrl)
if (valid) {
Glide.with(requireContext())
.load(bookLN.coverRemote)
.error(R.drawable.ic_image_error)
.into(image)
}
if (book.synopsis != null && book.synopsis.length >= 2) {
binding.detailSynopsis.text = book.synopsis
}
if (URLUtil.isValidUrl(book.download)) {
binding.detailButton.setBackgroundColor(Color.RED) }
binding.detailButton.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(book.download))
try {
startActivity(intent)
} catch (_: ActivityNotFoundException) {
}
}
}
return view
}
#Deprecated("Deprecated in Java")
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.detail_menu, menu)
}
#Deprecated("Deprecated in Java")
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.detail_menu_update_book -> {
val action = DetailFragmentDirections.actionDetailFragmentToUpdateFragment(args.bookID)
findNavController().navigate(action)
}
R.id.detail_menu_delete_book -> {
deleteBook(bookLN)
}
}
return super.onOptionsItemSelected(item)
}
private fun deleteBook(Ln: LightNovel) {
val builder = AlertDialog.Builder(requireContext())
builder.setPositiveButton("Delete") {_,_ ->
bookViewModel.deleteBook(Ln)
findNavController().navigate(R.id.action_detailFragment_to_bookFragment)
}
builder.setNegativeButton("Cancel") {_,_ ->}
builder.setTitle("Delete Entry?")
builder.setMessage("Are you sure you want to delete ${Ln.title}")
builder.create().show()
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
}
I think it might be due to bookViewModel.fetchLiveBook(bookid) not working. But I do not know where it is not working.
Logcat shows following
FATAL EXCEPTION: main
Process: com.knight.moonreaderdatabase, PID: 21562
java.lang.IllegalStateException: Can't access the Fragment View's LifecycleOwner when getView() is null i.e., before onCreateView() or after onDestroyView()
at androidx.fragment.app.Fragment.getViewLifecycleOwner(Fragment.java:377)
at com.knight.moonreaderdatabase.fragments.list.BookFragment.searchDB(BookFragment.kt:98)
at com.knight.moonreaderdatabase.fragments.list.BookFragment.onQueryTextChange(BookFragment.kt:92)
at androidx.appcompat.widget.SearchView.onTextChanged(SearchView.java:1198)
at androidx.appcompat.widget.SearchView$10.onTextChanged(SearchView.java:1736)
at android.widget.TextView.sendOnTextChanged(TextView.java:10586)
at android.widget.TextView.setText(TextView.java:6322)
at android.widget.TextView.setText(TextView.java:6147)
at android.widget.EditText.setText(EditText.java:121)
at android.widget.TextView.setText(TextView.java:6099)
at androidx.appcompat.widget.SearchView.setQuery(SearchView.java:579)
at androidx.appcompat.widget.SearchView.onActionViewCollapsed(SearchView.java:1295)
at androidx.appcompat.widget.Toolbar$ExpandedActionViewMenuPresenter.collapseItemActionView(Toolbar.java:2656)
at androidx.appcompat.view.menu.MenuBuilder.collapseItemActionView(MenuBuilder.java:1384)
at androidx.appcompat.view.menu.MenuBuilder.clear(MenuBuilder.java:607)
at androidx.appcompat.app.AppCompatDelegateImpl.doInvalidatePanelMenu(AppCompatDelegateImpl.java:2183)
at androidx.appcompat.app.AppCompatDelegateImpl$2.run(AppCompatDelegateImpl.java:273)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:975)
at android.view.Choreographer.doCallbacks(Choreographer.java:799)
at android.view.Choreographer.doFrame(Choreographer.java:730)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:960)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:236)
at android.app.ActivityThread.main(ActivityThread.java:7864)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:620)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1011)

Related

Kotlin RecyclerView MVVM data not showing

I have a RecyclerView in MVVM project. I have to get text from editText (searchWordEt) and then pass it to the function that invokes API method in viewmodel.API works fine and returns data. But when I invoke searchDefAdapter.submitList(response) in SearchFragment nothing happens and RecyclerView data not showing.
class SearchDefAdapter(
private var infoListener: OnItemClickListener,
private var addListener: OnItemClickListener
):
ListAdapter<Def, SearchDefViewHolder>(differCallback) {
interface OnItemClickListener {
fun onItemClick(position: Int)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchDefViewHolder {
return SearchDefViewHolder(
SearchWordCardBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
addListener,
infoListener
)
}
override fun onBindViewHolder(holder: SearchDefViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
class SearchDefViewHolder(
private val binding: SearchWordCardBinding,
addListener: SearchDefAdapter.OnItemClickListener,
infoListener: SearchDefAdapter.OnItemClickListener
): RecyclerView.ViewHolder(binding.root) {
fun bind(data: Def) {
with (binding) {
searchCardTv.text = "${data.text} - ${data.tr[0].text}"
}
}
init {
binding.addSearchCard.setOnClickListener {
addListener.onItemClick(adapterPosition)
}
binding.infoSearchCard.setOnClickListener {
infoListener.onItemClick(adapterPosition)
}
}
}
val differCallback = object : DiffUtil.ItemCallback<Def>() {
override fun areItemsTheSame(oldItem: Def, newItem: Def): Boolean {
return oldItem.text == newItem.text
}
override fun areContentsTheSame(oldItem: Def, newItem: Def): Boolean {
return oldItem == newItem
}
}
#AndroidEntryPoint
class SearchFragment : Fragment() {
private var _binding: FragmentSearchBinding? = null
private val binding get() = _binding!!
lateinit var searchDefAdapter: SearchDefAdapter
private val viewModel: DictionaryViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentSearchBinding.inflate(inflater, container, false)
val view = binding.root
searchDefAdapter = SearchDefAdapter(
object : SearchDefAdapter.OnItemClickListener {
override fun onItemClick(position: Int) {
Log.d("tag", "Item Added!")
//viewModel.saveWord(position)
}
},
object : SearchDefAdapter.OnItemClickListener {
override fun onItemClick(position: Int) {
val wordFragment = WordFragment()
fragmentManager?.beginTransaction()?.replace(
R.id.nav_host_fragment_container,
wordFragment
)?.commit()
}
}
)
setUpRecyclerView(searchDefAdapter)
var job: Job? = null
binding.searchWordEt.addTextChangedListener { editable ->
job?.cancel()
job = MainScope().launch {
delay(SEARCH_WORD_TIME_DELAY)
editable?.let {
if (editable.toString().isNotEmpty())
viewModel.getTranslation(editable.toString())
}
}
}
viewModel.def.observe(viewLifecycleOwner, Observer { response ->
binding.apply {
searchDefAdapter.submitList(response)
}
})
return view
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
private fun setUpRecyclerView(adapter: SearchDefAdapter){
binding.searchRv.apply {
adapter
layoutManager = LinearLayoutManager(activity)
}
}
}
You haven't actually set the adapter in setUpRecyclerView()
private fun setUpRecyclerView(myAdapter: SearchDefAdapter){
binding.searchRv.apply {
layoutManager = LinearLayoutManager(activity)
adapter = myAdapter // here
}
}

How to Sort Descending with ActionBar in Live Data Recycleriview?

Iam using observe for recyclerview Adapter.There is Default Name Sort Ascending.I need to use Sort Descending after click actionbar
private fun getAllTeams(){
viewModel.getMyGroupMembers().observe(viewLifecycleOwner, Observer {
val sortedDescending = descending(it) // -> it have to be in onOptionsItemSelected
val sortedList = it.sortedBy {
it.name }
Log.d("sorted",sortedList.toString())
scoreAdapter.submitList(sortedList)
scoreAdapter.notifyDataSetChanged()
})
}
After click a navigation item it will be change with Sort Descending Salary(DataClass variable)
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.menuTogle){
//descending() // -> How can i use the method here?
}
return super.onOptionsItemSelected(item)
}
Its my descending method
private fun descending (getList: List<DataClass>) : List<DataClass>{
val test = getList.sortedByDescending {
it.value
}
return test
}
How do I combine all this in the observer?
class TeamScoreTableFragment : Fragment() {
private lateinit var binding: FragmentTeamScoreTableBinding
private lateinit var scoreAdapterBinding: ItemScoreLayoutBinding
private val scoreAdapter: ScoreAdapter by inject()
private val viewModel : ScoreViewModel by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentTeamScoreTableBinding.inflate(inflater,container,false)
scoreAdapterBinding = ItemScoreLayoutBinding.inflate(inflater,container,false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
getAllTeams()
getGroups()
binding.rvScoreRecyclerView.apply {
adapter = this#TeamScoreTableFragment.scoreAdapter
layoutManager = LinearLayoutManager(context)
}
}
private fun getAllTeams(){
viewModel.getMyGroupMembers().observe(viewLifecycleOwner, Observer {
val sortedDescending = descending(it)
val sortedList = it.sortedBy {
it.name }
Log.d("sorted",sortedList.toString())
scoreAdapter.submitList(sortedDescending)
scoreAdapter.notifyDataSetChanged()
})
}
private fun getGroups() {
val test = viewModel.getAllGroups()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_togle_items,menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.menuTogle){
}
return super.onOptionsItemSelected(item)
}
private fun descending (getList: List<DataClass>) : List<DataClass>{
val test = getList.sortedByDescending {
it.value
}
return test
}
private lateinit var binding: FragmentTeamScoreTableBinding
private lateinit var scoreAdapterBinding: ItemScoreLayoutBinding
private val scoreAdapter: ScoreAdapter by inject()
private val viewModel : ScoreViewModel by inject()
private lateinit var items : List<DataClass>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentTeamScoreTableBinding.inflate(inflater,container,false)
scoreAdapterBinding = ItemScoreLayoutBinding.inflate(inflater,container,false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
getAllTeams()
getGroups()
binding.rvScoreRecyclerView.apply {
adapter = this#TeamScoreTableFragment.scoreAdapter
layoutManager = LinearLayoutManager(context)
}
}
private fun getAllTeams(){
viewModel.getMyGroupMembers().observe(viewLifecycleOwner, Observer {
items = it
val sortedList = it.sortedBy {
it.name }
Log.d("sorted",sortedList.toString())
scoreAdapter.submitList(sortedList)
scoreAdapter.notifyDataSetChanged()
})
}
private fun getGroups() {
val test = viewModel.getAllGroups()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_togle_items,menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.menuTogle){
descending(items)
scoreAdapter.submitList(items)
scoreAdapter.notifyDataSetChanged()
}
return super.onOptionsItemSelected(item)
}
private fun descending (getList: List<DataClass>) : List<DataClass>{
val test = getList.sortedByDescending {
it.value
}
return test
}
**Do you mean so?**
private var items : List<DataClass>? = null
private fun getAllTeams(){
viewModel.getMyGroupMembers().observe(viewLifecycleOwner, Observer {
items = it
val sortedList = it.sortedBy {
it.name }
Log.d("sorted",sortedList.toString())
scoreAdapter.submitList(sortedList)
scoreAdapter.notifyDataSetChanged()
})
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.menuTogle){
items?.let { descending(it) }
}
return super.onOptionsItemSelected(item)
}

How to get total items from Paging 3 Android using Kotlin to implement onclicklistener for each item?

I am following this article here.
All is working well but now I want to implement the onclicklistener for each item.
Here's my code:
SongViewModelAdapter.kt
class SongViewModelAdapter(private val onSelect: ((ViewModelLyric?) -> Unit)?) : PagingDataAdapter<ViewModelLyric,
SongViewModelAdapter.SongViewHolder>(Companion) {
val TAG = "SongViewModelAdapter"
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val dataBinding = LyricDataBinding.inflate(
layoutInflater,
parent,
false
)
return SongViewHolder(dataBinding)
}
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
val lyric = getItem(position) ?: return
holder.bindLyric(lyric, onSelect)
}
companion object : DiffUtil.ItemCallback<ViewModelLyric>() {
override fun areItemsTheSame(oldItem: ViewModelLyric, newItem: ViewModelLyric): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ViewModelLyric, newItem: ViewModelLyric): Boolean {
return oldItem == newItem
}
}
inner class SongViewHolder(
private val dataBinding: LyricDataBinding
) : RecyclerView.ViewHolder(dataBinding.root) {
fun bindLyric(lyric: ViewModelLyric, onSelect: ((ViewModelLyric?) -> Unit)?) {
dataBinding.itemTitle.text = lyric.title
dataBinding.itemArtist.text = lyric.artist
dataBinding.root.setOnClickListener {
onSelect?.let { it1 -> it1(lyric) }
}
}
}
}
HomeFragment.kt
#AndroidEntryPoint
class HomeFragment : Fragment() {
val TAG = "HomeFragment"
private lateinit var binding : FragmentHomeBinding
private val viewModel by viewModels<HomeFragmentViewModel>()
private var adapter = SongViewModelAdapter(null)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater, R.layout.fragment_home, container, false)
binding.productsRecyclerView.adapter = SongViewModelAdapter(::onSelect)
return binding.root
}
private fun onSelect(viewModelLyric: ViewModelLyric?) {
val id = viewModelLyric?.id
val bundle = bundleOf("id" to id)
Log.d(TAG, id.toString())
view?.findNavController()?.navigate(R.id.songDetailFragment, bundle)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setLyricsAdapter()
getLyrics()
setProgressBarAccordingToLoadState()
}
private fun setLyricsAdapter() {
binding.productsRecyclerView.adapter = adapter
}
private fun getLyrics() {
lifecycleScope.launch {
viewModel.flow.collectLatest {
adapter.submitData(it)
}
}
}
private fun setProgressBarAccordingToLoadState() {
lifecycleScope.launch {
adapter.loadStateFlow.collectLatest {
binding.progressBar.isVisible = it.append is LoadState.Loading
}
}
}
}
I am using FirestorePagingAdapter at the moment and it works well. For those experienced in paging 3, how would you pass total items to implement the click listener for each item. Saw similar questions but never a clear solution. Been scratching my head for a day. Your help will be much appreciated.
Edit 1: Added HomeFragment.kt
Edit 2: Updated with user suggestions. Click still not working. I needed to add the null checks.
It is not a good practice to handle click inside your adapter.
The adapter concern is just to act as a bridge b/w our data and the recyclerview not to handle click. Passing it back to fragment/activity restores that. Second, using callbacks may lead to Memory leaks as it going to have the reference to the fragment/activity.
class YourRecyclerViewAdapter(private val onSelect: (ViewModelLyric?) -> Unit) : PagingDataAdapter<ViewModelLyric, SongViewModelAdapter.SongViewHolder>(Companion) {
override fun onBindViewHolder(holder: YourViewHolder, position: Int) {
val lyric = getItem(position) ?: return
holder.bindLyric(lyric)
}
class SongViewHolder(private val binding: YourViewBinding) : RecyclerView.ViewHolder(dataBinding.root) {
fun bindLyric(yourDataType: ViewModelLyric?, onSelect: (ViewModelLyric?) -> Unit) {
dataBinding.itemTitle.text = lyric.title
dataBinding.itemArtist.text = lyric.artist
dataBinding.root.setOnClickListener {
onSelect(yourDataType)
}
}
}
}
In your Fragment/Activity
// Set this adapter to your recycler view
binding.productsRecyclerView.adapter = YourRecyclerViewAdapter { viewModelLyric->
// Handle click here
}
EDIT:
SongViewModelAdapter
class SongViewModelAdapter(private val onSelect: (ViewModelLyric) -> Unit) : PagingDataAdapter<ViewModelLyric, SongViewModelAdapter.SongViewHolder>(Companion) {
val TAG = "SongViewModelAdapter"
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val dataBinding = LyricDataBinding.inflate(layoutInflater, parent, false)
return SongViewHolder(dataBinding)
}
override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
val lyric = getItem(position) ?: return
holder.bindLyric(lyric, onSelect)
}
companion object : DiffUtil.ItemCallback<ViewModelLyric>() {
override fun areItemsTheSame(oldItem: ViewModelLyric, newItem: ViewModelLyric): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ViewModelLyric, newItem: ViewModelLyric): Boolean {
return oldItem == newItem
}
}
class SongViewHolder(private val dataBinding: LyricDataBinding) : RecyclerView.ViewHolder(dataBinding.root) {
fun bindLyric(lyric: ViewModelLyric, onSelect: (ViewModelLyric) -> Unit)) {
dataBinding.itemTitle.text = lyric.title
dataBinding.itemArtist.text = lyric.artist
dataBinding.root.setOnClickListener {
onSelect(lyric)
}
}
}
}
HomeFragment
#AndroidEntryPoint
class HomeFragment : Fragment() {
val TAG = "HomeFragment"
private lateinit var binding : FragmentHomeBinding
private val viewModel by viewModels<HomeFragmentViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_home, container, false)
binding.productsRecyclerView.adapter = SongViewModelAdapter(::onSelect)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
getLyrics()
setProgressBarAccordingToLoadState()
}
private fun onSelect(lyric: ViewModelLyric) {
// handle click here
}
private fun getLyrics() {
lifecycleScope.launch {
viewModel.flow.collectLatest {
binding.adapter.submitData(it)
}
}
}
private fun setProgressBarAccordingToLoadState() {
lifecycleScope.launch {
adapter.loadStateFlow.collectLatest {
binding.progressBar.isVisible = it.append is LoadState.Loading
}
}
}
}

Observables updates in the live data dont working in fragment

I have one fragment that instance that instantiates a view model injected by koin, the problem is that, one of the observed attributes are not stimulated in the fragment after the view model's postValue () action, it simply does not enter the method in the fragment even though it has been updated.
Fragement:
class ListFragment : Fragment() {
private lateinit var adapter: PostAdapter
private val viewModel: PostsViewModel by viewModel()
private var _binding: ListFragmentBinding? = null
private var defaultTopic = "news"
private var afterPage: String = ""
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.list_fragment, container, false)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = ListFragmentBinding.bind(view)
adapter = PostAdapter(mutableListOf(), requireContext()) {
val action = ListFragmentDirections.openDetailsFragment(it)
findNavController().navigate(action)
}
_binding?.let{
val llm = LinearLayoutManager(requireContext())
it.recyclerviewPosts.layoutManager = llm
it.recyclerviewPosts.adapter = adapter
recyclerViewListenerSetup(it, llm)
}
setupObservers()
}
private fun recyclerViewListenerSetup(it: ListFragmentBinding, llm: LinearLayoutManager) {
it.recyclerviewPosts.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val visibleItemCount: Int = llm.childCount
val totalItemCount: Int = llm.itemCount
val firstVisibleItemPosition: Int = llm.findFirstVisibleItemPosition()
if((visibleItemCount + firstVisibleItemPosition) >= totalItemCount
&& firstVisibleItemPosition >= 0) {
viewModel.getNextPage(defaultTopic, afterPage)
}
}
})
}
private fun setupObservers(){
viewModel.getPostList(defaultTopic)
viewModel.posts.observe(this as LifecycleOwner, { posts ->
if (posts.isSuccess && posts.getOrNull() != null) {
adapter.updateList(posts.getOrNull()!! as MutableList<PostsDTO>)
afterPage = posts.getOrNull()!![0].after
showList()
} else {
showError()
}
})
viewModel.loading.observe(this as LifecycleOwner, {
if (it) {
showLoading()
} else {
hideLoading()
}
})
viewModel.next.observe(this as LifecycleOwner, {
if (it.isSuccess && it.getOrNull() != null) {
adapter.addList(it.getOrNull() as MutableList<PostsDTO>)
afterPage = it.getOrNull()!![0].after
}
})
}
private fun showList(){
_binding?.let {
it.recyclerviewPosts.visibility = VISIBLE
}
}
private fun showLoading(){
_binding?.let {
it.loading.visibility = VISIBLE
it.containerError.root.visibility = GONE
it.recyclerviewPosts.visibility = GONE
}
}
private fun hideLoading(){
_binding?.let {
it.loading.visibility = GONE
}
}
private fun showError() {
_binding?.let {
it.containerError.root.visibility = VISIBLE
it.recyclerviewPosts.visibility = GONE
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_main, menu)
val searchItem = menu.findItem(R.id.action_search)
val searchManager =
requireActivity().getSystemService(Context.SEARCH_SERVICE) as SearchManager
val searchView = searchItem.actionView as SearchView
searchView.setSearchableInfo(searchManager.getSearchableInfo(activity?.componentName))
searchView.apply {
queryHint = "Search SubReddit"
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
defaultTopic = query!!
viewModel.getPostList(defaultTopic)
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
return true
}
})
}
super.onCreateOptionsMenu(menu, inflater)
}
}
I try to call the viewmodel method to update the displayed list when the recyclerView scrolls.
ViewModel:
class PostsViewModel(private val repository: PostsRepository) : ViewModel(){
private val _posts = MutableLiveData<Result<List<PostsDTO>>>()
val posts: LiveData<Result<List<PostsDTO>>>
get() =_posts
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean>
get() = _loading
private val _next = MutableLiveData<Result<List<PostsDTO>>>()
val next: LiveData<Result<List<PostsDTO>>>
get() =_posts
fun getPostList(q: String){
viewModelScope.launch {
_loading.postValue(true)
repository.fetchPosts(q)
.collect {
_posts.value = it
}
_loading.postValue(false)
}
}
fun getNextPage(topic: String, afterPage: String) {
viewModelScope.launch {
repository.fetchNextPage(topic, afterPage)
.collect{
_next.postValue(it)
}
}
}
}
In this case after the request result of the next method has updated the viewmodel, the fragment is not stimulated in viewmodel.next.observer()
OPAH! after a unit test I was able to find out what the problem was, the problem was ctrl + v, the next property, it was returning the _post property and not the _next property, so the view was not notified of the update.
The correction the correction was in the viewmoldel, changing the get method of the next variable:
private val _next = MutableLiveData<Result<List<PostsDTO>>>()
val next: LiveData<Result<List<PostsDTO>>>
get() =_next

Android Recycleview Selection stops working correctly after navigation between fragments

I implemented SelectionTracker for RecycleView and it works fine until I navigate to another fragment and press the back button. After navigation, it stops working correctly and after selection I can't deselect item anymore.
I created a sample project on github and I can reproduce bug there:
https://github.com/alborozd/RecycleViewSelectionProblem
Here is my code from that sample project:
Adapter and ViewHolder:
class MyListAdapter()
: ListAdapter<MyModel, MyListAdapter.MyItemViewHolder>(DiffCallback()) {
init {
setHasStableIds(true)
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
private var tracker: SelectionTracker<String>? = null
fun setTracker(tracker: SelectionTracker<String>?) {
this.tracker = tracker
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyItemViewHolder {
return MyItemViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, false),
this
)
}
override fun onBindViewHolder(holder: MyItemViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item, tracker!!.isSelected(item.id))
}
class MyItemViewHolder(itemView: View, private val adapter: MyListAdapter) : RecyclerView.ViewHolder(itemView) {
private var text: TextView? = null
private var container: View? = null
init {
text = itemView.findViewById(R.id.text)
container = itemView.findViewById(R.id.itemContainer)
}
fun bind(item: MyModel, selected: Boolean) {
text?.text = item.name
if (selected) {
val theme = itemView.context!!.theme
container?.setBackgroundColor(
itemView.context!!.resources.getColor(
android.R.color.darker_gray,
theme
)
)
} else {
container?.setBackgroundColor(0)
}
}
fun getItemDetails(): ItemDetailsLookup.ItemDetails<String> =
object : ItemDetailsLookup.ItemDetails<String>() {
override fun getPosition(): Int = adapterPosition
override fun getSelectionKey(): String? = adapter.getItem(adapterPosition).id
override fun inSelectionHotspot(e: MotionEvent): Boolean {
return true
}
}
}
class DiffCallback : DiffUtil.ItemCallback<MyModel>() {
override fun areItemsTheSame(oldItem: MyModel, newItem: MyModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MyModel, newItem: MyModel): Boolean {
return oldItem == newItem
}
}
}
ItemIdKeyProvider:
class ItemIdKeyProvider(
private val adapter: MyListAdapter
) : ItemKeyProvider<String>(SCOPE_MAPPED) {
override fun getKey(position: Int): String? {
return adapter.currentList[position].id
}
override fun getPosition(key: String): Int {
return adapter.currentList.indexOfFirst { c -> c.id == key }
}
}
ItemLookup:
class ItemLookup(private val rv: RecyclerView) : ItemDetailsLookup<String>() {
override fun getItemDetails(event: MotionEvent)
: ItemDetails<String>? {
val view = rv.findChildViewUnder(event.x, event.y)
if (view != null) {
return (rv.getChildViewHolder(view) as MyListAdapter.MyItemViewHolder)
.getItemDetails()
}
return null
}
}
And here is how I initialize all of this in my fragment:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
viewModel = createViewModel()
binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false)
binding.viewModel = viewModel
binding.lifecycleOwner = this
viewModel.initViewModel()
viewModel.items.observe(this, Observer { items ->
val adapter = MyListAdapter()
adapter.submitList(items)
binding.recycleView.adapter = adapter
trackSelectedItems(adapter, binding.recycleView)
adapter.notifyDataSetChanged()
})
binding.btnGoToNextFragment.setOnClickListener {
val action = MainFragmentDirections.actionMainFragmentToOtherFragment()
findNavController().navigate(action)
}
return binding.root
}
private fun trackSelectedItems(
adapter: MyListAdapter,
recyclerView: RecyclerView
) {
tracker = SelectionTracker.Builder<String>(
"selectionTracker",
recyclerView,
ItemIdKeyProvider(adapter),
ItemLookup(recyclerView),
StorageStrategy.createStringStorage()
).withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build()
adapter.setTracker(tracker)
tracker.addObserver(object : SelectionTracker.SelectionObserver<String>() {
override fun onSelectionChanged() {
super.onSelectionChanged()
}
})
}
Steps to reproduce:
Open first fragment with recycleview, try to select/deselect items. Everything works fine
Go to another fragment, then press back button
Try to select/deselect items again and you'll see that deselection doesn't work anymore
Don't initialize adapter inside live data's observer. Because Live Data might be observed n number of times, so if you initialize adapter inside that, adapter will be initialized many times.
To resolve issue use below code
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
viewModel = createViewModel()
binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false)
binding.viewModel = viewModel
binding.lifecycleOwner = this
viewModel.initViewModel()
val adapter = MyListAdapter()
binding.recycleView.adapter = adapter
trackSelectedItems(adapter, binding.recycleView)
//adapter.notifyDataSetChanged()
viewModel.items.observe(this, Observer { items ->
adapter.submitList(items)
})
binding.btnGoToNextFragment.setOnClickListener {
val action = MainFragmentDirections.actionMainFragmentToOtherFragment()
findNavController().navigate(action)
}
return binding.root
}

Categories

Resources