この記事は Developer Advocate、 Doug Stevenson による The Firebase Blog の記事 "Using Android Architecture Components with Firebase Realtime Database (Part 2) " を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
本ブログシリーズのパート 2 にようこそ。ここでは、ライフサイクル対応の Android Architecture Components(LiveData および ViewModel )と Firebase Realtime Database を組み合わせて、安定性が高く、テストしやすいアプリを実装する方法を紹介しています。最初のパート では、LiveData と ViewModel を使って Activity をリファクタリングし、Realtime Database に接続する細かい実装のほとんどを削除しました。これで Activity のコードはシンプルになりましたが、まだ手をつけていない部分もあります。それは、Activity が株価を含む DataSnapshot にアクセスする箇所です。今回は、可読性とテストのしやすさを向上させるため、Activity からすべての Realtime Database SDK の痕跡を削除したいと思います。そして最終的には、アプリで Realtime Database ではなく Firestore を使う場合でも、Activity のコードは一切変えなくてもよいようにします。
データベース内のデータは、次のようになっています。
また、DataSnapshot から読み取り、いくつかの TextView にコピーするコードは次のようになっています。
// update the UI with values from the snapshot
String ticker = dataSnapshot.child("ticker").getValue(String.class);
tvTicker.setText(ticker);
Float price = dataSnapshot.child("price").getValue(Float.class);
tvPrice.setText(String.format(Locale.getDefault(), "%.2f", price));
Realtime Database SDK を使うと、実に簡単に DataSnapshot を JavaBean スタイルのオブジェクトに変換できます。最初に行う必要があるのは、スナップショットのフィールド名に一致する getter と setter を持つ Bean クラスを定義することです。
public class HotStock {
private String ticker;
private float price;
public String getTicker() {
return ticker;
}
public void setTicker(String ticker) {
this.ticker = ticker;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
public String toString() {
return "{HotStock ticker=" + ticker + " price=" + price + "}";
}
}
次に、SDK を呼び出して、自動マッピングを行います。
HotStock stock = dataSnapshot.getValue(HotStock.class)
この行が実行されると、ticker と price の値を含む HotStock の新しいインスタンスが作成されます。この便利な行を使うと、HotStockViewModel の実装を更新してこの変換 を行うことができます。この変換によって、LiveData オブジェクトを作成できます。これは、入力される DataSnapshot を自動的に HotStock に変換します。この変換は、Function オブジェクトの中で行います。具体的には、ViewModel 内で次のようにして構築することができます。
// This is a LiveData<DataSnapshot> from part 1
private final FirebaseQueryLiveData liveData = new FirebaseQueryLiveData(HOT_STOCK_REF);
private final LiveData<HotStock> hotStockLiveData =
Transformations.map(liveData, new Deserializer());
private class Deserializer implements Function<DataSnapshot, HotStock> {
@Override
public HotStock apply(DataSnapshot dataSnapshot) {
return dataSnapshot.getValue(HotStock.class);
}
}
@NonNull
public LiveData<HotStock> getHotStockLiveData() {
return hotStockLiveData;
}
ユーティリティ クラス Transformations には、静的メソッド map() があります。ソースとなる LiveData オブジェクトと Function の実装をこのメソッドに渡すと、新しい LiveData オブジェクトが返されます。この新しい LiveData は、ソースから取得したすべてのオブジェクトに Function を適用し、その Function の出力を返します。Deserializer 関数は、入力型 DataSnapshot と出力型 HotStock というパラメータを持っています。これは、DataSnapshot をデシリアライズして HotStock を生成するという単純な仕事をしています。最後に、LiveData に変換されたこの新しい HotStock オブジェクトを返す getter を追加します。
これを追加することによって、アプリケーションのコードは DataSnapshot オブジェクトと HotStock オブジェクトのどちらの更新を受け取ることも選択できるようになります。ベスト プラクティスとしては、ViewModel オブジェクトは、UI コンポーネントがデータを処理しなくてもいいように、そのままデータを表示できる状態のオブジェクトを返すべきです。つまり、UI レイヤーで必要となる前処理は、すべて HotStockViewModel が行わなくてはなりません。これは当然、今回のケースにも当てはまります。HotStock は、いつでも UI を設定する Activity で利用可能な状態になっているからです。この時点で、Activity 全体は次のようになります。
public class MainActivity extends AppCompatActivity {
private TextView tvTicker;
private TextView tvPrice;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvTicker = findViewById(R.id.ticker);
tvPrice = findViewById(R.id.price);
HotStockViewModel hotStockViewModel =
ViewModelProviders.of(this).get(HotStockViewModel.class);
LiveData<HotStock> hotStockLiveData =
hotStockViewModel.getHotStockLiveData();
hotStockLiveData.observe(this, new Observer() {
@Override
public void onChanged(@Nullable HotStock hotStock) {
if (hotStock != null) {
// update the UI here with values in the snapshot
tvTicker.setText(hotStock.getTicker());
tvPrice.setText(String.format(Locale.getDefault(), "%.2f",
hotStock.getPrice()));
}
});
}
}
Realtime Database オブジェクトへの参照はすべてなくなり、HotStockViewModel と LiveData で抽象化されています!しかし、まだ問題になる可能性があることが 1 つ残されています。
LiveData への変換の負荷が大きい場合
LiveData の onChanged() に対するコールバックは、すべてメインスレッドで実行されます。同じように、変換もメインスレッドで行われます。ここで示した例は小さく単純なものなので、パフォーマンスが問題になることはないでしょう。しかし、Realtime Database SDK が DataSnapshot を JavaBean タイプのオブジェクトにデシリアライズする際には、リフレクションを使って Bean に値を設定する setter メソッドを動的に検索して呼び出しています。オブジェクトの量やサイズが大きくなると、この処理に大量の計算が必要になります。変換に必要な合計時間が 16ms(メインスレッド上の作業単位に対して使える時間)を超える場合、Android でフレーム落ちが発生するようになります。フレーム落ちが発生すると、なめらかな 60fps でレンダリングできず、UI の動作はぎこちなくなります。これが「ジャンク 」と呼ばれる現象です。ジャンクが起きると、質の悪いアプリに見えます。さらに悪いことに、データの変換に何らかの I/O が必要となる場合、アプリが無応答になって ANR が発生する場合もあります。
変換の負荷が大きくなることが心配な方は、計算処理を別のスレッドに移すべきでしょう。これは、変換処理の中で行うことはできませんが(同期的に実行されるため)、MediatorLiveData と呼ばれる仕組みを使うことができます。MediatorLiveData は、map 変換上に構築されている仕組みで、別の LiveData ソースの変更を監視して、それぞれのイベントにどのように反応するかを指定することができます。では、既存の変換を置き換えて、本シリーズのパート 1 で作成した HotStockViewModel の引数のないコンストラクタで初期化するようにしてみましょう。
private final FirebaseQueryLiveData liveData = new FirebaseQueryLiveData(HOT_STOCK_REF);
private final MediatorLiveData<HotStock> hotStockLiveData = new MediatorLiveData<>();
public HotStockViewModel() {
// Set up the MediatorLiveData to convert DataSnapshot objects into HotStock objects
hotStockLiveData.addSource(liveData, new Observer<DataSnapshot>() {
@Override
public void onChanged(@Nullable final DataSnapshot dataSnapshot) {
if (dataSnapshot != null) {
new Thread(new Runnable() {
@Override
public void run() {
hotStockLiveData.postValue(dataSnapshot.getValue(HotStock.class));
}
}).start();
} else {
hotStockLiveData.setValue(null);
}
}
});
}
ここでは、ソース LiveData オブジェクトとソースが変更された際に呼び出される Observer を使い、MediatorLiveData インスタンスの addSource() を呼び出しています。onChanged() の内部では、デシリアライズ作業を新しいスレッドにオフロードしています。このスレッド化した作業では、postValue() を使って MediatorLiveData オブジェクトを更新 していますが、スレッド化していない作業(dataSnapshot が null の場合)は、setValue() を使っています。この違いは重要です。postValue() による更新はスレッドセーフですが、setValue() はメインスレッド上でのみ呼び出すことができます。
注: 実際のアプリでは、このようにして新しいスレッドを起動することはおすすめしません。 これは、動作をスレッド化する「ベスト プラクティス」を示す例ではありません。このようなジョブでは、再利用可能なスレッドのプールとともに Executor を使う方が適しています(例 )。
まだ改善の余地はある
以上で、Activity から Realtime Database オブジェクトを削除することができ、DataSnapshot から HotStock への変換を行う際のパフォーマンスにも対処できました。しかし、パフォーマンスを改善するためにまだできることがあります。Activity に構成変更(端末の回転など)が発生すると、onInactive() が実行されて FirebaseQueryLiveData オブジェクトがデータベース リスナーを削除し、その後 onActive() で再追加されることになります。これはあまり問題にはならないと思うかもしれませんが、/hotstock 以下の全データが(不必要に)往復する点を意識しておくことが重要です。回転が起こったときは、リスナーを追加した状態のままにしておき、ユーザーのデータプランを節約する方が望ましいでしょう。そこで、本シリーズの次のパートでは、これを実現する方法について説明します。
またお会いできることを楽しみにしています。忘れずに Twitter で @Firebase をフォローし、本シリーズの最新情報をチェックしてください!パート 3 は、こちら からご覧いただけます。
Reviewed by Khanh LeViet - Developer Relations Team