Обработка поворота экрана. Parcelable.

Урок #31

Урок 31 — Обработка поворота экрана. Parcelable.

У Android есть особенность — при смене конфигурации (например, при повороте экрана) Activity будет уничтожена и создана заново.

Давайте посмотрим на конкретном примере. Создайте новый проект.

Теряем данные при повороте экрана

Суть приложения до безумия проста: в Activity есть переменная типа int, которую мы увеличиваем на 1 при нажатии на кнопку.

Рядом есть TextView, в нём отображается значение, которое мы хранится в переменной.

Лэйаут MainActivity будет таким:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context="com.skillberg.rotation.MainActivity">

    <TextView
        android:id="@+id/counter_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/counter_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="@string/title_increment" />

</LinearLayout>

Код MainActivity:

public class MainActivity extends AppCompatActivity {

    private TextView counterTv;

    private int currentNumber = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        counterTv = findViewById(R.id.counter_tv);

        updateCounter();

        findViewById(R.id.counter_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                currentNumber++;

                updateCounter();
            }
        });
    }

    /**
     * Обновляет счётчик в соответствиии с currentNumber
     */
    private void updateCounter() {
        counterTv.setText(String.valueOf(currentNumber));
    }
}

Запустите приложение и понажимайте на кнопку, чтобы счётчик увеличился:

Счётчик в портертной ориентации
Счётчик в портертной ориентации

А теперь поверните экран. В эмуляторе это можно сделать, нажав на одну из этих кнопок:

Поворот экрана в эмуляторе
Поворот экрана в эмуляторе

Итак, после поворота вы увидите это:

Счётчик в ландшафтной ориентации
Счётчик в ландшафтной ориентации

Упс! Счётчик сбросился.

Это произошло потому что Activity пересоздалась и, соответственно, значение переменной currentNumber сбросилось.

Что же делать?

Метод onSaveInstanceState()

У Activity есть метод onSaveInstanceState(). Перед тем, как Activity уничтожится для пересоздания, система вызовет этот метод, чтобы дать нам возможность сохранить данные.

Обратите внимание: этот метод будет вызван только в случае пересоздания Activity. При безвозвратном уничтожении (например, если пользователь нажал на кнопку назад) этот метод вызван не будет.

У метода onSaveInstanceState() есть параметр Bundle outState. Именно в него мы и должны сохранить те данные, которые нужно будет восстановить позже.

В нашем примере нам нужно будет сохранить лишь одно поле:

@Override
protected void onSaveInstanceState(Bundle outState) {
    outState.putInt(KEY_NUMBER, currentNumber);

    super.onSaveInstanceState(outState);
}

Константа KEY_NUMBER:

public class MainActivity extends AppCompatActivity {

    private static final String KEY_NUMBER = "number";

Обратите внимание: нужно обязательно вызвать метод суперкласса после сохранения данных, чтобы Activity могла самостоятельно сохранить некоторые другие данные.

Какие "другие данные"?

Дело в том, что этот механизм автоматически сохраняет такие вещи, как, например, содержимое EditText. Единственное условие — у этих View должны быть заданы идентификаторы.

Это серьёзно упрощает жизнь, так как вам не нужно выполнять лишнюю работу.

Восстановление из savedInstanceState.

Все те данные, которые вы сохранили в onSaveInstanceState() будут переданы обратно после пересоздания Activity.

Есть два варианта восстановления данных:

  1. В методе onRestoreInstanceState().
  2. В методе onCreate().

Разница между ними несущественна: onRestoreInstanceState() будет вызван после вызова метода onStart(), то есть существенно позже onCreate(). В целом — это вся разница.

Зачем тогда два метода?

Представьте, что у вас есть Activity, наследующаяся от другой вашей же Activity. У каждой из этих Activity может быть свой onCreate(), тогда может не получиться реализовать восстановление один раз для обоих классов, либо наоборот, не получится реализовать раздельное восстановление. В таких ситуациях рекомендуется использовать onRestoreInstanceState().

В остальных случаях лучше восстанавливать состояние в onCreate().

Как вы могли заметить, в onCreate() передаётся параметр Bundle savedInstanceState. Именно тут будут лежать сохранённые данные.

Восстановление будет невероятно простым:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

    counterTv = findViewById(R.id.counter_tv);

    if (savedInstanceState != null) {
        currentNumber = savedInstanceState.getInt(KEY_NUMBER, 0);
    }

    updateCounter();

    findViewById(R.id.counter_btn).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
           // ...
        }
    });
}

Как видите, я добавил всего лишь три строчки кода и восстановил данные.

Восстановление объектов с использованием Parcelable.

А что, если нам нужно сохранить не какой-то примитивный тип, а сложный объект? Давайте создадим класс Counter и добавим в него пару дополнительных полей (которые по факту не нужны, но будут присутствовать для примера):

public class Counter {

    private int currentNumber = 0;

    private final boolean someBoolean;
    private final String someString;

    public Counter() {
        SecureRandom secureRandom = new SecureRandom();

        someBoolean = secureRandom.nextBoolean();
        someString = "Test" + secureRandom.nextInt();
    }

    public int getCurrentNumber() {
        return currentNumber;
    }

    public boolean isSomeBoolean() {
        return someBoolean;
    }

    public String getSomeString() {
        return someString;
    }

    public void incrementCounter() {
        currentNumber++;
    }

    @Override
    public String toString() {
        return "Counter{" +
                "currentNumber=" + currentNumber +
                ", someBoolean=" + someBoolean +
                ", someString='" + someString + '\'' +
                '}';
    }
}

Два поля, добавленных для примера, генерируются случайным образом.

Теперь в MainActivity заменим "цифровой" счётчик на этот:

public class MainActivity extends AppCompatActivity {

    private static final String KEY_NUMBER = "number";

    private TextView counterTv;

    private final Counter counter = new Counter();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        counterTv = findViewById(R.id.counter_tv);

        updateCounter();

        findViewById(R.id.counter_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                counter.incrementCounter();

                updateCounter();
            }
        });
    }

    /**
     * Обновляет счётчик в соответствиии с currentNumber
     */
    private void updateCounter() {
        counterTv.setText(String.valueOf(counter.getCurrentNumber()));
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
    }
}

Собственно, проблема в том, что мы не можем вот так просто передать произвольный объект в Bundle. У нас есть три варианта:

  1. Сохранять каждое поле по отдельности. Такой себе вариант.
  2. Реализовать интерфейс Serializable. Неплохой вариант, "встроенный" в Java, но он не очень хорошо подходит для мобильных платформ из-за низкой скорости.
  3. Реализовать интерфейс Parcelable. Это идеальный вариант, отлично работающий на Android.

Объект класса, реализовавшего интерфейс Parcelable, может быть сериализован в специальный объект класса Parcel. При этом каждое нужное нам поле будет сохранено в компактном виде, и потом может быть с лёгкостью восстановлено.



Продолжение доступно на платных тарифах

А вместе с ним — проверка домашних заданий нашими менторами.

Это совсем недорого — всего от 440 ₽ в месяц!



ВЫБРАТЬ ТАРИФ

Курсовая работа

После этого урока нужно выполнить курсовую работу.



Вход

Войдите, чтобы пользоваться всеми преимуществами.
Это займёт всего пару секунд!

или