- PVSM.RU - https://www.pvsm.ru -
Одна из естественных и первых задач при разработке под Андроид – организация асинхронного взаимодействия. Например, обращение к серверу из некоторой активности и отображение на ней результата. Трудность состоит в том, что за время обращения к серверу поверх может быть открыта другая активность или другое приложение, исходная активность может быть безвозвратно завершена (пользователь нажал Back) и т. д. Вот получили мы результат от сервера, но активность «неактивна». Под «активна», в зависимости от обстоятельств, можно понимать, например, что находится между onStart и onStop, onResume и onPause (или, как у нас в проекте, между onPostResume и первым из onSaveInstanceState и onStop). Как понять, завершена активность окончательно (и результат нужно отдать сборщику мусора) или лишь временно неактивна (результат нужно хранить, и отобразить, как только активность станет активной)?
Удивительно, но в документации, интернетах, при личном общении я ни разу не встречал корректного и приемлемо универсального способа. Хочу безвозмездно поделиться решением, которое мы применяем два с половиной года в мобильном интернет-банкинге. Приложение установлено (как часть более крупной системы) у нескольких сотен банков, на данный момент имеет около миллиона пользователей.
Уточним понятия активность и activity record. Активность – это экземпляр класса, короткоживущий объект. Activity record – логическое понятие, экран с точки зрения пользователя, более долгоживущий.
Рассмотрим схему Bottom > Middle > Top.
Предлагаемое решение основывается на следующем замечательном свойстве android.os.Binder. Если записать Binder в android.os.Parcel, то при чтении в том же процессе (в той же виртуальной машине) гарантированно прочитаем тот же самый экземпляр объекта, который был записан. Соответственно, можно проассоциировать с активностью экземпляр объекта activity record, и сохранять этот объект неизменным с помощью механизма onSaveInstanceState. В асинхронную задачу передаётся объект activity record, в который возвращается результат. Если activity record умирает, то становится доступен сборщику мусора, вместе с результатами работы асинхронных задач.
Для иллюстрации создадим простое приложение «Length». Оно состоит из двух активностей и четырёх инфраструктурных классов.
MenuActivity состоит из одной кнопки, которая запускает LengthActivity.
Работать с 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. При нажатии на кнопку асинхронно вычисляется длина введённой строки. Заметим, что при повороте экрана вычисление не начинается заново, а продолжается.
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 [1].
Спасибо за внимание! Буду рад услышать комментарии и поучаствовать в обсуждении. Буду счастлив узнать более простое решение, без заморочек с Binder.
Автор: mychka
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android/108346
Ссылки в тексте:
[1] архив со всеми исходниками и собранным APK: https://drive.google.com/uc?export=download&id=0B6tnJ3H2DtJMRXVwd2syNUFlcDg
[2] Источник: http://habrahabr.ru/post/274635/
Нажмите здесь для печати.