Uploaded by vasyuk.dima27

lab3

advertisement
1
ЛАБОРАТОРНА РОБОТА №3
БАГАТОПОТОЧНІСТЬ В ОС ANDROID
3.1 Мета роботи
Ознайомитись з основними принципами роботи з асинхронними
подіями в ОС Android.
3.2 Короткі теоретичні відомості
В операційній системі Android доступні всі інструменти для реалізації
асинхронних довготривалих задач, які пропонуються стандартною
бібліотекою Java. Їх можна вільно та безпечно використовувати, якщо
правильно врахувати взаємодію з життєвим циклом компонентів Android:
Activity та Fragment. Крім того, в Android SDK також є допоміжні класи для
організації багато поточності, хоча на даний момент більшість з них є
застарілими та не рекомендуються до використання. Спочатку розглянемо
найважливіші особливості Android.
3.2.1 Головний потік
При старті процесу створюється головний потік виконання, який
називають Main Thread або UI Thread. Всі методи життєвого циклу
викликаються саме в головному потоці. Більш того, як вже було розглянуто в
попередній роботі, навіть методи служб за замовчуванням також
викликаються системою в головному потоці. Прослуховувачі подій
користувача, такі як натискання на кнопку, зміна тексту та ін. також
викликаються в головному потоці. Подібний механізм має як переваги так і
недоліки для розробника. Перевагою є те, що в методах-обробниках подій
можна безпечно виконувати будь-які операції, які пов’язані з інтерфейсом
користувача. Однак з іншого боку для виконання тяжких та довготривалих
операцій необхідно вручну створювати альтернативний потік виконання, з
метою уникнення блокування інтерфейсу.
Варто пам’ятати, що будь-яка робота з інтерфейсом можлива тільки в
головному потоці. Якщо спробувати змінити, наприклад, надпис на кнопці
або колір якого-небудь компоненту, то додаток завершиться помилкою
CalledFromWrongThreadException.
В головному потоці автоматично система організовує цикл обробки
подій. Робота з циклом обробки подій відбувається за допомогою
спеціальних класів Looper та Handler. Розглянемо їх біль детально.
2
3.2.2 Класи Looper та Handler
На практиці при розробці додатків для ОС Androidнабагато частіше
розробники мають справу з класом Handler.
Handler – це спеціальний клас, який організовує чергу обробку
повідомлень. Фактично повідомленнями є об’єкти класу Message та
інтерфейсу Runnable. Всі повідомлення оброблюються в порядку запитів,
реалізуючи принцип FIFO (First Input First Output). Handler гарантує
виконання запитів в потоці, з яким був зв’язаний об’єкт Handler при
створенні.
Важлива особливість класу Handler: об’єкти даного класу можуть бути
створені лише в тих потоках, які мають правильно ініціалізований Looper.
Але оскільки в більшості випадків Handler створюється в головному потоці
виконання, то необхідності створювати Looper для нього немає, оскільки
головний потік вже має організований системою цикл обробки подій на базі
Looper.
Загалом є два найрозповсюдженіших випадки використання Handler:
1) Організація виконання операцій/обробки повідомлень в певний
момент часу в майбутньому;
2) Трансфер повідомлення/операції для обробки/виконання в потоці, в
якому працює Handler. Наприклад, якщо був створений окремий потік для
обробки даних, а потім необхідно відобразити результат в інтерфейсі
додатку, то всі операції по роботі з інтерфейсом передаються в Handler і,
таким чином, вони гарантовано будуть виконані в головному потоці додатку.
Створення об’єкту класу Handler:
Java:
// створення Handler, який працює в головному потоці виконання
Handler handler1 = new Handler(Looper.getMainLooper());
// створення Handler, який працює в тому ж поточному
// потоці виконання
Handler handler2 = new Handler(Looper.myLooper());
// створення Handler, який працює в головному потоці виконання
// та який описує логіку обробки повідомлень
Handler handler3 = new Handler(Looper.getMainLooper(), msg -> {
// some logic here
return true;
});
Kotlin:
// створення Handler, який працює в головному потоці виконання
val handler1 = Handler(Looper.getMainLooper())
// створення Handler, який працює в тому ж поточному
// потоці виконання
val handler2 = Handler(Looper.myLooper())
3
// створення Handler, який працює в головному потоці виконання
// та який описує логіку обробки повідомлень
val handler3 = Handler(Looper.getMainLooper()) {
// some logic here
true;
}
Всі інші конструктори є застарілими та не рекомендуються до
використання.
Основні методи класу Handler:
Java:
Runnable runnable = () -> {
Log.d(TAG, "Some delayed task");
};
long delayMillis = 1000;
long uptimeMillis = SystemClock.uptimeMillis() + 1000;
// виконати в потоці Handler операцію, описану об'єктом Runnable
// в наступній ітерації циклу обробки повідомлень (~18 мс)
handler.post(runnable);
// виконати в потоці Handler операцію, описану об'єктом Runnable
// через 1 секунду, база часу - момент виклику методу
handler.postDelayed(runnable, delayMillis);
// виконати в потоці Handler операцію, описану об'єктом Runnable
// через 1 секунду, база часу - момент завантаження системи
handler.postAtTime(runnable, uptimeMillis);
Kotlin:
val runnable = Runnable {
Log.d(TAG, "Some delayed task");
}
val delayMillis = 1000
val uptimeMillis = SystemClock.uptimeMillis() + 1000
// виконати в потоці Handler операцію, описану об'єктом Runnable
// в наступній ітерації циклу обробки повідомлень (~18 мс)
handler.post(runnable)
// виконати в потоці Handler операцію, описану об'єктом Runnable
// через 1 секунду, база часу - момент виклику методу
handler.postDelayed(runnable, delayMillis)
// виконати в потоці Handler операцію, описану об'єктом Runnable
// через 1 секунду, база часу - момент завантаження системи
handler.postAtTime(runnable, uptimeMillis)
В другому та третьому випадку операція, задана в runnable, буде
виконана в один момент часу.
Окрім операцій, можна також передавати повідомлення (Message). Для
обробки
повідомлень
необхідно
вказати
реалізацію
інтерфейсу
Handler.Callback в конструкторі):
4
Java:
private static final int SAY_HELLO = 1;
private static final int SAY_BYE = 2;
private static final int SAY_CUSTOM = 3;
…
Handler handler = new Handler(Looper.getMainLooper(), message ->{
Log.d(TAG, "Receiving message in the thread: " +
Thread.currentThread().getId());
switch (message.what) {
case SAY_HELLO:
Log.d(TAG, "Hello, " + message.obj);
return true;
case SAY_BYE:
Log.d(TAG, "Bye, " + message.obj);
return true;
case SAY_CUSTOM:
Log.d(TAG, "Handling custom message");
((Runnable) message.obj).run();
return true;
default:
return false;
}
});
new Thread(() -> {
Log.d(TAG, "Sending messages from the thread: "
+ Thread.currentThread().getId());
Message hiMessage = handler.obtainMessage(SAY_HELLO);
hiMessage.obj = "John";
handler.sendMessage(hiMessage);
Message byeMessage = handler.obtainMessage();
byeMessage.what = SAY_BYE;
byeMessage.obj = "Smith";
byeMessage.sendToTarget();
Message callbackMessage = Message.obtain(handler, () -> {
Log.d(TAG, "Just a callback message");
});
callbackMessage.what = SAY_CUSTOM;
callbackMessage.obj = "TEST";
callbackMessage.sendToTarget();
Message customCallbackMessage =
handler.obtainMessage(SAY_CUSTOM);
customCallbackMessage.obj = (Runnable) () -> {
Log.d(TAG, "Just a custom callback message");
};
customCallbackMessage.sendToTarget();
}).start();
5
Kotlin:
companion object {
const val SAY_HELLO = 1
const val SAY_BYE = 2
const val SAY_CUSTOM = 3
}
…
val handler = Handler(Looper.getMainLooper()) {
val threadId = Thread.currentThread().getId()
Log.d(TAG, "Receiving message in the thread: $threadId")
return when (it.what) {
SAY_HELLO -> {
Log.d(TAG, "Hello, ${it.obj}")
true
}
SAY_BYE -> {
Log.d(TAG, "Bye, ${it.obj}")
true;
}
SAY_CUSTOM -> {
Log.d(TAG, "Handling custom message")
(it.obj as Runnable).run()
true
}
else -> false;
}
})
Thread {
val threaded = Thread.currentThread().getId()
Log.d(TAG, "Sending messages from the thread: $threadId")
val hiMessage = handler.obtainMessage(SAY_HELLO)
hiMessage.obj = "John"
handler.sendMessage(hiMessage)
val byeMessage = handler.obtainMessage()
byeMessage.what = SAY_BYE
byeMessage.obj = "Smith"
byeMessage.sendToTarget()
val callbackMessage = Message.obtain(handler) {
Log.d(TAG, "Just a callback message")
}
callbackMessage.what = SAY_CUSTOM
callbackMessage.obj = "TEST"
callbackMessage.sendToTarget()
val customCallbackMessage =
handler.obtainMessage(SAY_CUSTOM)
customCallbackMessage.obj = Runnable {
Log.d(TAG, "Just a custom callback message")
}
6
customCallbackMessage.sendToTarget()
}.start()
Результати логування 4-х повідомлень:
D/Handlers: Sending messages from the thread: 8445
D/Handlers: Receiving message in the thread: 1
D/Handlers: Hello, John
D/Handlers: Receiving message in the thread: 1
D/Handlers: Bye, Smith
D/Handlers: Just a callback message
D/Handlers: Receiving message in the thread: 1
D/Handlers: Handling custom message
D/Handlers: Just a custom callback message
Далі розглянемо клас Looper.
Looper – допоміжний клас, зазвичай напряму він не використовується.
Наявність черги повідомлень, яку організують саме об’єкти класу Looper, є
необхідною умовою роботи Handler. Якщо створити Handler в потоці, в
якому немає черги повідомлень, отримаємо помилку:
Java:
new Thread(() -> {
// Конструктор без аргументів автоматично намагається
// зв'язати Handler з поточним потоком виконання.
// Зараз всі конструктори, в який напряму не вказується
// Looper, є застрарілими.
Handler handler = new Handler(); // помилка
// RuntimeException: Can't create handler inside thread that
// has not called Looper.prepare()
}).start();
Kotlin:
Thread {
// Конструктор без аргументів автоматично намагається
// зв'язати Handler з поточним потоком виконання.
// Зараз всі конструктори, в який напряму не вказується
// Looper, є застрарілими.
val handler = Handler() // помилка
// RuntimeException: Can't create handler inside thread that
// has not called Looper.prepare()
}.start();
Розглянемо приклад створення власного потоку з Looper. Варто
зазначити, що з моменту створення потоку до моменту запуску проходить
деякий час, тому необхідно або блокувати користувача до ініціалізації черги
повідомлень (стандартна реалізація в HandlerThread, який розглянемо
7
пізніше), або надати інтерфейс прослуховувача, який сповістить про успішну
ініціалізацію:
Java:
public interface OnHandlerCreatedListener {
void onHandlerCreated(LooperThread thread, Handler handler);
}
…
public class LooperThread extends Thread {
private static final int BREAK_LOOP = -1;
private Handler handler;
private OnHandlerCreatedListener listener;
public LooperThread(OnHandlerCreatedListener listener) {
this.listener = listener;
}
@Override
public void run() {
Log.d(TAG, "Started thread with ID="
+ Thread.currentThread().getId());
Looper.prepare();
Looper myLooper = Looper.myLooper();
handler = new Handler(myLooper, msg -> {
if (msg.what == BREAK_LOOP) {
myLooper.quitSafely();
return true;
}
return false;
});
notifyHandlerCreated();
Looper.loop();
Log.d(TAG, "Looper has been stopped");
}
public void shutdown() {
handler.obtainMessage(BREAK_LOOP).sendToTarget();
}
private void notifyHandlerCreated() {
Handler mainHandler =
new Handler(Looper.getMainLooper());
mainHandler.post(() ->
listener.onHandlerCreated(this, handler));
}
}
8
…
new LooperThread((thread, looperHandler) -> {
Log.d(TAG, "Action send from the thread: "
+ Thread.currentThread().getId());
looperHandler.post(() -> {
Log.d(TAG, "Action executed in the thread: "
+ Thread.currentThread().getId());
});
thread.shutdown();
}).start();
Kotlin:
typealias OnHandlerCreatedListener =
(LooperThread, Handler) -> Unit
…
class LooperThread(
private val listener: OnHandlerCreatedListener
) : Thread() {
companion object {
private const val BREAK_LOOP = -1
}
private lateinit var handler: Handler
override fun run() {
val threadId = Thread.currentThread().getId()
Log.d(TAG, "Started thread with ID=$threadId")
Looper.prepare()
Looper myLooper = Looper.myLooper()
handler = Handler(myLooper) {
return if (it.what == BREAK_LOOP) {
myLooper.quitSafely()
true
} else false
}
notifyHandlerCreated()
Looper.loop()
Log.d(TAG, "Looper has been stopped")
}
fun shutdown() {
handler.obtainMessage(BREAK_LOOP).sendToTarget()
}
9
private fun notifyHandlerCreated() {
val mainHandler = Handler(Looper.getMainLooper())
mainHandler.post { listener.invoke(this, handler) }
}
}
…
LooperThread { thread, looperHandler -> {
val threadId = Thread.currentThread().getId()
Log.d(TAG, "Action send from the thread: $threadId")
looperHandler.post {
val execId = Thread.currentThread().getId()
Log.d(TAG, "Action executed in the thread: $execId")
}
thread.shutdown();
}.start();
Логи виконання:
D/Handlers: Started thread with ID=8873
D/Handlers: Action send from the thread: 1
D/Handlers: Action executed in the thread: 8873
D/Handlers: Looper has been stopped
Android SDK надає вже реалізований клас HandlerThread, який
автоматично ініціалізує чергу повідомлень.
Рисунок 3.1 – Handler, Looper, HandlerThread
10
Він доволі простий у використанні.
попереднього коду на базі HandlerThread:
Java:
Нижче
наведено
аналог
HandlerThread handlerThread = new HandlerThread("CustomThread");
handlerThread.start();
Handler looperHandler = new Handler(handlerThread.getLooper());
Log.d(TAG, "Action send from the thread: "
+ Thread.currentThread().getId());
looperHandler.post(() -> {
Log.d(TAG, "Action executed in the thread: "
+ Thread.currentThread().getId());
});
handlerThread.quitSafely();
Kotlin:
val handlerThread = HandlerThread("CustomThread")
handlerThread.start()
val looperHandler = Handler(handlerThread.getLooper())
val sendId = Thread.currentThread().getId()
Log.d(TAG, "Action send from the thread: $sendId")
looperHandler.post(() -> {
val execId = Thread.currentThread().getId()
Log.d(TAG, "Action executed in the thread: $execId")
});
handlerThread.quitSafely();
Логи виконання:
D/Handlers: Action send from the thread: 1
D/Handlers: Action executed in the thread: 9005
3.2.3 Створення потоків виконання
Способи організації паралельних обчислень, запропонованих Android
SDK, такі як AsyncTask та Loaders, станом на 2019 рік є застарілими, а тому в
даній роботі не розглядаються. Замість них рекомендується використовувати
стандартні механізми Java та Kotlin, або ж сторонні бібліотеки.
Найпростіший спосіб створення нового потоку (але, очевидно, не
найпродуктивніший спосіб) – за допомогою класу Thread:
1) Створення підкласу Thread:
11
Java:
Log.d(TAG, "Called from: "
+ Thread.currentThread().getId());
Thread thread = new Thread() {
@Override
public void run() {
Log.d(TAG, "Executed on: "
+ Thread.currentThread().getId());
}
};
thread.start();
Kotlin:
val sendId = Thread.currentThread().getId()
Log.d(TAG, "Called from: $sendId")
val thread = object : Thread() {
override fun run() {
Log.d(TAG, "Executed on: ${Thread.currentThread().getId()}")
}
}
thread.start();
2) Передача об’єкту Runnable в конструкторі:
Java:
Log.d(TAG, "Called from: "
+ Thread.currentThread().getId());
Thread thread = new Thread(() -> {
Log.d(TAG, "Executed on: " + Thread.currentThread().getId());
});
thread.start();
Kotlin:
Log.d(TAG, "Called from: ${Thread.currentThread().getId()}")
val thread = Thread {
Log.d(TAG, "Executed on: ${Thread.currentThread().getId()}")
}
thread.start();
Результат:
D/Handlers: Called from: 1
D/Handlers: Executed on: 9157
Інший спосіб – за допомогою набору служб управління потоками
(Executor services), не слід путати зі службами в Android. Клас Executors
надає безліч статичних методів для створення різних видів цих служб:
12
Java:
// всі операції виконуються по черзі, одна за одною,
// в одному потоці
ExecutorService executorService1 =
Executors.newSingleThreadExecutor();
// операції виконуються паралельно в декілької потоках,
// потоки створюються лише при необхідності, після виконання
// задачі живуть 1 хвилину та можуть в цес час бути
// використаними іншими задачами
ExecutorService executorService2 =
Executors.newCachedThreadPool();
// операції виконуються паралельно в декілької потоках,
// максимальна кількість потоків задається в конструкторі.
// Якщо нема вільних потоків, задача очікує звільнення потоку
ExecutorService executorService3 =
Executors.newFixedThreadPool(4);
// дозволяє виконувати періодичні або відкладені у
// часі задачі
ScheduledExecutorService executorService4 =
Executors.newScheduledThreadPool(4);
Kotlin:
// всі операції виконуються по черзі, одна за одною,
// в одному потоці
val executorService1 = Executors.newSingleThreadExecutor()
// операції виконуються паралельно в декілької потоках,
// потоки створюються лише при необхідності, після виконання
// задачі живуть 1 хвилину та можуть в цес час бути
// використаними іншими задачами
val executorService2 = Executors.newCachedThreadPool()
// операції виконуються паралельно в декілької потоках,
// максимальна кількість потоків задається в конструкторі.
// Якщо нема вільних потоків, задача очікує звільнення потоку
val executorService3 = Executors.newFixedThreadPool(4)
// дозволяє виконувати періодичні або відкладені у
// часі задачі
val executorService4 = Executors.newScheduledThreadPool(4)
Для відправки задачі на виконання використовуються метод submit(),
який можу приймати об’єкти типу Runnable або Callable. Останній дозволяє
повертати результат та генерувати виключення (Exceptions):
Java:
ExecutorService executorService =
Executors.newSingleThreadExecutor();
Future<Integer> future = executorService.submit(() -> {
Log.d(TAG, "Generating random number");
Random random = new Random();
Thread.sleep(5000);
return random.nextInt();
});
13
// якщо необхідно припинити виконання задачі
// Якщо в параметрі вказано значення true - потік
// може бути примусово зупинено (методи, які очікують або які
// виконують роботу з потоками даних, в цьому випадку можуть
// викинути виключення InterruptedException.
future.cancel(true);
// перевірка, чи була задача припинена
future.isCancelled();
// отримати результат або очікувати, якщо результату ще немає
future.get();
Kotlin:
val executorService =
Executors.newSingleThreadExecutor();
val future = executorService.submit (Callable {
val random = Random()
Thread.sleep(5000);
random.nextInt()
})
// якщо необхідно припинити виконання задачі
// Якщо в параметрі вказано значення true - потік
// може бути примусово зупинено (методи, які очікують або які
// виконують роботу з потоками даних, в цьому випадку можуть
// викинути виключення InterruptedException.
future.cancel(true)
// перевірка, чи була задача припинена
future.isCancelled()
// отримати результат або очікувати, якщо результату ще немає
future.get()
Стандартні методи класу Future для отримання результатів зазвичай не
є прийнятними в Android, оскільки блокування UI недопустиме. Тому далі
розглянемо механізми методів зворотного виклику.
3.2.4 Сповіщення про результати виконання задач
Очевидно, що в більшості випадків, виконання задачі має на меті
деякий результат. Наприклад, завантаження списку даних, результат обробки
зображення, результати фільтрації та/або пошуку і т.д. Результати виконання
задачі мають бути відображені в інтерфейсі додатку, і тут постає проблема:
задачі виконуються в потоках, відмінних від Main Thread, а зміни в
інтерфейсі мають відбуватися виключно в головному потоці. Для вирішення
даної проблеми використовуються комбінація механізму методів зворотного
виклику та класу Handler.
14
Розглянемо найпростіший варіант на базі single thread executor.
Реалізація «в лоб» може мати наступний вигляд:
Java:
public class TaskActivity extends AppCompatActivity {
public static final String TAG =
TaskActivity.class.getSimpleName();
private ExecutorService executorService;
private Handler handler = new Handler();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState){
super.onCreate(savedInstanceState);
executorService = Executors.newSingleThreadExecutor();
findViewById(R.id.execButton).setOnClickListener(v -> {
progressBar.setVisibility(View.VISIBLE);
executorService.submit(() -> {
generateNumber();
});
});
findViewById(R.id.cancelButton).setOnClickListener(v -> {
progressBar.setVisibility(View.GONE);
});
}
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);
executorService.shutdownNow();
}
private void generateNumber() {
// worker thread:
Random random = new Random();
try {
Thread.sleep(5000);
int number = random.nextInt();
handler.post(() -> {
// main thread:
progressBar.setVisibility(View.GONE);
numberTextView.setText(String.valueOf(number));
});
} catch (Exception e) {
Log.e(TAG, "Error!", e);
}
}
}
15
Kotlin:
class TaskActivity : AppCompatActivity() {
companion object {
val TAG = TaskActivity::class.java.simpleName
}
private lateinit var executorService: ExecutorService
private val handler = Handler()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
executorService = Executors.newSingleThreadExecutor()
execButton.setOnClickListener {
progressBar.visibility = View.VISIBLE
executorService.submit { generateNumber() }
}
cancelButton.setOnClickListener {
progressBar.visibility = View.GONE
}
}
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
executorService.shutdownNow()
}
private fun generateNumber() {
// worker thread:
try {
Thread.sleep(5000)
val number = Random.nextInt()
handler.post {
// main thread:
progressBar.visibility = View.GONE
numberTextView.text = number.toString()
}
} catch (e: Exception) {
Log.e(TAG, "Error!", e)
}
}
}
Очевидно, що в даній реалізації багато недоліків, серед яких:
1) При повороті екрану задача буде знищена
2) Завдання Activity – робота з інтерфейсом, в даному ж випадку вона
також організовує виконання задач в інших потоках виконання
3) Для кожної задачі необхідно знову прописувати постійні виклики
Handler.post() для отримання результатів.
4) Задачі не можна перевикористовувати в інших екранах.
16
Для вирішення даних проблем використовуються:
1) Retain-фрагменти
2) Архітектурні підходи, такі як MVP (Model-View-Presenter) та MVVM
(Model-View-ViewModel).
Фактично, бібліотеки, які реалізують архітектурні патерни MVP та
MVVM в своїй реалізацій тим чи іншим чином все одно використовують
Retain-фрагменти. Тому розглянемо їх більш детально.
Retain-фрагменти – це фрагменти, які виживають при змінах
конфігурації, тобто якщо активність знищується, то фрагменти від’єднуються
від неї, очікують створення нового інстансу активності, та знову
під’єднуються. Зазвичай такі фрагменти не мають інтерфейсу (хоча
теоретично це не забороняється, але й не рекомендується). Для створення
Retain-фрагменту достатньо в методі onCreate() викликати метод
setRetainInstance() з аргументом true:
Java:
public class MyRetainFragment extends Fragment {
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
setRetainInstance(true);
}
}
Kotlin:
class MyRetainFragment : Fragment() {
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
retainInstance = true
}
}
Якщо перемістити логіку виконання задач в Retain-фрагмент, то такі
задачі продовжать виконуватись навіть якщо повернути екран додатку.
Отримати результати виконання можна за допомогою звичайних механізмів
комунікації між фрагментами.
3.3 Хід виконання роботи
Модифікуємо проект RandomGallery з минулої лабораторної роботи
таким чином, щоб всі асинхронні операції виконувались не в службах, а
потоках.
Створимо клас індикації статусу задачі. Наприклад, він може мати
наступний вигляд:
public enum Status {
IN_PROGRESS,
17
SUCCESS,
ERROR
}
Створимо клас, який міститиме не тільки статус, а й результати
виконання задачі. Назвемо його, наприклад, Result<T>. Даний клас матиме
Generic-аргумент Т – тип результату:
public class Result<T> {
private T data;
private Throwable error;
private Status status;
private Result(T data, Throwable error, Status status) {
this.data = data;
this.error = error;
this.status = status;
}
public static <T> Result<T> success(T data) {
return new Result<>(data, null, Status.SUCCESS);
}
public static <T> Result<T> error(Throwable error) {
return new Result<>(null, error, Status.ERROR);
}
public static <T> Result<T> inProgress() {
return new Result<>(null, null, Status.IN_PROGRESS);
}
public T getData() {
return data;
}
public Throwable getError() {
return error;
}
public Status getStatus() {
return status;
}
}
Задамо функціональний інтерфейс прослуховування результатів задачі
TaskListener<T>, який міститиме один метод зворотного виклику:
public interface TaskListener<T> {
void onResults(Result<T> results);
}
18
Далі опишемо інтерфейс взаємодії з результатом задачі
TaskSubject<T>. Надамо можливість прослуховувати задачу та припиняти її
роботу:
public interface TaskSubject<T> {
void addListener(TaskListener<T> listener);
void removeListener(TaskListener<T> listener);
void cancel();
}
Створимо спеціальний тип виключення, який буде сповіщувати про те,
що задача була перервана в результаті виклику методу TaskSubject.cancel():
public class CancelledException extends RuntimeException {
public CancelledException() {
super("Task has been cancelled");
}
}
Далі реалізуємо Retain-фрагмент TaskManager, який відповідатиме за
управління асинхронними задачами:
public class TaskManagerFragment extends Fragment {
public static final String TAG =
TaskManagerFragment.class.getSimpleName();
private ExecutorService executorService;
private Handler handler =
new Handler(Looper.getMainLooper());
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// помічаємо фрагмент як такий, що має "виживати"
// при зміні конфігурації
setRetainInstance(true);
executorService = Executors.newSingleThreadExecutor();
}
@Override
public void onDestroy() {
super.onDestroy();
// перериваємо виконання всіх задач
executorService.shutdownNow();
}
public <T> TaskSubject<T> submitTask(Callable<T> callable) {
TaskSubjectImpl<T> taskSubject = new TaskSubjectImpl<>();
taskSubject.future = executorService.submit(() -> {
try {
T result = callable.call();
taskSubject.setResult(Result.success(result));
19
} catch (Exception e) {
taskSubject.setResult(Result.error(e));
}
});
return taskSubject;
}
/**
* Внутрішня реалізація TaskSubject
*/
class TaskSubjectImpl<T> implements TaskSubject<T> {
// тут зберігається поточний стан та результат
// виконання задачі
private Result<T> result = Result.inProgress();
// набір слухачів задачі
// LinkedHashSet зберагіє елементи в порядку додавання
private volatile Set<TaskListener<T>> listeners =
new LinkedHashSet<>();
// об'єкт Future, в даному випадку він зберігається для
// можливості відміни виконання задачі
private Future<?> future;
@Override
public synchronized void
addListener(TaskListener<T> listener) {
this.listeners.add(listener);
listener.onResults(result);
}
@Override
public synchronized void
removeListener(TaskListener<T> listener) {
this.listeners.remove(listener);
}
@Override
public void cancel() {
if (!future.isCancelled()) {
// відразу сповіщуємо про відміну задачі
setResult(Result.error(new
CancelledException()));
future.cancel(true);
}
}
void setResult(Result<T> result) {
if (isMainThread()) {
// знаходимось в головному потоці, немає
// потреби використовувати Handler
doSetResult(result);
} else {
handler.post(() -> doSetResult(result));
}
}
20
private void doSetResult(Result<T> result) {
if (this.result.getStatus() != Status.IN_PROGRESS) {
// якщо результат задачі вже встановлено, то
// ігноруємо подальші спроби зміни результату
return;
}
this.result = result;
List<TaskListener<T>> listeners =
new ArrayList<>(this.listeners);
for (TaskListener<T> listener : listeners) {
listener.onResults(result);
}
}
private boolean isMainThread() {
return Thread.currentThread().getId()
== Looper.getMainLooper().getThread().getId();
}
}
}
Для того, щоб не втрачати посилання на поточний стан виконання
задач при зміні конфігурації, необхідно додати ще один Retain-фрагмент,
основне завдання якого – зберігати посилання на поточні результати:
public class TaskResultsFragment extends Fragment {
public static final String TAG =
TaskResultsFragment.class.getSimpleName();
TaskSubject<Boolean> hasUpdatesSubject;
TaskSubject<Boolean> syncSubject;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
@Override
public void onDestroy() {
super.onDestroy();
if (hasUpdatesSubject != null) {
hasUpdatesSubject.cancel();
}
if (syncSubject != null) {
syncSubject.cancel();
}
}
}
Далі створимо класи задач.
Задача на перевірку оновлень:
21
public class CheckUpdatesTask implements Callable<Boolean> {
private RandomGalleryClient client;
public CheckUpdatesTask(RandomGalleryClient client) {
this.client = client;
}
@Override
public Boolean call() throws Exception {
return client.hasUpdates();
}
}
Задача на синхронізацію:
public class SyncTask implements Callable<Boolean> {
private RandomGalleryClient client;
public SyncTask(RandomGalleryClient client) {
this.client = client;
}
@Override
public Boolean call() throws Exception {
return client.syncGallery(EMPTY_LISTENER);
}
private static ProgressListener EMPTY_LISTENER = p -> {};
}
Додамо клас-фабрику для створення даних задач:
public class RandomGalleryTasks {
private RandomGalleryClient client;
public RandomGalleryTasks(RandomGalleryClient client) {
this.client = client;
}
public Callable<Boolean> createCheckUpdatesTask() {
return new CheckUpdatesTask(client);
}
public Callable<Boolean> createSyncTask() {
return new SyncTask(client);
}
}
Модифікуємо макетний файл activity_main.xml таким чином, щоб
замість TextView з відображенням прогресу у відсотках відображався
компонент ProgressBar:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
22
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
tools:context="ua.cn.stu.randomgallery.app.MainActivity">
<RelativeLayout
android:id="@+id/messageContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/notification"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/messageTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:paddingStart=
"@dimen/default_notification_padding"
android:textColor="@android:color/white"
android:layout_toStartOf="@id/actionTextView"
tools:text="@string/update_available"/>
<TextView
android:id="@+id/actionTextView"
style="@style/BottomNotification"
android:background=
"?android:attr/selectableItemBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/update"/>
<ProgressBar
android:id="@+id/syncProgressBar"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd=
"@dimen/default_notification_padding"
android:indeterminateTint="@android:color/white" />
</RelativeLayout>
<FrameLayout
android:id="@+id/fragmentContainer"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf=
"@id/messageContainer" />
23
</androidx.constraintlayout.widget.ConstraintLayout>
Внесемо відповідні зміни до MainActivity:
public class MainTasksActivity
extends AppCompatActivity
implements Router {
private App app;
private TextView messageTextView;
private ProgressBar syncProgressBar;
private TextView actionTextView;
private View messageContainer;
private TaskManagerFragment taskManagerFragment;
private TaskResultsFragment taskResultsFragment;
private RandomGalleryTasks tasks;
private boolean syncInProgress;
private boolean hasUpdates;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_tasks);
app = (App) getApplicationContext();
tasks = new RandomGalleryTasks(app.getGalleryClient());
if (savedInstanceState == null) {
// clean launch, no fragments
taskManagerFragment = new TaskManagerFragment();
taskResultsFragment = new TaskResultsFragment();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragmentContainer, new GalleryFragment())
.add(taskManagerFragment, TaskManagerFragment.TAG)
.add(taskResultsFragment, TaskResultsFragment.TAG)
.commit();
} else {
taskManagerFragment =
(TaskManagerFragment) getSupportFragmentManager()
.findFragmentByTag(TaskManagerFragment.TAG);
taskResultsFragment =
(TaskResultsFragment) getSupportFragmentManager()
.findFragmentByTag(TaskResultsFragment.TAG);
}
actionTextView = findViewById(R.id.actionTextView);
actionTextView.setOnClickListener(v -> {
if (taskResultsFragment.syncSubject != null) {
taskResultsFragment.syncSubject
.removeListener(syncListener);
}
24
taskResultsFragment.syncSubject = taskManagerFragment
.submitTask(tasks.createSyncTask());
taskResultsFragment.syncSubject
.addListener(syncListener);
});
messageTextView = findViewById(R.id.messageTextView);
messageContainer = findViewById(R.id.messageContainer);
syncProgressBar = findViewById(R.id.syncProgressBar);
updateUi();
}
@Override
protected void onStart() {
super.onStart();
if (taskResultsFragment.hasUpdatesSubject == null) {
taskResultsFragment.hasUpdatesSubject =
taskManagerFragment
.submitTask(tasks.createCheckUpdatesTask());
}
taskResultsFragment.hasUpdatesSubject
.addListener(hasUpdatesListener);
if (taskResultsFragment.syncSubject != null) {
taskResultsFragment.syncSubject
.addListener(syncListener);
}
}
@Override
protected void onStop() {
super.onStop();
taskResultsFragment.hasUpdatesSubject
.removeListener(hasUpdatesListener);
if (taskResultsFragment.syncSubject != null) {
taskResultsFragment.syncSubject
.removeListener(syncListener);
}
}
@Override
public void launchDetails(View sharedView,
String localPhotoId) {
Fragment fragment = DetailsFragment
.newInstance(localPhotoId);
// transition for shared image
TransitionSet transitionSet = new TransitionSet()
.setOrdering(TransitionSet.ORDERING_TOGETHER)
.addTransition(new ChangeBounds())
.addTransition(new ChangeTransform())
.addTransition(new ChangeImageTransform());
fragment.setSharedElementEnterTransition(transitionSet);
fragment.setSharedElementReturnTransition(transitionSet);
25
// transitions for fragments
fragment.setEnterTransition(new Fade());
getSupportFragmentManager()
.findFragmentById(R.id.fragmentContainer)
.setReturnTransition(new Fade());
getSupportFragmentManager()
.beginTransaction()
.addSharedElement(
sharedView, getString(R.string.shared_tag))
.addToBackStack(null)
.replace(R.id.fragmentContainer, fragment)
.commit();
}
@Override
public void back() {
onBackPressed();
}
private void updateUi() {
if (syncInProgress) {
messageContainer.setVisibility(View.VISIBLE);
messageTextView.setText(R.string.updating_gallery);
syncProgressBar.setVisibility(View.VISIBLE);
actionTextView.setVisibility(View.INVISIBLE);
} else if (hasUpdates) {
messageContainer.setVisibility(View.VISIBLE);
messageTextView.setText(R.string.update_available);
syncProgressBar.setVisibility(View.INVISIBLE);
actionTextView.setVisibility(View.VISIBLE);
} else {
messageContainer.setVisibility(View.GONE);
}
}
private TaskListener<Boolean> hasUpdatesListener = res -> {
if (res.getStatus() == Status.SUCCESS) {
this.hasUpdates = res.getData();
updateUi();
}
};
private TaskListener<Boolean> syncListener = res -> {
this.syncInProgress = res.getStatus() ==
Status.IN_PROGRESS;
if (res.getStatus() == Status.SUCCESS && res.getData()) {
this.hasUpdates = false;
}
updateUi();
};
}
26
Після внесення усіх вище зазначених змін, структура проекту матиме
приблизно наступний вигляд (назви пакетів/класів можуть відрізнятись):
Рисунок 3.2 – Структура проекту
3.4 Завдання на самостійну роботу
1) Реалізувати можливість відображення прогресу синхронізації у
відсотках (за аналогією з попередньою роботою).
2) Ознайомитись самостійно з Android Architecture Components та
архітектурою MVVM. Розглянути класи: LifecycleOwner, LifecycleObserver,
ViewModel, LiveData, MutableLiveData.
1) Реалізувати Android-додаток згідно варіанту завдання, наведеному в
таблиці 3.1. Номер варіанту завдання розраховується за допомогою додатку,
розробленого в процесі виконання лабораторної роботи №1.
Таблиця 3.1 – Варіанти завдань
№
1
Завдання
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб розрахунок наступного ходу виконувався в потоці HandlerThread.
27
2
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб генерація числа та розрахунок відповіді відбувались в потоці, який створюється
за допомогою класу Thread з чергою повідомлень Looper.
3
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб генерація відповіді на запитання відбувалась за допомогою ExecutorService.
4
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб розрахунок заощаджень відбувався за допомогою потоку, що наслідується від
класу Thread.
5
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб список запитань завантажувався з текстового файлу за допомогою
ExecutorService.
6
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб зберігання та завантаження відбувалось у потоці на базі класу Thread.
7
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб слова завантажувались у потоці HandlerThread.
8
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб налаштування та список слів завантажувались з Shared Preferences за допомогою
ExecutorService.
9
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб логіка конвертування була реалізована всередині потоку на базі класу Thread з
чергою повідомлень Looper.
10
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб за роботу таймеру відповідав потік HandlerThread.
11
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб розрахунок наступного ходу виконувався в потоці Thread.
12
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб генерація числа та розрахунок відповіді відбувались в потоці, який створюється
за допомогою ExecutorService.
13
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб генерація відповіді на запитання відбувалась в потоці HandlerThread.
14
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб розрахунок заощаджень відбувався за допомогою потоків ExecutorService.
15
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб список запитань завантажувався з текстового файлу за допомогою класу Thread.
16
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб зберігання та завантаження відбувалось у потоці на базі класу Thread з чергою
повідомлень Looper.
28
17
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб слова завантажувались за допомогою ExecutorService.
18
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб налаштування та список слів завантажувались з Shared Preferences за допомогою
HandlerThread.
19
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб логіка конвертування була реалізована всередині потоку на базі класу Thread.
20
Модифікувати розроблений у попередній лабораторній роботі додаток таким чином,
щоб за роботу таймеру відповідав ScheduledExecutorService.
3.5 Звітність
Звіт з лабораторної роботи має містити:
1) Номер та назву роботи;
2) Мету роботи;
3) Хід виконання роботи:
a. Результати виконання п.п. 3.3, 3.4 зі скріншотами та логами
виконання;
b. Тексти класів та макетів інтерфейсу додатку;
c. Скріншоти додатку;
d. Опис виявлених проблем при модифікації додатку та шляхи їх
вирішення;
4) Висновки.
3.6 Контрольні запитання
Поняття головного потоку (Main Thread).
Особливості виконання операцій з інтерфейсом користувача при
реалізації багато поточних додатків.
3) Відмінності головного потоку від інших потоків виконання.
4) Клас Handler. Виконання вікладених дій у часі.
5) Клас Handler. Передача та обробка повідомлень (Messages).
6) Клас Looper.
7) Порядок створення черги повідомлень для потоків, відмінних від
головного потоку (Looper + Handler).
8) Клас HandlerThread. Порядок виконання запитів у черзі
повідомлень.
9) Завершення HandlerThread. Завершення циклу обробки подій
Looper.
10) Поняття методів зворотнього виклику.
1)
2)
29
11) Поняття функціонального інтерфейсу в Java.
12) Інтерфейси Runnable та Callable<V>.
13) Методи створення нових потоків.
14) Retain-фрагменти: відмінності від звичайних фрагментів та їх
призначення.
15) Створення Retain-фрагменту.
Download