Slide 1

Slide 1 text

RecylerView Performance Tuning Oleksandr Tolstykh @a_tolstykh Head of Mobile Development, Android engineer

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Sample App - Travel With Us City guides Base city info Data is loaded from server @a_tolstykh RecyclerView!

Slide 6

Slide 6 text

Optimize Cells hierarchies Avoid deep hierarchy Save level with when Layout matters! @a_tolstykh https://developer.android.com/topic/performance/rendering/optimizing-view-hierarchies.html android.support.constraint.ConstraintLayout com.google.android.flexbox.FlexboxLayout

Slide 7

Slide 7 text

Minimize onBindViewHolder() Make onBindViewHolder() as cheap as possible Avoid item instantiations (memory allocations) Do as much as you can in onCreateViewHolder() @a_tolstykh

Slide 8

Slide 8 text

Correct images scale Make sure their size and compression are optimal. Scaling images may also affect the performance. Do not reinvent the wheel. @a_tolstykh Picasso, ImageLoader, Fresco, Glide. Picasso v/s Imageloader v/s Fresco vs Glide [closed] http://stackoverflow.com/q/29363321/2308720

Slide 9

Slide 9 text

Nested RecyclerView Override the LinearLayoutManager#getInitialPrefetchItemCount() @a_tolstykh public int getInitialPrefetchItemCount () Defines how many inner items should be prefetched when this LayoutManager's RecyclerView is nested inside another RecyclerView Binding the inner RecyclerView doesn’t allocate any children. The prefetching system has to know how many.

Slide 10

Slide 10 text

Update only the affected items adapter.notifyItemRemoved(position); adapter.notifyItemChanged(position); adapter.notifyItemInserted(position); * only if you know position of updated item @a_tolstykh

Slide 11

Slide 11 text

Profit! Animations for free! Optimizations for free! @a_tolstykh

Slide 12

Slide 12 text

Remove an item with swipe: SimpleCallback touchCallback = new SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { // ... @Override public void onSwiped(ViewHolder viewHolder, int swipeDir) { adapter.remove(viewHolder.getAdapterPosition()); adapter.notifyItemRemoved(viewHolder.getAdapterPosition()); } }; @a_tolstykh

Slide 13

Slide 13 text

Items cache recyclerView.setItemViewCacheSize (int size); Documentation: “Set the number of offscreen views to retain before adding them to the potentially shared recycled view pool.” @a_tolstykh static final int DEFAULT_CACHE_SIZE = 2; // supportVersion = 25.3.0

Slide 14

Slide 14 text

Stable ids - adapter.setHasStableIds() adapter.setHasStableIds(true); // YourAdapter.java @Override public long getItemId(int position) { return items.get(position).hashcode(); //id() } @a_tolstykh Only if item in the data set can be represented with a unique identifier Avoid unnecessary bindViewHolder() calls if view exists in RecyclerView’s cache

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Advanced prefetch Override the LinearLayoutManager#getExtraLayoutSpace(RecyclerView.State s) @a_tolstykh protected int getExtraLayoutSpace (RecyclerView.State state) Returns the amount of extra space that should be laid out by LayoutManager. By default, LinearLayoutManager lays out 1 extra page of items while smooth scrolling and 0 otherwise. You can override this method to implement your custom layout pre-cache logic.

Slide 17

Slide 17 text

Advanced prefetch @a_tolstykh public class PreCachingLayoutManager extends LinearLayoutManager { // .... public void setExtraLayoutSpace(int extraLayoutSpace) { this.extraLayoutSpace = extraLayoutSpace; } @Override protected int getExtraLayoutSpace(RecyclerView.State state) { return extraLayoutSpace > 0 ? extraLayoutSpace : DEFAULT_EXTRA_SPACE; } }

Slide 18

Slide 18 text

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). @a_tolstykh BUT it improves USER EXPERIENCE! Need to find balance. Maybe 1 extra screen is The Balance?

Slide 19

Slide 19 text

Even more advanced prefetch... Prefetch images! (on wi-fi only) @a_tolstykh Picasso.with(context).load(url).fetch(); Glide.with(context).load(url).downloadOnly(width, height);

Slide 20

Slide 20 text

@a_tolstykh Without images prefetch With images prefetch Images prefetch

Slide 21

Slide 21 text

@a_tolstykh public class CitiesAdapter extends RecyclerView.Adapter { public void updateData(List cities) { if (onWifi()) prefetch(cities); // TODO update adapter data } private void prefetch(List cities) { for (City city : cities) { PreFetcher prefetcher = city.getPrefetcher(); if (prefetcher != null) { prefetcher.prefetch(mContext); } } } }

Slide 22

Slide 22 text

@a_tolstykh public class ImagePrefetcher implements PreFetcher { private final Image[] mImages; public ImagePrefetcher(Image... images) { this.mImages = images; } @Override public void prefetch(Context context) { for (Image image : mImages) { Picasso.with(context).load(image.url()).fetch(); } } }

Slide 23

Slide 23 text

@a_tolstykh public class TweetPrefetcher implements PreFetcher { private final List mIds; @Override public void prefetch(Context context) { TwitterHelper.loadTweets(mIds, new Callback>() { @Override public void success(Result> result) { for (Tweet tweet : result.data) { TwitterHelper.preFetchTweetImage(tweet); } } }); } }

Slide 24

Slide 24 text

Displayed data pre-calculations Displayed items are POJOs. Pre-format date, time, other strings during data parsing. Pre-calculate immutable values during data parsing. @a_tolstykh Profit: + Minor performance optimisations. + Formatting is done in BG thread. + Formatting is done in single place.

Slide 25

Slide 25 text

Displayed data pre-calculations (formatting) @WorkerThread private static Rating createRating(float value, int count) { String ratingFormatted = String.format(Locale.US, "%.1f", value); String countFormatted = formatWithMetricPrefix(count); return Rating.create(value, ratingFormatted, count, countFormatted); } @UiThread public void setRating(Rating rating) { ratingText.setText(rating.ratingFormatted()); reviewsCount.setText(rating.numberOfReviewsFormatted()); } @a_tolstykh

Slide 26

Slide 26 text

DiffUtil @a_tolstykh *since support library version 24.2.0 https://developer.android.com/reference/android/support/v7/util/DiffUtil.html

Slide 27

Slide 27 text

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. @a_tolstykh It can be used to calculate updates for a RecyclerView Adapter.” DiffUtil.Callback cb = new YourDiffCallback(oldList, newList); DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(cb); diffResult.dispatchUpdatesTo(adapter);

Slide 28

Slide 28 text

DiffUtil.Callback public class DiffCallback extends DiffUtil.Callback { public DiffCallback(@NonNull List newList, @NonNull List oldList) {...} public int getOldListSize() {...} public int getNewListSize() {...} public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {...} public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {...} @Nullable public Object getChangePayload(int oldItemPosition, int newItemPosition) {...} } @a_tolstykh

Slide 29

Slide 29 text

DiffUtil.Callback public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) When areItemsTheSame(int, int) returns true for two items and areContentsTheSame(int, int) returns false for them, DiffUtil calls this method to get a payload about the change. public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) Checks whether two items have the same data.You can change its behavior depending on your UI. This method is called by DiffUtil only if areItemsTheSame returns true. @a_tolstykh

Slide 30

Slide 30 text

DiffUtil.Callback @Override public int getOldListSize() { return oldCities.size(); } @Override public int getNewListSize() { return newCities.size(); } @Override public boolean areItemsTheSame(int oldPosition, int newPosition) { return oldCities.get(oldPosition).id() == newCities.get(newPosition).id(); } @Override public boolean areContentsTheSame(int oldPosition, int newPosition) { return oldCities.get(oldPosition).equals(newCities.get(newPosition)); } @a_tolstykh

Slide 31

Slide 31 text

@a_tolstykh Without DiffUtil With DiffUtil Lenovo Vibe X Android OS, v4.2 1080 x 1920 pixels Quad-core 1.5 GHz

Slide 32

Slide 32 text

DiffUtil.Callback public Object getChangePayload(int oldItemPosition, int newItemPosition) When areItemsTheSame(int, int) returns true for two items and areContentsTheSame(int, int) returns false for them, DiffUtil calls this method to get a payload about the change. @a_tolstykh For example, if you are using DiffUtil with RecyclerView, you can return the particular field that changed in the item and your ItemAnimator can use that information to run the correct animation.

Slide 33

Slide 33 text

DiffUtil animated values change public Object getChangePayload(int oldItemPosition, int newItemPosition) { Bundle diff = new Bundle(); Rating newRating = newCities.get(newItemPosition).rating(); Rating oldRating = oldCities.get(oldItemPosition).rating(); if (newRating.numberOfReviews() != oldRating.numberOfReviews()) { diff.putString(KEY_NUMBER_OF_REVIEWS, newRating.numberOfReviewsFormatted()); } if (Float.compare(newRating.rating(), oldRating.rating()) != 0) { diff.putString(KEY_RATING_FORMATTED, newRating.ratingFormatted()); } return diff.size() == 0 ? null : diff; } @a_tolstykh

Slide 34

Slide 34 text

public void onBindViewHolder(CityViewHolder holder, int index, List p) { if (p.isEmpty()) { onBindViewHolder(holder, index); return; } Bundle payload = (Bundle) p.get(0); for (String key : payload.keySet()) { if (key.equals(CitiesDiffCallback.KEY_NUMBER_OF_REVIEWS)) { holder.animateReviews(payload.getString(key)); } else if (key.equals(CitiesDiffCallback.KEY_RATING_FORMATTED)) { holder.animateRating(payload.getString(key)); } } } @a_tolstykh

Slide 35

Slide 35 text

Use any animation you want! ● android.widget.TextSwitcher ● com.hanks.htextview.HTextView ● custom... @a_tolstykh DiffUtil animated values change

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

DiffUtil May take significant time for large dataset... @a_tolstykh Use it on a background thread!

Slide 38

Slide 38 text

DiffUtil. Summary Avoid unnecessary UI updates! @a_tolstykh Animated values change!

Slide 39

Slide 39 text

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 @a_tolstykh https://github.com/a-tolstykh/textview-rich-drawable TextViewRichDrawable

Slide 40

Slide 40 text

@a_tolstykh Before After https://github.com/a-tolstykh/textview-rich-drawable

Slide 41

Slide 41 text

Simplify your layout @a_tolstykh Before After https://github.com/a-tolstykh/textview-rich-drawable 4 Views 2 Views

Slide 42

Slide 42 text

RecyclerView Performance Tuning Prefetch. Cache. Reuse! @a_tolstykh

Slide 43

Slide 43 text

RecyclerView Performance Tuning Do not over engineer. Keep it simple! @a_tolstykh

Slide 44

Slide 44 text

RecyclerView Performance Tuning Avoid premature optimizations. Think about optimizations when you need them. @a_tolstykh

Slide 45

Slide 45 text

Thanks @a_tolstykh @a_tolstykh @a_tolstykh +OleksandrTolstykh