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.
Related
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.
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)
READ FIRST:
Apologies, it seems I have played myself. I was using RecyclerView in my xml earlier, but switched it over for CardStackView (it still uses the exact same RecyclerView adapter). If I switch back to RecyclerView, the original code below works - the scroll position is saved and restored automatically on configuration change.
I'm using a MVVM viewmodel class which successfully retains list data for a RecyclerView after a configuration change. However, the previous RecyclerView position is not restored. Is this expected behaviour? What would be a good way to solve this?
I saw a blog post on medium briefly mentioning you can preserve scroll position by setting the adapter data before setting said adapter on the RecyclerView.
From what I understand, after a configuration change the livedata that was being observed earlier gets a callback. That callback is where I set my adapter data. But it seems this callback happens after the onCreate() function finishes by which point my RecyclerView adapter is already set.
class MainActivity : AppCompatActivity() {
private val adapter = MovieAdapter()
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Create or retrieve viewmodel and observe data needed for recyclerview
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
viewModel.movies.observe(this, {
adapter.items = it
})
binding.recyclerview.adapter = adapter
// If viewmodel has no data for recyclerview, retrieve it
if (viewModel.movies.value == null) viewModel.retrieveMovies()
}
}
class MovieAdapter :
RecyclerView.Adapter<MovieAdapter.MovieViewHolder>() {
var items: List<Movie> by Delegates.observable(emptyList()) { _, _, _ ->
notifyDataSetChanged()
}
class MovieViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val binding = ItemMovieCardBinding.bind(itemView)
fun bind(item: Movie) {
with(binding) {
imagePoster.load(item.posterUrl)
textRating.text = item.rating.toString()
textDate.text = item.date
textOverview.text = item.overview
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_movie_card, parent, false)
return MovieViewHolder(view)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
}
class MainViewModel : ViewModel() {
private val _movies = MutableLiveData<List<Movie>>()
val movies: LiveData<List<Movie>> get() = _movies
fun retrieveMovies() {
viewModelScope.launch {
val client = ApiClient.create()
val result: Movies = withContext(Dispatchers.IO) { client.getPopularMovies() }
_movies.value = result.movies
}
}
}
Set adapter only after its items are available.
viewModel.movies.observe(this, {
adapter.items = it
binding.recyclerview.adapter = adapter
})
I am fetching a list of categories from an api and setting them into recyclerview . Adapter code is written in viewModel class and is called by the fragment that is calling the api. Below are the methods for setting adapters.
fun getAdapter(
listener: OtherVideoCategoriesRecyclerAdapter.OtherVideoCategoriesRecyclerAdapterListener,context: Context
): OtherVideoCategoriesRecyclerAdapter {
if (categoriesRecyclerAdapter == null)
categoriesRecyclerAdapter = OtherVideoCategoriesRecyclerAdapter(listener,context)
return categoriesRecyclerAdapter as OtherVideoCategoriesRecyclerAdapter
}
fun setItems(categories: ArrayList<OtherCategoriesItem>) {
categoriesList = categories
categoriesRecyclerAdapter!!.setItems(categoriesList!!)
}
And this is how I call these methods from my fragment class.
otherVideoViewModel.setItems(it.first.data!!.otherCategories as ArrayList<OtherCategoriesItem>)
Set Adapter method
private fun setAdapter() {
otherVideosCategoriesBinding.recyclerView.layoutManager = LinearLayoutManager(activity)
otherVideosCategoriesBinding.recyclerView.itemAnimator = DefaultItemAnimator()
adapter = otherVideoViewModel.getAdapter(adapterListener,activity!!)
otherVideosCategoriesBinding.recyclerView.adapter = adapter
}
And this is the adapter class.
class OtherVideoCategoriesRecyclerAdapter(private val listener: OtherVideoCategoriesRecyclerAdapterListener,val context: Context): RecyclerView.Adapter<OtherVideoCategoriesRecyclerAdapter.ViewHolder>() {
var categories = ArrayList<OtherCategoriesItem>()
interface OtherVideoCategoriesRecyclerAdapterListener {
fun onItemClicked(position: Int)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OtherVideoCategoriesRecyclerAdapter.ViewHolder {
val inflater=LayoutInflater.from(context)
val binding = ItemOtherVideoCategoryBinding.inflate(inflater, parent, false)
return OtherVideoCategoriesRecyclerAdapter.ViewHolder(binding)
}
override fun getItemCount(): Int {
return categories.size
}
override fun onBindViewHolder(holder: OtherVideoCategoriesRecyclerAdapter.ViewHolder, position: Int) {
val item = categories[position]
holder.bindViews(item)
}
class ViewHolder(private val binding: ItemOtherVideoCategoryBinding): RecyclerView.ViewHolder(binding.root) {
fun bindViews(model: OtherCategoriesItem){
binding.model=model
binding.executePendingBindings()
}
}
fun setItems(categoriesList: ArrayList<OtherCategoriesItem>) {
categories = categoriesList
notifyDataSetChanged()
}
}
When I run this code, it crashes with following exception.
java.lang.IllegalStateException: ViewHolder views must not be attached when created. Ensure that you are not passing 'true' to the attachToRoot parameter of LayoutInflater.inflate(..., boolean attachToRoot)
I have tried all the related answers to this error but none of them worked for my case as many of those answers doesn't included data binding.
Hey just changing your onCreateViewHolder a bit. Try this:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OtherVideoCategoriesRecyclerAdapter.ViewHolder {
val inflater=LayoutInflater.from(context)
val binding:ItemOtherVideoCategoryBinding = DataBindingUtil.inflate(inflater,R.layout.your_layout_name, parent, false)
return OtherVideoCategoriesRecyclerAdapter.ViewHolder(binding)
}
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