1 of 46

The World

of

RecyclerView

Oleksandr Tolstykh

@a_tolstykh

2 of 46

RecyclerView vs ListView

- Animations

- Advanced API

- Performance

- Other improvements

@a_tolstykh

3 of 46

RecyclerView vs ListView

- Animations

- Advanced API

- Performance

- Other improvements

@a_tolstykh

@a_tolstykh

4 of 46

How to measure UI performance?

  • FPS�https://github.com/friendlyrobotnyc/TinyDancer
  • Profile GPU rendering�https://developer.android.com/studio/profile/dev-options-rendering.html
  • Aggregate frame stats�https://developer.android.com/training/testing/performance.html
  • Many others...

@a_tolstykh

5 of 46

How to measure UI performance?

  • FPS�https://github.com/friendlyrobotnyc/TinyDancer
  • Profile GPU rendering�https://developer.android.com/studio/profile/dev-options-rendering.html
  • Aggregate frame stats�https://developer.android.com/training/testing/performance.html
  • Many others...

@a_tolstykh

6 of 46

Sample App - Travel With Us

City guides

Base city info

Data is loaded from server

RecyclerView!

@a_tolstykh

7 of 46

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

8 of 46

Minimize onBindViewHolder()

Make onBindViewHolder() as cheap as possible

Avoid item instantiations (memory allocations)

Do as much as you can in onCreateViewHolder()

@a_tolstykh

9 of 46

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

10 of 46

Update only the affected items

adapter.notifyItemRemoved(position)�adapter.notifyItemChanged(position)�adapter.notifyItemInserted(position)

* only if you know position of updated item

@a_tolstykh

11 of 46

Profit!

Animations for free!

Optimizations for free!

@a_tolstykh

12 of 46

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 viewItemTouchHelper(touchCallback).attachToRecyclerView(recyclerView);

@a_tolstykh

13 of 46

Nested RecyclerView

Override the LinearLayoutManager#getInitialPrefetchItemCount()

RecyclerView is nested inside another RecyclerView

@a_tolstykh

14 of 46

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

15 of 46

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

16 of 46

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

17 of 46

Advanced prefetch

class PreCachingLayoutManager(context: Context) : LinearLayoutManager(context) {private var customExtraLayoutSpace: Int = -1set(value) { field = value }�� override fun getExtraLayoutSpace(state: RecyclerView.State): Int {return if (customExtraLayoutSpace >= 0) customExtraLayoutSpace� else super.getExtraLayoutSpace(state)}}

@a_tolstykh

18 of 46

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

19 of 46

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

20 of 46

Images�prefetch

Without

images �prefetch

With

images�prefetch

@a_tolstykh

21 of 46

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

22 of 46

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

23 of 46

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

24 of 46

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:

  • Minor performance optimisations.
  • Formatting is done in BG thread.
  • Formatting is done in single place.

@a_tolstykh

25 of 46

Displayed data pre-calculations (formatting)

@WorkerThreadprivate 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)}@UiThreadfun setRating(rating: Rating) {� ratingText.setText(rating.ratingFormatted())� reviewsCount.setText(rating.numberOfReviewsFormatted())}

@a_tolstykh

26 of 46

DiffUtil

*since support library version 24.2.0

https://developer.android.com/reference/android/support/v7/util/DiffUtil.html

@a_tolstykh

27 of 46

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

28 of 46

DiffUtil.Callback

public boolean areItemsTheSame(int oldItemPosition, int newItemPosition)

public boolean areContentsTheSame(int oldItemPosition, int newItemPosition)

@a_tolstykh

29 of 46

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

30 of 46

DiffUtil.Callback

val cb = YourDiffCallback(oldList, newList)val diffResult = DiffUtil.calculateDiff(cb)�diffResult.dispatchUpdatesTo(adapter)

@a_tolstykh

31 of 46

Lenovo Vibe X

Android OS, v4.2

1080 x 1920 pixels

Quad-core 1.5 GHz

Without

DiffUtil

With

DiffUtil

@a_tolstykh

32 of 46

DiffUtil.Callback

public Object getChangePayload(int oldItemPosition, int newItemPosition)

@a_tolstykh

33 of 46

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

34 of 46

override fun onBindViewHolder(holder: CityViewHolder, index: Int, p: List<Any>) {if (p.isEmpty()) {� onBindViewHolder(holder, index)return}val payload = p[0] as Bundlefor (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

35 of 46

DiffUtil per field items update

Use any animation you want!

  • android.widget.TextSwitcher
  • com.hanks.htextview.HTextView
  • custom...

@a_tolstykh

36 of 46

DiffUtil - average runtimes

  • 100 items and 10 modifications: avg: 0.39 ms, median: 0.35 ms
  • 100 items and 100 modifications: 3.82 ms, median: 3.75 ms
  • 100 items and 100 modifications without moves: 2.09 ms, median: 2.06 ms
  • 1000 items and 50 modifications: avg: 4.67 ms, median: 4.59 ms
  • 1000 items and 50 modifications without moves: avg: 3.59 ms, median: 3.50 ms
  • 1000 items and 200 modifications: 27.07 ms, median: 26.92 ms
  • 1000 items and 200 modifications without moves: 13.54 ms, median: 13.36 ms

tests are run on Nexus 5X with M

@a_tolstykh

37 of 46

DiffUtil

May take significant time for large dataset...

Use it on a background thread!

@a_tolstykh

38 of 46

DiffUtil. Summary

Avoid unnecessary UI updates!

Animated values change!

@a_tolstykh

39 of 46

TextViewRichDrawable

Android TextView with rich support �of compound drawables.

This is a tiny library which empowers �TextView's compound drawables with:

  • size specifying
  • vector support
  • tinting

https://github.com/a-tolstykh/textview-rich-drawable

@a_tolstykh

40 of 46

TextViewRichDrawable

  • Optimized layout
  • More readable code
  • Better User Experience

@a_tolstykh

https://github.com/a-tolstykh/textview-rich-drawable

41 of 46

<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.TextViewRichDrawableandroid: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.TextViewRichDrawableandroid: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

42 of 46

Simplify your layout

4 Views

2 Views

@a_tolstykh

https://github.com/a-tolstykh/textview-rich-drawable

Before

After

43 of 46

RecyclerView Performance Tuning

Prefetch. Cache. Reuse!

@a_tolstykh

44 of 46

RecyclerView Performance Tuning

Do not over engineer. Keep it simple!

@a_tolstykh

45 of 46

RecyclerView Performance Tuning

Avoid premature optimizations.

Think about optimizations when you need them.

@a_tolstykh

46 of 46

Thanks

@a_tolstykh

@a_tolstykh

+OleksandrTolstykh