FragmentからActivityのメソッドを呼び出したい(コールバックしたい)こと、ありますよね。
二昔前には幾度か議論になりましたが、概ね以下のような内容だったかと思います。
当時、私の中で(面倒くさいながらも)有力だったのは、「インターフェースを被せることで特定のActivityへの依存性を排除しつつ、基本的には getActivity()
で逐一取ってきて、キャストの成否をちゃんとチェックする」という方針でした。
今回ご紹介するものも方針はまったく同じなのですが、Java8のOptionalと高階関数を使うと少しスッキリ書けるよ、というお話です。
コード
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);
}
}
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-APIのcom.annimon.stream.Optional
を利用するべきでしょう。
まとめ
「AndroidでJava8は使えない。もしくは得体の知れないRetrolambdaとかいうツールを使うしかない」という言い訳で、AndroidエンジニアがJava8を敬遠できた時代はもう終わりました。
ラムダや高階関数は、上手に使えば、従来ならば煩雑になってしまいがちだった処理を、少しだけすっきりさせてくれることが多い道具です。見た目が煩雑になるからと避けてきた複雑な処理を、可読性を保ったまま書けそうという勇気を与えてくれる存在でもあります。
生産性を上げるために、Java8、書いてみませんか。
反省1
Java8とラムダを混同している印象を与える文面が随所にあるかもですが、Java8の魅力はラムダだけじゃないので気になった人はちゃんと調べてくださいね!
反省2
なぜ私は世間的にはJava9がリリースされた日にJava8の話をしているのだろうか・・・
Java 9 is Out!!!!#JDK9 #Java9 #Javahttps://t.co/VE7BI4KPlK pic.twitter.com/kOdNiLJ1ky
— Java (@java) 2017年9月21日
9/22 13:15追記
@ryugoo さんから更にグレートなやり方を教えてもらいました!
Lightweight Stream API を使う場合、filter → map の流れを独自オペレータの select(Class) を使う手もありますねー。さらに短くなります😃
— Ryutaro Miyashita (@ryugoo_) 2017年9月22日
Optional.ofNullable(getActivity())
.select(OnHogeListener.class)
.orElseThrow(() -> new IllegalStateException("ActivityにOnHogeListenerを実装してください"))
.onHoge(hoge);
すごい(語彙薄弱)
9/22 16:55追記
@sys1yagi さんからド正論をいただきました。
ViewModel使ったらいいと思った。 [ FragmentからActivityにコールバックする方法2017 https://t.co/wTFY66wM9q ]
— 八木 (@sys1yagi) 2017年9月22日
これ https://t.co/JzH20LCaj5
Activityの生存期間中ずっと生きてるViewModelさんがいるんだし、LiveDataとかでデータ変更のイベントをもらえるようにしとけばActivityとFragmentの間で通知する機会も減るんだし、そうだよねという感じ。