Створення потоків і візуальний інтерфейс
Коли ми запускаємо додаток на Android, система створює потік, який називається основним потоком додатка або UI-потоком. Цей потік обробляє всі зміни та події інтерфейсу користувача. Однак для допоміжних операцій, таких як відправка або завантаження файлів, тривалі обчислення тощо, ми можемо створювати додаткові потоки.
Для створення нових потоків нам доступний стандартний функціонал класу Thread з базової бібліотеки Java з пакету java.util.concurrent, які не викликають особливих труднощів. Однак труднощі можуть виникнути при оновленні візуального інтерфейсу з потоку.
Наприклад, створимо простий додаток із використанням потоків. Окреслимо наступну розмітку інтерфейсу в файлі activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:textSize="22sp"
app:layout_constraintBottom_toTopOf="@id/button"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Запустити потік"
app:layout_constraintTop_toBottomOf="@id/textView"
app:layout_constraintLeft_toLeftOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Тут визначена кнопка для запуску фоновго потоку, а також текстове поле для відображення деяких даних, які будуть генеруватися в запущеному потоці.
Далі визначимо в класі MainActivity наступний код:
package com.example.threadapp;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.util.Calendar;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.textView);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Визначаємо об'єкт Runnable
Runnable runnable = new Runnable() {
@Override
public void run() {
// отримуємо поточний час
Calendar c = Calendar.getInstance();
int hours = c.get(Calendar.HOUR_OF_DAY);
int minutes = c.get(Calendar.MINUTE);
int seconds = c.get(Calendar.SECOND);
String time = hours + ":" + minutes + ":" + seconds;
// відображаємо в текстовому полі
textView.setText(time);
}
};
// Визначаємо об'єкт Thread - новий потік
Thread thread = new Thread(runnable);
// Запускаємо потік
thread.start();
}
});
}
}
Отже, тут до кнопки прикріплений обробник натискання, який запускає новий потік. Створювати та запускати потік в Java можна різними способами. У цьому випадку самі дії, які виконуються в потоці, визначаються в методі run() об'єкта Runnable:
Runnable runnable = new Runnable() {
@Override
public void run() {
// отримуємо поточний час
Calendar c = Calendar.getInstance();
int hours = c.get(Calendar.HOUR_OF_DAY);
int minutes = c.get(Calendar.MINUTE);
int seconds = c.get(Calendar.SECOND);
String time = hours + ":" + minutes + ":" + seconds;
// відображаємо в текстовому полі
textView.setText(time);
}
};
Для прикладу ми отримуємо поточний час і намагаємося відобразити його в елементі TextView.
Далі визначаємо об'єкт потоку - об'єкт Thread, який приймає об'єкт Runnable. І за допомогою методу start() запускаємо потік:
// Визначаємо об'єкт Thread - новий потік
Thread thread = new Thread(runnable);
// Запускаємо потік
thread.start();
Начебто нічого складного. Але якщо ми запустимо додаток і натиснемо на кнопку, то ми зіткнемося з помилкою:

Оскільки змінювати стан візуальних елементів, звертатися до них ми можемо тільки в основному потоці додатка або UI-потоці.
Для вирішення цієї проблеми — взаємодії у вторинних потоках з елементами графічного інтерфейсу клас View() визначає метод post():
boolean post (Runnable action)
Як параметр він приймає задачу, яку треба виконати, і повертає логічне значення — true, якщо задача Runnable успішно поміщена в чергу повідомлень, або false, якщо не вдалося розмістити в черзі.
Також у класу View є аналогічний метод:
boolean postDelayed (Runnable action, long millsec)
Він також запускає задачу, тільки через певний проміжок часу в мілісекундах, який вказується в другому параметрі.
Тепер змінемо код MainActivity наступним чином:
package com.example.threadapp;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.util.Calendar;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.textView);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Визначаємо об'єкт Runnable
Runnable runnable = new Runnable() {
@Override
public void run() {
// отримуємо поточний час
Calendar c = Calendar.getInstance();
int hours = c.get(Calendar.HOUR_OF_DAY);
int minutes = c.get(Calendar.MINUTE);
int seconds = c.get(Calendar.SECOND);
String time = hours + ":" + minutes + ":" + seconds;
// відображаємо в текстовому полі
textView.post(new Runnable() {
public void run() {
textView.setText(time);
}
});
}
};
// Визначаємо об'єкт Thread - новий потік
Thread thread = new Thread(runnable);
// Запускаємо потік
thread.start();
}
});
}
}
Тепер для оновлення TextView застосовується метод post:
textView.post(new Runnable() {
public void run() {
textView.setText(time);
}
});
Тобто, в методі run(), переданому в метод post() об'єкта Runnable, ми можемо звертатися до елементів візуального інтерфейсу та взаємодіяти з ними.
Подібним чином можна працювати і з іншими віджетами, які успадковуються від класу View.