For a particular Recycler View Item, If I select the Checkbox (tick it) then I need the text of its corresponding TextView to formatted as Strikethrough.
I am using Binding Adapters, Flow and Live Data.
But after selecting the checkbox, its corresponding TextView is not getting formatted.
But If I navigate to some other fragment and come back to here(FruitFragmnet) then the TextView data is formatted. (i.e. the database gets updated correctly on ticking checkbox but the live data emission is delayed to UI)
Possible Root Cause: My update to Room Database is happening immeialtey, but from database the LiveData is not flown to UI immediately.
I did lot of trial and errors, read multiple similar questions but I was unable to find the missing link and solution to this issue.
Please advice. Following is the code:
BindingAdapter
#BindingAdapter("markAsCompleted")
fun markAsCompleted(textView: TextView, completed: Boolean) {
if (completed) {
textView.paintFlags = textView.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
} else {
textView.paintFlags = textView.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
}
#BindingAdapter("setItems")
fun setItems(view: RecyclerView, items: List<Fruit>?) {
items?.let {
(view.adapter as SettingAdapter).submitList(items)
}
}
Fruit Fragment with Recycler View
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="settingViewModel"
type="com.example.ui.SettingViewModel" />
</data>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/fruits_list"
setItems="#{settingViewModel.allList}" // This is Binding Adapter
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</layout>
Above Fruit's Fragment Item View
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.widget.CompoundButton" />
<variable
name="fruit"
type="com.example.data.Fruit" />
<variable
name="settingViewModel"
type="com.example.ui.SettingViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
...
<CheckBox
android:id="#+id/fruit_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="#{fruit.completed}"
android:onClick="#{(view) -> settingViewModel.completeFruit(fruit,((CompoundButton)view).isChecked())}"
/>
<TextView
android:id="#+id/fruit_name"
markAsCompleted="#{fruit.completed}" // This is Binding Adapter
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="#{fruit.fruit}" />
....
Fruit Fragment
class FruitFragment : Fragment() {
private lateinit var binding: FragmentFruitBinding
private lateinit var fruitAdapter: FruitAdapter
private val viewModel: SettingViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentFruitBinding.inflate(layoutInflater, container, false).apply {
lifecycleOwner = viewLifecycleOwner
settingViewModel = viewModel
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fruitAdapter = FruitAdapter(viewModel)
binding.fruitslist.apply {
adapter = fruitAdapter
}
}
}
SettingViewModel
class SettingViewModel(application: Application) : AndroidViewModel(application) {
private val app = getApplication<Application>()
private val dao = Database.getDatabase(app.applicationContext).dao
val allList: LiveData<List<Fruit>> = dao.getFruits().asLiveData().distinctUntilChanged()
fun completeFruit(fruit: Fruit, completed: Boolean) {
viewModelScope.launch {
if (completed) {
dao.updateCompleted(fruit.id, completed)
} else {
dao.updateCompleted(fruit.id, completed)
}
}
}
....
}
DAO Class
#Dao
interface DatabaseDao {
#Query("SELECT * FROM fruit_table")
fun getFruits(): Flow<List<Fruit>>
#Query("UPDATE fruit_table SET completed = :completed WHERE id = :id")
suspend fun updateCompleted(id: Int, completed: Boolean)
}
Recycler View Adapter
class FruitAdapter(private val viewModel: SettingViewModel) : ListAdapter<Fruit, ViewHolder>(FruitDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item, viewModel)
}
class ViewHolder private constructor(val binding: ContainerFruitBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Fruit, viewModel: SettingViewModel) {
binding.apply {
settingViewModel = viewModel
fruit = item
executePendingBindings()
}
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ContainerFruitBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
}
class FruitDiffCallback : DiffUtil.ItemCallback<Fruit>() {
override fun areItemsTheSame(oldItem: Fruit, newItem: Fruit): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Fruit, newItem: Fruit): Boolean {
return oldItem.fruit == newItem.fruit
}
}
Data Class
#Entity(tableName = "fruit_table")
data class Fruit(
#PrimaryKey(autoGenerate = true)
var id: Int = 0,
var fruit: String,
var completed: Boolean = false
)
I guess you need to change the second parameter of setItems function to LiveData in BindingAdapter:
#BindingAdapter("setItems")
fun setItems(view: RecyclerView, data: LiveData<List<Fruit>>) {
data.value?.let {
(view.adapter as SettingAdapter).submitList(it)
}
}
Related
I have a list of items on a Recyclerview, and I also have a SearchView to filther those items.
Every item has a favourite button, so when you click, the item adds to favorite table.
The problem is that, when I filter something and I start clicking those buttons, odd things happens: some items dissapear from the filtered list. It doesn't happen always, only sometimes. How can I fix this?
My code:
My class:
class CoasterFragment : Fragment() {
private val myAdapter by lazy { CoasterRecyclerViewAdapter(CoasterListenerImpl(requireContext(), viewModel),requireContext()) }
private lateinit var searchView: SearchView
private var _binding: FragmentCoasterBinding? = null
private val binding get() = _binding!!
private val viewModel: CoastersViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCoasterBinding.inflate(inflater, container, false)
val root: View = binding.root
val recyclerView = binding.recyclerCoaster
recyclerView.adapter = myAdapter
recyclerView.layoutManager = LinearLayoutManager(requireContext())
viewModel.coasters().observe(viewLifecycleOwner){myAdapter.setData(it)}
searchView = binding.search
searchView.clearFocus()
searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
if(query != null){
searchDatabase(query)
searchView.clearFocus()
}
return true
}
override fun onQueryTextChange(query: String?): Boolean {
if(query != null){
searchDatabase(query)
}
return true
}
})
return root
}
fun searchDatabase(query: String) {
val searchQuery = "%$query%"
viewModel.searchDatabase(searchQuery).observe(viewLifecycleOwner) { myAdapter.setData(it)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Here is my adapter code:
class CoasterRecyclerViewAdapter( val listener: CoasterListener,
val context: Context ) : RecyclerView.Adapter<CoasterRecyclerViewAdapter.ViewHolder>(){
private var coasterList = emptyList<CoasterFavorito>()
class ViewHolder private constructor(val binding: CoasterItemBinding, private val listener: CoasterListener,
private val context: Context): RecyclerView.ViewHolder(binding.root){
companion object{
fun crearViewHolder(parent: ViewGroup, listener: CoasterListener, context: Context):ViewHolder{
val layoutInflater = LayoutInflater.from(parent.context)
val binding = CoasterItemBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding, listener, context )
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder.crearViewHolder(parent, listener, context)
override fun onBindViewHolder(holder: ViewHolder, position: Int){
holder.binding.nombre.text = coasterList[position].coaster.nombre
holder.binding.parque.text = coasterList[position].coaster.parque
holder.binding.ciudad.text = coasterList[position].coaster.ciudad
holder.binding.provincia.text = coasterList[position].coaster.provincia
holder.binding.comunidad.text = coasterList[position].coaster.comunidadAutonoma
Glide
.with(context)
.load(coasterList[position].coaster.imagen)
.centerCrop()
.into(holder.binding.imagen)
holder.binding.check.isChecked = coasterList[position].favorito
holder.binding.check.setOnClickListener{
if (coasterList[position].favorito) {
listener.delFavorito(coasterList[position].coaster.id)
holder.binding.check.isChecked = false
} else {
listener.addFavorito(coasterList[position].coaster.id)
holder.binding.check.isChecked = true
}
}
}
override fun getItemCount(): Int{
return coasterList.size
}
fun setData(coaster: List<CoasterFavorito>){
coasterList = coaster
notifyDataSetChanged()
}
}
interface CoasterListener {
fun addFavorito(id: Long)
fun delFavorito(id: Long)
}
The search Query:
#Query ("SELECT c.*, " + "EXISTS (SELECT * from montarse where usuario_id=:id and coaster_id = c.id) as favorito " + "FROM coasters c " + "WHERE nombre LIKE :searchQuery OR parque LIKE :searchQuery OR ciudad LIKE :searchQuery OR comunidadAutonoma LIKE :searchQuery OR provincia LIKE :searchQuery")
fun searchCoaster(id: Long, searchQuery: String): Flow<List<CoasterFavorito>>
My viewModel:
fun searchDatabase( searchQuery: String): LiveData<List<CoasterFavorito>> {
return coasterDao.searchCoaster( App.getUsuario()!!.id, searchQuery).asLiveData()
}
fun addFavorito( coasterId: Long) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
usuarioCoasterDao.create(UsuarioCoaster(App.getUsuario()!!.id, coasterId, null, null))
}
}
}
fun coasters(): LiveData<List<CoasterFavorito>> {
return coasterDao.findAllFav(App.getUsuario()!!.id).asLiveData()
}
my 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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.coaster.HomeCoasterFragment">
<androidx.appcompat.widget.SearchView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:id="#+id/search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:iconifiedByDefault="false"
app:searchHintIcon="#null"
android:queryHint="Buscar..."
android:focusable="false"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerCoaster"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="5dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/search"
tools:listitem="#layout/coaster_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
I tried changind the clear focus().
Also I added some if else (for example, if the searchView is Empty, then load the list from the adapter as normal. If it is not empty, use the SearchView code to filter and load the list.
I make an Android application in Kotlin, and i want to get some information from the webserver and put in a recycler view using data binding. I use repository pattern, so each time i make a get request, i save the information in room database and get data from it with live data. For some reason my ryclerview show nothing, and i get no errors. The request work and i get the data but the recyclerview not working.
My ViewModel:
class HomeViewModel(application: Application) : ViewModel() {
private val availableTicketsRepo = AvailableTicketsRepository(getInstance(application))
val tickets = availableTicketsRepo.availableTickets
init {
refreshDataFromRepository()
}
private fun refreshDataFromRepository() {
viewModelScope.launch {
try {
availableTicketsRepo.refreshAvailableTickets()
} catch (ex: Exception) {
ex.message?.let { Resource.error(data = null, message = it) }
Log.i("Home", "*** Eccezione: ${ex} ***")
}
}
}
My fragment class (with adapter and viewholder):
package com.example.ticketapp.ui.home
class HomeFragment : Fragment() {
private val viewModel: HomeViewModel by lazy {
val activity = requireNotNull(this.activity) {
"You can only access the viewModel after onActivityCreated()"
}
ViewModelProvider(this, HomeViewModel.Factory(activity.application))
.get(HomeViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = DataBindingUtil.inflate<FragmentHomeBinding>(
inflater,
R.layout.fragment_home,
container,
false
)
binding.lifecycleOwner = this
binding.viewModel = viewModel
binding.availableTicketsList.adapter = AvailableTicketsAdapter()
return binding.root
}
}
class AvailableTicketsAdapter() :
ListAdapter<AvailableTicket, AvailableTicketsAdapter.AvailableTicketViewHolder>(DiffCallback) {
class AvailableTicketViewHolder(private var binding: AvailableTicketItemsBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(tickets: AvailableTicket) {
binding.ticket = tickets
binding.executePendingBindings()
}
}
companion object DiffCallback : DiffUtil.ItemCallback<AvailableTicket>() {
override fun areItemsTheSame(oldItem: AvailableTicket, newItem: AvailableTicket): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(
oldItem: AvailableTicket,
newItem: AvailableTicket
): Boolean {
return oldItem.Id == newItem.Id
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AvailableTicketViewHolder {
return AvailableTicketViewHolder(
AvailableTicketItemsBinding.inflate(
LayoutInflater.from(
parent.context
)
)
)
}
override fun onBindViewHolder(holder: AvailableTicketViewHolder, position: Int) {
val tickets = getItem(position)
holder.bind(tickets)
}
}
My repository:
class AvailableTicketsRepository(private val database: TicketDatabase) {
val availableTickets: LiveData<List<AvailableTicket>> =
Transformations.map(database.ticketDatabaseDao.getAvailableTickets()) {
it.asDomainModel()
}
suspend fun refreshAvailableTickets() {
withContext(Dispatchers.IO) {
val tickets = RetrofitBuilder.apiService.getAvailableTickets()
database.ticketDatabaseDao.insertAllAvailableTickets(tickets.map { it.asDataBaseModel() })
}
}
}
The xml for items in recylerview:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View"/>
<variable
name="ticket"
type="com.example.ticketapp.model.AvailableTicket" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/titleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLength="50"
android:text="#{ticket.title}"
android:singleLine="true"
android:textAlignment="center"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Titolo di max 50 caratteri per non uscire dalla riga" />
<TextView
android:id="#+id/customerText"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textSize="14sp"
android:text="#{ticket.customer}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/titleText"
tools:text="ESEMPIO DI UN REFERENTE" />
<TextView
android:id="#+id/dateText"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textAlignment="textEnd"
android:textSize="14sp"
android:text="#{ticket.date_Time}"
app:layout_constraintBaseline_toBaselineOf="#+id/customerText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#+id/titleText"
tools:text="20/01/2021 10:00:00" />
<ImageView
android:id="#+id/warningImg"
style="#style/iconImg"
android:layout_marginTop="20dp"
android:visibility="#{ticket.triangle ? View.VISIBLE : View.GONE}"
android:foregroundGravity="center"
app:layout_constraintEnd_toStartOf="#+id/dateText"
app:layout_constraintStart_toEndOf="#+id/customerText"
app:layout_constraintTop_toBottomOf="#+id/titleText"
app:srcCompat="#drawable/ic_warning" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
XML of ryclerview:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/availableTicketsList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="10dp"
android:paddingEnd="10dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/toolbar"
tools:listitem="#layout/available_ticket_items" />
My Model class:
#Parcelize
data class AvailableTicket (
val Id: Int,
val Title: String,
val Description: String,
val Date_Time: String,
val Customer: String,
val Field: String,
val Category: String,
val Color: String,
val Author: String,
val Triangle: Boolean,
val MyPriority: String
): Parcelable
You should, notify adapter when data is modified, add a LiveData inside ViewModel and post it after availableTicketsRepo.refreshAvailableTickets(), then call submitList inside adapter once the data is received
ViewModel
class HomeViewModel(application: Application) : ViewModel() {
private val availableTicketsRepo = AvailableTicketsRepository(getInstance(application))
val tickets = availableTicketsRepo.availableTickets
init {
refreshDataFromRepository()
}
private fun refreshDataFromRepository() {
viewModelScope.launch {
try {
availableTicketsRepo.refreshAvailableTickets()
} catch (ex: Exception) {
ex.message?.let { Resource.error(data = null, message = it) }
Log.i("Home", "*** Eccezione: ${ex} ***")
}
}
}
}
Then in Fragment, submit list to adapter once fetched
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = DataBindingUtil.inflate<FragmentHomeBinding>(
inflater,
R.layout.fragment_home,
container,
false
)
binding.lifecycleOwner = this
binding.viewModel = viewModel
binding.availableTicketsList.adapter = AvailableTicketsAdapter()
viewModel.tickets.observe(this, Observer{
(binding.availableTicketsList.adapter as? AvailableTicketsAdapter)?.submitList(it)
})
return binding.root
}
How can I change the backgroudColor ofselectedItem in recyclerView adapter when I use android databinding?
this is my Adapte,and class CategoyItemClickListener is implemened for handling item clicks :
class ProgramCatAdapter(
val mContext: Context,
val mData: MutableList<CategoryResponse>,
val clickListener: CategoyItemClickListener
) : RecyclerView.Adapter<ProgramCatAdapter.CategoryViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): CategoryViewHolder {
return CategoryViewHolder.from(parent)
}
override fun getItemCount(): Int {
return mData.size
}
override fun onBindViewHolder(holder: CategoryViewHolder, position: Int) {
holder.bind(
mData[position],
clickListener,
position
)
}
fun getItem(position: Int): CategoryResponse = mData[position]
fun getPosition(item: CategoryResponse): Int = mData.indexOf(item)
class CategoryViewHolder private constructor(val binding: ProgramCatHorizontalBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
item: CategoryResponse,
clickListener: CategoyItemClickListener,
position: Int
) {
itemView.isActivated = isSelected
binding.item = item
binding.position = position
binding.clickListener = clickListener
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): CategoryViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ProgramCatHorizontalBinding.inflate(layoutInflater, parent, false)
return CategoryViewHolder(binding)
}
}
}
}
class CategoyItemClickListener(val clickListener: (item: CategoryResponse) -> Unit) {
fun onClick(item: CategoryResponse) {
clickListener(item)
}
}
And this code is for binding adapter to recyclerView :
private fun bindCategories(cats: MutableList<CategoryResponse>?) {
programCatAdapter = ProgramCatAdapter(mContext!!,
cats!!, CategoyItemClickListener {
viewModel.setSelectedCat(it)
})
binding.catRecycler.layoutManager =
LinearLayoutManager(activity!!, LinearLayoutManager.HORIZONTAL, false)
binding.catRecycler.adapter = programCatAdapter
}
How can I have access to the position of selected Item from CategoyItemClickListener and change the bgColor of that item?
Here an example:
This video might give you an overview: https://youtu.be/g8GDLOMt600 (You are not going to find a solution for this question in this video. It just gives you a nice overview about data binding, RecyclerView and ClickListener)
You need the following files:
YourFragment, fragment_your.xml, list_item
class YourFragment : Fragment(){
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Get a reference to the binding object and inflate the fragment views.
val binding: FragmentYoutFragmentBinding = DataBindingUtil.inflate(
inflater, R.layout.fragment_your, container, false
)
binding.lifecycleOwner = this
// Very important! Otherwise the layout of the recycler view wont work
binding.yourList.layoutManager = LinearLayoutManager(this.context)
// Adapter for the RecyclerView in order to show all items
val adapter = YourAdapter(YourListener{youtItemObeject: YourItemObject, view:
View ->
// THIS IS THE SOLUTION
// change selected item image, if user selects this item in the list
view.ok_image.setImageResource(R.drawable.ok_green)
})
}
}
in list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="your_object"
type="your_package_name.your_folder.YourObject" />
<variable
name="clickListener"
type="your_package_name.your_folder.YourListener" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/constraint_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="#dimen/item_margin_horizontal"
android:layout_marginTop="#dimen/item_margin_vertical"
android:layout_marginEnd="#dimen/item_margin_horizontal"
android:layout_marginBottom="#dimen/item_margin_vertical"
android:onClick="#{(thisView) -> clickListener.onClick(your_object, thisView)}">
<ImageView
android:id="#+id/ok_image"
... />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
And add YourListener class into the adapter-File
class YourAdapter(val clickListener: YourListener) :
ListAdapter<YourObject, YourAdapter.ViewHolder>(YourDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position)!!, clickListener)
}
class ViewHolder private constructor(val binding: ListItemYourObjectBinding) :
RecyclerView.ViewHolder(binding.root) {
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding =
ListItemYourObjectBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
fun bind(item: YourObject, clickListener: YourListener) {
binding.yourObject = item
binding.clickListener = clickListener
// for example
binding.okImage.setImageRessouce(R.drawable.ok_gray)
binding.executePendingBindings()
}
}
}
/**
* Callback for calculating the diff between two non-null items in a list.
*
* Used by ListAdapter to calculate the minimum number of changes between and old list
* and a new list that's been passed to `submitList`.
*/
class YourObjectDiffCallback : DiffUtil.ItemCallback<YourObject>() {
override fun areItemsTheSame(oldItem: YourObject, newItem: YourObject): Boolean {
return oldItem.yourObjectValue == newItem.yourObjectValue
}
override fun areContentsTheSame(oldItem: YourObject, newItem: YourObject): Boolean {
return oldItem == newItem
}
}
/**
* Listener for your list items
*/
class YourListener(val clickListener: (yourObject: YourObject,
view: View) -> Unit) {
fun onClick(yourObject: YourObject, view: View) =
clickListener(yourObject, view)
}
I am working on the Movies app where I want to load movies and save as cache in local db. I was following the Udacity's "Building Andoid apps with Kotlin" course "Mars real estate" example. My app is using Live data, MVVM, databinding, Room and Retrofit. Each time new data is loaded, it is saved in db, and is the single source of 'truth'.
This is the main layout:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.moviesapp.presentation.main.MainViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.moviesapp.presentation.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/movies_grid"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="6dp"
android:clipToPadding="false"
android:background="#android:color/black"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:listData="#{viewModel.movies}"
app:spanCount="3"
tools:itemCount="16"
tools:listitem="#layout/grid_view_item"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Fragment:
class MainFragment : DaggerFragment(), SharedPreferences.OnSharedPreferenceChangeListener {
/**
// * Lazily initialize [MainViewModel].
// */
private val viewModel: MainViewModel by lazy {
ViewModelProviders.of(this, providerFactory).get(MainViewModel::class.java)
}
#Inject
lateinit var providerFactory: ViewModelProviderFactory
#Inject
lateinit var sharedPreferences: SharedPreferences
override fun onResume() {
super.onResume()
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
val binding = FragmentMainBinding.inflate(inflater)
binding.lifecycleOwner = this
binding.viewModel = viewModel
binding.moviesGrid.adapter = MainAdapter(MainAdapter.OnClickListener {
viewModel.displayMovieDetails(it)
})
viewModel.navigateToSelectedMovie.observe(this, Observer {
if (null != it) {
this.findNavController().navigate(MainFragmentDirections.actionShowDetail(it.id))
viewModel.displayMovieDetailsComplete()
}
})
setHasOptionsMenu(true)
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.main, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_settings -> {
this.findNavController().navigate(
MainFragmentDirections.actionShowSettings()
)
true
}
R.id.action_search -> {
this.findNavController().navigate(
MainFragmentDirections.actionMainFragmentToSearchFragment()
)
true
}
R.id.action_back -> {
viewModel.paginateBack()
true
}
R.id.action_forward -> {
viewModel.paginateForward()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onDestroy() {
super.onDestroy()
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
viewModel.onPreferencesChanged()
}
}
View Model:
class MainViewModel #Inject constructor(application: Application, private val getMoviesUseCase: GetMoviesUseCase) :
AndroidViewModel(application) {
private var currentPage = 1
private val totalPages: Int by lazy {
with (PreferenceManager.getDefaultSharedPreferences(application)) {
getInt(TOTAL_PAGES, 0)
}
}
// get movies saved in local db
val movies = getMoviesUseCase.allMovies()
init {
loadMovies()
}
private fun loadMovies() {
if (isInternetAvailable(getApplication()))
viewModelScope.launch {
getMoviesUseCase.refreshMovies(currentPage)
}
}
private val _navigateToSelectedMovie = MutableLiveData<Movie>()
val navigateToSelectedMovie: LiveData<Movie>
get() = _navigateToSelectedMovie
fun displayMovieDetails(movie: Movie?) {
if (movie != null) _navigateToSelectedMovie.value = movie
}
fun displayMovieDetailsComplete() {
_navigateToSelectedMovie.value = null
}
Adapter:
class MainAdapter(private val onClickListener: OnClickListener) :
ListAdapter<Movie, MainAdapter.MovieViewHolder>(DiffCallback) {
class MovieViewHolder(private var binding: GridViewItemBinding):
RecyclerView.ViewHolder(binding.root ) {
fun bind(movie: Movie) {
binding.movie = movie
binding.executePendingBindings()
}
}
companion object DiffCallback : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem.id == newItem.id
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
return MovieViewHolder(
GridViewItemBinding.inflate(
LayoutInflater.from(parent.context)
)
)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
val movie = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(movie)
}
holder.bind(movie)
}
class OnClickListener(val clickListener: (movie: Movie) -> Unit) {
fun onClick(movie: Movie) = clickListener(movie)
}
}
Data binding adapters:
#BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, data: List<Movie>?) {
val adapter = recyclerView.adapter as MainAdapter
adapter.submitList(data)
}
#BindingAdapter("posterImageUrl")
fun bindPosterImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = (IMAGE_BASE_URL + POSTER_SIZE + imgUrl).toUri().buildUpon().build()
Glide.with(imgView.context)
.load(imgUri)
.apply(RequestOptions().placeholder(R.drawable.loading_animation).error(R.drawable.ic_broken_image))
.into(imgView)
}
}
I now data new Live data appears in my View model, but the Recyclerview doesn't get updated until I swipe to refresh. The viewmodel instance is passed to the recyclerview via databinding. I now in fragments we call the observe method on Live data, but don't know how its is done in this example. In the Mars real estate example it appears to be working. I cannot find where is the difference in my code.
Thanks in advance,
Armands
// in viewModel, is the type of movies LiveData<List<Movie>>?
val movies = getMoviesUseCase.allMovies()
If the type of movies returned from getMoviesUseCase.allMovies() is LivaData<List<Movie>> type, the observers(Recyclerview) can receive the changes.
If the type of movies is List<Movie>, the observers cannot receive the changes, because no one to notify the observers. You should, in this case, change it to the LiveData<List<Movie>>.
private val _movies = MutableLiveData<List<Movie>>().apply { value = emptyList() }
val movies: LiveData<List<Movie>> = _movies
and try to init the _movies like this:
viewModelScope.launch {
_movies.value = getMoviesUseCase.allMovies()
}
I have a Dao with this method.
#Query("SELECT * FROM expense WHERE date BETWEEN :dateStart AND :dateEnd")
fun getExpensesBetweenTheDate(dateStart: Calendar, dateEnd: Calendar):
DataSource.Factory<Int, Expense>
My repository get Dao and create LiveData> object.
fun getExpensesBetweenTheDate(startDay: Calendar, endDay: Calendar): LiveData<PagedList<Expense>> {
val factory = expenseDao.getExpensesBetweenTheDate(startDay, endDay)
val config = PagedList.Config.Builder()
.setPageSize(30)
.setMaxSize(200)
.setEnablePlaceholders(true)
.build()
return LivePagedListBuilder(factory, config)
.build()
}
My ViewModel get repository and create a variable.
val expenses = repository.getExpensesBetweenTheDate(startCalendar, endCalendar)
Finally, MainActivity observes on LiveData.
viewModel.expenses.observe(this, Observer(simpleExpenseAdapter::submitList))
All working fine, but when I try to add a new record to the database, it appears there not immediately, but after restarting the application. Similar code without a paging library works well. Maybe i do something wrong. Just in case, I give below the code of the adapter, viewHolder and layout.
Adapter.
class ExpenseAdapter : PagedListAdapter<Expense, ExpenseViewHolder>(EXPENSE_COMPARATOR) {
companion object {
private val EXPENSE_COMPARATOR = object : DiffUtil.ItemCallback<Expense>() {
override fun areItemsTheSame(oldItem: Expense, newItem: Expense): Boolean {
return oldItem.expenseId == newItem.expenseId
}
override fun areContentsTheSame(oldItem: Expense, newItem: Expense): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExpenseViewHolder {
return ExpenseViewHolder.create(parent)
}
override fun onBindViewHolder(holder: ExpenseViewHolder, position: Int) {
val expenseItem = getItem(position)
if (expenseItem != null) holder.bind(expenseItem)
}
}
ViewHolder.
class ExpenseViewHolder(binding: ExpenseElementSimpleBinding) : RecyclerView.ViewHolder(binding.root) {
private val mBinding = binding
init {
mBinding.root.setOnClickListener {
val intent = Intent(it.context, ShowExpenseActivity::class.java)
intent.putExtra("expense", mBinding.expense)
it.context.startActivity(intent)
}
}
companion object {
fun create(parent: ViewGroup): ExpenseViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ExpenseElementSimpleBinding.inflate(inflater, parent, false)
return ExpenseViewHolder(binding)
}
}
fun bind(item: Expense) {
mBinding.apply {
expense = item
executePendingBindings()
}
}
}
Layout.
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="expense"
type="com.example.budgetplanning.data.model.Expense"/>
</data>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
android:text="#{expense.description}"
tools:text="Gasoline"
android:padding="5dp"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"/>
<androidx.appcompat.widget.AppCompatTextView
android:text="#{String.valueOf(expense.amount)}"
tools:text="123"
android:padding="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>
You have to call the simpleExpenseAdapter.notifyDataSetChanged() after the the submitList
This is happening because when you are calling simpleExpenseAdapter::submitList is equivalent to call simpleExpenseAdapter:submitList() when the list diff is not called at this time. So, you have to notify that the list has changed.
Or so, you can pass the new list as a parameter like:
viewModel.expenses.observe(this, Observer<YourObjectListened> {
simpleExpenseAdapter.submitList(it)
})
try to use toLiveData with original example from Paging library overview