当 LiveData 更新其内容时,向下滚动 RecyclerView 跳回顶部,它应该保持其当前的 Y 偏移量

问题描述

我有一个 RecyclerView 可以使用复选框选择语言列表。它使用 LiveData,因此当用户从另一个客户端选择或取消选择一种语言时,更改会自动反映在所有客户端中。

每次我单击复选框时,都会在房间数据库和服务器上选择和取消选择语言记录。用户登录的其他设备也会自动更新,所以我使用的是 LiveData。我正在与 RecyclerView 错误发生冲突,该错误会在每次更新时将列表推回到顶部,而不管当前滚动位置如何。

因此,假设我有两部手机打开了语言屏幕,我将它们一直向下滚动到底部,然后选择或取消选择最后一种语言。两个设备上的 RecyclerView 都滚动回顶部,但我希望它保持静止。

我已经看到了很多解决此问题的方法,但我对 Android 和 Kotlin 相对较新,并且所有建议的补丁都没有得到很好的解释,无法实现它们。我需要做什么,我应该在哪里做?

RecyclerView 在一个片段中:

<FrameLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/languages_container"
    tools:context=".ui.members.languages.LanguagesFragment">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/languages_recycler_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:paddingBottom="15dp"
            app:layoutManager="linearlayoutmanager"
            app:layout_constraintTop_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:context="com.example.flashcardmallard.ui.members.languages.LanguagesFragment"
            tools:listitem="@layout/fragment_languages_cards" />

</FrameLayout>

这是列表项:

<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="40dp"
    android:orientation="horizontal">

    <CheckBox
        android:id="@+id/language_selection"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

片段(LANGUAGES_SCREEN_VIEWS 是伴随对象中的静态引用,用于我需要在其他类中使用的屏幕元素,例如 viewmodel):

class LanguagesFragment : Fragment()
{
    override fun onCreateView ( inflater : LayoutInflater,container : ViewGroup?,savedInstanceState : Bundle? ) : View
    {
        LANGUAGES_SCREEN_VIEWS = FragmentLanguagesBinding.inflate ( inflater,container,false )
        val languagesviewmodel = viewmodelProvider ( this ).get ( Languagesviewmodel::class.java )
        LANGUAGES_FRAGMENT = this

        languagesviewmodel.getLanguagesForUser().observe ( viewLifecycleOwner,{ languages ->
            LANGUAGES_SCREEN_VIEWS.languagesRecyclerView.adapter = LanguagesRecyclerViewAdapter ( languages ) } )

        return LANGUAGES_SCREEN_VIEWS.root
    }
}

这是适配器:

class LanguagesRecyclerViewAdapter ( private val languages: List < LanguagesEntity > ) : RecyclerView.Adapter < LanguagesRecyclerViewAdapter.ViewHolder > ()
{
    override fun onCreateViewHolder ( viewGroup: ViewGroup,i: Int ): ViewHolder
    {
        return ViewHolder ( LayoutInflater.from ( viewGroup.context ).inflate ( R.layout.fragment_languages_cards,viewGroup,false ) )
    }

    override fun onBindViewHolder ( viewHolder: ViewHolder,index: Int )
    {
        val language = languages [ index ]
        viewHolder.languageCard.text      = language.name
        viewHolder.languageCard.isChecked = language.spoken
    }

    override fun getItemCount() = languages.size

    inner class ViewHolder ( itemView: View ) : RecyclerView.ViewHolder ( itemView )
    {
        val languageCard: CheckBox = itemView.findViewById ( R.id.language_selection )

        init
        {
            languageCard.setonClickListener {

                LANGUAGE_RECYCLER_VIEW_STATE = LANGUAGES_SCREEN_VIEWS.languagesRecyclerView.layoutManager!!.onSaveInstanceState()!!

                GlobalScope.launch {
                    // Save to local database
                    LanguageRepository ().updateLanguage ( languageCard )

                // Delete on server,pop up if not possible
                var dataSync = DataSyncBean ( languages = arraylistof ( LanguageBean ( addedOrDeleted = if (
                    languageCard.isChecked ) ADDED else DELETED,name = languageCard.text.toString() ) ) )
                try
                {
                    dataSync = Retrofit.Builder().baseUrl ( ConstantsUtil.BASE_URL ).addConverterFactory ( GsonConverterFactory.create () ).client (
                    OkHttpClient.Builder().cookieJar ( CookieUtil () ).build() ).build().create (
                        RestfulCalls::class.java ).updateUserLanguageMapping ( dataSync )
                }
                catch ( e : Exception )
                {
                    dataSync.syncOutcome = ConstantsUtil.SERVER_UNREACHABLE
                    withContext ( dispatchers.Main ) {
                        val alertDialog : AlertDialog.Builder = AlertDialog.Builder ( LANGUAGES_FRAGMENT.context )
                        alertDialog.setTitle                  ( R.string.no_server_connection )
                        alertDialog.setMessage                ( R.string.changes_local_only   )
                            alertDialog.setPositiveButton         ( R.string.ok ) { _,_ -> }
                            val alert: AlertDialog = alertDialog.create()
                            alert.setCanceledOnTouchOutside       ( true )
                            alert.show()
                        }
                    }
                }
            }
        }
    }
}

我发现的一些解决方案提到使用 AsyncData,据我所知已被弃用,因此我不会考虑这些。我在 DAO 中使用了 Flow < List < LanguagesEntity > >

添加Layout的解决方法没用:

RecyclerView notifyDataSetChanged scrolls to top position

val linearlayoutmanager = linearlayoutmanager ( context,RecyclerView.VERTICAL,false )
LANGUAGES_SCREEN_VIEWS.languagesRecyclerView.setLayoutManager(linearlayoutmanager)

一个解决方案表示该错误取决于布局的高度是“包裹内容”并建议固定高度,但尽管高度的数字非常高,但 RecyclerView 根本没有滚动。

建议包含一个解决方法

页面提供了一些解决方案:

Refreshing data in RecyclerView and keeping its scroll position

一个

// Save state
private Parcelable recyclerViewState;
recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState();

// Restore state
recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);

但我完全不知道将这些行放在我的文件中的什么位置。

我想我应该截取数据更新之前的那一刻(它不能被点击,因为数据库可以通过其他设备的同步来更新)和回收者视图正在更新的那一刻。我不明白这是哪里发生的。

解决方法

Teo 在这个更具体的问题中给出了解决方案:

Problem with LiveData observer changing rather than staying the same

这是他的解决方案:

// STEP 1 - 制作一个函数来通知变量

internal fun setLanguage(lang: List<LanguagesEntity>) {
   languages = lang
   notifyDataSetChanged()    
}

// STEP 2 - 在一切之前设置 recyclerview

val languages = mutableListOf < LanguagesEntity > ()
var languagesAdapter = LanguagesRecyclerViewAdapter(languages)
  LANGUAGES_SCREEN_VIEWS.languagesRecyclerView.adapter = languagesAdapter 

// 第 3 步 - 为适配器设置新值

languagesViewModel.getLanguagesForUser().observe( requireActivity(),{ languages ->

languagesAdapter.setLanguage(language)
} )