なるようになるかも

力は多くの場合、その人の思いを超えない。

AndroidのHttpURLConnection。

これはAndroidじゃなくてJDKのインターフェースの設計の問題なのですが、HttpURLConnectionは入出力エラーが発生した際にIOExceptionを投げるという規定があります。

問題は、HTTPステータスコードが400番台ないし500番台のコードのボディを読もうとした際に、getInputStream()を使うと入出力エラー扱いされてIOExceptionが発生することです。

最近のRESTfulなサーバーインターフェースの設計だと、HTTPステータスコードに意味を持たつつ、レスポンスボディにコンテンツを渡すのが主流ですが、HttpURLConnectionのちょっとしたサンプルでは400 Bad Request401 Unauthrizedが返されることを考慮していないことが多いです。

HttpURLConnectionを本格的に使おうとすると、大抵ここで躓くことになります。

正しい実装方法は…?

サーバーがHTTPステータスコード上でエラーを返したときは、getErrorStream()でしかレスポンスボディを読めません。

定番なのは getResponseCode()を読んで、getInputStream()を読むか、getErrorStream()を判定する実装です。

しかしHttpURLConnectionはどのHTTPステータスコードが入出力エラーに該当するのかを規定していないので、「実装上そうなっている」という経験則で400番台と500番台を特別扱いすることになり、これが非常に気持ち悪いです。

Androidでは FileNotFoundExceptionをキャッチした上で、getErrorStream()を読むという実装方法が紹介されていることがあります。

FileNotFoundExceptionをスローすることはAndroidのコードを読めば分かるのですが、404 NotFound以外でもFileNotFoundExceptionをキャッチして処理するのは直感に反していますし、ドキュメントに記載されている内容でもないので、将来的に変更されないとも限りません。あまり推奨された方法ではないと思います。

なおFileNotFoundExceptionIOExceptionのサブクラスなので、IOExceptionより先にキャッチする必要があります。

401 Unauthrizedの辛さ

アプリとサーバー間でやり取りする場合、認証処理が入るケースが多く、セッションのタイムアウトや、パスワード変更などでサーバーが401エラーを返し、それにハンドリングしなければならないケースはよくあります。

自前で実装する場合にはいろいろ闇があるので注意してください。

まず、前述した getResponseCode()で識別する方法ですが、古いAndroidでは使えません。

FROYO以前のAndroidには、HTTPステータスコードで401が返却された時点でコネクションをcloseするため、getResponseCode()の返り値が-1になる致命的なバグがあるためです。

古いAndroid端末もターゲットにする場合は、絶対にHttpURLConnectionを使わない でください。他にもコネクションプール汚染の深刻なバグもあります。Volleyの実装を見てもGoogle自身ですら、FROYO以前のAndroidではApache HTTP Clientで動作するようにしています。

あと、ヘッダにWWW-Authenticateを付与してくれない場合にIOExceptionになるケースが非常に厄介です。これは最近のAndroidでも起きるような…。

これについて言えば、仕様上は401 Unauthrizedを返すときは、WWW-Authenticateの付与が MUST とされているので正しい挙動なのです。サーバーとクライアント間で認証方法が自明であっても付与しましょう。しかしRFC 2616を理解せず、「流行ってるからRESTにしようぜ」的なノリでサーバーを実装する人は後を絶ちません。

稀に、不正な認証リクエストに対してWWW-Authenticateヘッダなしで401を返すサーバーも存在します。有名どころではTwitterくらいしか聞きませんけども。

Androidの通信処理に何使えばいいのか分からないって話。

特に結論はないです。本当に分からないので。

ソケットレベルまで踏み込むと、途端に面倒になってどのライブラリを使っても手に負えませんし、単にGETとかPOSTとかする分には正直どれ使ってもそこまで変わらない気がしてます。

それより自己署名証明書の検証を無視して通信を行うと端末が爆発するライブラリが必要だと思います。

Apache HTTP Client

みんなお馴染みDefaultHttpClient。色々なライブラリがあるけど、最終的にはここに行き着いていることが多いです。

しかし「Apache HTTP Clientとは何なのか」、という説明はあまり見ない気がします。

自分も「Apacheソフトウェア財団のトップレベルプロジェクトとして運用されている、RFCを満たす実装を目指したJava向けのHTTPインターフェース」という超ふんわりとした認識しかないです。

かなり巨大なライブラリで、全貌を理解するには、HTTPの仕様に関する理解が必要な感じ。

Android SDKの中にも入ってるから特に意識せず使ってる人も多いと思うんだけど、Android SDKに入っているApache HTTP Clientは、バージョン4.0の不完全な状態のものらしい?そのため、可能な限り最新の4.3との互換性を保つAndroid専用のサブライブラリがあります。

また、バージョンごとに大幅にAPIのインターフェースが変わる印象があって、Android以前の人が書き残したコードは、Deprecatedの嵐に見舞われていることが多いです。

Multipartの送信など面倒なことをやろうとする場合に、SDK内臓ではないApache HTTP Clientをライブラリとして選択するケースがあります。記述の冗長さがやはり大きな欠点ですが、RFCを意識したAPIは、仕様が共通言語の人たちにとっては苦にならないのかもしれません。

また、「Android向け」を謳うライブラリには通信量削減のために勝手にgzip圧縮などを行うものが多く、ほとんどの場合その方が効率的に通信を行えるのですが、「プログラマが書いた処理を厳密に実行して欲しい」という業務系ニーズでも強い印象です。

AndroidHttpClient

API Level8のころの遺物みたいなもの?それ以前では使えません。

Android SDKに組み込まれた「Apache HTTP ClientをAndroidに最適化したもの」という説明がされているDefaultHttpClientの亜種です。

コンテンツの送受信にgzip圧縮をかけていたり、タイムアウトの設定が携帯端末に合わせたものに調整されていますが、通信処理そのものはApache HTTP Clientに委譲しています。

また、API Level11以降のAndroidでは、UIスレッドで通信を行うとNetworkOnMainThreadExceptionでクラッシュしてくれる親切設計になりましたが、この時代は通信が平気でUIスレッドを固めていたので、プログラマの矯正の意味合いもありました。

これ使ってる人って本当にいるんです?

HttpURLConnection

java.net.HttpURLConnectionですが、AndroidAPIは、当然ながらインターフェースが同じなだけでJDK実装とは別物です。

この事実は、基本的なコレクションクラス等でバグに遭遇することはないので、まず気にすることではないのですが、暗号論的擬似乱数生成器のはずのSecureRandom脆弱性があった、などの闇が広がっています。

初期のHttpURLConnectionもそうした深淵の一つだったのですが、API Level10のころに大々的に「AndroidのHttpURLConnectionも進化したからみんな使ってね!」とアピールしていた記憶があります。もちろんAPI Level10未満の端末ではバグが残ったままなんですけどね…。

そのときアピールしてたメリットは、デフォルトでgzip圧縮してくれることでした。やっぱりAndroidHttpClient使ってる人いないのでは?

もともとはJDKのインターフェースだけあって、「DefaultHttpClientの生成方法がバージョンによってまったく別物になった」みたいな混乱はないですし、ちょっとした用途に使う分には十分です。

しかし、初期の試作にHttpURLConnectionのラッパーを作って楽をしようとした結果、後々複雑な通信をすることになってハマっているケースとか、OSのバージョンによって異なるIOExceptionのエラー文言を判定してる闇なコードを見たりします。

Async Http Client

Java用の非同期なHttpClient。

ちょっと前にAndroidの生きたライブラリとして紹介されてましたけども、Androidだと似たコンセプトの後者の採用率の方が高い気がします。

どうせAndroidではClosableとか使えませんからね…。

Android Asynchronous Http Client

中身としては、Apache HTTP Clientを、ExecutorServiceによるスレッドプールで非同期処理してくれるというものです。軽量なライブラリであることが特徴で、有名なアプリでも採用されており実績も豊富です。

ネットワークのために定型的な非同期処理を書く面倒さから解放してくれます。

反面、コールバックハンドラを記述する方式は、レスポンスをデータベースに格納するなど、非同期処理と連結させる必要がある場合に可読性を著しく損う印象があります。

簡単な処理は本当に簡単に書けますが、処理が複雑になることが予想される場合には、自前で非同期処理を書いたほうが見通しが良くなるかも?

Google HTTP Client Library for Java

冗長なApache Commonsの記述を、簡易に書くためのラッパーライブラリです(HttpURLConnectionでも使えるみたいです)。

ただしHTTP通信自体が主眼ではなくて、それより上位のユースケース、実際的にはXMLJSONシリアライズ/デシリアライズ等をアノテーションなどで簡潔に書くためのライブラリといった認識です。

このライブラリを直接使う機会はあまりないと思いますが、Googleのサービスとの接続に使われるGoogle APIs Client Libraryや、OAuth2.0による認可を簡単に行えるGoogle OAuth Client Libraryはこれがベースになっているので、間接的にお世話になっている人は多いんじゃないかなぁという印象です。

AndroidだとGoogleサービスとの連携がシームレスだから、関係ないんじゃないの?」と思うじゃないですか。思いましたよ。しかしGoogle Drive Android APIとか凄まじく貧弱なので、それなりなことをしようとすると結局こっちを使うハメになるんですよね…。

Volley

いい意味でも悪い意味でも「えー、今時Volley使ってないのー」みたいなノリを感じます。

GoogleによるAndroid向けのネットワークライブラリと銘打っているだけあって、HTTPURLConnectionの闇を打ち払ったり、ImageViewなどActivityのコンポーネント群との連携は抜群で、特に厄介なBitmap周りの処理を簡潔にしてくれますが、それだけではなく非常に汎用性が高いです。

ただやや概念が特殊なので、最初のとっかかりが悪いのと、下手に拡張性が高いために独自のリクエストクラスを量産した結果、Volley職人にしか理解ができないみたいな状態になりかねないのが二の足を踏む要素です。

AndroidでSNS連携する方法

すごく今更な話題なんですけど、iOS8で「AndroidのIntentみたいに簡単に共有できるようになる!」っていう話をよく聞いたので。

OS標準で共有機能のあるiOSアプリをAndroidに移植を行う場合に、「TwitterFacebookに投稿するボタン作って。IntentのあるAndroidなら簡単でしょ?」みたいに言われるんですが、現実は割と甘くないです。

確かに、ACTION_SENDなどで暗黙のインテントを飛ばすだけで勝手に投稿可能なメーラーSNSクライアントが一覧表示されます。

ただし、ACTION_SENDによる共有の考え方では、受け取ったアプリが全てハンドリングしてくれる利点と引き換えに、「Twitterのみに投稿したい」という制約条件を付与できないのです。

加えてSNSごとの文字数制限や、後述するFacebookの独自仕様などがあるため、送る側が何も考えなくても、インテントを受け取ったアプリでどうにかしてくれる…というわけでもありません。

Twitterに投稿したい場合

公式アプリのパッケージ名を指定してACTION_SEND

公式アプリ以外を使ってるユーザーを完全無視してもいいならこの方法が使えます。

URI.parse()を使う

https://twitter.com/intent/tweet?text=に送信したいメッセージを付与してURI.parse()します。大抵のTwitterクライアントはこのURLをハンドリングしてくれるはずです。

http://スキーマなので、普通のブラウザも反応するのが難です。このためTwitterクライアントがインストールされていないことを検知することはできませんし、インストールされていたとしてもログイン状況がどうなっているのかは把握できません。

Twitter4Jを組み込む。

そのため、Intentを投げ捨ててTwitter4Jを使うの最も現実的な解となっているような気がします。

それぞれのアプリがWebViewでログインさせている現状って正直微妙だと思うんですよね。

Account ManagerからOAuth2トークンを引っ張ってくるとか、もうちょっとマシな方法がある気がするのですが…。

Facebookに投稿したい場合

公式アプリのパッケージ名を指定してACTION_SEND

Intent連携を否定しているのがFacebookです。やれることは以下の2点のみです。

  • ウェブサイトへのリンクをシェアする
  • 画像をシェアする

「リンクもしくは画像」以外のものをIntent.EXTRA_TEXTに付与した場合、 その内容は破棄されます

なぜなら、Facebookには「 ウォールに投稿する文章はユーザーが制御するべき 」というポリシーがあるため、アプリ側から文字列を事前にプリセットする行為そのものが非推奨なのです。

じゃあなんでiOSのSocial.Frameworkだとできるの…。

Facebook SDKを使う

Android用の公式SDKを使う場合も、上記制約は変わらないため、FacebookDialog.ShareDialogではテキストのプリセットはできません。

何らかの文字をプリセットしたい場合、投稿用のDialogFragmentを自作しなければなりません。

LINEに投稿したい場合

LINEもIntent連携を無視した独自ルールを採用しています。

具体的には、line://msg/text/URI.parse()以外の方法で連携する手段がありません。

それiOSのURLスキームだと思うんですけど…。

UIKit徹底解説読んでる。

StoryBoardに乗り遅れてる感があるので手にとってみたのですけど、いい本です。まだ完全に読み込めてないのでざっくりとした感想ですけども。

UIKit徹底解説 iOSユーザーインターフェイスの開発

UIKit徹底解説 iOSユーザーインターフェイスの開発

特にUIFontDescriptorまわりについては、これほど丁寧な解説は存在しないと思います。

StoryBoardを駆使してコード量を減らしつつTODOアプリを作る章など、読み応えのある本でした。

iOS6と7による違いのトラップについても随所に述べられています。

ただUIKitの処理について徹底的に書かれているかというと、ヒットテストビューやレスポンダチェーンについての解説はイベント処理ガイド(iOS用)をほぼなぞりつつ、ジェスチャレコナイザのiOS7用メソッドに関する解説が付け足されているような感じでした。

いくつか食い足りない感じの場所もありました。

UIAppearanceの話

UIAppearanceの外観設定についても割とあっさりでした。

実行時にしか反映されないから、StoryBoardとの相性は悪いのでしょうがないのかも。

そもそもクラスリファレンスにどのプロパティがUIAppearanceに対応してあるのか書いていないという致命的な問題があって、ヘッダファイルにUI_APPEARANCE_SELECTORマクロが定義されているかを追っかけて回るわけですけど、SDKバージョンによって予告なく変更されたり、指定されていなくても実は使えたりとか、iOS7でtintColorの解釈が変わったりとか…。

まともに使えたものじゃないですんですよね。

それでも現状、UIButtonの見栄えを統一したいときは、resizableImageWithCapInsets:resizingMode:で縮尺の設定をした画像を、UIAppearance経由でsetBackgroundImageなどに設定するのが、便利だし実装コストも低い方法だと思っているのですけれど、IBやSBに反映されないというのはやはり痛くて、iOS7に置き換わっていくとアセットカタログのスライシングに流れていくんでしょうか。

iOS7以降ではtintColorでアプリのテーマカラーを決めて欲しいように見えますし。

AutoLayoutの話

この本はAutoLayoutをStoryBoardで扱う方法について相当力を入れて解説していますけども、コード上で扱う方法についてはVisual Format Languageで少し言及しているだけです。

Visual Format Languageのヒドさは今更言及する必要はないかと思いますが、ではこれを使わずまともに制約を書こうとすると、これがまた難解というか面倒でやってられないんですよね…。

SB任せでも基本的には問題ないんですけど、制約ベースでアニメーションをしようと思うと、制約を修正(場合によっては一度消してから再生成)して、アニメーションブロックでlayoutIfNeededを呼ぶ必要があるので、複雑な制約が付いているビューを動かす状況など、コードベースの制約についての理解もそれなりに必要なんじゃないの?という気がします。

ところで本書にも制約ベースのアニメーションに関する記述が若干載っていて、そこでは制約を相対的に操作して、layoutSubViewsを呼び出しているのですけれど、UIViewのクラスリファレンスなど読むに、

You should not call this method directly. If you want to force a layout update, call the setNeedsLayout method instead to do so prior to the next drawing update. If you want to update the layout of your views immediately, call the layoutIfNeeded method.

layoutSubViewsの直接呼出しって作法として微妙なんじゃないのかな?

ここでのshould notは「推奨されない」程度の意味だと思いますけど。

XCode6の話

NDAの関係で白昼夢なんですけど、XCode6で個人的に一番インパクトのある変更点は、IBやSBがdrawRect:を解釈してくれることなんですよね。

今までカスタムビューを作っても、IBやSB上では透明なビューにしか見えなかったのですけど、これで面白いコントロール系ライブラリがまた増えてくる可能性があります。もっともコントロール系は暗黙アニメーションを利用するために、drawInContext:で実装する方が多い気がしますけど…。

本書ではカスタムビューの説明をかなり端折ってあって、そこがちょっと勿体無いなーという感想でした。

ついでに言えば、drawRect:のライブレンダリングの説明には、intrinsicContentSizeを解釈してくれるという記述は見当たらないから、AutoLayout上にカスタムビューを配置するにはプレースホルダーを指定しなきゃいけないという残念な仕様はそのままな気がします。

iOS6/7のviewDidLoadが呼ばれるタイミングの違い

viewDidLoadが呼ばれるタイミングは、UIViewControllerviewプロパティが初めてアクセスされたタイミングである」と公式ドキュメントに書いてあります。

しかし、 いつ・誰がviewプロパティにアクセスするのか についてはフレームワーク内部なのでブラックボックスとなっています(スタックトレースを追えばすぐ分かるんですけど)。

例えばこういう書き方をしたとします。

HogeViewController *vc = [HogeViewController new];
[self.navigationController pushViewController:vc animated:YES];
vc.hogeLabel.text = @"hoge";

このコードはiOS6ではラベルが書き換わるのだけど、iOS7ではvc.hogeLabelnilと評価されるため、動作しません。ここでは画面遷移をコードで管理していますが、storyboardを用いて、[segue destinationViewController]で取得した場合でも同じことが言えます。

つまるところ、iOS6ではpushViewController:animated:presentViewController:animated:が実行された時点でviewプロパティにアクセスされ、viewDidLoadがコールされていたのに対して、iOS7ではそのスタックを抜けた時点で初めてviewプロパティにアクセスされるよう変更されました。

遷移処理をその場で行わず、メインスレッドにキューイングするように変更されたのでしょうね。

こんなトラップにハマる人はまずいないと思うのですけど、イニシャライザやviewDidLoadで時間の掛かる処理(ネットワーク通信など)を行い、プログレス表示を行いたい場合、本来は非同期処理にしてコールバックを受け取るような仕組みを作る必要があります。

これを同期的に処理を書こうと考えたのか、

[self showProgress]; // keyWindowに対してプログレス用のviewをaddする
HogeViewController *vc = [HogeViewController new];
[self.navigationController pushViewController:vc animated:YES];
[self hideProgress]; // addしたviewをremoveする

と書いてあるコードが存在し、もはや期待した動作をしていませんでした。ていうかこんな発想自体ないわー。

バッドノウハウですが、

HogeViewController *vc = [HogeViewController new];
[self.navigationController pushViewController:vc animated:YES];
vc.view;
vc.hogeLabel.text = @"hoge";

と書いて無理やりviewDidLoadと呼びだすこともできます。ただしコンパイラの最適化や今後のiOSバージョンによって挙動が変わる恐れがありますし、適切に非同期処理を書くべきです。

iTunes Connectの変更の話。

TechCrunchの記事(というかその翻訳)が酷かったので。

原文タイトルは「Apple Developers Must Now Agree To Ad Identifier Rules Or Risk App Store Rejection」。

Appleデベロッパーは今後広告識別子の規則に同意しなければ、AppStoreからリジェクトされるだろう」という話。

職業訳者ならばもっとこなれた日本語にするのでしょうけれど、「同意が必要となった」を「規則遵守が義務化」という頭痛が痛い表現にはしないだろうし、間違ってもリスクを「違反者は拒絶される」と超訳したりはしないでしょう。

Apple悪帝国」というシナリオの文脈で翻訳したのだと思うんだけど、別に今回の件はそういう話じゃないと思うんだよね。

iTunes Connectの変更点

具体的に何が変更されたのかといいますと、

  • Advertising Identifierとは何か?の説明
  • Advertising Identifierを利用しているか?ONにした場合、以下の追加項目

が追加されました。利用用途については現在の典型的な、ユーザーのプライバシー保護を考える上で容認される利用法をカテゴライズしたものです。

このアプリがAdvertising Identifierを利用する目的 (当てはまるもの全てを選択)

  • このアプリ内で広告を提供するため
  • 提供された広告によって、このアプリがインストールされるため
  • 提供された広告によって、このアプリでアクションを実行するため

2、3番目はいわゆる「リワード広告」を指しています。

Advertising Identifierを使ってどの広告からアプリがインストールされたのか検知したい場合、いわゆるCPI広告を組み込む場合には2番目をチェックする必要があります。3番目のユースケースが具体的にどういう状況かは良く分かりません。

そしてこれらの選択肢の後には次の文が続いています。

If you think you have another acceptable use for the Advertising Identifier, contact us.

「もし他に許容される広告識別子の利用があると考えるのであれば、我々にご連絡下さい」とあります。

連絡すると個別対応が行われるんでしょうか。しかし「許容される利用用途の範囲」であり、特例措置が取られるわけではないと読み取れます。

iTunes Connect更新の真意

Advertising Identifierは広告のみに利用すること、取得した情報の利用範囲の定義については、Developerプログラムライセンスの利用規約の3.3.12項と3.3.13項で明文化されています。

ライセンスを保持している時点で、この規約を読んで同意しているはずで、今回新たに義務や契約が発生したということはないです。

ではなぜ更新が行われたのでしょう?

サードパーティライブラリであっても、アプリ開発者の責任になるよ 」という点に、明示的に同意を求めなければならないほど、状況が悲惨だったからでしょう。

開発者の責務

Advertising IdentifierはUDIDを代替するスーパークッキーを意図して設計されているため、利便性と危険性を伴います。用途が広告のみに限定されているのはそのためです。

実際のところ、開発者自身が用途の限られているAdvertising Identifierを取得するのは稀だと思います。組み込んだ広告SDKが利用しているケースがほとんどでしょう。

そのため、アプリ開発者はAdvertising Identifierが何に利用されているのか以前に、その存在そのものを知らないということがありえました。

今回、開発者に対して、取得の意図をチェックさせる変更を加えたのは、実質的にはAdvertising Identifierの存在と存在意義を開発者に対して周知し、そして サードパーティのポリシーを確認したことを表明させるため でしょう。

Advertising Identifierの十全性

「広告型追跡を制限」の尊重について、チェックボックスがあるのはなぜでしょう?

Advertising Identifierは実際のところ完璧ではないためです。

例えばユーザーがAdvertising Identifierをリセットしたとしても、以前のAdvertising Identifierと紐付けることで、半永続的に個人を一意特定することができますし、ユーザーが「広告型追跡を制限」の設定を有効にしたにも関わらず、これを無視して端末内部に保持したAdvertising Identifierを使い続けることが可能です。

もちろん、これらの行為は禁止されています。

しかし広告ライブラリはソース非公開のため、evilな動作を行っていないと証明することはできません。

これからはアプリ開発者は自身が利用しているブラックボックスなライブラリも含めて、Advertising Identifierを規則に沿って利用していることを誓約する必要があるのです。

で、結局どーなるのか。

先に述べた通り、「同意しなければリジェクトされるリスクを負う」だの「義務化され違反者は追放される」というような表現が正しいとは思えません。ライセンスを持った時点で既に義務は背負っているはずですし、サードパーティーの無理解に伴うリジェクトラッシュはむしろ峠を越えた頃じゃないでしょうか。

この確認事項の追加によって、開発者のこれまでの、「ライブラリのせいだから自分のコードは悪くない」という意識が変わり、コンバージョンだけでなく、セキュリティを尊重した広告SDKを選択しようという動きへ変わっていくのではないでしょうか。

一方、広告SDKを提供する側は、開発者に対してAdvertising Identifierの利用ポリシーを遵守していることを表明しなければなりません。evilな広告業者は淘汰され、コンシューマサイドから見ると平和に収束していくと考えられます。…やや楽観的過ぎるかもしれませんが。

もっともiTunes Connectの確認事項がそれほど重要視されていないのは、戦略物資である暗号について適当な解答をしてもどーにかなってる辺りでお察し下さい。外国為替及び外国貿易法とかEARとか気にしてる人、そんなにいないでしょう。

FragmentとActivityの連携方法

最初に、エアコードなので動作するかは分かりません。ちゃんと動くコードをまとめて公開するようにした方がいい気がするけど時間があるときに。

Activity→Activity→Activity

超基本。呼び出し元はstartActivityForResult()を使う。

Intent intent = new Intent(this, FugaActivity.class);
startActivityForResult(intent, HOGE_QUEST_CODE);

結果はsetResult()で返す。

Intent data = new Intent();
data.putExtra("key", "value"); // 任意のデータを呼び出し元に返せる
setResult(RESULT_OK, data);
finish(); // finishのタイミングは任意だが、速やかに書かないと可読性が著しく落ちる

すると呼び出し元ActivityのonActivityResult()が呼び出されるのでオーバーライドして処理を記述する。

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data){
  if(requestCode == HOGE_QUEST_CODE && resultCode == RESULT_OK){
     data.getStringExtra("key");  // "value"を取得できる
  }
}

Fragment→Activity

コールバック用のインターフェース(以下、リスナ)を作成し、Activityに実装する。あとは普通にActivityからFragmentを表示する。

ActivityにFragmentを表示するのは以下のお馴染みのコード。

HogeFragment hogeFragment = new HogeFragment();
getSupportFragmentManager().beginTransaction().add(R.id.placeholder, hogeFragment ).commit();

するとFragment側でonAttach()が呼び出されるので、両者の紐付けをここで行う。

  @Override
  public void void onAttach (Activity activity){
    super.void onAttach (activity);
     try {
       mListener = (HogeFragmentListener) activity;
     } catch (ClassCastException e) {
       // リスナが実装されていない場合、キャスト例外が発生する
       // バグなので、ClassCastExceptionを再スローするかIllegalStateException辺りを投げるのが一般的
       e.printStackTrace();
     }
   }

getActivity()を使えば任意のタイミングでリスナを登録できるように思えるが、なぜonAttach()で行うのか。

おそらく画面回転(および画面サイズ変更)時には以下の4ケースが考えられるからと考えられる。

  • Activity/Fragment共に再作成されるケース(デフォルト)
  • Activityの再作成を抑制するケース
    • android:configChanges="orientation|screenSize"を指定している場合、Activityは破棄されないが、 Fragmentは再作成される 。その際、再びonAttach()が呼び出される。
  • Fragmentの再作成を抑制するケース
    • setRetainInstance(true)でFragmentの再作成を抑制した場合は、 Activityのみ破棄される 。新しいActivityに対してonAttach()が呼び出され、正しく紐付けられる。
  • Activity/Fragmentの再作成を抑制するケース
    • 両者の複合パターン。やったことないけどやる人もいるんだろう、きっと。この場合は最初の紐付けが有効のままなので特に問題ないはず。

いずれのパターンでも必ず一度だけ紐付けられるonAttach()が最も都合が良いわけですね。

FragmentのonAttach()onCreate()より先に呼び出されるので、このタイミングはまだFragmentの状態は不完全であることに注意。リスナの登録のみ行うという認識であること。

Fragment→Fragment→Fragment

Fragmentの場合、それが兄弟関係にあるFragmentか、親子関係にあるFragmentかによってコールバックの方法を変えなければならない

Fragmentが兄弟関係にある場合は、呼び出し元はsetTargetFragment()を利用して、自身を渡す。

HogeFragment hogeFragment = (HogeFragment)getFragmentManager().findFragmentByTag(HOGE_FRAGMENT)
hogeFragment.setTargetFragment(this, HOGE_QUEST_CODE);    

呼ばれた側はgetTargetFragment()を用いて取得できる。画面回転などでいずれかのFragmentが再作成された場合でも両者は紐付けられたままとなる。

FugaFragment callerFragment = (FugaFragment)getTargetFragment();
callerFragment.doSomething();

直接メソッドを呼び出すのではなく、Activity→Fragmentのときと同様に、何らかのリスナを実装して汎化するのが一般的。キャスト例外を捕まえてエンバグを防ぐ手法も同じように使える。

DialogFragmentchildFragmentManager()で表示する場合など、Fragment in FramentでsetTargetFragment()をすると、Activityが再生成されたときに正しく復元されない。

この場合はコールバック先は、getParentFragment()で取得する。

FugaDialogFragment.newInstance();
    .show(getChildFragmentManager());

呼び出し側のコードは以下の通り。

Fragment callerFragment = getParentFragment();
callerFragment.doSomething();

FragmentManagerの処理は闇が深い。

Fragment→Activity→Activity→Activity

そのFragmentが属している親Activityが、別のActivityに結果を求めて、親Activityで結果を処理するケース。

処理そのものは最初の「Activity→Activity→Activity」と同じになる。

Intent intent = new Intent(this, FugaActivity.class);
getActivity().startActivityForResult(intent, HOGE_QUEST_CODE);

が、この書き方は混乱を招くので、一旦リスナなどでActivityへ処理を移譲して、ActivityからstartActivityForResult()を呼び出したほうが良い。

Fragment→Activity→Fragment

API Lv11以上以上では、Fragmentから直接別のActivityに結果を求めて、Fragmentで結果を処理することができる。

Intent intent = new Intent(this, FugaActivity.class);
// getActivity()がないことに注意
startActivityForResult(intent, HOGE_QUEST_CODE); 

上と比較すると、getActivity()がないだけでほぼ同じコードに見えるが、Activityからのコールバックは Fragment内のonActivityResult()に記述する という点が異なる。

// Activityではなく、Fragmentに書く
@Override  
public void onActivityResult(int requestCode, int resultCode, Intent intent){
  if(requestCode == HOGE_QUEST_CODE && resultCode == Activity.RESULT_OK){
     //RESULT_OKはActivityクラスの定数なので、Activity.RESULT_OKと書く
  }
}

Fragment→Activity→Activity→Fragment

サポートパッケージのFragmentが利用されている場合でも「Fragment→Activity→Fragment」と同じ処理は行える。ただし、 仕組みが異なるため、直接ActivityからFragmentへコールバックできない

コードそのものは「Fragment→Activity→Fragment」パターンと同じなので割愛するが、親ActivityであるFragmentActivityが、そのonActivityResult()内で、呼び出し元FragmentのonActivityResult()をコールしているという点を認識しておけばOK。

この場合、もしActivityとFragmentとでリクエストコードが重複したら、親ActivityのonActivityResult()で処理がブロックされ、正しくコールバックされないのではないか?と疑問に思うところだけど、そうならないためにリクエストコードに対して16bitのシフト演算を行っている。

リクエストコードのintは32bitなので、下位16bitをFragmentの、上位16bitをActivityのリクエストコードとして引き渡して識別するしている模様。

このシフト演算は自動的に行ってくれるため通常意識する必要はないが、演算の結果オーバーフローする場合(つまりFragmentから指定したリクエストコードが16bitを超える場合)には例外が発生することは、頭の片隅に入れる必要がある。

ちなみにこの方法はgetChildFragmentManager()が絡むと以下略。

アンチパターン

  • FragmentのgetActivity()からstartActivityForResult()をコールするのは実際ヤバイ。
  • 基本だけど、Fragmentのコンストラクタ引数を使わないこと。ここでコールバックリスナを渡しても、画面を回転させると引数なしのデフォルトコンストラクタで再生成されるため。(lintを走らせるとコンパイルエラーになる)
  • setArgments()でコールバックリスナを渡す。リスナが再作成された場合に紐付けが更新されないため不適切。
  • Fragment in Fragment(Fragmentが親子関係にある)ときに、setTargetFragment()を使う。Activityが再生成された場合に紐付けがおかしくなるため不適切。最悪クラッシュする。
  • FragmentからFragmentに結果を返すとき、getTargetFragment()で取得したFragmentのonActivityResult()を直接呼び出せばリスナは不要だけどたぶんバッドノウハウ

うーーん…。