DiffUtil で簡単に RecyclerView を更新する

list_mail

例えば, RecyclerView で一覧を作成して表示しているとして


1. 田中一郎
2. 佐藤二郎
3. 鈴木三郎
....

サーバなどに問い合わせて取得された以下データ


1. 田中一郎
2. ジローラモ
3. 鈴木三郎
...

を一覧画面に反映させたい場合.

RecyclerView.Adapter に更新を通知する場合,

RecyclerView.Adapter | Android Developers

notify*() のうちアイテムの更新に関するものは


notifyDataSetChanged()
notifyItemChanged(int position, Object payload)
notifyItemChanged(int position)
notifyItemRangeChanged(int positionStart, int itemCount, Object payload)
notifyItemRangeChanged(int positionStart, int itemCount)

ですが, それぞれ以下の面倒な部分がありました.


notifyDataSetChanged()

→ 変更部分のみに関してのアニメーションが表示されない.


notifyItemChanged(int position, Object payload)
notifyItemChanged(int position)
notifyItemRangeChanged(int positionStart, int itemCount, Object payload)
notifyItemRangeChanged(int positionStart, int itemCount)

→ アニメーションは表示されるが, 変更部分の position を計算して渡す必要がある.

その他 hasStableIds() と Loader を使ったりなど面倒でしたよね.

DiffUtil を使う

support-library-v7 24.2.0 で登場の DiffUtil を使いましょう.

DiffUtil | Android Developers

コールバッククラス作成後, それを使った更新メソッドを Adapter 内に記述します.

MyDiffUtilCallback.java


public class MyDiffUtilCallback extends DiffUtil.Callback{

    List<Person> oldPersons;
    List<Person> newPersons;

    public MyDiffUtilCallback(List<Person> oldPersons, List<Person> newPersons) {
        this.oldPersons = oldPersons;
        this.newPersons = newPersons;
    }

    @Override
    public int getOldListSize() {
        return oldPersons.size();
    }

    @Override
    public int getNewListSize() {
        return newPersons.size();
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return oldPersons.get(oldItemPosition).id == newPersons.get(newItemPosition).id;
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        return oldPersons.get(oldItemPosition).equals(newPersons.get(newItemPosition));
    }

    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
	// some additional information
        return super.getChangePayload(oldItemPosition, newItemPosition);
    }
}

MyRecyclerViewAdapter.java


public void updateList(ArrayList<Person> newList) {
        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffUtilCallback(this.persons, newList));
        diffResult.dispatchUpdatesTo(this);
}

これで, アイテムのポジションの計算はおまかせにして, 同時に綺麗なアニメーションが変更部分に適用されます.

実際, データ量が多い場合は厳しいようなので, RxJava なら メインスレッドにて以下.


@Override
public void onNext(DiffUtil.DiffResult result) {
    result.dispatchUpdatesTo(mProductAdapter);
}

kotlinなら同様に以下。


internal class DiffUtilCallback(
    private val oldItems: List<Item>,
    private val newItems: List<Item>
) : DiffUtil.Callback() {

  override fun getOldListSize() = oldItems.size

  override fun getNewListSize() = newItems.size

  override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int)
      = oldItems[oldItemPosition].id == newItems[newItemPosition].id

  override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int)
      = oldItems[oldItemPosition] == newItems[newItemPosition]
}

以下のように使うと便利かもしれません.

リアクティブにデータ更新検知して自動画面更新させる


75%速度向上という Gradle 3.0 を Android Studio で試す

更新されてます, Gradle が 3.0 に.

Gradle_3_0_released_-_News_-_Gradle_Forums

Gradle 3.0 released - News - Gradle Forums

Gradle_Distributions

Gradle Distributions

The Gradle Daemon is a key performance enhancer, making builds up to 75% faster, but it needed to be explicitly enabled in previous versions of Gradle. This is no longer necessary as the Daemon is now enabled by default in 3.0.

Daemon がデフォルトで起動され, 75% の速度向上らしいので.

 

Android Studio で使ってみよう

プロジェクトルートの位置で.

./gradlew wrapper --gradle-version=3.0

これで, Gradle 側はラッパーインストールできますが, Android Plugin が 2.1.x系では対応できていないようです.

最新のbetaを入れてみましょう.

com_android_tools_build_gradle

com.android.tools.build.gradle

(project-root)/build.gradle


buildscript {
  //System.properties['com.android.build.gradle.overrideVersionCheck'] = 'true'
  repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.0-beta1'
    }
}

Total time: 6.543 secs

Total time: 1.472 secs

小さいプロジェクトでも, 2回目以降のビルドで, 高速になったことが体感できます.

まあ, beta ということで少し待っていたほうがいいのかも知れませんが.

Latest Bugs topics - Gradle Forums

Android お手軽なビルド時間の短縮メモ


リアクティブにデータ更新検知して自動画面更新させる

Square 生まれのライブラリを使います.

SQLBrite

square/sqlbrite: A lightweight wrapper around SQLiteOpenHelper which introduces reactive stream semantics to SQL operations.

SQLiteOpenHelper をラップするように使い, ContentProvider(ContentResolver) のように更新を通知する役割を担当します.

SQLBrite__A_Reactive_Database_Foundation

SQLBrite: A Reactive Database Foundation

RxJava と連携させて使いましょう.


SqlBrite sqlBrite = SqlBrite.create();
BriteDatabase db = sqlBrite.wrapDatabaseHelper(openHelper, Schedulers.io());

// 対象テーブル名を含むSQLクエリーを登録
Observable<Query> users = db.createQuery("users", "SELECT * FROM users");
final AtomicInteger queries = new AtomicInteger();

// データ更新を監視して受け取る
users.subscribe(new Action1<Query>() {
  @Override public void call(Query query) {
    queries.getAndIncrement();
  }
});


System.out.println("Queries: " + queries.get()); // Prints 1

db.insert("users", createUser("jw", "Jake Wharton"));
db.insert("users", createUser("mattp", "Matt Precious"));
db.insert("users", createUser("strong", "Alec Strong"));

System.out.println("Queries: " + queries.get()); // Prints 4

このしくみを利用すると,

CursorLoader + ContentProvider

の部分を

RxJava + SQLBrite

で置き換えてリアクティブに記述できます.

MVP実装の例

googlesamples/android-architecture at dev-todo-mvp-rxjava

Observable を DataSource で作成して,

TasksLocalDataSource.java:77


    @Override
    public Observable<List<Task>> getTasks() {
        String[] projection = {
                TaskEntry.COLUMN_NAME_ENTRY_ID,
                TaskEntry.COLUMN_NAME_TITLE,
                TaskEntry.COLUMN_NAME_DESCRIPTION,
                TaskEntry.COLUMN_NAME_COMPLETED
        };
        String sql = String.format("SELECT %s FROM %s", TextUtils.join(",", projection), TaskEntry.TABLE_NAME);
        return mDatabaseHelper.createQuery(TaskEntry.TABLE_NAME, sql)
                .mapToList(mTaskMapperFunction);
    }

Repository 経由させて,

TaskRepository:96


    @Override
    public Observable<List<Task>> getTasks() {
        // ...
        if (mCacheIsDirty) {
            return remoteTasks;
        } else {

            // Query the local storage if available. If not, query the network.
            Observable<List<Task>> localTasks = mTasksLocalDataSource.getTasks();
            return Observable.concat(localTasks, remoteTasks).first();

        }
    }

Presenter 上で subscribe しておけば,

TaskPresenter:104


        Subscription subscription = mTasksRepository
                .getTasks()
                .flatMap(new Func1<List<Task>, Observable<Task>>() {
                    @Override
                    public Observable<Task> call(List<Task> tasks) {
                        return Observable.from(tasks);
                    }
                })
                .filter(new Func1<Task, Boolean>() {
		  // ...
                })
                .toList()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<List<Task>>() {
                    @Override
                    public void onCompleted() {
                        mTasksView.setLoadingIndicator(false);
                    }

                    @Override
                    public void onError(Throwable e) {
                        mTasksView.showLoadingTasksError();
                    }

                    @Override
                    public void onNext(List<Task> tasks) {
                        processTasks(tasks);
                    }
                });

データベースで対象テーブルに更新されるとすぐに自動的に再読み込みが実行されて画面が更新されます.

MVP に CursorLoader を利用すると, Presenter で受け取るコールバックが煩雑になりやすいのでこれを使うと便利です.

DiffUtil で簡単に RecyclerView を更新する