Android - Store ArrayList persistent - android

My app is displaying a list of various categories (herbs, side dishes, ..) in a RecyclerView. Depending on the category you clicked on, a new Activity with a new RecylcerView opens containing all the ingredients.
Right now I have an ArrayList which gets filled with the ingredients via ".add" depending on the choosen category.
The problem im facing right now is, that I want to implement an option for the user to add own Ingredients. I tried storing the ArrayList containing the ingredients in SharedPreferences by using Gson, but I couldn't manage to add elements, since it always overwrote the current list.
What would be the best way to store the ingredients? A room, sqlite, ..?
Without further explanation, the ingredient list will only contain about 70 items max.
Thanks in advance.
Edit:
CatList.kt
class CatList : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cat_list)
//Create List for categories
val cats = ArrayList<IngCat>()
//Fill categories
cats.add(IngCat(R.drawable.herbs, "Herbs"))
cats.add(IngCat(R.drawable.fluessiges, "Liquids"))
cats.add(IngCat(R.drawable.festes, "Solids"))
cats.add(IngCat(R.drawable.beilagen, "Sides"))
//Recyclerview
id_rv_CatList.layoutManager = LinearLayoutManager(this)
id_rv_CatList.adapter =
CatListAdapter(cats) {listItem, position -> //go to Ingredient List Activity
goToIngList(position, listItem.name)
}
//id_rv_CatList.addItemDecoration(DividerItemDecoration(this,DividerItemDecoration.HORIZONTAL))
//actionbar
val actionbar = supportActionBar
//set actionbar title
actionbar!!.title = "Ingredient - Categories"
}
private fun goToIngList(cat: Int, name: String){
val intent = Intent(this, IngList::class.java)
intent.putExtra("Category", cat)
intent.putExtra("Name", name)
startActivity(intent)
}
}
data class IngCat(var mImageResource:Int, var name:String)
IngList.kt
class IngList : AppCompatActivity() {
companion object {
var categoryChoosen : Int = 0
var catName : String = "Err"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_ing_list)
//Initilize Ingredient List
val ings :ArrayList<IngIng> = ArrayList()
//Get category and category name
categoryChoosen = intent.getIntExtra("Kategorie",0)
catName = intent.getStringExtra("Name")!!
when (categoryChoosen) {
0 -> {
ings.add(IngIng("https://doeel.com/images/thumbnails/1100/900/detailed /92/Turmeric_Powder___Holud_Gura__.png", "Turmeric Powder"))
}
1 -> ings.add(IngIng("https://www.miraherba.de/4923-large_default/bio-ghee-300-g.jpg", "Ghee"))
2 -> ings.add(IngIng("https://www.organicfacts.net/wp-content/uploads/coriander-1.jpg", "Coriander leaves"))
3 -> ings.add(IngIng("https://gbc-cdn-public-media.azureedge.net/img75602.1426x713.jpg", "Potatoes"))
}
//Actionbar Settings
setSupportActionBar(toolbar)
val actionbar = supportActionBar
actionbar!!.title = "Ingredients- $catName"
actionbar.setDisplayHomeAsUpEnabled(true)
//Recyclerview
id_rv_IngList.layoutManager = GridLayoutManager(this,2)
id_rv_IngList.adapter =
IngListAdapter(ings) {//ClickListener RecyclerView
Toast.makeText(this, "Item clicked: ${it.name}", Toast.LENGTH_SHORT).show()
}
//Actionbar
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.actionbar_ing_list, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.id_menu_action_add -> {
val intent = Intent(this, AddIngredient::class.java)
startActivity(intent)
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
}
IngListAdapter.kt
class IngListAdapter (private val ings: ArrayList<IngIng>, val clickListener: (IngIng)->Unit): RecyclerView.Adapter<RecyclerView.ViewHolder>(){
override fun getItemCount(): Int = ings.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val v: View = LayoutInflater.from(parent.context).inflate(R.layout.recyclerview_ing_list_item, parent, false)
return IngViewHolder(v)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
var currentItem = ings.get(position)
when (holder) {
is IngViewHolder -> {
holder.tvIngList.text = currentItem.name
// holder.ivIngImage.setImageResource(currentItem.mImageResource)
Picasso.get().load(currentItem.mImageResource).placeholder(R.drawable.ic_broken_image_black_200dp).error(R.drawable.ic_broken_image_red_24dp).into(holder.ivIngImage)
holder.cvIngCard.setOnClickListener{
clickListener(currentItem)
}
}
}
}
}
class IngViewHolder (view: View) : RecyclerView.ViewHolder(view) {
val tvIngList: TextView = view.id_text_ing
val ivIngImage: ImageView = view.id_img_ing
val cvIngCard: MaterialCardView = view.id_cv_ing_list
}

I personally think Json/Gson in a SharedPreference is the easiest way to go if there are so few items. The way I would handle it is to store the list in memory at application startup, and persist the list back to the SharedPreference when the app is shut down. Also when the app gets stopped for good measure because you can't 100% be sure onDestroy will be called.
So first I'd make a class to store the data. If you were using Fragments that all are in the same Activity, you'd put this in a ViewModel. But since they are separate Activities, you need a singleton for them. (Google doesn't recommend using multiple Activities because it's hard to share data between them. But it's not impossible. It's what we did before Fragments.)
To do it as a singleton, you could have a class like this:
class IngredientsRepo private constructor (application: Application) {
companion object {
private val INSTANCE: IngredientsRepo? = null
fun getInstance(application: Application) =
INSTANCE ?: IngredientsRepo(application).also { INSTANCE = it }
private const KEY_JSON_PREF = "ingredientsJson"
}
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
val herbsList: MutableList<IngCat>
val liquidsList: MutableList<IngCat>
val solidsList: MutableList<IngCat>
val sidesList: MutableList<IngCat>
init {
val json = sharedPreferences.getString(KEY_JSON_PREF, null)
if (json == null) {
// initialize your list contents for the first time
} else {
// convert your json and fill the data into your lists
}
}
fun save {
val jsonString = // Convert your lists to Json
sharedPreferences.edit().putString(KEY_JSON_PREF, jsonString).apply()
}
}
This class becomes responsible for setting up your lists. You can retrieve it from any Activity with IngredientsRepo.getInstance(this) and you can add and remove items from the lists whenever you like. You can also call save on it whenever you like to persist the latest data. It's probably sufficient to do this in onStop() of any Activity that modifies the list.
More properly, the data in this class would only be exposed with immutable lists, and you'd add functions for adding and removing items, so only this class directly modifies the lists. I didn't want to overcomplicate the example, but it would be better for encapsulation to not have Activities (which are supposed to be pure UI components) directly modifying data structures.

Related

How to update a textView of recycler view with live data

I want to update a textview of the recycler view with a live data.
how can I do this? I am a beginner please help
this is my viewModel class
class ViewModelClass : ViewModel() {
var rating = MutableLiveData<String>("NA")
}
I have a textView in a recycler view which I want to update after taking the text from the edit text but I could not do this because I can not get the reference of that textview in the MainActivity.
btnSave.setOnClickListener {
val rating : String = etRating.text.toString()
viewModel.rating.value = rating
}
Follow these steps:
Create an adapter to configure your recyclerview. Make sure to
implement de DiffUtils correctly.
class MyAdapter: ListAdapter<String, MyAdapter.MyAdapterViewHolder>(DIFF_CALLBACK) {
class MyAdapterViewHolder(
private val binding: ItemSimplePosterBinding
): RecyclerView.ViewHolder(binding.root) {
// implementation here
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
// need a unique identifier to have sure they are the same item. could be a comparison of ids. In this case, that is just a list of strings just compare like this below
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
// compare the objects
return oldItem == newItem
}
}
}
}
On your ViewModel, create a function that will add the new rating to your list:
class MyViewModel: ViewModel() {
private val _rating = MutableLiveData<List<String>>(emptyList())
val rating: LiveData<List<String>>
get() = _rating
// implement a function that adds a new item on the rating list
fun addRating(newRate: String) {
val newList = mutableListOf<String>()
_rating.value?.let { newList.addAll(it) }
newList.add(newRate)
_rating.value = newList
}
}
See that I didn't expose the mutableLiveData, always pay attention to following this best practice to let private the mutableLiveData and let public just the immutable live data.
Then, the last step is to observe the live data changes. So, when the list change on viewmodel, you will receive the updated list on the observer just submit the list for adapter:
class MyFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// set the adapter to your recycler view
// observe changes of your live data
viewModel.rating.observe(viewLifecycleOwner) { ratings ->
adapter.submitList(ratings)
}
}
}

How can i change the visibility of a textview in every element of the recyclerview?

I have an arraylist (called Itemlist) of all recyclerview elements. In each element there are 2 textviews - a german and english word. only one of them is shown (because they overlap). when i click on the element it shows the other language (for example: the german word is set to gone and the english word is visible now).
Now I want a function which sets all english textviews (in every element) to gone and the german to visible. My problem is - i dont know how to reach all elements in this arraylist and check the visibility of the textviews. in my example it resets only the first word.
For better understanding
Here is the code:
fun reset_to_EN() {
ItemList.forEach { test_if_german() }
}
OR
fun reset_to_EN2() {
for (item in ItemList) {
test_if_german()
}
}
Check visibility
fun test_if_german(){
if (text_view_de.visibility == View.VISIBLE) {
text_view_en.visibility = View.VISIBLE
text_view_de.visibility = View.GONE
}
adapter.notifyDataSetChanged()
}
If you can please show me a code example for better understanding.
Thanks to everyone who tries to help.
Or here is the whole code for the adapter and mainActivity if it's needed:
class Adapter(
val c: Context,
private val ArrList: ArrayList<Item>):
RecyclerView.Adapter<Adapter.ViewHolder>()
{
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
return ViewHolder(inflater)
}
override fun getItemCount() = ArrList.size
inner class ViewHolder(v: View) : RecyclerView.ViewHolder(v), View.OnClickListener {
var textViewDe: TextView = v.text_view_de
var textViewEn: TextView = v.text_view_en
private var menueImage: Button
init {
v.setOnClickListener(this)
textViewDe = v.findViewById(R.id.text_view_de)
textViewEn = v.findViewById(R.id.text_view_en)
menueImage = v.findViewById(R.id.menu_button)
menueImage.setOnClickListener { popupMenu(it) }
}
private fun popupMenu(v:View) {
val drop = PopupMenu(c, v)
val position = ArrList[adapterPosition]
drop.inflate(R.menu.drop_menu)
drop.setOnMenuItemClickListener {
when(it.itemId){
R.id.edit_menu->{
val v2 = LayoutInflater.from(c).inflate(R.layout.add_item_layout,null)
val DE = v2.findViewById<EditText>(R.id.editText)
val EN = v2.findViewById<EditText>(R.id.editText2)
AlertDialog.Builder(c)
.setView(v2)
.setPositiveButton("Ok"){
dialog,_->
position.Englisch = DE.text.toString()
position.Deutsch = EN.text.toString()
notifyDataSetChanged()
//Toast.makeText(c,"User Information is Edited",Toast.LENGTH_SHORT).show()
dialog.dismiss()
}
.setNegativeButton("Cancel"){
dialog,_->
dialog.dismiss()
}
.create()
.show()
true
}
R.id.delete_menu-> {
ArrList.removeAt(adapterPosition)
notifyDataSetChanged()
//Toast.makeText(c,"entfernt",Toast.LENGTH_SHORT).show()
true
}
else -> true
}
}
drop.show()
val popup = PopupMenu::class.java.getDeclaredField("mPopup")
popup.isAccessible = true
val menu = popup.get(drop)
menu.javaClass.getDeclaredMethod("setForceShowIcon",Boolean::class.java)
.invoke(menu,true)
}
override fun onClick(p0: View?) {
if (textViewDe.visibility == View.VISIBLE) {
textViewDe.visibility = View.GONE
textViewEn.visibility = View.VISIBLE
} else {
textViewDe.visibility = View.VISIBLE
textViewEn.visibility = View.GONE
}
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentItem = ArrList[position]
holder.textViewDe.text = currentItem.Deutsch
holder.textViewEn.text = currentItem.Englisch
}
And MainActivity:
class MainActivity : AppCompatActivity() {
//DEFINITION
private lateinit var addButton: FloatingActionButton
private lateinit var ItemList: ArrayList<Item>
private lateinit var recy: RecyclerView
private lateinit var adapter: Adapter
//ONCREATE
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//FINDVIEWBYID
addButton = findViewById(R.id.addingBtn)
ItemList = ArrayList()
recy = findViewById(R.id.recycler_view)
//RECYCLERVIEW
adapter = Adapter(this, ItemList)
recy.layoutManager = LinearLayoutManager(this)
recy.adapter = adapter
//FUNCTION-CALL
addButton.setOnClickListener { addInfo() }
}
//FUNKTIONENS
private fun addInfo() {
val inflter = LayoutInflater.from(this)
val v = inflter.inflate(R.layout.add_item_layout, null) //
val eng = v.findViewById<EditText>(R.id.editText)
val deu = v.findViewById<EditText>(R.id.editText2)
val addDialog = AlertDialog.Builder(this)
addDialog.setView(v)
addDialog.setPositiveButton("OK"){ dialog, _->
val eng2 = eng.text.toString()
val deu2 = deu.text.toString()
val UUID = UUID.randomUUID()
ItemList.add(Item(UUID, eng2, deu2))
adapter.notifyDataSetChanged()
//Toast.makeText(this, "Adding Success", Toast.LENGTH_SHORT).show()
dialog.dismiss()
}
addDialog.setNegativeButton("Cancel"){ dialog, _->
dialog.dismiss()
}
addDialog.create()
addDialog.show()
}
fun clearData() {
ItemList.clear()
adapter.notifyDataSetChanged()
Toast.makeText(this, "Alles gelöscht", Toast.LENGTH_SHORT).show()
}
fun reset_all_EN() {
//ArrayList = ItemList
val size: Int = ItemList.size
for (i in 0 until size) {
if (text_view_de.visibility == View.VISIBLE) {
text_view_en.visibility = View.VISIBLE
text_view_de.visibility = View.GONE
}
adapter.notifyDataSetChanged()
}
}
fun reset_to_EN() {
// using forEach() method
ItemList.forEach { test_if_german() }
}
fun reset_to_EN2() {
for (item in ItemList) {
test_if_german()
}
}
fun test_if_german(){
if (text_view_de.visibility == View.VISIBLE) {
text_view_en.visibility = View.VISIBLE
text_view_de.visibility = View.GONE
}
adapter.notifyDataSetChanged()
}
//MENU CLASSES
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.open_menu -> {
val intent = Intent(this, InfoActivity::class.java)
startActivity(intent)
}
R.id.open_menu2 -> {
val intent = Intent(this, SettingsActivity::class.java)
startActivity(intent)
}
R.id.reset_all -> {
reset_to_EN2()
}
}
return super.onOptionsItemSelected(item)
}
}
Since I don't see any declaration of text_view_de or text_view_en, I'm guessing you're using synthetic view properties from the deprecated Android Kotlin Extensions. Assuming that is the case:
When you use text_view_de, it is performing a search in your view hierarchy for the first view it finds with the matching ID. So even though you are doing it within a for loop that iterates through your list of items, you are only working with the same view, over and over.
Edit:
I realized you want to be able to toggle individual views and you were only asking how to add a button to reset all views back to the same language. If this is the case, it does not make sense to add a property to the adapter that controls the state of all views at once like I had suggested in the previous revision of this answer.
Instead, you need to change your data model to have a Boolean that determines which specific language that specific item should show. The problem with how you're doing it now in your click listener is that it is trying to use the Views themselves to determine what state the item is when you change it, but this will cause weird glitches when items scroll off of the screen and back on because ViewHolders get recycled and assigned to different items when they go off and back on screen.
To get started, add a Boolean for the state of the item to your Item class. I don't know exactly what your class looks like now, so adapt this as needed:
data class Item (
val UUID: Long,
val english: String,
val deutsch: String,
var isShowDeutch: Boolean = true
)
A good practice is to have your Adapter class expose a callback for items being clicked so the outside class (Activity) is responsible for manipulating the data model and the Adapter's responsibility is limited to connecting data to views, not manipulating data. So create a callback that the Activity can implement that toggles a single Item's isShowDeutsch property. And when you bind data to a view, use that item's isShowDeutsch to determine visibility.
In Adapter class:
var onItemClickListener: ((itemPosition: Int)->Unit)? = null
//...
// In ViewHolder:
override fun onClick(view: View) {
itemClickListener?.invoke(adapterPosition)
}
//...
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentItem = ArrList[position]
holder.textViewDe.text = currentItem.Deutsch
holder.textViewEn.text = currentItem.Englisch
holder.textViewDe.isVisible = currentItem.isShowDeutsch
holder.textViewEn.isVibible = !currentItem.isShowDeutsch
}
In your Activity when you set up your adapter, you can define a click listener for it that toggles the state of that single item and notifies the adapter of the change:
//RECYCLERVIEW
adapter = Adapter(this, ItemList)
recy.layoutManager = LinearLayoutManager(this)
recy.adapter = adapter
adapter.onItemClickListener = { position ->
ItemList[position].apply { isShowDeutsch = !isShowDeutsch }
adapter.notifyItemChanged(position)
}
And finally, to reset all items back to their original language, you can iterate the items in your list and then notify the adapter. This is more appropriate to do in your Activity, since the Adapter should not be responsible for manipulating data.
fun resetLanguage() {
for (item in ItemList) {
item.isShowDeutsch = true
}
adapter.notifyDataSetChanged()
}
I also recommend you change lateinit var ItemList: ArrayList<Item> to val ItemList = ArrayList<Item>(). It is error prone to have a mutable list type in a mutable var property because there are two different ways to change it and it creates the possibility of having your adapter looking at a different list than the one your Activity is working with.

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)

RecyclerView doesn't update its view but List is updated

I am using recyclerView to show list of available apps in device..moreover I am using bottomSheet to show more details about selected app ...in this section, I place uninstall button ...here I use uninstall code and from onActivityResult method in BottomSheetDialog.kt file ... on OK pressed ....I want to delete that app/item from list and update View....here list is correct in coding but recyclerView doesn't update its list
Note: I debug the code and found that list got updated in BottomSheet File...I comment out that ....but
recyclerView doesn't
I searched on internet, but didn't find solution which fits in my case
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView.adapter = Adapter(applicationList) // here I send mutable list of all apps in device to adapter
recyclerView.layoutManager = LinearLayoutManager(this)
private fun getApps(List: MutableList<ResolveInfo>): MutableList<AppData> {
// here I return list to adapter with details of installed apps like icon, name, packageName etc
}
DataClass
data class AppData(
val icon: Drawable,
val name: String,
val packageName: String
.....
.....)
Adapter.kt
class Adapter(private val listOfApps: MutableList<AppData>) :
RecyclerView.Adapter<Adapter.ViewHolder>() {
// here I receive mutableList in constructor of Adapter
class ViewHolder(appView: View) : RecyclerView.ViewHolder(appView), View.OnClickListener,
View.OnLongClickListener {
init { // initiate both click listeners
appView.setOnClickListener(this)
appView.setOnLongClickListener(this)
}
// call elements from activity.xml
val icon: ImageView = appView.App_icon
val name: TextView = appView.App_name
val size: TextView = appView.App_size
override fun onClick(v: View?) {
Toast.makeText(v?.context, "OnClick", Toast.LENGTH_SHORT).show()
}
override fun onLongClick(v: View?): Boolean {
val bottomSheetDialog = BottomSheetDialog(currentItem, appList)
// send currentItem and all List to BottomSheetDialog to show details with the help of function
// Show bottomSheet on LongPress
return true
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder // done
override fun getItemCount() = listOfApps.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentItem = listOfApps[position]
holder.icon.setImageDrawable(currentItem.icon)
holder.name.text = currentItem.name
holder.size.text = currentItem.size
}
BottomSheetDialog.kt ...... here in onActivtyResult I delete item and call notify method ... problem is here
class BottomSheetDialog(private val appData: AppData, private val appList: MutableList<AppData>) :
BottomSheetDialogFragment() {
// here I receive appData and AppList in constructor from Adapter OnLongPress
override fun onCreateView() // done
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// here when bottomSheet popup and on click of uninstall.....I check whether user click on OK or CANCEL in onActivity Method (overidden below)
Uninstall_App.setOnClickListener {
// permission in manifest added
val intent = Intent(Intent.ACTION_DELETE)
intent.data = Uri.parse("package:${appData.packageName}")
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
startActivityForResult(intent, 1)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// get result from uninstall dialog
if (resultCode == -1) { // ok pressed
Toast.makeText(context, "ok clicked", Toast.LENGTH_SHORT).show()
dismiss()
// here when user pressed OK....delete that item from List
val index = appList.indexOf(appData)
appList.removeAt(index)
Adapter(appList).notifyItemRemoved(index)
Adapter(appList).notifyDataSetChanged()
// I check above three line by debugging it
// 1. val index .. return index of current item
// 2. this line remove that item
// 3. Adapter(appList) .... notify Item removed
// 4. here that indexed item is removed but view is not updated
// Note: if it is wrong which is the best method to do this
} else if (resultCode == 0) { // cancel pressed
Toast.makeText(context, "Cancel Click", Toast.LENGTH_SHORT).show()
}
}
these lines
Adapter(appList).notifyItemRemoved(index)
Adapter(appList).notifyDataSetChanged()
are both creating new adapters, notify them and.. thats all. they aren't attached to any RecyclerView (as you do in onCreate), so won't be drawn anywhere
you should notify adapter already set for RecyclerView - keep reference in Activity and refer to it instead of creating new one
What you did here is created two new adapters (that have no relation to the adapter used by recycler view except the type of adapter is the same):
Adapter(appList).notifyItemRemoved(index)
Adapter(appList).notifyDataSetChanged()
You can create an interface to listen for changes from BottomSheetDialog:
interface OnAppDeletedListener {
fun appDeletedAtIndex(index: Int)
}
Update your BottomSheetDialog to accept an additional argument of type OnAppDeletedListener:
class BottomSheetDialog(private val appData: AppData, private val appList: MutableList<AppData>, private val listener: OnAppDeletedListener) :
BottomSheetDialogFragment() {
...
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
val index = appList.indexOf(appData)
listener.appDeletedAtIndex(index)
dismiss()
}
}
}
Update your adapter. It must not be responsible for showing any dialogues. Activity or Fragment is responsible for that.
class Adapter(private val listOfApps: MutableList<AppData>, private val longClickListener: View.OnLongClickListener) :
RecyclerView.Adapter<Adapter.ViewHolder>() {
// here I receive mutableList in constructor of Adapter
class ViewHolder(appView: View) : RecyclerView.ViewHolder(appView), View.OnClickListener {
init { // initiate both click listeners
appView.setOnClickListener(this)
appView.setOnLongClickListener(longClickListener)
}
// call elements from activity.xml
val icon: ImageView = appView.App_icon
val name: TextView = appView.App_name
val size: TextView = appView.App_size
override fun onClick(v: View?) {
Toast.makeText(v?.context, "OnClick", Toast.LENGTH_SHORT).show()
}
}
}
And update your activity code:
class MainActivity : AppCompatActivity() {
private lateinit var adapter: Adapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val longClickListener = object: View.OnLongClickListener {
override fun onLongClick(v: View?): Boolean {
displayAppInfoDialog()
return true
}
}
adapter = Adapter(applicationList)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
}
private fun displayAppInfoDialog() {
val listener = object: OnAppDeletedListener() {
fun appDeletedAtIndex(index: Int) {
adapter.notifyItemRemoved(index)
}
}
val bottomSheetDialog = BottomSheetDialog(currentItem, appList, listener)
bottomSheetDialog.show()
}
...
}
As already stated, you are not updating the existing Adapter, you are instead creating two new instances.
Replace this line:
recyclerView.adapter = Adapter(applicationList) // here I send mutable list of all apps in device to adapter
With
this.adapter = Adapter(applicationList)
recyclerView.adapter = this.adapter
Also add val adapter: Adapter? to your class.
Now you have a reference to the adapter the RecyclerView has.
Finally, when you want to "update" it:
// here when user pressed OK....delete that item from List
val index = appList.indexOf(appData)
appList.removeAt(index)
Adapter(appList).notifyItemRemoved(index)
Adapter(appList).notifyDataSetChanged()
Should become...
// here when user pressed OK....delete that item from List
val index = appList.indexOf(appData)
appList.removeAt(index)
this.adapter.notifyItemRemoved(index)
this.adapter.notifyDataSetChanged()
IMPORTANT CAVEAT: There are other issues here with the separation of concerns in your code, but among them, the fact that the appList you use here, is a local list; does it contain the items from applicationList (the one you used when you created the adapter)? If it doesn't then you need to expose said list so you can either modify it/replace it, etc.
The Adapter does NOT manage the list for you, it merely uses it to adapt each item to a ViewHolder. If you modify the list the adapter has, and you tell it that you inserted an item at certain position, etc. All the adapter does is (a lot behind the scenes) and "re-binds" the view at that position (if it's visible) with the new data.

Changing list on fragment is changing in other fragments

I have a TabLayout/ViewPager with two fragments in my activity. The tabs are created and then I request some information. When I get this information, I update the RecyclerView of each tab.
Main Activity
private fun setViews() {
val adapter = TabsAdapter(supportFragmentManager)
adapter.addFragment(NewsFragment(), getString(R.string.dossier_activity_news))
adapter.addFragment(PhotosFragment(), resources.getString(R.string.dossier_activity_photos))
view_pager.adapter = adapter
tab_layout.setupWithViewPager(view_pager)
// request info
}
fun setPages(pages: List<Page>) {
// got info
((view_pager.adapter as TabsAdapter).getItem(0) as NewsFragment).setPages(pages)
((view_pager.adapter as TabsAdapter).getItem(1) as PhotosFragment).setPages(pages)
}
The problem is: In the second tab(PhotosFragment), I want for remove all pages that does not contain a photo/thumbnail.
NewsFragment
fun setPages(pages: List<Page>) {
if (pages.isNotEmpty()) recycler_view.adapter = PagesAdapter(this, pages)
}
PhotosFragment
fun setPages(pages: List<Page>) {
val iterator = (pages as ArrayList<Page>).iterator()
while (iterator.hasNext()) if (iterator.next().thumbnail == null) iterator.remove()
if (pages.isNotEmpty()) recycler_view.adapter = PagesAdapter(this, pages)
}
When I create this iterator on the second tab, all the pages on the first fragment are also updated to no photos/thumbnails pages. Each fragment has is own life cycle and by changing the content of one fragment it should not update the other right?
Could this be because of my PagesAdapter extend RecyclerView?
PagesAdapter
class PagesAdapter(private var fragment: Fragment, private var pages: List<Page>): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, type: Int): RecyclerView.ViewHolder {
return when (type) {
0 -> HeaderViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.article_header_item, parent, false))
else -> ArticleViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.article_item, parent, false))
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val type = getItemViewType(position)
when (type) {
0 -> setHeader(holder as PagesAdapter.HeaderViewHolder, position)
else -> setArticle(holder as PagesAdapter.ArticleViewHolder, position)
}
}
inner class HeaderViewHolder internal constructor(view: View) : RecyclerView.ViewHolder(view) {
val layout: RelativeLayout = view.article_header_layout
val image: ImageView = view.article_header_image
val title: AOTextView = view.title_header_text
val date: AOTextView = view.date_header_text
}
inner class ArticleViewHolder internal constructor(view: View) : RecyclerView.ViewHolder(view) {
val layout: RelativeLayout = view.article_layout
val image: ImageView = view.article_image
val strapline: AOTextView = view.strapline_text
val title: AOTextView = view.title_text
val date: AOTextView = view.date_text
}
}
Both fragments have the same pages list, but I want the second fragment to filter this list to show only pages with photos/thumbnails. By filtering the list in the second fragment, the first one shows the same filtered list. Should I aproach this differently? Or is some bug in my code?
You are sharing one list between two fragments, which is not a good idea.
Kotlin favors immutability and in it should be the default way of doing things, unless you have very good reason.
Two fragments are sharing list reference and that's why when one of it changes it, those changes are reflected in the other fragment.
What you should do is instead of
fun setPages(pages: List<Page>) {
val iterator = (pages as ArrayList<Page>).iterator()
while (iterator.hasNext()) if (iterator.next().thumbnail == null) iterator.remove()
if (pages.isNotEmpty()) recycler_view.adapter = PagesAdapter(this, pages)
}
you should write:
fun setPages(pages: List<Page>) {
val pagesWithThumbnails = pages.filter { it.thumbnail != null }
if (pagesWithThumbnails.isNotEmpty()) recycler_view.adapter = PagesAdapter(this, pagesWithThumbnails)
}
There you don't modify original list while having more readable and error-proof code.

Categories

Resources