Долгие асинхронные операции в Андроид?

Необходимо в приложении реализовать "формочку входа". Упрощённая модель выглядит так:
1. пользователь нажимает кнопку
2. запускается "долгая" операция
3. по итогам операции пользователя перекидывает на какую-то активити
Как это реализовать корректно?

Известные нюансы платформы Андроид:
1. Операции, не связанные напрямую с обновлением UI нужно выполнять вне потока UI.
2. Пользователь может покинуть активити в любой момент, включая "сразу после запуска операции".
3. Пользователь может вернуться в активити в любой момент, включая "ещё до завершения операции" и "уже после завершения операции"
4. Если пользователь ушёл из активити, нет никаких гарантий, что при возврате в эту активити будет использоваться тот же инстанс.
(Для упрощения задачи предположим, что после нажатия кнопки мы тут же показываем ProgressDialog, который нельзя закрыть, поэтому сценарий с Back можно пропустить)

Требования:
Если пользователь нажал кнопку, то без всяких "предположим" и "если" операция должна отработать и, как только результат доступен, пользователя должно перекинуть на другую активити. Это поведение не должно зависеть от того, что пользователь сделал сразу после нажатия кнопки: принял входящий звонок, повернул девайс, нажал Home, решив почитать твиттер, и т.д.

1. Есть известное простое неправильное решение вроде такого:
...
new AsyncTask() {
  void doWork() {
    boolean authenticated = webService.authenticate(email, password);
    if(authenticated) {
      Intent intent = new Intent(AuthActivity.this, HomeActivity.class);
      startActivity(intent);
      finish();
    }
  }
}
...

Оно работает только если пользователь не крутит девайс, ему никто не звонит и он не решает пойти в Твиттер, пока мы тут аутентифицируемся.

2. Есть сложное неправильное решение вроде такого:
class WebServiceFacade extends IntentService {
  ... // оно пришлёт нам Broadcast
}
...
// дёргаем сервис
Intent authenticateIntent = new Intent(...)
startService(authenticateIntent);
...
// ждём ответ от сервиса
BroadcastReceiver receiver = .... {
  Intent intent = new Intent(AuthActivity.this, HomeActivity.class);
  startActivity(intent);
  finish(); 
}

Эта штука совершенно точно не будет работать, если к моменту завершения операции пользователь не будет смотреть на наше активити. Т.к. мы просто будем отписаны от этих уведомлений.

3. Сложное правильное решение
Никакой специфики Андроида - пишем самодельный сервис например на ExecutorService таким образом, чтобы во-первых он всегда существовал, а во-вторых кроме отправки уведомлений через коллбек предоставлял доступ к результатам операций, которые завершились, но которые на момент завершения операций никто "не слушал". В этом случае при запуске активити мы сначала проверяем готовые результаты и на основании их делаем выводы, если результатов нет, то подписываемся и просто висим - ждём результат, если пользователь решает уйти - отписываемся (ну и дальше по кругу). Это даёт гарантию безопасного убийства и перезапуска активити, при этом для пользователя всё прозрачно. Что-то вроде такого:
(at)Singleton
class MyService {
  ...
}
...
(at)Inject
private MyService service;
...
void onResume() {
  if(service.hasAuthResult()) {
    Intent intent = new Intent(AuthActivity.this, HomeActivity.class);
    startActivity(intent);
    finish(); 
  } else {
    service.setListener(this);
  }
}
...
void onPause() {
  service.setListener(null);
}
...
void MyServiceListener.onAuthResult(bool isAuthenticated) {
    Intent intent = new Intent(AuthActivity.this, HomeActivity.class);
    startActivity(intent);
    finish();  
}
...

Я реализовал решение номер 3, оно отлично работает, но мне не нравится объём кода и сам факт наличия такого велосипеда. Может я что-то упускаю и при той же корректности можно всё сделать проще?
  • Вопрос задан
  • 3862 просмотра
Пригласить эксперта
Ответы на вопрос 3
@Mintormo
Вы настолько подробно описали все это, что и возразить-то нечего. У вас такие требования что проще, видимо, и не сделать. Вы наверняка знаете что в Андроиде полно решений такого рода: сложных и размашистых. Например при работе с XML. Так что думаю вам стоит оставить так.
Ответ написан
@FoxInSox
Вы слишком категорично относитесь к AsyncTask. AsyncTask'у не обязательно запускать другую активити, у него должна быть одна простая задача. В вашем случае: послать запрос > получить ответ > сохранить в БД данные о том что пользователь авторизован или нет > оповестить интересующихся о том что запрос успешно/не успешно завершен. Тогда сценария всего два:
1. Если пользователь еще находится в ативити авторизации, то запускается следующая активити
2. Если пользователь находится где-то еще, то при возвращении в приложение вызовется метод onResume в котором надо проверить БД на наличие авторизации.

PS Та же история кстати и с IntentService. Он должен просто выполнять запрос и сохранять результат в БД, он не должен бродкастить событие аля "авторизация прошла успешно, запускайте конкрентую активити"

PS2 ProgressDialog без возможности его отмены в данном случае моветон, т.к. пользователь мог просто случайно нажать на кнопку, или он находится в зоне плохой сети и ему придется в течении 20-30 секунд смотреть пока запрос завершится.
Ответ написан
Я бы такое сделал через AsyncTask. Причина проста, так как вы не хотите дабы вам пользователь заметил, что там работает какой-то дополнительный сервис...
А диалоги - все блокирующие диалоги в андроиде - это маразм.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы