Освой программирование играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Теперь рассмотрим важный вопрос, как сохранять данные при изменении конфигурации и других операциях.
Для примера возьмём старый урок по подсчёту ворон, но на этот раз будем считать котов. Вороны вечно порхают с ветки на ветку, их сложно считать. Коты - совсем другое дело, они лежат себе на одном месте и спят. Считать их одно удовольствие.
Кажется, я поторопился со своим утверждением.
Новый проект создавать не будем, а модифицируем старый. Заодно закрепим материал.
Создадим в проекте два новых фрагмента: WithButtonFragment и WithTextViewFragment. Из названий понятно, что в первом фрагменте будет кнопка, а во втором - TextView, в котором будет отражаться информация о количестве котов.
Повторяем прошлые шаги. Создаём новый класс, наследуемся от Fragment.
package ru.alexanderklimov.fragmentdemo;
import android.support.v4.app.Fragment;
public class WithButtonFragment extends Fragment {
}
Второй фрагмент.
package ru.alexanderklimov.fragmentdemo;
import android.support.v4.app.Fragment;
public class WithTextViewFragment extends Fragment {
}
Создадим две разметки для них. Для удобства будем использовать фон, чтобы различать фрагменты на экране.
fragment_with_button.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:background="@android:color/holo_green_light"
android:orientation="vertical">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="20dp"
android:text="Считаем котов" />
</LinearLayout>
fragment_with_textview.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:background="@android:color/holo_blue_light"
android:orientation="vertical">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="20dp"
android:text="Всего:"
android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout>
Подключаем разметки к соответствующим фрагментам в методе onCreateView().
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_with_button, container, false);
}
Для второго фрагмента напишите код самостоятельно.
Подключаем фрагменты в разметке основной активности activity_main.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
<fragment
android:id="@+id/fragment_withButton"
android:name="ru.alexanderklimov.fragmentdemo.WithButtonFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
tools:layout="@layout/fragment_with_button" />
<fragment
android:id="@+id/fragment_withTextView"
android:name="ru.alexanderklimov.fragmentdemo.WithTextViewFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
tools:layout="@layout/fragment_with_textview" />
</LinearLayout>
Не копируйте код с сайта, проделывайте операции самостоятельно через редактор в режиме Design и Text, как это объяснялось в предыдущих примерах.
Подключим кнопку в первом фрагменте.
package ru.alexanderklimov.fragmentdemo;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
public class WithButtonFragment extends Fragment implements View.OnClickListener {
private int mCounter = 0; // счётчик котов
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView =
inflater.inflate(R.layout.fragment_with_button, container, false);
Button button = (Button)rootView.findViewById(R.id.button);
button.setOnClickListener(this);
return rootView;
}
@Override
public void onClick(View v) {
mCounter++;
}
}
Во втором фрагменте подключим компонент TextView и добавим метод для вывода текста.
package ru.alexanderklimov.fragmentdemo;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class WithTextViewFragment extends Fragment {
private TextView mTextView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView =
inflater.inflate(R.layout.fragment_with_textview, container, false);
mTextView = (TextView) rootView.findViewById(R.id.textView);
return rootView;
}
public void changeText(String data) {
mTextView.setText(data);
}
}
Если мы вызовем метод changeText() с каким-нибудь текстом, то текст отобразится в TextView. Наша задача - научить фрагменты обмениваться данными через активность.
Если сейчас запустить пример, то фрагменты будут работать независимо друг от друга. Счётчик будет увеличиваться, но его значения пока не попадают во второй фрагмент и мы не можем увидеть число котов.
Напоминаю, мы не связываем два фрагмента между собой напрямую. Создаём интерфейс для этих целей.
В предыдущем примере мы создавали интерфейс внутри первого фрагмента. Для разнообразия изменим подход. Создадим новый класс через New | Java Class и в диалоговом окне для поля Kind выберем Interface. Также зададим ему имя Communicator.
Интерфейс будет состоять из одного метода.
package ru.alexanderklimov.fragmentdemo;
public interface Communicator {
public void count(String data);
}
Активность должна реализовать интерфейс.
public class MainActivity extends ActionBarActivity implements Communicator {
@Override
public void count(String data) {
}
}
Фрагмент с кнопкой может использовать объект интерфейса для отправки данных. Инициализируем его в новом методе onActivityCreated(), который ранее не использовали. Метод сработает, когда активность будет создана и готова к работе.
package ru.alexanderklimov.fragmentdemo;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
public class WithButtonFragment extends Fragment implements View.OnClickListener {
private int mCounter = 0; // счётчик котов
private Communicator mCommunicator;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView =
inflater.inflate(R.layout.fragment_with_button, container, false);
Button button = (Button)rootView.findViewById(R.id.button);
button.setOnClickListener(this);
return rootView;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mCommunicator = (Communicator)getActivity();
}
@Override
public void onClick(View v) {
mCounter++;
mCommunicator.count("Я насчитал " + mCounter + " котов");
}
}
При щелчках на кнопках мы вызываем метод count(), которому передаём нужную информацию.
А сам метод в активности будет выглядеть следующим образом:
@Override
public void count(String data) {
FragmentManager manager = getSupportFragmentManager();
WithTextViewFragment withTextViewFragment =
(WithTextViewFragment) manager.findFragmentById(R.id.fragment_withTextView);
withTextViewFragment.changeText(data);
}
Фрагмент передаёт через метод count() данные data, а активность их принимает и передаёт их в метод второго фрагмента changeText().
Подготовительные работы завершены и можно запустить пример для проверки.
Если вы создавали пример на основе предыдущих уроков, то не поворачивайте экран. Сначала удалите (или переименуйте) файлы для альбомной ориентации, чтобы не получить ошибку и крах приложения.
Теперь переходим непосредственно к теме нашего урока. Повернув экран, мы обнаружим, что данные из текстового блока пропали. Наши подсчёты - коту под хвост!
Мы можем в новой ориентации начать новый подсчёт, но повернув устройство обратно в портретный режим, снова потеряем данные.
Мы знаем, что при поворотах активность создаётся заново. Поэтому все данные сбрасываются. Чтобы сохранить данные, у фрагментов есть соответствующие методы, схожие с подобными методами у активностей. Задействуем их.
Метод onSaveInstanceState() поможет нам. Добавим метод в первый фрагмент с кнопкой.
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("counter", mCounter);
}
Параметр метода outState является объектом класса Bundle и позволяет хранить различные типы в формате "ключ-значение".
У фрагментов также есть метод onCreate(Bundle savedInstanceState), где используется объект этого же класса Bundle только под другим именем savedInstanceState. Несмотря на разные имена, речь идёт об одном и том же объекте. И сохраняя данные в методе onSaveInstanceState(), мы можем их получить в методе onCreate():
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
mCounter = 0;
} else {
mCounter = savedInstanceState.getInt("counter", 0);
}
}
При повороте фрагмент сохранит значение счётчика, перезапустится и восстановит значение счётчика. Если запустить пример, то увидим следующее. Щёлкнем несколько раз по кнопке и повернём экран. Данные сбросятся и мы снова увидим пустой текст. Но стоит нам нажать на кнопку, то увидим, что отсчёт пошёл не сначала, а продолжил со своего последнего значения. Мы видим, что первый фрагмент запоминает свои данные. А второй фрагмент пока тупит. Поможем ему.
package ru.alexanderklimov.fragmentdemo;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class WithTextViewFragment extends Fragment {
private TextView mTextView;
private String mData;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView =
inflater.inflate(R.layout.fragment_with_textview, container, false);
mTextView = (TextView) rootView.findViewById(R.id.textView);
if(savedInstanceState == null){
}else {
mData = savedInstanceState.getString("text");
mTextView.setText(mData);
}
return rootView;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("text", mData);
}
public void changeText(String data) {
mData = data;
mTextView.setText(mData);
}
}
Добавим новую переменную mData, которая будет хранить текст сообщения. У метода onCreateView() также есть параметр savedInstanceState класса Bundle, позволяющий извлечь сохранённые данные.
А текст мы сохраним в методе onSaveInstanceState(). В методе changeText() добавим строку кода, чтобы текст брался из новой переменной. Теперь при первом запуске всё работает как прежде. При повороте текст из mData сохраняется в методе putString() и восстанавливается через getString().
После этих изменений программа больше не теряет своих данных и можно спокойно считать котов. Я стал смотреть на гифку и считать. Через 8 минут 16 секунд последний кот выпрыгнул в окно и в итоге получилось 154578 котов. Перепроверьте.
Часть шестая. Удержание состояния фрагмента
Обсуждение статьи на форуме.