5

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?
aminography
  • 18,195
  • 11
  • 51
  • 57
user1851366
  • 1,056
  • 3
  • 17
  • 34
  • 2
    You should post the layout xml and other related blocks of code in your question. – aminography Oct 06 '20 at 05:59
  • 1
    Is there any problem if you create demo work? – IntelliJ Amiya Oct 06 '20 at 07:01
  • 1
    It always confuses me, that there are reasons to put a recyclerview into a scrollview. Recyclerview itself is scrollable, and nesting scrolling elements is always something i try to avoid (even if this means some arguing with UX-designers). `..but it freeze main thread` i asume your problem comes due to wrong recyclerview updates on the main thread. Layout should work, and Scrollview will not layout itself if recylerview items are updated! Only if the height from the Recyclerview will be changed – longi Oct 06 '20 at 07:47
  • thats the problem. Recyclerview height will be change – user1851366 Oct 07 '20 at 12:44
  • What are you trying to achieve, exactly? – Ethan Moore Oct 07 '20 at 19:25
  • The layout that i show above, but elements inside recyclerview and others can change height – user1851366 Oct 09 '20 at 07:28
  • You should upload the code – Braian Coronel Oct 11 '20 at 05:19
  • I've created a layout exactly like what you described. But unfortunately, there is no freezing when I add a new item to the `RecyclerView`. Have a look at this: https://media.giphy.com/media/3KOEbayR6xmW4YhO8g/source.mp4 – aminography Oct 14 '20 at 12:17
  • You have to provide a [mcve] which freezes the UI. Not the whole code just the essential. – ADM Oct 18 '20 at 08:59
  • Try horizontal Recycler View in Nested scroll view layout, because if you choose vertical scrolling then nested scroll view will Set all items of recycler view at a time so, thats why screen freezes. – DeePanShu Oct 19 '20 at 05:01

5 Answers5

2

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.

Nezih Yılmaz
  • 596
  • 3
  • 8
0

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.

Hamed Rahimvand
  • 425
  • 4
  • 11
0

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.

Louis Nguyen
  • 317
  • 5
  • 16
0

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.

Amirhosein Heydari
  • 3,062
  • 4
  • 15
  • 26
-1

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),
        // ...
    )
}
zOqvxf
  • 1,045
  • 12
  • 16