Показаны сообщения с ярлыком onSaveInstanceState. Показать все сообщения
Показаны сообщения с ярлыком onSaveInstanceState. Показать все сообщения

14 мая 2014 г.

Задачи и обратный стек (Tasks and back stack). Часть 8 (практика)

За время отпуска, во время долгих перелетов, мои приложения AP0003 и AP0004 изрядно мутировали и перешли на новый виток эволюции. Во первых, для лучшего понимания я добавил вывод ID задачи в скобках в каждом методе: onCreate(), onStart и т.д. Во вторых, третьих, четвертых и пятых сделал кучу изменений. О них по порядку.

В приложении AP0003 так и осталось четыре активности A, B, C и D. Но их функционал и внешний вид несколько поменялся. Активность C имеет launcMode=”singleTask”.  Активность D – launchMode="singleTop". Кроме того в Активности D была добавлена кнопка запуска Акитвности В в приложении AP0004.

В приложении AP0004 добавилась еще одна Активность Е со свойством launchMode="singleInstance".

Кроме того, я дописал код, который корректирует работу счетчика Активностей в задаче при убивании Активности D в приложении AP0003. Ну и еще много чего дописал.

Теперь к практике. Начнем с того момента где остановились в прошлый раз. Запускаем приложение AP0004.

NT0001

Стрелочками отмечено то, на что следует обратить внимание. Теперь смотрим логи.

NT0002

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

Нажмем кнопку Info

NT0003

Видим, что в задаче 9 сейчас одна Активность А. Тут все просто.

Жмем кнопку Start D AP0003

NT0004

Теперь видим, что в задаче 9 две Активности: A и D. Активность А принадлежит приложению
AP0004, а Активность D – приложению AP0003. НО ОНИ В ОДНОЙ ЗАДАЧЕ 9.

Посмотрим логи.

NT0005

Активность D была запущена в задаче 9 но в процессе приложения AP0003 с ID=1763. А у Активности А из которой она была запущена процесс ID=1813.

Жмем кнопку Info и смотрим логи.

NT0006

И логов еще раз, подробнее, видно, что сейчас в нашей задаче 9 есть две Активности A и D. Активность D сейчас у нас на вершине стека. В прочем тут пока все как в прошлой практике.

Теперь введем какой-нибудь текст в Активности D, чтобы видеть будет ли он сохранен.

NT0007

И жмем кнопку запустить Активность B AP0004.

NT0008

Смотрим логи

NT0009

Видим что Активность В была запущена в процессе приложения AP0004 с ID=1813. Жмем кнопку Info

NT0010

И так, сейчас в нашей задаче 9 есть три Активности A-D-B. Активность D принадлежит приложению AP0003, а Активности A и B – приложению AP0004. Активность B сейчас на вершине стека. Так же стоит отметить что нажатие кнопки Info отрабатывается в процессе текущей Активности, что собственно логично. И еще запоминаем что процесс Активности D имеет значение 1763. Сейчас мы его будем убивать.

Жмем кнопку HOME и запускаем снова приложение ProcessView. Находим процесс 1763 приложения AP0003.

NT0011

И убиваем его

NT0012

Возвращаемся к нашему приложению AP0004

NT0008

Теперь жмем кнопку ОБРАТНО, должна открыться Активность D, процесс которой мы убили.

NT0013

Счетчик теперь работает правильно, благодаря изменениям в коде, которые я таки сподобился написать. Кроме того текст введенный нами тоже сохранился не смотря на то, что процесс Активности D был убит. Это говорит о том что объект Bundle, в котором сохраняются данные, привязан к задаче, а не процессу.

Теперь посмотрим логи

NT0014

В начале данного скрина первые четыре строки мы видим отработку нажатия кнопки HOME и возврата к приложению. Затем в пятой строке логов нажатие кнопки ОБРАТНО. И происходи СОЗДАНИЕ Активности D. То есть срабатывает метод onCreate() вместо метода onRestart(), так как мы убили процесс Активности D. И создается Активность D уже в новом процессе с номером 1857, а старый был 1763, о чем нам собственно и докладывает наш код.

Но этот код работает только в том случае, если Активность D, запускается из другого приложения. Если же мы просто запустим приложение AP0003 и дойдем до Активности D, а затем убьем процесс приложения AP0003, то вместе с ним убьется и задача, а в месте с ней и объект Bundle.

Привету код Активности D приложения AP0003

package com.example.ap0003;

import java.util.List;

import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

public class ActivityD extends Activity {

 final String TAG = "States";
 TextView tvTextLife;
 List<ActivityManager.RunningTaskInfo> list;
 ActivityManager am;
 Integer TotalActCount;
 Boolean FirstStart;
 Boolean NextAct;
 static final String SaveMyPID = "MY_OLD_PID";
 static final String SaveMyNextClick = "MY_NEXT_CLICK";
 Integer SavedPID;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.layout_d);
  setTitle(getResources().getString(R.string.app_name) + " | "
    + getLocalClassName() + " | TaskID: " + getTaskId());
  Log.d(TAG, "ActivityD: onCreate("+getTaskId()+")");
  // активность запущена впервые
  FirstStart = true;
  // кнопка запуска следующей Активности не нажималась
  NextAct = false;
  // определяем есть ли сохранненные данные
  if (savedInstanceState != null) {
   // Restore value of members from saved state
   Log.d(TAG, "ActivityD: onCreate("+getTaskId()+") NOT NULL: ");
   // сравниваем сохраненный и текущий PID
   SavedPID = savedInstanceState.getInt(SaveMyPID);
   if (SavedPID != android.os.Process.myPid()) {
    Log.d(TAG, "!!! Pocess AP0003 was killed !!!!");
    Log.d(TAG, "Old PID: " + SavedPID);
    Log.d(TAG, "NEW PID: " + android.os.Process.myPid());
    //коррекция счетчика если процесс приложения был убит
    FirstStart = false;
    //коррекция счетчика на запуск другой Активности
    if (savedInstanceState.getInt(SaveMyNextClick)==1){
     NextAct=true;
     Log.d(TAG, "!!! NextAct=true !!!");
    }

   }
  } else {
   // Probably initialize members with default values for a new
   // instance
   Log.d(TAG, "ActivityD: onCreate("+getTaskId()+") NULL");
  }
 }

 @Override
 protected void onStart() {
  super.onStart();
  Log.d(TAG, "ActivityD: onStart("+getTaskId()+")");
 }

 @Override
 protected void onResume() {
  super.onResume();
  Log.d(TAG, "ActivityD: onResume("+getTaskId()+")");
  // получаем список 10 последних задач
  am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
  list = am.getRunningTasks(10);
  // перебираем список задач и выбираем свою по TaskID
  for (RunningTaskInfo task : list) {
   if (task.id == getTaskId()) {
    // находим поле для вывода информации о количестве запущенных
    // Активностей
    tvTextLife = (TextView) findViewById(R.id.textActCountD);
    TotalActCount = task.numActivities;
    // коррекция счетчика для кнопки ДОМОЙ и ОБРАТНО
    if (NextAct == true & FirstStart == false)
     TotalActCount = TotalActCount - 1;
    // выводим количество Активностей в задаче
    tvTextLife.setText("Activites in task " + TotalActCount);
    // коррекция счетчика для кнопки ДОМОЙ
    NextAct = false;
   }
  }
 }

 @Override
 protected void onPause() {
  super.onPause();
  // флаг что активность уже была запущена
  FirstStart = false;
  // находим текстовое поле по его идентификатору
  tvTextLife = (TextView) findViewById(R.id.textStateActD);
  // присваиваем значение атрибуту Text для выбранного TextView
  tvTextLife.setText("Этот экземпляр ActivityD уже был запущен!");
  Log.d(TAG, "ActivityD: onPause("+getTaskId()+")");
 }

 @Override
 protected void onStop() {
  super.onStop();
  Log.d(TAG, "ActivityD: onStop("+getTaskId()+")");
 }

 @Override
 protected void onRestart() {
  super.onRestart();
  Log.d(TAG, "ActivityD: onRestart("+getTaskId()+")");
 }

 @Override
 protected void onDestroy() {
  super.onDestroy();
  Log.d(TAG, "ActivityD: onDestroy("+getTaskId()+")");
 }

 @Override
 protected void onSaveInstanceState(Bundle savedInstanceState) {
  super.onSaveInstanceState(savedInstanceState);
  // сохраняем значение PID
  savedInstanceState.putInt(SaveMyPID, android.os.Process.myPid());
  Log.d(TAG, "ActivityD: onSave("+getTaskId()+") PID:" + android.os.Process.myPid());
  //сохраняем нажатие запуска другой Активности
  if (NextAct==true){
  savedInstanceState.putInt(SaveMyNextClick, 1);
  Log.d(TAG, "ActivityD: onSave("+getTaskId()+") NextAct=true");
  }
 }

 public void onClickStartA(View v) {
  Intent intent = new Intent(ActivityD.this, ActivityA.class);
  startActivity(intent);
  // кнопка запуска следующей Активности была нажата
  NextAct = true;
 }

 public void onClickStartD(View v) {
  startActivity(new Intent(this, ActivityD.class));
 }

 public void onClickStartB0004(View v) {
  startActivity(new Intent("AP0004_ActB"));
  NextAct = true;
 }

 public void onInfoClick(View v) {
  final String TAG = "States";
  // получаем список 10 последних задач

  list = am.getRunningTasks(10);
  // перебираем список задач и выбираем свои по имени пакетов
  // com.example.ap000
  for (RunningTaskInfo task : list) {
   if (task.baseActivity.flattenToShortString().startsWith(
     "com.example.ap000")) {
    // находим поле для вывода информации о количестве запущенных
    // Активностей

    Log.d(TAG, "------------------");
    Log.d(TAG, "TaskID: " + task.id);
    Log.d(TAG, "Num: " + task.numActivities);
    Log.d(TAG, "Base: " + task.baseActivity.flattenToShortString());
    Log.d(TAG, "Top: " + task.topActivity.flattenToShortString());
    Log.d(TAG, "Thread ID: " + android.os.Process.myTid());
    Log.d(TAG, "Process ID: " + android.os.Process.myPid());
    Log.d(TAG, "------------------");

   }
  }

 }

}

10 апреля 2014 г.

Сохранение состояния Активности. Часть 4 (практика)

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

Приложение AP0003 продолжает эволюционировать Улыбка

Чтобы не загромождать функционал и интерфейс Активности А, я модифицировал Активность В и ее разметку. Как показано на скрине:

S0016По нажатию на кнопке Count происходит увеличение счетчика (переменной MyCount) на единицу.

Затем это значение пишется в текстовые атрибуты самой кнопки, текстового поля и поля для ввода текста.

Это сделано для того чтобы посмотреть, что из измененных данных будет сохраняться в Активности А при смене ориентации экрана.

Сперва посмотрим, что и как сохраняется в Активности по умолчанию системой, а так же как ведет себя интерфейс при смене ориентации экрана.

Затем модифицируем если что-то отображается не так как нам хотелось бы.

А так же исправим то, что не сохраняется по умолчанию и будем это сохранять и восстанавливать.

Собственно некоторые ошибки проектирования тут уже есть и я их покажу специально, чтобы было лучшее понимание как и что происходит.

А уж потом исправим все это дело и приведем к желаемому состоянию.

Кроме того, в этой теме, мы все же чуть коснемся темы жизненного цикла Активности. В прошлый раз когда мы разбирали эту тему на примере этого же приложения, мы не вращали экран устройства, поэтому упустили один тонкий момент который стоит упомянуть, что мы и сделаем ниже.

Вот так практика и ведет к совершенству.

Рассмотрим код разметки layout_b.xml:

<RelativeLayout 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:background="#faf20d"
    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=".ActivityB" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/strB" />

    <Button
        android:id="@+id/buttonStartC"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/textView1"
        android:onClick="onClickStartC"
        android:text="Start Activity C" />

    <TextView
        android:id="@+id/textStateActB"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/buttonStartC"
        android:layout_below="@+id/buttonStartC"
        android:background="#fffcfc"
        android:text="Этот экземпляр АсtivityB запущен в первый раз" />

    <TextView
        android:id="@+id/textActCountB"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/textStateActB"
        android:layout_below="@+id/textStateActB"
        android:layout_marginTop="14dp"
        android:background="#fffcfc"
        android:text="TextView" />

    <EditText
        android:id="@+id/editTextB"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/textActCountB"
        android:layout_below="@+id/textActCountB"
        android:layout_marginTop="14dp"
        android:background="#fcf5ab"
        android:ems="10" />

    <Button
        android:id="@+id/buttonCount"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/editTextB"
        android:layout_centerVertical="true"
        android:onClick="onClickCount"
        android:text="Count" />

    <TextView
        android:id="@+id/textViewCount"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_toRightOf="@+id/buttonCount"
        android:text="Count=" />

</RelativeLayout>

Мы видим что все элементы разметки имеют уникальные ID и по идее их состояние должно сохраняться и восстанавливаться системой по умолчанию. Но как покажет практика не все будет работать так как ожидается. Хотя в принципе все логично.

В строке 61 происходит вызов обработчика нажатия кнопки Count.

Теперь код ActivityB.java:
package com.example.ap0003;

import java.util.List;

import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class ActivityB extends Activity {

 final String TAG = "States";
 TextView tvTextLife;
 List<ActivityManager.RunningTaskInfo> list;
 ActivityManager am;
 Integer TotalActCount;
 Boolean FirstStart;
 Boolean NextAct;
 TextView MyTextB, MyTextCount;
 Integer MyCount = 0;
 Button MyCountButton;
 EditText MyEditTextB;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.layout_b);
  Log.d(TAG, "ActivityB: onCreate()");
  // флаг что активность запущена впервые
  FirstStart = true;
  // кнопка запуска следующей Активности не нажималась
  NextAct = false;
 }

 @Override
 protected void onStart() {
  super.onStart();
  Log.d(TAG, "ActivityB: onStart()");
 }

 @Override
 protected void onResume() {
  super.onResume();
  Log.d(TAG, "ActivityB: onResume()");
  // получаем список 10 последних задач
  am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
  list = am.getRunningTasks(10);
  // перебираем список задач и выбираем свою по имени пакета
  // com.example.ap0003
  for (RunningTaskInfo task : list) {
   if (task.baseActivity.flattenToShortString().startsWith(
     "com.example.ap0003")) {
    // находим поле для вывода информации о количестве запущенных
    // Активностей
    tvTextLife = (TextView) findViewById(R.id.textActCountB);
    TotalActCount = task.numActivities;
    // коррекция счетчика для кнопки ОБРАТНО
    if (NextAct == true & FirstStart == false)
     TotalActCount = TotalActCount - 1;
    // выводим количество Активностей в задаче
    tvTextLife.setText("Activites in task " + TotalActCount);
    // коррекция счетчика для кнопки ДОМОЙ
    NextAct = false;
   }
  }
 }

 @Override
 protected void onPause() {
  super.onPause();
  Log.d(TAG, "ActivityB: onPause()");
  // флаг что активность уже была запущена
  FirstStart = false;
  // находим текстовое поле по его идентификатору
  tvTextLife = (TextView) findViewById(R.id.textStateActB);
  // присваиваем значение атрибуту Text для выбранного TextView
  tvTextLife.setText("Этот экземпляр ActivityB уже был запущен!");
 }

 @Override
 protected void onStop() {
  super.onStop();
  Log.d(TAG, "ActivityB: onStop()");
 }

 @Override
 protected void onRestart() {
  super.onRestart();
  Log.d(TAG, "ActivityB: onRestart()");
 }

 @Override
 protected void onDestroy() {
  super.onDestroy();
  Log.d(TAG, "ActivityB: onDestroy()");
 }

 public void onClickStartC(View v) {
  Intent intent = new Intent(ActivityB.this, ActivityC.class);
  startActivity(intent);
  // кнопка запуска следующей Активности была нажата
  NextAct = true;
 }

 @Override
 protected void onSaveInstanceState(Bundle savedInstanceState) {
  super.onSaveInstanceState(savedInstanceState);
  // получаем ссылку на поле ввода текста
  MyTextB = (EditText) findViewById(R.id.editTextB);
  Log.d(TAG, "onSaveInstanceState B text: "
    + MyTextB.getText().toString());
 }

 @Override
 protected void onRestoreInstanceState(Bundle savedInstanceState) {
  super.onRestoreInstanceState(savedInstanceState);
  // получаем ссылку на поле ввода текста
  MyTextB = (EditText) findViewById(R.id.editTextB);
  Log.d(TAG, "onRestoreInstanceState B text: "
    + MyTextB.getText().toString());
 }

 public void onClickCount(View v) {
  Log.d(TAG, "Click Count Button");
  MyCount = MyCount + 1;
  // находим текстовое поле по его идентификатору
  MyTextCount = (TextView) findViewById(R.id.textViewCount);
  // присваиваем значение атрибуту Text для выбранного TextView
  MyTextCount.setText("Count = " + MyCount);
  // находим кнопку по ее идентификатору
  MyCountButton = (Button) findViewById(R.id.buttonCount);
  // присваиваем значение атрибуту Text для выбранной кнопки
  MyCountButton.setText("Count " + MyCount);
  // находим поле ввода текста по его идентификатору
  MyEditTextB = (EditText) findViewById(R.id.editTextB);
  // присваиваем значение атрибуту Text для выбранного поля ввода текста
  MyEditTextB.setText("Count = " + MyCount);
 }

}

Код со 129 строки и до 143 обрабатывает нажатие кнопки Count.

Теперь рассмотрим как работает наше измененное приложение. Запускаем его

S0017

И сразу же стартуем Активность В

S0019

Посмотрим логи

S0018


И так пока все как доктор прописал. Обращаем внимание что в Активности В написано что она запущена впервые, что есть правда.

Теперь нажмем кнопку HOME (ДОМОЙ) и снова вернемся в приложение кликнув по его иконке в списке приложений.

S0020

Наша Активность В сообщила нам что она была уже запущена, что тоже есть правда. Посмотрим логи.

S0021

Метод onSaveInstanceState() был запущен Активностью В при ее переходе в остановленное состояние. И затем Активность В снова была нами запущена. И так пока все гладко.

Теперь жмакнем по кнопочке Count

S0022

Видно что после нажатия кнопки Count изменилось состояние элементов. Так как мы нажали один раз, то счетчик с нуля увеличился на единичку.

Глянем логи

S0023

Вот видно что кликнули по кнопке.

Теперь повернем экран и что мы видим?????

S0024

Во первых, обращаем внимание что наша Активность В, сообщает нам что она была запущена в первый раз. И она не врет. Это действительно так. Посмотрим логи.

S0025

И так видно, что при смене ориентации экрана, Активность уничтожается и создается вновь. Делается это не для того, чтобы добавить разработчикам головной боли, а для того, что при смене ориентации экрана может отображаться  другая разметка, предназначенная и оптимизированная именно для этой ориентации экрана.

Все как в матчасти было описано. Перед методом onPause() вызывается метод onSaveInstanceState(), затем Активность останавливается, уничтожается, создается вновь, вызывается метод onRestoreInstanceState() и т.д.

Как мы видим сохранилось только значение которое было в поле для ввода текста. А измененная надпись на кнопке и в поле TextView не были сохранены, хотя у этих элементов и есть ID. Собственно об этом уже тоже говорилось в альма-матер

some of the activity state is restored by the Activity class's default implementation

некоторые данные состояния Активности восстанавливаются классом Активности по умолчанию

Собственно какие это, некоторые данные, видимо придется узнавать на собственном опыте Улыбка

Зато интересно и весело Улыбка

Видимо предполагается, что значения надписей на кнопках и в полях TextView меняются редко и их незачем сохранять по умолчанию. Хотя может и не так.

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

Для этого в дереве папок проекта, в папке res создаем папку layout-land и в ней файл layout_b.xml, в который копируем содержимое файла layout_b.xml из папки layout. И приводим все вот к такому виду

S0026

Среда разработки сама понимает, что в данной папке должны находится разметки для горизонтальной ориентации экрана.

Если не все понятно как это сделать, то рекомендую посмотреть этот урок

Layout файл для разных ориентаций экрана

На всякий случай привожу код файла layout_b.xml из папки layout-land:
<RelativeLayout 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:background="#faf20d"
    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=".ActivityB" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/strB" />

    <Button
        android:id="@+id/buttonStartC"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/textView1"
        android:onClick="onClickStartC"
        android:text="Start Activity C" />

    <TextView
        android:id="@+id/textStateActB"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/buttonStartC"
        android:layout_below="@+id/buttonStartC"
        android:background="#fffcfc"
        android:text="Этот экземпляр АсtivityB запущен в первый раз" />

    <TextView
        android:id="@+id/textActCountB"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/textStateActB"
        android:layout_below="@+id/textStateActB"
        android:layout_marginTop="14dp"
        android:background="#fffcfc"
        android:text="TextView" />

    <EditText
        android:id="@+id/editTextB"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/textActCountB"
        android:layout_below="@+id/textActCountB"
        android:layout_marginTop="14dp"
        android:background="#fcf5ab"
        android:ems="10" />

    <Button
        android:id="@+id/buttonCount"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@+id/textStateActB"
        android:layout_marginLeft="19dp"
        android:layout_toRightOf="@+id/buttonStartC"
        android:onClick="onClickCount"
        android:text="Count" />

    <TextView
        android:id="@+id/textViewCount"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/buttonCount"
        android:layout_alignBottom="@+id/buttonCount"
        android:layout_marginLeft="18dp"
        android:layout_toRightOf="@+id/buttonCount"
        android:text="Count=" />

</RelativeLayout>

И так продолжаем наши эксперименты и эволюцию приложения AP0003 Улыбка

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

Теперь при повороте экрана увидим такую картинку

S0027

Теперь отображается layout_b.xml для горизонтальной ориентации экрана, как мы и запроектировали, но значения кнопки Count и поля TextView не сохраняются по умолчанию.

Посмотрим логи:

S0028

Собственно в логах все как в матчасти. Теперь нам надо самим позаботиться о сохранении значения счетчика и его восстановлении. Сделаем все как описано тут, но сперва, для пущего понимания, еще немного поиграемся с приложением.

Жмакнем на кнопку Count еще пару раз и увидим

S0029

Хотя на предыдущем скрине было видно, что Count=1 и мы нажали по кнопке Count ДВА раза, возможно было предположить, что Count будет равен 3, но это не так, поскольку состояние счетчика не сохранилось и он начал отсчет нажатий заново. Именно поэтому мы видим что Count=2.

Теперь логи

S0030

Теперь повернем экран

S0031

Посмотрим логи

S0032

И один раз нажмем на кнопку Count

S0033

Значение Count стало равно снова 1, хотя до этого было 2. Собственно это я уже объяснял почему такое происходит и на скрине это объяснено еще раз.

Теперь поправим сложившуюся ситуацию. Смотрим измененный код ActivityB.java.
package com.example.ap0003;

import java.util.List;

import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class ActivityB extends Activity {

 final String TAG = "States";
 TextView tvTextLife;
 List<ActivityManager.RunningTaskInfo> list;
 ActivityManager am;
 Integer TotalActCount;
 Boolean FirstStart;
 Boolean NextAct;
 TextView MyTextB, MyTextCount;
 Integer MyCount = 0;
 Button MyCountButton;
 EditText MyEditTextB;
 static final String SaveMyCount = "MY_CLICK_COUNT";

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.layout_b);
  // флаг что активность запущена впервые
  FirstStart = true;
  // кнопка запуска следующей Активности не нажималась
  NextAct = false;
  if (savedInstanceState != null) {
   // Restore value of members from saved state
   // восстанавливаем значение счетчика
   MyCount = savedInstanceState.getInt(SaveMyCount);
   // находим текстовое поле по его идентификатору
   MyTextCount = (TextView) findViewById(R.id.textViewCount);
   // присваиваем значение атрибуту Text для выбранного TextView
   MyTextCount.setText("Count = " + MyCount);
   // находим кнопку по ее идентификатору
   MyCountButton = (Button) findViewById(R.id.buttonCount);
   // присваиваем значение атрибуту Text для выбранной кнопки
   MyCountButton.setText("Count " + MyCount);
   // получаем ссылку на поле ввода текста
   MyTextB = (EditText) findViewById(R.id.editTextB);
   Log.d(TAG, "ActivityB: onCreate() NOT NULL: "
     + MyTextB.getText().toString());
  } else {
   // Probably initialize members with default values for a new
   // instance
   Log.d(TAG, "ActivityB: onCreate() NULL");
  }
  Log.d(TAG, "ActivityB: onCreate()");
 }

 @Override
 protected void onStart() {
  super.onStart();
  Log.d(TAG, "ActivityB: onStart()");
 }

 @Override
 protected void onResume() {
  super.onResume();
  Log.d(TAG, "ActivityB: onResume()");
  // получаем список 10 последних задач
  am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
  list = am.getRunningTasks(10);
  // перебираем список задач и выбираем свою по имени пакета
  // com.example.ap0003
  for (RunningTaskInfo task : list) {
   if (task.baseActivity.flattenToShortString().startsWith(
     "com.example.ap0003")) {
    // находим поле для вывода информации о количестве запущенных
    // Активностей
    tvTextLife = (TextView) findViewById(R.id.textActCountB);
    TotalActCount = task.numActivities;
    // коррекция счетчика для кнопки ОБРАТНО
    if (NextAct == true & FirstStart == false)
     TotalActCount = TotalActCount - 1;
    // выводим количество Активностей в задаче
    tvTextLife.setText("Activites in task " + TotalActCount);
    // коррекция счетчика для кнопки ДОМОЙ
    NextAct = false;
   }
  }
 }

 @Override
 protected void onPause() {
  super.onPause();
  Log.d(TAG, "ActivityB: onPause()");
  // флаг что активность уже была запущена
  FirstStart = false;
  // находим текстовое поле по его идентификатору
  tvTextLife = (TextView) findViewById(R.id.textStateActB);
  // присваиваем значение атрибуту Text для выбранного TextView
  tvTextLife.setText("Этот экземпляр ActivityB уже был запущен!");
 }

 @Override
 protected void onStop() {
  super.onStop();
  Log.d(TAG, "ActivityB: onStop()");
 }

 @Override
 protected void onRestart() {
  super.onRestart();
  Log.d(TAG, "ActivityB: onRestart()");
 }

 @Override
 protected void onDestroy() {
  super.onDestroy();
  Log.d(TAG, "ActivityB: onDestroy()");
 }

 public void onClickStartC(View v) {
  Intent intent = new Intent(ActivityB.this, ActivityC.class);
  startActivity(intent);
  // кнопка запуска следующей Активности была нажата
  NextAct = true;
 }

 @Override
 protected void onSaveInstanceState(Bundle savedInstanceState) {
  super.onSaveInstanceState(savedInstanceState);
  // сохраняем значение счетчика
  savedInstanceState.putInt(SaveMyCount, MyCount);
  // получаем ссылку на поле ввода текста
  MyTextB = (EditText) findViewById(R.id.editTextB);
  Log.d(TAG, "onSaveInstanceState B text: "
    + MyTextB.getText().toString());
 }

 @Override
 protected void onRestoreInstanceState(Bundle savedInstanceState) {
  super.onRestoreInstanceState(savedInstanceState);
  // получаем ссылку на поле ввода текста
  MyTextB = (EditText) findViewById(R.id.editTextB);
  Log.d(TAG, "onRestoreInstanceState B text: "
    + MyTextB.getText().toString());
 }

 public void onClickCount(View v) {
  Log.d(TAG, "Click Count Button");
  MyCount = MyCount + 1;
  // находим текстовое поле по его идентификатору
  MyTextCount = (TextView) findViewById(R.id.textViewCount);
  // присваиваем значение атрибуту Text для выбранного TextView
  MyTextCount.setText("Count = " + MyCount);
  // находим кнопку по ее идентификатору
  MyCountButton = (Button) findViewById(R.id.buttonCount);
  // присваиваем значение атрибуту Text для выбранной кнопки
  MyCountButton.setText("Count " + MyCount);
  // находим поле ввода текста по его идентификатору
  MyEditTextB = (EditText) findViewById(R.id.editTextB);
  // присваиваем значение атрибуту Text для выбранного поля ввода текста
  MyEditTextB.setText("Count = " + MyCount);
 }

}

В строке 29 мы определили переменную для сохранения состояния счетчика в наборе Bundle. В строке 137 мы сохраняем в нее значение счетчика MyCount. В строках 39-59 восстанавливаем значение счетчика (строка 42) и значение атрибута Text на кнопе и в поле TextView. Ну и так же добавлены таги для отображения в логах необходимой для понимания и отладки информации.

Теперь запустим приложение и посмотрим как оно теперь работает. Запускаем, и с Активности А сразу запускаем Активность В, в которой клацаем по кнопке Count ТРИ раза.

S0034

Смотрим логи.

S0035

Теперь повернем экран

S0036

Как видим все в порядке, то есть все данные сохранились и восстановились. Посмотрим логи:

S0037

И так после наших трех кликов по кнопке Count мы повернули экран. Перед уничтожением Актвиности В ее состояние было сохранено методом onSaveInstanceState(), в котором мы так же сохранили и значение счетчика MyCount в объекте Bundle.

Далее уже начинается чуть интереснее. Хотя при создании мы видим, что в методе onCreate() объект Bundle не равен null, все же получить доступ к значению поля EditText не получается, потому там написано только NOT NULL, хотя код в строках 53 и 54 пытается вывести значение этого поля в логах … но там пусто. Значение в поле восстанавливается только в методе onRestoreInstanceState(), где уже можно прочитать значение атрибута Text в поле EditText. Строки кода 52-54 и 148-150 аналогичны в обоих методах onCreate() и onRestoreInstanceState(), но в первом мы не видим вывода значения атрибута TEXT в логах, а во втором он уже есть.

Теперь еще пару раз жмакнем по кнопке Count

S0038

И добавим в текстовое поле свой текст, чтобы все выглядело вот так:

S0039

И повернем экран опять

S0040

Посмотрим логи:

S0041

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

Значение в поле ввода текста меняет только наш метод onClickCount(). Конечно данное поведение тоже можно поменять но мы не будем. Так как это только способствует пониманию данной темы.

Теперь еще пару раз кликнем по кнопке Count

S0042

Как видим со счетчиком все в порядке, а так же текст в поле ввода текста был изменен нашим методом onClickCount().

На этом, пожалуй, тему сохранения состояния Активности мы закончим и вернемся к тебе задач и обратного стека.

9 апреля 2014 г.

Сохранение состояния Активности. Часть 3 (практика)

Мое приложение AP003 продолжает эволюционировать.

Я добавил два поля EditText, одно с ID другое без него (бирюзового цвета). А так же еще одно поле TextView, в которое буду записывать сохраненные данные из поля EditText. Приведу скрин для наглядности:

S0001

Розовое поле EditText имеет ID, бирюзовое – нет. В элемент TextView с текстом “Просто текст”, будет писаться сохраненное значение из розового поля EditText.

Код разметки layout_a.xml

<RelativeLayout 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:background="#f20808"
    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=".ActivityA" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/strA" />

    <Button
        android:id="@+id/buttonStartB"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@+id/textView1"
        android:onClick="onClickStartB"
        android:text="Start Activity B" />

    <TextView
        android:id="@+id/textStateActA"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/buttonStartB"
        android:layout_below="@+id/buttonStartB"
        android:background="#fffcfc"
        android:text="Этот экземпляр АсtivityA запущен в первый раз" />

    <TextView
        android:id="@+id/textActCountA"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/textStateActA"
        android:layout_below="@+id/textStateActA"
        android:layout_marginTop="15dp"
        android:background="#fffcfc"
        android:text="TextView" />

    <EditText
        android:id="@+id/editTextA"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/textActCountA"
        android:layout_below="@+id/textActCountA"
        android:layout_marginTop="15dp"
        android:background="#ffaacc"
        android:ems="10" />

    <TextView
        android:id="@+id/textViewA"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/editTextA"
        android:layout_below="@+id/editTextA"
        android:layout_marginTop="14dp"
        android:background="#faaaaa"
        android:text="Просто текст" />

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/textViewA"
        android:layout_below="@+id/textViewA"
        android:layout_marginTop="18dp"
        android:background="#22ddff"
        android:ems="10" >

        <requestFocus />
    </EditText>

</RelativeLayout>

В строке 47 определен ID для EditText android:id="@+id/editTextA". Как видно из кода от строки 67 у другого элемента EditText нет ID. Это сделано для демонстрации того, что текст в данном поле не будет сохранятся системой при смене ориентации экрана.

В поле TextView с ID android:id="@+id/textViewA" будет писаться текст, введенный в поле с ID android:id="@+id/editTextA".

Теперь код ActivityA.java:

package com.example.ap0003;

import java.util.List;

import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;

public class ActivityA extends Activity {

 final String TAG = "States";
 TextView tvTextLife, MyTextA, vTextA;
 List<ActivityManager.RunningTaskInfo> list;
 ActivityManager am;
 Integer TotalActCount;
 Boolean FirstStart;
 Boolean NextAct;
 static final String SaveTextEditA = "TEXTEDIT_A_STATE";
 SharedPreferences sPref;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState); //супер класс всегда вызывайте первым
  setContentView(R.layout.layout_a);
  
  // флаг что активность запущена впервые
  FirstStart = true;
  // кнопка запуска следующей Активности не нажималась
  NextAct = false;
  if (savedInstanceState != null) {
         // Restore value of members from saved state
   // находим текстовое поле по его идентификатору
   vTextA = (TextView) findViewById(R.id.textViewA);
   // и присваиваем ему сохраненное в методе onSaveInstanceState значение
   vTextA.setText(savedInstanceState.getString(SaveTextEditA));
   Log.d(TAG, "ActivityA: onCreate() NOT NULL: " + vTextA.getText().toString());
     } else {
         // Probably initialize members with default values for a new instance
      Log.d(TAG, "ActivityA: onCreate() NULL");
     }
  
  Log.d(TAG, "ActivityA: onCreate()");

 }

 @Override
 protected void onStart() {
  super.onStart();
  Log.d(TAG, "ActivityA: onStart()");
 }

 @Override
 protected void onResume() {
  super.onResume();
  Log.d(TAG, "ActivityA: onResume()");
  // получаем список 10 последних задач
  am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
  list = am.getRunningTasks(10);
  // перебираем список задач и выбираем свою по имени пакета
  // com.example.ap0003
  for (RunningTaskInfo task : list) {
   if (task.baseActivity.flattenToShortString().startsWith(
     "com.example.ap0003")) {
    // находим поле для вывода информации о количестве запущенных
    // Активностей
    tvTextLife = (TextView) findViewById(R.id.textActCountA);
    TotalActCount = task.numActivities;
    // коррекция счетчика для кнопки ОБРАТНО
    if (NextAct == true & FirstStart == false)
     TotalActCount = TotalActCount - 1;
    // выводим количество Активностей в задаче
    tvTextLife.setText("Activites in task " + TotalActCount);
    // коррекция счетчика для кнопки ДОМОЙ
    NextAct = false;
   }
  }
 }

 @Override
 protected void onPause() {
  super.onPause();
  Log.d(TAG, "ActivityA: oPause()");
  // флаг что активность уже была запущена
  FirstStart = false;
  // находим текстовое поле по его идентификатору
  tvTextLife = (TextView) findViewById(R.id.textStateActA);
  // присваиваем значение атрибуту Text для выбранного TextView
  tvTextLife.setText("Этот экземпляр ActivityA уже был запущен!");

 }

 @Override
 protected void onStop() {
  super.onStop();
  Log.d(TAG, "ActivityA: onStop()");
 }

 @Override
 protected void onRestart() {
  super.onRestart();
  Log.d(TAG, "ActivityA: onRestart()");
 }

 @Override
 protected void onDestroy() {
  super.onDestroy();
  Log.d(TAG, "ActivityA: onDestroy()");
 }

 public void onClickStartB(View v) {
  Intent intent = new Intent(ActivityA.this, ActivityB.class);
  startActivity(intent);
  // кнопка запуска следующей Активности была нажата
  NextAct = true;

 }

 @Override
 protected void onSaveInstanceState(Bundle savedInstanceState) {
  // получаем ссылку на поле ввода текста
  MyTextA = (EditText) findViewById(R.id.editTextA);
  // сохраняем состояние поля ввода текста
  savedInstanceState.putString(SaveTextEditA, MyTextA.getText()
    .toString());
  Log.d(TAG, "onSaveInstanceState A text: "
    + MyTextA.getText().toString());
  // всегда вызывайте супер класс чтобы сохранить
  // состояние всех view активности
  super.onSaveInstanceState(savedInstanceState);

 }

 @Override
 protected void onRestoreInstanceState(Bundle savedInstanceState) {
  super.onRestoreInstanceState(savedInstanceState);
  // находим текстовое поле по его идентификатору
  vTextA = (TextView) findViewById(R.id.textViewA);
  // и присваиваем ему сохраненное в методе onSaveInstanceState значение
  vTextA.setText(savedInstanceState.getString(SaveTextEditA));
  Log.d(TAG, "onRestoreInstanceState A: " + vTextA.getText().toString());
 }

}

Теперь приведу скрины и логи работы приложения.

И так запускаем приложение и вводим в поля для ввода текста следующий текст:

S0002

И так приложение было запущенно впервые. Мы ввели текст в соответствующие поля. Теперь посмотрим логи работы приложения.

S0003

В логах видим как отработали строки кода 46 (поскольку еще не было сохранения данных в Bundle) и строка 49.

Теперь нажмем кнопку HOME и посмотрим что будет.

S0004

И так мы видим что сработал метод onSaveInstanceState() перед методом onPause(). При выводе в отладке текста который мы ввели в поле сработала строка 132. Теперь наша Активность А находится в остановленном состоянии. Вернемся к ней жмакнув по ее иконке в списке приложений.

Мы увидим следующее

S0006

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

S0005

Логи это же и показывают, что активность была выведена из остановленного состояния в активное. Метод onCreate() не был запущен, так как Активность А не уничтожалась.

Теперь повернем экран. И посмотрим что будет.

S0007

Посмотрим логи.

S0008

Как видно из логов, при повороте экрана Активность А уничтожается, но перед этим вызывается метод onSaveInstanceState(). Затем в методе onCreate() происходит восстановление данных и их запись в поле TextView (строки 30 и 42), так как объект Bundle у нас уже не равен null. Так же восстановление данных происходит в методе onRestoreInstanceState(). Эти два метода дублирую друг друга, можно было бы обойтись любым из них, но так как это учебный пример, то это сделано для наглядности и пущего понимания процессов.

Теперь добавим в розовое поле ввода текста три восклицательных знака, а в бирюзовое снова введем любой текст.

S0009

А затем снова повернем экран.

S0010

Как видим текст в полях стал одинаковым. Так как сработали вышеописанные методы.

Посмотрим логи.

S0011

Собственно из логов видно, что перед уничтожением Активности А, измененный текст (с добавленными !!!) был сохранен, а затем восстановлен. Текст же в бирюзовом поле все время, после уничтожения Активности А, исчезает, так как у этого поля нет ID.

В общем все как было описано в матчасти.

Теперь, ради более полного понимания, закомментируем строки 136 и 142 которые вызывают суперкласс в методах onSaveInstanceState() и onRestoreInstanceState(). И посмотрим что будет.

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

И так запускаем измененное приложение и вводим текст в поля.

S0012

Логи:

S0013

Теперь повернем экран

S0014

И посмотрим логи

S0015

Как видим сработали все наши методы, НО текст в розовом поле не был восстановлен, так как мы не вызвали СУПЕРКЛАСС и поэтому было сохранено и сделано только то, что мы явно указали.

И так, опять, все как в матчасти. Улыбка

Далее еще чуть чуть модифицируем приложение, чтобы еще глубже понять данную тему. И немного попрактиковаться.

И на последок еще линки для внимательного изучения по данной теме, и немного текста с этих источников, а то вдруг они исчезнут.

Методы Активности.

И текст по теме от туда.

Метод onSaveInstanceState()

Когда система завершает активность в принудительном порядке, чтобы освободить ресурсы для других приложений, пользователь может снова вызвать эту активность с сохранённым предыдущим состоянием. Чтобы зафиксировать состояние активности перед её уничтожением, в классе активности необходимо реализовать метод onSaveinstancestate().

Сам метод вызывается прямо перед методом onPause(). Он предоставляет возможность сохранять состояние пользовательского интерфейса активности в объект Bundle, который потом будет передаваться в методы onCreate() и onRestoreInstanceState(). В объект Bundle можно записать параметры, динамическое состояние активности как пары имя-значение. Когда активность будет снова вызвана, объект Bundle передается системой в качестве параметра в метод onCreate() и в метод onRestoreInstanceState(), которыЙ вызывается после onStart(), чтобы один из них или они оба могли установить активность в предыдущее состояние. Прежде чем передавать изменённый параметр Bundle в обработчик родительского класса, сохраните значения с помощью методов getXXX() и putXXX().

Используйте обработчик onSaveInstanceState() для сохранения состояния интерфейса (например, состояния флажков, текущего выделенного элемента или введенных, но не сохраненных данных), чтобы объект Activity при следующем входе в активное состояние мог вывести на экран тот же UI. Рассчитывайте, что перед завершением работы процесса во время активного состояния будут вызваны обработчики onSaveInstanceState и onPause.

В отличие от базовых методов, методы onSaveInstanceState() и onRestoreInstanceState() не относятся к методам жизненного цикла активности. Система будет вызывать их не во всех случаях. Например, Android вызывает onSaveinstancestate() прежде, чем активность становится уязвимой к уничтожению системой, но не вызывает его, когда экземпляр активности разрушается пользовательским действием (при нажатии клавиши BACK). В этом случае нет никаких причин для сохранения состояния активности.

Метод onSaveInstanceState() вызывается системой в случае изменения конфигурации устройства в процессе выполнения приложения (например, при вращении устройства пользователем или выдвижении физической клавиатуры устройства.

Поскольку метод onSaveinstanceState() вызывается не во всех случаях, его необходимо использовать только для сохранения промежуточного состояния активности. Для сохранения данных лучше использовать метод onPause().


Метод onRestoreInstanceState()

У метода onRestoreInstanceState() есть такой же параметр Bundle, как у onCreate(), и вы можете восстанавливать сохранённые значения из метода onSaveInstanceState().

Метод вызывается после метода onStart(). Система вызывает метод onRestoreInstanceState() только в том случае, если имеются сохранённые данные для восстановления. Таким образом вам не нужно проверять Bundle на null, как в методе onCreate().