Android Kotlin RecyclerView in Fragment is not working - android

I'm very new in Kotlin and Android programming. I tried to create a Fragment which populates a Recycler view, but somehow I get the following error: E/RecyclerView: No adapter attached; skipping layout
I don't really understand why I get this, since I binded everything. If somebody can explain what I'm doing wrong I would really appreciate it. My code is the following:
My class:
data class Movie(val id:Int, val posterPath:String, val vote:Double, val language:String,val releaseDate:String, val title:String) {}
My fragment:
class MovelistScreen : Fragment(R.layout.fragment_movelist_screen) {
#ExperimentalStdlibApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// View created, can be accessed
// val args = arguments ?: throw IllegalArgumentException("Use new instance method")
// val argValue = args.getString(ARG_NAME)
val binding = FragmentMovelistScreenBinding.inflate(layoutInflater)
val lst : List<Movie> = buildList {
add(Movie(id=1,posterPath="/asdasd",vote=7.3,language="Eng",releaseDate="2017",title="Test1"))
add(Movie(id=2,posterPath="/asdasd",vote=6.3,language="Hun",releaseDate="2013",title="Test2"))
}
val listAdapter = MovieListAdapter()
binding.itemList.adapter=listAdapter
listAdapter.submitList(lst)
}
companion object {
private const val ARG_NAME = "test_argument"
fun newInstance(testArg: String): DetailpageFragment = DetailpageFragment().apply {
arguments = Bundle().apply { putString(ARG_NAME, testArg) }
}
}
}
My adapter
class MovieListAdapter : ListAdapter<Movie, MovieViewHolder>(diffUtil) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val layoutInflater: LayoutInflater = LayoutInflater.from(parent.context)
return MovieViewHolder(MovieItemBinding.inflate(layoutInflater,parent,false))
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
val item:Movie=getItem(position)
val binding:MovieItemBinding = holder.binding
binding.movieTitle.text=item.title
binding.releaseYear.text=item.releaseDate
binding.language.text=item.language
binding.ratingtext.text=item.vote.toString()
binding.movieImage.load("https://i.postimg.cc/VLbN4hkz/the-hobbit-the-desolation-of-smaug.jpg")
}
}
private val diffUtil : DiffUtil.ItemCallback<Movie> = object : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean = oldItem == newItem
}
class MovieViewHolder(val binding: MovieItemBinding):RecyclerView.ViewHolder(binding.root)
fragment_movelist_screen.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/item_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:orientation="vertical"
android:padding="16dp">
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>
mainactivity.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/fragmentmovielist"
android:name="com.example.ubbassignment2.MovelistScreen"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="#layout/fragment_detailpage" />
</FrameLayout>

You're creating a new view and binding in onViewCreated and setting adapter to it (that view will be garbage collected) you should inflate your view in onCreateView and set the adapter to that recycler view instead of your temporary one.
Fragment:
override fun onCreateView(...): View {
val binding = FragmentMovelistScreenBinding.inflate(layoutInflater)
val lst : List<Movie> = buildList {
add(Movie(id=1,posterPath="/asdasd",vote=7.3,language="Eng",releaseDate="2017",title="Test1"))
add(Movie(id=2,posterPath="/asdasd",vote=6.3,language="Hun",releaseDate="2013",title="Test2"))
}
val listAdapter = MovieListAdapter()
binding.itemList.adapter=listAdapter
listAdapter.submitList(lst)
return binding.root
}

Related

onCreateViewHolder() of child recycler view not being called in Android [Kotlin]

I have a nested recycler view structure. The data for the recycler view is coming from Room Database and it follows MVVM architecture with repositories and viewmodels.
I am able to inflate the parent recycler view and get the list on screen but the child recycler view is not being inflated. I can see the value being passed through the adapter by using logs. But that is not being used on onBindViewHolder() and neither onCreateViewHolder() is called
Any help would be appreciated.
My CODE:
Fragment :
class ControlPanelFragment : Fragment() {
private var _binding: FragmentControlPanelBinding? = null
private val binding: FragmentControlPanelBinding get() = _binding!!
private lateinit var controlPanelViewModel: ControlPanelViewModel
//floor adapter
private lateinit var adapter: FloorsAdapter
//rooms adapter
private lateinit var roomAdapter: RoomsAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentControlPanelBinding.inflate(layoutInflater)
controlPanelViewModel = ViewModelProvider(this)[ControlPanelViewModel::class.java]
adapter = FloorsAdapter()
roomAdapter = RoomsAdapter()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.rvFloors.adapter = adapter
binding.rvFloors.layoutManager = LinearLayoutManager(requireContext())
ListItemControlPanelFloorsBinding.inflate(layoutInflater).apply {
rvRoomControlPanel.adapter = roomAdapter
rvRoomControlPanel.layoutManager = LinearLayoutManager(activity)
Timber.d("Inside list item")
}
controlPanelViewModel.getAllFloors.observe(viewLifecycleOwner, Observer {
Timber.d("List is $it")
//Remove duplicates from received list
val distinct = it.toSet().toList().sorted()
Timber.d("List after removing duplicates and sorting: $distinct")
adapter.floorList(distinct)
for (i in distinct) {
controlPanelViewModel.getAllRooms(i).observe(viewLifecycleOwner, Observer { rooms ->
Timber.d("Floor: $i, Rooms: $rooms")
val distinctRooms = rooms.toSet().toList()
roomAdapter.roomList(distinctRooms)
})
}
})
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Parent recycler view adapter
open class FloorsAdapter() : RecyclerView.Adapter<FloorsAdapter.FloorViewHolder>() {
private var floorList = emptyList<String>()
inner class FloorViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): FloorViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ListItemControlPanelFloorsBinding.inflate(layoutInflater, parent, false)
return FloorViewHolder(binding.root)
}
#SuppressLint("NotifyDataSetChanged")
override fun onBindViewHolder(holder: FloorsAdapter.FloorViewHolder, position: Int) {
val item = floorList[position]
Timber.d("Current floor is $item, Floor List is : $floorList")
ListItemControlPanelFloorsBinding.bind(holder.itemView).apply {
Timber.d("Current floor is $item, Floor List is : $floorList")
tvFloor.text = "Floor : $item"
}
}
#SuppressLint("NotifyDataSetChanged")
fun floorList(floors: List<String>) {
this.floorList = floors
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return floorList.size
}
}
Child Recycler view adapter:
class RoomsAdapter() : RecyclerView.Adapter<RoomsAdapter.RoomsViewHolder>() {
private var roomList = emptyList<String>()
inner class RoomsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RoomsViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ListItemControlPanelRoomsBinding.inflate(inflater, parent, false)
return RoomsViewHolder(binding.root)
}
override fun onBindViewHolder(holder: RoomsViewHolder, position: Int) {
Timber.d("Room List onBindViewHolder: $roomList")
val item = roomList[position]
ListItemControlPanelRoomsBinding.bind(holder.itemView).apply {
tvRoom.text = item
}
}
override fun getItemCount(): Int {
return roomList.size
}
#SuppressLint("NotifyDataSetChanged")
fun roomList(room: List<String>) {
this.roomList = room
Timber.d("Room List: $roomList")
notifyDataSetChanged()
}
}
Fragment layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context=".ui.controlPanel.ui.ControlPanelFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="10dp">
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="#+id/switchFactory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="60dp"
android:checked="false"
android:text="Factory"
android:textOff="OFF"
android:textOn="ON"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Spinner
android:id="#+id/spnChooseFloor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="200dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="#+id/switchFactory"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/switchFactory"
app:layout_constraintTop_toTopOf="#+id/switchFactory" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="10dp"
android:elevation="8dp"
app:cardCornerRadius="8dp">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvFloors"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="#layout/list_item_control_panel_floors" />
</androidx.cardview.widget.CardView>
</LinearLayout>
List Item for fragment recycler view(parent recycler view)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="200dp"
android:orientation="vertical">
<TextView
android:id="#+id/tvFloor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:text="Floor 1"
android:textColor="#color/black"
android:textSize="20sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvRoomControlPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
tools:listitem="#layout/list_item_control_panel_rooms" />
</LinearLayout>
List item for child recycler view
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="40dp">
<TextView
android:id="#+id/tvRoom"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:text="Conference Room"
android:textSize="20sp" />
</LinearLayout>
Edit 1:
My entity :
data class AddedDevicesInformation(
#ColumnInfo(name = "floor_name")
var floorName: String = "",
#ColumnInfo(name = "room_name")
var roomName: String = "",
#ColumnInfo(name = "machine_name")
var machineName: String = "",
#ColumnInfo(name = "device_name")
var deviceName: String = "",
#ColumnInfo(name = "factory_status")
var factoryStatus: Boolean? = false,
#PrimaryKey(autoGenerate = true)
var id: Int? = null,
) {
}
DAO query :
//returns all rooms associated with a floor
#Query("SELECT room_name from added_device_information where floor_name =:floor")
fun readAllRoomsOnAFloor(floor: String): LiveData<List<String>>
Repository:
class ControlPanelRepository(private val devicesInformationDao: DevicesInformationDao) {
fun getAllRooms(floor: String): LiveData<List<String>> = devicesInformationDao.readAllRoomsOnAFloor(floor)
ViewModel:
class ControlPanelViewModel(application: Application) : AndroidViewModel(application) {
//repository instance
val repository: ControlPanelRepository
//Variable for getting all floors
val getAllFloors: LiveData<List<String>>
init {
val database = PowerManagementDatabase.getDatabase(application)
val dao = database.getAddedDevicesInformationDao()
repository = ControlPanelRepository(dao)
getAllFloors = repository.getAllFloors
}
fun getAllRooms(floor: String): LiveData<List<String>> {
return repository.getAllRooms(floor)
}
}
The problem is that each "floor" (row in the floor RecyclerView) has its own unique "room" RecyclerView, but you never attach an adapter to those room RecyclerViews. Instead, you inflate an unused "floor" layout in the Fragment and attach a room adapter to it. That layout will never be displayed anywhere so its adapter is never used.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.rvFloors.adapter = adapter
binding.rvFloors.layoutManager = LinearLayoutManager(requireContext())
// The problem is this part.
// This view isn't shown anywhere, and the views inflated in FloorsAdapter
// never get a RoomAdapter attached
ListItemControlPanelFloorsBinding.inflate(layoutInflater).apply {
rvRoomControlPanel.adapter = roomAdapter
rvRoomControlPanel.layoutManager = LinearLayoutManager(activity)
Timber.d("Inside list item")
}
Each displayed floor layout is inflated in the floor adapter in onCreateViewHolder and the data for the floor then gets set in onBindViewHolder. This means you need to create a separate RoomsAdapter for each floor and attach it inside the floor adapter somewhere.
This makes accessing the room adapters more difficult with your current pattern, but if you make a Floor object that contains both its name and its list of rooms, then the floor object selected in onBindViewHolder can pass the list of rooms to its RoomAdapter. This means the FloorsAdapter would take a List<Floor> instead of List<String>. Then in the main fragment you only need to pass the list of Floor objects to the floor adapter and it will handle passing room lists on to the room adapters appropriately.
data class Floor(val name: String, val rooms: List<String>) {}
The floor adapter would look something like this:
// modify the ViewHolder to store the binding (so you don't have to
// re-bind the views so often) and store the room adapter for this
// floor. There should be one room adapter per floor.
inner class FloorViewHolder(val binding: ListItemControlPanelFloorsBinding,
val roomAdapter: RoomsAdapter)
: RecyclerView.ViewHolder(binding.root) {}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): FloorViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ListItemControlPanelFloorsBinding.inflate(layoutInflater, parent, false)
// Create one adapter per floor here
val adapter = RoomsAdapter()
binding.rvRoomControlPanel.adapter = adapter
binding.rvRoomControlPanel.layoutManager = LinearLayoutManager(activity)
return FloorViewHolder(binding, adapter)
}
override fun onBindViewHolder(holder: FloorsAdapter.FloorViewHolder, position: Int) {
// for a given floor, set the rooms on that floor's room adapter
val currentFloor = floorList[position]
holder.roomAdapter.roomList(currentFloor.rooms)
holder.binding.tvFloor.text = "Floor : ${currentFloor.name}"
}
Here's an example of how that might look in the ViewModel, when it constructs the list of Floor objects, and the activity would observe the floorList LiveData. Each Floor contains all the data (including nested data) that it needs to display.
private val floorListLiveData = MutableLiveData<List<Floor>>()
val floorList: LiveData<List<Floor>>
get() = floorListLiveData
fun load() {
viewModelScope.launch {
val floors = mutableListOf<Floor>()
val floorList = dao.readFloors()
for(floorName in floorList) {
val rooms = dao.readRoomsInFloor(floorName)
floors.add(Floor(floorName, rooms))
}
floorListLiveData.postValue(floors)
}
}

Swipe down update from Firebase in RecyclerView

I am trying to make update of my RecyclerViewer with data from Storage for profile picture and Firestore for other data. But I tried many methods and I don't know how to make this.
The goal is to make a RecyclerViewer update each time user goes to fragment and also be able to swipe to refresh the list. Also the layout gets inflated after second 'visit' on the Fragment, the first time it does not get inflated. Is this a problem with a Glide?
And the cache keeps the data in RecyclerViewer even when new user is logged in with completely different data in Firebase.
fragment_item_list.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/swiperefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<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:id="#+id/itemContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FriendFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/list"
android:name="com.msmmhm.hauimetyourmother.FriendFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
app:layoutManager="LinearLayoutManager"
tools:context=".FriendFragment"
tools:listitem="#layout/fragment_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
FriendFragment.kt
imports ...
class FriendFragment : Fragment() {
private lateinit var binding: FragmentItemListBinding
private lateinit var mSwipeRefreshLayout: SwipeRefreshLayout
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentItemListBinding.inflate(inflater, container, false)
with(binding.list) {
layoutManager = LinearLayoutManager(context)
adapter = MyFriendRecyclerViewAdapter(Friend.ITEMS,container!!.context)
return binding.root
}
}
}
MyFriendRecyclerViewAdapter.kt
imports ...
class MyFriendRecyclerViewAdapter(
private val values: List<FriendItem>,
val context: Context
) : RecyclerView.Adapter<MyFriendRecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
FragmentItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = values[position]
var events: CollectionReference = FirebaseFirestore.getInstance().collection("events")
val docRef = events.document(item.id)
Log.i(ContentValues.TAG,"EVENTS ITEM ID: "+item.id)
docRef.get()
.addOnSuccessListener { document ->
if (document.exists())
{
Log.i(ContentValues.TAG,"DOCUMENT EXISTS")
val map = document.data
if (map!!.size != 0)
{
holder.activeIcon.setColorFilter(R.drawable.presence_online)
}
else
{
holder.activeIcon.setImageResource(R.drawable.presence_invisible)
}
}
else
{
holder.activeIcon.setImageResource(R.drawable.presence_invisible)
}
}
holder.friendNickname.text = item.friendName
loadWithGlide2(item.id,holder)
}
fun loadWithGlide2(id: String, holder: ViewHolder) {
val storageReference = Firebase.storage.reference.child("ProfilePictures/${id}.jpg")
Log.d(ContentValues.TAG, "NAME IS: ${storageReference.name}")
Glide.with(context)
.load(storageReference)
.error(R.drawable.defaultprofilepicture)
.into(holder.friendImg)
}
override fun getItemCount(): Int = values.size
inner class ViewHolder(binding: FragmentItemBinding) : RecyclerView.ViewHolder(binding.root) {
val friendImg: ImageView = binding.friendPicture
var friendNickname: TextView = binding.friendNickname
var activeIcon: ImageView = binding.activeIcon
val itemContainer: View = binding.root
override fun toString(): String {
return super.toString() + " '" + friendNickname.text + "'"
}
}
}

Flow is not working with Binding Adapters

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)
}
}

Why my pagedList is not updated via LiveData method .observe?

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

ImageView doesn't load images in the RecyclerView after binding the Url

I am a newbie with android databinding, but the ImageView doesn't bind in the RecyclerView. I have read several blogs but no luck. What am I missing?
Below are some of the blog posts I have read:
link 1
link2
Below is how I have styled my xml layout.
<?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">
<data>
<variable
name="movie"
type="com.movieapp.huxymovies.model.Result" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:background="#color/bg"
android:orientation="vertical">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="0dp"
android:layout_marginTop="8dp"
android:background="#color/bg"
android:orientation="horizontal">
<ImageView
android:id="#+id/img"
android:layout_width="70dp"
android:layout_height="100dp"
android:layout_marginLeft="8dp"
app:movieImage="#{movie.MPosterPath}" />
</LinearLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
</LinearLayout>
</layout>
Then this is the modal class which contains all the attributes:
#Entity(tableName = "Results")
class Result {
companion object {
#JvmStatic
#BindingAdapter("movieImage")
fun LoadImage(view: View, mPosterPath: String?) {
val imageView = view as ImageView
Glide.with(view.context)
.load(Utils.IMAGE_BASE_URL + mPosterPath)
.into(imageView)
}
#BindingAdapter("rating")
fun setRating(ratingBar: RatingBar, rating: Float) {
if (rating != null) {
ratingBar.rating = rating
}
}
}
constructor(mId: Long?, mOverview: String?, mPosterPath: String?, mTitle: String?, mVoteAverage: Double?) {
this.mId = mId
this.mOverview = mOverview
this.mPosterPath = mPosterPath
this.mTitle = mTitle
this.mVoteAverage = mVoteAverage
}
constructor()
#PrimaryKey
#SerializedName("id")
var mId: Long? = null
#SerializedName("overview")
var mOverview: String? = null
#SerializedName("poster_path")
var mPosterPath: String? = null
#SerializedName("title")
var mTitle: String? = null
#SerializedName("vote_average")
var mVoteAverage: Double? = null
}
Then finally, in my adapter class, I tried to bind the item layout.
class ResultAdapter(private val context: Context) : PagedListAdapter<Result, ResultAdapter.ResultViewHolder>(DIFF_CALLBACK) {
public lateinit var mBinding: ItemActivitymainBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ResultViewHolder {
mBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.item_activitymain, parent, false)
return ResultViewHolder(mBinding)
}
override fun onBindViewHolder(holder: ResultViewHolder, position: Int) {
val result = getItem(position)
if (result != null) {
holder.itemActivitymainBinding.titleTxt.text = result.mTitle
}
}
class ResultViewHolder(itemView: ItemActivitymainBinding) : RecyclerView.ViewHolder(itemView.root) {
var itemActivitymainBinding: ItemActivitymainBinding
var root: View
init {
root = itemView.root
itemActivitymainBinding = itemView
}
}
companion object {
const val MOVIE_ID = "MOVIE_ID"
const val MOVIE_NAME = "MOVIE_NAME"
const val MOVIE_OVERVIEW = "MOVIE_OVERVIEW"
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Result>() {
override fun areItemsTheSame(oldItem: Result, newItem: Result): Boolean {
return oldItem.mId === newItem.mId
}
override fun areContentsTheSame(oldItem: Result, newItem: Result): Boolean {
return oldItem == newItem
}
}
}
}
Now I am still wondering why the image doesn't display because I have read some of the blog posts about this and I followed all their procedures.
First, you binding is missing its lifecycle owner (i.e., the activity or fragment in which you use the adapter). You should pass it to your adapter and then set it:
class ResultAdapter(private val lifecycleOwner: LifecycleOwner)
: PagedListAdapter<Result, ResultAdapter.ResultViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ResultViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = DataBindingUtil.inflate<ItemActivitymainBinding>(inflater, R.layout.item_activitymain, parent, false)
// We set the lifecycle owner here
binding.setLifecycleOwner(lifecycleOwner)
return ResultViewHolder(binding)
}
...
}
// In your activity/fragment, pass the view as a parameter when creating the adapter
adapter = ResultAdapter(this)
(In the adapter, I have removed the property mBinding and the constructor parameter context, as neither of them were necessary.)
Second, you are defining the property movie in your layout, but you are not setting it with an actual value. To fix this, you have to update your implementation of onBindViewHolder():
override fun onBindViewHolder(holder: ResultViewHolder, position: Int) {
val movie = getItem(position)
// Here we set the layout variable "movie" with its corresponding value
holder.itemActivitymainBinding.movie = movie
}
(Please note that here I have removed the code you had written to change the title of your textview because you should change it through data-binding in the layout by doing this: android:text="#{movie.mTitle}".)
With these changes, your implementation should hopefully work!

Categories

Resources