Initialize viewHolder in Fragment - android

I want to add editing tasks through dialogs in my app. The thing is I can't initialize viewHolder to access the bindingAdapterPosition. I need it to pass the data and update the viewModel. Tried adding it in the constructor - didn't work. I know I have to initialize the viewHolder, but don't know how.
RecyclerviewFragment.kt:
class RecyclerviewFragment : Fragment() {
private lateinit var mUserViewModel: UserViewModel
private lateinit var viewHolder: ViewHolder
private lateinit var adapter: ListAdapter
private var _binding: FragmentRecyclerviewBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentRecyclerviewBinding.inflate(inflater, container, false)
mUserViewModel = ViewModelProvider(this)[UserViewModel::class.java]
adapter = ListAdapter{showUpdateDialog()}
val adapter = ListAdapter{showUpdateDialog()}
val recyclerView = binding.recyclerView
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(requireContext())
// Creates a controller responsible for swiping and moving the views in recyclerview
val itemTouchController = ItemTouchHelper(
object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: ViewHolder, target: ViewHolder,
): Boolean {
// Move specific item from "fromPos" to "toPos" in recyclerview adapter
val fromPos = viewHolder.bindingAdapterPosition
val toPos = target.bindingAdapterPosition
adapter.notifyItemMoved(fromPos, toPos)
return true // true if moved, false otherwise
}
override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
mUserViewModel.deleteUser(adapter.getTaskPosition(viewHolder.bindingAdapterPosition))
Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show()
adapter.notifyItemRemoved(viewHolder.bindingAdapterPosition)
}
})
itemTouchController.attachToRecyclerView(binding.recyclerView)
mUserViewModel.readAllData.observe(viewLifecycleOwner) { user ->
adapter.setData(user)
}
return binding.root
}
private fun updateItemInDatabase(dialog: DialogInterface) {
val editText = (dialog as AlertDialog).findViewById<EditText>(R.id.editTextDialog)
val task = editText?.text.toString()
if(inputCheck(task)) {
// Update an entity
mUserViewModel.updateUser(adapter.getTaskPosition(viewHolder.bindingAdapterPosition))
Toast.makeText(context, "Task updated", Toast.LENGTH_SHORT).show()
}
else {
Toast.makeText(context, "Please fill out required fields", Toast.LENGTH_SHORT).show()
}
}
private fun inputCheck(task: String): Boolean {
return !(TextUtils.isEmpty(task))
}
private fun showUpdateDialog() {
MaterialAlertDialogBuilder(requireContext())
.setView(R.layout.fragment_add)
.setNegativeButton(getString(R.string.cancel)) { _, _ ->
// Respond to negative button press
Toast.makeText(context, getString(R.string.cancelled), Toast.LENGTH_SHORT).show()
}
.setPositiveButton(getString(R.string.ok)) { dialogInterface, _ ->
// Respond to positive button press
updateItemInDatabase(dialogInterface)
}
.show()
}
}
Edit:
class RecyclerviewFragment : Fragment() {
private lateinit var mUserViewModel: UserViewModel
private lateinit var adapter: ListAdapter
private var _binding: FragmentRecyclerviewBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentRecyclerviewBinding.inflate(inflater, container, false)
mUserViewModel = ViewModelProvider(this)[UserViewModel::class.java]
adapter = ListAdapter{ user -> showUpdateDialog(user)}
val recyclerView = binding.recyclerView
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(requireContext())
// Creates a controller responsible for swiping and moving the views in recyclerview
val itemTouchController = ItemTouchHelper(
object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: ViewHolder, target: ViewHolder,
): Boolean {
// Move specific item from "fromPos" to "toPos" in recyclerview adapter
val fromPos = viewHolder.bindingAdapterPosition
val toPos = target.bindingAdapterPosition
adapter.notifyItemMoved(fromPos, toPos)
return true // true if moved, false otherwise
}
override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
mUserViewModel.deleteUser(adapter.getTaskPosition(viewHolder.bindingAdapterPosition))
Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show()
adapter.notifyItemRemoved(viewHolder.bindingAdapterPosition)
}
})
itemTouchController.attachToRecyclerView(binding.recyclerView)
mUserViewModel.readAllData.observe(viewLifecycleOwner) { user ->
adapter.setData(user)
}
return binding.root
}
private fun updateItemInDatabase(user: User) {
val editText = view?.findViewById<EditText>(R.id.editTextDialog)
val task = editText?.text.toString()
if(inputCheck(task)) {
// Update an entity
mUserViewModel.updateUser(user)
Toast.makeText(context, "Task updated", Toast.LENGTH_SHORT).show()
}
else {
Toast.makeText(context, "Please fill out required fields", Toast.LENGTH_SHORT).show()
}
}
private fun inputCheck(task: String): Boolean {
return !(TextUtils.isEmpty(task))
}
private fun showUpdateDialog(user: User) {
MaterialAlertDialogBuilder(requireContext())
.setView(R.layout.fragment_add)
.setNegativeButton(getString(R.string.cancel)) { _, _ ->
// Respond to negative button press
Toast.makeText(context, getString(R.string.cancelled), Toast.LENGTH_SHORT).show()
}
.setPositiveButton(getString(R.string.ok)) { _, _ ->
// Respond to positive button press
val taskText = view
?.findViewById<EditText>(R.id.editTextDialog)
?.text?.toString()
updateItemInDatabase(user)
}
.show()
}
}
The app doesn't crash when you press ok in the updateDialog anymore, but it doesn't really update the database or the recyclerview items. The cause is that I can't figure out how to update it as I made the list adapter return the whole user(id, task) and don't know how to update only the task. Adding some adapter code to let it explain it by itself.
class ListAdapter(var imageListener:(user: User)->Unit) : RecyclerView.Adapter<ListAdapter.MyViewHolder>() {
...
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val currentItem = dataSet[position]
holder.taskTitle.text = currentItem.task
holder.editImage.setOnClickListener {
imageListener(getTaskPosition(position))
}
holder.notificationImage.setOnClickListener {
val action = RecyclerviewFragmentDirections.actionRecyclerFragmentToNotificationFragment()
holder.itemView.findNavController().navigate(action)
}
}
fun getTaskPosition(position: Int): User {
return dataSet[position]
}
I get the idea and seem to understand the problem more. Now I see that I didn't use the whole potential of passing the data from adapter, but there is still an issue, if you could guide me through it I would be honored :))
Edit 2:
class RecyclerviewFragment : Fragment() {
private lateinit var mUserViewModel: UserViewModel
private lateinit var adapter: ListAdapter
private var _binding: FragmentRecyclerviewBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentRecyclerviewBinding.inflate(inflater, container, false)
mUserViewModel = ViewModelProvider(this)[UserViewModel::class.java]
adapter = ListAdapter{ user -> showUpdateDialog(user)}
val recyclerView = binding.recyclerView
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(requireContext())
// Creates a controller responsible for swiping and moving the views in recyclerview
val itemTouchController = ItemTouchHelper(
object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: ViewHolder, target: ViewHolder,
): Boolean {
// Move specific item from "fromPos" to "toPos" in recyclerview adapter
val fromPos = viewHolder.bindingAdapterPosition
val toPos = target.bindingAdapterPosition
adapter.notifyItemMoved(fromPos, toPos)
return true // true if moved, false otherwise
}
override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
mUserViewModel.deleteUser(adapter.getTaskPosition(viewHolder.bindingAdapterPosition))
Toast.makeText(context, "Task deleted", Toast.LENGTH_SHORT).show()
adapter.notifyItemRemoved(viewHolder.bindingAdapterPosition)
}
})
itemTouchController.attachToRecyclerView(binding.recyclerView)
mUserViewModel.readAllData.observe(viewLifecycleOwner) { user ->
adapter.setData(user)
}
return binding.root
}
#SuppressLint("NotifyDataSetChanged")
private fun updateItemInDatabase(user: User) {
val editText = view?.findViewById<EditText>(R.id.editTextDialog)
val task = editText?.text.toString()
if(inputCheck(task)) {
// Update an entity
mUserViewModel.updateUser(user)
Toast.makeText(context, "Task updated", Toast.LENGTH_SHORT).show()
adapter.notifyDataSetChanged()
}
else {
Toast.makeText(context, "Please fill out required fields", Toast.LENGTH_SHORT).show()
}
}
private fun inputCheck(task: String): Boolean {
return !(TextUtils.isEmpty(task))
}
private fun showUpdateDialog(user: User) {
MaterialAlertDialogBuilder(requireContext())
.setView(R.layout.fragment_add)
.setNegativeButton(getString(R.string.cancel)) { _, _ ->
// Respond to negative button press
Toast.makeText(context, getString(R.string.cancelled), Toast.LENGTH_SHORT).show()
}
.setPositiveButton(getString(R.string.ok)) { _, _ ->
// Respond to positive button press
val taskText = view
?.findViewById<EditText>(R.id.editTextDialog)
?.text?.toString()
updateItemInDatabase(User(user.id, taskText.toString()))
}
.show()
}
}
With this code it seems like the function is working, however, it can't access the taskText value? If I try to edit any of the tasks in the emulator it updates to "null" Providing the ViewModel, but I don't think there is an issue there. It is probably rooted somewhere in the value itself.
UserViewModel.kt:
class UserViewModel(application: Application) : AndroidViewModel(application) {
val readAllData: LiveData<List<User>>
private val repository: UserRepository
init {
val userDao = UserDatabase.getDatabase(application).userDao()
repository = UserRepository(userDao)
readAllData = repository.readAllData
}
fun addUser(user: User) {
viewModelScope.launch(Dispatchers.IO) {
repository.addUser(user)
}
}
fun updateUser(user: User) {
viewModelScope.launch(Dispatchers.IO) {
repository.updateUser(user)
}
}
fun deleteUser(user: User) {
viewModelScope.launch(Dispatchers.IO) {
repository.deleteUser(user)
}
}
}

Just as a quick sketch so you see what I'm talking about:
// your adapter already takes a callback function - make it send some data
// about the item being clicked. I'm assuming it's an ID here but you could
// pass back a specific object too
class ListAdapter(
private val onDeleteListener: (itemId: Int) -> Unit
) ...
In the Fragment
// onCreate
adapter = ListAdapter { itemId -> showUpdateDialog(itemId) }
// dialog function should take the ID as a parameter
private fun showUpdateDialog(itemId: Int) {
...
.setPositiveButton(getString(R.string.ok)) { dialogInterface, _ ->
// Don't send the dialog interface - pass the actual data you want to use
val taskText = (dialogInterface as AlertDialog)
.findViewById<EditText>(R.id.editTextDialog)
?.text?.toString()
updateItemInDatabase(itemId, taskText)
}
...
}
// Your update function acts on specific data - it has no knowledge of how the rest
// of the app is implemented, it's not hardwired into other components etc
private fun updateItemInDatabase(itemId: Int, task: String?) {
if(inputCheck(task)) {
mUserViewModel.updateUser(itemId)
}
}
See how much simpler that is? You have a clear direction of data flow, where the Adapter hands a specific piece of data to its callback function, which passes it to the confirmation dialog, which passes it to the update function which needs that specific piece of data. The only involvement the Adapter has is saying "hey, an item was clicked, here's the info". You don't need to go asking for more details later, like "so hey what are you currently displaying" - that was passed as part of the event's data.
That's cleaner in general, but especially with RecyclerViews you don't want to be poking at their internals, keeping references to ViewHolders etc, because that state is volatile. The way they work is by reusing those objects to display different data, so keeping long-running references to them assuming they're displaying a particular item is asking for trouble. It probably doesn't matter so much here (if you tap an item to get a dialog the user probably can't get it to scroll to another position) but it's better to not do that thing at all.
also btw, this is a bug:
// top-level variable
adapter = ListAdapter{showUpdateDialog()}
// local variable
val adapter = ListAdapter{showUpdateDialog()}
// local variable
recyclerView.adapter = adapter
You're creating two separate instances of your ListAdapter - one is stored long-term, the other is the one you actually set on your RecyclerView. The long-term one is what you're accessing in updateItemInDatabase, the one that's not actually being used by a RecyclerView, so it's not the thing that was actually being clicked (and it won't have any ViewHolders yet either). This is why it's better to just pass data in one direction if you can, less chance of complications being introduced!

Related

Adding a single choice alert dialog into a recycler view

I am new to alert dialogs and was hoping somebody could help me with this. I want to develop a single choice alert dialog and have it show in a recyclerview textview along side an incremental counter.
I have searched all types of documentation but all I can find is how to display the single choice item in either a Toast or a single text view.
I know the code I have is incorrect, but after numerous other attempts, this is the closest I got to getting the result I am seeking. I was able to get it to set the most recent choice but then the other choices change into what look like memory allocations after the button is pressed.
Screenshot:
Here is my code:
Main Activity (I realize that tv_choice.setText(multiItems[i]) is part of the problem it in my dialogAlert(). This is what I need help with.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val itemsList = generateItemsList()
private val adapter = MyAdapter(itemsList)
var count = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.setHasFixedSize(false)
binding.button.setOnClickListener {
addItemsToRecyclerView()
}
}
private fun generateItemsList(): ArrayList<Items> {
return ArrayList()
}
fun addItemsToRecyclerView() {
val addItems = Items(getCount(), "Your Choice Is:", dialogAlert())
itemsList.add(addItems)
adapter.notifyItemInserted(itemsList.size - 1)
}
private fun getCount(): String {
count += 1
return count.toString()
}
fun dialogAlert(): String {
val multiItems = arrayOf("Item 1", "Item 2", "Item 3")
val singleChoice = AlertDialog.Builder(this)
.setTitle("Choose one:")
.setSingleChoiceItems(multiItems, 1) { dialogInterface, i ->
tv_choice.setText(multiItems[i])
}
.setPositiveButton("ok") { _, _ ->
}
.create()
singleChoice.show()
val singleChoiceString = singleChoice.toString()
return singleChoiceString
}
}
The Adapter:
class MyAdapter(private val rvDisplay: MutableList<Items>) : RecyclerView
.Adapter<MyAdapter.AdapterViewHolder>(){
class AdapterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val textView1: TextView = itemView.findViewById(R.id.tv_count)
val textView2: TextView = itemView.findViewById(R.id.tv_choice_string)
val textView3: TextView = itemView.findViewById(R.id.tv_choice)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AdapterViewHolder {
val myItemView = LayoutInflater.from(parent.context).inflate(
R.layout.rv_items,
parent, false
)
return AdapterViewHolder(myItemView)
}
override fun onBindViewHolder(holder: MyAdapter.AdapterViewHolder, position: Int) {
val currentDisplay = rvDisplay[position]
holder.itemView.apply {
holder.textView1.text = currentDisplay.count
holder.textView2.text = currentDisplay.choiceString
holder.textView3.text = currentDisplay.choice
}
}
override fun getItemCount() = rvDisplay.size
}
While you tried to add the dialog's selected value to the recycler view, what you actually did was adding the dialogAlert() returned value to the recycler view.
Instead of "adding" an item when the button is clicked, you should add the item once the dialog is closed. So first present the dialog:
binding.button.setOnClickListener {
dialogAlert()
}
Remove the return value from dialogAlert() method and then, when selecting an option from the dialog, add it to the recycler view:
fun dialogAlert() {
val multiItems = arrayOf("Item 1", "Item 2", "Item 3")
val singleChoice = AlertDialog.Builder(this)
.setTitle("Choose one:")
.setSingleChoiceItems(multiItems, 1) { dialogInterface, i ->
addItemsToRecyclerView(multiItems[i])
}
.create()
singleChoice.show()
}
Change the method to receive a String (your item):
fun addItemsToRecyclerView(item: String) {
val addItems = Items(getCount(), "Your Choice Is:", item)
itemsList.add(addItems)
adapter.notifyItemInserted(itemsList.size - 1)
}
Note that I did not run this code so it might need some adjustments.

Layout doesn't update right away after deleting last item from Room database

I have this fragment in which I store my 'favorite items' and I can delete them when I click on a button if I want to. The implementation works well until I get to the last item and it doesn't disappear unless I go to another fragment and then come back (as in, the item is deleted but the recycler view still shows it unless I update the fragment myself).
How can I make the last item disappear right away? Setting notifyDataSetChanged() after the deleteHandler in the adapter does not seem to work.
This is the fragment where I have the items:
class FavoritesFragment : Fragment() {
private val mfavoriteViewModel by viewModels<FavoriteViewModel>()
private lateinit var binding: FragmentFavoritesBinding
private val deleteHandler: (Favorites) -> Unit = {
mfavoriteViewModel.deleteFavorite(it)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding = FragmentFavoritesBinding.inflate(layoutInflater)
//recyclerview
val adapter = FavoritesAdapter(deleteHandler)
binding.rvFavList.layoutManager = LinearLayoutManager(context)
binding.rvFavList.adapter = adapter
//favoriteViewModel
mfavoriteViewModel.readAllData.observe(viewLifecycleOwner, { favorite ->
if (favorite.isEmpty()) {
binding.emptyState.text = getString(R.string.emptyState)
binding.emptyState.visibility = View.VISIBLE
} else {
adapter.setData(favorite)
binding.emptyState.visibility = View.GONE
}
})
return binding.root
}
}
The adapter:
class FavoritesAdapter(val deleteHandler: (Favorites) -> Unit) :
RecyclerView.Adapter<FavoritesAdapter.ViewHolder>() {
private var favoriteList = emptyList<Favorites>()
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val binding = FavItemBinding.bind(itemView)
val favTitle: TextView = binding.tvFavsTitle
val favItem: ImageButton = binding.btnFavs
val favImg: ImageView = binding.ivFavs
fun bind(favorites: Favorites) {
Picasso.get().load(favorites.image).into(favImg)
favTitle.text = favorites.title
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.fav_item, parent, false)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(favoriteList[position])
//delete favorite item
holder.favItem.setOnClickListener {
deleteHandler(favoriteList[position])
}
}
override fun getItemCount(): Int {
return favoriteList.size
}
fun setData(favorite: List<Favorites>) {
this.favoriteList = favorite
notifyDataSetChanged()
}
}
This is the favorite's viewmodel:
class FavoriteViewModel(application: Application) : AndroidViewModel(application) {
val readAllData: LiveData<List<Favorites>>
private val repository: FavoritesRepository
init {
val favoriteDao = FavoriteDatabase.getDatabase(application).favoriteDao()
repository = FavoritesRepository(favoriteDao)
readAllData = repository.readAllData
}
fun addFavorite(favorite: Favorites) {
viewModelScope.launch(Dispatchers.IO) {
repository.addFavorite(favorite)
}
}
fun deleteFavorite(favorite: Favorites) {
viewModelScope.launch(Dispatchers.IO) {
repository.deleteFavorite(favorite)
}
}
fun deleteAllFavorites() {
viewModelScope.launch(Dispatchers.IO) {
repository.deleteAllFavorites()
}
}
}
Here in your observer:
mfavoriteViewModel.readAllData.observe(viewLifecycleOwner, { favorite ->
if (favorite.isEmpty()) {
binding.emptyState.text = getString(R.string.emptyState)
binding.emptyState.visibility = View.VISIBLE
} else {
adapter.setData(favorite)
binding.emptyState.visibility = View.GONE
}
})
When the list goes from one item to zero items, in the if block you show an empty message, but you fail to update the adapter data or hide the RecyclerView so it will continue to show what it did before. You should move the adapter.setData(favorite) outside the if/else.
Clear your favourites list before setting the new items in it. You can do this in your setData() function. Like this,
fun setData(favorite: List<Favorites>) {
if (favouriteList.isNotEmpty()) {
favouriteList.clear()
}
this.favoriteList = favorite
notifyDataSetChanged()
}

Need to bind Adapter to RecyclerView twice for data to appear

I have an Android app where I bind a list of service to a RecyclerView as such:
fragment.kt
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
mBinding = FragmentAllServicesBinding.inflate(inflater, container, false)
mViewModel = ViewModelProvider(this).get(AllServicesViewModel::class.java)
binding.viewModel = viewModel
binding.lifecycleOwner = this
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribeServices()
}
// Private Functions
private fun subscribeServices(){
val adapter = ServiceAdapter()
binding.RecyclerViewServices.apply {
/*
* State that layout size will not change for better performance
*/
setHasFixedSize(true)
/* Bind the layout manager */
layoutManager = LinearLayoutManager(requireContext())
this.adapter = adapter
}
viewModel.services.observe(viewLifecycleOwner, { services ->
if(services != null){
lifecycleScope.launch {
adapter.submitList(services)
}
}
})
}
viewmodel.kt
package com.th3pl4gu3.mes.ui.main.all_services
import android.app.Application
import androidx.lifecycle.*
import com.th3pl4gu3.mes.api.ApiRepository
import com.th3pl4gu3.mes.api.Service
import com.th3pl4gu3.mes.ui.utils.extensions.lowercase
import kotlinx.coroutines.launch
import kotlin.collections.ArrayList
class AllServicesViewModel(application: Application) : AndroidViewModel(application) {
// Private Variables
private val mServices = MutableLiveData<List<Service>>()
private val mMessage = MutableLiveData<String>()
private val mLoading = MutableLiveData(true)
private var mSearchQuery = MutableLiveData<String>()
private var mRawServices = ArrayList<Service>()
// Properties
val message: LiveData<String>
get() = mMessage
val loading: LiveData<Boolean>
get() = mLoading
val services: LiveData<List<Service>> = Transformations.switchMap(mSearchQuery) { query ->
if (query.isEmpty()) {
mServices.value = mRawServices
} else {
mServices.value = mRawServices.filter {
it.name.lowercase().contains(query.lowercase()) ||
it.identifier.lowercase().contains(query.lowercase()) ||
it.type.lowercase().contains(query.lowercase())
}
}
mServices
}
init {
loadServices()
}
// Functions
internal fun loadServices() {
// Set loading to true to
// notify the fragment that loading
// has started and to show loading animation
mLoading.value = true
viewModelScope.launch {
//TODO("Ensure connected to internet first")
val response = ApiRepository.getInstance().getServices()
if (response.success) {
// Bind raw services
mRawServices = ArrayList(response.services)
// Set the default search string
mSearchQuery.value = ""
} else {
mMessage.value = response.message
}
}.invokeOnCompletion {
// Set loading to false to
// notify the fragment that loading
// has completed and to hide loading animation
mLoading.value = false
}
}
internal fun search(query: String) {
mSearchQuery.value = query
}
}
ServiceAdapter.kt
class ServiceAdapter : ListAdapter<Service, ServiceViewHolder>(
diffCallback
) {
companion object {
private val diffCallback = object : DiffUtil.ItemCallback<Service>() {
override fun areItemsTheSame(oldItem: Service, newItem: Service): Boolean {
return oldItem.identifier == newItem.identifier
}
override fun areContentsTheSame(oldItem: Service, newItem: Service): Boolean {
return oldItem == newItem
}
}
}
override fun onBindViewHolder(holder: ServiceViewHolder, position: Int) {
holder.bind(
getItem(position)
)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ServiceViewHolder {
return ServiceViewHolder.from(
parent
)
}
}
ServiceViewHolder.kt
class ServiceViewHolder private constructor(val binding: CustomRecyclerviewServiceBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
service: Service?
) {
binding.service = service
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ServiceViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding =
CustomRecyclerviewServiceBinding.inflate(layoutInflater, parent, false)
return ServiceViewHolder(
binding
)
}
}
}
The problem here is that, the data won't show on the screen.
For some reasons, if i change my fragment's code to this:
viewModel.services.observe(viewLifecycleOwner, { services ->
if(services != null){
lifecycleScope.launch {
adapter.submitList(services)
// Add this code
binding.RecyclerViewServices.adapter = adapter
}
}
})
Then the data shows up on the screen.
Does anyone have any idea why I need to set the adapter twice for this to work ?
I have another app where I didn't have to set it twice, and it worked. For some reason, this app is not working. (The only difference between the other app and this one is that this one fetches the data from an API whereas the other one fetches data from Room (SQLite) database)
Inside
binding.RecyclerViewServices.apply {
...
}
Change this.adapter = adapter to this.adapter = this#YourFragmentName.adapter
The reason is, you named your Adapter variable "adapter" which conflicts the property name of RecyclerView.adapter. You are actually not setting the adapter for the first time. It's very sneaky, because lint doesn't give any warning and code compiles with no errors...
Or you could rename your "adapter" variable in your fragment to something like "servicesAdapter" an shortly use
binding.RecyclerViewServices.apply {
adapter = servicesAdapter
}
Instead of adding the adapter again try calling adapter.notifyDataSetChanged() after adapter.submitList(services)

How to update cart value 'in real time' in Cart Fragment(Android)

I am working on an Ecommerce app. I have come to the first problem I don't know how to tackle, I want to update Cart Value without having to reenter Cart Fragment so if somebody removes a product from the Cart, Cart Value should go down immediately straight away. I have tried several different approaches but none seem to be working.
Users add products to cart in Product Detail fragment -> User goes to cart and sees products in cart via Recycler View -> I have a field with Total Price that is not a part of a recycler. This field does not update when I remove products from cart and I know it cannot do it as of now, cannot figure out how to do it.
I'm using Firebase Cloud to get User and Product data.
Cart Fragment
class CartFragment : RootFragment(), OnProductClick {
private val cartViewModel by viewModels<CartFragmentViewModel>()
private lateinit var binding: FragmentCartBinding
private val adapter = CartAdapter(this)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_cart,
container,
false
)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerCart.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerCart.adapter = adapter
binding.buttonToCheckout.setOnClickListener {
navigateToCheckout()
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
cartViewModel.userCart.observe(viewLifecycleOwner, { list ->
adapter.setCartProducts(list)
val cartQuantity = list.size
binding.textCartQuantityValue.text = cartQuantity.toString()
val cartValue = cartViewModel.calculateCartValue(list)
binding.textCartTotalValue.text = cartValue.toString()
})
}
// TODO
override fun onProductClick(product: Product, position: Int) {
cartViewModel.removeFromCart(product)
adapter.removeFromCart(product, position)
}
}
Cart View Model
class CartFragmentViewModel : ViewModel() {
private val repository = FirebaseCloud()
private val user = repository.getUserData()
val userCart = user.switchMap {
repository.getProductsFromCart(it.cart)
}
fun calculateCartValue(list: List<Product>): Long {
var cartValue = 0L
if (list.isNotEmpty()) {
for (product in list) {
cartValue += product.price!!
}
}
return cartValue
}
fun removeFromCart(product: Product) {
repository.removeFromCart(product)
}
}
Cart Adapter
class CartAdapter(private val listener: OnProductClick) : RecyclerView.Adapter<CartAdapter.CartViewHolder>() {
private val cartList = ArrayList<Product>()
fun setCartProducts(list: List<Product>) {
cartList.clear()
cartList.addAll(list)
notifyDataSetChanged()
}
fun removeFromCart(product: Product, position: Int) {
cartList.remove(product)
notifyItemRemoved(position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.list_row_cart, parent, false)
return CartViewHolder(view)
}
override fun onBindViewHolder(holder: CartViewHolder, position: Int) {
bindCartData(holder)
}
private fun bindCartData(holder: CartViewHolder) {
val name = holder.itemView.findViewById<TextView>(R.id.text_product_name_cart)
val price = holder.itemView.findViewById<TextView>(R.id.text_product_price_cart)
val image = holder.itemView.findViewById<ImageView>(R.id.image_product_image_cart)
name.text = cartList[holder.adapterPosition].name
price.text = cartList[holder.adapterPosition].price.toString()
Glide.with(holder.itemView)
.load(cartList[holder.adapterPosition].imageUrl)
.into(image)
}
override fun getItemCount(): Int {
return cartList.size
}
inner class CartViewHolder(view: View) : RecyclerView.ViewHolder(view) {
init {
view.findViewById<ImageView>(R.id.button_remove_from_cart)
.setOnClickListener{
listener.onProductClick(cartList[adapterPosition], adapterPosition)
}
}
}
}
I managed to make it work with not so elegant way and if someone has tips how to use the observer I already have observer in Cart Fragment it would be awesome.
What I've done is I updated onProductClick() in Cart Fragment to recalculate the value once again.
override fun onProductClick(product: Product, position: Int) {
cartViewModel.removeFromCart(product)
adapter.removeFromCart(product, position)
val productsInCart = adapter.cartList
val cartValue = cartViewModel.calculateCartValue(productsInCart)
binding.textCartTotalValue.text = cartValue.toString()
binding.textCartQuantityValue.text = productsInCart.size.toString()
}

Android Adapter redrawing view with all the same original items rather than removing selected item

I've been trying to delete an item from my list so that it updates without the removed item, but the list seems to redraw itself and keeps displaying all the original items as before. For a short bit of time it's possible to see the item as if it's being removed, however, due to this redrawing everything gets back to what it was before the removal.
I've tried several combinations of the following methods but none of them seem to work in this case.
adapter.notifyItemRangeChanged(position, adapter.itemCount)
adapter.notifyItemRemoved(position)
adapter.notifyItemChanged(position)
adapter.notifyDataSetChanged()
These are my files. Please notice I'm using the Groupie library as a replacement for the default RecyclerView.
class RecyclerProductItem(
private val activity: MainActivity,
private val product: Product,
private val onItemClickListener: OnItemClickListener?
) : Item<GroupieViewHolder>() {
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.apply {
with(viewHolder.itemView) {
ivTrash.setOnClickListener {
if (onItemClickListener != null) {
Toast.makeText(context, "delete method to be added here", Toast.LENGTH_SHORT).show()
onItemClickListener.onClick(viewHolder.adapterPosition)
// deleteProduct(product.id)
}
}
}
}
}
interface OnItemClickListener {
fun onClick(position: Int) //pass your object types.
}
override fun getLayout() = R.layout.recyclerview_item_row
}
And here my fragment:
class ProductsListFragment : Fragment() {
private lateinit var adapter: GroupAdapter<GroupieViewHolder>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_products_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val linearLayoutManager = LinearLayoutManager(activity)
recyclerView.layoutManager = linearLayoutManager
adapter = GroupAdapter()
recyclerView.adapter = adapter
loadProducts()
}
/**
* API calls
*/
private fun loadProducts() {
GetProductsAPI.postData(object : GetProductsAPI.ThisCallback,
RecyclerProductItem.OnItemClickListener {
override fun onSuccess(productList: List<JsonObject>) {
Log.i(LOG_TAG, "successful network call")
for (jo in productList) {
val gson = GsonBuilder().setPrettyPrinting().create()
val product: Product =
gson.fromJson(jo, Product::class.java)
adapter.add(
RecyclerProductItem(
activity as MainActivity,
Product(
product.id,
product.title,
product.description,
product.price
), this
)
)
}
}
override fun onClick(position: Int) {
Log.i(LOG_TAG, position.toString())
adapter.notifyItemRangeChanged(position,
adapter.itemCount)
adapter.notifyItemRemoved(position)
}
})
}
}
Many thanks.
Simple sample
class GroupAdapter(private val items: MutableList<Any>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
fun removeByPosition(position: Int) {
items.removeAt(position)
notifyItemRemoved(position)
}

Categories

Resources