Im trying to change value of true and false to the opposite, so if the value in the database is true then it needs to be changed to false and if false then to true.
It works the first time i press the button but if i press the button again then it keeps changing nonstop until the app crashes.
Here is the code i use to change the value:
package com.example.myapplication.adapters
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.recyclerview.widget.RecyclerView
import com.example.myapplication.Model.CategoryModel
import com.example.myapplication.R
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.database.ValueEventListener
class Category(private val listCategory: MutableList<CategoryModel>) :
RecyclerView.Adapter<Category.ViewHolder>() {
val mAuth = FirebaseAuth.getInstance()
val database = FirebaseDatabase.getInstance()
var currentUid = mAuth.currentUser?.uid
val myRef = database.getReference("User-following").child(currentUid!!)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val v = inflater.inflate(R.layout.category_layout, parent, false)
return ViewHolder(v)
}
private fun fetchNotificationStatus(category: String, b: Boolean) {
var status = if (b) {
"true"
} else {
"false"
}
myRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
for (childSnapshot in snapshot.children) {
val key = childSnapshot.key
if (childSnapshot.child("category").value == category) {
myRef.child(key!!).child("notifications").setValue(status)
return
}
}
}
override fun onCancelled(error: DatabaseError) {
Log.e("Database Error", error.toString())
}
})
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var category: TextView = itemView.findViewById(R.id.category)
var remove: Button = itemView.findViewById(R.id.remove)
val alert: CheckBox = itemView.findViewById(R.id.alert)
init {
alert.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
val cat = category.text
if (alert.isChecked) {
Snackbar.make(itemView,
"${category.text} has been added to your notifications list",
Snackbar.LENGTH_LONG)
.show()
fetchNotificationStatus(cat as String, true)
} else {
Snackbar.make(itemView,
"${category.text} has been removed from your notifications list",
Snackbar.LENGTH_LONG)
.show()
fetchNotificationStatus(cat as String, false)
}
}
}
}
So when you press the check box the first time it changes either from
false -> True or from true -> false
but if you press the check box again it keeps changing
(false -> true -> false -> true -> false ...)
and doesn't stop changing until you close the app or until you press another button which causes the app to crash
How do i fix this so that it only changes to the opposite everytime the checkbox is pressed?
The simple fix is to use addListenerForSingleValueEvent instead of addValueEventListener in fetchNotificationStatus. Using addListenerForSingleValueEvent ensures that the listener only gets one value and then stops listening. Your current addValueEventListener keeps listening to the data, so that it gets retriggered by your own write, which then writes it again, and again, and again...
In addition, I recommend using a query to find the node(s) that you want to modify:
private fun fetchNotificationStatus(category: String, b: Boolean) {
var status = if (b) {
"true"
} else {
"false"
}
Query query = myRef.orderByChild("category").equalTo(category);
query.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
for (childSnapshot in snapshot.children) {
childSnapshot.getRef().child("notifications").setValue(status)
// might be able to do .ref. instead of .getRef().
}
}
override fun onCancelled(error: DatabaseError) {
Log.e("Database Error", error.toString())
}
})
}
Using a query like this reduces the amount of data that your app reads and this reduces the bandwidth usage/cost both on the client and on the server.
Related
For an app I am making I have a list in which I display pixel art creations, I do this with a RecyclerView and DiffUtil, here is the code:
package com.therealbluepandabear.pixapencil.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.therealbluepandabear.pixapencil.R
import com.therealbluepandabear.pixapencil.databinding.RecentCreationsLayoutBinding
import com.therealbluepandabear.pixapencil.enums.SnackbarDuration
import com.therealbluepandabear.pixapencil.extensions.setOnLongPressListener
import com.therealbluepandabear.pixapencil.extensions.showSnackbar
import com.therealbluepandabear.pixapencil.listeners.RecentCreationsListener
import com.therealbluepandabear.pixapencil.models.PixelArt
import com.therealbluepandabear.pixapencil.viewholders.PixelArtViewHolder
class PixelArtAdapter(
private val snackbarView: View,
private val listener: RecentCreationsListener,
private val context: Context
) : ListAdapter<PixelArt, RecyclerView.ViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val binding = RecentCreationsLayoutBinding.inflate(LayoutInflater.from(parent.context))
return PixelArtViewHolder(binding, context)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val pixelArt = getItem(position)
if (holder is PixelArtViewHolder) {
holder.bind(pixelArt)
holder.binding.recentCreationsLayoutMaterialCardView.setOnClickListener {
listener.onCreationTapped(pixelArt)
}
holder.binding.recentCreationsLayoutMaterialCardView.setOnLongPressListener {
listener.onCreationLongTapped(pixelArt)
}
holder.binding.recentCreationsLayoutFavoriteButton.setOnClickListener {
if (pixelArt.starred) {
pixelArt.starred = false
listener.onUnstarredTapped(pixelArt)
unFavouriteRecentCreation(snackbarView, pixelArt)
holder.bind(pixelArt)
} else {
pixelArt.starred = true
listener.onStarredTapped(pixelArt)
favouriteRecentCreation(snackbarView, pixelArt)
holder.bind(pixelArt)
}
}
}
}
private fun favouriteRecentCreation(contextView: View, pixelArt: PixelArt) { // move to listener
contextView.showSnackbar(contextView.context.getString(R.string.snackbar_pixel_art_project_saved_to_starred_items_in_code_str, pixelArt.title), SnackbarDuration.Default)
pixelArt.starred = true
}
private fun unFavouriteRecentCreation(contextView: View, pixelArt: PixelArt) {
contextView.showSnackbar(contextView.context.getString(R.string.snackbar_pixel_art_project_removed_from_starred_items_in_code_str, pixelArt.title), SnackbarDuration.Default)
pixelArt.starred = false
}
companion object {
val diffCallback: DiffUtil.ItemCallback<PixelArt> = object : DiffUtil.ItemCallback<PixelArt>() {
override fun areItemsTheSame(oldItem: PixelArt, newItem: PixelArt): Boolean {
return oldItem.objId == newItem.objId
}
override fun areContentsTheSame(oldItem: PixelArt, newItem: PixelArt): Boolean {
return oldItem == newItem
}
}
}
}
ViewHolder:
class PixelArtViewHolder(val binding: RecentCreationsLayoutBinding, private val context: Context) : RecyclerView.ViewHolder(binding.root) {
private fun loadPixelArtCoverImage(pixelArt: PixelArt) {
val widthHeight = if (context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
350
} else {
750
}
val requestOptions: RequestOptions = RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.dontAnimate()
.priority(Priority.IMMEDIATE)
.encodeFormat(Bitmap.CompressFormat.PNG)
.override(widthHeight, widthHeight)
.centerInside()
.format(DecodeFormat.DEFAULT)
Glide.with(itemView.context)
.setDefaultRequestOptions(requestOptions)
.load(File(itemView.context.getFileStreamPath(pixelArt.coverBitmapFilePath).absolutePath))
.transition(DrawableTransitionOptions.withCrossFade())
.placeholder(R.drawable.transparent_placeholder)
.into(binding.recentCreationsLayoutImageView)
}
private fun loadPixelArtTitle(pixelArt: PixelArt) {
if (pixelArt.title.length > 6) {
binding.recentCreationsLayoutTitle.ellipsize = TextUtils.TruncateAt.MARQUEE
binding.recentCreationsLayoutTitle.isSelected = true
binding.recentCreationsLayoutTitle.isSingleLine = true
(pixelArt.title + " ".repeat(10)).repeat(200).also { binding.recentCreationsLayoutTitle.text = it }
} else {
binding.recentCreationsLayoutTitle.text = pixelArt.title
}
}
private fun loadPixelArtStarred(pixelArt: PixelArt) {
binding.recentCreationsLayoutFavoriteButton.setImageResource(
if (pixelArt.starred) {
R.drawable.ic_baseline_star_24
} else {
R.drawable.ic_baseline_star_border_24
}
)
}
fun bind(pixelArt: PixelArt){
loadPixelArtCoverImage(pixelArt)
binding.recentCreationsLayoutSubtitle.text = context.getString(R.string.recentCreationsLayoutSubtitle_str, pixelArt.width, pixelArt.height)
loadPixelArtStarred(pixelArt)
loadPixelArtTitle(pixelArt)
}
}
Here is the result:
When the user long taps on a project, they get the following dialog:
When they press 'Rename', they get the following dialog where they can rename the project:
My issue is, that when the user types in a new name, and then presses OK, the data is not updating. Sometimes it takes twice to update, sometimes I need to restart the app for it to update, and sometimes it doesn't update at all.
Here is the code responsible for renaming:
fun MainActivity.extendedOnRenameTapped(pixelArt: PixelArt, bottomSheetDialog: BottomSheetDialog) {
val inflatedActivity = activity()?.layoutInflater?.inflate(R.layout.save_file_under_new_name_alert, activity()?.findViewById(android.R.id.content),false)
val textInput: TextInputLayout = inflatedActivity as TextInputLayout
showDialog(
getString(R.string.dialog_rename_title_in_code_str),
null,
getString(R.string.generic_ok_in_code_str), { _, _ ->
val input: String = textInput.editText?.text.toString()
if (input.isNotBlank()) {
pixelArt.title = input
pixelArtViewModel.update(pixelArt)
adapter.submitList(pixelArtData)
bottomSheetDialog.dismiss()
}
}, getString(R.string.generic_cancel_in_code_str), null, view = textInput, dimBackground = false
)
}
I am following everything by the book, so I am confused why this is not working.
Edit
I tried to make it all 'val' and then add this:
pixelArtViewModel.update(pixelArt.copy(title = input))
pixelArtViewModel.getAll().observe(this) {
adapter.submitList(it)
}
bottomSheetDialog.dismiss()
Still not working.
I see that you are setting pixelArt.title, which means your PixelArt class is mutable (has var properties or val properties that reference mutable classes). DiffUtil is 100% incompatible with mutable classes, because they make it impossible to compare items in the old and new lists. It will see the old list as having the new value already so it will treat it as unchanged.
Example with my imagined version of your PixelArt class.
data class PixelArt(
val objId: Long,
val name: String,
val starred: Boolean,
val imageFilePath: String
)
// In ViewModel:
// You probably have the list backed up to disk somehow. I'm just using
// placeholder functions to represent working with the repo or files or
// whatever you use.
val pixelArtLiveData = MutableLiveData<List<PixelArt>>().also {
viewModelScope.launch { it.value = readThePersistedData() }
}
private fun modifyItem(oldItem: PixelArt, newItem: PixelArt) {
pixelArtLiveData.value = pixelArtLiveData.value.orEmpty()
.map { if (it == oldItem) newItem else it }
// also update your persisted data here
}
fun renameItem(originalItem: PixelArt, newName: String) {
modifyItem(originalItem, originalItem.copy(name = newName))
}
fun toggleItemStarred(originalItem: PixelArt) {
modifyItem(originalItem, originalItem.copy(starred = !originalItem.starred))
}
// etc. or you could just make modifyItem public instead of making
// all these helper functions
Then in your adapter, you must call through to these ViewModel functions instead of directly modifying the items or the list or calling submitList. Since the adapter doesn't have direct access to the ViewModel, you probably use your RecentCreationsListener for this by adding
appropriate actions to it that your various click listeners can call.
Your Activity or Fragment would observe this LiveData and simply call submitList() with the observed value.
I have a Todo Application and I want to hide (which basically means not showing)the tasks based on its completed status(strikeThrough over the text). However, the hideCompleted tasks implementation I followed isn't working but the sort and search is working and I said this because I put all the Implementations in a single query and made them work together with stateFlow but the hide isn't working. Here is my code.
Okay What I mean by isn't working is that it unchecks the checkBoxes besides the Tasks instead of hiding them.
First My Model class
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.*
/** Our Model class. This class will represent our database table **/
#Entity(tableName = "todo_table")
data class Todo(
#PrimaryKey (autoGenerate = true) // here "Room" will autoGenerate the id for us
instead of assigning a randomUUID value
val id : Int = 0,
var title : String = "",
var date : Date = Date(),
var time : Date = Date(),
var todoCheckBox : Boolean = false
)
Then my Dao. Only the two sort(By date and by Name) functions are directly accessed from the Dao. The others are through the repository.
import androidx.room.*
import com.bignerdranch.android.to_dolist.model.Todo
import kotlinx.coroutines.flow.Flow
/**
* This will be our DAO file where we will be update, delete and add Todos to our
database so it contains the methods used for accessing the database
*/
#Dao
interface TodoDao {
// function to hold all out queries and will be executed based on our sortOrder
fun getAllTasks(query : String, sortOrder: SortOrder, hideCompleted: Boolean) : Flow<List<Todo>> =
when(sortOrder) {
SortOrder.BY_DATE -> getTasksSortedByDateCreated(query, hideCompleted)
SortOrder.BY_NAME -> getTasksSortedByName(query, hideCompleted)
}
#Query("SELECT * FROM todo_table WHERE (todoCheckBox != :hideCompleted OR todoCheckBox = 0) AND title LIKE '%' || :searchQueryText || '%' ORDER BY title COLLATE NOCASE")
fun getTasksSortedByName(searchQueryText : String, hideCompleted : Boolean): Flow<List<Todo>>
#Query("SELECT * FROM todo_table WHERE (todoCheckBox != :hideCompleted OR todoCheckBox = 0) AND title LIKE '%' || :searchQueryText || '%' ORDER BY time ASC")
fun getTasksSortedByDateCreated(searchQueryText : String, hideCompleted : Boolean): Flow<List<Todo>>
// onConflict will ignore any known conflicts, in this case will remove duplicate "Todos" with the same name
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun addTodo(todo: Todo)
#Query("DELETE FROM todo_table WHERE id IN (:idList)")
suspend fun deleteSelectedTasks(idList : Long)
#Query("DELETE FROM todo_table")
suspend fun deleteAllTasks()
}
My ViewModel(Where I call the sort functions directly from the Dao)
import android.app.Application
import androidx.lifecycle.*
import com.bignerdranch.android.to_dolist.model.Todo
import com.bignerdranch.android.to_dolist.repository.TodoRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
/** Our AndroidViewModel. This AndroidViewModel holds reference to our Application context. **/
class TodoViewModel(application: Application) : AndroidViewModel(application) {
/**
* NOTE! : "Context" are needed to instantiate a database that is why we are using
an AndroidViewModel in this case because it holds reference to an
* Application context. And if I remember correctly, it will start as the "Application" starts.
**/
private val repository : TodoRepository
private val userDao = TodoDatabase.getDatabase(application).todoDao()
init {
// having access to our TodoDao from our database
val userDao = TodoDatabase.getDatabase(application).todoDao()
repository = TodoRepository(userDao)
}
val searchQuery = MutableStateFlow("")
val sortOrder = MutableStateFlow(SortOrder.BY_DATE) // adding BY_DATE to make the
lists sorted by date as default
val hideCompleted = MutableStateFlow(false)
/**
* The combine function here is a an object in the flow library that is used too
combine the most recent values of a flow, so if one value changes it will
* automatically return the latest values of the other flows. This is done so that the three flows will work in harmony.
*/
private val tasksFlow = combine(
searchQuery,
sortOrder,
hideCompleted
) { query, sortOrder, hideCompleted -> // LAMBDA
Triple(query, sortOrder, hideCompleted)
// flatMapLatest gets triggered when any of this flows changes and then passes it to the query to be executed.
}.flatMapLatest { (query, sortOrder, hideCompleted) ->
userDao.getAllTasks(query, sortOrder, hideCompleted)
}
val tasks = tasksFlow.asLiveData()
// All functions using coroutines objects indicates that whatever is in it should run in a background thread
fun addTodo(todo : Todo) {
viewModelScope.launch(Dispatchers.IO) {
repository.addTodo(todo)
}
}
fun deleteSelectedTasks(idList: Long) {
viewModelScope.launch(Dispatchers.IO) {
repository.delSelectedTasks(idList)
}
}
fun deleteAllTasks() {
viewModelScope.launch(Dispatchers.IO) {
repository.delAllTasks()
}
}
}
enum class SortOrder { BY_DATE, BY_NAME }
Then my Fragment
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bignerdranch.android.to_dolist.databinding.FragmentListBinding
import com.bignerdranch.android.to_dolist.R
import com.bignerdranch.android.to_dolist.data.SortOrder
import com.bignerdranch.android.to_dolist.data.TodoViewModel
import com.bignerdranch.android.to_dolist.model.Todo
import com.bignerdranch.android.to_dolist.utils.onQueryTextChanged
private const val TAG = "ListFragment"
class ListFragment : Fragment() {
private var _binding : FragmentListBinding? = null
private val binding get() = _binding!!
lateinit var mTodoViewModel: TodoViewModel
private lateinit var recyclerView: RecyclerView
private val adapter = ListAdapter() // getting reference to our ListAdapter
private var todosList = emptyList<Todo>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment with ViewBinding style
_binding = FragmentListBinding.inflate(inflater, container, false)
// this tells our activity/fragment that we have a menu_item it should respond to it.
setHasOptionsMenu(true)
recyclerView = binding.recyclerViewTodo
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(requireContext())
/**
* updates our recyclerView with the new "observed" changes in our database through our adapter
*/
// TodoViewModel
mTodoViewModel = ViewModelProvider(this)[TodoViewModel::class.java]
mTodoViewModel.tasks.observe(viewLifecycleOwner) { todos ->
adapter.setData(todos)
todosList = todos
}
// Add Task Button
binding.fbAdd.setOnClickListener {
findNavController().navigate(R.id.action_listFragment_to_addFragment)
}
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_list, menu)
val search = menu.findItem(R.id.todo_search)
val searchView = search.actionView as SearchView
searchView.onQueryTextChanged { querySearch ->
mTodoViewModel.searchQuery.value = querySearch
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when(item.itemId) {
R.id.sort_by_name -> {
mTodoViewModel.sortOrder.value = SortOrder.BY_NAME
true
}
R.id.sort_by_date -> {
mTodoViewModel.sortOrder.value = SortOrder.BY_DATE
true
}
R.id.todo_hide_completed -> {
item.isChecked = !item.isChecked
mTodoViewModel.hideCompleted.value = item.isChecked
true
}
R.id.del_selected_tasks -> {
deleteSelectedUsers()
true
}
R.id.del_all_tasks -> {
deleteAllTasks()
true
}
else -> super.onOptionsItemSelected(item)
}
}
// function to delete all of our Tasks
private fun deleteAllTasks() {
val builder = AlertDialog.Builder(requireContext())
builder.setPositiveButton("Yes") {_,_->
mTodoViewModel.deleteAllTasks()
Toast.makeText(requireContext(), "All tasks have been successfully deleted!", Toast.LENGTH_LONG).show()
}
builder.setNegativeButton("No") {_,_-> }
builder.setTitle("Confirm Deletion")
builder.setMessage("Are you sure you want to delete all Tasks?")
builder.create().show()
}
// function to delete only selected Tasks
#SuppressLint("NotifyDataSetChanged")
private fun deleteSelectedUsers() {
val builder = AlertDialog.Builder(requireContext())
// Our todos that have been marked completed by the checkBox
val finishedTodos = todosList.filter { it.todoCheckBox }
builder.setPositiveButton("Yes") {_,_->
finishedTodos.forEach { todos ->
mTodoViewModel.deleteSelectedTasks(todos.id.toLong())
}
Toast.makeText(requireContext(), "Selected tasks successfully deleted!", Toast.LENGTH_LONG).show()
}
builder.setNegativeButton("No") {_,_-> }
builder.setTitle("Confirm Deletion")
builder.setMessage("Are you sure you want to delete only selected Tasks?")
builder.create().show()
Log.i(TAG , "Our todos list size is ${finishedTodos.size}")
}
// We want to leave no trace of our Binding class Reference to avoid memory leaks
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
I was able to find a solution. It turns out there was no logic to actually change the boolean value of the todoCheckBox(was changed to completed), it was just adding a strikeThrough. So I followed a better method to Implement the strikeThrough and refactored some of the code. So here's my code.
My Adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.ListAdapter
import com.bignerdranch.android.to_dolist.databinding.CustomRowBinding
import com.bignerdranch.android.to_dolist.fragments.add.SIMPLE_DATE_FORMAT
import com.bignerdranch.android.to_dolist.fragments.add.SIMPLE_TIME_FORMAT
import com.bignerdranch.android.to_dolist.model.Todo
import java.text.SimpleDateFormat
import java.util.*
class TodoAdapter(private val listener : OnItemClickListener):
ListAdapter<Todo, TodoAdapter.TodoViewHolder>(DiffCallBack) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
// this can be done in an inline variable and I may experiment on it later.
val binding = CustomRowBinding.inflate(LayoutInflater.from(parent.context),
parent,
false
)
return TodoViewHolder(binding)
}
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
}
inner class TodoViewHolder(private val binding : CustomRowBinding) : RecyclerView.ViewHolder(binding.root) {
/** Calling onClickListeners for each _Todo and the associated checkBox. **/
init {
binding.apply {
root.setOnClickListener {
val position = adapterPosition // this represents the position of any item in the root layout
// NO_POSITION means that an item is invalid and out of this list, so this is a safe check because-
// we don't want to call a listener on an invalid item
if (position != RecyclerView.NO_POSITION) {
val todo = getItem(position)
listener.onItemClick(todo)
}
}
cbTask.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val todo = getItem(position)
listener.onCheckBoxClick(todo, cbTask.isChecked)
}
}
}
}
fun bind(todo : Todo) {
val dateLocales = SimpleDateFormat(SIMPLE_DATE_FORMAT, Locale.getDefault())
val timeLocales = SimpleDateFormat(SIMPLE_TIME_FORMAT, Locale.getDefault())
binding.apply {
tvTaskTitle.text = todo.title
tvTaskDate.text = dateLocales.format(todo.date)
tvTaskTime.text = timeLocales.format(todo.time)
cbTask.isChecked = todo.completed
tvTaskTitle.paint.isStrikeThruText = todo.completed
}
}
}
interface OnItemClickListener {
fun onItemClick(todo : Todo)
fun onCheckBoxClick(todo: Todo, isChecked: Boolean)
}
// This piece of code checks between our old and changed and lists and updates the recyclerView with the latest list.
// This also stops the recyclerView from redrawing itself after the position of an item has been changed. It even provides a nice animation.
object DiffCallBack : DiffUtil.ItemCallback<Todo>() {
override fun areItemsTheSame(oldItem: Todo, newItem: Todo) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Todo, newItem: Todo) =
oldItem == newItem
}
}
Fragment
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.Toast
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bignerdranch.android.to_dolist.databinding.FragmentListBinding
import com.bignerdranch.android.to_dolist.R
import com.bignerdranch.android.to_dolist.viewmodel.SortOrder
import com.bignerdranch.android.to_dolist.viewmodel.TodoViewModel
import com.bignerdranch.android.to_dolist.model.Todo
import com.bignerdranch.android.to_dolist.utils.onQueryTextChanged
private const val TAG = "ListFragment"
class ListFragment : Fragment(), TodoAdapter.OnItemClickListener {
private var _binding : FragmentListBinding? = null
private val binding get() = _binding!!
private lateinit var mTodoViewModel: TodoViewModel
private lateinit var recyclerView: RecyclerView
private val adapter = TodoAdapter(this)
private var todosList = emptyList<Todo>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment with ViewBinding style
_binding = FragmentListBinding.inflate(inflater, container, false)
// this tells our activity/fragment that we have a menu_item it should respond to it.
setHasOptionsMenu(true)
recyclerView = binding.recyclerViewTodo
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.setHasFixedSize(true)
/**
* updates our recyclerView with the new "observed" changes in our database through our adapter
*/
// TodoViewModel
mTodoViewModel = ViewModelProvider(this)[TodoViewModel::class.java]
mTodoViewModel.tasks.observe(viewLifecycleOwner) { todos ->
adapter.submitList(todos)
todosList = todos
}
// Add Task Button
binding.fbAdd.setOnClickListener {
findNavController().navigate(R.id.action_listFragment_to_addFragment)
}
return binding.root
}
override fun onItemClick(todo: Todo) {
mTodoViewModel.onTaskSelected(todo)
}
override fun onCheckBoxClick(todo: Todo, isChecked: Boolean) {
mTodoViewModel.onTaskCheckedChanged(todo, isChecked)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.fragment_list, menu)
val search = menu.findItem(R.id.todo_search)
val searchView = search.actionView as SearchView
searchView.onQueryTextChanged { querySearch ->
mTodoViewModel.searchQuery.value = querySearch
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when(item.itemId) {
R.id.sort_by_name -> {
mTodoViewModel.sortOrder.value = SortOrder.BY_NAME
true
}
R.id.sort_by_date -> {
mTodoViewModel.sortOrder.value = SortOrder.BY_DATE
true
}
R.id.action_hide_completed_tasks -> {
item.isChecked = !item.isChecked
mTodoViewModel.hideCompleted.value = item.isChecked
true
}
R.id.del_selected_tasks -> {
deleteSelectedUsers()
true
}
R.id.del_all_tasks -> {
deleteAllTasks()
true
}
else -> super.onOptionsItemSelected(item)
}
}
// function to delete all of our Tasks
private fun deleteAllTasks() {
val builder = AlertDialog.Builder(requireContext())
builder.setPositiveButton("Yes") {_,_->
mTodoViewModel.deleteAllTasks()
Toast.makeText(requireContext(), "All tasks have been successfully deleted!", Toast.LENGTH_LONG).show()
}
builder.setNegativeButton("No") {_,_-> }
builder.setTitle("Confirm Deletion")
builder.setMessage("Are you sure you want to delete all Tasks?")
builder.create().show()
}
// function to delete only selected Tasks
#SuppressLint("NotifyDataSetChanged")
private fun deleteSelectedUsers() {
val builder = AlertDialog.Builder(requireContext())
// Our todos that have been marked completed by the checkBox
val finishedTodos = todosList.filter { it.completed }
builder.setPositiveButton("Yes") {_,_->
finishedTodos.forEach { todos ->
mTodoViewModel.deleteSelectedTasks(todos.id.toLong())
}
Toast.makeText(requireContext(), "Selected tasks successfully deleted!", Toast.LENGTH_LONG).show()
}
builder.setNegativeButton("No") {_,_-> }
builder.setTitle("Confirm Deletion")
builder.setMessage("Are you sure you want to delete only selected Tasks?")
builder.create().show()
Log.i(TAG , "Our todos list size is ${finishedTodos.size}")
}
// We want to leave no trace of our Binding class Reference to avoid memory leaks
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
And then just add both functions in the ViewModel
fun onTaskSelected(task : Todo) {
TODO()
}
fun onTaskCheckedChanged(todo : Todo, isChecked : Boolean) {
viewModelScope.launch {
repository.updateTask(todo.copy(completed = isChecked))
}
}
In my app, when I click on an item (Issue) in a RecyclerView which uses FirebaseRecyclerPagingAdapter to paginate data from Firebase realtime database, it displays details about the item clicked in another fragment (using navigation component). This works fine on the first click, however when I return to the previous fragment and click the same item on the RecyclerView a second time, the details of the item are not shown.
Because I use safe args to pass the item id (issueId) to the next fragment which it uses to query the firebase realtime database and retrieve the details to be displayed, I decide to log the item id to my console in onViewCreated() just to be sure that the item id is being passed on the second click and also that the details (names of user who added an issue) are being retrieved from the database, but just not showing. Then, I noticed a weird behaviour.
On the first click, the item id is logged to the console, the details are logged to the console as well and the fragment displays the details. However on the second click, the item id is logged to the console (showing that the item id is being passed as should be the case), but the details are not logged to the console and not displayed in the fragment (hence the fragment shows up empty). Now the weird part, when I navigate back to the previous fragment, then I see a log of the details displayed twice.
Another strange thing I noticed is that, every item on the RecyclerView has this weird behaviour except the last item. The last item displays its details on the second click, but any other item I click doesn't.
I also noticed that the log shows the details for every item I have previously clicked twice when I navigate back even though I am clicking on a different item
I changed the adapter from FirebaseRecyclerPagingAdapter to FirebaseRecyclerAdapter, everything works fine. When I change back to using FirebaseRecyclerPagingAdapter, the same problem exists.
Is this a bug in my code or FirebaseRecyclerPagingAdapter itself. What could be the problem and what can I do to fix it?
Below is the FirebaseRecyclerPagingAdapter:
package com.colley.android.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.colley.android.R
import com.colley.android.databinding.ItemIssueBinding
import com.colley.android.model.Issue
import com.colley.android.model.Profile
import com.firebase.ui.database.paging.DatabasePagingOptions
import com.firebase.ui.database.paging.FirebaseRecyclerPagingAdapter
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
import com.google.firebase.database.ktx.database
import com.google.firebase.database.ktx.getValue
import com.google.firebase.ktx.Firebase
class IssuesPagingAdapter(
options: DatabasePagingOptions<Issue>,
private val context: Context,
private val currentUser: FirebaseUser?,
private val clickListener: IssuePagingItemClickedListener
) : FirebaseRecyclerPagingAdapter<Issue, IssuePagingViewHolder>(options) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IssuePagingViewHolder {
val viewBinding = ItemIssueBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return IssuePagingViewHolder(viewBinding)
}
override fun onBindViewHolder(viewHolder: IssuePagingViewHolder, position: Int, model: Issue) {
viewHolder.bind(currentUser, model, context, clickListener)
}
interface IssuePagingItemClickedListener {
fun onItemClick(issueId: String, view: View)
fun onItemLongCLicked(issueId: String, view: View)
fun onUserClicked(userId: String, view: View)
}
}
class IssuePagingViewHolder (private val itemBinding : ItemIssueBinding) : RecyclerView.ViewHolder(itemBinding.root) {
#SuppressLint("SetTextI18n")
fun bind(
currentUser: FirebaseUser?,
issue: Issue, context: Context,
clickListener: IssuesPagingAdapter.IssuePagingItemClickedListener) = with(itemBinding) {
//set issue title, body, timeStamp, contributions and endorsements count
issueTitleTextView.text = issue.title
issueBodyTextView.text = issue.body
issueTimeStampTextView.text = issue.timeStamp
contributionsTextView.text = issue.contributionsCount.toString()
endorsementTextView.text = issue.endorsementsCount.toString()
//check if userId is not null
issue.userId?.let { userId ->
//retrieve user profile
Firebase.database.reference.child("profiles").child(userId)
.addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val profile = snapshot.getValue<Profile>()
if (profile != null) {
//set the name of user who raised this issue
userNameTextView.text = profile.name
//set the school of the user who raised this issue
userSchoolTextView.text = profile.school
}
}
override fun onCancelled(error: DatabaseError) {}
}
)
//retrieve user photo
Firebase.database.reference.child("photos").child(userId)
.addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val photo = snapshot.getValue<String>()
//set photo
if (photo != null) {
Glide.with(root.context).load(photo)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).into(userImageView)
} else {
Glide.with(root.context).load(R.drawable.ic_person).into(userImageView)
}
}
override fun onCancelled(error: DatabaseError) {}
}
)
}
root.setOnClickListener {
if(issue.issueId != null) {
clickListener.onItemClick(issue.issueId, it)
}
}
root.setOnLongClickListener {
if(issue.issueId != null) {
clickListener.onItemLongCLicked(issue.issueId, it)
}
true
}
userNameTextView.setOnClickListener {
if(issue.userId != null) {
clickListener.onUserClicked(issue.userId, it)
}
}
}
}
Here is the fragment to display the item details:
package com.colley.android.view.fragment
import android.os.Bundle
import android.util.Log
import android.view.*
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.colley.android.R
import com.colley.android.adapter.IssuesCommentsRecyclerAdapter
import com.colley.android.databinding.FragmentViewIssueBinding
import com.colley.android.model.Comment
import com.colley.android.model.Issue
import com.colley.android.model.Profile
import com.colley.android.view.dialog.IssueCommentBottomSheetDialogFragment
import com.firebase.ui.database.FirebaseRecyclerOptions
import com.firebase.ui.database.ObservableSnapshotArray
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.ktx.auth
import com.google.firebase.database.*
import com.google.firebase.database.ktx.database
import com.google.firebase.database.ktx.getValue
import com.google.firebase.ktx.Firebase
class ViewIssueFragment :
Fragment(),
IssuesCommentsRecyclerAdapter.ItemClickedListener,
IssuesCommentsRecyclerAdapter.DataChangedListener {
private val args: ViewIssueFragmentArgs by navArgs()
private var _binding: FragmentViewIssueBinding? = null
private val binding get() = _binding
private lateinit var dbRef: DatabaseReference
private lateinit var auth: FirebaseAuth
private lateinit var currentUser: FirebaseUser
private lateinit var recyclerView: RecyclerView
private lateinit var commentSheetDialog: IssueCommentBottomSheetDialogFragment
private var issue: Issue? = null
private var adapter: IssuesCommentsRecyclerAdapter? = null
private var manager: LinearLayoutManager? = null
private val uid: String
get() = currentUser.uid
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentViewIssueBinding.inflate(inflater, container, false)
recyclerView = binding?.issuesCommentsRecyclerView!!
return binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//initialize Realtime Database
dbRef = Firebase.database.reference
//initialize authentication
auth = Firebase.auth
//initialize currentUser
currentUser = auth.currentUser!!
//log item id
Log.d("Log itemId", args.issueId)
//get a query reference to issue comments //order by time stamp
val commentsRef = dbRef.child("issues").child(args.issueId)
.child("comments").orderByChild("commentTimeStamp")
//the FirebaseRecyclerAdapter class and options come from the FirebaseUI library
//build an options to configure adapter. setQuery takes firebase query to listen to and a
//model class to which snapShots should be parsed
val options = FirebaseRecyclerOptions.Builder<Comment>()
.setQuery(commentsRef, Comment::class.java)
.build()
//initialize issue comments adapter
adapter = IssuesCommentsRecyclerAdapter(
options,
currentUser,
this,
this,
requireContext())
manager = LinearLayoutManager(requireContext())
//reversing layout and stacking fron end so that the most recent comments appear at the top
manager?.reverseLayout = true
manager?.stackFromEnd = true
recyclerView.layoutManager = manager
recyclerView.adapter = adapter
dbRef.child("issues").child(args.issueId).addValueEventListener(
object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
issue = snapshot.getValue<Issue>()
if(issue != null) {
//listener for contrbutions count used to set count text
dbRef.child("issues").child(args.issueId)
.child("contributionsCount").addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val count = snapshot.getValue<Int>()
if(count != null) {
binding?.contributionsTextView?.text = count.toString()
}
}
override fun onCancelled(error: DatabaseError) {}
}
)
//listener for endorsement counts used to set endorsement count text
dbRef.child("issues").child(args.issueId)
.child("endorsementsCount").addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val count = snapshot.getValue<Int>()
if(count != null) {
binding?.endorsementTextView?.text = count.toString()
}
}
override fun onCancelled(error: DatabaseError) {} }
)
//set issue title, body and time stamp, these don't need to change
binding?.issueTitleTextView?.text = issue?.title
binding?.issueBodyTextView?.text = issue?.body
binding?.issueTimeStampTextView?.text = issue?.timeStamp.toString()
//listener for user photo
dbRef.child("photos").child(issue?.userId.toString())
.addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val photo = snapshot.getValue<String>()
if(photo != null) {
context?.let { context -> binding?.userImageView?.let {
imageView ->
Glide.with(context).load(photo).into(
imageView
)
} }
} else {
context?.let { context -> binding?.userImageView?.let {
imageView ->
Glide.with(context).load(R.drawable.ic_profile).into(
imageView
)
} }
}
}
override fun onCancelled(error: DatabaseError) {}
}
)
//listener for profile to set name and school
dbRef.child("profiles").child(issue?.userId.toString())
.addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val profile = snapshot.getValue<Profile>()
if (profile != null) {
//log name details to console
profile.name?.let { Log.d("Log Details", it) }
binding?.userNameTextView?.text = profile.name
binding?.userSchoolTextView?.text = profile.school
}
}
override fun onCancelled(error: DatabaseError) {}
}
)
}
}
override fun onCancelled(error: DatabaseError) {}
}
)
binding?.commentLinearLayout?.setOnClickListener {
commentSheetDialog = IssueCommentBottomSheetDialogFragment(
requireContext(),
requireView())
commentSheetDialog.arguments = bundleOf("issueIdKey" to args.issueId)
commentSheetDialog.show(parentFragmentManager, null)
}
binding?.endorseLinearLayout?.setOnClickListener {
//update contributions count
dbRef.child("issues").child(args.issueId).child("endorsementsCount")
.runTransaction(
object : Transaction.Handler {
override fun doTransaction(currentData: MutableData): Transaction.Result {
//retrieve the current value of endorsement count at this location
var endorsementsCount = currentData.getValue<Int>()
if (endorsementsCount != null) {
//increase the count by 1
endorsementsCount++
//reassign the value to reflect the new update
currentData.value = endorsementsCount
}
//set database issue value to the new update
return Transaction.success(currentData)
}
override fun onComplete(
error: DatabaseError?,
committed: Boolean,
currentData: DataSnapshot?
) {
if (error == null && committed) {
Toast.makeText(requireContext(), "Endorsed", Toast.LENGTH_SHORT)
.show()
}
}
}
)
}
//view profile when clicked
binding?.userImageView?.setOnClickListener {
val action = issue?.userId?.let { it1 ->
ViewIssueFragmentDirections.actionViewIssueFragmentToUserInfoFragment(it1)
}
if (action != null) {
parentFragment?.findNavController()?.navigate(action)
}
}
//view user profile when clicked
binding?.userNameTextView?.setOnClickListener {
val action = issue?.userId?.let { it1 ->
ViewIssueFragmentDirections.actionViewIssueFragmentToUserInfoFragment(it1)
}
if (action != null) {
parentFragment?.findNavController()?.navigate(action)
}
}
}
override fun onItemClick(comment: Comment, view: View) {
//expand comment
}
override fun onItemLongCLicked(comment: Comment, view: View) {
//create option to delete
//create option to respond
}
//view user profile
override fun onUserClicked(userId: String, view: View) {
val action = ViewIssueFragmentDirections.actionViewIssueFragmentToUserInfoFragment(userId)
parentFragment?.findNavController()?.navigate(action)
}
override fun onStart() {
super.onStart()
adapter?.startListening()
}
override fun onStop() {
super.onStop()
adapter?.stopListening()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
override fun onDataAvailable(snapshotArray: ObservableSnapshotArray<Comment>) {
//dismiss progress bar once snapshot is available
binding?.issuesCommentProgressBar?.visibility = GONE
//show that there are no comments if snapshot is empty else hide view
//show recycler view if snapshot is not empty else hide
if (snapshotArray.isEmpty()) {
binding?.noCommentsLayout?.visibility = VISIBLE
} else {
binding?.noCommentsLayout?.visibility = GONE
binding?.issuesCommentsRecyclerView?.visibility = VISIBLE
}
}
}
Here is the fragment with the recyclerView showing how I have initialised the adapter:
package com.colley.android.view.fragment
import android.os.Bundle
import android.view.*
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.paging.LoadState
import androidx.paging.PagingConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.colley.android.R
import com.colley.android.adapter.IssuesPagingAdapter
import com.colley.android.databinding.FragmentIssuesBinding
import com.colley.android.model.Issue
import com.firebase.ui.database.paging.DatabasePagingOptions
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.ktx.auth
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class IssuesFragment :
Fragment(),
IssuesPagingAdapter.IssuePagingItemClickedListener {
private var _binding: FragmentIssuesBinding? = null
private val binding get() = _binding!!
private lateinit var dbRef: DatabaseReference
private lateinit var auth: FirebaseAuth
private lateinit var currentUser: FirebaseUser
private var adapter: IssuesPagingAdapter? = null
private var manager: LinearLayoutManager? = null
private lateinit var recyclerView: RecyclerView
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
private val uid: String
get() = currentUser.uid
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//fragment can participate in populating the options menu
setHasOptionsMenu(true)
//initialize Realtime Database
dbRef = Firebase.database.reference
//initialize authentication
auth = Firebase.auth
//initialize currentUser
currentUser = auth.currentUser!!
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
menu.clear()
inflater.inflate(R.menu.isssues_menu, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.search_issues_menu_item -> {
Toast.makeText(context, "Searching issues", Toast.LENGTH_LONG).show()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentIssuesBinding.inflate(inflater, container, false)
recyclerView = binding.issueRecyclerView
swipeRefreshLayout = binding.swipeRefreshLayout
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//get a query reference to issues
val issuesQuery = dbRef.child("issues")
//configuration for how the FirebaseRecyclerPagingAdapter should load pages
val config = PagingConfig(
pageSize = 30,
prefetchDistance = 15,
enablePlaceholders = false
)
//Options to configure an FirebasePagingAdapter
val options = DatabasePagingOptions.Builder<Issue>()
.setLifecycleOwner(viewLifecycleOwner)
.setQuery(issuesQuery, config, Issue::class.java)
.setDiffCallback(object : DiffUtil.ItemCallback<DataSnapshot>() {
override fun areItemsTheSame(
oldItem: DataSnapshot,
newItem: DataSnapshot
): Boolean {
return oldItem.getValue(Issue::class.java)?.issueId == newItem.getValue(Issue::class.java)?.issueId
}
override fun areContentsTheSame(
oldItem: DataSnapshot,
newItem: DataSnapshot
): Boolean {
return oldItem.getValue(Issue::class.java) == newItem.getValue(Issue::class.java)
}
})
.build()
//instantiate adapter
adapter = IssuesPagingAdapter(
options,
requireContext(),
currentUser,
this)
//Perform some action every time data changes or when there is an error.
viewLifecycleOwner.lifecycleScope.launch {
adapter?.loadStateFlow?.collectLatest { loadStates ->
when (loadStates.refresh) {
is LoadState.Error -> {
// The initial load failed. Call the retry() method
// in order to retry the load operation.
Toast.makeText(context, "Error fetching issues! Retrying..", Toast.LENGTH_SHORT).show()
//display no posts available at the moment
binding.noIssuesLayout.visibility = VISIBLE
adapter?.retry()
}
is LoadState.Loading -> {
// The initial Load has begun
// ...
swipeRefreshLayout.isRefreshing = true
}
is LoadState.NotLoading -> {
// The previous load (either initial or additional) completed
swipeRefreshLayout.isRefreshing = false
//remove display no posts available at the moment
binding.noIssuesLayout.visibility = GONE
}
}
when (loadStates.append) {
is LoadState.Error -> {
// The additional load failed. Call the retry() method
// in order to retry the load operation.
adapter?.retry()
}
is LoadState.Loading -> {
// The adapter has started to load an additional page
// ...
swipeRefreshLayout.isRefreshing = true
}
is LoadState.NotLoading -> {
if (loadStates.append.endOfPaginationReached) {
// The adapter has finished loading all of the data set
swipeRefreshLayout.isRefreshing = false
}
}
}
}
}
//set recycler view layout manager
manager = LinearLayoutManager(requireContext())
recyclerView.layoutManager = manager
//initialize adapter
recyclerView.adapter = adapter
swipeRefreshLayout.setOnRefreshListener {
adapter?.refresh()
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
//navigate to new fragment with issue id
override fun onItemClick(issueId: String, view: View) {
val action = HomeFragmentDirections.actionHomeFragmentToViewIssueFragment(issueId)
parentFragment?.findNavController()?.navigate(action)
}
override fun onItemLongCLicked(issueId: String, view: View) {
}
override fun onUserClicked(userId: String, view: View) {
val action = HomeFragmentDirections.actionHomeFragmentToUserInfoFragment(userId)
parentFragment?.findNavController()?.navigate(action)
}
}
Before click
After first click
After second click
Use addListenerForSingleValueEvent instead of addValueEventListener to query the item details (issue) from the database in the fragment that displays the clicked item details. Otherwise, remove the addValueEventListener in onStop() so that the listener is no longer attached to the database when navigating back to the previous fragment.
I'm trying to show the recycler view's data on my app. The thing is, even though the NetworkStatus is successful (I can tell because I don't get the toast's message and the loader disappears and I can also see the data in the logcat), the info is not displayed. I am not sure if the error is in the way I'm calling the recycler view on my MainActivity or in the RecyclerAdapter but any idea as to where the problem is would be very helpful.
This is the RecyclerAdapter:
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.app.mortyapp.databinding.ItemDetailBinding
class RecyclerAdapter(private var characterList: List<Character>): RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerAdapter.ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ItemDetailBinding.inflate(
layoutInflater,
parent,
false
)
return ViewHolder(binding)
}
override fun getItemCount(): Int = characterList.size
override fun onBindViewHolder(holder: RecyclerAdapter.ViewHolder, position: Int) {
holder.bind(characterList[position])
}
fun setCharacterList(characterList: List<Character>){
this.characterList = characterList
notifyDataSetChanged()
}
inner class ViewHolder(
private val binding: ItemDetailBinding
) : RecyclerView.ViewHolder(binding.root){
fun bind(character: Character) {
with(binding){
val itemName: TextView = binding.tvName
val itemGender: TextView = binding.tvGender
itemName.text = character.name
itemGender.text = character.gender
}
}
}
}
This is the MainActivity:
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.ProgressBar
import android.widget.Toast
import androidx.activity.viewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.app.mortyapp.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val characters = mutableListOf<Character>()
private lateinit var progressBar: ProgressBar
private lateinit var recyclerAdapter: RecyclerAdapter
private val viewModel: MainViewModel by viewModels(
factoryProducer = {MainViewModelFactory()}
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
progressBar = binding.ProgressBar
progressBar.visibility = View.INVISIBLE
setObservers()
initRecyclerView()
}
private fun initRecyclerView() {
with(binding.rvCharacters){
layoutManager = LinearLayoutManager(context)
recyclerAdapter = RecyclerAdapter(characters).apply {
setCharacterList(characters)
}
}
}
private fun setObservers(){
viewModel.characterList.observe(this, Observer {
when(it.status){
NetworkStatus.LOADING ->{
//show loading state
progressBar.visibility = View.VISIBLE
}
NetworkStatus.SUCCESS -> {
//hide loading state
progressBar.visibility = View.INVISIBLE
//render character list
recyclerAdapter.setCharacterList(characters)
}
NetworkStatus.ERROR -> {
//show error message
Toast.makeText(this,"Error loading content", Toast.LENGTH_SHORT).show()
//hide loading state
progressBar.visibility = View.INVISIBLE
}
}
})
}
}
API response:
import com.google.gson.annotations.SerializedName
data class Character (
#SerializedName("id") val id: Int,
#SerializedName("name") val name: String,
#SerializedName("gender") val gender: String
)
data class CharacterListResponse(
#SerializedName("results") val results: List<Character>
)
Remote data source:
package com.app.mortyapp
import com.app.mortyapp.Model.CharacterService
import com.app.mortyapp.Model.RetrofitServices
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class CharacterRemoteDataSource {
fun getCharacterList(networkResponse: NetworkResponse<List<Character>>) {
val service = RetrofitServices.instance
.create(CharacterService::class.java)
.getCharacterList()
service.enqueue(object : Callback<CharacterListResponse> {
override fun onResponse(
call: Call<CharacterListResponse>,
response: Response<CharacterListResponse>
) {
val resource = response.body()?.run {
if (results.isNotEmpty())
Resource(NetworkStatus.SUCCESS, results)
else
Resource(NetworkStatus.ERROR)
} ?: run {
Resource(NetworkStatus.ERROR)
}
networkResponse.onResponse(resource)
}
override fun onFailure(call: Call<CharacterListResponse>, t: Throwable) {
networkResponse.onResponse(Resource(NetworkStatus.ERROR, message = t.message))
}
})
}
}
interface NetworkResponse<T> {
fun onResponse(value: Resource<T>)
}
Set adapter for Recyclerview in
setupRecylerview ()
adapter = recyclerAdapter
NetworkStatus.SUCCESS -> {
//hide loading state
progressBar.visibility = View.INVISIBLE
//render character list
recyclerAdapter.setCharacterList(characters)
recyclerAdapter.notifydatasetchanged()//add this line
}
I think most problems found with recyclerView isn't linked to it, but with some adjourning codes. For example, a very similar problem was solved by finding out that the adapter POJO class was retrieving 0 rather than the actual size of the array list.
See the solved problem here:
Android Recycler View not loading Data (Peculiar problem, Not a Duplicate)
Below code is supposed to set "isEnabled" attribute of a button to true, but it doesn't.
I initialize a mutable list which adds a String when certain Switches are on, and remove them when are off.
I created an if condition where if the size of the list is equal to 2 then ok_button is enabled.
I can't see why the ok_button is not updated even when the conditions are met.
package com.example.malakes
import android.nfc.Tag
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.CompoundButton
import android.widget.Switch
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*
import android.util.Log
import android.view.View
import android.widget.Button
class MainActivity : AppCompatActivity() {
companion object{ const val TAG = "MyActivity" } //define TAG
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val epilegmenoi: MutableList<String> = mutableListOf()
val tony = findViewById<Switch>(R.id.switchTony)
val giorgis = findViewById<Switch>(R.id.switchGiorgos)
val duke = findViewById<Switch>(R.id.switchDuke)
val nikolas = findViewById<Switch>(R.id.switchNikolas)
val dionisis = findViewById<Switch>(R.id.switchDionisis)
val grigoris = findViewById<Switch>(R.id.switchGrigoris)
val ok_button = findViewById<Button>(R.id.buttonOK)
val clear_button = findViewById<Button>(R.id.buttonCLEAR)
tony.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
epilegmenoi.add("Tony")
} else {
epilegmenoi.remove("Tony")
}
}
giorgis.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
epilegmenoi.add("Giorgis")
} else {
epilegmenoi.remove("Giorgis")
}
}
if (epilegmenoi.size == 2) {ok_button.isEnabled=true}
}
}
onCreate() is a Lifecycle method in your activity. It is only called when your Activity is being called for the first time or when phone configs change, e.g. Screen Rotation, Locale change, and ...
Setting if (epilegmenoi.size == 2) {ok_button.isEnabled=true} inside onCreate doesn't do anything for you.
Consider moving this line of code to some Event-based function.
fun updateButtonState() {
my_button.isEnabled = (myList.size == 2)
}
And inside your check box events:
tony.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
epilegmenoi.add("Tony")
} else {
epilegmenoi.remove("Tony")
}
updateButtonState()
}
giorgis.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
epilegmenoi.add("Giorgis")
} else {
epilegmenoi.remove("Giorgis")
}
updateButtonState()
}