アクティビティ/フラグメントが一時停止しているときにハンドラーメッセージを処理する方法


98

私の他の投稿のわずかな変化

基本的に私がメッセージを持ってHandler、私の中Fragmentのダイアログにつながることができ、メッセージの束を受け取り、却下または示されています。

アプリがバックグラウンドに置かれるとonPause、メッセージが表示されますが、期待どおりにメッセージが届きます。ただし、フラグメントを使用しているため、ダイアログが表示されなくなり、結果がになるため、ダイアログを表示することはできませんIllegalStateException

状態の損失を許可することを却下またはキャンセルすることはできません。

私が持っていることを考えるとHandler、私は私がしばらく一時停止状態でメッセージをどのように処理するかのように推奨されるアプローチがあるかどうかを疑問に思っています。

私が考えている解決策の1つは、一時停止中に届いたメッセージを録音し、で再生することonResumeです。これはやや不十分で、これをよりエレガントに処理するためにフレームワークに何かがあるに違いないと私は考えています。


1
フラグメントのonPause()メソッドでハンドラー内のすべてのメッセージを削除することはできますが、メッセージを復元できないという問題があります。
Yashwanth Kumar

回答:


167

Androidオペレーティングシステムは問題に十分に対処するメカニズムを備えていないようですが、このパターンは回避策を実装するのが比較的簡単だと思います。

次のクラスはandroid.os.Handler、アクティビティが一時停止されたときにメッセージをバッファし、再開時にメッセージを再生するラッパーです。

フラグメントの状態を非同期に変更するコード(コミット、破棄など)がハンドラーのメッセージからのみ呼び出されることを確認します。

PauseHandlerクラスからハンドラーを派生させます。

あなたの活動は、受信するたびにonPause()電話をPauseHandler.pause()してためのonResume()コールPauseHandler.resume()

ハンドラの実装を置き換えhandleMessage()processMessage()

storeMessage()常に返される単純な実装を提供しますtrue

/**
 * Message Handler class that supports buffering up of messages when the
 * activity is paused i.e. in the background.
 */
public abstract class PauseHandler extends Handler {

    /**
     * Message Queue Buffer
     */
    final Vector<Message> messageQueueBuffer = new Vector<Message>();

    /**
     * Flag indicating the pause state
     */
    private boolean paused;

    /**
     * Resume the handler
     */
    final public void resume() {
        paused = false;

        while (messageQueueBuffer.size() > 0) {
            final Message msg = messageQueueBuffer.elementAt(0);
            messageQueueBuffer.removeElementAt(0);
            sendMessage(msg);
        }
    }

    /**
     * Pause the handler
     */
    final public void pause() {
        paused = true;
    }

    /**
     * Notification that the message is about to be stored as the activity is
     * paused. If not handled the message will be saved and replayed when the
     * activity resumes.
     * 
     * @param message
     *            the message which optional can be handled
     * @return true if the message is to be stored
     */
    protected abstract boolean storeMessage(Message message);

    /**
     * Notification message to be processed. This will either be directly from
     * handleMessage or played back from a saved message when the activity was
     * paused.
     * 
     * @param message
     *            the message to be handled
     */
    protected abstract void processMessage(Message message);

    /** {@inheritDoc} */
    @Override
    final public void handleMessage(Message msg) {
        if (paused) {
            if (storeMessage(msg)) {
                Message msgCopy = new Message();
                msgCopy.copyFrom(msg);
                messageQueueBuffer.add(msgCopy);
            }
        } else {
            processMessage(msg);
        }
    }
}

以下は、PausedHandlerクラスの使用方法の簡単な例です。

ボタンをクリックすると、遅延メッセージがハンドラーに送信されます。

ハンドラーは(UIスレッドで)メッセージを受信すると、を表示しDialogFragmentます。

PausedHandlerクラスが使用されていなかった場合、テストボタンを押してダイアログを起動した後にホームボタンが押されると、IllegalStateExceptionが表示されます。

public class FragmentTestActivity extends Activity {

    /**
     * Used for "what" parameter to handler messages
     */
    final static int MSG_WHAT = ('F' << 16) + ('T' << 8) + 'A';
    final static int MSG_SHOW_DIALOG = 1;

    int value = 1;

    final static class State extends Fragment {

        static final String TAG = "State";
        /**
         * Handler for this activity
         */
        public ConcreteTestHandler handler = new ConcreteTestHandler();

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setRetainInstance(true);            
        }

        @Override
        public void onResume() {
            super.onResume();

            handler.setActivity(getActivity());
            handler.resume();
        }

        @Override
        public void onPause() {
            super.onPause();

            handler.pause();
        }

        public void onDestroy() {
            super.onDestroy();
            handler.setActivity(null);
        }
    }

    /**
     * 2 second delay
     */
    final static int DELAY = 2000;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        if (savedInstanceState == null) {
            final Fragment state = new State();
            final FragmentManager fm = getFragmentManager();
            final FragmentTransaction ft = fm.beginTransaction();
            ft.add(state, State.TAG);
            ft.commit();
        }

        final Button button = (Button) findViewById(R.id.popup);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                final FragmentManager fm = getFragmentManager();
                State fragment = (State) fm.findFragmentByTag(State.TAG);
                if (fragment != null) {
                    // Send a message with a delay onto the message looper
                    fragment.handler.sendMessageDelayed(
                            fragment.handler.obtainMessage(MSG_WHAT, MSG_SHOW_DIALOG, value++),
                            DELAY);
                }
            }
        });
    }

    public void onSaveInstanceState(Bundle bundle) {
        super.onSaveInstanceState(bundle);
    }

    /**
     * Simple test dialog fragment
     */
    public static class TestDialog extends DialogFragment {

        int value;

        /**
         * Fragment Tag
         */
        final static String TAG = "TestDialog";

        public TestDialog() {
        }

        public TestDialog(int value) {
            this.value = value;
        }

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            final View inflatedView = inflater.inflate(R.layout.dialog, container, false);
            TextView text = (TextView) inflatedView.findViewById(R.id.count);
            text.setText(getString(R.string.count, value));
            return inflatedView;
        }
    }

    /**
     * Message Handler class that supports buffering up of messages when the
     * activity is paused i.e. in the background.
     */
    static class ConcreteTestHandler extends PauseHandler {

        /**
         * Activity instance
         */
        protected Activity activity;

        /**
         * Set the activity associated with the handler
         * 
         * @param activity
         *            the activity to set
         */
        final void setActivity(Activity activity) {
            this.activity = activity;
        }

        @Override
        final protected boolean storeMessage(Message message) {
            // All messages are stored by default
            return true;
        };

        @Override
        final protected void processMessage(Message msg) {

            final Activity activity = this.activity;
            if (activity != null) {
                switch (msg.what) {

                case MSG_WHAT:
                    switch (msg.arg1) {
                    case MSG_SHOW_DIALOG:
                        final FragmentManager fm = activity.getFragmentManager();
                        final TestDialog dialog = new TestDialog(msg.arg2);

                        // We are on the UI thread so display the dialog
                        // fragment
                        dialog.show(fm, TestDialog.TAG);
                        break;
                    }
                    break;
                }
            }
        }
    }
}

アクティビティが一時停止されている場合でもメッセージをすぐに処理する必要がある場合に備えstoreMessage()て、PausedHandlerクラスにメソッドを追加しました。メッセージが処理される場合、falseが返され、メッセージは破棄されます。


26
素敵な解決策は、御馳走を働かせます。フレームワークがこれを処理する必要があるとはいえ、考えずにはいられません。
PJL

1
DialogFragmentにコールバックを渡す方法は?
Malachiasz

Malachiaszの質問が理解できるかわかりません。詳しく説明してください。
quickdraw mcgraw 2014年

これは非常にエレガントなソリューションです!私が間違っていない限り、resumeメソッドはsendMessage(msg)技術的に使用するため、直前(またはループの反復の間)にメッセージをキューに入れる他のスレッドが存在する可能性があります。大したことかどうかわからない。たぶん使用してsendMessageAtFrontOfQueue(そしてもちろん逆に反復して)この問題を解決するでしょうか?
yan

4
私はこのアプローチが常に機能するとは限らないと思います-アクティビティがOSによって破棄された場合、プロセスの保留中のメッセージのリストは再開後に空になります。
GaRRaPeTa

10

quickdrawの優れたPauseHandlerのやや単純なバージョンは次のとおりです。

/**
 * Message Handler class that supports buffering up of messages when the activity is paused i.e. in the background.
 */
public abstract class PauseHandler extends Handler {

    /**
     * Message Queue Buffer
     */
    private final List<Message> messageQueueBuffer = Collections.synchronizedList(new ArrayList<Message>());

    /**
     * Flag indicating the pause state
     */
    private Activity activity;

    /**
     * Resume the handler.
     */
    public final synchronized void resume(Activity activity) {
        this.activity = activity;

        while (messageQueueBuffer.size() > 0) {
            final Message msg = messageQueueBuffer.get(0);
            messageQueueBuffer.remove(0);
            sendMessage(msg);
        }
    }

    /**
     * Pause the handler.
     */
    public final synchronized void pause() {
        activity = null;
    }

    /**
     * Store the message if we have been paused, otherwise handle it now.
     *
     * @param msg   Message to handle.
     */
    @Override
    public final synchronized void handleMessage(Message msg) {
        if (activity == null) {
            final Message msgCopy = new Message();
            msgCopy.copyFrom(msg);
            messageQueueBuffer.add(msgCopy);
        } else {
            processMessage(activity, msg);
        }
    }

    /**
     * Notification message to be processed. This will either be directly from
     * handleMessage or played back from a saved message when the activity was
     * paused.
     *
     * @param activity  Activity owning this Handler that isn't currently paused.
     * @param message   Message to be handled
     */
    protected abstract void processMessage(Activity activity, Message message);

}

常に再生のためにオフラインメッセージを保存することを想定しています。また、アクティビティを入力として提供する#processMessagesため、サブクラスでアクティビティを管理する必要はありません。


なぜあなたのresume()and pause()、そしてhandleMessage synchronized
Maksim Dmitriev

5
#handleMessage中に#pauseが呼び出されたくないので、#handleMessageでアクティビティを使用しているときに、アクティビティが突然nullであることがわかります。共有状態全体の同期です。
ウィリアム

@William PauseHandlerクラスで同期が必要な理由を詳しく説明してください。このクラスは1つのスレッド(UIスレッド)でのみ機能するようです。#handleMessageの実行中に#pauseを呼び出すことができなかったのは、どちらもUIスレッドで機能するためです。
Samik

@ウィリアムよろしいですか?HandlerThread handlerThread = new HandlerThread( "mHandlerNonMainThread"); handlerThread.start(); ルーパーlooperNonMainThread = handlerThread.getLooper(); ハンドラーhandlerNonMainThread = new Handler(looperNonMainThread、new Callback(){public boolean handleMessage(Message msg){return false;}});
2016

申し訳ありませんが@swooby従いません。私は何を確信していますか?そして、あなたが投稿したコードスニペットの目的は何ですか?
ウィリアム

2

以下は、コールバック関数でフラグメントコミットを実行し、IllegalStateExceptionの問題を回避するという問題に取り組むための少し異なる方法です。

まず、カスタムの実行可能なインターフェースを作成します。

public interface MyRunnable {
    void run(AppCompatActivity context);
}

次に、MyRunnableオブジェクトを処理するためのフラグメントを作成します。MyRunnableオブジェクトがアクティビティの一時停止後に作成された場合(画面が回転した場合、またはユーザーがホームボタンを押した場合など)は、新しいコンテキストで後で処理するためにキューに入れられます。setRetainインスタンスがtrueに設定されているため、キューは構成の変更後も存続します。runProtectedメソッドは、UIスレッドで実行され、isPausedフラグによる競合状態を回避します。

public class PauseHandlerFragment extends Fragment {

    private AppCompatActivity context;
    private boolean isPaused = true;
    private Vector<MyRunnable> buffer = new Vector<>();

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        this.context = (AppCompatActivity)context;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }

    @Override
    public void onPause() {
        isPaused = true;
        super.onPause();
    }

    @Override
    public void onResume() {
        isPaused = false;
        playback();
        super.onResume();
    }

    private void playback() {
        while (buffer.size() > 0) {
            final MyRunnable runnable = buffer.elementAt(0);
            buffer.removeElementAt(0);
            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    //execute run block, providing new context, incase 
                    //Android re-creates the parent activity
                    runnable.run(context);
                }
            });
        }
    }
    public final void runProtected(final MyRunnable runnable) {
        context.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if(isPaused) {
                    buffer.add(runnable);
                } else {
                    runnable.run(context);
                }
            }
        });
    }
}

最後に、フラグメントは次のようにメインアプリケーションで使用できます。

public class SomeActivity extends AppCompatActivity implements SomeListener {
    PauseHandlerFragment mPauseHandlerFragment;

    static class Storyboard {
        public static String PAUSE_HANDLER_FRAGMENT_TAG = "phft";
    }

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        ...

        //register pause handler 
        FragmentManager fm = getSupportFragmentManager();
        mPauseHandlerFragment = (PauseHandlerFragment) fm.
            findFragmentByTag(Storyboard.PAUSE_HANDLER_FRAGMENT_TAG);
        if(mPauseHandlerFragment == null) {
            mPauseHandlerFragment = new PauseHandlerFragment();
            fm.beginTransaction()
                .add(mPauseHandlerFragment, Storyboard.PAUSE_HANDLER_FRAGMENT_TAG)
                .commit();
        }

    }

    // part of SomeListener interface
    public void OnCallback(final String data) {
        mPauseHandlerFragment.runProtected(new MyRunnable() {
            @Override
            public void run(AppCompatActivity context) {
                //this block of code should be protected from IllegalStateException
                FragmentManager fm = context.getSupportFragmentManager();
                ...
            }
         });
    }
}

0

私のプロジェクトでは、オブザーバーのデザインパターンを使用してこれを解決しています。Androidでは、ブロードキャストレシーバーとインテントはこのパターンの実装です。

私は何をして作成されBroadcastReceiverフラグメントの/活動の中で私を登録onResumeフラグメントの/活動の中や登録解除onPauseを。でBroadcastReceiverの方法onReceive BroadcastReceiver - -テント(メッセージ)を受信一般的に、あなたのアプリに送られた私は、ニーズが結果として実行することを、すべてのコードを置きます。フラグメントが受信できるインテントのタイプの選択性を高めるには、以下の例のようにインテントフィルター使用できます。

このアプローチの利点は、インテント(メッセージ)をアプリ内のどこからでも送信できることです(フラグメントの上に開いたダイアログ、非同期タスク、別のフラグメントなど)。パラメータはインテントエクストラとして渡すこともできます。

BroadcastReceiversとIntentsがAPIレベル1で導入されているため、このアプローチはAndroid APIバージョンと互換性があるというもう1つの利点があります。

sendStickyBroadcastを使用する場合(BROADCAST_STICKYを追加する必要がある場合)を除き、アプリのマニフェストファイルに特別な権限を設定する必要はありません。

public class MyFragment extends Fragment { 

    public static final String INTENT_FILTER = "gr.tasos.myfragment.refresh";

    private BroadcastReceiver mReceiver = new BroadcastReceiver() {

        // this always runs in UI Thread 
        @Override
        public void onReceive(Context context, Intent intent) {
            // your UI related code here

            // you can receiver data login with the intent as below
            boolean parameter = intent.getExtras().getBoolean("parameter");
        }
    };

    public void onResume() {
        super.onResume();
        getActivity().registerReceiver(mReceiver, new IntentFilter(INTENT_FILTER));

    };

    @Override
    public void onPause() {
        getActivity().unregisterReceiver(mReceiver);
        super.onPause();
    }

    // send a broadcast that will be "caught" once the receiver is up
    protected void notifyFragment() {
        Intent intent = new Intent(SelectCategoryFragment.INTENT_FILTER);
        // you can send data to receiver as intent extras
        intent.putExtra("parameter", true);
        getActivity().sendBroadcast(intent);
    }

}

3
notifyFragment()内のsendBroadcast()がPause状態の間に呼び出された場合、unregisterReceiver()はすでに呼び出されているため、その意図をキャッチするレシーバーはありません。Androidシステムは、すぐに処理するコードがない場合、インテントを破棄しませんか?
スティーブB

緑のロボットのイベントバスの付箋はこんな感じでクールだと思います。
j2emanue 2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.