The Best Solution for Android Screen Rotation Processing

1. Overview (Hongyang)

It is well known that when Activity does not specify the orientation of the screen and configChanges explicitly, the user will restart the screen when he rotates it. Of course, in response to this situation, Android offers several options:

A. If there is a small amount of data, it can be saved and restored by onSaveInstanceState() and onRestoreInstanceState().

Android calls the onSaveInstanceState() method before destroying your Activity, so you can store data about the application state in this method. Then you can restore in onCreate() or onRestoreInstanceState() methods.

b. If there is a large amount of data, use Fragment s to keep objects that need to be recovered.

c. List file setting config change property

2. Difficulties

Assuming that the current Activity starts an asynchronous thread in onCreate to clip data, of course, in order to give users a good experience, there will be a ProgressDialog. When the data is loaded, the ProgressDialog disappears and sets the data.

Here, if the asynchronous data is loaded and the screen is rotated, it will not be difficult to use the above two methods, a and b, but to save and restore the data.

However, if you rotate while the thread is loading, you will have the following problems:

(a) If the data is not loaded at this time, the onCreate restart will start the thread again; the last thread may still be running, and it may update the control that no longer exists, resulting in errors.

b) The code to close ProgressDialog is in the onPostExecutez of the thread, but if the last thread has been killed, it cannot close the previous ProgressDialog.

c) Google officials do not recommend using Progress Dialog, here we will use the official recommendation of Dialog Fragment to create my loading box, if you do not understand: see Android official recommendation: Dialog Fragment Creation Dialog. In fact, this brings us a big problem. Dialog Fragment says fragment is bound to the life cycle of the current Activity. We rotate the screen and destroy the Activity. Of course, it will also affect the Dialog Fragment.

I will use a few examples below, using the above three ways, and how best to solve the above problems.

a. Data preservation and recovery using onSaveInstanceState() and onRestoreInstanceState()

Code:

public class saveInstanceActivity extends ListActivity {
    //Obtained data sources

    private ArrayList<String> data;
    private LoadingDialog loadingDialog;
    private ArrayAdapter<String> mAdapter;

    /****
     * The onsave Instances Tate () and onRestore Save Instance () methods can be used to save and read data without considering the rotation when the network is loaded.
     *
     * Life cycle: onsave Instance State - -- ondestroy () - - oncreate () - - onRestore Save Instance ()
     *
     * If you rotate when loading, an error occurs and NullPointException occurs when an exception exits (dialog.dismiss(), because the Fragment Manager bound to the current dialog box is null.
     * @param savedInstanceState
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_save_instance);
        initData(savedInstanceState);
    }

    private void initData(Bundle savedInstanceState) {
        //Have saved data;
        if (savedInstanceState!=null){


            //Request data operation indicates that there is data read directly in it

            data = savedInstanceState.getStringArrayList("mDatas");


        }

        if (data==null){


            //Load data first in

            loadingDialog = new LoadingDialog();


            loadingDialog.show(getFragmentManager(),"Loading");
            deAsyncTask myAsyncTask = new deAsyncTask();
            myAsyncTask.execute();


        }else{
           initAdapter();

        }
    }

    /***
     * Data preservation before destruction
     * @param outState
     */
    @Override
    protected void onSaveInstanceState(Bundle outState) {

        outState.putSerializable("mDatas",data);
    }

    /***
     * Asynchronous operation
     */
    class deAsyncTask extends AsyncTask<Void,Void,Void>{

       //Operations performed by sub-threads
        @Override
        protected Void doInBackground(Void... parmas) {
            //Loading data for code imitation takes time
            data = requestData();
            return null;
        }



        //Return to the main line after execution.
        @Override
        protected void onPostExecute(Void aVoid) {
            loadingDialog.dismiss();
            initAdapter();
        }
    }

    private void initAdapter() {

        mAdapter = new ArrayAdapter<>(

                this,
                android.R.layout.simple_list_item_1, data);
        setListAdapter(mAdapter);
    }

    private ArrayList<String> requestData() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return new ArrayList<String>(Arrays.asList("adopt Fragment Keep a large amount of data",
                "onSaveInstanceState Save data",
                "getLastNonConfigurationInstance Has been discarded", "RabbitMQ", "Hadoop",
                "Spark"));

    }


}


The interface is a ListView, which starts an asynchronous task in onCreate to load data. Thread.sleep is used to simulate a time-consuming operation. When the user restarts the rotating screen, onSave Instance State stores the data and restores the data in onCreate, eliminating unnecessary additional loading. All over.

Operation results:

When the normal loading data is completed, users continue to rotate the screen and the log will continue to type: onSave Instance State - > onDestroy - > onCreate - > onRestore Instance State. Verify that we did reboot, but we did not load the data again.

If you rotate while loading, an error will occur and an exception exit will occur (dialog.dismiss() because the Fragment Manager bound to the current dialog box is null and the interested one can go to Debug, which is not the key).

 

b. Use Fragment s to save objects for data recovery

If restarting your Activity requires restoring a large amount of data, rebuilding a network connection, or performing other intensive operations, it may be a slow user experience to restart completely because of configuration changes. Also, it may be unrealistic to use Bundle to completely restore your Activity status in the onSaveIntanceState() callback provided by the system (Bundle is not designed to carry large amounts of data (such as bitmap), and the data in Bundle must be serialized and deserialized), which consumes a lot of energy. Memory and causes slow configuration changes. In this case, when your Active restart due to configuration changes, you can ease the burden of restart by maintaining a Fragment. This fragment can contain references to objects that you want to keep stateful.

When Android shuts down your Activity due to configuration changes, the identified fragments in your Activity will not be destroyed. You can add such fragements to your Activity to save stateful objects.

Save stateful objects in Fragment s when the runtime configuration changes
a) Inherit Fragment and declare that the reference points to your stateful object
b) Call setRetainInstance(boolean) when Fragment is created
c) Add Fragment instances to Activity
d) When Activity restarts, use Fragment Manager to restore Fragment
Code:

public class retainFragment extends Fragment {
    public Bitmap getBtmap() {
        return btmap;
    }

    public void setBtmap(Bitmap btmap) {
        this.btmap = btmap;
    }

    //Instance references to be saved
    private Bitmap  btmap;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //Save the current fragment
        setRetainInstance(true);
    }





}

Simple, just declare the data object you need to save, and then provide getter and setter. Note that you must call setRetainInstance(true) in onCreate.

 

Then: Fragment Retain Data Activity

 

public class FragmentRetainDataActivity extends Activity
{

	private static final String TAG = "FragmentRetainDataActivity";
	private retainFragment dataFragment;
	private DialogFragment mLoadingDialog;
	private ImageView mImageView;
	private Bitmap mBitmap;

	@SuppressLint("LongLogTag")
	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		Log.e(TAG, "onCreate");

		// find the retained fragment on activity restarts
		//Find the saved fragment instance and open it again

		FragmentManager fm = getFragmentManager();
		dataFragment= (retainFragment) fm.findFragmentByTag("data");
		// Create fragment for the first time
		if (dataFragment == null)
		{
			// add the fragment
			dataFragment = new retainFragment();
			fm.beginTransaction().add(dataFragment,"data").commit();

		}
		mBitmap = collectMyLoadedData();
		initData();

		// the data is available in dataFragment.getData()
	}

	/**
	 * Initialization data
	 */
	private void initData()
	{
		mImageView = (ImageView) findViewById(R.id.id_imageView);
		if (mBitmap == null)
		{
			mLoadingDialog = new LoadingDialog();
			mLoadingDialog.show(getFragmentManager(), "LOADING_DIALOG");
			RequestQueue newRequestQueue = Volley
					.newRequestQueue(FragmentRetainDataActivity.this);
			ImageRequest imageRequest = new ImageRequest(
					"https://c-ssl.duitang.com/uploads/item/201410/11/20141011040700_3iXsj.thumb.1200_0.jpeg",
					new Response.Listener<Bitmap>()
					{
						@Override
						public void onResponse(Bitmap response)
						{
							mBitmap = response;
							mImageView.setImageBitmap(mBitmap);
							// load the data from the web
							dataFragment.setBtmap(mBitmap);
							mLoadingDialog.dismiss();
						}
					}, 0, 0, Config.RGB_565, null);
			newRequestQueue.add(imageRequest);
		} else
		{
			mImageView.setImageBitmap(mBitmap);
		}

	}

	/***
	 * Store data in fragment s when the interface is destroyed
	 */
	@SuppressLint("LongLogTag")
	@Override
	public void onDestroy()
	{
		Log.e(TAG, "onDestroy");
		super.onDestroy();
		// store the data in the fragment
		dataFragment.setBtmap(mBitmap);
	}

	private Bitmap collectMyLoadedData()
	{
		return dataFragment.getBtmap();
	}

}

In onCreate, Volley is always used to load a beautiful woman's photo, then Bitmap is stored in onDestroy, a reference to Fragment is added or restored in onCreate, and Bitmap is read and set. This method is suitable for large data storage and recovery.

c. Configure configChanges to handle changes in screen rotation

Property setting in menifest:

    <activity
            android:name=".ConfigChangesTestActivity"
            android:configChanges="screenSize|orientation" >
        </activity>


The lower version of API only needs orientation, while the higher version needs screenSize.

 

Best Practice of Rotating Screen

Now I want to start today's difficult point, which is to deal with what I said at the beginning of the article, when the asynchronous task is executed, rotate, if the above problem is solved.

First of all, let's talk about the process of exploration.

Initially, I thought that rotation would just start the thread again and not cause an exception. I just had to close the last asynchronous task in onDestroy. In fact, if I close it, the last dialog will always exist; if I don't close it, the activity will be destroyed, and the dismiss of the dialog will be abnormal. It's really painful, and even if the dialog box is closed, the task is closed; user rotation still causes the task to be recreated and loaded from scratch.

We hope to have a solution: when loading data, we rotate the screen without interrupting the loading task, and for users, the waiting box will display normally before loading is completed:

Of course, we also use Fragment for data preservation, which is officially recommended after all:

OtherRetainedFragment
 

/**
 * Save Fragment of a Heterogeneous Task Object
 * 
 * @author zhy
 * 
 */
public class OtherRetainedFragment extends Fragment
{

	// data object we want to retain
	// Save an asynchronous task
	private MyAsyncTask data;

	// this method is only called once for this fragment
	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		// retain this fragment
		setRetainInstance(true);
	}

	public void setData(MyAsyncTask data)
	{
		this.data = data;
	}

	public MyAsyncTask getData()
	{
		return data;
	}

	
}

The only difference is that it saves an asynchronous task for object programming. I believe that, as you can see, we already know one of the core problems mentioned above frequently. Save an asynchronous task and continue the task on reboot.

 

 

public class MyAsyncTask extends AsyncTask<Void, Void, Void>
{
	private FixProblemsActivity activity;
	/**
	 * Completion or not
	 */
	private boolean isCompleted;
	/**
	 * Schedule box
	 */
	private LoadingDialog mLoadingDialog;
	private List<String> items;

	public MyAsyncTask(FixProblemsActivity activity)
	{
		this.activity = activity;
	}

	/**
	 * Start by displaying the load box
	 */
	@Override
	protected void onPreExecute()
	{
		mLoadingDialog = new LoadingDialog();
		mLoadingDialog.show(activity.getFragmentManager(), "LOADING");
	}

	/**
	 * Loading data
	 */
	@Override
	protected Void doInBackground(Void... params)
	{
		items = loadingData();
		return null;
	}

	/**
	 * Load completion calls back the current Activity
	 */
	@Override
	protected void onPostExecute(Void unused)
	{
		isCompleted = true;
		notifyActivityTaskCompleted();
		if (mLoadingDialog != null)
			mLoadingDialog.dismiss();
	}

	public List<String> getItems()
	{
		return items;
	}

	private List<String> loadingData()
	{
		try
		{
			Thread.sleep(5000);
		} catch (InterruptedException e)
		{
		}
		return new ArrayList<String>(Arrays.asList("adopt Fragment Keep a large amount of data",
				"onSaveInstanceState Save data",
				"getLastNonConfigurationInstance Has been discarded", "RabbitMQ", "Hadoop",
				"Spark"));
	}

	/**
	 * Set up Activity, because Activity will always change
	 * 
	 * @param activity
	 */
	public void setActivity(FixProblemsActivity activity)
	{
		// If the previous Activity is destroyed, the Dialog Fragment bound to the previous Activity is destroyed
		if (activity == null)
		{
			mLoadingDialog.dismiss();
		}
		// Set to Current Activity
		this.activity = activity;
		// Open a wait box bound to the current Activity
		if (activity != null && !isCompleted)
		{
			mLoadingDialog = new LoadingDialog();
			mLoadingDialog.show(activity.getFragmentManager(), "LOADING");
		}
		// If completed, notify Activity
		if (isCompleted)
		{
			notifyActivityTaskCompleted();
		}
	}

	private void notifyActivityTaskCompleted()
	{
		if (null != activity)
		{
			activity.onTaskCompleted();
		}
	}

}

In an asynchronous task, manage a dialog box. Before the download starts, the progress box shows that the download end progress box disappears and provides callbacks for Activity. Of course, we also provide a setActivity method to prevent memory leaks when onDestory is running, and we also close the load box that binds to it; when onCreate imports a new activity, we open a load box again, of course, because of the screen. The rotation of the screen does not affect the loaded data, and all background data continues to load. Is it perfect?~~
 

Main Activity:

 

public class FixProblemsActivity extends ListActivity
{
	private static final String TAG = "MainActivity";
	private ListAdapter mAdapter;
	private List<String> mDatas;
	private OtherRetainedFragment dataFragment;
	private MyAsyncTask mMyTask;

	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		Log.e(TAG, "onCreate");

		// find the retained fragment on activity restarts
		FragmentManager fm = getFragmentManager();
		dataFragment = (OtherRetainedFragment) fm.findFragmentByTag("data");

		// create the fragment and data the first time
		if (dataFragment == null)
		{
			// add the fragment
			dataFragment = new OtherRetainedFragment();
			fm.beginTransaction().add(dataFragment, "data").commit();
		}
		mMyTask = dataFragment.getData();
		if (mMyTask != null)
		{
			mMyTask.setActivity(this);
		} else
		{
			mMyTask = new MyAsyncTask(this);
			dataFragment.setData(mMyTask);
			mMyTask.execute();
		}
		// the data is available in dataFragment.getData()
	}


	@Override
	protected void onRestoreInstanceState(Bundle state)
	{
		super.onRestoreInstanceState(state);
		Log.e(TAG, "onRestoreInstanceState");
	}


	@Override
	protected void onSaveInstanceState(Bundle outState)
	{
		mMyTask.setActivity(null);
		super.onSaveInstanceState(outState);
		Log.e(TAG, "onSaveInstanceState");
	}

	@Override
	protected void onDestroy()
	{
		Log.e(TAG, "onDestroy");
		super.onDestroy();

	}
	/**
	 * Callback
	 */
	public void onTaskCompleted()
	{
		mDatas = mMyTask.getItems();
		mAdapter = new ArrayAdapter<String>(FixProblemsActivity.this,
				android.R.layout.simple_list_item_1, mDatas);
		setListAdapter(mAdapter);
	}

}

In onCreate, if no task is opened (first entry), start the task; if it is already started, call setActivity (this);

 

 

Keywords: Fragment Android network RabbitMQ

Added by warren on Thu, 01 Aug 2019 08:25:50 +0300