Потоки, фрагменты и ViewModel

Последнее обновление: 17.10.2021

При использовании вторичных потоков следует учитывать следующий момент. Более оптимальным способом является работа потоков с фрагментом, нежели непосредственно с 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, и приложение перестанет работать должным образом.

Runnable и альбомный режим в Android и Java Runnable, Thread Fragment и фрагменты в Android

В данном случае проблема упирается в состояние, которым оперирует поток, а именно - переменную currentValue, к значению которой привязаны виджеты в Activity.

Добавление ViewModel

Для подобных случаев в качестве решения проблемы предлагается использовать 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 виджеты получат ее новое значение.

Теперь если мы запустим приложение, то вне зависимости от смены ориентации моильного устройства фоновая задача будет продолжать свою работу:

Runnable и альбомный режим в Android и Java Runnable, Thread Fragment setRetainInstance и фрагменты в Android и Java

Использование фрагментов

Аналогично мы можем использовать фрагементы. Итак, добавим в проект новый фрагмент, который назовем ProgressFragment.

ProgressBar во фрагменте в Android

Определим для него новый файл разметки интерфейса 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);
    }
}

И код с фрагементом будет работать аналогично:

Runnable и альбомный режим в Android и Java Runnable, Thread Fragment setRetainInstance и фрагменты в Android и Java
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850