При использовании вторичных потоков следует учитывать следующий момент. Более оптимальным способом является работа потоков с фрагментом, нежели непосредственно с activity. Например, определим в файле 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"> <Button android:id="@+id/progressBtn" android:text="Запуск" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toTopOf="@id/statusView" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent"/> <TextView android:id="@+id/statusView" android:text="Статус" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toTopOf="@id/indicator" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@id/progressBtn" /> <ProgressBar android:id="@+id/indicator" style="@android:style/Widget.ProgressBar.Horizontal" android:layout_width="0dp" android:layout_height="wrap_content" android:max="100" android:progress="0" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/statusView"/> </androidx.constraintlayout.widget.ConstraintLayout>
Здесь определена кнопка для запуска вторичной задачи и элементы TextView и ProgressBar, которые отображают индикацию выполнения задачи.
В классе 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.ProgressBar; import android.widget.TextView; public class MainActivity extends AppCompatActivity { int currentValue = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ProgressBar indicatorBar = findViewById(R.id.indicator); TextView statusView = findViewById(R.id.statusView); Button btnFetch = findViewById(R.id.progressBtn); btnFetch.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Runnable runnable = new Runnable() { @Override public void run() { for(; currentValue <= 100; currentValue++){ try { statusView.post(new Runnable() { public void run() { indicatorBar.setProgress(currentValue); statusView.setText("Статус: " + currentValue); } }); Thread.sleep(400); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Thread thread = new Thread(runnable); thread.start(); } }); } }
Здесь по нажатию кнопки мы запускаем задачу Runnable, в которой в цикле от 0 до 100 изменяем показатели ProgressBar и TextView, имитируя некоторую долгую работу.
Однако если в процессе работы задачи мы изменим ориентацию мобильного устройства, то произойдет пересоздание activity, и приложение перестанет работать должным образом.
В данном случае проблема упирается в состояние, которым оперирует поток, а именно - переменную currentValue
, к значению которой привязаны
виджеты в Activity.
Для подобных случаев в качестве решения проблемы предлагается использовать ViewModel. Итак, добавим в ту же папку, где находится файл MainActivity.java, новый класс MyViewModel со следующим кодом:
package com.example.threadapp; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; public class MyViewModel extends ViewModel { private MutableLiveData<Boolean> isStarted = new MutableLiveData<Boolean>(false); private MutableLiveData<Integer> value; public LiveData<Integer> getValue() { if (value == null) { value = new MutableLiveData<Integer>(0); } return value; } public void execute(){ if(!isStarted.getValue()){ isStarted.postValue(true); Runnable runnable = new Runnable() { @Override public void run() { for(int i = value.getValue(); i <= 100; i++){ try { value.postValue(i); Thread.sleep(400); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Thread thread = new Thread(runnable); thread.start(); } } }
Итак, здесь определен класс MyViewModel
, который унаследован от класса ViewModel, специально предназначенного для хранения и управления состоянием или моделью.
В качестве состояния здесь определены для объекта. В первую очередь, это числовое значение, к которым будут привязан виджеты MainActivity. И во-вторых, нам нужен некоторый индикатор того, что поток уже запущен, чтобы по нажатию на кнопку не было запущено лишних потоков.
Для хранения числового значения предназначена переменная value
:
private MutableLiveData<Integer> value;
Для привязки к этому значению оно имеет тип MutableLiveData
. А поскольку мы будем хранить в этой переменной числовое значение, то тип переменной
типизирован типом Integer
.
Для доступа извне класса к этому значению определен метод etValue
, который имеет тип LiveData
и который при первом обращении к переменной
устанавливает 0, либо просто возвращает значение переменной:
public LiveData<Integer> getValue() { if (value == null) { value = new MutableLiveData<Integer>(0); } return value; }
Для индикации, запущен ли поток, определена переменная isStarted
, которая хранит значение типа Boolean
, то есть фактически true
или false
.
По умолчанию она имеет значение false
(то есть поток не запущен).
Для изменения числового значения, к которому будут привязаны виджеты, определен метод execute()
. Он запускает поток, если поток не запущен:
if(!isStarted.getValue()){
Далее переключает значение переменной isStarted
на true
, поскольку мы запускаем поток.
В потоке также запускается цикл
for(int i = value.getValue(); i <= 100; i++){
И в данном случае мы пользуемся преимуществом класса ViewModel, который позволяет автоматически сохранять свое состояние.
Причем счетчик цикла в качестве начального значения берет значение из переменной value
и увеличается на единицу, пока не дойдет до ста.
В самом цикле изменяется значение переменной value
с помощью передачи значения в метод postValue()
value.postValue(i);
Таким образом, в цикле осуществится проход от 0 до 100, и при каждой итерации цикла будет изменяться значение переменной value.
Теперь задействуем наш класс MyViewModel
и для этого изменим код класса MainActivity:
package com.example.threadapp; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.ViewModelProvider; import android.os.Bundle; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ProgressBar indicatorBar = findViewById(R.id.indicator); TextView statusView = findViewById(R.id.statusView); Button btnFetch = findViewById(R.id.progressBtn); MyViewModel model = new ViewModelProvider(this).get(MyViewModel.class); model.getValue().observe(this, value -> { indicatorBar.setProgress(value); statusView.setText("Статус: " + value); }); btnFetch.setOnClickListener(v -> model.execute()); } }
Чтобы задействовать MyViewModel, создаем объект класса ViewModelProvider, в конструктор которого передается объект-владелец ViewModel. В данном случае это текущий объект MainActivity:
new ViewModelProvider(this)
И далее с помощью метода get()
создаем объект класса ViewModel, который будет использоваться в объекте MainActivity.
MyViewModel model = new ViewModelProvider(this).get(MyViewModel.class);
Получив объект MyViewModel, определяем прослушивание изменений его переменной value
с помощью метода observe:
model.getValue().observe(this, value -> { indicatorBar.setProgress(value); statusView.setText("Статус: " + value); });
Метод observe()
в качестве первого параметра принимает владельца функции обсервера - в данном случае текущий объект MainActivity. А в качестве второго параметра
- функцию обсервера (а точнее объект интерфейса Observer
). Функция обсервера принимает один параметр - новое значение отслеживаемой переменной (то есть в данном случае переменной value).
Получив новое значение переменной value, мы изменяем параметры виджетов.
Таким образом, при каждом изменении значения в переменной value виджеты получат ее новое значение.
Теперь если мы запустим приложение, то вне зависимости от смены ориентации моильного устройства фоновая задача будет продолжать свою работу:
Аналогично мы можем использовать фрагементы. Итак, добавим в проект новый фрагмент, который назовем ProgressFragment.
Определим для него новый файл разметки интерфейса fragment_progress.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"> <Button android:id="@+id/progressBtn" android:text="Запуск" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toTopOf="@id/statusView" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent"/> <TextView android:id="@+id/statusView" android:text="Статус" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toTopOf="@id/indicator" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@id/progressBtn" /> <ProgressBar android:id="@+id/indicator" style="@android:style/Widget.ProgressBar.Horizontal" android:layout_width="0dp" android:layout_height="wrap_content" android:max="100" android:progress="0" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/statusView"/> </androidx.constraintlayout.widget.ConstraintLayout>
Сам класс фрагмента ProgressFragment изменим следующим образом:
package com.example.threadapp; import android.os.Bundle; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; public class ProgressFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_progress, container, false); ProgressBar indicatorBar = (ProgressBar) view.findViewById(R.id.indicator); TextView statusView = (TextView) view.findViewById(R.id.statusView); Button btnFetch = (Button)view.findViewById(R.id.progressBtn); MyViewModel model = new ViewModelProvider(requireActivity()).get(MyViewModel.class); model.getValue().observe(getViewLifecycleOwner(), value -> { indicatorBar.setProgress(value); statusView.setText("Статус: " + value); }); btnFetch.setOnClickListener(v -> model.execute()); return view; } }
Здесь аналогичным образом применяется класс MyViewModel. Единственно для получения ассоциируемой с фрагментом Activity здесь применяется метод
requireActivity(). А для получения владельца жизненного цикла - метод getViewLifecycleOwner
.
Теперь свяжем фрагмент с activity. Для этого определим в файле activity_main.xml следующий код:
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/fragment_container_view" android:layout_width="match_parent" android:layout_height="match_parent" android:name="com.example.threadapp.ProgressFragment" />
А сам класс MainActivity сократим:
package com.example.threadapp; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }
И код с фрагементом будет работать аналогично: