Как известно каждому Android-разработчику Android SDK предоставляет несколько способов заставить опреденный кусок кода выполнятся в параллельном потоке. Многопоточность это хорошо, но кроме ее организации нужно также наладить канал общения между потоками. Например, между UI-потоком и потоком, в котором выполняются фоновые задачи. В данном коротком эссе хочу осветить один из способов, основанный на применении встроенного класса ResultReceiver.
Вместо вступления
В большинстве Android-проектов приходится организовывать общение с внешним миром, т.е. организовывать сетевое взаимодействие. Не буду повторяться почему выполнять такой долгоиграющий код в UI-потоке плохо. Это всем известно. Более того, начиная с API 11 (Honeycomb который) система бьет разработчика по рукам исключением когда тот пытается в UI-потоке делать сетевые вызовы.
Одним из вариантов общения UI-потока с параллельным потоком (в котором, к примеру, выполняется http-запрос) есть подход, основанный на применении встроенного системного класса android.os.ResultReceiver
совместно с сервисом.
Немного о архитектуре подхода
Для организации отдельного потока я выбрал IntentService. Почему именно он, а не простой Service? Потому, что IntentService при поступлении к нему команды автоматически начинает выполнять метод onHandleIntent(Intent intent)
в отдельном от UI потоке. Простой Service такого не позволяет ибо он выполняется в основном потоке. Организовывать запуск потока из Service'а нужно самостоятельно.
Общение между Activity и IntentService-ом будет происходить с помощью Intent'ов.
Код
Сначала исходный код, потом ниже краткие комментарии к тому, что там и как происходит.
Реализация ResultReceiver'а
public class AppResultsReceiver extends ResultReceiver {
public interface Receiver {
public void onReceiveResult(int resultCode, Bundle data);
}
private Receiver mReceiver;
public AppResultsReceiver(Handler handler) {
super(handler);
}
public void setReceiver(Receiver receiver) {
mReceiver = receiver;
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (mReceiver != null) {
mReceiver.onReceiveResult(resultCode, resultData);
}
}
}
Здесь следует обратить внимание на коллбэк (Receiver
). При получении результата в onReceiveResult()
делается проверка на не-null коллбэка. Дальше в коде активити будет показано как активировать и деактивировать ресивер с помощью этого коллбэка.
IntentService
public class AppService extends IntentService {
public AppService() {
this("AppService");
}
public AppService(String name) {
super(name);
}
@Override
protected void onHandleIntent(Intent intent) {
final ResultReceiver receiver = intent.getParcelableExtra(Constants.RECEIVER);
receiver.send(Constants.STATUS_RUNNING, Bundle.EMPTY);
final Bundle data = new Bundle();
try {
Thread.sleep(Constants.SERVICE_DELAY);
data.putString(Constants.RECEIVER_DATA, "Sample result data");
} catch (InterruptedException e) {
data.putString(Constants.RECEIVER_DATA, "Error");
}
receiver.send(Constants.STATUS_FINISHED, data);
}
}
onHandleIntent()
будет вызван после того, как вызывающий код (UI-классы etc.) выполнит startService()
. Инстанс ResultReceiver'а будет извлечен из интента и ему тут же будет отослана команда «ОК, я пошел трудиться». После выполнения полезной работы в этом методе результаты (извлеченные из JSON классы-модели, строки, что-угодно) помещается в бандл и отправляется ресиверу. Причем для индикации типа ответа используются разные коды (описаны константами). Как ResultReceiver получает и отправляет данные можно почитать в его исходниках.
Посылка команды сервису и обработка результата (Activity)
public class MainActivity extends Activity implements AppResultsReceiver.Receiver {
private AppResultsReceiver mReceiver;
private ProgressBar mProgress;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mProgress = (ProgressBar) findViewById(R.id.progressBar);
}
@Override
protected void onResume() {
super.onResume();
mReceiver = new AppResultsReceiver(new Handler());
mReceiver.setReceiver(this);
}
@Override
protected void onPause() {
super.onPause();
mReceiver.setReceiver(null);
}
public void onStartButtonClick(View anchor) {
final Intent intent = new Intent("SOME_COMMAND_ACTION", null, this, AppService.class);
intent.putExtra(Constants.RECEIVER, mReceiver);
startService(intent);
}
@Override
public void onReceiveResult(int resultCode, Bundle data) {
switch (resultCode) {
case Constants.STATUS_RUNNING :
mProgress.setVisibility(View.VISIBLE);
break;
case Constants.STATUS_FINISHED :
mProgress.setVisibility(View.INVISIBLE);
Toast.makeText(this, "Service finished with data: "
+ data.getString(Constants.RECEIVER_DATA), Toast.LENGTH_SHORT).show();
break;
}
}
}
Здесь все просто. Activity реализует интерфейс AppResultsReceiver.Receiver
. При старте создает экземпляр ресивера, при паузе — отвязывается от прослушивания ответов от сервиса. При клике на кнопку формируется команда (интент), в него помещается ссылка на наш ResultReceiver
и стартуется сервис.
При получении ответа от сервиса в методе onReceiveResult()
проверяется код ответа и выполняется соответствующее действие. Вот и все.
Демо-приложение выглядит просто, оно имеет всего одну кнопку «Послать запрос».
Исходный код демо-проекта доступен на GitHub
Вместо заключения
Обработка команды в фоновом сервисе реализована до безобразия просто: поток просто ставится на паузу на некоторое время. Конечно же в реальных приложениях нужно в интенте передавать код команды (action), которую нужно выполнить, дополнительные параметры и прочее. ООП вам в руки. Также стоит помнить, что данные (например, модели), которые будучи упакованными в бандл должны быть Parcelable-объектами. Это повысит эффективность их сериализации.
Конечно же описанный подход не есть истина в последней инстанции. Мы вольны выбирать разные архитектурные подходы, средства и комбинации. Будь то AsyncTask'и, Service+Thread+BroadcastReceiver или «ручная» передача Message'ей посредством Handler'а в UI-поток. Выбирать, как говориться, вам. Но это уже совсем другая история.
Автор: gshock