15

バックグラウンド

Android での非同期コールバック

Android で信頼性の高い方法で非同期操作を実行しようとすると、不必要に複雑になります。つまり、AsyncTask は本当に概念的に欠陥があるのでしょうか、それとも単に何かが足りないのでしょうか?

さて、これはすべて Fragments の導入前です。フラグメントの導入により、onRetainNonConfigurationInstance()は非推奨になりました。そのため、Google が容認する最新のハックは、構成の変更 (画面の回転、言語設定の変更など) が発生したときにアクティビティにアタッチ/デタッチする永続的な非 UI フラグメントを使用することです。

例: https://code.google.com/p/android/issues/detail?id=23096#c4

IllegalStateException - onSaveInstanceState の後でこのアクションを実行できない

理論的には、上記のハックにより、恐ろしいことを回避できます。

IllegalStateException - "Can not perform this action after onSaveInstanceState"

永続的な非 UI フラグメントが onViewStateRestored() (または onResume) および onSaveInstanceState() (または onPause) のコールバックを受け取るためです。そのため、インスタンスの状態がいつ保存/復元されるかがわかります。これは非常に単純なコードですが、この知識を利用すると、アクティビティの FragmentManager の mStateSaved 変数が false に設定されるまで、非同期コールバックを実行する前にキューに入れることができます。

mStateSaved は、この例外の発生を最終的に担当する変数です。

private void checkStateLoss() {
    if (mStateSaved) {
        throw new IllegalStateException(
                "Can not perform this action after onSaveInstanceState");
    }
    if (mNoTransactionsBecause != null) {
        throw new IllegalStateException(
                "Can not perform this action inside of " + mNoTransactionsBecause);
    }
}

理論的には、フラグメント トランザクションを安全に実行できるタイミングがわかったので、恐ろしい IllegalStateException を回避できます。

違う!

ネストされたフラグメント

上記のソリューションは、アクティビティの FragmentManager に対してのみ機能します。フラグメント自体にも、フラグメントのネストに使用される子フラグメント マネージャーがあります。残念ながら、子フラグメント マネージャーはアクティビティのフラグメント マネージャーとまったく同期していません。そのため、アクティビティのフラグメント マネージャーは最新であり、常に正しい mStateSaved を持っています。子フラグメントは別の方法で考え、不適切なときに恐ろしい IllegalStateException を喜んでスローします。

さて、サポート ライブラリの Fragment.java と FragmentManager.java を見たことがあれば、フラグメントに関係するすべてのことがエラーを起こしやすいことに驚かないでしょう。デザインとコードの品質は...ああ、疑わしいです。ブーリアンは好きですか?

mHasMenu = false
mHidden = false
mInLayout = false
mIndex = 1
mFromLayout = false
mFragmentId = 0
mLoadersStarted = true
mMenuVisible = true
mNextAnim = 0
mDetached = false
mRemoving = false
mRestored = false
mResumed = true
mRetainInstance = true
mRetaining = false
mDeferStart = false
mContainerId = 0
mState = 5
mStateAfterAnimating = 0
mCheckedForLoaderManager = true
mCalled = true
mTargetIndex = -1
mTargetRequestCode = 0
mUserVisibleHint = true
mBackStackNesting = 0
mAdded = true

というか、ちょっと話が脱線。

行き止まりの解決策

そのため、この問題の解決策は簡単だと思うかもしれませんが、これはこの時点では少し反意語のように思えますが、別の素敵なハッキーな非 UI フラグメントを子フラグメント マネージャーに追加することです。おそらく、そのコールバックは内部状態と同期しており、物事はすべてうまくいくでしょう。

再度間違える!

Android は、他のフラグメント (ネストされたフラグメント) に子としてアタッチされている保持フラグメント インスタンスをサポートしていません。さて、後から考えると、これは理にかなっているはずです。保持されていないためにアクティビティが破棄されたときに親フラグメントが破棄された場合、子フラグメントを再アタッチする場所がありません。だから、それはうまくいきません。

私の質問

非同期コード コールバックと組み合わせて、子フラグメントでフラグメント トランザクションを安全に実行できるかどうかを判断するためのソリューションを誰かが持っていますか?

4

1 に答える 1

4

更新 2

リアクトネイティブ

我慢できる場合は、React Native を使用してください。わかっています、わかっています... 「汚い Web テクノロジ」ですが、真剣に考えれば、Android SDK は大惨事です。プライドを捨てて、試してみてください。驚くかもしれません。私は知っている!

React Native を使用できない、または使用しない

ネットワーキングへのアプローチを根本的に変えることをお勧めします。リクエストを発行し、リクエスト ハンドラを実行して UI を更新することは、Android のコンポーネント ライフサイクルではうまく機能しません。

代わりに、次のいずれかを試してください。

  1. シンプルなメッセージ パッシング システムに移行しLocalBroadcastReceiver、寿命の長いオブジェクト (通常の Java クラスまたは Android サービス) がリクエストを実行し、アプリのローカル状態が変化したときにイベントを発生させます。次に、アクティビティ/フラグメントで、特定のものをリッスンし、Intentそれに応じて更新します。
  2. Reactive イベント ライブラリ (RxJava など) を使用します。私はこれを Android で自分で試したことはありませんが、同様のコンセプト ライブラリである Mac/デスクトップ アプリ用の ReactiveCocoa を使用してかなりの成功を収めました。確かに、これらのライブラリの学習曲線はかなり急ですが、慣れると、このアプローチは非常に新鮮です。

更新 1: クイック アンド ダーティ (公式) ソリューション

これは Google の最新の公式ソリューションだと思います。ただし、このソリューションは実際にはあまりうまく拡張できません。キュー、ハンドラー、および保持されたインスタンスの状態を自分でいじるのに慣れていない場合は、これが唯一の選択肢かもしれません...しかし、警告しなかったとは言わないでください!

Android のアクティビティとフラグメントは、 AsyncTaskLoader で使用できる LoaderManager をサポートします。バックグラウンドでローダー マネージャーは、保持されるフラグメントとまったく同じ方法で保持されます。そのため、このソリューションは、以下の私自身のソリューションと少し共通しています。AsyncTaskLoader は、技術的に機能する部分的に事前に準備されたソリューションです。ただし、API は非常に扱いにくいものです。使って数分でわかると思います。

私の解決策

まず、私のソリューションは実装が決して簡単ではありません。ただし、実装が機能するようになると、簡単に使用でき、心ゆくまでカスタマイズできます。

アクティビティのフラグメント マネージャー (または私の場合はサポート フラグメント マネージャー) に追加された保持フラグメントを使用します。これは、私の質問で述べたのと同じテクニックです。このフラグメントは、アタッチされているアクティビティを追跡する並べ替えのプロバイダーとして機能し、Message および Runnable (実際にはカスタム サブクラス) キューを持ちます。インスタンスの状態が保存されなくなり、対応するハンドラー (または実行可能) が「実行可能」になると、キューが実行されます

各ハンドラ/ランナブルは、コンシューマを参照する UUID を格納します。コンシューマーは通常、アクティビティ内のどこかにフラグメント (安全にネストできる) です。コンシューマフラグメントがアクティビティにアタッチされると、プロバイダフラグメントが検索され、その UUID を使用して自身が登録されます。

コンシューマー(フラグメント) を直接参照するのではなく、UUID などの何らかの抽象化を使用することが重要です。これは、フラグメントが頻繁に破棄および再作成されるため、コールバックに新しいフラグメントへの「参照」が必要なためです。破壊された活動に属する古いものではありません。そのため、残念ながら、匿名クラスによってキャプチャされた変数を安全に使用できることはほとんどありません。繰り返しますが、これは、これらの変数が古い破棄されたフラグメントまたはアクティビティを参照している可能性があるためです。代わりに、ハンドラーが保存した UUID と一致するコンシューマーをプロバイダーに問い合わせる必要があります。次に、このコンシューマーをキャストできます有効なコンテキスト(アクティビティ)を持つ最新のフラグメントであることがわかっているため、実際にどのようなフラグメント/オブジェクトであっても安全に使用できます。

コンシューマー(UUID で参照)の準備が整うと、ハンドラー (または実行可能) は「実行可能」になります。プロバイダーに加えてコンシューマーの準備ができているかどうかを確認する必要があります。これは、私の質問で述べたように、コンシューマー フラグメントは、プロバイダーが別のことを言ってもインスタンスの状態が保存されていると信じている可能性があるためです。コンシューマー (またはプロバイダー) の準備ができていない場合は、メッセージ (または実行可能) をプロバイダーのキューに入れます。

コンシューマーフラグメントが onResume() に到達すると、キューに入れられたメッセージ/ランナブルを消費する準備ができていることをプロバイダーに通知しますその時点で、プロバイダーは、準備が整ったばかりのコンシューマーに属するキュー内のすべての実行を試みることができます。

これにより、ハンドラーは常に有効なコンテキスト (プロバイダーによって参照されるアクティビティ) と最新の有効なフラグメント (別名「コンシューマー」) を使用して実行されます。

結論

ソリューションは非常に複雑ですが、実装方法を理解すれば問題なく機能します。誰かがより簡単な解決策を思いついたら、それを聞いてうれしいです.

于 2013-07-29T01:08:17.223 に答える