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
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.
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
I marked the parts that were added (added to code) after the moment
when the application was working, the data was successfully downloaded
from the database. I may be mistakenly trying to pass this information
to another screen. I tried to find a video that connects to the
database and forwards that data of recicler on another screen, but
without success, or they are in Java, which I understand less.
MySecondActivity
class BookDescription : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_book_description)
var books = intent.getSerializableExtra("noti") as Book //added to code
Glide.with(this).load(books.imageUrl).into(bookImg2)// added to code
nameTxt2.text = books.name //added to code
autorTxt2.text = books.writer //added to code
}
}
MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var adapter : Adapter
private val viewModel by lazy { ViewModelProviders.of(this).get(MainViewModel::class.java)}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setUpRecyclerView()
}
private fun setUpRecyclerView(){
adapter = Adapter(this){
startBookDescription()
}
recycle.layoutManager = GridLayoutManager(this, 2)
recycle.adapter = adapter
observerData()
}
fun observerData(){
viewModel.fetchUserData().observe(this,Observer{
adapter.setListdata(it)
adapter.notifyDataSetChanged()
})
}
private fun startBookDescription(){
val intent = Intent (this, BookDescription::class.java )
startActivity(intent)
}
}
Class Adapter with inner class Holder
class Adapter(private val context: Context,
private val onItemCliked: () -> Unit ) : RecyclerView.Adapter<Adapter.Holder>() {
private var datalist = mutableListOf<Book>()
fun setListdata(data: MutableList<Book>){
datalist = data
}
inner class Holder(itemView : View) : RecyclerView.ViewHolder(itemView){
fun bindView(book: Book, onItemClicked: () -> Unit){
Glide.with(context).load(book.imageUrl).into(itemView.bookImg)
itemView.nameTxt.text = book.name
itemView.autorTxt.text= book.writer
itemView.setOnClickListener { onItemClicked.invoke() }
itemView.bookImg.setOnClickListener(View.OnClickListener { //added
val intent = Intent(context, BookDescription::class.java)//added to code
intent.putExtra("noti", book)//added to code
context.startActivity(intent)//added to code
})
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val view = LayoutInflater.from(context).inflate(R.layout.book_format, parent,
false )
return Holder(view)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
val book = datalist[position]
holder.bindView(book, onItemCliked)
}
override fun getItemCount(): Int {
return if (datalist.size> 0){
datalist.size
}else{
0
}
}
}
The problem is here:
intent.putExtra("noti", book)
The book variable is of type Book, which is apparently neither a Parcelable or Serializable class. You must implement one of these two interfaces in the Book class in order to add it to an Intent or Bundle.
Assuming Book is made up of simple data types (String, Int, etc), then you can use the #Parcelize annotation to easily implement Parcelable. More here: https://developer.android.com/kotlin/parcelize
In your bindView() method, you have this block of code:
val intent = Intent(context, BookDescription::class.java)//added to code
intent.putExtra("noti", book)//added to code
context.startActivity(intent)//added to code
})
However, you don't actually do anything with this Intent; you start your activity from another place:
private fun startBookDescription(){
val intent = Intent (this, BookDescription::class.java )
startActivity(intent)
}
You will have to pass the Book instance to this method (via invoke(book)). This will require a corresponding type change to the click listener parameter of your adapter.
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 have a recycler view adapter in android. Part of my adapter class looks like this:
private lateinit var itemLongClick: ItemLongClick
override fun onCreateViewHolder(parent: ViewGroup, a: Int): RecyclerAdapter.ViewHolder {
// Define And Initialize The Custom View And Its Holder//
val myView = LayoutInflater.from(parent.context).inflate(customLayout, parent, false)
val viewHolder = ViewHolder(myView)
// What Happens When A List Item Is Long Clicked//
myView.setOnLongClickListener { view ->
// Differ Action To Class Instance//
itemLongClick.longClicked(context, viewHolder.layoutPosition, view)
// End Function//
true
}
// Returns The Custom View//
return viewHolder
}
fun setItemLongClick(itemLongClick: ItemLongClick) {
// Sets The Value For this.itemLongClick//
this.itemLongClick = itemLongClick
}
I created an interface tat looks like this:
interface ItemLongClick {
// Function Declaration For When An Item Is Long Clicked//
fun longClicked(context: Context, position: Int, view: View)
}
Instead of writing my on long click code in the adapter class I want to differ it to the activity that is calling the adapter class. I know one way of doing this is to make a kotlin interface then call it in the other class like this
userAdapter.setItemLongClick(object: ItemLongClick {
override fun longClicked(context: Context, position: Int, view: View) {
}
})
But this looks messy. I know java interfaces work with SAM but I don't want to do that either. What I want is for the onLongClick to be a Lambda but I'm not sure how to set up a Kotlin lambda expression to make this work and I can't find a good example anywhere.
Thanks in advance
You have two options:
1.) replace interface with typealias
typealias ItemLongClick = (Context, Int, View) -> Unit
2.) add an extension function for setting the interface as a lambda instead of with anonymous object
inline fun UserAdapter.setItemLongClick(crossinline longClick: (Context, Int, View) -> Unit) {
setItemLongClick(object: ItemLongClick {
override fun longClicked(context: Context, position: Int, view: View) {
longClick(context, position, view)
}
})
}
Now you can call
userAdapter.setItemLongClick { context, position, view ->
...
}
I had an adapter that i need to change the data based on a switch and i did something like this:
ListAdapter(private val context: Context, private val switchListener: (Boolean) -> Unit)
Then where i binded the header of my sectioned list i had:
private fun bindHeader(holder: HeaderViewHolder) {
holder.switch.setOnCheckedChangeListener { _, isChecked ->
callbackSwitchListener(isChecked)
}
}
And in my fragment:
private fun setupRecyclerView() {
fabricationDataListAdapter =
FabricationDataListAdapter(context!!) { isChecked: Boolean -> switchControl(isChecked) }
val layoutManager = ListLayoutManager(context!!)
this.recycler_view_all.layoutManager = layoutManager
this.recycler_view_all.adapter = fabricationDataListAdapter
}
Where the fun switchControl changed the data based on the boolean.
I'm not sure if this is what you need, i'm in a bit of a hurry, but this is called high order functions in kotlin, if i'm not mistaken.
As the Kotlin documentation for the Kotlin 1.4 release points out:
Before Kotlin 1.4.0, you could apply SAM (Single Abstract Method) conversions only when working with Java methods and Java interfaces from Kotlin. From now on, you can use SAM conversions for Kotlin interfaces as well. To do so, mark a Kotlin interface explicitly as functional with the fun modifier.
fun interface Operation1 {
operator fun invoke(x: String): String
}
fun interface Operation2 {
fun doSomething(x: Int): String
}
val operation1 = Operation1 { "$it world!" }
val operation2 = Operation2 { "$it world!" }
fun main() {
// Usage: First sample.
println(operation1("Hello"))
println(operation2.doSomething(0))
// Usage: Second sample.
println(Operation1 { "$it world!" }("Hello"))
println(Operation2 { "$it!" }.doSomething(0))
}
You can read more about functional interfaces here.
In below code I using filterable adapter to do search on list. Here I am using lambda as callback to notify to view model when no data is found for the search.
Instantiating Adapter in ViewModel. And passing lambda
var matterAdapter = MatterAdapter(matterList) {
//todo - got callback
}
Adapter
class MatterAdapter (var filteredList : MutableList<AndroidViewModel>, val funcNoSearchData : () -> Unit) : DataBindingRecyclerViewAdapter(filteredList), Filterable {
private var mViewModelMap: MutableMap<Class<*>, Int> = mutableMapOf()
private var originalList : MutableList<AndroidViewModel> = mutableListOf()
private val mFilter = ItemFilter()
init {
mViewModelMap.put(MatterRowViewModel::class.java, R.layout.row_matter)
}
override fun getViewModelLayoutMap(): MutableMap<Class<*>, Int> {
return mViewModelMap
}
override fun getFilter(): Filter {
return mFilter
}
private inner class ItemFilter : Filter() {
override fun performFiltering(constraint: CharSequence): FilterResults {
val filterString = constraint.toString().toLowerCase()
val results = FilterResults()
val list = originalList
val count = list.size
val nlist = ArrayList<AndroidViewModel>(count)
var filterableString: String
for (i in 0 until count) {
filterableString = (list.get(i) as MatterRowViewModel).matter.casestitle!!
if (filterableString.toLowerCase().contains(filterString)) {
nlist.add(list.get(i))
}
}
results.values = nlist
results.count = nlist.size
return results
}
override fun publishResults(constraint: CharSequence, results: Filter.FilterResults) {
filteredList.clear()
filteredList.addAll(results.values as ArrayList<AndroidViewModel>)
// sends empty search callback to viewmodel
if(filteredList.size == 0) {
funcNoSearchData()
}
notifyDataSetChanged()
}
}
fun resetSearch() {
filteredList.clear()
filteredList.addAll(originalList)
notifyDataSetChanged()
}
fun refreshData() {
originalList = ArrayList(filteredList)
notifyDataSetChanged()
}
}