Поиск по этому блогу

среда, 23 мая 2018 г.

Пример использования ViewModel

Что-то побудило меня написать статью про эту новую фичу. Погуглив пару секунд я наткнулся на замечательную статью на Medium'e, пример который я привожу тут взят оттуда, только с небольшим моим дополнением, а так по сути это реплика статьи с Medium'a…

Часто бывает когда сталкиваешься с проблемой поворота экрана, данные просто теряются при обновлении активити, и приходится или вызывать снова запрос на получение или сохранять эти данные в onSaveInstanceState(). 

ViewModel же помогает сохранять инстанс любого объекта который вы имеете, пусть то будет модель какого-то респонса с сервера, или на пример модель в которой хранятся локальные данные для текущей активити. При повороте экрана да и вообще на протяжении всего жизненого цикла приложения данная информация будет жить пока вы не закроете приложение вообще. 

Что бывает если мы не сохраняем инстанс переменной в onSaveInstanceState() или во ViewModel? Вот вам пример.

image

При обновлении активити когда экран поворачивается мы теряем наши данные, и так будет пока мы не сделаем переопределение этой переменной в onSaveInstanceState(). Вот пример кода который показан на этой гифке.

MainActivity.java
    @BindView(R.id.textA)
    TextView textA;

    private int counterA;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        displayForTeamA(counterA);
    }

    @OnClick(R.id.buttonA)
    public void onButtonAClick() {
        displayForTeamA(counterA++);
    }

    public void displayForTeamA(int v) {
        textA.setText(String.valueOf(v));
    }

То есть, просто ничего ни где не сохраняется, ничто нигде не записывается, просто есть переменная counterA, которую мы увеличиваем по нажатию кнопки и все. При повороте экрана она просто теряет свой инстанс и пересоздается заново. Для того что бы она у нас записывалась и хранилась пока живо приложение — нам нужно создать класс — модель который будет наследоваться от ViewModel и будет хранить в себе переменные которые мы хотим запомнить. То есть вынести всю логику работы с переменные в отдельный класс что бы он не затрагивал UI.

ScoreViewModel.java
public class ScoreViewModel extends ViewModel {

    public int scoreTeamA = 0;
    public int scoreTeamB = 0;
}

Тут я создал две переменные для хранения данных, их тут можно написать сколько угодно, разных типов и оно все будет сохраняться, для примера будет просто две простые переменные типа int. И этот класс унаследован от ViewModel который собственно и указывает на то что мы в дальнейшем сможем использовать для нашей магии по сохранению данных. 

Вот мы создали значит свою модель и унаследовали ее от ViewModel, вписали туда все нужные параметры, а что же дальше делать? А дальше нам нужно подключить провайдера который будет хранить инстанс данной модели в активити или фрагменте который мы передадим ему как контекст.

ViewModelProviders.of(<THIS ARGUMENT>).get(ScoreViewModel.class);

<THIS ARGUMENT> — это или this или getActivity() или какой-либо context UI класса.
ScoreViewModel.class — собственно наш класс — модель который мы создали для хранения, он может называться если что — как угодно.

Дальше нам нужно подключить две библиотеки для того что бы работать с данными провайдером, так как он не идет в стандартной сборке SDK.

app/build.gradle
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'

    implementation "android.arch.lifecycle:extensions:1.1.0"
    implementation "android.arch.lifecycle:viewmodel:1.1.0"

    implementation 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}

Для пример я привел все библиотеки что у меня тут есть, но нам важны две либы по центру, а именно lifecycle:extensions и lifecycle:viewmodel. Они обе нужны для того что бы использовать нужные классы для реализации (алгоритма или паттерна — как оно там у них называется без понятия) ViewModel. Так же у нас там как всегда ButterKnife и Appcompat для подключения вьюх и работы со стилями Compat.

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_vertical"
    android:padding="20dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical">

        <TextView
            android:id="@+id/textA"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_vertical|center_horizontal"
            android:text="0"
            android:textSize="30sp" />

        <Button
            android:id="@+id/buttonA"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:onClick="onButtonAClick"
            android:text="Button A" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical">

        <TextView
            android:id="@+id/textB"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_vertical|center_horizontal"
            android:text="0"
            android:textSize="30sp" />

        <Button
            android:id="@+id/buttonB"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:onClick="onButtonBClick"
            android:text="Button B" />
    </LinearLayout>

</LinearLayout>

Это не столь важный момент но для полной картинки он нужен, у нас тут две кнопки и две текствьюхи расположеные друг рядом с другом. По нажатию на одну и вторую кнопки у нас будут увеличиваться каунтеры над ними.

MainActivity.java
import android.arch.lifecycle.ViewModelProviders;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.textA)
    TextView textA;
    @BindView(R.id.textB)
    TextView textB;

    private ScoreViewModel mViewModel;
    private int counterA;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        mViewModel = ViewModelProviders.of(this).get(ScoreViewModel.class);
        displayForTeamA(counterA);
        displayForTeamB(mViewModel.scoreTeamB);
    }

    @OnClick(R.id.buttonA)
    public void onButtonAClick() {
        displayForTeamA(counterA++);
    }

    @OnClick(R.id.buttonB)
    public void onButtonBClick() {
        mViewModel.scoreTeamB = mViewModel.scoreTeamB + 1;
        displayForTeamB(mViewModel.scoreTeamB);
    }

    public void displayForTeamA(int v) {
        textA.setText(String.valueOf(v));
    }

    public void displayForTeamB(int v) {
        textB.setText(String.valueOf(v));
    }
}

Собственно к коду выше я добавил новую переменную ScoreViewModel которая является инстансом нашей модели, и в onCreate() я создал при помощи провайдера ViewModelProviders ее инстанс который теперь будет хранить все что мы будем в нее записывать. Дальше по нажатию на onButtonBClick() у нас будет производиться запись инстанса mViewModel.scoreTeamB увеличеный на 1. И так же в onCreate() мы достаем его и отображаем в displayForTeamB(). 

И теперь эффект получается такой что слева у нас хранится инстанс который создается в UI и он не сохраняется, а с права у нас хранится инстанс ScoreViewModel и каждый раз при перевороте оно не создает новый инстанс этой переменной, а просто достает из закрамов последний сохраненный вариант этого объекта.

image

Вот собственно и вся магия. Для примера это очень простой и классный вариант. Если усложнить то можно попробовать создать запрос на пример к серверу, и потом сохранять инстанс респонса в ViewModel и пользоваться им в дальнейших работах на экране. В общем это крутая штука, советую использовать вам в своих проектах.

Исходники:
GitHub