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()
}
Related
when i try to delete first row i get first row back and with duplicate second row
enter image description here
after delete any row from the list gives me duplicate data like this
enter image description here
This is my Adapter class. On my deleteItem function position is passed from fragment class and
it is supposed to delete an item from the given position and update the list but its rendering the duplicate data.
class MyListAdapter :ListAdapter<Article,MyListAdapter.MyViewHolder>(MyDiffUtil()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.recycler_item,parent,false)
return MyViewHolder(view)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
holder.onBind(item)
// to click each item of list
holder.itemView.setOnClickListener {
onItemClickListener?.let {it(item) }
}
}
class MyViewHolder(itemView :View) : RecyclerView.ViewHolder(itemView)
{
fun onBind(article: Article) = with(itemView)
{
Glide.with(this)
.load(article.urlToImage)
.error(R.drawable.notfound)
.into(imageView)
webSource.text = article.source?.name
newsDate.text = article.publishedAt
newsTitle.text = article.title
newsDescription.text = article.description
}
}
// lamda function for handling web view
private var onItemClickListener : ((Article) -> Unit)? = null
fun setOnItemClickListener(listener : (Article) ->Unit)
{
onItemClickListener = listener
}
// implementing diffutil class
class MyDiffUtil : DiffUtil.ItemCallback<Article>()
{
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
fun deleteItem(pos : Int)
{
var myl = ArrayList<Article>()
val currentList= currentList.toMutableList()
currentList.removeAt(pos)
myl.addAll(currentList)
submitList(myl)
}
}
`
I tried to delete the saved news from the ListAdapter and update the recycler view with animation of DiffUtill class but its not updating the recycler view and
giving duplicate data . How can i delete data with diffutil animation as its normal way. Thanks in advance.
this is my fragment
package com.example.news.Fragments
class SavedNews : Fragment() {
lateinit var mymainViewModel: MainViewModel
lateinit var databaseObject: MyDataBase
lateinit var myAdapter: MyListAdapter
lateinit var convertedArticleList : ArrayList<Article>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
myAdapter = MyListAdapter()
convertedArticleList = ArrayList()
return inflater.inflate(R.layout.fragment_saved_news, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
savedNewsRecyclerView.layoutManager = LinearLayoutManager(activity,LinearLayoutManager.VERTICAL,
false)
savedNewsRecyclerView.adapter = myAdapter
ItemTouchHelper(object :ItemTouchHelper.SimpleCallback(0,ItemTouchHelper.RIGHT){
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
// this method is called
// when the item is moved.
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// this method is called when we swipe our item to right direction.
// on below line we are getting the item at a particular position.
val deletedCourse: Article =
convertedArticleList.get(viewHolder.adapterPosition)
myAdapter.deleteItem(viewHolder.adapterPosition)
mymainViewModel.deleteSavedNews(deletedCourse.id!!)
// myAdapter.notifyItemRemoved(viewHolder.adapterPosition)
// myAdapter.submitList(myCurrentList)
// Snackbar.make(savedNewsRecyclerView,"Deleted" + deletedCourse.title,
// Snackbar.LENGTH_LONG).setAction(
// "Undo",
// View.OnClickListener {
//
// convertedArticleList.add(position,deletedCourse)
// myAdapter.notifyItemInserted(position)
// }
// ).show()
}
}).attachToRecyclerView(savedNewsRecyclerView)
}
override fun onResume() {
super.onResume()
Log.d("LIFE","ON Resume")
val myInterfaceObject = ApiInterface.MyObject.getInstance()
databaseObject = MyDataBase.MyObjectDB.getDBInstance(activity as MainActivity)
val myRepository = Repository(myInterfaceObject, databaseObject)
mymainViewModel = ViewModelProvider(
this,
MainViewModelFactory(myRepository)
).get(MainViewModel::class.java)
//listAdapter things
// show saved news in savednews fragment
lifecycleScope.launch(Dispatchers.Main) {
//abstract data from saved room database and converting likedartcile datacass object to
// Artticle data class article
mymainViewModel.abstractSavedNews().observe(viewLifecycleOwner, Observer {
it.forEach { eachLikedArticle ->
val obj = toArticle(eachLikedArticle)
convertedArticleList.add(obj)
}
myAdapter.submitList(convertedArticleList)
})
//clicking the item of save news
myAdapter.setOnItemClickListener {
val bundle = Bundle().apply {
putSerializable("article", it)
}
convertedArticleList.clear()
findNavController().navigate(R.id.action_savedNews_to_article, bundle)
}
}
}
private fun toArticle(rawObject: LikedArticle) = Article(
rawObject.author, rawObject.content,
rawObject.description, rawObject.publishedAt, rawObject.source, rawObject.title,
rawObject.url, rawObject.urlToImage, rawObject.id
)
}
There is not enough information to actually replicate how you're trying to use this in your Fragment/Activity but without changing your code too much, this works (note I used view binding):
class MyListAdapter :
ListAdapter<Article, MyViewHolder>(MyDiffUtil()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = MyViewHolder(
RecyclerItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
holder.onBind(item)
}
fun deleteItem(pos: Int) {
var myl = ArrayList<Article>()
val currentList = currentList.toMutableList()
currentList.removeAt(pos)
myl.addAll(currentList)
submitList(myl)
}
}
class MyViewHolder(private val binding: RecyclerItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun onBind(article: Article) = with(binding) {
textView.text = article.url
}
}
class MyDiffUtil : DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
Overall this should work.
You can also refactor deleteItem to something more meaningful:
fun deleteItem(pos: Int) {
val oldList = currentList.toMutableList()
oldList.removeAt(pos)
val updatedList = oldList
submitList(updatedList)
}
I have a RecyclerView with a ListAdapter. The list that is shown in the RecyclerView comes from a Flow that is observed in the Fragment that the recyclerView is instantiated.
When the Fragment is created, the data are calculated too (in onViewCreated Method) .
In the first data-calculation the RecyclerView is empty, a progressBar is shown, then the data is calculated, the progressbar hides, and the RecyclerView is populated.
If I go again in this Fragment to re-calculate the data, the previous list is shown simultaneously with the progressbar, and then is updated.
I want every time that new data is calculated to NOT show the previous list (just like the first data-calculation), but can't find a way to do it.
I tried to clear() the currentList and notifyDataSetChanged() but it still happens.. Any ideas?
Here is the code:
Fagment:
#AndroidEntryPoint
class YourPlanFragment : Fragment(R.layout.fragment_your_plan) {
lateinit var navController: NavController
private lateinit var binding: FragmentYourPlanBinding
private val sharedViewModel: WorkoutPlansViewModel by activityViewModels()
private lateinit var appBarConfiguration: AppBarConfiguration
#Inject
lateinit var dataStore: UserPreferencesRepo
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = Navigation.findNavController(view)
binding = FragmentYourPlanBinding.bind(view)
appBarConfiguration = AppBarConfiguration(navController.graph)
val toolbar = binding.yourPlanToolbar
toolbar.setupWithNavController(navController, appBarConfiguration)
val exerciseAdapter = DayListAdapter(DayListAdapter.OnClickListener {
navigateTo(sharedViewModel.weekIndex, it.dayNumber)
})
exerciseAdapter.currentList.clear()
exerciseAdapter.notifyDataSetChanged()
binding.recyclerViewYourPlan.apply {
adapter = exerciseAdapter
layoutManager = LinearLayoutManager(requireContext())
setHasFixedSize(true)
recycledViewPool.clear()
removeAllViews()
adapter?.notifyDataSetChanged()
}
if (!sharedViewModel.planGenerated) {
// Here the data is generated
sharedViewModel.onTriggerEvent(WorkoutPlansEvent.GetWorkoutPlanEvent)
} else if (sharedViewModel.planGenerated) {
sharedViewModel.onTriggerEvent(WorkoutPlansEvent.GetWeekEvent)
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
sharedViewModel.yourPlanState.collect { yourPlanState ->
when (yourPlanState.progressBarState) {
is ProgressBarState.Loading -> {
binding.progressBar.isVisible = true
}
is ProgressBarState.Idle -> {
binding.progressBar.isInvisible = true
}
}
exerciseAdapter.submitList(yourPlanState.planDays)
}
}
}
}
private fun navigateTo(
currentWeek: Int,
dayNumber: Int,
) {
val action =
YourPlanFragmentDirections.actionYourPlanFragmentToYourDayFragment(
currentWeek,
dayNumber
)
navController.navigate(action)
}
}
ListAdapter:
class DayListAdapter(private val onClickListener: OnClickListener) :
ListAdapter<Day, DayListAdapter.DayViewHolder>(ExerciseComparator()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DayViewHolder {
val binding = ListItemDayBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return DayViewHolder(binding)
}
override fun onBindViewHolder(holder: DayViewHolder, position: Int) {
val currentItem = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(currentItem)
}
if (currentItem != null) {
holder.bind(currentItem)
}
}
class DayViewHolder(private val binding: ListItemDayBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(day: Day) {
binding.apply {
dayNumber = day.dayNumber.toString()
executePendingBindings()
}
}
}
class ExerciseComparator : DiffUtil.ItemCallback<Day>() {
override fun areItemsTheSame(oldItem: Day, newItem: Day) =
oldItem.dayNumber == newItem.dayNumber
override fun areContentsTheSame(oldItem: Day, newItem: Day) =
oldItem == newItem
}
class OnClickListener(val clickListener: (day: Day) -> Unit) {
fun onClick(day: Day) = clickListener(day)
}
}
This is happing because of the activityViewModels() that will preserve the viewmodel based on activity lifecycle. Either create WorkoutPlansViewModel using viewmodels() or Try to clear the list from the WorkoutPlansViewModel when you are creating the fragment.
The title says, I'm trying to load items into a recyclerview but the items doesn't displaying.
I'm getting the items from the api, using retrofit and mutablelivedata, I'm getting the items right, and the adapter gets the items (there are 3 items, and the adapter gets the 3 items), but the items doesn't display on the UI.
Here is my code:
Adapter.java
class EstablecimientosAdapter : ListAdapter<EstablecimientoModel, EstablecimientosViewHolder>(
DIFF_CALLBACK
) {
companion object {
val DIFF_CALLBACK: DiffUtil.ItemCallback<EstablecimientoModel> =
object : DiffUtil.ItemCallback<EstablecimientoModel>() {
override fun areItemsTheSame(
oldItem: EstablecimientoModel,
newItem: EstablecimientoModel
): Boolean {
return oldItem.hash == newItem.hash
}
override fun areContentsTheSame(
oldItem: EstablecimientoModel,
newItem: EstablecimientoModel
): Boolean {
return oldItem.nombre == newItem.nombre
}
}
}
private val mEstablecimientos: MutableList<EstablecimientoModel> = mutableListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EstablecimientosViewHolder {
val binding = RowEstablecimientoBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return EstablecimientosViewHolder(binding)
}
override fun onBindViewHolder(holder: EstablecimientosViewHolder, position: Int) {
val establecimiento = getItem(position)
holder.bindItem(establecimiento!!)
}
override fun getItemCount(): Int {
return mEstablecimientos.size
}
fun addMoreEstablecimientos(newEstablecimientos: List<EstablecimientoModel>) {
mEstablecimientos.addAll(newEstablecimientos)
submitList(mEstablecimientos)
}
class EstablecimientosViewHolder(val binding: RowEstablecimientoBinding) :
RecyclerView.ViewHolder(
binding.root
) {
fun bindItem(establecimiento: EstablecimientoModel) {
binding.setVariable(BR.establecimiento, establecimiento)
}
}
}
recycler_view.xml
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvEstablecimientos"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="#layout/row_establecimiento" />
Fragment.java
val cvGoBackEstablecimientos = binding!!.cvGoBackEstablecimientos
val rvEstablecimientos = binding!!.rvEstablecimientos
val adapter = EstablecimientosAdapter()
rvEstablecimientos.setHasFixedSize(true)
rvEstablecimientos.adapter = adapter
cvGoBackEstablecimientos.setOnClickListener { requireActivity().onBackPressed() }
viewModel?.getNearFreeEstablecimientos(2.0, 2.0)
?.observe(viewLifecycleOwner) { t -> adapter.addMoreEstablecimientos(t!!) }
It seems that I had some problems with the layout, I removed a CoordinatorLayout and it works now.
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)
}
}
I have a RecyclerView displaying data from a Dao.
In the data that is shown in the recyclerView is a button with which the item should get deleted.
I know the function for that is :
fun deleteReceipt(receipts: Receipts) = viewModelScope.launch {
receiptDao.delete(receipts)
}
But I am not quite sure where to put it and call it. Because if I want to call it in the Adapter where i set the other displayed Item values I have no access to the Dao
Here is the fragment:
#AndroidEntryPoint
class HistoryFragment : Fragment(R.layout.fragment_history) {
private val viewModel: PurchaseViewmodel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentHistoryBinding.bind(view)
val exampleAdapter = ExampleAdapter()
binding.apply{
recyclerView.apply{
layoutManager = LinearLayoutManager(requireContext())
adapter = exampleAdapter
setHasFixedSize(true)
}
}
setFragmentResultListener("add_receipt_request"){_,bundle ->
val result = bundle.getInt("add_receipt_request")
viewModel.onAddResult(result)
}
viewModel.receipts.observe(viewLifecycleOwner){ /// New Items get passed to the List
exampleAdapter.submitList(it)
}
}
}
The ViewModel:
#HiltViewModel
class PurchaseViewmodel #Inject constructor(
private val receiptDao: ReceiptDao
): ViewModel() {
private val tasksEventChannel = Channel<TasksEvent>()
val addTaskEvent = tasksEventChannel.receiveAsFlow()
val receipts = receiptDao.getAllReceipts().asLiveData()
fun onAddResult(result: Int){
when (result){
ADD_RECEIPT_RESULT_OK ->showReceiptSavedConfirmation("Receipt is saved")
}
}
private fun showReceiptSavedConfirmation (text: String) = viewModelScope.launch {
tasksEventChannel.send(TasksEvent.ShowReceiptSavedConfirmation(text))
}
fun deleteReceipt(receipts: Receipts) = viewModelScope.launch {
receiptDao.delete(receipts)
}
sealed class TasksEvent {
data class ShowReceiptSavedConfirmation(val msg: String) : TasksEvent()
}
}
And the Adapter:
class ExampleAdapter : ListAdapter<Receipts,ExampleAdapter.ExampleViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExampleViewHolder { // Basically how to get a new Item from the List and display it
val binding = ReceiptsBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return ExampleViewHolder(binding)
}
override fun onBindViewHolder(holder: ExampleViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
}
override fun getItemCount(): Int {
return super.getItemCount()
}
class ExampleViewHolder(private val binding: ReceiptsBinding) : RecyclerView.ViewHolder(binding.root){ //Examples One Row in our list
fun bind (receipts: Receipts) {
binding.apply {
storeHistory.text = receipts.store
amountHistory.text = receipts.total
dateHistory.text = receipts.date
}
}
}
class DiffCallback : DiffUtil.ItemCallback<Receipts>() {
override fun areItemsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem == newItem
}
}
Create a listener using Interface
Implement the listener in Fragment
Create listener variable in Adapter
pass the listener from fragment to adapter
pass the listener to viewHolder
on Delete button click of that item call the listener method, which will trigger the implementation in Fragment where you have access to your ViewModel
delete the receipt using viewModel.deleteReceipt