Swipe down update from Firebase in RecyclerView - android

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 + "'"
}
}
}

Related

Problems with SearchView (items appearing and dissapearing when a button is clicked)

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.

Kotlin-Attempt to invoke virtual method 'void androidx.recyclerview.widget.RecyclerView.setAdapter(androidx.recyclerview.widget.RecyclerView$Adapter)'

I Am trying to run the application but it crashes when i try to access the content of a bottom navigation bar which has a fragment in it and the fragement cointains a recyclerView.The adpater is null here is the error
java.lang.NullPointerException: Attempt to invoke virtual method 'void androidx.recyclerview.widget.RecyclerView.setAdapter(androidx.recyclerview.widget.RecyclerView$Adapter)' on a null object reference
at com.example.accers.ChatFragment.recyclerView(ChatFragment.kt:67)
at com.example.accers.ChatFragment.onCreateView(ChatFragment.kt:41)
Fragment 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"
tools:context=".ChatFragment">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="#+id/ll_layout_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#E4E4E4"
android:orientation="horizontal">
<EditText
android:id="#+id/et_message"
android:inputType="textShortMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_weight=".5"
android:background="#drawable/round_button"
android:backgroundTint="#android:color/white"
android:hint="Type a message..."
android:padding="10dp"
android:singleLine="true" />
<Button
android:id="#+id/btn_send"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:layout_weight="1"
android:background="#drawable/round_button"
android:backgroundTint="#26A69A"
android:text="Send"
android:textColor="#android:color/white" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_messages"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="#id/ll_layout_bar"
tools:itemCount="20"
tools:listitem="#layout/message_item" />
<!-- <View-->
android:layout_below="#+id/dark_divider"
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="10dp"-->
<!-- android:background="#42A5F5"-->
<!-- android:id="#+id/dark_divider"/>-->
</RelativeLayout>
</FrameLayout>
Fragment Class
class ChatFragment : Fragment() {
private val TAG = "ChatFragment"
var messagesList = mutableListOf<Message>()
private lateinit var adapter: MessagingAdapter
private val botList = listOf("Cassandra", "Francesca", "Luigi", "Nico","Lesley","Hiyle","Roselind")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
var view = inflater.inflate(R.layout.fragment_chat, container, false)
// var button: Button = view.findViewById(R.id.btn_send)
recyclerView()
clickEvents()
val random = (0..3).random()
customBotMessage("Hello! Today you're speaking with ${botList[random]}, how may I help?")
return view
}
private fun clickEvents() {
//Send a message
btn_send.setOnClickListener {
sendMessage()
}
// Scroll back to correct position when user clicks on text view
et_message.setOnClickListener {
GlobalScope.launch {
delay(100)
withContext(Dispatchers.Main) {
rv_messages.scrollToPosition(adapter.itemCount - 1)
}
}
}
}
private fun recyclerView() {
adapter = MessagingAdapter()
rv_messages.adapter = adapter
rv_messages.layoutManager = LinearLayoutManager(activity)
}
override fun onStart() {
super.onStart()
//In case there are messages, scroll to bottom when re-opening app
GlobalScope.launch {
delay(100)
withContext(Dispatchers.Main) {
rv_messages.scrollToPosition(adapter.itemCount - 1)
}
}
}
private fun sendMessage() {
val message = et_message.text.toString()
val timeStamp = Time.timeStamp()
if (message.isNotEmpty()) {
//Adds it to our local list
messagesList.add(Message(message, SEND_ID, timeStamp))
et_message.setText("")
adapter.insertMessage(Message(message, SEND_ID, timeStamp))
rv_messages.scrollToPosition(adapter.itemCount - 1)
botResponse(message)
}
}
private fun botResponse(message: String) {
val timeStamp = Time.timeStamp()
GlobalScope.launch {
//Fake response delay
delay(1000)
withContext(Dispatchers.Main) {
//Gets the response
val response = BotResponse.basicResponses(message)
//Adds it to our local list
messagesList.add(Message(response, RECEIVE_ID, timeStamp))
//Inserts our message into the adapter
adapter.insertMessage(Message(response, RECEIVE_ID, timeStamp))
//Scrolls us to the position of the latest message
rv_messages.scrollToPosition(adapter.itemCount - 1)
//Starts Google
when (response) {
OPEN_GOOGLE -> {
val site = Intent(Intent.ACTION_VIEW)
site.data = Uri.parse("https://www.google.com/")
startActivity(site)
}
OPEN_SEARCH -> {
val site = Intent(Intent.ACTION_VIEW)
val searchTerm: String? = message.substringAfterLast("search")
site.data = Uri.parse("https://www.google.com/search?&q=$searchTerm")
startActivity(site)
}
}
}
}
}
private fun customBotMessage(message: String) {
GlobalScope.launch {
delay(1000)
withContext(Dispatchers.Main) {
val timeStamp = Time.timeStamp()
messagesList.add(Message(message, RECEIVE_ID, timeStamp))
adapter.insertMessage(Message(message, RECEIVE_ID, timeStamp))
rv_messages.scrollToPosition(adapter.itemCount - 1)
}
}
}
}
My Adapter class
class MessagingAdapter: RecyclerView.Adapter<MessagingAdapter.MessageViewHolder>() {
var messagesList = mutableListOf<Message>()
inner class MessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
init {
itemView.setOnClickListener {
//Remove message on the item clicked
messagesList.removeAt(adapterPosition)
notifyItemRemoved(adapterPosition)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
return MessageViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.message_item, parent, false)
)
}
override fun getItemCount(): Int {
return messagesList.size
}
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
val currentMessage = messagesList[position]
when (currentMessage.id) {
SEND_ID -> {
holder.itemView.tv_message.apply {
text = currentMessage.message
visibility = View.VISIBLE
}
holder.itemView.tv_bot_message.visibility = View.GONE
}
RECEIVE_ID -> {
holder.itemView.tv_bot_message.apply {
text = currentMessage.message
visibility = View.VISIBLE
}
holder.itemView.tv_message.visibility = View.GONE
}
}
}
fun insertMessage(message: Message) {
this.messagesList.add(message)
notifyItemInserted(messagesList.size)
}
}
MainActivity Class
Main Activity class has the bottom nav bar to replace the fragments
val navHostFragment = supportFragmentManager.findFragmentById(R.id.mainContainer) as NavHostFragment
navController = navHostFragment.navController
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottomNavigationView)
setupWithNavController(bottomNavigationView, navController)
I Have tried simillar solutions but i can't still figure how to apply simillar asked question and errors. Thank you
You are using kotlin synthetics. Internally it will work as getView().findViewById(R.id.rv_messages)
Since in oncreateView, you are trying to access view before even view is attached in the fragment layout tree.
getView() will always return null.
Several things you can do. You can pass view to the recycler view function and access like view.rv_messages.
It's better to handle like the below.
Else you can move all view related to onViewCreated(). In onCreateView() you will just inflate and return the view. So in onViewCreated() when it calls getView() , since view is already added in onCreateView it will return the correct view object.
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
var view = inflater.inflate(R.layout.fragment_chat, container, false)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView()
clickEvents()
val random = (0..3).random()
customBotMessage("Hello! Today you're speaking with ${botList[random]}, how may I help?")
}
Also synthetics have been deprecated, and currently, it is not recommended. will strongly recommend you to use view binding for binding the views. As ,earlier I had a weird issue with synthetics which I have covered here.
Refer here for more details about deprecation of kotlin synthetics.

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

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

recycler must not be null in kotlin

I have recycler where I show list of user previous orders but for new users this list is empty therefore I get following error, recycler must not be null I've tried to replace recycler with static image for this case scenario but my app crashes.
I have 2 guesses why this crash happening but how to fix it, I'm not sure
I've placed my visibility code in wrong place
I didn't get my recycler id correctly
Code
Fragment XML
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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=".OrdersFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/ordersList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="120dp" />
<TextView
android:id="#+id/order"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="#string/orders" />
<Button
android:id="#+id/back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|top"
android:layout_marginTop="25dp"
android:background="#3F51B5"
android:padding="10dp"
android:text="#string/incomingOrder"
android:textColor="#CDDC39" />
<!-- Replacement image incase of empty list -->
<ImageView
android:id="#+id/empty"
android:layout_width="250dp"
android:layout_height="250dp"
android:layout_gravity="center"
android:contentDescription="#string/emptyOrders"
android:src="#drawable/empt" />
</FrameLayout>
Fragment
class OrdersFragment : Fragment(), View.OnClickListener {
var navController: NavController? = null
private lateinit var emptyImage: ImageView
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val root = inflater.inflate(R.layout.fragment_orders, container, false)
callOrdersAPI()
emptyImage = root.findViewById(R.id.empty) as ImageView
return root
}
fun callOrdersAPI() {
var session = SessionManager(context)
session.checkLogin()
var user = session.getUserDetails()
var uId: String? = user.get(SessionManager.KEY_ID)
var token: String? = user.get(SessionManager.KEY_ACCESS_TOKEN)
val tokenFull = "Bearer $token"
val queue = Volley.newRequestQueue(context)
val url = "https://example.com/api/orders"
val stringReq : StringRequest =
object : StringRequest(Method.GET, url,
Response.Listener { response ->
// response
val list: ArrayList<Data> = ArrayList()
getOrders(response, list)
recycler.layoutManager = LinearLayoutManager(context)
// trying to set visibility
if (list.size == 0) {
emptyImage.setVisibility(View.VISIBLE)
recycler.setVisibility(View.GONE)
} else {
emptyImage.setVisibility(View.GONE)
recycler.setVisibility(View.VISIBLE)
recycler.adapter = OrdersAdapter(list)
}
},
Response.ErrorListener { error ->
Toast.makeText(context, error.message, Toast.LENGTH_SHORT)
.show()
}
){
override fun getHeaders(): Map<String, String> {
val headers = HashMap<String, String>()
headers["Content-Type"] = "application/json"
headers["Authorization"] = tokenFull
return headers
}
}
queue.add(stringReq)
}
fun getOrders(response: String, list: ArrayList<Data>) {
var jsonObject = JSONObject(response)
val jsonArray = jsonObject.getJSONArray("data")
for (i in 0 until jsonArray.length()) {
val jsonObject1 = jsonArray.getJSONObject(i)
var listingObject = Data(
jsonObject1.getInt("accepted"),
jsonObject1.getString("amount"),
jsonObject1.getString("created_at"),
jsonObject1.getString("id"),
jsonObject1.getString("payment"),
jsonObject1.getString("payment_id"),
jsonObject1.getString("payment_method"),
jsonObject1.getString("total"),
jsonObject1.getString("transport"),
jsonObject1.getString("weight"),
jsonObject1.get("laundry") as Laundry,
jsonObject1.get("customer") as Customer,
jsonObject1.get("driver") as Driver,
jsonObject1.get("progresses") as Progresses,
jsonObject1.get("services") as Servic
)
list.add(listingObject)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = Navigation.findNavController(view)
view.findViewById<Button>(R.id.back).setOnClickListener(this)
}
override fun onClick(v: View?) {
when(v!!.id) {
R.id.back -> navController!!.navigate(R.id.action_ordersFragment_to_incomingOrderFragment)
}
}
}
Adapter
class OrdersAdapter(private var orderList: ArrayList<Data>) : RecyclerView.Adapter<OrdersAdapter.OrderViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OrderViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.orders_item, parent, false)
return OrderViewHolder(itemView)
}
override fun onBindViewHolder(holder: OrderViewHolder, position: Int) {
val currentItem = orderList[position]
holder.textView.text = currentItem.amount
// todo...
}
override fun getItemCount() = orderList.size
class OrderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.amount
// todo...
}
}
Update
As requested my Logcat
1612282056.833 15860-15860/com.my.app E/RecyclerView: No adapter attached; skipping layout
1612282056.926 15860-15860/com.my.app E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.my.app, PID: 15860
java.lang.NullPointerException: recycler must not be null
at com.my.app.OrdersFragment$callOrdersAPI$stringReq$2.onResponse(OrdersFragment.kt:59) <-- //recycler.layoutManager = LinearLayoutManager(context)
at com.my.app.OrdersFragment$callOrdersAPI$stringReq$2.onResponse(OrdersFragment.kt:53) <-- //object : StringRequest(Method.GET, url,
at com.android.volley.toolbox.StringRequest.deliverResponse(StringRequest.java:82)
at com.android.volley.toolbox.StringRequest.deliverResponse(StringRequest.java:29)
at com.android.volley.ExecutorDelivery$ResponseDeliveryRunnable.run(ExecutorDelivery.java:102)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:224)
at android.app.ActivityThread.main(ActivityThread.java:7561)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:995)
add your build app build.gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
}
then your fragment add ordersList( your RecyclerView id). after that it will ask imports layout include it.
if (list.size == 0) {
emptyImage.setVisibility(View.VISIBLE)
ordersList.setVisibility(View.GONE)
} else {
emptyImage.setVisibility(View.GONE)
recycler.setVisibility(View.VISIBLE)
ordersList.adapter = OrdersAdapter(list)
}
it will work

Categories

Resources