Hooni
Hooni

Reputation: 374

How to set LiveData from Adapter?

I have two fragments: (1) Library Fragment, (2) Book Fragment

The library fragments displays all available books via a RecyclerView. The user can tab on each of the RecyclerView Items, which will set the LiveData to the corresponding book. At the same time the Book Fragment will be opened and the contents of that book will be shown.

I set up an onClickListener in the ViewHolder class inside the RecyclerView.Adapter of the Library Fragment. So, when an item is clicked, the livedata is being set and then navigated to the Book fragment via Navigation Component. The Book Fragment has an observer on the Live Data and shows it.

As you can see in the following code, I am passing viewmodel instance to the adapter, which is not correct....? Or is it? How should this be done?

Library Fragment Code Snippet

class LibraryFragment : Fragment() {
    [...]
    private val model: SharedViewModel by activityViewModels()
    private lateinit var gridlayoutManager: GridLayoutManager
    private lateinit var thumbnailAdapter: ThumbnailAdapter
    private lateinit var thumbnailRecyclerView: RecyclerView
    private lateinit var selectedFolder: Uri

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = layoutInflater.inflate(R.layout.fragment_thumbnail_viewer, container, false)
        initRecyclerView(view)
        view.allFolderPicker.setOnClickListener {
            pickFolder()
        }
        view.switchFragment.setOnClickListener {
            findNavController().navigate(R.id.action_allFoldersFragment_to_oneFolderFragment)
        }
        return view
    }

    private fun initRecyclerView(view: View) {
        thumbnailRecyclerView = view.findViewById(R.id.thumbnail_recycler_view)
        gridlayoutManager =
            GridLayoutManager(requireContext(), 3, LinearLayoutManager.VERTICAL, false)
        thumbnailRecyclerView.layoutManager = gridlayoutManager
        thumbnailAdapter = ThumbnailAdapter(model)
        thumbnailAdapter.setThumbnailList(listOf())
        thumbnailRecyclerView.adapter = thumbnailAdapter
    }
    [...]
}

Adapter Class Code Snippet

class ThumbnailAdapter(model: SharedViewModel) :
    RecyclerView.Adapter<ThumbnailAdapter.CustomViewHolder>() {

    private var thumbnailList = listOf<Pair<String, File>>()
    private val myModel = model

    inner class CustomViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        private val thumbnail = view.thumbnail
        private val title = view.title

        fun bind(item: Pair<String, File>) {
            val titleToSet = item.first
            val bitmapToSet = Util.uriToBitmap(item.second)
            val resizedBitmapToSet = Bitmap.createScaledBitmap(bitmapToSet, 150, 150, false)
            thumbnail.setImageBitmap(resizedBitmapToSet)
            title.text = titleToSet
            itemView.setOnClickListener {
                val folderWithImages = thumbnailList[adapterPosition].second.parentFile
                folderWithImages?.let {
                    myModel.setLibraryFolderList(it)
                    itemView.findNavController()
                        .navigate(R.id.action_allFoldersFragment_to_oneFolderFragment)
                }
            }
        }
    }
    [...]
}

Book Fragment Code Snippet

[...]

class BookViewFragment : Fragment() {

    private lateinit var viewPager: ViewPager2
    private lateinit var viewPagerAdapter: ViewPager2Adapter
    private val model: SharedViewModel by activityViewModels()


    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_image_viewer, container, false)
        initRecyclerView(view)
        return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model.selectedBookFile.observe(viewLifecycleOwner, androidx.lifecycle.Observer {
            viewPagerAdapter.setImageList(getImageList(it))
            viewPagerAdapter.notifyDataSetChanged()
        })
    }

    private fun initRecyclerView(view: View) {
        viewPager = view.findViewById(R.id.main_image)
        viewPagerAdapter = ViewPager2Adapter()
        viewPagerAdapter.setImageList(listOf())
        viewPager.adapter = viewPagerAdapter
    }

    [...]
}

Upvotes: 3

Views: 11539

Answers (3)

RBK
RBK

Reputation: 2417

I know it's too late to answer. But I hope it will help other developers searching for a resolution on a similar issue.

Take a look at LiveAdapter.

You just need to add the latest dependency in Gradle.

dependencies {
    implementation 'com.github.RaviKoradiya:LiveAdapter:1.3.4'
    // kapt 'com.android.databinding:compiler:GRADLE_PLUGIN_VERSION' // this line only for Kotlin projects
}

and bind adapter with your RecyclerView

// Kotlin sample
LiveAdapter(
        data = liveListOfItems,
        lifecycleOwner = this@MainActivity,
        variable = BR.item )
       .map<Header, ItemHeaderBinding>(R.layout.item_header) {
           onBind{

           }
           onClick{

           }
           areContentsTheSame { old: Header, new: Header ->
               return@areContentsTheSame old.text == new.text
           }
           areItemSame { old: Header, new: Header ->
               return@areContentsTheSame old.text == new.text
           }
       }
       .map<Point, ItemPointBinding>(R.layout.item_point) {
           onBind{

           }
           onClick{

           }
           areContentsTheSame { old: Point, new: Point ->
               return@areContentsTheSame old.id == new.id
           }
           areItemSame { old: Header, new: Header ->
               return@areContentsTheSame old.text == new.text
           }
       }
       .into(recyclerview)

That's it. No need to write extra code for adapter implementation, observe LiveData and notify the adapter.

Also, have DiffUtil implemented so only changed items will be updated on RecyclerView instead of all.

Upvotes: -1

Osama Remlawi
Osama Remlawi

Reputation: 2990

Simply use your Activity reference inside Adapter class instead of viewLifecycleOwner and it will work

class ThumbnailAdapter(

    private val activity: FragmentActivity

) : RecyclerView.Adapter<ThumbnailAdapter.CustomViewHolder>() {

    // ... Other functions

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        model.selectedBookFile.observe(activity, androidx.lifecycle.Observer {
            // Your code goes here
        })
    }

    // ... Other functions

}

Upvotes: 0

Biscuit
Biscuit

Reputation: 5247

You shouldn't do it that way, here is a course about RecyclerView that goes from displaying to handle clickListener.

Here is how they define it:

While the ViewHolder is a great place to listen for clicks, it's not usually the right place to handle them. You should usually handle clicks in the ViewModel, because the ViewModel has access to the data and logic for determining what needs to happen in response to the click.

So you should be doing this:

Adapter:

class ThumbnailAdapter(model: SharedViewModel, val clickListener: ThumbnailListener) :
    RecyclerView.Adapter<ThumbnailAdapter.CustomViewHolder>() {

    private var thumbnailList = listOf<Pair<String, File>>()
    private val myModel = model

    override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
        val item = getItem(position)
        holder.itemView.setOnClickListener {
            listener.onClick(item)
        }
        holder.bind(marsProperty)
    }

    inner class CustomViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        private val thumbnail = view.thumbnail
        private val title = view.title

        fun bind(item: Pair<String, File>) {
            val titleToSet = item.first
            val bitmapToSet = Util.uriToBitmap(item.second)
            val resizedBitmapToSet = Bitmap.createScaledBitmap(bitmapToSet, 150, 150, false)
            thumbnail.setImageBitmap(resizedBitmapToSet)
            title.text = titleToSet
        }
    }

    class ThumbnailListener(val clickListener: (libraryId: Long) -> Unit) {
        fun onClick(val library: Library) = clickListener(library.id)
    }

    [...]
}

ViewModel:

private val _navigateToBook = MutableLiveData<Long>()
val navigateToBook: LiveData<Long>()
   get() = _navigateToBook

fun onLibraryClicked(libraryId: Long) {
    // your stuff
    _navigateToBook.value = libraryId
}

Fragment:

thumbnailAdapter = ThumbnailAdapter(model, ThumbnailListener { libraryId ->
    viewModel.onLibraryClicked(libraryId)
})

viewModel.navigateToBook.observe(viewLifecycleOwner, Observer { book ->
    book?.let {
        this.findNavController().navigate(YOUR_DESTINATION)
    }
})

Second possiblity to set listener with databinding this time:

xml item file:

<data>

<variable
    name="listener"
    type="Your.Package.To.ThumbnailListener" />


</data>

<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="170dp"
    android:onClick="@{() -> listener.onClick(property)}">

    ...

</FrameLayout>

Adapter:

fun bind(item: Pair<String, File>, listener: ThumbnailListener) {
            val titleToSet = item.first
            val bitmapToSet = Util.uriToBitmap(item.second)
            val resizedBitmapToSet = Bitmap.createScaledBitmap(bitmapToSet, 150, 150, false)
            thumbnail.setImageBitmap(resizedBitmapToSet)
            title.text = titleToSet
            itemView.clickListener = listener
        }

Upvotes: 4

Related Questions