Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
299 views
in Technique[技术] by (71.8m points)

android - Scrolled down RecyclerView jumps back to top when LiveData updates its content, it should keep its current Y offset

Ladies and Gentlemen,

have a RecyclerView for a list of languages which can be selected using checkboxes. It uses LiveData, so that when the user selects or deselects a language from another client the change is automatically reflected in all of them.

Every time I click on the checkbox, the record for the language is selected and deselected in the room database, and on the server too. Other devices in which the user is logged in are automatically updated too, so I am using LiveData. I am clashing with the RecyclerView bug that pushes the list back to the top every time it is updated, regardless of the current scroll position.

So, say that I have two phones with the language screen open, I scroll them both all the way down to the bottom, and select or deselect the last language. The RecyclerView on both devices scrolls back to the top, but I would like it to stay still.

I have seen plenty of workaround to fix this behaviour, but I am relatively new to android and kotlin, and all the patches suggested are not explained well enough for me to be able to implement them. Answers which explain what to do, and where to do it, would be immensely appreciated.

The RecyclerView is in a Fragment:

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

And this is the list item:

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

The fragment: ( LANGUAGES_SCREEN_VIEWS is a static reference in a companion object for the elements of the screen I need to use in other classes, such as the 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
    }
}

This is the adapter:

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

Some of the solutions I found mention using AsyncData, which I understand is deprecated, so I will not consider those. I am using Flow < List < LanguagesEntity > > in the DAO.

The solution of adding a Layout didn't work:

RecyclerView notifyDataSetChanged scrolls to top position

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

Another solution said the bug depended on the layout's height being "wrap content" and suggested a fixed height, but despite having a stupidly high number for the height, the RecyclerView didn't scroll at all.

A workaround suggested included

This page offers a few solutions:

Refreshing data in RecyclerView and keeping its scroll position

One being

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

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

But I would have absolutely no idea on where to put those lines in my files.

I guess I should intercept the moment before the data is updated ( it can't be on click because the database could be updated by synchronisation from other device ) and the moment during which the recycler view is being updated. I don't understand where this happens.

Any other solution would be welcome, but I would kindly ask you to make it clear enough for a 5 years old to understand. I have been trying to understand how to fix this bug for three days now, and I am beginning to get frustrated!

Thank you very much in advance.

question from:https://stackoverflow.com/questions/66055706/scrolled-down-recyclerview-jumps-back-to-top-when-livedata-updates-its-content

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

The solution was given by Teo in this more specific question:

Problem with LiveData observer changing rather than staying the same

Here is his solution:

// STEP 1 - make a function to notify the variable

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

// STEP 2 - setup recyclerview before everything

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

// STEP 3 - set new value to adapter

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

languagesAdapter.setLanguage(language)
} )

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...