commitAllowingStateLoss() and commit() fragment

前端 未结 5 952
陌清茗
陌清茗 2021-02-04 03:47

I want commit a fragment after network background operation. I was calling commit() after successful network operation but in case activity goes to pause or stop state it was cr

相关标签:
5条回答
  • 2021-02-04 03:59

    I'd like to add informations to Aritra Roy (so far i readed, it's a really good answer).

    I encountered the problem before, and i found that the main problem is that you are trying to make some async operations (HTTP, computations, ...) in another thread, wich is a good pratice, but you must inform your user AFTER receiving answers.

    The main problem is that as it is async operations, there is no guarantee that the user is still on your activity/app anymore. And if he went away, there is no need to do UI changes. Moreover, as android may kill your app/activity for memory issues, you have no guarantees to be able to get your answer, and save it to be restored. The problem is not only "the user can open another app" but "my activity can be recreated from configuration change" and you may be trying to do UI changes during activity recreation which would be really, really bad.

    Using "commitAllowingStateLoss" is like saying "i don't care if the UI is not really in the good state". You can do it for little things (like activate a gif saying your download ended)... That's not a big issue, and this problem is not really worth dealing with it as "in general" the user will stay on your app.

    But, the user did something, you're trying to get informations on the web, information is ready, and you have to show it when the user resume the app... the main word is "resume".

    You must gather the data you needed into a variable (and if you can, a parcelable or primitive variable), and then, override your "onResume" or "onPostResume"(for activities) functions in the following way.

    public void onResume/onPostResume() {
        super.onResume/onPostResume();
        if(someTreatmentIsPending) {
            /*do what you need to do with your variable here : fragment 
            transactions, dialog showing...*/
        }
    }
    

    Additional informations : This topic and especially @jed answer, and @pjv, @Sufian comments to it. This blog in order to understand why the bug occurs, and why proposed/accepted answers work.

    Last word : Just in case you wonder "why using a service is better than asyncTask". For what i understood, that's not really better. The main difference is that using service properly allow you to register/unregister handlers when your activity is paused/resumed. Therefore, you always get your answers when your activity is active, preventing the bug to occurs.

    Notice that's not because the bug does not occurs that you are safe. If you made changes directly on your views, there is no fragmentTransactions involved, therefore, no guarantee that the change will be retained and recreated when the app is recreated, resumed, relaunched, or anything else.

    0 讨论(0)
  • 2021-02-04 04:05

    commit Schedules a commit of this transaction. The commit does not happen immediately; it will be scheduled as work on the main thread to be done the next time that thread is ready.

    A transaction can only be committed with this method prior to its containing activity saving its state. If the commit is attempted after that point, an exception will be thrown. This is because the state after the commit can be lost if the activity needs to be restored from its state.

    commitAllowingStateLoss

    Like commit() but allows the commit to be executed after an activity's state is saved. This is dangerous because the commit can be lost if the activity needs to later be restored from its state, so this should only be used for cases where it is okay for the UI state to change unexpectedly on the user.

    And According to your question you are using AsyncTask for network background operation. So this might be a problem that you are commiting fragment inside its callback method. To avoid this exception just dont commit inside asynctask callbacks methods. This is not a solution but precaution.

    0 讨论(0)
  • 2021-02-04 04:12

    Simple way would be waiting for Activity to resume, so you can commit your action, a simple workaround would look something like this:

    @Override
    public void onNetworkResponse(){
           //Move to next fragmnt if Activity is Started or Resumed
           shouldMove = true;
           if (isResumed()){
                moveToNext = false;
    
                //Move to Next Page
                getActivity().getSupportFragmentManager()
                        .beginTransaction()
                        .replace(R.id.fragment_container, new NextFragment())
                        .addToBackStack(null)
                        .commit();
           }
    }
    

    So if Fragment is resumed (Hence Activity) you can commit your action but if not you will wait for Activity to start to commit your action:

    @Override
    public void onResume() {
        super.onResume();
    
        if(moveToNext){
            moveToNext = false;
    
            //Move to Next Page
            getActivity().getSupportFragmentManager()
                    .beginTransaction()
                    .replace(R.id.fragment_container, new NextFragment())
                    .addToBackStack(null)
                    .commit();
        }
    }
    

    P.S: Pay attention to moveToNext = false; It is there to ensure that after commit you won't repeat commit in case of coming back using back press..

    0 讨论(0)
  • 2021-02-04 04:13

    Well I came across the same problem and found a very simple workaround. Since android os doesn't have any solution that time ( Still don't have a good one though commitAllowingStateLoss() is one of their solution, and you know the rest).

    The solution was to write a Handler class that buffers the messages when activity is pass and plays them on onResume again.

    By using this class, Make sure you all the code which asynchronously changing fragment state ( commit etc) is called from a message in this `handler.

    ExtendFragmenntPauseHandler from handle class.

    Whenever your activity receives an onPause() call FragmenntPauseHandler.pause() and for onResume() call FragmenntPauseHandler.resume().

    Replace your implementation of the Handler handleMessage() with processMessage().

    Provide a simple implementation of storeMessage() which always returns true.

    /**
     * Message Handler class that supports buffering up of messages when the
     * activity is paused i.e. in the background.
     */
    public abstract class FragmenntPauseHandler 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);
            }
        }
    }
    

    Below is a simple example of how the PausedHandler class can be used.

    On the click of a Button a delayed message is sent to the handler.

    When the handler receives the message (on the UI thread) it displays a DialogFragment.

    If the FragmenntPauseHandler class was not being used an IllegalStateException would be shown if the home button was pressed after pressing the test button to launch the 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 FragmenntPauseHandler {
    
            /**
             * 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;
                    }
                }
            }
        }
    }
    

    I've added a storeMessage() method to the FragmenntPauseHandler class in case any messages should be processed immediately even when the activity is paused. If a message is handled then false should be returned and the message will be discarded.Hope it helps. I used this one in 4 apps and never had the same issue again.

    0 讨论(0)
  • 2021-02-04 04:17

    Just check whether activity is finishing or no, and then commit() the transaction.

    if (!isFinishing()) {
        // commit transaction
    }
    

    The exception you get is in a scenario, when activity is being finished, and you are trying to commit a new transaction, which obviously won't be saved by FragmentManager because the onSavedInstanceState() has already been executed. That's why framework forces you to implement it "correctly".

    0 讨论(0)
提交回复
热议问题