Какую проблему решаем?
Не секрет, что для Android задача выполнения асинхронных операций всегда была одной из самых частовстречающихся. Действительно, крайне мало приложений работают исключительно в оффлайн, и где можно обойтись без сетевого взаимодействия. И уж совсем крохотная их часть обходится без обращения к постоянной памяти устройства, будь то база данных, Preferences или обычный файл. Однако, на протяжении истории развития системы нам так и не было предложено ни одного достаточно удобного решения “из коробки”.
1. Стандартные потоки
Button signInButton = (Button) findViewById(R.id.button_auth);
signInButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
final Activity activity = AuthActivity.this;
showProgress();
new Thread(new Runnable() {
@Override
public void run() {
APIFactory.getApi().signIn();
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
goToMainContent();
}
});
}
}).start();
}
});
В этом коде плохо буквально все. Он сложночитаемый, в нем протекает память, его нельзя отменить, в нем не обрабатывается поворот экрана, как и любые ошибки вызова API (а если их обрабатывать, то выглядеть все станет совсем уж неудобоваримо).
2. AsynkTask
Button signInButton = (Button) findViewById(R.id.button_auth);
signInButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
new AuthTask().execute();
}
});
private class AuthTask extends AsyncTask<Void, Void, Boolean>{
@Override
protected void onPreExecute() {
showProgress();
}
@Override
protected Boolean doInBackground(final Void... params) {
try {
APIFactory.getApi().signIn();
}catch (Exception e){
return false;
}
return true;
}
@Override
protected void onPostExecute(final Boolean result) {
if(!isCancelled() && result) {
goToMainContent();
}
}
}
Уже чуть лучше, но все еще недостаточно. Появилась читаемая обработка ошибок, возможность отмены. Однако до сих пор этот код не способен правильно отработать при повороте экрана в момент выполнения запроса к API – утекает ссылка на Activity, в которой определен класс.
3. Loader
Когда Google представил Loader’ы, то казалось, что они станут Silver bullet для асинхронных запросов, сместив классические на тот момент AsyncTask’и. К сожалению, чуда не произошло. На данный момент Loader’ы – редкий гость в коммерческий проектах, поскольку очень уж они оказались неудобны в использовании. В этом разделе я не буду приводить код по аналогии с предыдущими двумя. Вместо этого рекомендую любопытному читателю ознакомиться с официальным гайдом по этой технологии, чтобы оценить объем кода, требующегося Loader’ам: http://ift.tt/vhviaV
4. Service
Сервисы хороши для выполнения долгих задач, которые «висят» в фоне на протяжении использования приложения. Однако для запуска операций, результат которых нужен здесь и сейчас, структура сервисов не идеальна. Главным образом, ограничение накладывает методика передачи данных через Intent, который, во-первых, вмещает только ограниченное количество данных, а во-вторых, требует чтобы передаваемые данные были тем или иным способом сериализуемы. На этой технологии работает популярная библиотека Robospice.
Что предлагает Chronos?
Chronos делает за вас всю работу по выполнению задачи в параллельном потоке и доставке результата или ошибки выполнения в основной поток. Грубо говоря, эта библиотека предоставляет контейнер для любого рода долгих операций.
В проекте есть полноценная wiki, часть кода оттуда будет использоваться в статье, однако для более полного руководства обращайтесь на http://ift.tt/1CScwIl.
Пример
Давайте решим типовую задачу, используя Chronos: в Activity нужно запросить какой-то объект у некоего хранилища, доступ к которому достаточно долго, чтобы не делать запрос в UI потоке. Сначала напишем код, а потом разберем, что у нас получилось.
1. Первым делом нужно подключить Chronos к проекту. Для этого достаточно прописать зависимость в gradle:
compile 'com.redmadrobot:chronos:1.0.5'
2. Теперь опишем Activity. Базовый класс ChronosActivity– это одна из компонент библиотеки, однако вы легко можете написать его аналог, примеры этого есть в документации. Так же Chronos можно использовать во фрагментах, код не будет отличаться.
class MyActivity extends ChronosActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Button startButton = (Button) findViewById(R.id.button_start);
startButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
runOperation(new MyOperation());
}
});
}
public void onOperationFinished(final MyOperation.Result result) {
if (result.isSuccessful()) {
showData(result.getOutput());
} else {
showDataLoadError(result.getError());
}
}
private void showData(BusinessObject data){
//...
}
private void showDataLoadError(Exception exception){
//...
}
}
3. И, наконец, опишем бизнес-логику получения данных в классе MyOperation:
class MyOperation extends ChronosOperation<BusinessObject> {
@Nullable
@Override
public BusinessObject run() {
final BusinessObject result ;
// here you should write what you do to get the BusinessObject
return result;
}
@NonNull
@Override
public Class<? extends ChronosOperationResult<BusinessObject>> getResultClass(){
return Result.class;
}
public final static class Result extends ChronosOperationResult<BusinessObject> {
}
}
Вот, собственно, и все. Давайте разберемся подробно, что же происходит в этом коде. Начнем с начала.
Настройка класса UI
class MyActivity extends ChronosActivity {
Чтобы работать с Chronos, базовый класс Acvitity или фрагмента должен либо наследоваться от предложенных в библиотеке, либо содержать определенный код в методах жизненного цикла, примеры можно увидеть в документации.
Запуск операции
runOperation(new MyOperation());
Здесь вызывается базовый метод класса ChronosActivity, в который передается только что созданная операция. Сразу после вызова этого метода Chronos заберет операцию в очередь и начнет ее выполнение в параллельном потоке.
Обработка результата операции
public void onOperationFinished(final MyOperation.Result result) {
if (result.isSuccessful()) {
showData(result.getOutput());
} else {
showDataLoadError(result.getError());
}
}
Этот метод будет вызван после того, как операция будет выполнена, либо в ходе выполнения выбросится исключение. Такие методы-обработчики обязательно должны иметь сигнатуру public void onOperationFinished(ResultType). Важный момент: метод вызовется только между вызовами onResume() и onPause(), то есть в нем вы спокойно можете изменять UI, не боясь, что он к тому моменту уже стал невалидным. Более того, если Activity была пересоздана из-за поворота, ухода в бэкграунд, или других причин – Chronos вернет результат в любом случае (единственное исключение – в системе закончилась память, в этом случае для предотвращения OutOfMemory Chronos может стереть старые данные результатов).
Внимательный читатель заметит, что Activity не реализует никаких специфических интерфейсов, так откуда же вызовется именно этот метод? Ответ – из кода, содержащего рефлексию. Решение делать рефлексию вместо интерфейса было принято из-за TypeErasure в Java, который делает невозможным одновременную реализацию одного и того же шаблонного интерфейса с разными параметрами. То есть это сделано, чтобы в одной Activity можно было обработать результат скольких угодно типов операций.
Настройка класса операции
class MyOperation extends ChronosOperation<BusinessObject> {
Класс ChronosOperation инкапсулирует в себе бизнес-логику получения объекта определенного типа, в данном случае – BusinessObject. Все пользовательские операции должны наследоваться от ChronosOperation.
Бизнес-логика
@Nullable
@Override
public BusinessObject run() {
final BusinessObject result ;
// here you should write what you do to get the BusinessObject
return result;
}
Этот абстрактный метод класса ChronosOperation отвечает, собственно, за бизнес-логику получения объекта. Он выполняется в параллельном потоке, поэтому в нем можно делать сколь угодно долгие действия, это не вызовет лагов в интерфейсе приложения. Также любые исключения, выброшенные в нем, будут заботливо переданы в вызывающий объект, не приводя к крашу приложения.
Именование результата
@NonNull
@Override
public Class<? extends ChronosOperationResult<BusinessObject>> getResultClass(){
return Result.class;
}
public final static class Result extends ChronosOperationResult<BusinessObject> {
}
Следующие метод и класс призваны дать возможность в коде Activity прописать обработчик результатов для каждой конкретной операции, указывая класс в качестве типа параметра метода onOperationFinished. Допускается использование одного класса результата для разных операций, если вы хотите, чтобы их результат обрабатывался одинаково.
Резюмирую: соберем минимальный набор кодовых участков, нужных для работы с Chronos.
- Класс операции
- Код вызова операции в UI объекте
- Код обработки результата в UI объекте
Что здесь есть еще?
Итак, почему и зачем можно использовать Chronos?
- Chronos берет на себя передачу данных между потоками, оставляя вам заботы только о бизнес-логике.
- Chronos учитывает все нюансы жизненного цикла Activity и фрагментов, доставляя результат только тогда, когда они готовы его обработать, сохраняя данные до тех пор.
- В Chronos не течет память. Вы больше не рискуете поймать краш, потому что утекло слишком много объектов Activity.
- Chronos покрыт unit-тестами.
- И наконец, Chronos – open-source проект. Вы всегда можете взять код и переписать его под свои нужды. Благодаря тестам, вам будет легко валидировать изменения кода.
Ссылка на проект в GitHub. Там вы найдете полное руководство по библиотеке, примеры использования и, конечно, исходный код.
Читайте также:
Сажаем контроллеры на диету: Android
Архитектурный дизайн мобильных приложений: часть 1
Архитектурный дизайн мобильных приложений: часть 2
This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Комментариев нет:
Отправить комментарий