I have a list of categories and i want to show the amount of items in each category. Using Room with MVVM architecture basically i want to use simple query in my adapter, to return its value (amount of items)
DAO
#Query("SELECT COUNT(id) FROM items WHERE listId=:listID")
suspend fun countItems(listID: Long):Int
Repo
suspend fun countItems(id: Long): Int{
return itemsDao.countItems(id)
}
Adapter
class ListsAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<ListsAdapter.ListViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var lists = mutableListOf<ListItem>()
inner class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val listName: TextView = itemView.findViewById(R.id.single_list_name)
val listIcon: ImageView = itemView.findViewById(R.id.single_list_icon)
val wAmount: TextView = itemView.findViewById(R.id.single_list_amount)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListsAdapter.ListViewHolder {
val itemView = inflater.inflate(R.layout.single_list, parent, false)
return ListViewHolder(itemView)
}
override fun getItemCount() = lists.size
override fun onBindViewHolder(holder: ListsAdapter.ListViewHolder, position: Int) {
val current = lists[position]
holder.listName.text = current.name
// holder.wAmount.text =
holder.itemView.setOnClickListener {
val bundle = bundleOf("list_id" to current.id,"list_name" to current.name)
holder.itemView.findNavController().navigate(R.id.action_listsFragment_to_nav_items_list, bundle)
}
}
internal fun setLists(lists: List<ListItem>) {
this.lists = lists.toMutableList()
notifyDataSetChanged()
}
internal fun listToDelete(viewHolder: RecyclerView.ViewHolder) : ListItem{
val position = viewHolder.adapterPosition
return lists[position]
}
internal fun removeList(viewHolder: RecyclerView.ViewHolder){
lists.removeAt(viewHolder.adapterPosition)
notifyItemRemoved(viewHolder.adapterPosition)
}
}
Should it be done over ViewModel, but in that case it has to be passed to adapter? Or maybe there is better (cleaner) way to do it? Any help is appreciated. Thanks
Adapters shouldn't be responsible for loading data from storage. The clean way to do it is to access the repo in your ViewModel, then pass the data to your Adapter. This also gives you the benefit of being able to handle errors in a straightforward way, as it would be easy to update the layout containing Adapter easily, unlike the spaghetti you'll need to do this from the adapter, in the addition to being a wrong design of course.
Another step would be to introduce a more layered architecture,like Uncle Bob's clean architecture
Related
I am creating Recyclerview using MVVM and data binding. Now I need to perform some network operation in Recycler view adapter. So how can we create ViewModel and Live data for adapter. How can adapter observe live data.
I have create ViewModel using activity context and but not working proper
class CartAdapter(cartList: ArrayList<ProductData>, context: BaseActivity) :
RecyclerView.Adapter<CartAdapter.MyViewHolder>() {
private val itemList = cartList
private val activity = context
private var viewModel: CartAdapterViewModel =
ViewModelProvider(context).get(CartAdapterViewModel::class.java)
init {
initObserver()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.cart_item_layout, parent, false)
return MyViewHolder(view)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = itemList[position]
holder.setData(item)
}
override fun getItemCount(): Int {
return itemList.size
}
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var binding: CartItemLayoutBinding? = DataBindingUtil.bind(itemView)
fun setData(model: ProductData) {
binding?.item = model
}
}
private fun initObserver() {
viewModel.statusResponse.observe(activity, {
activity.hideLoader()
})
viewModel.serverError.observe(activity, {
activity.hideLoader()
})
}
}
You should not create a separate ViewModel for adapter.
The Classic way:
The adapter should expose an interface whose implementation would later handle e.g. clicks on an item in the RecyclerView.
class CartAdapter(
cartList: ArrayList<ProductData>,
private val itemClickListener: ItemClickListener // This is the interface implementation
// that will be provided for an item click in this example
) : RecyclerView.Adapter<CartAdapter.MyViewHolder>() {
interface ItemClickListener {
fun onItemClick(position: Int)
}
override fun onBindViewHolder(holder: ScanResultViewHolder, position: Int) {
holder.binding.root.setOnClickListener {
itemClickListener.onItemClick(position)
}
}
// this function would be useful for retrieving an item from the recyclerview
fun getItemAt(position: Int): ProductData = itemList[position]
...
}
Later on when instantiating the CartAdapter in Your Activity or Fragment You would have to provide that interface implementation:
private val cartAdapter: CartAdapter = CartAdapter(
cartList,
object : CartAdapter.ItemClickListener {
override fun onItemClick(position: Int) {
// this function will handle the item click on a provided position
doSomethingWithARecyclerViewItemFrom(position)
}
}
)
private fun doSomethingWithARecyclerViewItemFrom(position: Int) {
// get the adapter item from the position
val item = cartAdapter.getItemAt(position)
// later on You can use that item to make something usefull with Your ViewModel of an activity/fragment
...
}
This way the Adapter doesn't have to have any ViewModels - the corresponding actions on RecyclerView items can be handled by the Activity view model.
In my example this action is an item click but for a more specific action, You would have to update Your question with those details.
The more compact way:
You can implement the same functionality as above using even more compact and neat way by using function types:
class CartAdapter(
cartList: ArrayList<ProductData>,
private val itemClickListener: (productData: ProductData) -> Unit // notice here
) : RecyclerView.Adapter<CartAdapter.MyViewHolder>() {
override fun onBindViewHolder(holder: ScanResultViewHolder, position: Int) {
holder.binding.root.setOnClickListener {
itemClickListener(position) // slight change here also
}
}
}
My suggestion is using constructor to pass viewModel instance.
Without concerns of unhandled instance scope problem anyway.
Have a happy day.
I'm migrating from Paging 2 to Paging 3. The app stores a large dataset in a database using Room, and I can load the data from Room and display it okay. The issue I have is as soon as the app makes a change to the database, it crashes.
Code Snippets
IssueRepository
#Query("SELECT * FROM isssue WHERE project_id = ?")
fun findAllInProject(projectId:Int): PagingSource<Int, IssueListBean>
In the function onCreateView
val dataSource = DB.store.issueRepository().findAllInProject(project.id)
val pageConfig = PagingConfig(50)
val pager = Pager(pageConfig, null) { dataSource }.flow
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
pager.collectLatest { data ->
adapter.submitData(data)
}
}
class PagingAdapter : PagingDataAdapter<IssueListBean, PagingAdapter.ViewHolder>(EntityComparator()) {
inner class ViewHolder(private val adapterBinding: ItemIssueListBinding) : RecyclerView.ViewHolder(adapterBinding.root) {
fun bind(position: Int) {
val issueListBean = getItem(position)
adapterBinding.label.text = issueListBean.label
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemIssueListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(position)
}
}
So when users tap on an item they can edit it. As soon the item is saved via Room the app crashes with the following exception:
java.lang.IllegalStateException: An instance of PagingSource was re-used when Pager expected to create a new
instance. Ensure that the pagingSourceFactory passed to Pager always returns a
new instance of PagingSource.
Am I using Paging 3 wrong? I can't find many articles online talk about using Room as data source where you make changes.
The lambda you pass to Pager() should return a new instance of the data source each time, so move the call to findAllInProject() into that lambda, like
val pager = Pager(pageConfig, null) {
DB.store.issueRepository().findAllInProject(project.id)
}.flow
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 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 have a nested recyclerview which should look like in the .
I implemented it according to this helpful site.
The problem is, that I sometimes have a user with hundreds of items and in that case, it takes half a minute to open the activity.
I have a room database in the backend with two linked tables with foreign keys (users and items) and I select all users to get a user/item list where the items are a list in the user-table.
class userWithItems: (id: Int, name: String, ... ,List)
and I create the inner recycler view with the List of items in the adapter.
Would it be better to make one List UserItems (userid:Int, username:String, ... itemid:Int, itemList) and group them for the outer rv.
Or is there a possibility to get rid of the nested rv and make the design with just one recyclerview-list?
Or is there another solution to make the nested recyclerview work even if there are many items for a user?
code for the adapters:
// Code in Activity: (oncreate)
val recyclerView = findViewById<RecyclerView>(R.id.rv_users)
val adapter = UserAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
mainViewModel = ViewModelProviders.of(this, PassIntViewModelFactory(this.application, online_id)).get(MainViewModel::class.java!!)
mainViewModel.userList.observe(this, Observer {
it?.let {
adapter.setUserList(it)
}
})
data class UsersWithItems(
val id:Int, val username: String, val address, // fields from user table
val items: List<Items> // list of items for current user
)
data class Items (
val id: Int, val itemtext: String, val itemlocation: String, val image: String // ...
)
// UserAdapter (outside)
class UserAdapter internal constructor(
context: Context
) : RecyclerView.Adapter() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var userList = emptyList<UsersWithItems>()
inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val username: TextView = itemView.findViewById(R.id.user_name)
val num_pos: TextView = itemView.findViewById(R.id.user_num_pos)
val address: TextView = itemView.findViewById(R.id.user_addr)
val rv:RecyclerView = itemView.findViewById(R.id.rv_user_items)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val myItemView = inflater.inflate(R.layout.rv_row_user, parent, false)
return MyViewHolder(myItemView)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val current = userList[position]
holder.username.text="${current.user?.username}"
holder.num_pos.text="${current.items?.size}"
holder.address.text = "${current.user?.address}"
val adapter = UserItemAdapter(holder.rv.context)
adapter.setItems(current.items!!)
holder.rv.adapter = adapter
holder.rv.layoutManager = LinearLayoutManager(holder.rv.context,LinearLayout.VERTICAL,false)
}
internal fun setUserList(userList: List<UsersWithItems>){
this.userList=userList
notifyDataSetChanged()
}
override fun getItemCount() = userList.size
}
class UserItemAdapter internal constructor(
context: Context
) : RecyclerView.Adapter() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var itemList = emptyList<Items>()
inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val itemtext: TextView = itemView.findViewById(R.id.item_text)
val itemlocation:TextView = itemView.findViewById(R.id.item_location)
val image: ImageView = itemView.findViewById(R.id.item_image)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val myItemView = inflater.inflate(R.layout.rv_row_user_items, parent, false)
return MyViewHolder(myItemView)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val current = itemList[position]
holder.itemtext.text="${current.itemtext}"
holder.itemlocation.text = current.itemlocation
if (current.image.length>0) {
val image = Base64.decode(current.image, 0)
val bitmap = BitmapFactory.decodeByteArray(image, 0, image.size)
holder.image.setImageBitmap(bitmap)
}
}
internal fun setItems(items: List<Items>){
this.itemList=items
notifyDataSetChanged()
}
override fun getItemCount() = itemList.size
}
E. Reuter i have been through this situation the thing is the approach is quite correct by using nested Recycler View. Your code seems to be good. but the queries which you are using to query database. I think you should use queries in Background or on the other threas and show result as you get them instead of querying it from OnCreate or from main thread. Because getting this many items in one go can possibly create lag to activity and decreasing performance. try this out if you have not yet and let me know. What happens. Thanks...
I am editing my answer. the other thing you could do is if you have more than certain amount of items then instead of getting them at the first you should use some thing like pagination to load certain amount of items at once to avoid this lag.
Here i am attaching the code to query certain amount of data per load....
SApp.database!!.resultDao().loadAllUsersByPage(5, 10)
#Query("SELECT * FROM Result LIMIT :limit OFFSET :offset")
fun loadAllUsersByPage(limit: Int, offset: Int): List<Result>
Thank you very much for your answer. I think that paging is really a good approach. But I cannot add the pageing directly since I am getting my data from a roomdatabase like this:
#Query(SELECT * FROM users)
fun getData(): LiveData<List<userWithItems>>
and the actual items are added by room because of a relation between user and items I will have to change this behavior.
I will try something like
#Query(SELECT * FROM users)
fun getUserData(): LiveData<List<Users>>
and then try to add an LiveData observer in the outer recyclerview to get the items in a separate query which uses paging.
I solved the problem. When I thought about pagination it came into my mind that the problem could be that the inner recyclerview has a height of wrap_content and so it needs to build all of the items and makes the rv useless. When I make the height of the inner rv 250dp, it works quite even with 2000 items.
So now I just have to figure out a way to always find the optimal height for the inner rv and solve the scrolling problem but the original problem is solved.
Special thanks to Aman B!