TransWikia.com

RecyclerView inside NestedScrollview alternative

Stack Overflow Asked by user1851366 on January 29, 2021

I have a layout with this hierarchy

< NestedScrollView fillViewPort=true>
< LinearLayout>
< Viewgroup/>
< ViewGroup/>
< RecyclerView/>
< ViewGroup/>
< /LinearLayout>
< /NestedScrollView>

Sometimes i need to update my recyclerview elements, but it freeze main thread.
My guess its because scrollview need to measure it all again.
I really like to know how i should do this?

  • Replace recyclier view with layoutinlfate?
  • Recyclerview with height fixed?
  • Replace nestedscrollview, with recyclview? will have recyclview inside reclyerview. This works?

5 Answers

First of all, based on Android documentation:

Never add a RecyclerView or ListView to a scroll view. Doing so results in poor user interface performance and poor user experience.

Updating UI is always done on the main thread. So when you want to update the RecyclerView it affects performance. For a better user experience, you can do your calculation in another thread and update UI in the main thread. in bellow code, I simulate your situation:

activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout 

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="700dp"
            android:background="@color/black"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:nestedScrollingEnabled="false"/>

        </LinearLayout>

    </androidx.core.widget.NestedScrollView>

</androidx.constraintlayout.widget.ConstraintLayout>

in my example, updating the list occurs in every second. here is the code:

 public class MainActivity extends AppCompatActivity {
    
        private MyAdapter myAdapter;
        int cnt = 0;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            myAdapter = new MyAdapter();
    
            RecyclerView recyclerView = findViewById(R.id.recycler_view);
            LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
            recyclerView.setLayoutManager(layoutManager);
            recyclerView.setAdapter(myAdapter);
    
    
            AsyncTask.execute(() -> updateList());
    
    
        }
    
        private void updateList() {
    
            Timer timer = new Timer();
            timer.schedule(new TimerTask() {
    
                @Override
                public void run() {
                    cnt++;
                    myAdapter.addToList(cnt + "");
    
                }
    
            }, 1, 1000);//Update List every second
        }
    }

list is updated via updateList() method. for running this in new thread I use AsyncTask :

AsyncTask.execute(() -> updateList());

and finally here is my adapter:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
   
    List<String> list = new ArrayList<>(); 

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false);
        return new MyViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
        holder.textView.setText(list.get(position));
    }

    @Override
    public int getItemCount() {
        return list.size();
    }

    public class MyViewHolder extends RecyclerView.ViewHolder {

        TextView textView;

        public MyViewHolder(@NonNull View itemView) {
            super(itemView);
            textView = itemView.findViewById(R.id.text_view);

        }
    }

    public void addToList(String s) {
        list.add(s);
        new Handler(Looper.getMainLooper()).post(() -> {
            notifyItemInserted(list.size());
        });
    }
}

As can be seen notifyItemInserted is called in Handler for updating UI on the main thread. for notifying recycler in different situations you can refer to https://stackoverflow.com/a/48959184/12500284.

Answered by AlirezA Barakati on January 29, 2021

Try to wrap current layout with tag like below:

< RelativeLayout> // math_parent
< NestedScrollView fillViewPort=true>
< LinearLayout>
< Viewgroup/>
< ViewGroup/>
< RecyclerView/>
< ViewGroup/>
< /LinearLayout>
< /NestedScrollView>
< /RelativeLayout>

It will prevent scrollview and recyclerview measure again.

Answered by Louis Nguyen on January 29, 2021

when you are using vertical recycler view inside vertical NestedScrollView without using a constant height, RecyclerView height will be wrap_content even when you set the RecylcerView height to match_parent. so when you update your RecyclerView data, MainThread will be frozen because all the RecyclerView items onBind() method will be called. This means the RecyclerView doesn't recycle anything during scroll! if you can not use the MultipleViewType for your design, try to set a constant height to the RecyclerView. This solution makes some difficulties for you. such as handling scrollablity.

Answered by Hamed Rahimvand on January 29, 2021

The most likely reason for a UI freeze is that you're retrieving the data from some slow source, like the network or a database.

If that is the case, the solution is to retrieve the data on a background thread. These days I'd recommend using Kotlin Coroutines and Kotlin Flow.

Your DB would return a Flow<List>, the contents of which will automatically be updated whenever the data in the DB changes, and you can observe the data updates in the ViewModel.

Get a Flow object from your database:

@Dao
interface MyDataDao {

    @Query("SELECT * FROM mydata")
    fun flowAll(): Flow<List<MyDataEntity>>
}

The Fragment will observe LiveData and update the list adapter when it changes:

class MyScreenFragment(): Fragment() {

    override fun onCreate() {
        viewModel.myDataListItems.observe(viewLifecycleOwner) { listItems ->
            myListAdapter.setItems(listItems)
            myListAdapter.notifyDataSetChanged()
        }
    }
}

In the ViewModel:

@ExperimentalCoroutinesApi
class MyScreenViewModel(
    val myApi: MyApi,
    val myDataDao: MyDataDao
) : ViewModel() {

    val myDataListItems = MutableLiveData<List<MyDataListItem>>()

    init {
        // launch a coroutine in the background to retrieve our data 
        viewModelScope.launch(context = Dispatchers.IO) { 
            // trigger the loading of data from the network
            // which you should then save into the database,
            // and that will trigger the Flow to update
            myApi.fetchMyDataAndSaveToDB()
            collectMyData()
        }
    }

    /** begin collecting the flow of data */
    private fun collectMyData() {
        flowMyData.collect { myData ->
            // update the UI by posting the list items to a LiveData
            myDataListItems.postValue(myData.asListItems())
        }
    }

    /** retrieve a flow of the data from database */
    private fun flowMyData(): Flow<List<MyData>> {
        val roomItems = myDataDao.flowAll()
        return roomItems.map { it.map(MyDataEntity::toDomain) }
    }

    /** convert the data into list items */
    private fun MyData.asListItems(): MyDataListItem {
        return this.map { MyDataListItem(it) }
    }
}

In case you didn't know how to define the objects for in a Room Database, I'll give you this as a hint:

// your data representation in the DB, this is a Room entity
@Entity(tableName = "mydata")
class MyDataEntity(
    @PrimaryKey
    val id: Int,
    val date: String,
    // ...
)

// Domain representation of your data
data class MyData(
    val id: Int,
    val date: SomeDateType,
    // ...
)

// Map your DB entity to your Domain representation
fun MyDataEntity.toDomain(): MyData {
    return MyData(
        id = id,
        date = SomeDateFormatter.format(date),
        // ...
    )
}

Answered by Tomáš Beňo on January 29, 2021

This is a common UI pattern and android:nestedScrollingEnabled="true" is not a fix for this.

Best approach to this pattern is to use a single recyclerview with multiple view types instead of having nested elements. The result is a more complicated recyclerview but you have better performance and more control with it.

Answered by Nezih Yılmaz on January 29, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP