FragmentからActivityにコールバックする方法2017

  • 34
    いいね
  • 0
    コメント

FragmentからActivityのメソッドを呼び出したい(コールバックしたい)こと、ありますよね。

二昔前には幾度か議論になりましたが、概ね以下のような内容だったかと思います。

当時、私の中で(面倒くさいながらも)有力だったのは、「インターフェースを被せることで特定のActivityへの依存性を排除しつつ、基本的には getActivity() で逐一取ってきて、キャストの成否をちゃんとチェックする」という方針でした。

今回ご紹介するものも方針はまったく同じなのですが、Java8のOptionalと高階関数を使うと少しスッキリ書けるよ、というお話です。

コード

HogeFragment.java
public class HogeFragment extends Fragment {
  // 略
  private void onClickHoge(Hoge hoge) {
    Optional.ofNullable(getActivity())
      .filter(activity -> activity instanceof OnHogeListener)
      .map(activity -> (OnHogeListener) activity)
      .orElseThrow(() -> new IllegalStateException("ActivityにOnHogeListenerを実装してください"))
      .onHoge(hoge);
  }

  public interface OnHogeListener {
    void onHoge(Hoge hoge);
  }
}
HogeActivity.java
public class HogeActivity extends Activity implements HogeFragment.OnHogeListener {
  // 略
  @Override
  public void onHoge(Hoge hoge) {
    // TODO Activityでやりたいこと
  }
}

解説

まず、Activity側は普通にコールバック用のインターフェースを実装しているだけです。従来とまったく変わりません。

変わったことをしているのはFragmentです。

とりあえずOptionalで包む

なにはともあれ、とりあえず包みます。

Optional.ofNullable(getActivity())

実は今回のサンプルの文脈ではクリックイベントで呼ばれているので、Optional.of()でも大丈夫です。このタイミングで getActivity() がnullであることは考えづらいからですね。

とはいえ、非同期処理の結果をコールバックするタイミングなどではnullになることも多いので、基本的には ofNullable を使ったほうがよいでしょう。

この時点でnullだった場合は、後述する orElseThrow の処理が直後に実行されます。

型をチェックする

従来は丁寧にチェックしていく場合にはif文を書いていましたが、今回はfilterで判定します。

.filter(activity -> activity instanceof OnHogeListener)

これにより、型を確認したデータだけを次の処理に回すことができます。

この時点でinstanceOfがfalseを返す場合は、Optionalの値がemptyになるので、次のmapが実行されず、更に次の orElseThrow の処理が直後に実行されることになります。

キャストする

次はmapによる変換の中でキャストを試みます。

.map(activity -> (OnHogeListener) activity)

この処理にたどり着いた時点で、確実にキャストが成功することが保証されているので、この処理は必ず OnHogeListener 型の値を次の処理に渡します。

例外処理

ここまでに有効な OnHogeListener を確保できなかった場合に、例外を発生させます。

.orElseThrow(() -> new IllegalStateException("ActivityにOnHogeListenerを実装してください"))

orElseThrowは「Optionalに値が入っていたら値を返す」「Optionalがemptyだったら例外を発生させる」という特性を持っています。

ここで ClassCastException 相当の例外を発生させて、プログラマーにインターフェースの実装を促します。

ところで、この処理は getActivity() がnullだった場合にも呼ばれてしまいます。今回の文脈ではありえないので書きませんでしたが、非同期処理でnullになりうる場合には .ifPresent などを使って穏便に済ませるほうがよいかもしれません。

コールバックを実行する

OnHogeListener が確保できたので、安心してコールバックを呼び出せます。

.onHoge(hoge);

前述のように、例外を発生させたくない場合には、 orElseThrow を使わずに ifPresent を使う方法も有効です。

.ifPresent(listener -> listener.onHoge(hoge));

ただし、この方法ではインターフェースの実装忘れを防止できないので、ケースバイケースで使い分けていったほうがよいでしょう。

Androidバージョンの話

Android Studio 3.0 (Gradle Plugin for Android 3.0)からは、Java8の文法が制限付きながらAndroidで使えるようになります(もうRetrolambdaを使わなくてよいのです!)。今回の方式に大きな役割を果たしたラムダ式もそのひとつで、今後はすべてのminSdkVersionで使えるようになります。

しかしながら、java.util.Optional はminSdkVersionを選びますので、しばらくはLightweight-Stream-APIcom.annimon.stream.Optionalを利用するべきでしょう。

まとめ

「AndroidでJava8は使えない。もしくは得体の知れないRetrolambdaとかいうツールを使うしかない」という言い訳で、AndroidエンジニアがJava8を敬遠できた時代はもう終わりました。

ラムダや高階関数は、上手に使えば、従来ならば煩雑になってしまいがちだった処理を、少しだけすっきりさせてくれることが多い道具です。見た目が煩雑になるからと避けてきた複雑な処理を、可読性を保ったまま書けそうという勇気を与えてくれる存在でもあります。

生産性を上げるために、Java8、書いてみませんか。

反省1

Java8とラムダを混同している印象を与える文面が随所にあるかもですが、Java8の魅力はラムダだけじゃないので気になった人はちゃんと調べてくださいね!

反省2

なぜ私は世間的にはJava9がリリースされた日にJava8の話をしているのだろうか・・・

9/22 13:15追記

@ryugoo さんから更にグレートなやり方を教えてもらいました! :pray:

Optional.ofNullable(getActivity())
  .select(OnHogeListener.class)
  .orElseThrow(() -> new IllegalStateException("ActivityにOnHogeListenerを実装してください"))
  .onHoge(hoge);

すごい(語彙薄弱)

9/22 16:55追記

@sys1yagi さんからド正論をいただきました。

Activityの生存期間中ずっと生きてるViewModelさんがいるんだし、LiveDataとかでデータ変更のイベントをもらえるようにしとけばActivityとFragmentの間で通知する機会も減るんだし、そうだよねという感じ。