I'm struggling with glide loading custom item layout with groupie. Being particular - on item bind when I use glide to load images (images are ~12kb). Experiencing a "frozen frame". Profiler shows big CPU usage jump on the moment when glide loads images. It lags even if I add 3 items at a time but I need to add more (there is not much difference in cpu usage between 3 and 48 items so it freezes for about the same time). Tested with loading it from Resources/Precached/Direct download, with and without RequestOptions() - everything is the same
Some code related to the issue:
HomeScreen.kt
var subscription = 0
//Transformation
var loading = false
var itemWidth = 120
class HomeScreen : AppCompatActivity() {
val categoriesReference = FirebaseDatabase.getInstance().getReference("/categories")
var photos = ArrayList<PhotoItem>()
val adapter = GroupAdapter<ViewHolder>()
var itemsAllowed = 48
lateinit var manager: GridLayoutManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home_screen)
itemWidth = getItemWidth(this#HomeScreen)
setSupportActionBar(toolbar)
window.exitTransition = null
//Action bar options
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.title = "Все обои"
//FIX BLINKING ON TRANSITIONS
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
/*var fade = Fade()
fade.excludeTarget(toolbar, true)
fade.excludeTarget(toolbar2, true)
fade.excludeTarget(android.R.id.statusBarBackground, true);
fade.excludeTarget(android.R.id.navigationBarBackground, true);
getWindow().setEnterTransition(fade)
getWindow().setExitTransition(fade)*/
}
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_FULLSCREEN)
//Working with layout
manager = GridLayoutManager(this, calculateNoOfColumns(this#HomeScreen))
adapter.setHasStableIds(true)
recycleView.isNestedScrollingEnabled = false
recycleView.adapter = adapter
recycleView.layoutManager = manager
loadPageFully()
addItemsToMenu()
//Listener for navigation view
navigView.setNavigationItemSelectedListener {
/*if (it.toString() == "Купить тариф") {
subscription = 1
alert("Тариф успешно установлен") {
navigView.menu.clear()
addItemsToMenu()
loadPageFully()
yesButton { }
supportActionBar?.title = "Все обои"
}.show()
true
} else {*/
when(it.title.toString()){
"Все обои" -> {
loadPageFully()
}
}
drawerLayoutMain.closeDrawer(GravityCompat.START, true)
for (i in 0 until navigView.menu.size()) {
navigView.menu.getItem(i).setChecked(false)
}
it.setChecked(true)
loadPageFully(it.toString())
loadingImageView.visibility = View.GONE
waveImageView.visibility = View.GONE
supportActionBar?.title = it.toString()
true
// }
}
//Shuffle images in layout manager
shuffleButton.onClick {
photos.shuffle()
adapter.clear()
photos.forEach {
if (manager.itemCount < itemsAllowed) {
adapter.add(it)
}
}
}
scrollView3.setOnScrollChangeListener(object : View.OnScrollChangeListener {
override fun onScrollChange(v: View?, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int) {
println(loading.toString())
if (!loading && !scrollView3.canScrollVertically(1) && manager.itemCount > 0) {
loading = true
if (itemsAllowed == photos.size) {
} else if (itemsAllowed + 48 >= photos.size) {
for (i in 48.downTo(0)) {
if (itemsAllowed + i == photos.size) {
itemsAllowed += i
waveImageView.visibility = View.VISIBLE
doAsync {
Thread.sleep(300)
scrollView3.fullScroll(View.FOCUS_DOWN)
}
}
}
} else {
waveImageView.visibility = View.VISIBLE
doAsync {
Thread.sleep(300)
scrollView3.fullScroll(View.FOCUS_DOWN)
}
itemsAllowed += 48
}
checkOk()
}
}
})
recycleView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
when (newState) {
SCROLL_STATE_IDLE -> Glide.with(this#HomeScreen).resumeRequests()
SCROLL_STATE_TOUCH_SCROLL, SCROLL_STATE_FLING -> Glide.with(this#HomeScreen).pauseRequests()
}
}
})
}
//On home button clicked
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
drawerLayoutMain.openDrawer(GravityCompat.START)
true
}
else -> super.onOptionsItemSelected(item)
}
}
fun getEntries(categoryName: String = "All") {
//This fun with ChildEventListeners(Firebase SDK) gets urls and pushes them to photos no problem here because by the time lags occur they all are in photos.
}
var okToLoad = false
var loaded = 0
fun loadImages() {
if (okToLoad) {
okToLoad = false
if (photos.size < itemsAllowed) {
itemsAllowed = photos.size
}
loading = true
try {
for (i in manager.findLastVisibleItemPosition() + 1 until itemsAllowed) {
Glide.with(this#HomeScreen).load(photos[i].url).apply(
RequestOptions().diskCacheStrategy(
DiskCacheStrategy.ALL
)/*.skipMemoryCache(true)*/
).apply(RequestOptions().dontTransform()/*.override(220, 330)*/)
.listener(object : RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: com.bumptech.glide.request.target.Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
loaded += 1
println("1:$loaded")
println("2:$itemsAllowed")
if (loaded == itemsAllowed) {
Timer().schedule(object : TimerTask() {
override fun run() {
runOnUiThread {
waveImageView.visibility = View.GONE
shuffleButton.visibility = View.VISIBLE
okToLoad = true
loading = false
loadingImageView.visibility = View.GONE
for (i in manager.findLastVisibleItemPosition() + 1 until loaded) {
adapter.add(photos[i])
}
}
}
}, 1000)
}
return false
}
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: com.bumptech.glide.request.target.Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
return false
}
}).preload(220, 330)
}
} catch (e: IndexOutOfBoundsException) {
} catch (e: FileNotFoundException) {
Toast.makeText(this#HomeScreen, "Квота превышена", Toast.LENGTH_LONG).show()
}
}
}
fun checkOk() {
doAsync {
if (photos.size == 0) {
okToLoad = false
Thread.sleep(100)
checkOk()
} else {
okToLoad = true
uiThread {
loadImages()
}
}
}
}
fun addItemsToMenu() {
categoriesReference.addChildEventListener(object : ChildEventListener {
override fun onChildAdded(p0: DataSnapshot, p1: String?) {
navigView.menu.add(p0.key.toString()).setIcon(R.drawable.ic_bookmark_black_24dp)
}
override fun onCancelled(p0: DatabaseError) = Unit
override fun onChildChanged(p0: DataSnapshot, p1: String?) = Unit
override fun onChildMoved(p0: DataSnapshot, p1: String?) = Unit
override fun onChildRemoved(p0: DataSnapshot) = Unit
})
}
fun loadPageFully(key: String = "All") {
shuffleButton.visibility = View.GONE
loaded = 0
okToLoad = false
loadingImageView.visibility = View.VISIBLE
doAsync {
Glide.get(this#HomeScreen).clearDiskCache()
}
Glide.get(this#HomeScreen).clearMemory()
manager.removeAndRecycleAllViews(recycleView.Recycler())
adapter.clear()
photos.clear()
getEntries(key)
checkOk()
}
fun calculateNoOfColumns(context: Context): Int {
val displayMetrics = context.getResources().getDisplayMetrics();
val dpWidth = displayMetrics.widthPixels / displayMetrics.density;
val scalingFactor = 110 // You can vary the value held by the scalingFactor
// variable. The smaller it is the more no. of columns you can display, and the
// larger the value the less no. of columns will be calculated. It is the scaling
// factor to tweak to your needs.
var columnCount = (dpWidth / scalingFactor).toInt()
if (columnCount < 3) columnCount = 3
return columnCount // if column no. is less than 2, we still display 2 columns
}
fun getItemWidth(context: Context): Int {
val displayMetrics = context.resources.displayMetrics
val dpWidth = displayMetrics.widthPixels
return (dpWidth / calculateNoOfColumns(context)).toInt()
}
}
val factory = DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build()
//Layout item for adapter
class PhotoItem(
val cnt: Context,
var url: String,
var downloads: Int,
var ref: DatabaseReference,
var locked: Boolean
) : Item<ViewHolder>() {
override fun bind(viewHolder: ViewHolder, position: Int) {
viewHolder.itemView.itemImageView.layoutParams.width = itemWidth
viewHolder.itemView.itemImageView.layoutParams.height = itemWidth
Glide.with(cnt)
.load(url)
.apply(RequestOptions().placeholder(R.drawable.placeholder).format(DecodeFormat.PREFER_RGB_565))/*transition(DrawableTransitionOptions.withCrossFade(factory)).apply(RequestOptions().placeholder(R.drawable.background))*//**//*.skipMemoryCache(true)*//**//*priority(Priority.HIGH))*//**//*.listener(
object : RequestListener<Bitmap> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: com.bumptech.glide.request.target.Target<Bitmap>?,
isFirstResource: Boolean
): Boolean {
return false
}
override fun onResourceReady(
resource: Bitmap?,
model: Any?,
target: Target<Bitmap>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
return false
}
})*/.into(viewHolder.itemView.itemImageView)
viewHolder.itemView.itemImageView.transitionName = url
viewHolder.itemView.setOnClickListener {
var intent = Intent(cnt, PhotoScreen::class.java).apply {
putExtra("url", url)
putExtra("ref", ref.toString())
putExtra("downs", downloads.toString())
putExtra("locked", locked.toString())
}
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
cnt as AppCompatActivity,
viewHolder.itemView.itemImageView,
viewHolder.itemView.itemImageView.transitionName
)
cnt.startActivity(intent, options.toBundle())
}
}
override fun getLayout(): Int {
return R.layout.image_item
}
Home
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/drawerLayoutMain"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".HomeScreen"
android:background="#drawable/background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent" android:id="#+id/relativeLayout"
>
<androidx.core.widget.NestedScrollView
android:layout_width="0dp"
android:layout_height="0dp"
tools:layout_conversion_absoluteHeight="598dp"
tools:layout_conversion_absoluteWidth="384dp" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="#+id/toolbar"
android:id="#+id/scrollView3"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintBottom_toBottomOf="parent"
android:overScrollMode="ifContentScrolls"
android:fillViewport="false">
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:id="#+id/linearLayout"
android:descendantFocusability="blocksDescendants"
android:layout_marginLeft="0dp" android:layout_marginRight="0dp"
android:layout_gravity="fill">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="wrap_content"
android:layout_height="-100dp"
android:layout_marginTop="0dp" android:id="#+id/recycleView"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:layout_marginRight="0dp"
android:focusableInTouchMode="true"
android:scrollbars="vertical"
android:layout_gravity="center_horizontal|fill"
>
</androidx.recyclerview.widget.RecyclerView>
<pl.droidsonroids.gif.GifImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/waveImageView" android:src="#drawable/intro_image"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:visibility="gone" android:background="#drawable/background"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<androidx.appcompat.widget.Toolbar android:id="#+id/toolbar" android:layout_width="0dp"
android:layout_height="45dp"
android:background="#drawable/background"
app:titleTextColor="#android:color/white"
android:textAlignment="gravity"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:isScrollContainer="false"
android:fitsSystemWindows="false"
tools:layout_conversion_absoluteHeight="42dp"
tools:layout_conversion_absoluteWidth="384dp"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="#drawable/ic_list_black_24dp">
</androidx.appcompat.widget.Toolbar>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content" app:srcCompat="#drawable/ic_shuffle_black_24dp"
android:id="#+id/shuffleButton"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="#+id/toolbar"
android:padding="10dp"
app:layout_constraintBottom_toTopOf="#+id/scrollView3"
android:focusedByDefault="true"
/>
<pl.droidsonroids.gif.GifImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/loadingImageView" android:visibility="visible"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp" android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent" android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent" android:src="#drawable/placeholder_gif"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.navigation.NavigationView
android:layout_width="250dp"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="#layout/header_menu"
android:theme="#style/ThemeOverlay.AppCompat.ActionBar" android:id="#+id/navigView"
app:itemBackground="#drawable/menu_item_background"
android:background="#drawable/menu_bitmap"/>
</androidx.drawerlayout.widget.DrawerLayout>
ImageItem layout
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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="wrap_content"
android:layout_height="wrap_content"
>
<ImageView
android:layout_width="110dp"
android:layout_height="110dp"
android:id="#+id/itemImageView"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"
android:scaleType="centerCrop"/>
</FrameLayout>
OnMeasure() and nativePollOnce() methods takes a lot as I see. Still, don't know what to do with it
Solved it
The main problem was that groupie adapter as for now makes diffs on UI thread. And that caused lags. Switching to another library may help you. I've switched to Epoxy by airbnb
P.S. I had nested recyclerview in scrollview. Removing scrollview added more performance
Related
android beginner here. any help is much appreciated .
I am working on an app that can do a simple experiment.
the app displays images and asks users to rate it from 1 to 10.
I have 1 activity and two layouts experiment begins.
I have two layouts merged under FrameLayout.
what I want to achieve is :
'====>start experiment >show first image for 10 seconds >change layout to ratings layout>after user selects rating>loopback with different image>finish() when imagelist is empty. '
here is what I tried
I have tried viewflipper but shownext() and showprevious() methods dont update values.
now i am trying using layout visibility ,showing and hiding the layouts
class Presenter : AppCompatActivity() {
//val currentTime = Calendar.getInstance().time
private lateinit var binding: ActivityPresenterBinding
lateinit var layout1:View
lateinit var layout2:View
val imgList=Constants.getImages()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//setContentView(R.layout.activity_presenter)
binding = ActivityPresenterBinding.inflate(layoutInflater)
setContentView(binding.root)
layout1 = binding.imageView
layout2=binding.ratingView
startSlider()
}
private fun startSlider() {
Handler(Looper.getMainLooper()).apply {
//arr.shuffle()
var index = 0
var imageView = binding.imgPresenter
val runnable = object : Runnable {
override fun run() {
imageView.setImageResource(imgList[index].image)
layout1.visibility= View.VISIBLE
Log.i("number ","${index}")
postDelayed(this, 5000)
index++
layout1.visibility=View.GONE
layout2.visibility=View.VISIBLE
binding.sbSeekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{
override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) {
Toast.makeText(applicationContext, "$p1", Toast.LENGTH_LONG).show()
//save rating (1-10)
}
override fun onStartTrackingTouch(p0: SeekBar?) {
}
override fun onStopTrackingTouch(p0: SeekBar?) {
}
})
}
}
postDelayed(runnable, 1000)
}
}
here my layout file
<FrameLayout android:id="#+id/viewFliper"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="#+id/img_presenter"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/ic_bg"
/>
</LinearLayout>
<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:id="#+id/ratingView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="how do you feel about this image from one to 10"
android:orientation="vertical"
android:gravity="center"
android:layout_marginBottom="20dp"
/>
<SeekBar
android:id="#+id/sb_seekbar"
style="#style/Widget.AppCompat.SeekBar.Discrete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="10"
android:paddingStart="6dp"
android:paddingEnd="6dp"
android:progress="3"
android:stepSize="1" />
</LinearLayout>
this is not hiding the views after the second image
what am I doing wrong?
try this and apply viewbinding
lateinit var runnable: Runnable
private fun startSlider() {
Handler(Looper.getMainLooper()).apply {
var flag = 0
var index = 0
runnable = Runnable {
if (flag == 0) {
img_presenter.setImageResource(imgList[index])
layout1.visibility = View.VISIBLE
layout2.visibility = View.GONE
postDelayed(runnable, 5000)
flag = 1
index++
} else {
layout1.visibility = View.GONE
layout2.visibility = View.VISIBLE
sb_seekbar.setOnSeekBarChangeListener(object :
SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) {
// Toast.makeText(applicationContext, "$p1", Toast.LENGTH_LONG).show()
//save rating (1-10)
}
override fun onStartTrackingTouch(p0: SeekBar?) {
}
override fun onStopTrackingTouch(p0: SeekBar?) {
index++
flag = 0
postDelayed(runnable, 1000)
}
})
}
}
postDelayed(runnable, 1000)
}
}
In some devices the recyclerview are flashing items and nothing more, after that, appear from items in row. I can't debug that because in emulator and my phone it doesn't happen.
Has anyone ever experienced that?
The correct thing would be to keep as the follow img:
I'm creating the RecyclerView as below, in SubItemsActivity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
subMainList = ArrayList()
subMainList = db.listSubItems("AND id_main_item=$mainId")
layoutManagerRecycler = LinearLayoutManager(this)
subMainItemsRecycler.layoutManager = layoutManagerRecycler
subMainItemsRecycler.addItemDecoration(DividerItemDecoration(this, 1))
var subMainAdapter = SubMainAdapter(this, subMainList, this, activate)
subMainAdapter.setHasStableIds(true)
subMainItemsRecycler.adapter = subMainAdapter
}
The recyclerView layout is here:
I have two ConstraintLayouts, the main is the first, with id "prioritize_edit", the other layout with id "prioritize_sub" will not appear in the image example.
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/prioritize_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/counter"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:textSize="17sp"
android:textStyle="bold"
android:textColor="#color/white"
android:paddingStart="4dp"
android:paddingEnd="2dp"
android:paddingVertical="7.5dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="1dp"
android:background="#BFBFBF"
android:layout_marginStart="8dp"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="#+id/arrows"
app:layout_constraintTop_toTopOf="parent" >
</TextView>
<ImageView
android:id="#+id/arrows"
android:src="#drawable/icon_arrow_bottom"
android:layout_height="0dp"
android:layout_width="15dp"
android:paddingEnd="1dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="1dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="#+id/counter"
app:layout_constraintEnd_toStartOf="#+id/check_item"
app:layout_constraintTop_toTopOf="parent">
</ImageView>
<CheckBox
android:id="#+id/check_item"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:layout_gravity="start|center_vertical"
android:gravity="start|center_vertical"
android:layout_marginStart="3dp"
android:layout_marginEnd="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="#+id/arrows"
app:layout_constraintEnd_toStartOf="#+id/color_picker"
app:layout_constraintTop_toTopOf="parent" />
<Spinner
android:id="#+id/color_picker"
android:layout_width="35dp"
android:layout_height="wrap_content"
android:background="#null"
android:gravity="center"
android:layout_gravity="center"
android:layout_marginRight="5dp"
android:layout_marginEnd="5dp"
android:layout_marginLeft="13dp"
android:layout_marginStart="13dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="#+id/check_item"
app:layout_constraintEnd_toStartOf="#+id/list_subitem"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="#+id/list_subitem"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:layout_marginBottom="0dp"
android:textSize="17sp"
android:fontFamily="sans-serif-black"
android:textColor="#color/text_gray"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="#+id/color_picker"
app:layout_constraintEnd_toStartOf="#+id/menu_subitem"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="#+id/menu_subitem"
android:layout_width="40dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:paddingEnd="8dp"
android:src="#drawable/round_more_vert_black_36"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="#+id/list_subitem"
app:layout_constraintEnd_toStartOf="#+id/draggable_subitem"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="#+id/draggable_subitem"
android:layout_width="42dp"
android:layout_height="42dp"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:layout_marginStart="0dp"
android:layout_marginEnd="10dp"
android:layout_marginVertical="0dp"
android:src="#drawable/icon_draggable"
android:background="#null"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="#+id/menu_subitem"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/prioritize_sub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
android:layout_marginBottom="5dp">
<Button
android:id="#+id/prioritizer_button"
android:layout_width="0dp"
android:layout_height="45dp"
android:fontFamily="sans-serif-black"
android:text="#string/prioritize"
android:textColor="#color/white"
android:background="#color/colorPrimary"
android:alpha="0.80"
android:paddingHorizontal="15dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="15dp"
android:textSize="16sp"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#id/counter_invisible"/>
<TextView
android:id="#+id/counter_invisible"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="17sp"
android:textStyle="bold"
android:textColor="#color/white"
android:paddingHorizontal="10dp"
android:paddingVertical="7.5dp"
android:layout_marginTop="7dp"
android:background="#BFBFBF"
android:layout_marginStart="8dp"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="#+id/prioritizer_button"/>
</androidx.constraintlayout.widget.ConstraintLayout>
My Adapter is this:
class SubMainAdapter(
var activity: SubMainActivity,
var items: MutableList<SubItems>,
var clickListener: OnSubMainItemClickListener,
var activate: Boolean
) : RecyclerView.Adapter<SubMainViewHolder>(){
override fun getItemCount(): Int {
return items.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubMainViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(
R.layout.layout_subitem_view,
parent,
false
)
val viewHolder = SubMainViewHolder(itemView)
return viewHolder
}
#SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: SubMainViewHolder, position: Int) {
holder.initialize(
activity, items[position], clickListener, items.size,
items,
position
)
if (activate) {
holder.dragButton.visibility = View.VISIBLE
holder.check_item.visibility = View.GONE
holder.menuButton.visibility = View.GONE
} else {
holder.dragButton.visibility = View.GONE
holder.check_item.visibility = View.VISIBLE
holder.menuButton.visibility = View.VISIBLE
}
holder.dragButton.setOnTouchListener { v, event ->
if(event.actionMasked== MotionEvent.ACTION_DOWN){
v.performClick()
activity.touchHelper?.startDrag(holder)
}
false
}
}
override fun getItemId(position: Int): Long {
val subItems: SubItems = items[position]
return subItems.id.toLong()
}
override fun getItemViewType(position: Int): Int {
return position
}
}
class SubMainViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
var subMainItem = itemView.list_subitem
var counter = itemView.counter
var arrows = itemView.arrows
var counterInvisible = itemView.counter_invisible
var prioritizeSub = itemView.prioritize_sub
var prioritizerButton = itemView.prioritizer_button
var prioritizeEdit = itemView.prioritize_edit
var check_item = itemView.check_item
var deleteButton = R.id.trash_subitem
var menuButton = itemView.menu_subitem
var dragButton = itemView.draggable_subitem
var color_picker = itemView.color_picker
fun initialize(
activity: SubMainActivity,
item: SubItems,
action: OnSubMainItemClickListener,
sizeArrayItems: Int,
listItems: MutableList<SubItems>,
position: Int
){
// var counterValue: String = item.list_order.toString()
// var digitsNumber: Int = sizeArrayItems.toString().length
// while (counterValue.length<digitsNumber){
// counterValue = "0$counterValue"
// }
var counterValue: String = item.path_index
subMainItem.setText(item.sub_item)
counterInvisible.text = "$counterValue"
counter.text = "$counterValue"
counter.setBackgroundColor(Color.parseColor("#a5a5a5"))
counter.background.alpha = if(item.max_level==0) 0 else (255*item.level)/item.max_level
arrows.setBackgroundColor(Color.parseColor("#a5a5a5"))
arrows.background.alpha = if(item.max_level==0) 0 else (255*item.level)/item.max_level
val prevItem = if(position <= listItems.size){listItems[position]}else{if(position-1 <= listItems.size){listItems[position]}else{listItems[position - 1]}}
val childs = item.direct_childs
val nextItem = if(position >= listItems.size){listItems[position]}else{if(position+1 >= listItems.size){listItems[position]}else{listItems[position + 1]}}
if(childs==0){
arrows.setImageResource(R.drawable.icon_arrow_right)
}else{
if(nextItem.visibility == 0){
arrows.setImageResource(R.drawable.icon_arrow_right)
}else {
arrows.setImageResource(R.drawable.icon_arrow_bottom)
}
arrows.setOnClickListener {
action.toggleSubItems(item, adapterPosition, itemView)
}
counter.setOnClickListener {
action.toggleSubItems(item, adapterPosition, itemView)
}
}
if (item.prioritizeButton){
prioritizeSub.visibility = View.VISIBLE
val prioritizeString = activity.getString(R.string.prioritize)
val finalStr = String.format(prioritizeString, item.path_index)
prioritizerButton.text = finalStr
prioritizerButton.setOnClickListener{
action.onPrioritizeItemsClick(item)
}
prioritizeEdit.visibility = View.GONE
}else{
prioritizeSub.visibility = View.GONE
prioritizeEdit.visibility = View.VISIBLE
}
val param = itemView.layoutParams as RecyclerView.LayoutParams
if (item.visibility==1) {
param.height = LinearLayout.LayoutParams.WRAP_CONTENT
param.width = LinearLayout.LayoutParams.MATCH_PARENT
itemView.visibility = View.VISIBLE
} else {
itemView.visibility = View.GONE
param.height = 0
param.width = 0
}
itemView.layoutParams = param
if (item.checked==Constants.TRUE){
check_item.isChecked = true
subMainItem.setTextColor(ContextCompat.getColor(activity, (R.color.checked)))
counter.setTextColor(ContextCompat.getColor(activity, (R.color.checked)))
subMainItem.apply {
paintFlags = paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
}
counter.apply {
paintFlags = paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
}
}else{
check_item.isChecked = false
subMainItem.setTextColor(ContextCompat.getColor(activity, (R.color.text_grayer)))
counter.setTextColor(ContextCompat.getColor(activity, (R.color.text_gray_darker)))
subMainItem.apply {
paintFlags = paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
counter.apply {
paintFlags = paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
}
check_item.setOnClickListener{
action.onCheckItemClick(item, adapterPosition, itemView)
}
menuButton.setOnClickListener{
val popup = PopupMenu(activity, menuButton)
popup.inflate(R.menu.submain_options)
popup.setOnMenuItemClickListener(object : PopupMenu.OnMenuItemClickListener {
override fun onMenuItemClick(menuItem: MenuItem): Boolean {
when (menuItem.itemId) {
R.id.trash_subitem -> {
action.onExcludeItemClick(item, adapterPosition)
return true
}
R.id.add_sub_subitem -> {
action.onAddChildItemClick(item, adapterPosition)
return true
}
R.id.prioritize_sub_subitem -> {
action.manageDragButtons(true, item.id, item.direct_childs)
return true
}
else -> return false
}
}
})
popup.show()
}
val textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(
text: CharSequence?,
start: Int,
before: Int,
count: Int
) {
action.onKeyDownItem(item, itemView, text)
}
}
subMainItem.addTextChangedListener(textWatcher)
color_picker.adapter = CustomSpinnerAdapter(
color_picker.context,
listOf(
SpinnerData(R.drawable.white_arrow),
SpinnerData(R.drawable.red_arrow),
SpinnerData(R.drawable.orange_arrow),
SpinnerData(R.drawable.yellow_arrow),
SpinnerData(R.drawable.green_arrow),
SpinnerData(R.drawable.blue_arrow),
SpinnerData(R.drawable.turquoise_arrow),
SpinnerData(R.drawable.purple_arrow)
)
)
// var checkSpinner: Int = 0
color_picker.isSelected = false;
color_picker.setSelection(item.color, false)
color_picker.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
adapterView: AdapterView<*>?,
view: View,
spinnerPosition: Int,
l: Long
) {
// if(++checkSpinner > 1) {
action.onSpinnerChange(item, spinnerPosition)
// }
}
override fun onNothingSelected(adapterView: AdapterView<*>?) {
return
}
}
}
}
interface OnSubMainItemClickListener{
fun onCheckItemClick(item: SubItems, position: Int, view: View)
fun onExcludeItemClick(item: SubItems, position: Int)
fun onAddChildItemClick(item: SubItems, position: Int)
fun onPrioritizeItemsClick(item: SubItems)
fun onKeyDownItem(item: SubItems, view: View, text: CharSequence?)
fun onChangeItem(item: SubItems, view: View, text: CharSequence?)
fun onSpinnerChange(item: SubItems, spinnerPosition: Int)
fun manageDragButtons(activate: Boolean, parentId: Int, childs: Int)
fun toggleSubItems(item: SubItems, position: Int, view: View)
}
As I'm using setHasStableIds(true) I've tried to change the getItemId function in adapter, but the problem remains. Has anyone had a similar problem?
It was a problem in OnItemSelectedListener from my spinner. The function inside it was always calling the database list, because of OnItemSelectedListener are called one first time when it is being created, I've uncommented the checkSpinner variable in Adapter to prevent this first call and now it has been solved.
var checkSpinner: Int = 0
color_picker.isSelected = false;
color_picker.setSelection(item.color, false)
color_picker.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
adapterView: AdapterView<*>?,
view: View,
spinnerPosition: Int,
l: Long
) {
if(++checkSpinner > 1) {
action.onSpinnerChange(item, spinnerPosition)
}
}
override fun onNothingSelected(adapterView: AdapterView<*>?) {
return
}
}
onBindViewHolder() will draw the views. You are having a lot of views visible in default case in the xml. All views that are going to be toggled with visibility must have visibility as gone or invisible by default in the xml. onBindViewHolder() can control the visibility once views are loaded.
The issue is recycler inflates the view in onCreateViewHolder(), the views are visible. After this the data is set and notifyDataSetChanged is called. This will update the views as per the conditions in the onBindViewHolder() function. But, till this happens, you see the views. Since this takes small amount of time, you see flashing.
This will happen in devices which are having low memory or slower processing.
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 am using a Androidx Paging library with RecyclerView for pagination in my view.
The issue I am facing is when I scroll down RecyclerView, after the mentioned number of PAGE_SIZE, loadAfter is called and once the new data is received, my RecyclerView scroll up to the top(first item) instead of further scroll down.
I debug my code and saw that my RecyclerView's onBindViewHolder also getting called with position 0. So, I think the whole data is being refreshed. Ideally, this should not happen.
Library version:
implementation 'androidx.paging:paging-runtime:2.1.2'
Here is the code:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="com.xyz.viewmodel.orders.OrderListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:background="#color/colorWindowsSecondaryBg"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="#dimen/_10sdp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<!--List-->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/srlFav"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
bind:isRefreshing="#{viewModel.refreshVisibility}"
bind:onRefreshListener="#{viewModel.swipeRefreshListener}">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvOrders"
android:layout_width="match_parent"
android:layout_height="0dp"
bind:listitem="#layout/item_order" />
<androidx.constraintlayout.widget.Barrier
android:id="#+id/barrierVisibility"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:barrierAllowsGoneWidgets="true"
app:barrierDirection="top"
app:constraint_referenced_ids="clBottomLoading" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/clBottomLoading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/_10sdp"
android:background="#android:color/transparent"
android:paddingTop="#dimen/_2sdp"
android:visibility="#{viewModel.bottomProgressVisibility && !viewModel.refreshVisibility? View.VISIBLE : View.GONE,default=gone}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/barrierVisibility">
<com.github.ybq.android.spinkit.SpinKitView
android:id="#+id/skv_loading_bottom"
style="#style/SpinKitView.ThreeBounce"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="#android:color/transparent"
app:SpinKit_Color="#color/colorPrimaryDark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<!--No data layout-->
<include
android:id="#+id/includeError"
layout="#layout/include_layout_error"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="#{!viewModel.progressVisibility && viewModel.errorVisibility ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:message="#{viewModel.errorString}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
OrderListDataSource
class OrderListDataSource(
private val orderRepository: OrderRepository,
private val coroutineScope: CoroutineScope?,
private var showProgress: Boolean = true
) : PageKeyedDataSource<Int, OrdersItem>() {
val ordersLiveData: MutableLiveData<Resource<ResOrders>> = MutableLiveData()
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, OrdersItem>
) {
if (BaseApp.instance?.let { NetworkUtils.isNetworkAvailable(it) } == true) {
if (showProgress) {
ordersLiveData.postValue(Resource.Loading(EnumLoading.LOADING_FIRST_PAGE))
}
coroutineScope?.launch {
val result = orderRepository.getOrders(1)
if (result is ResOrders) {
ordersLiveData.postValue(Resource.Success(result))
result.orders?.let {
callback.onResult(it, null, 2)
}
} else if (result is BaseError) {
ordersLiveData.postValue(Resource.Error(result))
}
}
} else {
ordersLiveData.postValue(Resource.Loading(EnumLoading.LOADING_CLOSE))
}
}
override fun loadAfter(
params: LoadParams<Int>,
callback: LoadCallback<Int, OrdersItem>
) {
if (BaseApp.instance?.let { NetworkUtils.isNetworkAvailable(it) } == true) {
if (showProgress) {
ordersLiveData.postValue(Resource.Loading(EnumLoading.LOADING_MORE))
}
coroutineScope?.launch {
val result = orderRepository.getOrders(params.key)
if (result is ResOrders) {
ordersLiveData.postValue(Resource.Success(result))
result.orders?.let { callback.onResult(it, params.key + 1) }
} else if (result is BaseError) {
ordersLiveData.postValue(Resource.Error(result))
}
}
} else {
ordersLiveData.postValue(Resource.Loading(EnumLoading.LOADING_CLOSE))
}
}
override fun loadBefore(
params: LoadParams<Int>,
callback: LoadCallback<Int, OrdersItem>
) {
}
}
OrderListDataSourceFactory
class OrderListDataSourceFactory(
private val productRepository: OrderRepository, private val coroutineScope: CoroutineScope?
) : DataSource.Factory<Int, OrdersItem>() {
val ordersLiveData = MutableLiveData<OrderListDataSource>()
private var showProgress: Boolean = true
private var orderListDataSource: OrderListDataSource? = null
override fun create(): DataSource<Int, OrdersItem> {
orderListDataSource = OrderListDataSource(
productRepository, coroutineScope,
showProgress
)
ordersLiveData.postValue(orderListDataSource)
return orderListDataSource!!
}
}
OrderListViewModel
private val pageSize = 10
val listHasData = MutableLiveData<Boolean>().apply { value = true }
private val orderListDataSourceFactory: OrderListDataSourceFactory =
OrderListDataSourceFactory(orderRepository, viewModelScope)
init {
val config = PagedList.Config.Builder()
.setPageSize(pageSize)
.setInitialLoadSizeHint(pageSize * 2)
.setEnablePlaceholders(false)
.build()
orderItem = LivePagedListBuilder(orderListDataSourceFactory, config).build()
}
OrdersAdapter
class OrdersAdapter constructor(private val itemTouchListener: ItemTouchListener) :
PagedListAdapter<OrdersItem, OrderViewHolder>(DiffUtilCallBack()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OrderViewHolder {
val mItemProductBinding = DataBindingUtil.inflate<ViewDataBinding>(
LayoutInflater.from(parent.context),
R.layout.item_order,
parent,
false
)
return OrderViewHolder(mItemProductBinding)
}
override fun onBindViewHolder(holder: OrderViewHolder, position: Int) {
getItem(position)?.let {
holder.bind(it)
val itemClickListener: View.OnClickListener = View.OnClickListener { _ ->
itemTouchListener.onOrderClick(position, it)
}
holder.setItemClickListener(itemClickListener)
}
}
class DiffUtilCallBack : DiffUtil.ItemCallback<OrdersItem?>() {
override fun areItemsTheSame(
oldItem: OrdersItem,
newItem: OrdersItem
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(
oldItem: OrdersItem,
newItem: OrdersItem
): Boolean {
return oldItem.id == newItem.id
&& oldItem.orderDate.equals(newItem.orderDate)
&& oldItem.orderId.equals(newItem.orderId)
&& oldItem.totalPrice == newItem.totalPrice
&& oldItem.deliveryStatus.equals(newItem.deliveryStatus)
}
}
interface ItemTouchListener {
fun onOrderClick(position: Int, ordersItem: OrdersItem)
}
}
class OrderViewHolder constructor(private val binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(ordersItem: OrdersItem) {
binding.setVariable(BR.order, ordersItem)
binding.executePendingBindings()
}
fun setItemClickListener(clickListener: View.OnClickListener) {
binding.root.lOrder.setOnClickListener(clickListener)
}
}
OrderListingFragment
mViewModel.orderItem.observe(this, Observer {
ordersAdapter.submitList(it)
})}
mViewModel.getOrders().observe(this, Observer {
...
}
Can anyone please help me with this?
Thank you in advance!
The issue was because of Skeleton I have used with RecyclerView.
mSkeleton?.hide()
This line was called every time I got the API response.
Ideally, the Skeleton will be displayed initially. So, this line should be called only once. I handled this case and now it's working as expected.
I'm using Recyclerview inside a Fragment Following Google's sample of MVP Android architecture and I tried to make the View part passive as possible following this article , which makes the whole Recyclerview Adapter passive of the Data Models and the presenter handles it.
Here is my code of the Fragment:
class OrderHistoryFragment : Fragment(), OrderHistoryContract.View {
lateinit var mPresenter: OrderHistoryContract.Presenter
lateinit var rvOrderHistory: RecyclerView
lateinit var orderHistoryAdapter : OrderHistoryAdapter
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val root = inflater!!.inflate(R.layout.order_history_fragment, container, false)
rvOrderHistory = root.findViewById<RecyclerView>(R.id.rvOrderHistory)
rvOrderHistory.layoutManager = LinearLayoutManager(context, LinearLayout.VERTICAL, false)
orderHistoryAdapter = OrderHistoryAdapter(mPresenter, object : HistoryItemListener {
override fun onReorder(orderHistory: OrderHistory) {
}
override fun onOpenOrder(orderHistory: OrderHistory) {
val orderIntent = Intent(activity, OrderDetailActivity::class.java)
orderIntent.putExtra("orderId", orderHistory.id)
startActivity(orderIntent)
}
})
rvOrderHistory.adapter = orderHistoryAdapter
return root
}
override fun onResume() {
super.onResume()
mPresenter.start()
}
override fun setPresenter(presenter: OrderHistoryContract.Presenter) {
mPresenter = checkNotNull<OrderHistoryContract.Presenter>(presenter)
}
override fun showLoadingIndicator(load: Boolean?) {
}
override fun updateOrdersAdapter() {
orderHistoryAdapter.notifyDataSetChanged()
}
override fun showSnackBar(Message: String) {
val parentLayout = activity.findViewById<View>(android.R.id.content)
val snackBar = Snackbar
.make(parentLayout, Message, Snackbar.LENGTH_INDEFINITE)
snackBar.setAction("Dismiss") { snackBar.dismiss() }
snackBar.setActionTextColor(Color.RED)
snackBar.show()
}
interface HistoryItemListener {
fun onReorder(orderHistory: OrderHistory)
fun onOpenOrder(orderHistory: OrderHistory)
}
companion object {
fun newInstance(): OrderHistoryFragment {
return OrderHistoryFragment()
}
}
fun OrderHistoryFragment() {
}
}
And this is my RecyclerView Adapters code
class OrderHistoryAdapter(internal var orderHistoryPresenter: OrderHistoryContract.Presenter, private val listener: OrderHistoryFragment.HistoryItemListener) : RecyclerView.Adapter<OrderHistoryAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.order_history_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
orderHistoryPresenter.onBindOrdersRow(position, holder)
holder.bReOrder!!.setOnClickListener { v -> listener.onReorder(orderHistoryPresenter.getOrderHistoryItem(position)) }
holder.cvOrderItem!!.setOnClickListener { v -> listener.onOpenOrder(orderHistoryPresenter.getOrderHistoryItem(position)) }
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getItemCount(): Int {
return orderHistoryPresenter.getOrdersCount()
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), OrderHistoryContract.orderRowView {
internal var ivOrderVendor: ImageView? = null
internal var tvOrderId: TextView? = null
internal var tvOrderItems: TextView? = null
internal var tvOrderDate: TextView? = null
internal var tvOrderPrice: TextView? = null
internal var bReOrder: Button? = null
internal var cvOrderItem: CardView? = null
init {
ivOrderVendor = itemView.findViewById<ImageView>(R.id.ivOrderVendor)
tvOrderId = itemView.findViewById<TextView>(R.id.tvOrderId)
tvOrderItems = itemView.findViewById<TextView>(R.id.tvOrderItems)
tvOrderDate = itemView.findViewById<TextView>(R.id.tvOrderDate)
tvOrderPrice = itemView.findViewById<TextView>(R.id.tvOrderPrice)
bReOrder = itemView.findViewById<Button>(R.id.bReOrder)
cvOrderItem = itemView.findViewById<CardView>(R.id.cvOrderItem)
}
override fun setOrderImage(url: String) {
Glide.with(itemView.context).load(url).into(ivOrderVendor!!)
}
override fun setOrderDate(orderDate: String) {
tvOrderDate!!.text = orderDate
}
override fun setOrderId(orderId: String) {
tvOrderId!!.text = orderId
}
override fun setOrderItems(orderItems: ArrayList<String>) {
val stringBuilder = StringBuilder()
for (item in orderItems) {
stringBuilder.append(item)
}
tvOrderItems!!.text = stringBuilder.toString()
}
override fun setOrderPrice(orderPrice: String) {
tvOrderPrice!!.text = R.string.price.toString() + " " + orderPrice + " " + R.string.egp
}
}
}
And Here is code of the presenter which handles the Adapter data and it's binding to the ViewHolder
class OrderHistoryPresenter internal constructor(mDataRepository: DataRepository, mOrdeHistoryView: OrderHistoryContract.View) : OrderHistoryContract.Presenter {
private val mDataRepository: DataRepository
//refrence of the View to trigger the functions after proccessing the task
private val mOrdeHistoryView: OrderHistoryContract.View
private var orderHistoryItems = ArrayList<OrderHistory>()
init {
this.mDataRepository = checkNotNull(mDataRepository, "tasksRepository cannot be null")
this.mOrdeHistoryView = checkNotNull<OrderHistoryContract.View>(mOrdeHistoryView, "tasksView cannot be null!")
mOrdeHistoryView.setPresenter(this)
}
override fun start() {
mOrdeHistoryView.showLoadingIndicator(true)
mDataRepository.getCurrentUser(object : LocalDataSource.userRequestCallback {
override fun onUserRequestSuccess(botitUser: BotitUser) {
val urlParams = HashMap<String, String>()
urlParams.put(Endpoints.USER_ID_KEY, botitUser.userId!!)
val url = Endpoints.getUrl(Endpoints.urls.ORDER_HISTORY, urlParams)
mDataRepository.buildEndPointRequest(url, " ", Endpoints.requestsType.GET, object : EndpointDataSource.RequestCallback {
override fun onRequestSuccess(Body: String) {
try {
mOrdeHistoryView.showLoadingIndicator(false)
orderHistoryItems = JSONParser.parseData(JSONParser.parsers.ORDER_HISTORY, JSONObject(Body)) as ArrayList<OrderHistory>
mOrdeHistoryView.updateOrdersAdapter()
} catch (e: JSONException) {
e.printStackTrace()
}
}
override fun onRequestError(Body: String) {
mOrdeHistoryView.showLoadingIndicator(false)
mOrdeHistoryView.showSnackBar("Cannot load data")
}
})
}
override fun onUserRequestError(Body: String) {
}
})
}
override fun refreshData() {
}
override fun getOrdersCount(): Int {
return orderHistoryItems.size
}
override fun onBindOrdersRow(position: Int, orderViewHolder: OrderHistoryContract.orderRowView) {
if (orderHistoryItems.isNotEmpty()) {
val orderHistory = orderHistoryItems[position]
// orderViewHolder.setOrderDate(orderHistory.orderDate!!)
orderViewHolder.setOrderId(orderHistory.orderId!!)
orderViewHolder.setOrderImage(orderHistory.orderImage!!)
orderViewHolder.setOrderItems(orderHistory.orderItems)
orderViewHolder.setOrderPrice(orderHistory.orderPrice!!)
}
}
override fun getOrderHistoryItem(position: Int): OrderHistory {
return orderHistoryItems[position]
}
override fun actionReOrder(ordreId: String) {
}
}
Here is the Fragment XML
<android.support.v7.widget.RecyclerView android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/rvOrderHistory"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android">
</android.support.v7.widget.RecyclerView>
and Here is the RecyclerView Item XML order_history_item.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView 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"
android:id="#+id/cvOrderItem"
android:layout_margin="4dp"
android:orientation="vertical">
<android.support.constraint.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:orientation="vertical"
android:padding="8dp">
<ImageView
android:id="#+id/ivOrderVendor"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="#drawable/mac" />
<TextView
android:id="#+id/tvOrderId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="Order #2123"
android:textAppearance="#style/TextAppearance.AppCompat.Body2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toRightOf="#+id/ivOrderVendor"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/tvOrderItems"
android:layout_width="242dp"
android:layout_height="35dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:text="MacDonald’s: Big Mac Beef, Big Tasty Beef. El Ezaby: Signal 2, Pantene Shampoo"
android:textAppearance="#style/TextAppearance.AppCompat.Small"
android:layout_marginRight="8dp"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="#+id/tvOrderId"
app:layout_constraintLeft_toRightOf="#+id/ivOrderVendor"
android:layout_marginLeft="8dp"
app:layout_constraintHorizontal_bias="0.0" />
<TextView
android:id="#+id/tvOrderDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:text="03:22 PM 23/2/2017"
app:layout_constraintBottom_toTopOf="#+id/tvOrderItems"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintHorizontal_bias="1.0" />
<TextView
android:id="#+id/tvOrderPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:text="Price: 225.50 LE"
android:textAppearance="#style/TextAppearance.AppCompat.Body2"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#+id/tvOrderItems" />
<Button
android:id="#+id/bReOrder"
android:layout_width="wrap_content"
android:layout_height="35dp"
android:layout_margin="4dp"
android:background="#drawable/chip_accent"
android:foreground="?attr/selectableItemBackground"
android:orientation="vertical"
android:padding="8dp"
android:text="Order Again"
android:textAllCaps="false"
android:textColor="#color/colorAccent"
android:textSize="15sp"
app:layout_constraintHorizontal_bias="0.937"
app:layout_constraintLeft_toRightOf="#+id/tvOrderPrice"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#+id/tvOrderItems"
tools:layout_editor_absoluteY="74dp" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
The issue is that when I start the Activity showing the Fragment, the RecyclerView doesn't show the Item. It only appears if I scrolled the empty RecyclerView or by leaving the App on the foreground and go back to it again.
At the initialization the data of the Adapter is empty but onResume() I make a request which updates the data at the Presenter, Then I notifyDataChange() on the Adapter but nothing updates.
When I debugged I found that onBindViewHolder() isn't called after notifyDataChange() on the Adapter so i don't know why the notifyDataChange() doesn't notify the Adapter that the data is changed.
Anyone has an idea or any solution that might fix this issue?
You need to use runOnUiThread.
if(activity != null) {
activity!!.runOnUiThread {
root.Recycleview.adapter = Adapter(Array)
Adapter(Array).notifyDataSetChanged()
}
}
have a look at this answer
As stupid as it might sound, calling this line of code after setting data for recyclerView, helped me for this issue:
recyclerView.smoothScrollToPosition(0)
PS: technologies that I was using that may have something to do with this were: RJava, Retrofit2, NavigationUI, Fragments, LiveData, and Databinding.
EDIT:
I followed #SudoPlz comment and answer on another question and it also worked too, you have to extend RecyclerView and override requestLayout:
private boolean mRequestedLayout = false;
#SuppressLint("WrongCall")
#Override
public void requestLayout() {
super.requestLayout();
// We need to intercept this method because if we don't our children will never update
// Check https://stackoverflow.com/questions/49371866/recyclerview-wont-update-child-until-i-scroll
if (!mRequestedLayout) {
mRequestedLayout = true;
this.post(() -> {
mRequestedLayout = false;
layout(getLeft(), getTop(), getRight(), getBottom());
onLayout(false, getLeft(), getTop(), getRight(), getBottom());
});
}
}
while still, I would have preferred this to fixed after 4, 5 years, however, this was a good workaround, and you won't forget about them in your view.
I noticed in your item xml your constraintLayout height is match_parent right?
I recommend you to use it as wrap_content