📜 ⬆️ ⬇️

Use android.os.Binder to organize asynchronous interaction in Android

One of the natural and first tasks in developing for Android is the organization of asynchronous interaction. For example, accessing the server from some activity and displaying the result on it. The difficulty is that during the time of accessing the server, another activity or another application can be opened on top, the initial activity can be irrevocably completed (the user pressed Back), etc. Here we got the result from the server, but the activity is “inactive”. Under "active", depending on the circumstances, you can understand, for example, what is between onStart and onStop, onResume and onPause (or, as we have in the project, between onPostResume and the first ofSaveInstanceState and onStop). How to understand if the activity is completed completely (and the result needs to be given to the garbage collector) or is only temporarily inactive (the result needs to be stored and displayed as soon as the activity becomes active)?

Surprisingly, in the documentation, the Internet, in person, I have never met the correct and acceptable universal method. I want to share for free the solution that we have been using for two and a half years in mobile Internet banking. The application is installed (as part of a larger system) with several hundred banks, currently has about a million users.

Clarify the concept of activity and activity record . An activity is an instance of a class, a short-lived object. Activity record - a logical concept, the screen from the point of view of the user, more long-lived.
Consider the scheme Bottom> Middle> Top.
  1. We launch the BottomActivity activity, on top of it MiddleActivity. When you rotate the screen, temporarily switch to another application, etc., the activity (instance of the MiddleActivity class) can be destroyed and a new one created, but the activity record Middle remains unchanged. Launch TopActivity over MiddleActivity, press the Back button. The MiddleActivity activity is again at the top of the stack, it could be recreated, but the activity record Middle still remains unchanged.
  2. Click Back - BottomActivity at the top of the stack. Run the MiddleActivity again. Again at the top of the activity record Middle. But this is a new activity record, which is not related to the activity record from item 1. That activity record irretrievably died.

The proposed solution is based on the following remarkable android.os.Binder property. If you write Binder in android.os.Parcel, then when reading in the same process (in the same virtual machine) you are guaranteed to read the same copy of the object that was written. Accordingly, you can associate with activity an instance of the activity activity record object, and keep this object unchanged using the onSaveInstanceState mechanism. The activity record object is passed to the asynchronous task, to which the result is returned. If the activity record dies, then the garbage collector becomes available, along with the results of the asynchronous tasks.

To illustrate, create a simple application “Length”. It consists of two activities and four infrastructure classes.
')
Project files

MenuActivity consists of one button that launches LengthActivity.

Main menu

Working with Binder directly is inconvenient, since it cannot be written to android.os.Bundle. Therefore, we wrap the Binder in android.os.Parcelable.

public class IdentityParcelable implements Parcelable { private final ReferenceBinder referenceBinder = new ReferenceBinder(); public final Object content; public static final Parcelable.Creator<IdentityParcelable> CREATOR = new Creator<IdentityParcelable>() { @Override public IdentityParcelable createFromParcel(Parcel source) { try { return ((ReferenceBinder) source.readStrongBinder()).get(); } catch (ClassCastException e) { // It must be application recover from crash. return null; } } @Override public IdentityParcelable[] newArray(int size) { return new IdentityParcelable[size]; } }; public IdentityParcelable(Object content) { this.content = content; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeStrongBinder(referenceBinder); } private class ReferenceBinder extends Binder { IdentityParcelable get() { return IdentityParcelable.this; } } } 


The IdentityParcelable class allows you to pass objects through the parcel mechanism. For example, pass as an extra (Intent # putExtra) object that is neither Serializable nor Parcelable, and later get (getExtra) the same instance in another activity.

The ActivityRecord and BasicActivity classes act in conjunction. ActivityRecord is able to execute callback. If the activity is visible (in the state between onStart and onStop), then the callback is executed immediately, otherwise it is saved for later execution. When activity becomes visible, all deferred callbacks are executed. When you create an activity record (the first call to BasicActivity # onCreate), a new ActivityRecord object is created, and is further supported in onSaveInstanceState / onCreate.

 public class ActivityRecord { private static final Handler UI_HANDLER = new Handler(Looper.getMainLooper()); private Activity visibleActivity; private final Collection<Runnable> pendingVisibleActivityCallbacks = new LinkedList<>(); public void executeOnVisible(final Runnable callback) { UI_HANDLER.post(new Runnable() { @Override public void run() { if (visibleActivity == null) { pendingVisibleActivityCallbacks.add(callback); } else { callback.run(); } } }); } void setVisibleActivity(Activity visibleActivity) { this.visibleActivity = visibleActivity; if (visibleActivity != null) { for (Runnable callback : pendingVisibleActivityCallbacks) { callback.run(); } pendingVisibleActivityCallbacks.clear(); } } public Activity getVisibleActivity() { return visibleActivity; } } 


 public class BasicActivity extends Activity { private static final String ACTIVITY_RECORD_KEY = "com.zzz.ACTIVITY_RECORD_KEY"; private ActivityRecord activityRecord; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState == null) { activityRecord = new ActivityRecord(); } else { activityRecord = (ActivityRecord) ((IdentityParcelable) savedInstanceState.getParcelable(ACTIVITY_RECORD_KEY)).content; } } @Override protected void onStart() { super.onStart(); activityRecord.setVisibleActivity(this); } @Override protected void onStop() { activityRecord.setVisibleActivity(null); super.onStop(); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(ACTIVITY_RECORD_KEY, new IdentityParcelable(activityRecord)); } public ActivityRecord getActivityRecord() { return activityRecord; } } 


Based on ActivityRecord, we are doing a base class for asynchronous tasks, similar to the contract for android.os.AsyncTask.

 public class BackgroundTask { private final ActivityRecord activityRecord; public BackgroundTask(ActivityRecord activityRecord) { this.activityRecord = activityRecord; } public void execute() { new Thread() { @Override public void run() { doInBackground(); activityRecord.executeOnVisible(new Runnable() { @Override public void run() { onPostExecute(activityRecord.getVisibleActivity()); } }); } }.start(); } protected void publishProgress(final int progress) { activityRecord.executeOnVisible(new Runnable() { @Override public void run() { onProgressUpdate(activityRecord.getVisibleActivity(), progress); } }); } protected void doInBackground() { } protected void onProgressUpdate(Activity activity, int progress) { } protected void onPostExecute(Activity activity) { } } 


Now, having adjusted the infrastructure, we do the LengthActivity. Clicking the button asynchronously calculates the length of the entered string. Note that when the screen is rotated, the calculation does not start over, but continues.

Length activity

 public class LengthActivity extends BasicActivity { private TextView statusText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.length_activity); statusText = (TextView) findViewById(R.id.statusText); findViewById(R.id.calculateLengthButton).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new LengthTask( getActivityRecord(), ((TextView) findViewById(R.id.sampleField)).getText().toString() ).execute(); } }); } private void setCalculationResult(CharSequence sample, int length) { statusText.setText("Length of " + sample + " is " + length); } private void setCalculationProgress(CharSequence sample, int progress) { statusText.setText("Calculating length of " + sample + ". Step " + progress + " of 100."); } private static class LengthTask extends BackgroundTask { final String sample; int length; LengthTask(ActivityRecord activityRecord, String sample) { super(activityRecord); this.sample = sample; } @Override protected void doInBackground() { for (int i = 0; i < 100; i++) { publishProgress(i); try { Thread.sleep(50); } catch (InterruptedException e) { throw new IllegalStateException(e); } } length = sample.length(); } @Override protected void onProgressUpdate(Activity activity, int progress) { ((LengthActivity) activity).setCalculationProgress(sample, progress); } @Override protected void onPostExecute(Activity activity) { ((LengthActivity) activity).setCalculationResult(sample, length); } } } 


I attach the archive with all the sources and compiled APK .

Thanks for attention! I will be glad to hear comments and participate in the discussion. I would be happy to learn a simpler solution, without problems with the Binder.

UPD: deej prompted the android.support.v4.app.BundleCompat class, which can write IBinder in the Bundle. When developing a solution, this class was not yet. BundleCompat slightly simplifies the code, allowing you to do without IdentityParcelable, one Binder-like
 public class ValueBinder extends Binder { public Object value; public ValueBinder() { } public ValueBinder(Object value) { this.value = value; } public <V> V value() { //noinspection unchecked return (V) value; } } 

Perhaps IdentityParcelable can still be useful, for example, to transfer arbitrary objects to the Intent as an extra, although ValueBinder can be dispensed with by passing through the Bundle.

Source: https://habr.com/ru/post/274635/


All Articles