Одна из естественных и первых задач при разработке под Андроид – организация асинхронного взаимодействия. Например, обращение к серверу из некоторой активности и отображение на ней результата. Трудность состоит в том, что за время обращения к серверу поверх может быть открыта другая активность или другое приложение, исходная активность может быть безвозвратно завершена (пользователь нажал Back) и т. д. Вот получили мы результат от сервера, но активность «неактивна». Под «активна», в зависимости от обстоятельств, можно понимать, например, что находится между onStart и onStop, onResume и onPause (или, как у нас в проекте, между onPostResume и первым из onSaveInstanceState и onStop). Как понять, завершена активность окончательно (и результат нужно отдать сборщику мусора) или лишь временно неактивна (результат нужно хранить, и отобразить, как только активность станет активной)?

Удивительно, но в документации, интернетах, при личном общении я ни разу не встречал корректного и приемлемо универсального способа. Хочу безвозмездно поделиться решением, которое мы применяем два с половиной года в мобильном интернет-банкинге. Приложение установлено (как часть более крупной системы) у нескольких сотен банков, на данный момент имеет около миллиона пользователей.

Уточним понятия активность и activity record. Активность – это экземпляр класса, короткоживущий объект. Activity record – логическое понятие, экран с точки зрения пользователя, более долгоживущий.
Рассмотрим схему Bottom > Middle > Top.
  1. Запускаем активность BottomActivity, поверх неё MiddleActivity. При повороте экрана, временном переключении на другое приложение и т. п. активность (экземпляр класса MiddleActivity) может уничтожаться и создаваться новая, но activity record Middle остаётся неизменным. Запускаем TopActivity поверх MiddleActivity, нажимаем ��нопку Back. Активность MiddleActivity снова наверху стека, могла быть пересоздана, но activity record Middle всё так же сохраняется неизменным.
  2. Нажимаем Back – BottomActivity наверху стека. Снова запускаем MiddleActivity. Опять наверху activity record Middle. Но это уже новый activity record, не имеющий отношения к activity record из пункта 1. Тот activity record безвозвратно умер.

Предлагаемое решение основывается на следующем замечательном свойстве android.os.Binder. Если записать Binder в android.os.Parcel, то при чтении в том же процессе (в той же виртуальной машине) гарантированно прочитаем тот же самый экземпляр объекта, который был записан. Соответственно, можно проассоциировать с активностью экземпляр объекта activity record, и сохранять этот объект неизменным с помощью механизма onSaveInstanceState. В асинхронную задачу передаётся объект activity record, в который возвращается результат. Если activity record умирает, то становится доступен сборщику мусора, вместе с результатами работы асинхронных задач.

Для иллюстрации создадим простое приложение «Length». Оно состоит из двух активностей и четырёх инфраструктурных классов.

Project files

MenuActivity состоит из одной кнопки, которая запускает LengthActivity.

Main menu

Работать с Binder напрямую неудобно, так как его нельзя записать в android.os.Bundle. Поэтому обернём Binder в 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;
        }
    }
}


Класс IdentityParcelable позволяет передавать через parcel-механизм «ссылки» на объекты. Например, передать в качестве extra (Intent#putExtra) объект, который не является ни Serializable, ни Parcelable, и позже получить (getExtra) тот же экземпляр в другой активности.

Классы ActivityRecord и BasicActivity действуют в связке. ActivityRecord умеет исполнять callback-и. Если активность видна (находится в состоянии между onStart и onStop), то callback исполняется сразу, иначе сохраняется для более позднего исполнения. Когда активность становится видимой, исполняются все отложенные callback-и. При создании activity record (первый вызов BasicActivity#onCreate) создаётся новый объект ActivityRecord, и дальше поддерживается в 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;
    }
}


На основе ActivityRecord делаем для асинхронных задач базовый класс, похожий контрактом на 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) {
    }
}


Теперь, наладив инфраструктуру, делаем LengthActivity. При нажатии на кнопку асинхронно вычисляется длина введённой строки. Заметим, что при повороте экрана вычисление не начинается заново, а продолжается.

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


Прикладываю архив со всеми исходниками и собранным APK.

Спасибо за внимание! Буду рад услышать комментарии и поучаствовать в обсуждении. Буду счастлив узнать более простое решение, без заморочек с Binder.

UPD: deej подсказал класс android.support.v4.app.BundleCompat, который умеет записывать IBinder в Bundle. Когда разрабатывали решение, этого класса ещё не было. BundleCompat немного упрощает код, позволяя обойтись без IdentityParcelable, одним Binder-ом наподобие
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;
    }
}

Возможно, IdentityParcelable всё равно может быть полезен, например, для передачи произвольных объектов в Intent в качестве extra, хотя можно обойтись ValueBinder-ом, передавая через Bundle.