Reputation: 2117
I have a RecyclerView implemented along with Paging to load a list from Room Database. The list works fine when the size is small. When the size reaches around 50 - 60, the list still works fine but when i switch to another fragment and then come back to the list, its blocks the UI for around 1.5 - 2 seconds which is super dull in user experience (See GIF below):
My code is as follows:
DAO
@Query("SELECT * FROM account_table WHERE userID = :userID")
fun getAll(userID: String): DataSource.Factory<Int, Account>
Repository
class AccountRepository private constructor(application: Application) {
private val database =
LockyDatabase.getDatabase(
application
)
private val accountDao = database.accountDao()
companion object {
@Volatile
private var instance: AccountRepository? = null
fun getInstance(application: Application) =
instance ?: synchronized(this) {
instance ?: AccountRepository(application).also { instance = it }
}
}
fun getAll(userID: String) = accountDao.getAll(userID)
}
adapter
class CredentialsPagingAdapter(
private val clickListener: ClickListener,
private val optionsClickListener: OptionsClickListener?,
private val isSimplified: Boolean
) : PagedListAdapter<Credentials, CredentialsViewHolder>(
diffCallback
) {
companion object {
private val diffCallback = object : DiffUtil.ItemCallback<Credentials>() {
override fun areItemsTheSame(oldItem: Credentials, newItem: Credentials): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Credentials, newItem: Credentials): Boolean {
return oldItem.equals(newItem)
}
}
}
override fun onBindViewHolder(holder: CredentialsViewHolder, position: Int) {
holder.bind(
clickListener,
optionsClickListener,
getItem(position),
isSimplified
)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CredentialsViewHolder {
return CredentialsViewHolder.from(
parent
)
}
}
viewModel
val accounts = Transformations.switchMap(_sort) {
when (true) {
it.name -> _accounts.sortByEntryName
it.username -> _accounts.sortByUsername
it.email -> _accounts.sortByEmail
it.website -> _accounts.sortByWebsite
it.authType -> _accounts.sortByAuthenticationType
else -> _accounts
}.toLiveData(pageSize = resources.getInteger(R.integer.size_paging_list_default))
}
fragment
private fun subscribeAccounts() {
val adapter = CredentialsPagingAdapter(
/* The click listener to handle account on clicks */
ClickListener {
navigateTo(
AccountFragmentDirections.actionFragmentAccountToFragmentViewAccount(
it as Account
)
)
},
/* The click listener to handle popup menu for each accounts */
OptionsClickListener { view, credential ->
view.apply {
isEnabled = false
}
createPopupMenu(view, credential as Account)
},
false
)
binding.RecyclerViewAccount.apply {
/*
* State that layout size will not change for better performance
*/
setHasFixedSize(true)
/* Bind the layout manager */
layoutManager = LinearLayoutManager(requireContext())
/* Bind the adapter */
this.adapter = adapter
}
viewModel.accounts.observe(viewLifecycleOwner, Observer {
if (it != null) {
/*
* If accounts is not null
* Load recyclerview and
* Update the ui
*/
lifecycleScope.launch {
adapter.submitList(it as PagedList<Credentials>)
}
updateUI(it.size)
}
})
}
Main Activity Layout
<?xml version="1.0" encoding="utf-8"?>
<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">
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/Drawer_Main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.main.MainActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/Layout_Coordinator_Main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/Toolbar_Main"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@drawable/custom_rounded_background_toolbar"
android:clipChildren="false"
android:outlineAmbientShadowColor="@color/colorShadowColor"
android:outlineSpotShadowColor="@color/colorShadowColor"
android:paddingStart="8dp"
android:paddingEnd="8dp"
app:contentInsetStartWithNavigation="0dp"
tools:targetApi="p">
...
</com.google.android.material.appbar.MaterialToolbar>
<androidx.core.widget.NestedScrollView
android:id="@+id/Nested_Scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize"
android:fillViewport="true">
<fragment
android:id="@+id/Navigation_Host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation_drawer_main"
tools:ignore="FragmentTagUsage" />
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton ... />
<com.google.android.material.floatingactionbutton.FloatingActionButton ... />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/Navigation_View"
style="@style/Locky.Widget.Custom.NavigationView"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:clipToPadding="false"
android:paddingStart="0dp"
android:paddingEnd="16dp"
app:headerLayout="@layout/drawer_header"
app:itemTextAppearance="@style/Locky.Text.Body.Drawer"
app:menu="@menu/menu_drawer_main" />
</androidx.drawerlayout.widget.DrawerLayout>
</layout>
Fragment Account Layout:
<?xml version="1.0" encoding="utf-8"?>
<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">
<data>
<import type="android.view.View" />
<import type="com.th3pl4gu3.locky_offline.repository.Loading.List" />
<variable
name="ViewModel"
type="com.th3pl4gu3.locky_offline.ui.main.main.account.AccountViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/Layout_Fragment_Account"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorOnSurface">
<!--
Recyclerview
-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/RecyclerView_Account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="@{ViewModel.loadingStatus==List.LIST ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone" />
<!--
Empty Views and group
-->
<androidx.constraintlayout.widget.Group
android:id="@+id/Empty_View"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{ViewModel.loadingStatus==List.EMPTY_VIEW ? View.VISIBLE : View.GONE}"
app:constraint_referenced_ids="Empty_View_Illustration,Empty_View_Title,Empty_View_Subtitle" />
<ImageView
android:id="@+id/Empty_View_Illustration" ... />
<TextView
android:id="@+id/Empty_View_Title" ... />
<TextView
android:id="@+id/Empty_View_Subtitle" ... />
<!--
Progress Bar
-->
<include
android:id="@+id/Progress_Bar"
layout="@layout/custom_view_list_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="@{ViewModel.loadingStatus==List.LOADING ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Recyclerview List Layout
<?xml version="1.0" encoding="utf-8"?>
<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">
<data>
<import type="android.view.View" />
<variable
name="IsSimplifiedVersion"
type="Boolean" />
<variable
name="Credential"
type="com.th3pl4gu3.locky_offline.core.main.credentials.Credentials" />
<variable
name="ClickListener"
type="com.th3pl4gu3.locky_offline.ui.main.main.ClickListener" />
<variable
name="OptionsClickListener"
type="com.th3pl4gu3.locky_offline.ui.main.main.OptionsClickListener" />
</data>
<com.google.android.material.card.MaterialCardView
style="@style/Locky.ListCardView"
credentialCardConfiguration="@{Credential}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:clickable="true"
android:focusable="true"
android:onClick="@{() -> ClickListener.onClick(Credential)}">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/Credential_Logo"
configureLogo="@{Credential}"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginEnd="16dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/Barrier_Logo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_locky_with_background_circle" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/Barrier_Logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="Credential_Logo" />
<TextView
android:id="@+id/Credential_Entry_Name"
style="@style/Locky.Text.Title6.List"
listTitleMessageCardEligibility="@{Credential}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:ems="11"
android:singleLine="true"
android:text="@{Credential.entryName}"
app:layout_constraintBottom_toTopOf="@+id/Credential_First_Subtitle"
app:layout_constraintStart_toEndOf="@id/Barrier_Logo"
app:layout_constraintTop_toTopOf="@+id/Credential_Logo"
app:layout_constraintVertical_chainStyle="spread_inside"
tools:text="This is an entry name and it can be very very very long" />
<TextView
android:id="@+id/Credential_First_Subtitle"
style="@style/Locky.Text.Subtitle.List.Primary"
setCredentialSubtitle="@{Credential}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:ems="13"
android:singleLine="true"
app:layout_constraintBottom_toTopOf="@+id/Credential_Second_Subtitle"
app:layout_constraintStart_toStartOf="@+id/Credential_Entry_Name"
app:layout_constraintTop_toBottomOf="@+id/Credential_Entry_Name"
app:layout_constraintVertical_chainStyle="spread"
tools:text="This is the very first subtitle and this can be very long too" />
<TextView
android:id="@+id/Credential_Second_Subtitle"
style="@style/Locky.Text.Subtitle.List.Secondary"
setCredentialOtherSubtitle="@{Credential}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:ems="14"
android:singleLine="true"
android:textColor="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="@+id/Credential_Logo"
app:layout_constraintStart_toStartOf="@+id/Credential_Entry_Name"
app:layout_constraintTop_toBottomOf="@+id/Credential_First_Subtitle"
app:layout_constraintVertical_chainStyle="spread"
tools:text="This is the second subtitle and this can be very long too" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/Barrier_More_Options"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="start"
app:constraint_referenced_ids="Credential_More_Options" />
<ImageButton
android:id="@+id/Credential_More_Options"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:background="@drawable/custom_states_background_button_image"
android:onClick="@{(view) -> OptionsClickListener.onClick(view, Credential)}"
android:scaleType="centerCrop"
android:src="@drawable/ic_more_options"
android:visibility="@{IsSimplifiedVersion ? View.GONE : View.VISIBLE}"
app:layout_constraintBottom_toBottomOf="@+id/Credential_Logo"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/Barrier_More_Options"
app:layout_constraintTop_toTopOf="@+id/Credential_Logo" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
My paging version is 2.1.2
Can someone please help me on this. I tried several days looking for fix but nothing works.
I appreciate the help.
Upvotes: 1
Views: 1172
Reputation: 199805
You must remove your NestedScrollView
(Nested_Scroll
) in your activity layout - you cannot put a vertical RecyclerView
within a NestedScrollView
.
A NestedScrollView
expands every child in the vertical scroll direction to determine the maximum scroll distance. This means that it gives the RecyclerView
an infinite height to expand into. This causes RecyclerView
to inflate every element, defeating all view recycling and defeating the use of paging - given infinite height, it'll continue to ask Paging for more and more rows to fill the space.
Upvotes: 7