【问题标题】:Fragment takes time to load RecyclerView when the list is large当列表很大时,Fragment 需要时间加载 RecyclerView
【发布时间】:2020-07-27 18:27:19
【问题描述】:

我有一个 RecyclerView 与 Paging 一起实现,以从 Room Database 加载列表。当大小很小时,该列表可以正常工作。当大小达到 50 - 60 左右时,列表仍然可以正常工作,但是当我切换到另一个片段然后返回列表时,它会阻塞 UI 大约 1.5 - 2 秒,这在用户体验中非常沉闷(参见下面的 GIF ):

我的代码如下:

@Query("SELECT * FROM account_table WHERE userID = :userID")
fun getAll(userID: String): DataSource.Factory<Int, Account>

存储库

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)

}

适配器

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

视图模型

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

片段

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

主要活动布局

<?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>

片段帐户布局:

<?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 列表布局

<?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>

我的分页版本是2.1.2

有人可以帮我解决这个问题吗?我尝试了几天寻找修复方法,但没有任何效果。

感谢您的帮助。

【问题讨论】:

  • 请包含您的布局 XML
  • 嗨,Ian,我已经编辑了问题并包含了我的 xml 布局。请帮忙

标签: android kotlin android-recyclerview android-paging android-paging-library


【解决方案1】:

您必须在活动布局中删除您的 NestedScrollView (Nested_Scroll) - 您不能在 NestedScrollView 中放置垂直的 RecyclerView

NestedScrollView 在垂直滚动方向上扩展每个子项以确定最大滚动距离。这意味着它为RecyclerView 提供了无限的高度来展开。这会导致RecyclerView 膨胀每个元素,破坏所有视图回收破坏分页的使用 - 给定无限的高度,它会继续要求分页越来越多行来填充空间。

【讨论】:

  • 非常感谢,我会试试看。另一件事是我使用所有片段中的嵌套滚动来确定滚动更改。如果页面被滚动,我会在我的工具栏上添加高程,当滚动为 0 时,我会删除高程以获得视觉效果(您可以在 GIF 中看到它)。现在,当我删除这个嵌套滚动时,是否有另一种方法可以实现这种效果?
  • 您应该使用AppBarLayout,它会自动执行此操作。
  • 非常感谢伊恩。你拯救了这一天!感谢帮助
猜你喜欢
  • 1970-01-01
  • 2023-03-10
  • 1970-01-01
  • 1970-01-01
  • 2013-12-09
  • 2021-05-14
  • 2012-06-24
  • 2017-08-10
  • 1970-01-01
相关资源
最近更新 更多