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 を更新する


Dependency Injection 一歩目

3173827605_427626c6af-1

話題でした.

「なぜDI(依存性注入)が必要なのか?」についてGoogleが解説しているページを翻訳した  - Qiita

これを幼稚に理解しておこうかとな.

 

「Dependency Injection」という言葉

「依存性注入」てのがしっくりこないので少し調べてみる.

「オブジェクトを注入する」と言うと日本語としても意味がはっきりしていますね。

「Dependency」は「依存性」ではなく「使われる側のオブジェクト」(依存オブジェクト)という意味になります。

やはりあなた方のDependency Injectionはまちがっている。 — A Day in Serenity

「外部オブジェクトの注入」と覚えたら直感的にわかりやすいですよね.

 

修正前/後のコードを比べてみる

修正前.

メソッド内で外部オブジェクトを生成している.


public class RealBillingService implements BillingService { 

  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    CreditCardProcessor processor = new PaypalCreditCardProcessor();
    TransactionLog transactionLog = new DatabaseTransactionLog();
    // ...
  }

}

修正後.

外部オブジェクトを外部で生成してコンストラクタから注入している.


public class RealBillingService implements BillingService {

  private final CreditCardProcessor processor;
  private final TransactionLog transactionLog;

  public RealBillingService(CreditCardProcessor processor,TransactionLog transactionLog) {
    this.processor = processor;
    this.transactionLog = transactionLog;
  }

  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
     // ...
  }
}

これをテスト時には, メンバ初期化で生成したオブジェクトを各メソッド内でコンストラクタから注入して利用する.


public class RealBillingServiceTest extends TestCase {

  private final PizzaOrder order = new PizzaOrder(100);
  private final CreditCard creditCard = new CreditCard("1234", 11, 2010);

  private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog();
  private final FakeCreditCardProcessor processor = new FakeCreditCardProcessor();

  public void testSuccessfulCharge() {
    RealBillingService billingService
        = new RealBillingService(processor, transactionLog);
    Receipt receipt = billingService.chargeOrder(order, creditCard);

    // assert etc...
  }

}

 

例) データ抽出先のサーバを切り替える

Repository クラスに, RemoteDataSource オブジェクトを注入しておいてテスト時には切り替えます.

それぞれのインスタンスは Singleton によって一意であることを保証しています.

公開用コード.
android-architecture/Injection.java at todo-mvp · googlesamples/android-architecture


public class Injection {

    public static TasksRepository provideTasksRepository(@NonNull Context context) {
        checkNotNull(context);
        return TasksRepository.getInstance(TasksRemoteDataSource.getInstance(),
                TasksLocalDataSource.getInstance(context));
    }
}

テスト用コード.
android-architecture/Injection.java at todo-mvp · googlesamples/android-architecture


public class Injection {

    public static TasksRepository provideTasksRepository(@NonNull Context context) {
        checkNotNull(context);
        return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
                TasksLocalDataSource.getInstance(context));
    }
}

外部オブジェクトの変更が容易になっていることがよくわかります.

注入の方法は他にもあるようですがまあいいか.

 

まとめ

メソッド外で生成してコンストラクタから注入する.
(メソッド内で生成しない)


Related Categories :  AndroidDevelopmemt


【急げ】オライリー「Gradle Recipes for Android」が無料ダウンロード配布中!!

あのオライリーから発売されているこの本ですが.

Amazon_co_jp:_Gradle_Recipes_for_Android__Master_the_New_Build_System_for_Android__Ken_Kousen__洋書

Chapter 1 Gradle for Android Basics
1.1. Gradle Build Files in Android
1.2. Configure SDK Versions and Other Defaults
1.3. Executing Gradle Builds from the Command Line
1.4. Executing Gradle Builds from Android Studio
1.5. Adding Java Library Dependencies
1.6. Adding Library Dependencies Using Android Studio
1.7. Configuring Repositories

Chapter 2 From Project Import to Release
2.1. Setting Project Properties
2.2. Porting Apps from Eclipse ADT to Android Studio
2.3. Porting Apps from Eclipse ADT Using Eclipse
2.4. Upgrading to a Newer Version of Gradle
2.5. Sharing Settings Among Projects
2.6. Signing a Release APK
2.7. Signing a Release APK Using Android Studio

Chapter 3 Build Types and Flavors
3.1. Working with Build Types
3.2. Product Flavors and Variants
3.3. Merging Resources
3.4. Flavor Dimensions
3.5. Merging Java Sources Across Flavors

Chapter 4 Custom Tasks
4.1. Writing Your Own Custom Tasks
4.2. Adding Custom Tasks to the Build Process
4.3. Excluding Tasks
4.4. Custom Source Sets
4.5. Using Android Libraries

Chapter 5 Testing
5.1. Unit Testing
5.2. Testing with the Android Testing Support Library
5.3. Functional Testing with Robotium
5.4. Activity Testing with Espresso

Chapter 6 Performance and Documentation
6.1. Performance Recommendations
6.2. DSL Documentation
Appendix Just Enough Groovy to Get By
Appendix Gradle Basics



無料版PDFが本家Gradleのサイトからダウンロードできるようになっています!

New_Gradle_Android_Ebook_for_Free_-_Gradle

New Gradle Android Ebook for Free - Gradle

メールアドレスなど入力後, 送信されてくるメール内のリンクからダウンロードできます.

Free_ebook_from_O_Reilly__Learn_Gradle_for_Beginners

図表入りで, 初心者にもわかりやすい内容になっています.

Gradle_Recipes_for_Android

Gradle_Recipes_for_Android 2

早めにダウンロードしたほうがいいような気がします!