Lazy-load views in RecyclerView within NestedScrollView - android

Targeting androidx 1.0.0 to allow for pre-Lollipop devices (let's say minSdkVersion 17 or so).
I have tried many combinations of views, settings, scrolling modes and layout managers. I have read everything - e.g. this and this and this - regarding this problem. Either I get bad layout/rendering performance or incorrect/buggy scrolling.
Requirements:
A bottomsheet is draggable from the bottom. A common UI pattern in modern apps.
10-20 heavy equally sized child views. These must not be inflated/drawn when invisible.
Using native Android/Google views is preferable.
How can I achieve this?
Here's some pseudo code, showing what I'm trying to accomplish; a RecyclerView (or equivalent) inside a NestedScrollView with a BottomSheetBehavior:
<ScrollView>
<!-- Main content -->
</ScrollView>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true"
android:fitsSystemWindows="true"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
app:behavior_hideable="false"
app:behavior_peekHeight="#dimen/bottom_sheet_peek_height">
<!-- RecyclerView? -->
<TextView android:layout_width="match_parent"
android:layout_height="#dimen/bottom_sheet_peek_height"
android:text="Bottom sheet header" />
<-- N heavy equally sized child views here -->
</androidx.core.widget.NestedScrollView>
I have read that RecyclerView outperforms ListView. Still, it seems to never recycle its views given the above configuration.

Related

Scrollable list trailed by fixed views

This might be a very beginner question, but I'm yet unable to find myself around the android jungle.
I've already got a RecyclerView working to show a list of items (with data binding and Room database and DiffUtil.ItemCallback and all).
I'd like to put 2 links after the list: "missing something?" and "add new entry" that will lead to other fragments.
What I have:
When I put 2 buttons (I don't know yet how to put links, but this is not the point of this question) after the RecyclerView, all in a LinearLayout, they stay fixed near the screen bottom. I mean, the RecyclerView is scrollable by itself, scrolling "beneath" the two buttons, the entire LinearLayout expanding to fill the screen (match_parent).
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="top"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Missing something?"
android:onClick="#{...}" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Add new item"
android:onClick="#{...}" />
</LinearLayout>
What I want
I'd like the 2 buttons to scroll along with the list, so that they are always positioned after the last item (think as if they were items themselves, albeit an heterogeneous list with different types/RecyclerView.ViewHolder).
For a big enough list the buttons will be initially off screen; to be scrolled in if the user happen to scroll to the bottom of the list.
What I tried
I tried with ScrollView around the LinearLayout, and it works, but everywhere everybody say that one should never put a RecyclerView inside a ScrollView (maybe because it is scrollable itself).
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/routines_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<!-- buttons -->
</LinearLayout>
</ScrollView>
Being really a beginner in android programming, I'd like to know how usually this kind of layout should be done. Only main directions will be enough for me.
NB. I don't know if I really need a RecyclerView because I don't expect this list to be lengthy. Maybe usually something around 4 to 8 items, possibly 10. But I really don't expect it to be much bigger than that. For many users the two links will even be visible all the time (i.e. no scroll at all).
RecyclerView is always the most efficient to show a list especially if you are getting the data from a database or an API. Don't put your recyclerview in a scrollview. You can add two items to the bottom of the list as your links and program your recyclerview to exhibit different properties for last two items. That is the best way I can think of. Good Luck!
Also, Recyclerview is very difficult to work with when you are working with complex data. With small lists such as in your case, it can seem inconvenient to create a whole adapter class and do everything you are supposed to do. When you have grasped the concepts on xml android and have plenty experience with that. You can move to jetpack compose and lazy column will make your life easy.

Is NestedScrollView still useful from API 26 and above?

I am currently checking my app for any issues with the new Android 12 overscroll animation. And I came across plenty screens which contain a RecyclerView inside a NestedScrollView. Usually like this:
<androidx.core.widget.NestedScrollView 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/constraintLayout_root"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView [...] />
<TextView [...] />
<TextView [...] />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView_attachment_classifications"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="?marginM"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/textView_categorize_title"
tools:itemCount="4"
tools:listitem="#layout/list_item_adm_attachment_classification" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
This causes some problems with the new overscroll animation. Unfortunately, I cannot show you a recording of the animation problem, but let me describe it: When the screen is at the topmost position and the user is scrolling upwards, all views should stretch a little in relation to the drag event. But it does not do that. It only shows this stretch animation for a very short period of time AFTER the user released their thumb from the screen.
What I have found out so far:
Setting the RecyclerViews isScrollContainer to any value has no impact
Setting the RecyclerViews isNestedScrollingEnabled to any value has no impact
Setting the RecyclerViews overScrollMode to any value has no impact
The same goes for the NestedScrollView
Ironically, replacing the NestedScrollView with a standard ScrollView solves my issue.
I was unable to replicate the problem in a sample app, so it is relatively safe to say that this issue is somewhere in my apps config and architecture. But since using a ScrollView solves my issue, I wanted to know if a NestedScrollView still has any usefulness on API 26 and above or if NestedScrollView is just for backwards compatibility for apps which support older Android versions as well?
Turns out the issues were produced by NestedScrollView and fixed by Google in core-ktx:1.7.0. I had it on version 1.6.0.

Prevent RecyclerView in BottomSheet from rendering offscreen views

Reproducible sample project on github: recyclerview-bottomsheet-not-recycling
On Android it is common to have a draggable bottomsheet. For any scrollable content, RecyclerView is a great candidate. However, when put inside a bottomsheet, recycling/virtualization does not occur for the items that would have been visible if the bottomsheet was fully expanded. This implies a significant performance hit for no apparent reason. I.e. views are inflated and the onDraw() method is called. I believe tons of BottomSheet featured apps silently suffer from this. There ought to be a solution to this non-performant behavior.
Google issue exists: https://issuetracker.google.com/issues/180537056
How can you configure/tweak RecyclerView or its LayoutManager to prevent invisible bottomsheet items from being rendered? Please avoid ViewStub solutions. Let's focus on the root of the problem - the RecyclerView.
Illustration
E.g. If the height of the main layout is 1000px and the invisible RecyclerView has items with a height of 200px, then five views will be immediately created and rendered:
My App
Main View (1000px)
Collapsed bottom sheet
Item 1 renderedItem 2 renderedItem 3 renderedItem 4 renderedItem 5 renderedItem 6 NOT renderedItem 7 NOT rendered...
And a screenshot from the sample app on github. Here a bunch of items below the screen have been rendered - even though they are invisible.
The relevant part of the layout
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="#+id/layout_retainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/black">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:clipToPadding="false"
android:background="#color/white"
app:behavior_hideable="false"
app:behavior_peekHeight="#dimen/bottom_sheet_peek_height"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
android:adapter="#{adapter}" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

RecyclerView wrap_content not working on API 23 and above

I have a layout with a RecyclerView inside a LinearLayout which is also inside a custom NestedScrollView. In api 21 and 22 the layout looks like it's supposed to showing all the elements of the RecyclerView, but in api 23 and above only one or two items are shown leaving the rest of the screen blank. I know the point of RecyclerView is to not use wrap_content, but it is my understanding that you can.
I noticed that when the views above the RecyclerView are visible, wrap_content on the recyclerview works correctly, but in the particular case I'm having the issue those views are all programmatically set to gone, so it seems to have something to do with that. So I'm not sure what to do about it since those views are supposed to be gone. Is this an android sdk bug I can't get around?
<CustomNestedScrollView
android:id="#+id/editProfileScroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical">
<LinearLayout
android:id="#+id/editProfileMainContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/material_baseline_grid_10x"
android:orientation="vertical">
<!-- More code: TextViews and TextViews inside LinearLayouts -->
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/editProfileFieldsRV"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</CustomNestedScrollView>
Try using a RelativeLayout instead of a LinearLayout.
With LinearLayout you will have to set
android:orientation="vertical"
android:weight_sum="3"
and in each element you will have to add android:layout_weight="1".
By doing the above it will space 3 items evenly across the vertical axis.

Having RecyclerView inside a NestedScrollView calls onBindView for all the items

I have two RecyclerViews placed vertically in a LinearLayout. I need to make both of them scrollable and that is why I have put the LinearLayout inside NestedScrollView
This is the my layout file.
<android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="#+id/featured_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<android.support.v7.widget.RecyclerView
android:id="#+id/all_topic_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
Also, I am disabling nested scrolling in Java code.
disableNestedScrolling(findViewById(R.id.all_topic_list));
disableNestedScrolling(findViewById(R.id.featured_list));
My RecylerView library version is 26.1.0
This works fine perfectly, but then onBindViewHolder method is getting called for all the items in the list. Ideally it should only be called for the visible items in the list.
I think the issue is happening because I am giving wrap_content to the RecyclerView. A lot of answers on this question suggest that the issue is solved in v23.2.1, but I am already using v26.1.0. How to solve this issue?
I had exactly the same problem. RecyclerViews are not meant to be placed inside scroll containers with the same scroll direction. The view recycling only works when the height is set to MATCH_PARENT.
Depending on the complexity of the content inside of the NestedScrollView and the anticipated amount of RecyclerView items:
Ignore the problem. If there are only a few simple items, you may
not need view recycling at all.
When I hit the problem, I analysed the layouts of other popular apps: For example, WhatsApp only uses RecyclerViews (or ListViews with view recycling) in some parts of their app.
Particularly, this group settings screen with hundreds of possible items is made of multiple ListViews wrapped by a ScrollView, without any view recycling.
Replace the NestedScrollView with a single
ReyclerView with multiple item types and put all of your scrollable content inside of it. This is the way to go if you need view recycling.
Beware that you also have to convert all the other content in the NestedScrollView (headers and footers, spacing) to RecyclerView items with their own ViewHolders.
If the setup is rather simple, I would recommend you to implement it without additional libraries, following the link above.
There are a few different libraries available to solve your problem (all of them follow the second approach with a single RecyclerView), but most come with a lot of extra features which you may not need:
RendererRecyclerViewAdapter
It comes with a ViewRenderer/ViewModel interface, which works like a
"partial" RecyclerView for a single item type. You would create one
for every item type and then register them in a single adapter.
Epoxy
A library/framework create by airbnb and used heavily in their app.
They have a lot of scrollable content (similar to a web page) with a
lot of different item types. Epoxy also helps with the composition of
the different items on a page, and it handles animations when the
content or its order changes. Too much if you only need it for a single screen.
Litho
A complete UI framework created by Facebook which comes with it's own rendering engine, a replacement for xml layouts and much more. As far as I understand, it allows you to do to handle large amounts of items (like the Facebook timeline) and the view recycling is handled automatically. Like Epoxy, you would only use this if your app includes things like endless scrolling with a lot of different item types and you really need the performance.
I tried Epoxy and RendererRecyclerViewAdapter, but after all I created my own multiple item type adapter. It can be created in less than 100 lines of code.
Starting from RecyclerView:1.2.0-alpha04 we can use ConcatAdapter to solve this problem
https://developer.android.com/reference/androidx/recyclerview/widget/ConcatAdapter
I tried your problem by adding 20 items in each recyclerview, with NestedScrollView application called onBindViewHolder method 40 times. As you disabling nested scrolling in Java code i suggest to use Scrollview. By using ScrollView application called onBindViewHolder 33 times.
If you fix your recyclerView's height to specific size instead of "match-parent" it will reduce call to onBindViewHolder greatly.
<ScrollView 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="false">
<android.support.v7.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.vishal.my2.MainActivity">
<android.support.v7.widget.RecyclerView
android:id="#+id/featured_list"
android:layout_width="match_parent"
android:layout_height="300dp" />
<android.support.v7.widget.RecyclerView
android:id="#+id/all_topic_list"
android:layout_width="match_parent"
android:layout_height="300dp" />
</android.support.v7.widget.LinearLayoutCompat>
</ScrollView>
If Specifying hardcoded value to recyclerView's height does not meet your application requirement then you can try using ListView instead of recyclerView. pardon me if i am wrong, This was my first time answering any question.
Add this to nested scroll view android:fillViewport="false"

Categories

Resources