I'm making a function that looks like an image.
Although not shown in the image, these items can be added or deleted through the button.
Item is composed of Header and Detail. The adapter also has HeaderAdapter and DetailAdapter respectively.
I'm using ConcatAdapter to make sure there are HeaderAdapter and DetailAdatper per group rather than the whole list.
Because I thought it would be more manageable than using multiview types in one adapter (purely my opinion).
But I have a question. HeaderAdapter.
As you can see from the image, there is one header per group. So, there must be only one HeaderItem in the HeaderAdapter of each group.
In this case, I don't think there is much reason to use the Adapter.
In my case, is it better to use a multiview type for one Adapter?
RoutineItem
sealed class RoutineItem(
val layoutId: Int
) {
data class Header(
val id: String = "1",
val workout: String = "2",
val unit: String = "3",
) : RoutineItem(VIEW_TYPE) {
companion object {
const val VIEW_TYPE = R.layout.routine_item
}
}
data class Detail(
val id: String = UUID.randomUUID().toString(), // UUID
val set: Int = 1,
var weight: String ="",
val reps: String = "1"
) : RoutineItem(VIEW_TYPE) {
companion object {
const val VIEW_TYPE = R.layout.item_routine_detail
}
}
}
HeaderAdapter
class HeaderAdapter(item: RoutineItem.Header) : BaseAdapter<RoutineItem.Header>(initialItem = listOf(item)) {
override fun createViewHolder(itemView: View): GenericViewHolder<RoutineItem.Header> {
return HeaderViewHolder(itemView)
}
override fun getItemCount(): Int = 1
class HeaderViewHolder(itemView: View) : GenericViewHolder<RoutineItem.Header>(itemView)
}
DetailAdapter
class DetailAdapter(private val items: List<RoutineItem.Detail> = emptyList())
: BaseAdapter<RoutineItem.Detail>(initialItem = items) {
override fun createViewHolder(itemView: View): GenericViewHolder<RoutineItem.Detail> {
return DetailViewHolder(itemView)
}
override fun getItemCount(): Int = items.size
class DetailViewHolder(itemView: View) : GenericViewHolder<RoutineItem.Detail>(itemView)
}
Activity
class MainActivity : AppCompatActivity() {
var concatAdpater: ConcatAdapter = ConcatAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val rv: RecyclerView = findViewById(R.id.rv)
val adapterItems: ArrayList<Pair<RoutineItem.Header, List<RoutineItem.Detail>>> = arrayListOf()
val childItems : List<RoutineItem.Detail> = listOf(
RoutineItem.Detail(),
RoutineItem.Detail(),
RoutineItem.Detail(),
RoutineItem.Detail(),
RoutineItem.Detail()
)
adapterItems.add(Pair(RoutineItem.Header(), childItems))
adapterItems.add(Pair(RoutineItem.Header(), childItems))
adapterItems.add(Pair(RoutineItem.Header(), childItems))
for ((header, list) in adapterItems) { // 1 adapter per group
concatAdpater.addAdapter(HeaderAdapter(header))
concatAdpater.addAdapter(DetailAdapter(list))
}
rv.adapter = concatAdpater
}
}
Because it is a test code, there are parts that are not functionally implemented! (Ex. Dynamically adding and deleting items..)
It's always better to use a single adapter because item animation and state changes are way more managable with DiffUtil. Also it's easier to maintain and way more efficient (in terms of speed and resource managment).
More detailed answers:
https://stackoverflow.com/a/53117359/6694770
https://proandroiddev.com/writing-better-adapters-1b09758407d2
Related
So I'm trying to show all the data from a JSON file in a recyclerview, and that works fine.
My data has 2 types of accounts, bank account & card account. I'm trying to put them all in on recyclerview but split them into 2 categories each with a title above the category. All need to scroll up and down as one.
group 1 all the card accounts together
group 2 all the bank accounts together
Below is my adapter, works great but doesn't split things as needed. Any ideas?
class DataAdapter(private val list: List<Data>, var context: Context) :
RecyclerView.Adapter<DataAdapter.DataViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.bank_card_view,parent,
false)
return DataViewHolder(itemView)
}
override fun onBindViewHolder(holder: DataViewHolder, position: Int) {
val currentItem = list[position]
holder.card.setOnClickListener(object :View.OnClickListener{
override fun onClick(v: View?) {
val intent = Intent(context, DetailsActivity::class.java)
intent.putExtra("name",currentItem.account_name)
intent.putExtra("desc",currentItem.desc)
intent.putExtra("type",currentItem.account_type)
context.startActivity(intent)
}
})
holder.name.text = (currentItem.account_name)
holder.description.text = (currentItem.desc)
if (currentItem.account_type.equals("card")){
holder.img.setBackgroundResource(R.drawable.baseline_credit_card_black_48pt_1x)
holder.type.text = context.getString(R.string.card)
}
else{
holder.img.setBackgroundResource(R.drawable.baseline_account_balance_black_48pt_1x)
holder.type.text = context.getString(R.string.bank)
}
}
override fun getItemCount() = list.size
class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val name: TextView = itemView.findViewById(R.id.account_name)
val description: TextView = itemView.findViewById(R.id.desc)
val type: TextView = itemView.findViewById(R.id.type)
val card: CardView = itemView.findViewById(R.id.card)
val img: ImageView = itemView.findViewById(R.id.img)
}
}
This is my JSON file:
[
{
"account_type":"bank",
"account_name":"",
"desc": ""
},
{
"account_type":"card",
"account_name":"",
"desc": ""
}
]
I ended up filtering through my json file and splitting it into 2 lists. One that contains all the card types, another for all the bank types. Then passing each through their individual recycler.
val cardData: MutableList <Data> = mutableListOf<Data>()
val bankData: MutableList <Data> = mutableListOf<Data>()
for(i in data) {
if (i.account_type.equals("card")){
cardData.add(i)
}
}
for(i in data) {
if (i.account_type.equals("bank")){
bankData.add(i)
}
}
To clarify, data is a list that uses the helper class Data using Gson, assign the json file to it in my main activity.
There is a great solution for your case here in this codelab, you should take a look: https://developer.android.com/codelabs/kotlin-android-training-headers#3
You can override the getItemViewType function to inflate different types of view holders inside a single RecyclerView.
Then, you can use the kotlin function map or filter to reorganize your list and add a header type before populating the adapter, to inflate a header view holder to each section.
What I have is a ViewModel, a Fragment and a RecyclerViewAdapter.
The use case is as follows:
User wants to change the name of one item in the RecyclerView.
His command is sent to the ViewModel.
ViewModel updates the item and postValue using LiveData
Fragment observes on the live property and sends a command to the RecyclerViewAdapter to update the list.
List is updated.
Snippet containing only methods that I checked are valid for this problem.
data class Name(
var firstName: String = "",
var lastName: String = "",
)
class NameListRecyclerViewAdapter : RecyclerView.Adapter<NameListRecyclerViewAdapter.ListViewHolder>() {
private var names: List<Name> = listOf()
fun setData(newList: List<Name>) {
names = newList
notifyDataSetChanged()
}
}
class NameListViewModel : ViewModel() {
private var names: MutableList<Name>? = null
private val _nameList = MutableLiveData<MutableList<Name>>()
val nameList: LiveData<MutableList<Name>>
get() = _nameList
fun changeFirstName(index: Int, name: String) {
names?.get(0)?.firstName = name // in this very moment the names property in NameListRecyclerViewAdapter is being changed
_nameList.postValue(names)
}
}
class NamesListFragment : Fragment() {
private lateinit var viewModel: NameListViewModel
private lateinit var recyclerViewAdapter: NameListRecyclerViewAdapter
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
viewModel = ViewModelProvider(
requireActivity(),
NameListViewModelFactory(requireActivity().applicationContext)
).get(NameListViewModel::class.java)
recyclerViewAdapter = NameListRecyclerViewAdapter()
recycler_view_layout.apply {
setHasFixedSize(true)
layoutManager =
LinearLayoutManager(context).apply {
orientation = LinearLayoutManager.VERTICAL
}
adapter = recyclerViewAdapter
}
viewModel.nameList.observe(
viewLifecycleOwner,
Observer {
val result = it ?: return#Observer
recyclerViewAdapter.setData(result)
}
)
}
}
For the first time, the name is changing as it should, but the second one is very peculiar.
The name (the property in the RecyclerViewAdapter) is updated not when it should (in step 5) but in step 3 - even before the new value is posted using liveData.
My theory is that its the same list in the ViewModel and the RecyclerViewAdapter, but I have no idea why is that, why one is not a copy of the other??
Update The problem seems to be solved when in the ViewModel i add .toMutableList() as follows:
class NameListViewModel : ViewModel() {
//...
fun changeFirstName(index: Int, name: String) {
names?.get(0)?.firstName = name
_nameList.postValue(names.toMutableList()) // instead of simple _nameList.postValue(names)
}
}
Does that mean that live data property has the exact same list and I have to make sure to copy it always?
I want to create a search function for my user to quick access to my items .
Well , the first thing is that i have my product in a room table(List) and store them in database and show them with a recyclerview in the my main activity(Home activity ) .
So i want code a Query to search between them after user click on button search .
I code my query and after use it in my home activity nothing happend .i'm using mvvm model. pls help me with this .
Code :
My Table (List of Product ) :
#Entity(tableName = "cart")
data class RoomTables(
#PrimaryKey(autoGenerate = true) val id: Int?,
#ColumnInfo val title: String,
#ColumnInfo val price: Int,
#ColumnInfo val image: Int,
#ColumnInfo var amount: Int
)
My dao :
#Query ("SELECT * FROM cart WHERE title LIKE :search")
fun searchItem (search : String?):List<RoomTables>
My Repository :
fun searchItem(search :String) = db.GetDao().searchItem(search)
My Viewmodel :
fun searchItem(search : String) = CoroutineScope(Dispatchers.Default).launch {
repository.searchItem(search)
}
And HomeActivity :
class HomeActivity : AppCompatActivity() {
lateinit var viewModelRoom: ViewModelRoom
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.home_activity)
val list = ArrayList<RoomTables>()
for (i in 0..20) {
list.add(
RoomTables(
null, "$i banana", 12,
R.drawable.bannana, 0
)
)
}
recycler_main.apply {
layoutManager = GridLayoutManager(this#HomeActivity, 2)
adapter = RecyclerAdapterMain(list, context)
}
val database = DataBaseRoom(this)
val repositoryCart = RepositoryCart (database)
val factoryRoom = FactoryRoom(repositoryCart)
viewModelRoom = ViewModelRoom(repositoryCart)
viewModelRoom = ViewModelProvider(this , factoryRoom ).get(ViewModelRoom::class.java)
val editText : EditText = findViewById(R.id.edittextSearch)
val searchbtn : ImageView = findViewById(R.id.search_main)
searchbtn.setOnClickListener{
viewModelRoom.searchItem(editText.text.toString())
}
Let's try this approach.
First get list items from the table.
#Query ("SELECT * FROM cart")
fun searchItem():List<RoomTables>
Now from your repository.
fun searchItem() : List<RoomTables> = db.GetDao().searchItem()
In ViewModel.
fun searchItem(search : String): <List<RoomTables> {
filterWithQuery(query)
return filteredList
}
private fun filterWithQuery(query: String, repository: YourRepository) {
val filterList = ArrayList<RoomTables>()
for (currentItem: RoomTables in repository.searchItem()) {
val formatTitle: String = currentItem.title.toLowerCase(Locale.getDefault())
if (formatTitle.contains(query)) {
filterList.add(currentItem)
}
}
filteredList.value = filterList
}
Make sure you add Coroutines above.
Now you have all elements filtered and returns new list items based on search query user entered.
In your fragment or activity observe data.
searchbtn.setOnClickListener{
viewModel.searchItem(query).observe(viewLifecycleOwner, Observer { items -> {
// Add data to your recyclerview
}
}
The flow and approach is correct and it is working, it is hard to follow your code since i'm not sure if the return types match because you are not using LiveData, in this case you must.
If you found confusing or hard to follow, i have a working example in github, compare and make changes.
https://github.com/RajashekarRaju/ProjectSubmission-GoogleDevelopers
I want each row of my RecyclerView to display all the details of one document of the collection.
I've used this exact same adapter code, albeit with a different class to serialize into. And it works well. But in this instance, it's simply not working.
But the code just doesn't get into populating the views.
My database is like:
reviews--Orange--vault--|
|-firstReview
|-secondReview
|-sjdeifhaih5aseoi
...
My query and adapter from the fragment:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(ReviewViewModel::class.java)
val reviewQuery = FirebaseFirestore.getInstance().collection("reviews").document("Orange").collection("vault")
val reviewBurnOptions = FirestoreRecyclerOptions.Builder<Review>()
.setQuery(reviewQuery, object : SnapshotParser<Review> {
override fun parseSnapshot(snapshot: DocumentSnapshot): Review {
return snapshot.toObject(Review::class.java)!!.also {
it.id = snapshot.id
}
}
}).setLifecycleOwner(this)
reviewRecycler.adapter=ReviewBurnAdapter(reviewBurnOptions.build())}
class ReviewBurnAdapter(options: FirestoreRecyclerOptions<Review>) :
FirestoreRecyclerAdapter<Review, ReviewBurnAdapter.ViewHolder>(options) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {//I never reach this point
val view = LayoutInflater.from(parent.context).inflate(R.layout.row_review, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int, item: Review) {
holder.apply {
holder.itemView.rowAuthor.text = item.author
}
}
inner class ViewHolder(override val containerView: View) :
RecyclerView.ViewHolder(containerView), LayoutContainer
}
Class to serialize into:
import com.google.firebase.firestore.Exclude
import com.google.firebase.firestore.PropertyName
import java.util.*
class Review(
#get:Exclude var id: String = "DEVIL",
#JvmField #PropertyName(AUTHOR) var author: String = "",
#JvmField #PropertyName(WRITEUP) var writeup: String = "",
//#JvmField #PropertyName(MOMENT) var moment:Date=Date(1997,12,1),
#JvmField #PropertyName(RATING) var rating: Int = 0
) {
companion object {
const val AUTHOR = "author"
const val WRITEUP = "writeup"
const val RATING = "rating"
//const val MOMENT="moment"
}
}
Also, there's no errors, it just never reaches the code that would generate and populate with viewHolders.
Alright, the fix was ultra simple, as #Prashant Jha pointed out, I hadn't specified a layout manager for my RecyclerView -_-
To be crystal clear, I added app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
to my xml, and everything worked.
I know to capitalize the arraylist with strings as data can be done with
list.map({ it.capitalize()})which returns as a list.
Now, what if it's a data class instead of strings?
class MainActivity : AppCompatActivity() {
val animals: ArrayList<Animals> = ArrayList()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
addAnimals()
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.addItemDecoration(DividerItemDecoration(this,DividerItemDecoration.VERTICAL))
recyclerView.adapter = AnimalAdapter(this, animals)
}
data class Animals(val name: String, val type: String)
fun addAnimals() {
animals.add(Animals("dog","bark"))
animals.add(Animals("cat","meow"))
animals.add(Animals("owl","hoot"))
animals.add(Animals("cheetah","roar, growl, snarl"))
animals.add(Animals("raccoon","trill"))
animals.add(Animals("bird","chirp"))
animals.add(Animals("snake","hiss"))
animals.add(Animals("lizard","?"))
animals.add(Animals("hamster","squeak"))
animals.add(Animals("bear","roar, growl"))
animals.add(Animals("lion","roar, growl, snarl"))
animals.add(Animals("tiger","roar, growl, snarl"))
animals.add(Animals("horse","neigh"))
animals.add(Animals("frog","croak"))
animals.add(Animals("fish","?"))
animals.add(Animals("shark","?"))
animals.add(Animals("turtle","?"))
animals.add(Animals("elephant","trumpet"))
animals.add(Animals("cow","moo"))
animals.add(Animals("beaver","?"))
animals.add(Animals("bison","moo"))
animals.add(Animals("porcupine","?"))
animals.add(Animals("rat","woof"))
animals.add(Animals("mouse","squeak"))
animals.add(Animals("goose","honk, hiss"))
animals.add(Animals("deer","bellow"))
animals.add(Animals("fox","bark, howl, growl, bay"))
animals.add(Animals("moose","bellow"))
animals.add(Animals("buffalo","moo"))
animals.add(Animals("monkey","scream, chatter"))
animals.add(Animals("penguin","?"))
animals.add(Animals("parrot","squawk"))
}
AnimalAdapter:
private class AnimalAdapter(val context: Context, val items: ArrayList<Animals>) : RecyclerView.Adapter<ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(context).inflate(R.layout.animal_item, parent, false))
}
override fun getItemCount(): Int {
return items.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.tvAnimalType?.text = items.get(position).name.capitalize()
holder.tvAnimalSounds?.text = items.get(position).type.capitalize()
holder.itemView.setOnClickListener {
val alertDialog: AlertDialog.Builder = AlertDialog.Builder(context)
alertDialog.setMessage("Success")
.setPositiveButton("Ok") { dialog, which -> dialog.dismiss()
}
.setNegativeButton("Cancel") { dialog, which -> dialog.dismiss()
}
val alert = alertDialog.create()
// set title for alert dialog box
alert.setTitle("AlertDialogExample")
// show alert dialog
alert.show()
}
}
}
private class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
// Holds the TextView that will add each animal to
val tvAnimalType = view.animal_type
val tvAnimalSounds = view.animal_sounds
}
P.S : I know that I can capitalize it in the adapter class while
setting it, which I have already done. But what if I have to do that
before passing the List to the adapter?
The first option is adding a capitalized name while creating animal object. If that's not possible then you can pass animals list like this
recyclerView.adapter = AnimalAdapter(this, animals.map({Animals(it.name.capitalize(),it.type)}));
Depending on what your needs are, you could either create an Animal object with already capitalized values:
class Animal(name: String, type: String) {
val name: String = name.capitalize()
val type: String = type.capitalize()
}
Note that in this case the Animal class is not a data class any more.
or map your list of animals before using it:
val mappedAnimals = animals.map { Animal(it.name.capitalize(), it.type.capitalize()) }
recyclerView.adapter = AnimalAdapter(this, mappedAnimals)
I wouldn't use mapping to capitalise all strings.
Actually, there is a better approach. You can capitalise those strings in a TextView, at view level.
Why do I think it is a better approach than changing your data?
You do not modify data. Now, your mapped model is different that original. It can be a source of mistakes, because Animal("Dog", "John") != Animal("DOG", "JOHN"). Also, when you change Animal class there is a chance that somewhere you should change something too.
Making the capitalisation at view level makes it much more easier to read, find and modify.
And it's easy:
<TextView>
...
android:textAllCaps=true
</TextView>
#see:
https://developer.android.com/reference/android/widget/TextView.html#attr_android:textAllCaps