Developers Club geek daily blog

1 year, 10 months ago
One of natural and first tasks when developing under the Android – the organization of asynchronous interaction. For example, the appeal to the server from some activity and display of result on it. Difficulty consists that for access time to the server other activity or other application can be atop open, initial activity can be irrevocably complete (the user clicked Back) etc. Here we received result from the server, but activity "is inactive". Under "is active", depending on circumstances, it is possible to understand, for example, what is between onStart and onStop, onResume and onPause (or, as at us in the project, between onPostResume and the first of onSaveInstanceState and onStop). How to understand, activity finally (and the result needs to be given to a garbage collector) is complete or is only temporarily inactive (the result needs to be kept, and to display as soon as activity becomes active)?

Surprisingly, but in documentation, the Internet, at personal contact I never met correct and is acceptable a universal method. I want to share non-paid a solution which we apply two and a half years in mobile Internet banking. The application is installed (as part of larger system) at several hundred banks, about one million users at the moment have.

Let's specify concepts activity and activity record. Activity is a copy of a class, short-lived object. Activity record – logical concept, the screen from the point of view of the user, more long-living.
Let's consider the scheme Bottom> Middle> Top.
  1. We start activity of BottomActivity, over it MiddleActivity. At turn of the screen, temporary switching on other application, etc. activity (a class MiddleActivity copy) can be destroyed and be created new, but activity record Middle remains invariable. We start TopActivity over MiddleActivity, we click Back. Activity of MiddleActivity again above a stack, could be recreated, but activity record Middle still remains invariable.
  2. We click Back – BottomActivity above a stack. Again we start MiddleActivity. Again above activity record Middle. But it is already new activity record which does not have relations to activity record from point 1. That activity record irrevocably died.

Proposed solution is based on the following remarkable android.os.Binder property. If to write Binder in android.os.Parcel, then when reading in the same process (in the same virtual computer) with guarantee we will read the same copy of object which was written. Respectively, it is possible to proassotsiirovat a copy of object of activity record with activity and to save this object invariable by means of the onSaveInstanceState mechanism. The object of activity record to which the result returns is transferred to an asynchronous task. If activity record dies, then becomes available to a garbage collector, together with results of work of asynchronous tasks.

For an illustration we will create the simple Length application. It consists of two activities and four infrastructure classes.

Project files

MenuActivity consists of one button which starts LengthActivity.

Main menu

It is directly inconvenient to work with Binder as it cannot be written in android.os.Bundle. Therefore we will envelop 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 class IdentityParcelable allows to transfer via the links parcel-mechanism to objects. For example, to transfer in quality of extra (Intent#putExtra) object which is not either Serializable, or Parcelable, and later to receive (getExtra) the same copy in other activity.

The classes ActivityRecord and BasicActivity work in a sheaf. ActivityRecord is able to perform callback-and. If activity is visible (is in a status between onStart and onStop), then callback is performed at once, differently remains for later execution. When activity becomes visible, are performed all postponed callback-and. During creation of activity record (the first challenge BasicActivity#onCreate) the new object of ActivityRecord is created, and it is supported in onSaveInstanceState/onCreate further.

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


On the basis of ActivityRecord we do for asynchronous tasks the base class similar by 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 infrastructure, we do LengthActivity. When clicking the button length of the entered line is asynchronously calculated. Let's notice that at turn of the screen calculation does not begin again, and proceeds.

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 put archive with all source codes and collected APK.

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

This article is a translation of the original post at habrahabr.ru/post/274635/
If you have any questions regarding the material covered in the article above, please, contact the original author of the post.
If you have any complaints about this article or you want this article to be deleted, please, drop an email here: sysmagazine.com@gmail.com.

We believe that the knowledge, which is available at the most popular Russian IT blog habrahabr.ru, should be accessed by everyone, even though it is poorly translated.
Shared knowledge makes the world better.
Best wishes.

comments powered by Disqus