Bagaimana menangani pesan Handler saat aktivitas / fragmen dijeda

98

Sedikit variasi pada postingan saya yang lain

Pada dasarnya saya memiliki pesan Handlerdi saya Fragmentyang menerima banyak pesan yang dapat mengakibatkan dialog ditutup atau ditampilkan.

Ketika aplikasi diletakkan di latar belakang saya mendapatkan onPausetapi kemudian masih mendapatkan pesan saya datang seperti yang diharapkan. Namun, karena saya menggunakan fragmen, saya tidak bisa begitu saja mengabaikan dan menampilkan dialog karena itu akan menghasilkan file IllegalStateException.

Saya tidak bisa begitu saja mengabaikan atau membatalkan membiarkan kerugian negara.

Mengingat bahwa saya memiliki, Handlersaya bertanya-tanya apakah ada pendekatan yang disarankan tentang bagaimana saya harus menangani pesan saat dalam keadaan jeda.

Salah satu solusi yang mungkin saya pertimbangkan adalah merekam pesan yang masuk saat dijeda dan memutarnya kembali di onResume. Ini agak tidak memuaskan dan saya berpikir pasti ada sesuatu dalam kerangka kerja untuk menangani ini dengan lebih elegan.

PJL
sumber
1
Anda dapat menghapus semua pesan di handler dalam metode onPause () dari fragmen, tetapi ada masalah dalam memulihkan pesan yang menurut saya tidak mungkin dilakukan.
Yashwanth Kumar

Jawaban:

167

Meskipun sistem operasi Android tampaknya tidak memiliki mekanisme yang cukup untuk mengatasi masalah Anda, saya yakin pola ini memberikan solusi yang relatif sederhana untuk diterapkan.

Kelas berikut adalah pembungkus android.os.Handleryang mendukung pesan ketika sebuah aktivitas dijeda dan memutarnya kembali pada resume.

Pastikan kode apa pun yang Anda miliki yang secara asinkron mengubah status fragmen (mis. Komit, tutup) hanya dipanggil dari pesan di penangan.

Turunkan pawang Anda dari PauseHandlerkelas.

Setiap kali aktivitas Anda menerima onPause()panggilan PauseHandler.pause()dan onResume()panggilan PauseHandler.resume().

Gantilah implementasi Handler Anda handleMessage()dengan processMessage().

Memberikan implementasi sederhana storeMessage()yang selalu kembali 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);
        }
    }
}

Di bawah ini adalah contoh sederhana bagaimana PausedHandlerkelas dapat digunakan.

Dengan mengklik tombol, pesan tertunda dikirim ke penangan.

Saat penangan menerima pesan (di thread UI), ini akan menampilkan DialogFragment.

Jika PausedHandlerkelas tidak digunakan, IllegalStateException akan ditampilkan jika tombol beranda ditekan setelah menekan tombol uji untuk meluncurkan dialog.

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;
                }
            }
        }
    }
}

Saya telah menambahkan storeMessage()metode ke PausedHandlerkelas jika ada pesan yang harus segera diproses bahkan ketika aktivitas dihentikan sementara. Jika pesan ditangani maka false harus dikembalikan dan pesan tersebut akan dibuang.

quickdraw mcgraw
sumber
26
Solusi bagus, mujarab. Tidak dapat membantu berpikir bahwa kerangka kerja harus menangani ini.
PJL
1
bagaimana cara meneruskan callback ke DialogFragment?
Malachiasz
Saya tidak yakin bahwa saya memahami pertanyaan Malachiasz, bisakah Anda menjelaskannya.
quickdraw mcgraw
Ini adalah solusi yang sangat elegan! Kecuali saya salah, karena resumemetode yang digunakan secara sendMessage(msg)teknis mungkin ada utas lain yang mengantre pesan tepat sebelum (atau di antara iterasi loop), yang berarti bahwa pesan yang disimpan dapat disisipkan dengan pesan baru yang tiba. Tidak yakin apakah itu masalah besar. Mungkin menggunakan sendMessageAtFrontOfQueue(dan tentu saja melakukan iterasi ke belakang) akan menyelesaikan masalah ini?
yan
4
Saya pikir pendekatan ini mungkin tidak selalu berhasil - jika aktivitas dihancurkan oleh OS, daftar pesan yang menunggu proses akan kosong setelah dilanjutkan.
GaRRaPeTa
10

Versi PauseHandler yang sangat baik dari quickdraw yang sedikit lebih sederhana adalah

/**
 * 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);

}

Itu mengasumsikan bahwa Anda selalu ingin menyimpan pesan offline untuk diputar ulang. Dan memberikan Activity sebagai input #processMessagesagar Anda tidak perlu mengelolanya di sub class.

William
sumber
Mengapa Anda resume()dan pause(), dan handleMessage synchronized?
Maksim Dmitriev
5
Karena Anda tidak ingin #pause dipanggil selama #handleMessage dan tiba-tiba menemukan bahwa aktivitas tersebut null saat Anda menggunakannya di #handleMessage. Ini adalah sinkronisasi antar negara bagian.
William
@William Bisakah Anda menjelaskan lebih detailnya kepada saya mengapa Anda memerlukan sinkronisasi dalam kelas PauseHandler? Tampaknya kelas ini hanya berfungsi di satu utas, utas UI. Saya rasa #pause tidak dapat dipanggil selama #handleMessage karena keduanya berfungsi di thread UI.
Samik
@William apakah kamu yakin? HandlerThread handlerThread = new HandlerThread ("mHandlerNonMainThread"); handlerThread.start (); Looper looperNonMainThread = handlerThread.getLooper (); Handler handlerNonMainThread = new Handler (looperNonMainThread, new Callback () {public boolean handleMessage (Message msg) {return false;}});
swooby
Maaf @swooby saya tidak mengikuti. Apakah saya yakin tentang apa? Dan apa tujuan potongan kode yang Anda posting?
William
2

Berikut adalah cara yang sedikit berbeda untuk mendekati masalah melakukan Fragment yang dilakukan dalam fungsi callback dan menghindari masalah IllegalStateException.

Pertama buat antarmuka yang dapat dijalankan kustom.

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

Selanjutnya, buat fragmen untuk memproses objek MyRunnable. Jika objek MyRunnable dibuat setelah Aktivitas dihentikan sementara, misalnya jika layar diputar, atau pengguna menekan tombol beranda, itu akan dimasukkan ke dalam antrian untuk diproses nanti dengan konteks baru. Antrian bertahan dari perubahan konfigurasi apa pun karena instance setRetain disetel ke true. Metode runProtected dijalankan pada UI thread untuk menghindari kondisi balapan dengan tanda 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);
                }
            }
        });
    }
}

Akhirnya, fragmen dapat digunakan dalam aplikasi utama sebagai berikut:

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();
                ...
            }
         });
    }
}
Rua109
sumber
0

Dalam proyek saya, saya menggunakan pola desain pengamat untuk menyelesaikannya. Di Android, penerima siaran dan maksud adalah implementasi dari pola ini.

Apa yang saya lakukan adalah membuat BroadcastReceiver yang saya mendaftar di fragmen / kegiatan ini onResume dan unregister di fragmen / kegiatan ini onPause . Dalam BroadcastReceiver metode 's onReceive Aku meletakkan semua kode yang kebutuhan untuk menjalankan sebagai akibat dari - BroadcastReceiver yang - menerima Intent (pesan) yang dikirim ke aplikasi Anda secara umum. Untuk meningkatkan selektivitas pada tipe maksud apa yang bisa diterima fragmen Anda, Anda bisa menggunakan filter maksud seperti pada contoh di bawah ini.

Keuntungan dari pendekatan ini adalah Intent (pesan) bisa dikirim dari mana saja di dalam aplikasi Anda (dialog yang terbuka di atas fragmen Anda, tugas asinkron, fragmen lain, dll.). Parameter bahkan bisa diteruskan sebagai ekstra maksud.

Keuntungan lainnya adalah pendekatan ini kompatibel dengan versi Android API apa pun, karena BroadcastReceivers dan Intents telah diperkenalkan pada API level 1.

Anda tidak perlu menyiapkan izin khusus apa pun pada file manifes aplikasi Anda kecuali jika Anda berencana menggunakan sendStickyBroadcast (di mana Anda perlu menambahkan 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);
    }

}
dangel
sumber
3
Jika sendBroadcast () di notifyFragment () dipanggil selama status Jeda, unregisterReceiver () akan dipanggil dan oleh karena itu tidak ada penerima yang akan menangkap maksud tersebut. Tidakkah sistem Android akan membuang maksud jika tidak ada kode untuk segera menanganinya?
Steve B
Saya pikir posting tempel eventbus robot hijau seperti ini, keren.
j2emanue