2016年10月25日火曜日

Fragment に Toolbar を持たせるのはやめなさい

NavigationDrawer や BottomNavigation パターンを実現するために、各画面を Fragment で実装することがあります。 Fragment によって ActionBar に持たせる機能が違うからか、Fragment のレイアウトに Toolbar を持たせて、Fragment で ((AppCompatActivity) getActivity()).setSupportActionBar(toolbar); のような処理をさせているコードを見かけることがあります。

やめなさい

(Toolbar をただの View として使って、ActionBar としては使わない(setSupportActionBar()しない)というのであればまだ許容できるが、それならそもそも Toolbar を使う必要がない)

ViewPager のように複数の Fragment を一度に attach する場合、これでは予期しない動作になることがありえます。ちゃんと Fragment に用意されている機能を使ってください。 Fragment で setHasOptionsMenu(true) を呼ぶと onCreateOptionsMenu() が呼ばれるので、Fragment 用の Menu を inflate します。

ViewPager はこの機能を適切に処理しており、現在のページの Fragment の Menu だけ inflate されるようになっています。 また、FragmentTransaction の show() / hide() で Fragment の表示・非表示を切り替える際も OptionsMenu であれば一緒に適切に処理されます。


Fragment にこのような機能があることを知っているにもかかわらず、上記のようなひどいコードを実装してしまう要因として、Menu ではなく View を置きたいという状況があります。
よくあるのが ActionBar に検索用の入力フィールド(EditText)を持たせたい場合です。
Menu ではなく View を置きたいのだから OptionsMenu の機能は使えないと思ってしまうのでしょうか。

OptionsMenu にはこのような用途のために ActionView という機能があります。MenuItem に独自のレイアウト/Viewを設定できる機能です。 Menu リソースの item で android:actionLayout(app:actionLayout)を使ってレイアウトを指定することもできます。
また、android:actionViewClass(app:actionViewClass) で View クラスを指定することもできます。

この ActionView 用に用意されているクラスとして SearchView があります。 SearchView は ActionBar に検索用の入力フィールド(EditText)を持たせてくれるそのものずばりの機能です。文字が入力されているときにクリアボタン(xボタン)が出る機能も実装されています。 これを利用せずにわざわざ自分で実装する意味はあまりないと思いますが、独自でやりたいのであればそれ用のViewクラスを自分で用意して android:actionViewClass で指定すればよいのです。

まとめると、
  • Fragment のレイアウトに toolbar を持たせない
  • Fragment 独自の機能を ActionBar に入れたいときは OptionsMenu の機能を使う
  • OptionsMenu の機能なら ViewPager で適切に処理される
  • OptionsMenu の機能なら FragmentTransaction の show() / hide() で適切に処理される
  • OptionsMenu には独自の View を配置できる ActionView 機能がある


追記1

Toolbar の中に複雑な View を入れること自体をダメだと言っているわけではありません。 Activityのレイアウトで <android.support.v7.widget.Toolbar> に子ビュー持たせるのは別にいいと思います。 このエントリはあくまで Fragment に toolbar を持たせることについての話です。

2016年10月21日金曜日

BehaviorSubject を使って Activity と Fragment のデータの読み込みを待ち合わせる

画面構成は
  • MainActivity
    • TabLayout + ViewPager
    • ViewPagerの各ページは MainFragment
  • MainFragment
    • RecyclerView

やりたいことは
  • MainActivity
    • 各ページで共通のデータ(以後 CommonData)をサーバーから取得する
  • MainFragment
    • ページ特有のデータ(以後 SpecificData)をサーバーから取得する
    • MainActivity から CommonData をもらう
    • CommonData と SpecificData 両方の読み込みが終わったら RecyclerView にデータを追加する

キモになるのが、CommonData と SpecificData 両方の読み込みが終わるのを待ち合わせたいというところです。
CommonData の読み込みが終わる前に生成された MainFragment なら両方の読み込みを待ち合わせるし、 CommonData の読み込みが終わった後に生成された MainFragment なら SpecificData の読み込みだけ待てばいいわけです。
でも、この2つの状態をわけて処理すると煩雑になってしまいます。

そこで、BehaviorSubject を使って、CommonData の値を MainFragment 側に渡せるようにします。 BehaviorSubject は onNext() が呼ばれたときにその値を通知し、さらにその値をキャッシュします。新しく subscribe されると、最新のキャッシュした値があればその時点で通知します。

public class MainActivity extends AppCompatActivity implements MainFragment.MainFragmentListener { private final BehaviorSubject<CommonData> commonDataBehaviorSubject = BehaviorSubject.create(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... loadCommonData(); } private void loadCommonData() { subscription = DataRetriever.getInstance().getCommonData() .onErrorReturn(new Func1<Throwable, CommonData>() { @Override public CommonData call(Throwable throwable) { // エラーのときはデータがないものとして扱う return CommonData.empty(); } }) .subscribe(new Action1<CommonData>() { @Override public void call(CommonData commonData) { commonDataBehaviorSubject.onNext(commonData); } }); } @NonNull @Override public Observable<CommonData> getCommonDataObservable() { return commonDataBehaviorSubject; } } public class MainFragment extends Fragment { public interface MainFragmentListener { @NonNull Observable<CommonData> getCommonDataObservable(); } @Nullable private MainFragmentListener listener; ... @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof MainFragmentListener) { listener = (MainFragmentListener) context; } } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (adapter == null) { adapter = new DataAdapter(); load(); } recyclerView.setAdapter(adapter); } void load() { recyclerView.setVisibility(View.GONE); progressView.setVisibility(View.VISIBLE); // 共通データとタブ独自のデータ両方揃うまで待ち合わせ subscription = Observable .combineLatest( getCommonDataObservable(), getSpecificDataObservable(), new Func2<CommonData, SpecificData, Pair<CommonData, SpecificData>>() { @Override public Pair<CommonData, SpecificData> call(CommonData commonData, SpecificData specificData) { return new Pair<>(commonData, specificData); } }) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<Pair<CommonData, SpecificData>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { recyclerView.setVisibility(View.VISIBLE); progressView.setVisibility(View.GONE); } @Override public void onNext(Pair<CommonData, SpecificData> combinedData) { recyclerView.setVisibility(View.VISIBLE); progressView.setVisibility(View.GONE); final List<String> list = new ArrayList<>(); final CommonData commonData = combinedData.first; list.add("CommonData : " + (commonData.isEmpty() ? "empty" : commonData.getData())); final SpecificData specificData = combinedData.second; list.addAll(specificData.getData()); adapter.addAll(list); } }); } /** * 共通のデータを取得 */ private Observable<CommonData> getCommonDataObservable() { return listener != null // first() を介して onComplete()が呼ばれるようにしている ? listener.getCommonDataObservable().first() : Observable.just(CommonData.empty()); } /** * このタブ独自のデータを取得 */ private Observable<SpecificData> getSpecificDataObservable() { final int position = getArguments() == null ? -1 : getArguments().getInt(ARGS_POSITION); return DataRetriever.getInstance().getSpecificData(position); } ... }

さらに、MainActivity に SwipeRefreshLayout を追加して、PullToRefresh で共通データを取り直し、各 MainFragment にもデータを取り直させる処理を追加したサンプルが
https://github.com/yanzm/BehaviorSubjectSample
です。


2016年6月12日日曜日

AutoValue ライブラリを試してみた

https://github.com/google/auto/blob/master/value/userguide/index.md

immutable value class を生成してくれるライブラリ。abstract クラスを用意して @AutoValue をつけると、equals() や hashCode() などの boilerplate なコードを実装したクラスを用意してくれる。

設定

dependencies { compile 'com.google.auto.value:auto-value:1.2' apt 'com.google.auto.value:auto-value:1.2' }

使い方

例えば @AutoValue abstract class Animal { abstract String name(); abstract int numberOfLegs(); } のようなクラスを定義すると、AutoValue_Animalというクラスが生成される。 final class AutoValue_Animal extends Animal { private final String name; private final int numberOfLegs; AutoValue_Animal( String name, int numberOfLegs) { if (name == null) { throw new NullPointerException("Null name"); } this.name = name; this.numberOfLegs = numberOfLegs; } @Override String name() { return name; } @Override int numberOfLegs() { return numberOfLegs; } @Override public String toString() { return "Animal{" + "name=" + name + ", " + "numberOfLegs=" + numberOfLegs + "}"; } @Override public boolean equals(Object o) { if (o == this) { return true; } if (o instanceof Animal) { Animal that = (Animal) o; return (this.name.equals(that.name())) && (this.numberOfLegs == that.numberOfLegs()); } return false; } @Override public int hashCode() { int h = 1; h *= 1000003; h ^= this.name.hashCode(); h *= 1000003; h ^= this.numberOfLegs; return h; } } コンスタクタでは name と numberOfLegs を引数に取り、equals() や hasCode() ではこれらを使った実装になっている。 immutable value class なのでコンスタクタで受け取った引数は final として保持される。

abstract クラスに static な生成メソッドを用意して利用する。 @AutoValue abstract class Animal { static Animal create(String name, int numberOfLegs) { return new AutoValue_Animal(name, numberOfLegs); } abstract String name(); abstract int numberOfLegs(); } コンスタクタの引数は null チェックされる。これを止めたいときは abstract メソッドの戻り値に @Nullable をつける。 @AutoValue abstract class Animal { @Nullable abstract String name(); abstract int numberOfLegs(); } final class AutoValue_Animal extends Animal { private final String name; private final int numberOfLegs; AutoValue_Animal( @Nullable String name, int numberOfLegs) { this.name = name; this.numberOfLegs = numberOfLegs; } ... } Builder を用意することも可能。@AutoValue をつけるクラスにインナークラスとして abstract static な Builder クラスを定義し、@AutoValue.Builder をつける。 @AutoValue abstract class Animal { abstract String name(); abstract int numberOfLegs(); static Builder builder() { return new AutoValue_Animal.Builder(); } @AutoValue.Builder abstract static class Builder { abstract Builder name(String value); abstract Builder numberOfLegs(int value); abstract Animal build(); } } final class AutoValue_Animal extends Animal { ... static final class Builder extends Animal.Builder { private String name; private Integer numberOfLegs; Builder() { } Builder(Animal source) { this.name = source.name(); this.numberOfLegs = source.numberOfLegs(); } @Override public Animal.Builder name(String name) { this.name = name; return this; } @Override public Animal.Builder numberOfLegs(int numberOfLegs) { this.numberOfLegs = numberOfLegs; return this; } @Override public Animal build() { String missing = ""; if (name == null) { missing += " name"; } if (numberOfLegs == null) { missing += " numberOfLegs"; } if (!missing.isEmpty()) { throw new IllegalStateException("Missing required properties:" + missing); } return new AutoValue_Animal( this.name, this.numberOfLegs); } } }