How to get value from ViewModel fromRecyclerView Adapter - android

I have an Android project in MVVM Structure. In that project, consist RecyclerView. This is my code.
1.MyViewModel.kt
class MyViewModel(context: Application, val myRepository: MyRepository) : AndroidViewModel(context), Observable{
... other code ...
val listUri: MutableLiveData<MutableList<Uri>> by lazy {
MutableLiveData<MutableList<Uri>>().apply {
value = mutableListOf()
}
}
... other code ...
}
2.MyAdapter.kt
class MyAdapter(var items: MutableList<Uri>, val context: Context) : RecyclerView.Adapter<ViewHolder>() {
var listItems: ArrayList<Uri> = items as ArrayList<Uri>
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(context).inflate(R.layout.pod_list_item, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.imageIcon.setImageURI(listItems[position])
holder.deleteIcon.setOnClickListener {
val viewModel: MyViewModel? = null
....THIS PART....
viewModel?.listUri?.value.removeAt(position)
....UNTIL THIS PART....
}
}
override fun getItemCount(): Int {
return editedItems.size
}
}
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
var imageIcon = view.icon_image
var deleteIcon = view.icon_delete
}
When user click the delete icon in the recyclerview item, the application suppose to delete the selected value of MutableLiveData<MutableList<Uri>> in MyViewModel which name listUri.
In my marked code above, when i debug the code, it return null value, even though the variable listUri in ViewModel have value.
So i think, my way to access the value of ViewModel from Adapter is wrong.
How to access value of ViewModel from the adapter and then manipulate it?
If can't do that, any suggestion would be nice.

you can access your ViewModel from the context which you are getting into the adapter.

I guess instead of passing the context in the constructor, pass the ViewModel instead:
class MyAdapter(var items: MutableList<Uri>, val viewModel: MyViewModel) : RecyclerView.Adapter<ViewHolder>()
Then instead of:
holder.deleteIcon.setOnClickListener {
val viewModel: MyViewModel? = null
....THIS PART....
viewModel?.listUri?.value.removeAt(position)
....UNTIL THIS PART....
}
You can do:
holder.deleteIcon.setOnClickListener {
viewModel.listUri.value?.removeAt(position)
}
I hope this helps

Related

Notifydatasetchanged, dialogs and view binding

Scenario
Recently moved from synthetics to view binding and I'm still struggling (haven't done any coding in months, so I'm rusty as it gets).
MainActivity has two recyclerviews, one displaying totals, one displaying a list of transactions. Each transaction has a "OnLongClickListener" attached to it (attached in the adapter class). This listener calls for a MaterialAlertDialog with two options: edit or delete.
There's also a separate fragment for adding transactions.
Requirement
Upon addition, deletion or modification of these transactions, both recycleradapters need to refresh the data in order to reflect the correct information on screen.
Problem
I'm not able to get the adapters to receive the "notifydatasetchanged" as I am not sure how to get the adapters' reference.
Code being used
MainActivity
private val db = FirebaseFirestore.getInstance()
private val dbSettings = firestoreSettings { isPersistenceEnabled = true }
CoroutineScope(Dispatchers.Default).launch {
val adapterRV1 = Adapter_RV1(FSDB.Get_calculations, FSDB.Get_transactions(db))
val adapterRV2 = Adapter_RV2(FSDB.Get_transactions(db))
runOnUiThread {
binding.rvCalculations.adapter = adapterRV1
binding.rvTransactions.adapter = adapterRV2
}
}
Adapter_RV1
class Adapter_RV1(val calculations_list: List<calculation>,val transactions_list: ArrayList<Transactions>) : RecyclerView.Adapter<Adapter_RV1.CalculationViewHolder>() {
private lateinit var binding: RvCalculationsLayoutBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Adapter_RV1.CalculationViewHolder {
val binding = RvCalculationsLayoutBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return CalculationViewHolder(binding)
}
override fun onBindViewHolder(holder: KPIViewHolder, position: Int) {
with(holder) {
with(calculations_list[position]) {
...
}
}
}
override fun getItemCount() = calculations_list.size
inner class CalculationViewHolder(val binding: RvCalculationsLayoutBinding) :
RecyclerView.ViewHolder(binding.root)
}
Adapter_RV2
class Adapter_RV2(var transactions_list: List<Transaction>):
RecyclerView.Adapter<Adapter_RV2.TransactionsViewHolder>() {
private lateinit var binding: RvTransactionsLayoutBinding
inner class TransactionsViewHolder(val binding: RvTransactionsLayoutBinding) : RecyclerView.ViewHolder(binding.root){
init {
binding.root.setOnLongClickListener {
val position = absoluteAdapterPosition
val item = transactions_list[position]
CoroutineScope(Dispatchers.Main).launch {
CreateDialog(item,position,binding)
}
true
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Adapter_RV2.TransactionsViewHolder {
val binding = RvTransactionsLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return TransactionsViewHolder(binding)
}
override fun onBindViewHolder(holder: TransactionsViewHolder, position: Int) {
...
}
override fun getItemCount() = transactions_list.size
}
CreateDialog
class CreateDialog (transaction: Transaccion, position: Int, binding: RvVistaTransaccionesBinding){
private suspend fun DeleteTransaction(position: Int) {
...
traRef.update("transactions", FieldValue.arrayRemove(transaction)).await()
}
private val puBinding : PopupLayoutBinding = PopupLayoutBinding.inflate(LayoutInflater.from(binding.root.context))
init {
with(puBinding){
...
CoroutineScope(Dispatchers.Main).launch {
supervisorScope {
val task = async {
DeleteTransaction(position)
}
try{
task.await()
/// This is where, I guess, adapters should be notified of dataset changes
popup.dismiss()
}
catch (e: Throwable){
crashy.recordException(e)
Log.d("Transaction",e.message!!)
popup.dismiss()
}
}
}
}
}
popup.setView(puBinding.root)
popup.show()
}
What I've tested so far
I honestly have no clue how to proceed. I've tried a few things but none work and considering I'm super green in Dev in general, View Binding is a bit more confusing than usual.
I have replaced the line below
notifyDataSetChanged();
with
binding.recycler.getAdapter().notifyDataSetChanged();
in my own code.
Here, "recyler" is my RecyclerView, and "binding" is ActivityListBinding.
I didnt read your code, maybe my tip will help you, it worked for me.

RecyclerView items changes randomly on clicking an item

I have a RecyclerView list, I want the color of the clicked item to be changed. But whenever I am clicking an item the RecyclerView items change randomly.
I have also taken a boolean in my data class to keep track of item selection.
data class
data class OrderAvailableDaysResponseItem(
val actual_date: String,
val day_name: String,
val date_name: String,
val day_num: Int,
// For item selection
var isDateSelected: Boolean)
Inside my fragment, checking if the clicked item matches the list item and updating the isDateSelected to true, then calling notifydatasetchanged.
private var availableDaysList: OrderAvailableDaysResponse = OrderAvailableDaysResponse()
orderAvailableDaysAdapter.setDateClickListener {
for(resItem in availableDaysList){
resItem.isDateSelected = resItem.date_name == it.date_name
}
orderAvailableDaysAdapter.notifyDataSetChanged()
}
Adapter clicklistener
var onDateClickListener: ((OrderAvailableDaysResponseItem) -> Unit)? = null
fun setDateClickListener(listener: (OrderAvailableDaysResponseItem) -> Unit){
onDateClickListener = listener
}
inner class AvailableDaysViewHolder(binding: ItemAvailableDaysBinding) : RecyclerView.ViewHolder(binding.root) {
fun setBindings(itemRes: OrderAvailableDaysResponseItem){
binding.resItem = itemRes
binding.executePendingBindings()
binding.clRoot.setOnClickListener {
onDateClickListener?.let {
it(itemRes)
}
}
}
}
Please refer to the attachment for a better understanding of the situation
As you can see on clicking on an item the items are changing randomly. Please help me. Am I missing something?
Edit 1:
Complete adapter code
class OrderAvailableDaysAdapter(var orderAvailableDaysResponseList: OrderAvailableDaysResponse) : RecyclerView.Adapter<OrderAvailableDaysAdapter.AvailableDaysViewHolder>() {
private lateinit var binding: ItemAvailableDaysBinding
var onDateClickListener: ((OrderAvailableDaysResponseItem) -> Unit)? = null
fun setDateClickListener(listener: (OrderAvailableDaysResponseItem) -> Unit){
onDateClickListener = listener
}
inner class AvailableDaysViewHolder(binding: ItemAvailableDaysBinding) : RecyclerView.ViewHolder(binding.root) {
fun setBindings(itemRes: OrderAvailableDaysResponseItem){
binding.resItem = itemRes
binding.executePendingBindings()
binding.clRoot.setOnClickListener {
onDateClickListener?.let {
it(itemRes)
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AvailableDaysViewHolder {
binding = ItemAvailableDaysBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return AvailableDaysViewHolder(binding)
}
override fun onBindViewHolder(holder: AvailableDaysViewHolder, position: Int) {
holder.setBindings(orderAvailableDaysResponseList[position])
}
override fun getItemCount(): Int {
return orderAvailableDaysResponseList.size
}}
remove private lateinit var binding: ItemAvailableDaysBinding from adapter, don' keep it "globally", initialisation is made only once, in onCreateViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AvailableDaysViewHolder {
ItemAvailableDaysBinding binding = ItemAvailableDaysBinding.inflate(
LayoutInflater.from(parent.context),parent,false)
return AvailableDaysViewHolder(binding)
}
same naming of this "global" object and in AvailableDaysViewHolder inner class may confuse it and setBindings may be called on lastly initialised (global kept) object rather that this passed in constructor

Kotlin Recyclerview ItemClick to new Intent

I have a problem. I can print a toast message when I click on any item in Recyclerview. But I want to switch to a different activity when clicked. Could you please help me in a very specific way? It is a very important project for me. The code blocks I wrote are as follows. Thanks in advance .
CustomersAdapter
class CustomersAdapter(private val companynameArray:ArrayList,private val companyoffArray:ArrayList, private val companyPhoneArray:ArrayList):
RecyclerView.Adapter<CustomersAdapter.CustomerHolder>() {
class CustomerHolder(view: View) : RecyclerView.ViewHolder(view) {
var recycleCompanyName: TextView?=null
var recycleOffName: TextView?=null
var recycleOffPhone: TextView?=null
init {
recycleCompanyName=view.findViewById(R.id.recycleCompanyName)
recycleOffName=view.findViewById(R.id.recycleOffName)
recycleOffPhone=view.findViewById(R.id.recycleOffPhone)
itemView.setOnClickListener {
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomerHolder {
val inflater= LayoutInflater.from(parent.context)
val view= inflater.inflate(R.layout.recycler_view_customer_row,parent,false)
return CustomersAdapter.CustomerHolder(view)
}
override fun getItemCount(): Int {
return companynameArray.size
}
override fun onBindViewHolder(holder: CustomerHolder, position: Int) {
holder.recycleCompanyName?.text="Şirket Adı:"+companynameArray[position]
holder.recycleOffName?.text="Şirket Yetkilisi :"+companyoffArray[position]
holder.recycleOffPhone?.text="İrtibat No : "+companyPhoneArray[position]
}
}
I did not specify the activity I want to go to. I would appreciate it if you could help as a method.
If it helps, I write the codes of the client class here.
class Customers : AppCompatActivity()
{
private lateinit var auth: FirebaseAuth
private lateinit var db: FirebaseFirestore
var companynameFB:ArrayList<String> = ArrayList()
var companyoffFB:ArrayList<String> = ArrayList()
var companyphoneFB:ArrayList<String> = ArrayList()
var adapter :CustomersAdapter?=null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_customers)
auth= FirebaseAuth.getInstance()
db= FirebaseFirestore.getInstance()
getDataFromFirestore()
var layoutManager= LinearLayoutManager(this)
recyclerView.layoutManager=layoutManager
adapter= CustomersAdapter(companynameFB,companyoffFB,companyphoneFB)
recyclerView.adapter=adapter
}
fun getDataFromFirestore(){
db.collection("Customers")
.addSnapshotListener { snapshot, exception ->
if(exception!=null){
Toast.makeText(applicationContext,exception.localizedMessage.toString(),Toast.LENGTH_LONG).show()
}else{
if(snapshot!=null){
if (!snapshot.isEmpty){
companynameFB.clear()
companyoffFB.clear()
companyphoneFB.clear()
val documents=snapshot.documents
for (document in documents){
val compname=document.get("compname") as String
val compoff=document.get("compoff") as String
val compphone=document.get("compphone") as String
val user=document.get("user") as String
val timeStamp=document.get("date") as Timestamp
val date=timeStamp.toDate()
companynameFB.add(compname)
companyoffFB.add(compoff)
companyphoneFB.add(compphone)
adapter!!.notifyDataSetChanged()
}
}
}
}
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
val menuInflater=getMenuInflater()
menuInflater.inflate(R.menu.add_customer,menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if(item.itemId==R.id.add){
val intent= Intent(this,AddCustomers::class.java)
intent.putExtra("info","new")
startActivity(intent)
}
return super.onOptionsItemSelected(item)
}
For starting a new Activity you need at least a context instance. And as is written in some other answers here, you could start the activity directly inside ViewHolder with View's context. But for better separations of responsibilities, I suggest you move your navigation to your Fragment/Activity, where all other navigations happen. That way you know at the look of Activity/Fragment, which navigations could be done from that screen, and you don't need to search for some hidden navigations that happen inside Adapter or ViewHolder. Also, if you need to perform startActivityForResult, then an instance of Context is not enough to do that. You need an Activity or Fragment instance for that and we have another reason to put all navigations there.
So you can do this like that:
ADD LAMBDA TO ADAPTER CONSTRUCTOR:
class CustomersAdapter(private val companynameArray: ArrayList,
private val companyoffArray: ArrayList,
private val companyPhoneArray: ArrayList,
private val onItemClick: () -> Unit) : RecyclerView.Adapter<CustomersAdapter.CustomerHolder>() {
NOTE: put lambda as the last constructor parameter.
ADD LAMBDA TO VIEWHOLDER CONSTRUCTOR:
class CustomerHolder(view: View,
private val onItemClick: () -> Unit) : RecyclerView.ViewHolder(view) {
MODIFY VIEWHOLDER CONSTRUCTOR CALL:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomerHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.recycler_view_customer_row,parent,false)
return CustomersAdapter.CustomerHolder(view, onItemClick) //Add onItemClick lambda instance from adapter here
}
INVOKE LAMBDA ON CLICK:
itemView.setOnClickListener {
onItemClick()
}
FIX ADAPTER CONSTRUCTOR CALL:
val adapter = CustomersAdapter(companynameArray, companyoffArray, companyPhoneArray) {
startActivity() // do your navigation here
}
Why did we place lambda as the last parameter of a constructor in our adapter? If the last parameter of a function is a function, then a lambda expression passed as the corresponding argument can be placed outside the parentheses(SOURCE). And we did exactly that with the last junk of code above where we handle our navigation.
put your click listener inside onBindViewHolder method
override fun onBindViewHolder(holder: CustomerHolder, position: Int) {
holder.recycleCompanyName?.text="Şirket Adı:"+companynameArray[position]
holder.recycleOffName?.text="Şirket Yetkilisi :"+companyoffArray[position]
holder.recycleOffPhone?.text="İrtibat No : "+companyPhoneArray[position]
holder.itemView.setOnClickListener {
// do what ever you want here
holder.itemView.context.startActivity( /*your intent here*/)
}
}
}
Step 1 : pass context here
class CustomersAdapter(val context:Context,private val
companynameArray:ArrayList,private val
companyoffArray:ArrayList, private val companyPhoneArray:ArrayList):
RecyclerView.Adapter<CustomersAdapter.CustomerHolder>() {
step 2:
itemView.setOnClickListener {
val intent=Intent(context,yourActivity::java.class)
startActivity(intent)
}
you need a context for intent so you can get it form the activity or fragment that are associated with this adapter and pass it to your intent or you can get it from any view in your inflated layout like this
val context=holder.title.context
val intent = Intent( context, EventDetail::class.java)
context.startActivity(intent)

How to observe member variables of a class in MutableLiveData?

I am trying to observe fields of my class without exposing it. So far, I've tried this:
TaskItemViewModel.kt
open class TaskItemViewModel(private val navigator: ITaskNavigator) : ViewModel() {
private val taskItem: MutableLiveData<TaskItem> = MutableLiveData()
val title: LiveData<String?> = Transformations.map(taskItem) { it.title }
val content: LiveData<String?> = Transformations.map(taskItem) { it.content }
var showCheck: LiveData<Boolean> = Transformations.map(taskItem) { it.isCompleted }
fun setModel(model: TaskItem) {
this.taskItem.value = model
}
}
ItemListScreenAdapter.kt
class ItemListScreenAdapter(private val navigator: ITaskNavigator) : RecyclerView.Adapter<ItemListScreenAdapter.TaskItemViewHolder>() {
private val TAG = "ItemListScreenAdapter"
private var dataset: List<TaskItem> = listOf()
override fun onBindViewHolder(viewHolder: TaskItemViewHolder, position: Int) {
with(viewHolder.binding) {
this.viewModel?.setModel(dataset[position])
executePendingBindings()
}
}
fun updateDataset(dataset: List<TaskItem>) {
Log.d(TAG,"Updating dataset")
this.dataset = dataset
notifyDataSetChanged()
}
override fun getItemCount(): Int = dataset.size
override fun onCreateViewHolder(parent: ViewGroup, type: Int): TaskItemViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ItemTaskBinding.inflate(inflater, parent, false)
binding.viewModel = TaskItemViewModel(navigator)
return TaskItemViewHolder(binding)
}
class TaskItemViewHolder(val binding: ItemTaskBinding) : RecyclerView.ViewHolder(binding.root)
}
If I call setModel before inflating the view, everything works fine. However, after the view is inflated, the view is not updated even if taskItem 's value is updated. You can be assured that updateDataset is called everytime there is a change in dataset.
I want the view to be updated whenever I call setModel in corresponding viewmodel. What are the ways to achieve this?
For this viewmodel, I want to use ViewModel rather than BaseObservable. Therefore, please give your answers according to this.
EDIT:
I have found the solution to the problem.
in ItemListScreenAdapter's onCreateViewHolder method, after inflating, I needed to set LifeCycleOwner of the binding.
I added the following line after inflating the ItemTaskBinding.
binding.setLifecycleOwner(parent.context as MainActivity)
and the problem is solved and view is being updated.

I want to set TextView text from ViewHolder's itemView clicklistener . I can call companion function. but cant access TextView object from it

I want to set TextView text from ViewHolder's itemView clicklistener . I tried making companion object in Activity class, and tried calling it from ViewHolders itemView clicklistener. But inside companion object I couldn't access TextView object in layout
Following is the adapter code for setting recyclerview
class QnAdapter(val context: Context, val ques: List<QnoList>):
RecyclerView.Adapter<QnAdapter.ViewHolder>(){
override fun onCreateViewHolder(p0: ViewGroup, p1: Int): ViewHolder{
val view = LayoutInflater.from(context).inflate(R.layout.recycler_qno, p0, false)
return ViewHolder(view)
}
override fun getItemCount(): Int {
return ques.size
}
override fun onBindViewHolder(p0: ViewHolder, p1: Int){
val question = ques[p1]
p0.setData(question, p1)
}
inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
var currentQ: QnoList?=null
var currentQno: Int =0
init{
itemView.setOnClickListener{
Toast.makeText(context,currentQ!!.qno.toString()+" Selected",Toast.LENGTH_SHORT).show()
var a1 = MCQActivity
a1.setText(context)
a1.setTxt() // here I want to set TextView, present in Layout of activity(not of recyclerview) text from recyclerview event
}
}
fun setData(ques:QnoList?,p1:Int){
itemView.rqno.text = ques!!.qno.toString()
this.currentQ = ques
this.currentQno = p1+1
}
}
}
private fun MCQActivity.Companion.setTxt() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
following is the activity class code
class MCQActivity : AppCompatActivity() {
companion object {
fun setText(context: Context){
Toast.makeText(context, "Hi I am called", Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_mcq)
setRecyclerView()
}
open fun setRecyclerView(){
val layoutmanager = LinearLayoutManager(this)
layoutmanager.orientation= LinearLayoutManager.HORIZONTAL
recycler_qno.layoutManager = layoutmanager
val adapter = QnAdapter(this, mcq.queslst)
recycler_qno.adapter = adapter
}
}
the companion function is the same as a static function in Java. In your case, you should remove the companion object and make the setText function public in your MCQActivity.
To access your activity from ViewHolder, it's straightforward:
(itemView.context as? MCQActivity)?.setText("I am handsome")
Off course you can't access the TextView via companion object.
First you should understand the definition of companion object. It is an object that is common to all instances of that class or any other classes. It’d come to be similar to static fields in Java. It doesn't have any references to any objects created for that class.
So how to solve your problem?
Since you have already passing context to QnAdapter, and I suppose it is an instance/object of MCQActivity, you can cast that context to MCQActivity and set the text for that instance.
Refer to this code
class MCQActivity : AppCompatActivity() {
fun setText(theTextYouWantToSet: String) {
findViewById<TextView>(R.id.textView).text = theTextYouWantToSet
}
}
class QnAdapter(val context: Context, val ques: List<String>) : RecyclerView.Adapter<QnAdapter.ViewHolder>() {
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
init {
itemView.setOnClickListener {
(context as MCQActivity).setText("I am handsome")
}
}
}
}

Categories

Resources