Pull to refresh
133.11
Rating
red_mad_robot
№1 в разработке цифровых решений для бизнеса

Библиотека Chronos: облегчаем написание долгих операций

red_mad_robot corporate blog Java *Development of mobile applications *Development for Android *
Привет, Хабр! Хочу рассказать вам о библиотеке Chronos для Android (API level >= 9), цель которой – облегчить написание долгих операций, например, сетевых запросов, или обращений к БД.

Какую проблему решаем?

Не секрет, что для 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’ам: developer.android.com/reference/android/content/AsyncTaskLoader.html

4. Service
Сервисы хороши для выполнения долгих задач, которые «висят» в фоне на протяжении использования приложения. Однако для запуска операций, результат которых нужен здесь и сейчас, структура сервисов не идеальна. Главным образом, ограничение накладывает методика передачи данных через Intent, который, во-первых, вмещает только ограниченное количество данных, а во-вторых, требует чтобы передаваемые данные были тем или иным способом сериализуемы. На этой технологии работает популярная библиотека Robospice.

Что предлагает Chronos?



Chronos делает за вас всю работу по выполнению задачи в параллельном потоке и доставке результата или ошибки выполнения в основной поток. Грубо говоря, эта библиотека предоставляет контейнер для любого рода долгих операций.
В проекте есть полноценная wiki, часть кода оттуда будет использоваться в статье, однако для более полного руководства обращайтесь на github.com/RedMadRobot/Chronos/wiki.

Пример

Давайте решим типовую задачу, используя 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
Tags:
Hubs:
Total votes 14: ↑12 and ↓2 +10
Views 14K
Comments 35
Comments Comments 35

Posts

Information

Website
redmadrobot.ru
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия