วิธีจัดการข้อความตัวจัดการเมื่อหยุดกิจกรรม / ส่วนย่อยชั่วคราว


99

การเปลี่ยนแปลงเล็กน้อยในโพสต์อื่น ๆของฉัน

โดยทั่วไปฉันมีข้อความHandlerในของฉันFragmentที่ได้รับพวงของข้อความที่จะส่งผลในไดอะล็อกถูกไล่ออกหรือแสดง

เมื่อแอปถูกวางลงในพื้นหลังฉันจะได้รับonPauseแต่ก็ยังได้รับข้อความของฉันตามที่คาดหวัง อย่างไรก็ตามเนื่องจากฉันใช้ชิ้นส่วนฉันไม่สามารถปิดและแสดงกล่องโต้ตอบได้เนื่องจากจะทำให้เกิดIllegalStateExceptionไฟล์.

ฉันไม่สามารถยกเลิกหรือยกเลิกการอนุญาตให้สูญเสียสถานะได้

เนื่องจากฉันมีข้อHandlerสงสัยว่ามีแนวทางที่แนะนำเกี่ยวกับวิธีจัดการข้อความในขณะที่อยู่ในสถานะหยุดชั่วคราวหรือไม่

วิธีแก้ปัญหาที่เป็นไปได้วิธีหนึ่งที่ฉันกำลังพิจารณาคือบันทึกข้อความที่ผ่านเข้ามาในขณะที่หยุดชั่วคราวและเล่นกลับใน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ชั้นเรียนแล้วในกรณีที่ข้อความใด ๆ ควรได้รับการประมวลผลทันทีแม้ว่ากิจกรรมจะหยุดชั่วคราว หากมีการจัดการข้อความแล้วควรส่งคืนเท็จและข้อความจะถูกทิ้ง


26
ทางออกที่ดีใช้งานได้ดี อดไม่ได้ที่จะคิดว่ากรอบควรจะจัดการเรื่องนี้
PJL

1
จะส่งการโทรกลับไปยัง DialogFragment ได้อย่างไร
Malachiasz

ฉันไม่แน่ใจว่าฉันเข้าใจคำถาม Malachiasz โปรดอธิบายให้ละเอียด
Quickdraw mcgraw

นี่เป็นวิธีการแก้ปัญหาที่หรูหรามาก! เว้นแต่ว่าฉันจะผิดเพราะresumeวิธีนี้ใช้ในsendMessage(msg)ทางเทคนิคอาจมีเธรดอื่น ๆ กำลังจัดคิวข้อความอยู่ก่อนหน้า (หรืออยู่ระหว่างการวนซ้ำของลูป) ซึ่งหมายความว่าข้อความที่จัดเก็บอาจถูกแทรกระหว่างข้อความใหม่ที่มาถึง ไม่แน่ใจว่าเป็นเรื่องใหญ่ บางทีการใช้sendMessageAtFrontOfQueue(และแน่นอนว่าการย้อนกลับ) จะช่วยแก้ปัญหานี้ได้หรือไม่
ยัน

4
ฉันคิดว่าวิธีนี้อาจไม่ได้ผลเสมอไป - หากระบบปฏิบัติการถูกทำลายรายการข้อความที่รอดำเนินการจะว่างเปล่าหลังจากดำเนินการต่อ
GaRRaPeTa

10

PauseHandler ที่ยอดเยี่ยมของ Quickdraw เวอร์ชันที่ง่ายกว่าเล็กน้อยคือ

/**
 * 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()และpause()และhandleMessage synchronized?
Maksim Dmitriev

5
เนื่องจากคุณไม่ต้องการให้ #pause ถูกเรียกระหว่าง #handleMessage และทันใดนั้นพบว่ากิจกรรมนั้นเป็นโมฆะในขณะที่คุณใช้งานใน #handleMessage เป็นการซิงโครไนซ์ระหว่างสถานะที่ใช้ร่วมกัน
William

@ วิลเลียมคุณช่วยอธิบายรายละเอียดเพิ่มเติมได้ไหมว่าทำไมคุณถึงต้องการซิงโครไนซ์ในคลาส PauseHandler ดูเหมือนว่าคลาสนี้จะทำงานในเธรด UI เธรดเดียวเท่านั้น ฉันเดาว่าไม่สามารถเรียก #pause ระหว่าง #handleMessage ได้เพราะทั้งสองอย่างทำงานในเธรด UI
Samik

@ วิลเลียมแน่ใจเหรอ HandlerThread handlerThread = HandlerThread ใหม่ ("mHandlerNonMainThread"); handlerThread.start (); Looper looperNonMainThread = handlerThread.getLooper (); Handler handlerNonMainThread = ตัวจัดการใหม่ (looperNonMainThread, callback ใหม่ () {public boolean handleMessage (Message msg) (return false;}});
swooby

ขออภัย @swooby ฉันไม่ได้ติดตาม ฉันแน่ใจเกี่ยวกับอะไร? และวัตถุประสงค์ของข้อมูลโค้ดที่คุณโพสต์คืออะไร?
William

2

นี่คือวิธีที่แตกต่างกันเล็กน้อยในการแก้ไขปัญหาการทำ Fragment commits ในฟังก์ชันเรียกกลับและหลีกเลี่ยงปัญหา 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วิธี 's onReceiveฉันใส่รหัสทุกสิ่งที่ตอบสนองความต้องการที่จะทำงานเป็นผลมาจาก - BroadcastReceiver - การได้รับการแสดงเจตจำนง (ข้อความ) ที่ถูกส่งไปยังแอปทั่วไป หากต้องการเพิ่มความสามารถในการเลือกประเภทของ Intent ที่ชิ้นส่วนของคุณสามารถรับได้คุณสามารถใช้ตัวกรองความตั้งใจดังตัวอย่างด้านล่าง

ข้อดีของวิธีนี้คือIntent (ข้อความ) สามารถส่งได้จากทุกที่ในแอปของคุณ (กล่องโต้ตอบที่เปิดอยู่ด้านบนของแฟรกเมนต์ของคุณงาน async ส่วนอื่นเป็นต้น) พารามิเตอร์สามารถส่งผ่านเป็นส่วนเสริมเจตนา

ข้อดีอีกอย่างคือแนวทางนี้เข้ากันได้กับ Android API ทุกเวอร์ชันเนื่องจาก BroadcastReceivers และ Intents ได้รับการแนะนำในระดับ API 1

คุณไม่จำเป็นต้องตั้งค่าการอนุญาตพิเศษใด ๆ ในไฟล์ Manifest ของแอปยกเว้นว่าคุณวางแผนที่จะใช้ 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
ถ้า sendBroadcast () ใน informFragment () ถูกเรียกในระหว่างสถานะ Pause จะมีการเรียก unregisterReceiver () ไปแล้วและจะไม่มีผู้รับรายใดจับเจตนานั้นได้ ระบบ Android จะไม่ยกเลิกเจตนาหากไม่มีรหัสให้จัดการทันทีหรือไม่?
Steve B

ฉันคิดว่าโพสต์เหนียว ๆ ของอีเวนต์บัสหุ่นยนต์สีเขียวเป็นแบบนี้เจ๋งดี
j2emanue
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.