The World
of
RecyclerView
Oleksandr Tolstykh
@a_tolstykh
RecyclerView vs ListView
- Animations
- Advanced API
- Performance
- Other improvements
@a_tolstykh
RecyclerView vs ListView
- Animations
- Advanced API
- Performance
- Other improvements
@a_tolstykh
@a_tolstykh
How to measure UI performance?
@a_tolstykh
How to measure UI performance?
@a_tolstykh
Sample App - Travel With Us
City guides
Base city info
Data is loaded from server
RecyclerView!
@a_tolstykh
Optimize Cells hierarchies
Avoid deep hierarchy
Save level with <merge> when <include>
Layout matters!
android.support.constraint.ConstraintLayout
com.google.android.flexbox.FlexboxLayout
https://developer.android.com/topic/performance/rendering/optimizing-view-hierarchies.html
@a_tolstykh
Minimize onBindViewHolder()
Make onBindViewHolder() as cheap as possible
Avoid item instantiations (memory allocations)
Do as much as you can in onCreateViewHolder()
@a_tolstykh
Correct images scale
Make sure their size and compression are optimal.
Scaling images may also affect the performance.
Do not reinvent the wheel.
Picasso, ImageLoader, Fresco, Glide.
Picasso v/s Imageloader v/s Fresco vs Glide [closed]
http://stackoverflow.com/q/29363321/2308720
@a_tolstykh
Update only the affected items
adapter.notifyItemRemoved(position)�adapter.notifyItemChanged(position)�adapter.notifyItemInserted(position)
* only if you know position of updated item
@a_tolstykh
Profit!
Animations for free!
Optimizations for free!
@a_tolstykh
Remove an item with swipe:
val touchCallback = object : SimpleCallback(0, LEFT or RIGHT) {� // ...� override fun onSwiped(viewHolder: ViewHolder, swipeDir: Int) {� adapter.remove(viewHolder.getAdapterPosition())� adapter.notifyItemRemoved(viewHolder.getAdapterPosition())� }�}�// attaching the touch helper to recycler view�ItemTouchHelper(touchCallback).attachToRecyclerView(recyclerView);
@a_tolstykh
Nested RecyclerView
Override the LinearLayoutManager#getInitialPrefetchItemCount()
RecyclerView is nested inside another RecyclerView
@a_tolstykh
Items cache
recyclerView.setItemViewCacheSize(size: Int)
Documentation:
“Set the number of offscreen views �to retain before adding them �to the potentially shared recycled view pool.”
static final int DEFAULT_CACHE_SIZE = 2; // supportVersion = 25.3.0
@a_tolstykh
Prefetch
Use latest version of support library to use native prefetch optimizations
supportVersion >= 25.1.0
// (enabled by default)
RecyclerView Prefetch by Chet Haase
https://medium.com/google-developers/c2f269075710
*Lollipop and newer
@a_tolstykh
Advanced prefetch
Override the LinearLayoutManager#getExtraLayoutSpace(RecyclerView.State s)
protected int getExtraLayoutSpace (RecyclerView.State state)
Returns the amount of extra space that should be laid out by LayoutManager.
@a_tolstykh
Advanced prefetch
class PreCachingLayoutManager(context: Context) : LinearLayoutManager(context) {� private var customExtraLayoutSpace: Int = -1� set(value) { field = value }�� override fun getExtraLayoutSpace(state: RecyclerView.State): Int {� return if (customExtraLayoutSpace >= 0) customExtraLayoutSpace� else super.getExtraLayoutSpace(state)� }�}
@a_tolstykh
BUT...
Expensive if done while the user may change scrolling direction.
Laying out invisible elements generally comes with significant performance cost.
Useless without increasing cache size (setItemViewCacheSize).
BUT it improves USER EXPERIENCE!
Need to find a balance.�Maybe 1 extra screen (=default) is The Balance?
@a_tolstykh
Even more advanced prefetch...
Prefetch images! (on wi-fi only)
Picasso.with(context).load(url).fetch()�Glide.with(context).load(url).downloadOnly(width, height)
@a_tolstykh
Images�prefetch
Without
images �prefetch
With
images�prefetch
@a_tolstykh
class CitiesAdapter : RecyclerView.Adapter<CityViewHolder>() {� fun updateData(cities: List<City>) {� if (onWifi()) prefetch(cities)� // TODO update adapter data� }� private fun prefetch(cities: List<City>) {� for (city in cities) {� val prefetcher = city.getPrefetcher()� if (prefetcher != null) {� prefetcher.prefetch(mContext)� }� }� }�}
@a_tolstykh
class ImagePrefetcher(private val images: List<Image>) : PreFetcher {�� fun prefetch(context: Context) {� for (image in images) {� Picasso.with(context).load(image.url()).fetch()� }� }�}
@a_tolstykh
class TweetPrefetcher(private val tweetIds: List<Int>) : PreFetcher {�
fun prefetch(context: Context) {� TwitterHelper.loadTweets(tweetIds, object : Callback<List<Tweet>>() {� fun success(result: Result<List<Tweet>>) {� for (tweet in result.data) {� TwitterHelper.preFetchTweetImage(tweet)� }� }� })� }�}
@a_tolstykh
Displayed data pre-calculations
Displayed items are POJOs (Kotlin data classes).
Pre-format date, time, other strings during data parsing.
Pre-calculate immutable values during data parsing.
Profit:
@a_tolstykh
Displayed data pre-calculations (formatting)
@WorkerThread�private fun createRating(value: Float, count: Int): Rating {� val ratingFormatted = String.format(Locale.US, "%.1f", value)� val countFormatted = formatWithMetricPrefix(count)� return Rating.create(value, ratingFormatted, count, countFormatted)�}�@UiThread�fun setRating(rating: Rating) {� ratingText.setText(rating.ratingFormatted())� reviewsCount.setText(rating.numberOfReviewsFormatted())�}
@a_tolstykh
DiffUtil
*since support library version 24.2.0
https://developer.android.com/reference/android/support/v7/util/DiffUtil.html
@a_tolstykh
android.support.v7.util.DiffUtil
Documentation:
“DiffUtil is a utility class that can calculate the difference between two lists and output a list of update operations that converts the first list into the second one.
It can be used to calculate updates for a RecyclerView Adapter.”
@a_tolstykh
DiffUtil.Callback
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition)
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition)
@a_tolstykh
DiffUtil.Callback
class DiffCallback(val newList: List<City>, val oldList: List<City>)
: DiffUtil.Callback() {� override fun getOldListSize(): Int = oldList.size�� override fun getNewListSize(): Int = newList.size�� override fun areItemsTheSame(oldItemPos: Int, newItemPos: Int): Boolean =� oldList.get(oldItemPos).itemId == newList.get(newItemPos).itemId�� override fun areContentsTheSame(oldItemPos: Int, newItemPos: Int): Boolean =� oldList.get(oldItemPos) == newList.get(newItemPos)�
@a_tolstykh
DiffUtil.Callback
val cb = YourDiffCallback(oldList, newList)�val diffResult = DiffUtil.calculateDiff(cb)�diffResult.dispatchUpdatesTo(adapter)
@a_tolstykh
Lenovo Vibe X
Android OS, v4.2
1080 x 1920 pixels
Quad-core 1.5 GHz
Without
DiffUtil
With
DiffUtil
@a_tolstykh
DiffUtil.Callback
public Object getChangePayload(int oldItemPosition, int newItemPosition)
@a_tolstykh
DiffUtil animated values change
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {� val diff = Bundle()� val newRating = newList.get(newItemPosition).rating()� val oldRating = oldList.get(oldItemPosition).rating()� if (newRating.numberOfReviews() !== oldRating.numberOfReviews()) {� diff.putString(KEY_NUMBER_OF_REVIEWS, newRating.numberOfReviewsFormatted())� }� if (newRating.rating().compareTo(oldRating.rating()) != 0) {� diff.putString(KEY_RATING_FORMATTED, newRating.ratingFormatted())� }� return if (diff.size() == 0) null else diff�}
@a_tolstykh
override fun onBindViewHolder(holder: CityViewHolder, index: Int, p: List<Any>) {� if (p.isEmpty()) {� onBindViewHolder(holder, index)� return� }� val payload = p[0] as Bundle� for (key in payload.keySet()) {� if (key == CitiesDiffCallback.KEY_NUMBER_OF_REVIEWS) {� holder.animateReviews(payload.getString(key))� } else if (key == CitiesDiffCallback.KEY_RATING_FORMATTED) {� holder.animateRating(payload.getString(key))� }� }�}
@a_tolstykh
DiffUtil per field items update
Use any animation you want!
@a_tolstykh
DiffUtil - average runtimes
tests are run on Nexus 5X with M
@a_tolstykh
DiffUtil
May take significant time for large dataset...
Use it on a background thread!
@a_tolstykh
DiffUtil. Summary
Avoid unnecessary UI updates!
Animated values change!
@a_tolstykh
TextViewRichDrawable
Android TextView with rich support �of compound drawables.
This is a tiny library which empowers �TextView's compound drawables with:
https://github.com/a-tolstykh/textview-rich-drawable
@a_tolstykh
TextViewRichDrawable
@a_tolstykh
https://github.com/a-tolstykh/textview-rich-drawable
<TextView android:id="@+id/reviews_count"� android:layout_width="wrap_content"� android:layout_height="wrap_content" />�<ImageView android:layout_width="@dimen/icon_small_size"� android:layout_height="@dimen/icon_small_size"� android:layout_marginEnd="@dimen/space_medium"� android:src="@drawable/ic_person" />�<TextView android:id="@+id/rating_value"� android:layout_width="wrap_content"� android:layout_height="wrap_content" />�<ImageView android:layout_width="@dimen/icon_small_size"� android:layout_height="@dimen/icon_small_size"� android:src="@drawable/ic_star" />
<com.tolstykh.textviewrichdrawable.TextViewRichDrawable� android:id="@+id/reviews_count"� android:layout_width="wrap_content"� android:layout_height="wrap_content"� android:layout_marginEnd="@dimen/space_medium"� android:drawableRight="@drawable/ic_person"� app:compoundDrawableHeight="@dimen/icon_small_size"� app:compoundDrawableWidth="@dimen/icon_small_size" />�<com.tolstykh.textviewrichdrawable.TextViewRichDrawable� android:id="@+id/rating_value"� android:layout_width="wrap_content"� android:layout_height="wrap_content"� android:drawableRight="@drawable/ic_star"� app:compoundDrawableHeight="@dimen/icon_small_size"� app:compoundDrawableWidth="@dimen/icon_small_size" />
Before
After
@a_tolstykh
https://github.com/a-tolstykh/textview-rich-drawable
Simplify your layout
4 Views
2 Views
@a_tolstykh
https://github.com/a-tolstykh/textview-rich-drawable
Before
After
RecyclerView Performance Tuning
Prefetch. Cache. Reuse!
@a_tolstykh
RecyclerView Performance Tuning
Do not over engineer. Keep it simple!
@a_tolstykh
RecyclerView Performance Tuning
Avoid premature optimizations.
Think about optimizations when you need them.
@a_tolstykh
Thanks
@a_tolstykh
@a_tolstykh
+OleksandrTolstykh