I have got the Recyclerview working with findViewById but cant get it working with View bindings.
This Works
val tdList: RecyclerView = findViewById(R.id.td_list);
tdList.layoutManager = LinearLayoutManager(this)
tdList.setHasFixedSize(true);
tdList.adapter = reportAdapter
But this does not work
tdBinding.tdList.layoutManager = LinearLayoutManager(this)
tdBinding.tdList.setHasFixedSize(true);
tdBinding.tdList.adapter = reportAdapter
onCreateViewHolder or onBindViewHolder are never called and I get an error 'No adapter attached; skipping layout' so no data is shown in the RecyclerView.
I've been trying to get this working for a few days with View Bindings and finally found the code to get it working with findViewById and part of me thinks I should just be glad I got it working but would like to understand why (also as findViewById is quite expensive).
The full code is
DurationsReport.kt
package com.funkytwig.tasktimer
import android.database.Cursor
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.funkytwig.tasktimer.databinding.ActivityDurationsReportBinding
import com.funkytwig.tasktimer.databinding.TaskDurationsBinding
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
private const val TAG = "DurationsReportXX"
enum class SortColumns { NAME, DESCRIPTION, START_DATE, DURATION }
class DurationsReport : AppCompatActivity() {
private val reportAdapter by lazy { DurationsRVAdapter(this, null) }
var databaseCursor: Cursor? = null
var sortOrder = SortColumns.NAME
private val selection = "${DurationsContract.Columns.START_TIME} BETWEEN ? AND ?"
private var selectionArgs = arrayOf("0", "1559347199")
private lateinit var binding: ActivityDurationsReportBinding
private lateinit var tdBinding: TaskDurationsBinding
override fun onCreate(savedInstanceState: Bundle?) {
val func = "onCreate"
Log.d(TAG, func)
super.onCreate(savedInstanceState)
binding = ActivityDurationsReportBinding.inflate(layoutInflater)
tdBinding = TaskDurationsBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
Log.d(TAG, "$func: Setup adapter")
val tdList: RecyclerView = findViewById(R.id.td_list);
tdList.layoutManager = LinearLayoutManager(this)
tdList.setHasFixedSize(true);
tdList.adapter = reportAdapter
// This does not work
// tdBinding.tdList.layoutManager = LinearLayoutManager(this)
// tdBinding.tdList.setHasFixedSize(true);
// tdBinding.tdList.adapter = reportAdapter
loadData()
}
private fun loadData() {
val func = "loadData"
Log.d(TAG, func)
val order = when (sortOrder) {
SortColumns.NAME -> DurationsContract.Columns.NAME
SortColumns.DESCRIPTION -> DurationsContract.Columns.DESCRIPTION
SortColumns.START_DATE -> DurationsContract.Columns.START_DATE
SortColumns.DURATION -> DurationsContract.Columns.DURATION
}
Log.d(TAG, "order=$order")
GlobalScope.launch {
val cursor = application.contentResolver.query(
DurationsContract.CONTENT_URI, null, selection, selectionArgs, order
)
Log.d(TAG, "$func: cursor.count=${cursor?.count}")
databaseCursor = cursor
reportAdapter.swapCursor(cursor)?.close()
}
}
}
DurationsRVAdapter.kt
package com.funkytwig.tasktimer
import android.content.Context
import android.database.Cursor
import android.text.format.DateFormat
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.funkytwig.tasktimer.databinding.TaskDurationItemsBinding
import java.util.Locale
import java.lang.IllegalStateException
private const val TAG = "DurationsRVAdapterXX"
class DurationsRVAdapter(context: Context, private var cursor: Cursor?) :
RecyclerView.Adapter<DurationsRVAdapter.DurationsViewHolder>() {
inner class DurationsViewHolder(val bindings: TaskDurationItemsBinding) :
RecyclerView.ViewHolder(bindings.root)
private val dateFormat = DateFormat.getDateFormat(context)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DurationsViewHolder {
Log.d(TAG, "onCreateViewHolder")
val view =
TaskDurationItemsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return DurationsViewHolder(view)
}
override fun onBindViewHolder(holder: DurationsViewHolder, position: Int) {
val func = "onBindViewHolder"
Log.d(TAG, "$func: position = $position")
val cursor = cursor
if (cursor != null && cursor.count != 0) {
if (!cursor.moveToPosition(position)) {
throw IllegalStateException("Couldn't move cursor to position $position")
}
val name = cursor.getString(cursor.getColumnIndex(DurationsContract.Columns.NAME))
val description =
cursor.getString(cursor.getColumnIndex(DurationsContract.Columns.DESCRIPTION))
val startTime =
cursor.getLong(cursor.getColumnIndex(DurationsContract.Columns.START_TIME))
val totalDuration =
cursor.getLong(cursor.getColumnIndex(DurationsContract.Columns.DURATION))
val userDate =
dateFormat.format(startTime * 1000) // The database stores seconds, we need milliseconds
val totalTime = formatDuration(totalDuration)
holder.bindings.tdName.text = name
holder.bindings.tdDescription?.text = description
holder.bindings.tdStart.text = userDate
holder.bindings.tdDuration.text = totalTime
}
}
private fun formatDuration(duration: Long): String {
// convert duration Long to hours:mins:secs String (can be > 24 hours so cant use dateFormat)
val hours = duration / 3600
val remainder = duration - hours * 3600
val minutes = remainder / 60
val seconds = remainder % 60
return String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds)
}
override fun getItemCount(): Int {
val func = "getItemCount"
val count = cursor?.count ?: 0
Log.d(TAG, "$func: count=$count")
return count
}
fun swapCursor(newCursor: Cursor?): Cursor? {
val func = "swapCursor"
Log.d(TAG, func)
if (newCursor === cursor) return null
val numItems = itemCount
val oldCursor = cursor
cursor = newCursor
Log.d(TAG, "$func: cursor.count=${cursor?.count}")
Log.d(TAG, "$func newCursor.count=${newCursor?.count}, oldCursor.cont=${oldCursor?.count}")
if (newCursor != null) {
Log.d(TAG, "$func notify the observers about the new cursor")
// notify the observers about the new cursor
this.notifyDataSetChanged()
Log.d(TAG, "$func: notifyDataSetChanged")
} else {
Log.d(TAG, "$func Notify observer about lack of dataset")
// Notify observer about lack of dataset, all of it from 0 to newItems,
// i.e. whole range of records has gone
this.notifyItemRangeChanged(0, numItems)
Log.d(TAG, "$func: notifyItemRangeChanged(0, $numItems)")
}
return oldCursor
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
val func = "onAttachedToRecyclerView"
Log.d(TAG, "$func: ${recyclerView.adapter.toString()}")
super.onAttachedToRecyclerView(recyclerView)
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
val func = "onDetachedFromRecyclerView"
Log.d(TAG, "$func: ${recyclerView.adapter.toString()}")
super.onDetachedFromRecyclerView(recyclerView)
}
}
task_durations.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
app:layout_constraintHorizontal_chainStyle="spread">
<TextView
android:id="#+id/td_name_heading"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:background="?attr/colorButtonNormal"
android:padding="4dp"
android:text="#string/td_text_name"
android:textAlignment="viewStart"
android:textStyle="bold"
app:layout_constraintHorizontal_weight="2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread" />
<TextView
android:id="#+id/td_start_heading"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:background="?attr/colorButtonNormal"
android:padding="4dp"
android:text="#string/td_text_date"
android:textAlignment="viewStart"
android:textStyle="bold"
app:layout_constraintBaseline_toBaselineOf="#+id/td_name_heading"
app:layout_constraintEnd_toStartOf="#+id/td_duration_heading"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="#+id/td_name_heading" />
<TextView
android:id="#+id/td_duration_heading"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:background="?attr/colorButtonNormal"
android:padding="4dp"
android:text="#string/td_text_duration"
android:textAlignment="viewStart"
android:textStyle="bold"
app:layout_constraintBaseline_toBaselineOf="#+id/td_start_heading"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="#+id/td_start_heading" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/td_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="8dp"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/td_name_heading"
tools:listitem="#layout/task_duration_items" />
</androidx.constraintlayout.widget.ConstraintLayout>
task_duration_itesm.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:showIn="#layout/task_durations">
<TextView
android:id="#+id/td_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:padding="4dp"
android:textAlignment="viewStart"
app:layout_constraintHorizontal_weight="2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread"
tools:text="#string/td_text_name" />
<TextView
android:id="#+id/td_start"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:padding="4dp"
android:textAlignment="viewStart"
app:layout_constraintBaseline_toBaselineOf="#+id/td_name"
app:layout_constraintEnd_toStartOf="#+id/td_duration"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="#+id/td_name"
tools:text="#string/td_text_date" />
<TextView
android:id="#+id/td_duration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:padding="4dp"
android:textAlignment="viewStart"
app:layout_constraintBaseline_toBaselineOf="#+id/td_start"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="#+id/td_start"
tools:text="#string/td_text_duration" />
</androidx.constraintlayout.widget.ConstraintLayout>
If it works with findViewById, then your view is in your binding and not your tdBinding, because findViewById searches the layout that you set with setContentView.
If it didn't give a compiler error, you must have a view with the same ID in both layouts. I don't know why you have two layouts, since you're only using one of them.
View Binding is not better performing than findViewById unless you're using findViewById repeatedly to find the same view. I'm pretty sure View Binding uses findViewById internally. The purpose of View Binding is not better performance. It's for eliminating the need to manually sync up your code with your XML and risking runtime errors instead of compile-time errors.
OK, the answer was to just use one View Binding. The master XML layout has the other layout as an include. The trick is to give the include an ID then you can traverse the XML from both layouts in a single view binding.
<include
android:id="#+id/items"
layout="#layout/task_durations" />
The Kotlin code to to setup RecyclerView is
binding.items.tdList.layoutManager = LinearLayoutManager(this)
binding.items.tdList.setHasFixedSize(true)
binding.items.tdList.adapter = reportAdapter
I think you generaly only need a single View Binding where you are using include, but as I said to traverse them you need to give the include a ID.
Related
I am trying to fetch the data from Google Spreadsheet and display it in a recyclerView in Kotlin. I could do that without any error but the issue I am facing is when I scroll up or down the data in the recyclerView get disappeared. When I scroll up and then scroll down I can see that all the data that went up is missing and the same with scrolling down. But if I scroll up for more I can see one line of data after every few scrolls.
Another issue I have is with the date that is being displayed. My data in the Google Spreadsheet starts from 01-Jan-2023 (this is how it's shown in the spreadsheet, and without time in it), when it's shown in the recyclerView, all dates are one day earlier. I mean, it shows 31-Dec-2022 for 01-Jan-2023, 01-Jan-2023 for 02-Jan-2023 and so on.
Can somebody help correct my mistakes and improve my code? I have been after this for a couple of days and I couldn't fix the issue.
My code is,
SalesData.kt
class SalesData : AppCompatActivity() {
private lateinit var binding: ActivitySalesDataBinding
#SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySalesDataBinding.inflate(layoutInflater)
setContentView(binding.root)
val salesList = arrayListOf<SalesDataModel>()
val queue = Volley.newRequestQueue(this)
val url = "https://script.google.com/macros/s/AKfsdaffdbghjhfWVM2FeIH3gZY5kAnb6JVeWpg2XeBOZyU6sghhfkuytytg/exec"
val jsonObjectRequest = object: JsonObjectRequest(
Request.Method.GET,url,null,
Response.Listener {
val data = it.getJSONArray("items")
for(i in 0 until data.length()){
val salesJasonObject = data.getJSONObject(i)
val dt = salesJasonObject.getString("Date")
val dateFmt = SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(dt)
val formattedDatesString = dateFmt?.let { it1 -> SimpleDateFormat("dd-MMM-yyyy", Locale.US).format(it1) }
val salesObject = formattedDatesString?.let { it1 ->
SalesDataModel(
// salesJasonObject.getString("Date"),
it1,
salesJasonObject.getString("Branch"),
salesJasonObject.getDouble("NetSale"),
salesJasonObject.getDouble("Profit"),
)
}
if (salesObject != null) {
salesList.add(salesObject)
}
val adapter = SalesDataRecyclerAdapter(this#SalesData,salesList)
binding.rvSalesData.adapter = adapter
binding.rvSalesData.layoutManager = LinearLayoutManager(this#SalesData)
binding.rvSalesData.setHasFixedSize(true)
adapter.notifyDataSetChanged()
}
Toast.makeText(this#SalesData, "Data loaded successfully", Toast.LENGTH_LONG).show()
},Response.ErrorListener {
Toast.makeText(this#SalesData, it.toString(), Toast.LENGTH_LONG).show()
}
){
override fun getHeaders(): MutableMap<String, String> {
return super.getHeaders()
}
}
Toast.makeText(this#SalesData, "Hi", Toast.LENGTH_LONG).show()
queue.add(jsonObjectRequest)
}
}
SalesDataRecyclerAdapter.kt
class SalesDataRecyclerAdapter(
val context: Context,
private val saleDataList:ArrayList<SalesDataModel>
):RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return MyViewHolder(
SalesDataLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent, false
)
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val model = saleDataList[position]
if (holder is MyViewHolder){
holder.binding.tvSales.text = model.salesAmount.toString()
holder.binding.tvBranch.text = model.branch
holder.binding.tvDate.text = model.date
holder.binding.tvProfit.text = model.profit.toString()
}
}
override fun getItemCount(): Int {
return saleDataList.size
}
class MyViewHolder(val binding: SalesDataLayoutBinding) : RecyclerView.ViewHolder(binding.root)
}
activity_sales_data.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/white"
tools:context=".SalesData">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<!--
<androidx.core.widget.ContentLoadingProgressBar
android:id="#+id/progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>-->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/new_green"
android:padding="10dp"
android:text="SALES DATA"
android:textColor="#color/white"
android:textSize="24sp"
android:layout_gravity="bottom|end"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvSalesData"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
sales_data_layout.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="#+id/tvDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="Date"
android:layout_weight="1"/>
<TextView
android:id="#+id/tvBranch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="Branch"
android:layout_weight="1"/>
<TextView
android:id="#+id/tvSales"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="Sales"
android:layout_weight="1"/>
<TextView
android:id="#+id/tvProfit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="Profit"
android:layout_weight="1"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Since your root view in sales_data_layout.xml has the size:
android:layout_width="match_parent"
android:layout_height="match_parent"
Each item will take up the whole parent area, which in your case will end up with each individual item taking up the whole screen, and thus needing multiple scrolls to see the next item. You probably want to change the height to wrap_content for the root view, to see more items on the screen at once.
Add a comparator that tells the recylcer view exactly when to redraw.
class WordsComparator : DiffUtil.ItemCallback<Word>() {
override fun areItemsTheSame(oldItem: Word, newItem: Word): Boolean {
//=== here doesn't work for complex objects
// simple high-speed code goes here it is called over and over
// my app the same item has the same id easy compare
return (oldItem._id == newItem._id)
}
override fun areContentsTheSame(oldItem: Word, newItem: Word): Boolean { // you developer have to compare the contents of complex objects
// you need high speed code here for best results
// if possible don't call any functions that could do other
// unecessary things.
// compare the contents of the complex items.
return (oldItem._id == newItem._id
&& oldItem.checked == newItem.checked
&& oldItem.word == newItem.word
&& oldItem.color == newItem.color
&& oldItem.recolor == newItem.recolor
&& oldItem.rechecked == newItem.rechecked)
}
}
I have implemented a simple adapter but it is causing RecyclerView not to recycler views and calls onCreateViewHolder() for every list item when scrolled. This causes jank
whenever I scroll the list. Few points listed below are not related to excessive calls of onCreateViewHolder(), but I tried them to improve scroll performance and avoid jank. Things I have tried so far:
recyclerView.setHasFixedSize(true)
recyclerView.recycledViewPool.setMaxRecycledViews(1, 10) with recyclerView.setItemViewCacheSize(10)
recyclerView.setDrawingCacheEnabled(true) with recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH)
setting RecyclerView height to "match_parent"
Was previously using Kotlin's synthetic, now moved to Android's ViewBinding
Rewrite complex nested layouts to Constraint Layout
override onFailedToRecycleView() to see if it is called, but it was never called
Here is my adapter:
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.suppstore.R
import com.example.suppstore.common.Models.Brand
import com.example.suppstore.databinding.LiBrandBinding
import com.google.firebase.perf.metrics.AddTrace
class BrandsAdapter(list: ArrayList<Brand>, var listener: BrandClickListener?) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val VIEW_TYPE_LOADING = 0
private val VIEW_TYPE_NORMAL = 1
private var brandsList: ArrayList<Brand> = list
#AddTrace(name = "Brands - onCreateViewHolder", enabled = true)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == VIEW_TYPE_NORMAL) {
ViewHolder(
LiBrandBinding.inflate(
LayoutInflater.from(parent.context),
parent, false
)
)
} else {
LoaderHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.li_loader, parent, false)
)
}
}
#AddTrace(name = "Brands - onBindViewHolder", enabled = true)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ViewHolder)
holder.setData(brandsList[position], listener!!)
}
class ViewHolder(itemView: LiBrandBinding) : RecyclerView.ViewHolder(itemView.root) {
private val binding: LiBrandBinding = itemView
#AddTrace(name = "Brands - ViewHolder-setData", enabled = true)
fun setData(brand: Brand, listener: BrandClickListener) {
binding.cardItem.setOnClickListener { listener.onItemClick(brand) }
binding.tvBrandName.text = brand.name
binding.tvCount.text = brand.count.toString() + " Products"
}
}
class LoaderHolder(itemView: View) : RecyclerView.ViewHolder(itemView.rootView) {
}
#AddTrace(name = "Brands - addLoader", enabled = true)
fun addLoader() {
brandsList.add(Brand())
notifyItemInserted(brandsList.size - 1)
}
#AddTrace(name = "Brands - setData", enabled = true)
fun setData(newList: ArrayList<Brand>) {
this.brandsList = newList
notifyDataSetChanged()
}
#AddTrace(name = "Brands - removeLoader", enabled = true)
fun removeLoader() {
if (brandsList.size == 0)
return
val pos = brandsList.size - 1
brandsList.removeAt(pos)
notifyItemRemoved(pos)
}
override fun getItemViewType(position: Int): Int {
return if (brandsList.get(position).count == -1) {
VIEW_TYPE_LOADING
} else
VIEW_TYPE_NORMAL
}
interface BrandClickListener {
fun onItemClick(brand: Brand)
}
override fun getItemCount(): Int {
return brandsList.size
}
}
Here is the list item (li_brand):
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/cardItem"
android:layout_width="match_parent"
android:layout_height="85dp"
android:background="#color/app_black">
<TextView
android:id="#+id/tvBrandName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:textColor="#color/app_yellow"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="#id/tvCount"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="#+id/tvCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="2dp"
android:textColor="#color/app_grey"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="#id/tvBrandName" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="15dp"
android:src="#drawable/ic_baseline_arrow_forward_ios_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="match_parent"
android:layout_height="3dp"
android:background="#color/app_bg"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Here are related functions in Fragment
class BrandsFragment : Fragment() {
private val adapter = BrandsAdapter(ArrayList(), brandClickListener())
fun brandClickListener(): BrandsAdapter.BrandClickListener {
return object : BrandsAdapter.BrandClickListener {
override fun onItemClick(brand: Brand) {
activityViewModel?.setSelectedBrand(brand)
}
}
}
fun setupRecyclerView() {
val llManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
binding.recyclerView.layoutManager = llManager
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) { //check for scroll down
val visibleItemCount = llManager.childCount
val totalItemCount = llManager.itemCount
val firstVisibleItemPos = llManager.findFirstVisibleItemPosition()
if (loadWhenScrolled
&& visibleItemCount + firstVisibleItemPos >= totalItemCount
&& firstVisibleItemPos >= 0
) {
//ensures that last item was visible, so fetch next page
loadWhenScrolled = false
viewModel.nextPage()
}
}
}
})
binding.recyclerView.adapter = adapter
}
}
And here is the fragment xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/app_black"
android:focusableInTouchMode="true"
android:orientation="vertical"
tools:context=".Brands.BrandsFragment">
<androidx.appcompat.widget.SearchView
android:id="#+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:background="#drawable/bottom_line_yellow"
android:theme="#style/SearchViewTheme"
app:closeIcon="#drawable/ic_baseline_close_24"
app:iconifiedByDefault="false"
app:queryBackground="#android:color/transparent"
app:queryHint="Atleast 3 characters to search"
app:searchIcon="#drawable/ic_baseline_search_24" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Have you tried RecyclerView Diffutil class? Hope it will resolve smooth scrolling issue and overwhelm recreation of items.
https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil
"DiffUtil is a utility class that calculates the difference between two lists and outputs a list of update operations that converts the first list into the second one."
I have created a recycle view and inside that using card view for items. I have a delete button inside a card view whenever I click on that button my item is deleted from SQLite database. But to reflect it on UI, app need to restart. How can I notify adpater that item is deleted?
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="57dp"
android:layout_height="64dp"
android:layout_marginEnd="40dp"
android:layout_marginBottom="40dp"
android:clickable="true"
android:onClick="addNewCredentials"
app:backgroundTint="#270867"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="#android:drawable/ic_menu_add" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="1dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="1dp"
android:layout_marginBottom="1dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" >
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>
list_item_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="5dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/urlView"
android:layout_width="300dp"
android:layout_height="30dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="url"
android:textAppearance="#style/TextAppearance.AppCompat.Large"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="#+id/userNameView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
<TextView
android:id="#+id/userNameView"
android:layout_width="300dp"
android:layout_height="25dp"
android:layout_marginBottom="16dp"
android:text="userName"
app:layout_constraintBottom_toTopOf="#+id/passwordView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.1"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="#+id/passwordView"
android:layout_width="300dp"
android:layout_height="25dp"
android:layout_marginBottom="16dp"
android:text="password"
app:layout_constraintBottom_toTopOf="#+id/noteView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.1"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="#+id/noteView"
android:layout_width="300dp"
android:layout_height="30dp"
android:layout_marginBottom="16dp"
android:text="note"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.1"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="#+id/delButton"
android:layout_width="78dp"
android:layout_height="40dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="36dp"
android:background="#E6360F"
android:text="#string/delete_credential_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
MainActivity.kt
package com.example.passwordmanager
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL,false)
val db = DataBaseHandler(this)
val detailsData = db.readCredentials()
val adapter = CredentialAdapter(detailsData,this,{credentialsModel: CredentialsModel->deleteClick(credentialsModel)})
recyclerView.adapter = adapter
}
fun deleteClick(credential: CredentialsModel){
val db = DataBaseHandler(this)
if(db.deleteData(credential.id)){
//adapter.notifyItemRemoved(position)
Toast.makeText(applicationContext,"Deleted", Toast.LENGTH_SHORT).show()
}
}
fun addNewCredentials(view : View){
print("hello world")
val intent = Intent(this, AddDetailActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
}
}
CredentialAdapter.kt
package com.example.passwordmanager
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.list_item_layout.view.*
class CredentialAdapter(
private val items: List<CredentialsModel>,
ctx: Context, val clickListener: (CredentialsModel) -> Unit
): RecyclerView.Adapter<CredentialAdapter.ViewHolder>() {
var context = ctx
class ViewHolder(itemView: View):RecyclerView.ViewHolder(itemView){
fun bind(credential: CredentialsModel,clickListener: (CredentialsModel) -> Unit){
itemView.urlView.text = credential.url
itemView.userNameView.text = credential.userName
itemView.passwordView.text = credential.password
itemView.noteView.text = credential.note
itemView.delButton.setOnClickListener{clickListener(credential)}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.list_item_layout,parent,false))
}
override fun getItemCount(): Int {
return items.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val credential:CredentialsModel = items[position]
holder.bind(credential,clickListener)
}
}
add remove setOnClickListener in your onBindViewHolder.
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.remove.setOnClickListener {
val db = DataBaseHandler(this)
if(db.deleteData(credential.id)){
notifyItemRemoved(holder.getAdapterPosition())
}
}
}
The best way to handle these kinds of situations is to use LiveData.
LiveData is basically an observable class which reads data only when there is a change.
What you can do is create a set function in your adapter like:
internal fun setData(data: List<Data>) {
this.data= dataList //this datalist is a list defined in your adapter
notifyDataSetChanged()
}
now in your main activity/fragment, create a LiveData List outside the onCreate function like this:
private lateinit var allData:LiveData<List<Data>>
Now inside your onCreate function, use can observe the livedata and set the data for recyclerview like this:
allData.observe(this, Observer { data->
data?.let { adapter.setData(it) }
})
You are deleting the item from database but not from the list inside recyclerview adapter.
class CredentialAdapter(
private val items: ArrayList<CredentialsModel>, // Change list to arraylist
ctx: Context, val clickListener: (CredentialsModel, Int) -> Unit
): RecyclerView.Adapter<CredentialAdapter.ViewHolder>() {
...
...
fun remove(position: Int) {
// Remove and notify the adapter to reload
items.removeAt(position)
notifyItemRemoved(position)
}
class ViewHolder(itemView: View):RecyclerView.ViewHolder(itemView) {
fun bind(credential: CredentialsModel,clickListener: (CredentialsModel, Int) -> Unit){
...
...
// Pass adapter item position so that we can update the list after delete
itemView.delButton.setOnClickListener{clickListener(credential, adapterPosition)
}
}
...
...
}
Inside MainActivity.kt
fun deleteClick(credential: CredentialsModel, position: Int) {
val db = DataBaseHandler(this)
if(db.deleteData(credential.id)){
adapter.remove(position)
Toast.makeText(applicationContext,"Deleted", Toast.LENGTH_SHORT).show()
}
}
use ListAdpater
class AdapterMain(var onClickListener: (Int) -> Unit) :
ListAdapter<Note, AdapterMain.NoteViewHolder>(DIFFCALBACK) {
companion object DIFFCALBACK : DiffUtil.ItemCallback<Note>() {
override fun areItemsTheSame(oldItem: Note, newItem: Note): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Note, newItem: Note): Boolean {
return oldItem.title == newItem.title &&
oldItem.description == newItem.description &&
oldItem.priority == newItem.priority
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.note_item, parent, false)
return NoteViewHolder(view)
}
override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
holder.txtTitle.text = getItem(position).title
holder.txtDesc.text = getItem(position).description
holder.txtPriority.text = getItem(position).priority.toString()
}
inner class NoteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var txtTitle: TextView = itemView.txt_title
var txtDesc: TextView = itemView.txt_desc
var txtPriority: TextView = itemView.txt_priority
init {
itemView.setOnClickListener { onClickListener(adapterPosition) }
}
}
fun getNoteAt(position: Int): Note {
return getItem(position)
}
}
you can see complete code of simple NoteApp with kotlin , recyclerView , MVVM and..
class coba : AppCompatActivity() {
private lateinit var recycleView :RecyclerView
private lateinit var datalis :ArrayList
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_coba)
recycleView = findViewById(R.id.rcycoba)
datalis = ArrayList()
val dtnama = arrayOf(
"Danial Sanganus",
"Bijonia Skolin",
"Alianes Pertoli",
"Sivanian Pertici",
"Olehsan alausi"
)
for (i in dtnama.indices){
datalis.add(
dataCoba(
dtnama[i]
)
)
populateData()
}
}
private fun populateData(){
val linearManager = LinearLayoutManager(this)
linearManager.reverseLayout=true
linearManager.stackFromEnd=true
recycleView.layoutManager=linearManager
val adp =adpCoba(this,datalis)
recycleView.adapter=adp
}
}
I have tried many different tutorials and haven't been able to relate any to my application. My application in a gist displays a user's medication that they are taking. Here is my data class...
import java.util.HashMap
class LocalMedication {
var m_medicationName: String? = null
var m_medicationQty: String? = null
var m_medicationType: String? = null
var m_medicationExpDate: String? = null
var m_medicationStatus: Boolean = false
constructor() {}
constructor(medicationName: String, medicationQty: String, medicationType: String, medicationExpDat : String, medicationStatus : Boolean) {
this.m_medicationName = medicationName
this.m_medicationType = medicationType
this.m_medicationQty = medicationQty
this.m_medicationExpDate = medicationExpDat
this.m_medicationStatus = medicationStatus
}
fun toMap(): Map<String, Any> {
val result = HashMap<String, Any>()
result.put("medicationName", m_medicationName!!)
result.put("medicationType", m_medicationType!!)
result.put("medicationQty", m_medicationQty!!)
result.put("medicationExpDate", m_medicationExpDate!!)
result.put("medicationStatus", m_medicationStatus!!)
return result
}
}
Here is my view holder class
package com.example.home_med.viewHolder
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.home_med.R
class medicationViewHolder(view: View) : RecyclerView.ViewHolder(view) {
var medicationName: TextView
var medicationType: TextView
var medicationQty: TextView
init {
medicationName = view.findViewById(R.id.rv_medicationName)
medicationType = view.findViewById(R.id.rv_medicationType)
medicationQty = view.findViewById(R.id.rv_medicationQty)
}
}
Here is my fragment
class LocalMedication : Fragment() {
private var adapter: FirestoreRecyclerAdapter<LocalMedication, medicationViewHolder>? = null
private var firestoreDB: FirebaseFirestore? = null
private var firestoreListener: ListenerRegistration? = null
private var medList = mutableListOf<LocalMedication>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//setContentView(R.layout.activity_main)
firestoreDB = FirebaseFirestore.getInstance()
val recyclerView = recyclerview as? RecyclerView
val mLayoutManager = LinearLayoutManager(context)
recyclerView?.layoutManager = mLayoutManager
recyclerView?.itemAnimator = DefaultItemAnimator()
loadMedication()
firestoreListener = firestoreDB!!.collection("notes")
.addSnapshotListener(EventListener { documentSnapshots, e ->
if (e != null) {
Log.e(TAG, "Listen failed!", e)
return#EventListener
}
medList = mutableListOf()
if (documentSnapshots != null) {
for (doc in documentSnapshots) {
val note = doc.toObject(LocalMedication::class.java)
note.m_medicationName = doc.id
medList.add(note)
}
}
adapter!!.notifyDataSetChanged()
recyclerView?.adapter = adapter
})
}
override fun onDestroy() {
super.onDestroy()
firestoreListener!!.remove()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding: FragmentLocalMedicationBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_local_medication, container, false)
binding.viewMedicationButton.setOnClickListener { v: View ->
v.findNavController().navigate(LocalMedicationDirections.actionLocalMedicationToViewMedication())
}
binding.addMedicationButton.setOnClickListener { v: View ->
v.findNavController().navigate(LocalMedicationDirections.actionLocalMedicationToAddMedication())
}
binding.homeButton.setOnClickListener { v: View ->
v.findNavController().navigate(LocalMedicationDirections.actionLocalMedicationToHome2())
}
setHasOptionsMenu(true)
return binding.root
}
private fun loadMedication() {
val query = firestoreDB!!.collection("notes")
val response = FirestoreRecyclerOptions.Builder<LocalMedication>()
.setQuery(query, LocalMedication::class.java)
.build()
adapter = object : FirestoreRecyclerAdapter<LocalMedication, medicationViewHolder>(response) {
override fun onBindViewHolder(holder: medicationViewHolder, position: Int, model: LocalMedication) {
val note = medList[position]
holder.medicationName.text = note.m_medicationName
holder.medicationType.text = note.m_medicationType
holder.medicationQty.text = note.m_medicationQty
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): medicationViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.recyclerview_item, parent, false)
return medicationViewHolder(view)
}
override fun onError(e: FirebaseFirestoreException) {
Log.e("error", e!!.message)
}
}
adapter!!.notifyDataSetChanged()
recyclerview?.adapter = adapter
}
public override fun onStart() {
super.onStart()
adapter!!.startListening()
}
public override fun onStop() {
super.onStop()
adapter!!.stopListening()
}
}
Here is my recyclerViewItem XML file
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/rv_medicationName"
style="#style/word_title"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:background="#android:color/holo_orange_light"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/rv_medicationQty"
style="#style/word_title"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:background="#android:color/holo_orange_light"
app:layout_constraintStart_toEndOf="#+id/rv_medicationName"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/rv_medicationType"
style="#style/word_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:background="#android:color/holo_orange_light"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/rv_medicationQty"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
Here is my localMedications XML file
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".LocalMedication">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="32dp"
android:text="LOCAL MEDICATION"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.478"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="543dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:background="#android:color/darker_gray"
app:layout_constraintBottom_toTopOf="#+id/homeButton"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textView"
tools:listitem="#layout/recyclerview_item" />
<Button
android:id="#+id/viewMedicationButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="100dp"
android:layout_marginBottom="100dp"
android:text="View Med"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="#+id/addMedicationButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="100dp"
android:layout_marginBottom="100dp"
android:text="Add Medication"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="#+id/homeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginTop="59dp"
android:layout_marginEnd="30dp"
android:layout_marginBottom="103dp"
android:text="Home"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#+id/viewMedicationButton"
app:layout_constraintStart_toEndOf="#+id/addMedicationButton"
app:layout_constraintTop_toBottomOf="#+id/addMedicationButton"
app:layout_constraintVertical_bias="0.972" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Here is what my database looks like...
Datatbase
Any help or guidance would be great. It runs the application, but in the Logs, it says "RecyclerView: No adapter attached; skipping layout"
The problem in your code lies in the fact that the names of the fields in your LocalMedication class are different than the name of the properties in your database. You have in your LocalMedication class a field named m_medicationName while in your database I see it as medicationName and this is not correct. The names must match. Behind the scene, Kotlin is creating a Java class with a getter named getM_medicationName() so Firebase is looking in the database for a field named m_medicationName and not medicationName.
There are two ways in which you can solve this problem. The first one would be to remove the data in your database and add it again using field names (m_medicationName, m_medicationQty etc) that exist in your LocalMedication class.
If you are not allowed to use the first solution, then the second approach will be to use annotations in front of your public fields. So you should use the PropertyName annotation in front of every field. So in your LocalMedication class, a field should look like this:
#get:PropertyName("medicationName")
#set:PropertyName("medicationName")
public var m_medicationName: String? = null
As explained in my answer from the following post:
I am trying to get the correct reference to my Firebase Database child and set the values in my RecyclerView but I am having some issues
It's for Firebase realtime database but the same rules apply to Cloud Firestore.
The app contains two buttons, one for ListView and other for GridView, so I created to ItemViews and one adapter. I have created method, to switch between ListView and GridView. Every thing is working fine, except the GridView, which seems like it is jammed. List View is scrolling vertically but only upto 6 items, the list contains 9. I want the GridView to scroll in same fashion as List View scrolls until end. I have spent 2 simultaneous hours for searching solution, but none has worked. I have tried, ScrollView, NestedScrollView, but still unable to solve.
Grid View = RecyclerView with GridLayoutManager
Here is screenshot:
This one works fine upto 6 item and hide 3 out of 9
This one don't scroll (only GridView)
Here is Code:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".MainActivity">
<FrameLayout
android:id="#+id/frameLayout_imageContainer"
android:layout_width="match_parent"
android:layout_height="180dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="#+id/iv_house"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
app:srcCompat="#drawable/house" />
</FrameLayout>
<LinearLayout
android:id="#+id/linearLayout_btnContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:gravity="end"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="#id/frameLayout_imageContainer">
<ImageButton
android:id="#+id/btnListView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/btn_gradient_purple"
android:padding="8dp"
android:tint="#color/colorWhite"
app:srcCompat="#drawable/ic_list" />
<ImageButton
android:id="#+id/btnGridView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:background="#drawable/btn_gradient_purple"
android:padding="8dp"
android:tint="#color/colorWhite"
app:srcCompat="#drawable/ic_grid" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_room"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
app:layout_constraintTop_toBottomOf="#+id/linearLayout_btnContainer">
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private lateinit var mRoomList: ArrayList<Room>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val roomTypeBedroom = RoomType("Bedroom", R.drawable.ic_bedroom)
val roomTypeKitchen = RoomType("Kitchen", R.drawable.ic_kitchen)
val roomTypeBathroom = RoomType("Bathroom", R.drawable.ic_bathroom)
val roomTypeLiving = RoomType("Living", R.drawable.ic_living)
val roomTypeDining = RoomType("Dining", R.drawable.ic_dinning)
val roomTypeEmpty = RoomType("Empty", R.drawable.ic_empty)
mRoomList = arrayListOf(
Room("Bedroom 1", R.drawable.bedroom_1, 1, roomTypeBedroom),
Room("Bedroom 2", R.drawable.bedroom_2, 2, roomTypeBedroom),
Room("Bedroom 3", R.drawable.bedroom_3, 3, roomTypeBedroom),
Room("Kitchen", R.drawable.kitchen, 4, roomTypeKitchen),
Room("Bathroom", R.drawable.bathroom, 5, roomTypeBathroom),
Room("Living Room", R.drawable.living, 6, roomTypeLiving),
Room("Dining Room", R.drawable.sitting_area, 7, roomTypeDining),
Room("Empty Room 1", R.drawable.empty_1, 8, roomTypeEmpty),
Room("Empty Room 2", R.drawable.empty_2, 9, roomTypeEmpty)
)
showRoomsAs(ViewType.GRID)
btnGridView.setOnClickListener {
showRoomsAs(ViewType.GRID)
}
btnListView.setOnClickListener {
showRoomsAs(ViewType.LIST)
}
}
private fun showRoomsAs(viewType: ViewType) {
val roomAdapter = RoomAdapter(
this,
mRoomList,
viewType,
frameLayout_imageContainer
)
if (viewType == ViewType.GRID) {
rv_room.layoutManager = GridLayoutManager(this, 3)
btnGridView.visibility = View.GONE
btnListView.visibility = View.VISIBLE
} else {
rv_room.layoutManager = LinearLayoutManager(this)
btnGridView.visibility = View.VISIBLE
btnListView.visibility = View.GONE
}
rv_room.adapter = roomAdapter
}
}
RoomAdapter.k
import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class RoomAdapter(
private val context: Context,
private val rooms: ArrayList<Room>,
private val type: ViewType,
private val imageContainer: FrameLayout)
: RecyclerView.Adapter<RoomAdapter.RoomViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RoomViewHolder {
val inflater = LayoutInflater.from(parent.context)
var view: View? = null
if (type == ViewType.GRID)
view = inflater.inflate(R.layout.rv_item_grid, parent, false)
else if (type == ViewType.LIST)
view = inflater.inflate(R.layout.rv_item_list, parent, false)
return RoomViewHolder(view!!)
}
override fun getItemCount(): Int {
return rooms.size
}
override fun onBindViewHolder(holder: RoomViewHolder, position: Int) {
val room = rooms[position]
holder.icon.setBackgroundResource(room.type.icon)
holder.name.text = room.name
holder.type.text = room.type.type
holder.status.text = room.isRoomOnOrOff()
}
inner class RoomViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
override fun onClick(p0: View?) {
val room = rooms[adapterPosition]
if (room.associatedImageView == null) {
val imageView = ImageView(context)
imageView.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
imageView.setImageResource(room.image)
room.associatedImageView = imageView
imageContainer.addView(imageView)
}
if (room.isOn) {
room.associatedImageView?.visibility = View.GONE
room.isOn = false
} else {
room.associatedImageView?.visibility = View.VISIBLE
room.isOn = true
}
notifyDataSetChanged()
}
val icon: ImageView
val name: TextView
val type: TextView
val status: TextView
init {
itemView.setOnClickListener(this)
icon = itemView.findViewById(R.id.rv_item_icon) as ImageView
name = itemView.findViewById(R.id.rv_room_name) as TextView
type = itemView.findViewById(R.id.rv_room_type) as TextView
status = itemView.findViewById(R.id.rv_status) as TextView
}
}
}
any kind of quick help will be appreciated
ConstraintLayout can be tricky sometimes. Your recyclerView might be sitting inches away from app's view port. You can try the below attributes on your recyclerView
Option 1 : use of large padding
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_room"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="250dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom ="20dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent">
</androidx.recyclerview.widget.RecyclerView>
Option 2: Use of app:layout_constraintBottom_toBottomOf
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_room"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="250dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom ="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="#+id/linearLayout_btnContainer">
</androidx.recyclerview.widget.RecyclerView>
Adding app:layout_constraintBottom_toBottomOf="parent" in second option forces the bottom of the recyclerView stick to the bottom of constraintLayout and not overflow