I am using a RecyclerView to populate data that are fetched in my Room database. When data is fetched or removed from my Room database, the RecyclerView is updated automatically. I have implemented a swipe to delete and undo in my RecyclerView. When i swipe, i delete the item in my Room Database and the RecyclerView gets updated automatically. However, when i click the "Undo" button, i insert the item again in the Room database and the item is shown in the Recyclerview. The problem here is when i restore the item, it gets restored in the last position. I want it to be restore in the position it was deleted, not last.
My code is as below:
fragment.xml
<androidx.recyclerview.widget.RecyclerView
android:id="#id/RecyclerView_fromFragmentToDo_Main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/Margin_Views_Small"
android:layout_marginBottom="?attr/actionBarSize"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
adapter.kt
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.th3pl4gu3.lifestyle.core.lifestyle.ToDo
import com.th3pl4gu3.lifestyle.databinding.CustomRecyclerviewListTodoBinding
class ToDoAdapter : ListAdapter<ToDo, ToDoAdapter.ViewHolder>(ToDoDiffCallback()) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val toDo = getItem(position)
holder.bind(toDo)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
class ViewHolder private constructor(val binding: CustomRecyclerviewListTodoBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(toDo: ToDo) {
binding.myToDo = toDo
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = CustomRecyclerviewListTodoBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
}
class ToDoDiffCallback: DiffUtil.ItemCallback<ToDo>() {
override fun areItemsTheSame(oldItem: ToDo, newItem: ToDo): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ToDo, newItem: ToDo): Boolean {
return oldItem == newItem
}
}
viewmodel.kt
import android.app.Application
import androidx.lifecycle.*
import com.th3pl4gu3.lifestyle.core.lifestyle.ToDo
import com.th3pl4gu3.lifestyle.core.operations.Filter
import com.th3pl4gu3.lifestyle.core.operations.ToDoOperations
import com.th3pl4gu3.lifestyle.database.LifestyleDatabase
import com.th3pl4gu3.lifestyle.ui.enums.ToggleButtonStates
import kotlinx.coroutines.*
class ToDoViewModel(
val database: LifestyleDatabase,
application: Application) : AndroidViewModel(application) {
private var _viewModelJob = Job()
private val _uiScope = CoroutineScope(Dispatchers.Main + _viewModelJob)
//Current state of the toggle button (Current button checked)
var currentToggleButtonState = ToggleButtonStates.BUTTON_ACTIVE
//Fetch all to dos from database
private var _toDos = ToDoOperations.getAllOffline(database)
val toDosMediatorLiveData = MediatorLiveData<List<ToDo>>()
init {
//Update the list of the recyclerview on INIT
updateList(currentToggleButtonState)
}
/**
* Public functions that are accessible from the outside
**/
fun updateList(toggleButton: ToggleButtonStates) {
toDosMediatorLiveData.removeSource(_toDos)
when(toggleButton){
ToggleButtonStates.BUTTON_ALL ->{
currentToggleButtonState = ToggleButtonStates.BUTTON_ALL
toDosMediatorLiveData.addSource(_toDos){
toDosMediatorLiveData.value = it
}
}
ToggleButtonStates.BUTTON_ACTIVE ->{
currentToggleButtonState = ToggleButtonStates.BUTTON_ACTIVE
toDosMediatorLiveData.addSource(_toDos){
toDosMediatorLiveData.value = Filter<ToDo>(it).getActive()
}
}
ToggleButtonStates.BUTTON_COMPLETE ->{
currentToggleButtonState = ToggleButtonStates.BUTTON_COMPLETE
toDosMediatorLiveData.addSource(_toDos){
toDosMediatorLiveData.value = Filter<ToDo>(it).getCompleted()
}
}
}
}
fun insertItem(toDo: ToDo) {
_uiScope.launch {
insert(toDo)
}
}
fun markAsDeleted(toDo: ToDo) {
_uiScope.launch {
remove(toDo)
}
}
fun markItem(toDo: ToDo){
if(toDo.dateCompleted == null){
markAsCompleted(toDo)
}else{
markAsIncomplete(toDo)
}
}
/**
* Private functions for internal use ONLY
**/
private fun markAsCompleted(newToDo: ToDo) {
_uiScope.launch {
newToDo.markAsComplete()
update(newToDo)
}
}
private fun markAsIncomplete(newToDo: ToDo) {
_uiScope.launch {
newToDo.markAsIncomplete()
update(newToDo)
}
}
private suspend fun insert(toDo: ToDo) {
withContext(Dispatchers.IO) {
toDo.add(database)
}
}
private suspend fun remove(toDo: ToDo) {
withContext(Dispatchers.IO) {
toDo.delete(database)
}
}
private suspend fun update(newToDo: ToDo) {
withContext(Dispatchers.IO) {
newToDo.update(database)
}
}
/**
* Overridden functions
**/
override fun onCleared() {
super.onCleared()
//Clear the view model job when user leave
_viewModelJob.cancel()
}
}
fragment.kt
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.th3pl4gu3.lifestyle.R
import com.th3pl4gu3.lifestyle.ui.Utils.toast
import com.th3pl4gu3.lifestyle.database.LifestyleDatabase
import com.th3pl4gu3.lifestyle.databinding.FragmentToDoBinding
import com.th3pl4gu3.lifestyle.ui.Utils.action
import com.th3pl4gu3.lifestyle.ui.Utils.snackBar
import com.th3pl4gu3.lifestyle.ui.Utils.snackBarWithAction
import com.th3pl4gu3.lifestyle.ui.enums.ToggleButtonStates
import java.util.*
class FragmentToDo : Fragment() {
private lateinit var mBinding: FragmentToDoBinding
private lateinit var mToDoViewModel: ToDoViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
//Inflate the layout for this fragment
mBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_to_do, container, false)
//Configures the screen views (Eg. Title, appearance of top bar etc...)
configureScreenAppearance()
//Get the activity's application
val application = requireNotNull(this.activity).application
//Fetch the database
val dataSource = LifestyleDatabase.getInstance(application)
//Instantiate the view model factory
val viewModelFactory = ToDoViewModelFactory(dataSource, application)
//Instantiate the view model of this fragment
mToDoViewModel = ViewModelProviders.of(this, viewModelFactory).get(ToDoViewModel::class.java)
//Bind view model
mBinding.toDoViewModel = mToDoViewModel
//Instantiate the lifecycle owner
mBinding.lifecycleOwner = this
//RecyclerView's configuration
val adapter = ToDoAdapter()
mBinding.RecyclerViewFromFragmentToDoMain.adapter = adapter
mToDoViewModel.toDosMediatorLiveData.observe(viewLifecycleOwner, Observer {
it.let { x ->
//Update the UI and determine whether recyclerview should be visible or not
updateUI(x.isNotEmpty())
adapter.submitList(x)
}
})
//Swipe configurations
val swipeHandler = object : ToDoSwipeToCallback(requireContext()) {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val swipedToDo = (mBinding.RecyclerViewFromFragmentToDoMain.adapter as ToDoAdapter).currentList[viewHolder.adapterPosition]
val fab = requireActivity().findViewById<FloatingActionButton>(R.id.FAB_fromHomeActivity_BottomAppBarAttached)
val position = viewHolder.adapterPosition
when(direction){
ItemTouchHelper.LEFT -> {
mToDoViewModel.markItem(swipedToDo)
}
ItemTouchHelper.RIGHT -> {
mToDoViewModel.markAsDeleted(swipedToDo)
//Show Snackbar with 'Undo' action
requireActivity().findViewById<View>(android.R.id.content).snackBarWithAction(getString(R.string.Message_Exception_fromFragmentLifeStyleItems_ErrorWhileSwiping, swipedToDo.title), anchorView = fab){
action("Undo"){
mToDoViewModel.insertItem(swipedToDo)
//Restore Item
}
}
}
else ->{
requireContext().toast(getString(R.string.Message_Exception_fromFragmentLifeStyleItems_ErrorWhileSwiping))
}
}
}
}
val itemTouchHelper = ItemTouchHelper(swipeHandler)
itemTouchHelper.attachToRecyclerView(mBinding.RecyclerViewFromFragmentToDoMain)
return mBinding.root
}
/**
* Private functions for internal use ONLY
**/
private fun updateUI(recyclerViewVisible: Boolean){
if(recyclerViewVisible){
mBinding.RecyclerViewFromFragmentToDoMain.visibility = View.VISIBLE
mBinding.EmptyViewForRecyclerView.visibility = View.GONE
}else{
if(mToDoViewModel.currentToggleButtonState == ToggleButtonStates.BUTTON_COMPLETE){
mBinding.TextViewFromFragmentToDoEmptyView.text = getString(R.string.TextView_fromToDoFragment_Message_EmptyList_Completed)
}else if(mToDoViewModel.currentToggleButtonState == ToggleButtonStates.BUTTON_ACTIVE){
mBinding.TextViewFromFragmentToDoEmptyView.text = getString(R.string.TextView_fromToDoFragment_Message_EmptyList_Active)
}
mBinding.RecyclerViewFromFragmentToDoMain.visibility = View.GONE
mBinding.EmptyViewForRecyclerView.visibility = View.VISIBLE
}
}
private fun configureScreenAppearance(){
//Set title of fragment
val screenTitle = requireActivity().findViewById<TextView>(R.id.TextView_fromHomeActivity_Screen_Title)
screenTitle.text = getString(R.string.TextView_fromFragmentInHomeActivity_ScreenTitle_ToDo)
//Show Top Bar
val topBar = requireActivity().findViewById<RelativeLayout>(R.id.RelativeLayout_fromHomeActivity_TopBar)
topBar.visibility = View.VISIBLE
//Show Fab
val fab = requireActivity().findViewById<FloatingActionButton>(R.id.FAB_fromHomeActivity_BottomAppBarAttached)
fab.show()
}
}
I need to restore the item here:
ItemTouchHelper.RIGHT -> {
mToDoViewModel.markAsDeleted(swipedToDo)
//Show Snackbar with 'Undo' action
requireActivity().findViewById<View>(android.R.id.content).snackBarWithAction(getString(R.string.Message_Exception_fromFragmentLifeStyleItems_ErrorWhileSwiping, swipedToDo.title), anchorView = fab){
action("Undo"){
mToDoViewModel.insertItem(swipedToDo)
//Restore Item
}
}
}
I've tried fetching the list and then do list.add(position, item) but it doesn't work. Can someone please help ? Thanks.
If list.add didn't work then I would suggest a different approach when you swipe to delete don't delete the item. Add another column isDeleted in your model and change room query to fetch all rows in which isDeleted is false.
select * from table where isDeleted = 0
Then when swipe to delete update the row to set isDeleted true and when you undo just set isDeleted to false. You can delete the row at later point when you're sure that there'll be no undo required.
Update your insert function in viewmodel.kt file as follows
private suspend fun insert(toDo: ToDo) {
withContext(Dispatchers.IO) {
toDo.add(index, database)
}
}
Here index is an integer which indicates the position that you want to insert the new item. And notify recyclerview adapter if necessary as follows
//Show Snackbar with 'Undo' action
requireActivity().findViewById<View>(android.R.id.content).snackBarWithAction(getString(R.string.Message_Exception_fromFragmentLifeStyleItems_ErrorWhileSwiping, swipedToDo.title), anchorView = fab){
action("Undo"){
mToDoViewModel.insertItem(swipedToDo)
adapter.notifyItemInserted(index)
//Restore Item
}
}
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))
}
}
I have a TodoList App and I am trying to delete a list Completed tasks based on a strikeThrough over the Task. I have set several Implementations to single out the Task that has a strikeThrough in it which I'm honestly not sure if it works properly but I definitely know something is wrong with the Delete Query as well because it wasn't also working. The second Implementation is attached to the main one but I commented it out.
This is my Model
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.
import androidx.lifecycle.LiveData
import androidx.room.*
import com.bignerdranch.android.to_dolist.model.Todo
/**
* 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 {
// 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)
fun readAllData() : LiveData<List<Todo>>
#Query("DELETE FROM todo_table WHERE todoCheckBox = 1")
suspend fun deleteSelectedTasks()
// second method - This is the second method where I tried passing in an array
// #Delete
// suspend fun deleteSelectedTasks(todoList : Array<Todo>)
// Method in use
#Query("DELETE FROM todo_table")
suspend fun deleteAllTasks()
}
And then this is my ListFragment and ListAdapter. I have tried calling the deleteSelectedTasks through the ViewModel from both the Fragment and Adapter but none as worked.
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.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.TodoViewModel
import com.bignerdranch.android.to_dolist.model.Todo
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 lateinit var selectedTodos : List<Todo>
// second method
// var selectedTodos = arrayOf<Todo>()
// TODO - WHEN I COME BACK, I WILL SEE IF I CAN DO THE IMPLEMENTATION HERE IN THE LIST
FRAGMENT
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.
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.readAllData.observe(viewLifecycleOwner) { todos ->
adapter.setData(todos)
selectedTodos = 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)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when(item.itemId) {
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())
// val todo = emptyList<Todo>()
val finishedTodos = selectedTodos.takeWhile { it.todoCheckBox }
builder.setPositiveButton("Yes") {_,_->
mTodoViewModel.deleteSelectedTasks()
adapter.notifyDataSetChanged()
}
builder.setNegativeButton("No") {_,_->}
builder.setTitle("Confirm Deletion")
builder.setMessage("Are you sure you want to delete only selected Tasks?")
builder.create().show()
Log.d(TAG, "Our todos $selectedTodos and $finishedTodos")
}
// We want to leave no trace of our Binding class Reference to avoid memory leaks
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
ListAdapter
import android.annotation.SuppressLint
import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter
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.*
private const val TAG = "ListAdapter"
class ListAdapter: Adapter<ListAdapter.TodoViewHolder>() {
private var todoList = emptyList<Todo>()
private var todo = Todo()
// will toggle strikeThrough on the Task title
private fun toggleStrikeThrough(tvTaskTitle : TextView, cbTask : Boolean) {
if (cbTask) {
tvTaskTitle.paintFlags = tvTaskTitle.paintFlags or STRIKE_THRU_TEXT_FLAG
} else {
tvTaskTitle.paintFlags = tvTaskTitle.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
}
}
inner class TodoViewHolder(val binding : CustomRowBinding) :
RecyclerView.ViewHolder(binding.root)
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 todo = todoList[position]
val dateLocales = SimpleDateFormat(SIMPLE_DATE_FORMAT, Locale.getDefault())
val timeLocales = SimpleDateFormat(SIMPLE_TIME_FORMAT, Locale.getDefault())
holder.apply {
binding.tvTaskTitle.text = todo.title
binding.tvTaskDate.text = dateLocales.format(todo.date)
binding.tvTaskTime.text = timeLocales.format(todo.time)
binding.cbTask.isChecked = todo.todoCheckBox
toggleStrikeThrough(binding.tvTaskTitle , todo.todoCheckBox)
binding.cbTask.setOnCheckedChangeListener { _, isChecked ->
toggleStrikeThrough(binding.tvTaskTitle, isChecked)
todo.todoCheckBox = !todo.todoCheckBox
taskCheck(todoList as MutableList<Todo>)
}
}
}
private fun taskCheck(todo : List<Todo>) {
val listFragment = ListFragment()
val finishedTodos = todo.takeWhile { it.todoCheckBox }
// second method
// listFragment.selectedTodos = finishedTodos.toTypedArray()
Log.i(TAG, "Our ${finishedTodos.size}")
}
// as usual will return the size of the List
override fun getItemCount() = todoList.size
#SuppressLint("NotifyDataSetChanged")
fun setData(todo : List<Todo>) {
this.todoList = todo
notifyDataSetChanged()
}
#SuppressLint("NotifyDataSetChanged")
fun deleteSelectedTasks() {
val listFragment = ListFragment()
listFragment.mTodoViewModel.deleteSelectedTasks()
notifyDataSetChanged()
}
}
#Delete does work with lists.
Consider the following demo based upon your code (with changes made for the convenience and brevity of running on the main thread and the avoidance of handling date conversion)
i.e. suspend removed, columns with type Date changed to String
So Todo is :-
#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 : String /* Date = Date()*/,
var time : String /* Date = Date()*/,
var todoCheckBox : Boolean = false
)
and TodoDao is :-
#Dao
interface TodoDao {
// 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("SELECT * FROM todo_table")
fun readAllData() : /*LiveData<*/List<Todo>/*>*/
#Query("DELETE FROM todo_table WHERE todoCheckBox = 1")
/*suspend*/ fun deleteSelectedTasks()
// second method - This is the second method where I tried passing in an array
#Delete
/*suspend*/ fun deleteSelectedTasks(todoList : Array<Todo>)
// Method in use
#Query("DELETE FROM todo_table")
/*suspend*/ fun deleteAllTasks()
}
The using the following code:-
val t1 = Todo(0,"Todo1","A","B",true)
val t2 = Todo(0,"Todo2","C","D", false)
val t3 = Todo(0,"Todo3","E","F")
val t4 = Todo(100,"Todo4","G","H",true)
var t5 = Todo(101,"Todo5","I","J")
todoDao.addTodo(t1)
todoDao.addTodo(t2)
todoDao.addTodo(t3)
todoDao.addTodo(t4)
todoDao.addTodo(t5)
t5.title = "Something Else"
t5.date = "Y"
t5.title = "Z"
t5.todoCheckBox = true
todoDao.deleteSelectedTasks(arrayOf(t1,t2,t3,t4,t5))
Results in (via App Inspection) :-
You may notice that only Todo4 and Todo5 have been deleted. The other 3 have not. This is actually intended to demonstrate how #Delete works.
The reason why only Todo4 and Todo5 have been deleted is that t4 and t5 have the actual id hard-coded. That is #Delete works by deleting according to the Primary Key (the id in your case) the other values are irrelevant (as is shown by changing all the other t5 values)
As proof that t4 and t5 were added (as you use autogenerate=true) then sqlite_sequence (the table that stores the highest used sequence (aka id)) shows 101 (t5's id) :-
Your Issue
As such your issue is that you are not setting the appropriate id value when building the list of items to be deleted. As such it will be 0 and there will be no rows with an id of 0 and hence no rows will be deleted (like t1-t3 in the demo above).
Additional as per the comment you could use:-
#Query("DELETE FROM todo_table WHERE id IN (:idList)")
fun deleteByIdList(idList: List<Long>)
this is effectively what Room builds and runs (I believe) the id's being obtained from the passed List/Array of Todo's.
Without the id's (you may not have them in the layout (perhaps have them hidden in layout)) you could use BUT ONLY IF ALL OTHER VALUES COMBINED WILL WITHOUT DOUBT BE UNIQUE use the following combination of functions:-
/* Note that this Query/Function is intended to be used by the following deleteByTodoListValues function */
#Query("DELETE FROM todo_table WHERE title=:title AND date=:date AND time=:time AND todoCheckBox=:todoCheckBox")
fun deleteByValuesOtherThanId(title: String, date: String, time: String, todoCheckBox: Boolean)
#Transaction /* Transaction so that all deletes are done in a single transaction */
#Query("") /* I believe this fools Room into accepting the transaction processing */
fun deleteByTodoListValues(todoList: Array<Todo>) {
for (t in todoList) {
deleteByValuesOtherThanId(t.title,t.date,t.time,t.todoCheckBox)
}
}
A transaction equates to a disk write without #Transaction then each delete would be a disk write
So after following Mike T's advise in the comments and doing some other research. I was able to find a solution but heavily thanks to Mike. Turns out there is no need for an Adapter, since the fresh list of todos were gotten from the LiveData in the ListFragment, we could just do it from there directly. So here it is...
ListFragment
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.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.TodoViewModel
import com.bignerdranch.android.to_dolist.model.Todo
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 todoList = 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.
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.readAllData.observe(viewLifecycleOwner) { todos ->
adapter.setData(todos)
todoList = 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)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when(item.itemId) {
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 = todoList.filter { it.todoCheckBox }
builder.setPositiveButton("Yes") {_,_->
finishedTodos.forEach { todos ->
mTodoViewModel.deleteSelectedTasks(todos.id.toLong())
}
Toast.makeText(requireContext(), "Task 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
}
}
Then in our DAO, we use obtain the todos by their ids according to Mike.
import androidx.lifecycle.LiveData
import androidx.room.*
import com.bignerdranch.android.to_dolist.model.Todo
/**
* 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 {
// 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("SELECT * FROM todo_table ORDER BY id ASC")
fun readAllData() : LiveData<List<Todo>>
#Query("DELETE FROM todo_table WHERE id IN (:idList)")
suspend fun deleteSelectedTasks(idList : Long)
#Query("DELETE FROM todo_table")
suspend fun deleteAllTasks()
}
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 have a recyclerview in my app and i wanted to make it work with Android paging3 library.Its kind of working but not the way i wanted. Here when i open the list, the number of data that i defined in PagingConfig.pageSize creating at the top. And when i keep contiune to scrool up it never stop like an infinite loop. But when i scroll down its working fine like this;
as you see scrolling up is always enabled.
I don't know what's the problem here my codes;
MyAdapter;
package com.tolgahantutar.bexworkfloww.ui.addressbook
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.Navigation
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.tolgahantutar.bexworkfloww.R
import com.tolgahantutar.bexworkfloww.data.models.userget.UserGetValue
import kotlinx.android.synthetic.main.contacts_layout.view.*
class UserAdapter() :
PagingDataAdapter<UserGetValue, UserAdapter.UserViewHolder>(DiffCallBack()) {
class DiffCallBack : DiffUtil.ItemCallback<UserGetValue>(){
override fun areItemsTheSame(oldItem: UserGetValue, newItem: UserGetValue): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: UserGetValue, newItem: UserGetValue): Boolean {
return oldItem.name == newItem.name
}
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.itemView.contac_name_textview.text =getItem(position)!!.name
holder.itemView.user_profile_text_left_drawable.setText("${(getItem(position)!!.name.substringBefore(" ")).subSequence(0,1)}"+"${(getItem(position)!!.name.substringAfter(" ").subSequence(0,1))}")
holder.itemView.contact_info_text.text = getItem(position)!!.title
var getContactValue = getItem(position)!!
holder.itemView.setOnClickListener {
val action = AdressBookFragmentDirections.actionUserDetail(getItem(position)!!)
Navigation.findNavController(it).navigate(action)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
return UserViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.contacts_layout, parent, false)
)
}
class UserViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
}
}
PagingSource;
import androidx.paging.PagingSource
import com.tolgahantutar.bexworkfloww.data.models.userget.UserGetValue
import com.tolgahantutar.bexworkfloww.data.network.apis.WorkFlowServicesApi
class UserRemotePagingSource(private val api : WorkFlowServicesApi): PagingSource<Int,UserGetValue>(){
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UserGetValue> {
return try {
val page = params.key?: 1
val response = api.userGet(2,page,2)
LoadResult.Page(
data = response.body()!!.userGetValue,
prevKey = page-1,
nextKey = page+1
)
}catch (e: Exception){
LoadResult.Error(e)
}
}
}
ViewModel;
fun usersPagingFlow(): Flow<PagingData<UserGetValue>>{
return Pager(
config = PagingConfig(
pageSize = 2,
prefetchDistance = 2,
enablePlaceholders = true,
initialLoadSize = 2*3
)
){
UserRemotePagingSource(api)
}.flow
}
And Fragment ;
recycler_view_contacts.setHasFixedSize(true)
recycler_view_contacts.adapter = pagingDataAdapter
recycler_view_contacts.layoutManager = LinearLayoutManager(requireContext())
lifecycleScope.launch {
addressBookViewModel.usersPagingFlow().collectLatest { pagingData ->
pagingDataAdapter.submitData(pagingData)
}
}
In your PagingSource implementation, it looks like you need to check when your key is 1, then set prevKey to null to mark the end of the list.
It kind of looks like what's happening when you give your api a negative number, is that it just loads the first page again.