Mervin Hemaraju
Mervin Hemaraju

Reputation: 2117

Fragment takes time to load RecyclerView when the list is large

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):

Recyclerview Issue

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

Answers (1)

ianhanniballake
ianhanniballake

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

Related Questions