كيفية حفظ حالة نشاط Android باستخدام حالة حفظ المثيل؟

StackOverflow https://stackoverflow.com/questions/151777

سؤال

لقد كنت أعمل على نظام Android SDK، ومن غير الواضح بعض الشيء كيفية حفظ حالة التطبيق.لذا، بالنظر إلى عملية إعادة الأدوات البسيطة هذه لمثال "Hello, Android":

package com.android.hello;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class HelloAndroid extends Activity {

  private TextView mTextView = null;

  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mTextView = new TextView(this);

    if (savedInstanceState == null) {
       mTextView.setText("Welcome to HelloAndroid!");
    } else {
       mTextView.setText("Welcome back.");
    }

    setContentView(mTextView);
  }
}

اعتقدت أنه سيكون كافيًا لأبسط الحالات، لكنه يستجيب دائمًا بالرسالة الأولى، بغض النظر عن كيفية انتقالي بعيدًا عن التطبيق.

أنا متأكد من أن الحل بسيط مثل التجاوز onPause أو شيء من هذا القبيل، لكنني كنت أبحث في الوثائق لمدة 30 دقيقة أو نحو ذلك ولم أجد أي شيء واضح.

هل كانت مفيدة؟

المحلول

تحتاج إلى التجاوز onSaveInstanceState(Bundle savedInstanceState) واكتب قيم حالة التطبيق التي تريد تغييرها إلى ملف Bundle المعلمة مثل هذا:

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
  super.onSaveInstanceState(savedInstanceState);
  // Save UI state changes to the savedInstanceState.
  // This bundle will be passed to onCreate if the process is
  // killed and restarted.
  savedInstanceState.putBoolean("MyBoolean", true);
  savedInstanceState.putDouble("myDouble", 1.9);
  savedInstanceState.putInt("MyInt", 1);
  savedInstanceState.putString("MyString", "Welcome back to Android");
  // etc.
}

تعد الحزمة في الأساس وسيلة لتخزين خريطة NVP ("زوج الاسم والقيمة")، وسيتم تمريرها إلى onCreate() و أيضا onRestoreInstanceState() حيث يمكنك بعد ذلك استخراج القيم مثل هذا:

@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
  super.onRestoreInstanceState(savedInstanceState);
  // Restore UI state from the savedInstanceState.
  // This bundle has also been passed to onCreate.
  boolean myBoolean = savedInstanceState.getBoolean("MyBoolean");
  double myDouble = savedInstanceState.getDouble("myDouble");
  int myInt = savedInstanceState.getInt("MyInt");
  String myString = savedInstanceState.getString("MyString");
}

عادةً ما تستخدم هذه التقنية لتخزين قيم المثيلات لتطبيقك (التحديدات، والنص غير المحفوظ، وما إلى ذلك).

نصائح أخرى

ال savedInstanceState مخصص فقط لحفظ الحالة المرتبطة بالمثيل الحالي للنشاط، على سبيل المثال معلومات التنقل أو التحديد الحالية، بحيث إذا قام Android بتدمير نشاط ما وإعادة إنشائه، فيمكن أن يعود كما كان من قبل.انظر الوثائق ل onCreate و onSaveInstanceState

للحصول على حالة طويلة الأمد، فكر في استخدام قاعدة بيانات SQLite أو ملف أو تفضيلات.يرى حفظ الحالة المستمرة.

لاحظ أنه كذلك لا آمنة للاستخدام onSaveInstanceState و onRestoreInstanceState للبيانات المستمرة, ، وفقًا للوثائق الخاصة بحالات النشاط في http://developer.android.com/reference/android/app/Activity.html.

تنص الوثيقة (في قسم "دورة حياة النشاط") على ما يلي:

لاحظ أنه من المهم حفظ البيانات المستمرة في onPause() بدلاً من onSaveInstanceState(Bundle)نظرًا لأن لاحقًا ليس جزءًا من عروض عمليات الاسترداد لدورة الحياة ، فلن يتم استدعاؤه في كل موقف كما هو موضح في وثائقها.

بمعنى آخر، ضع رمز الحفظ/الاستعادة للبيانات الدائمة فيه onPause() و onResume()!

يحرر:ولمزيد من التوضيح إليك onSaveInstanceState() توثيق:

يتم استدعاء هذه الطريقة قبل مقتل النشاط بحيث عندما يعود بعض الوقت في المستقبل ، يمكنه استعادة حالته.على سبيل المثال ، إذا تم إطلاق النشاط B أمام النشاط A ، وفي مرحلة ما يتم قتل النشاط A لاستعادة الموارد ، فسيحصل النشاط A على فرصة لتوفير الوضع الحالي لواجهة المستخدم الخاصة به عبر هذه الطريقة بحيث عند إرجاع المستخدم إلى النشاط أ ، يمكن استعادة حالة واجهة المستخدم عبر onCreate(Bundle) أو onRestoreInstanceState(Bundle).

كتب زميلي مقالاً يشرح حالة التطبيق على أجهزة Android بما في ذلك شروحات حول دورة حياة النشاط ومعلومات الحالة وكيفية تخزين معلومات الحالة والحفظ في الحالة Bundle و SharedPreferences و نلقي نظرة هنا.

تتناول المقالة ثلاثة طرق:

تخزين بيانات التحكم في المتغير/واجهة المستخدم المحلية طوال عمر التطبيق (أيمؤقتًا) باستخدام حزمة حالة المثيل

[Code sample – Store state in state bundle]
@Override
public void onSaveInstanceState(Bundle savedInstanceState)
{
  // Store UI state to the savedInstanceState.
  // This bundle will be passed to onCreate on next call.  EditText txtName = (EditText)findViewById(R.id.txtName);
  String strName = txtName.getText().toString();

  EditText txtEmail = (EditText)findViewById(R.id.txtEmail);
  String strEmail = txtEmail.getText().toString();

  CheckBox chkTandC = (CheckBox)findViewById(R.id.chkTandC);
  boolean blnTandC = chkTandC.isChecked();

  savedInstanceState.putString(“Name”, strName);
  savedInstanceState.putString(“Email”, strEmail);
  savedInstanceState.putBoolean(“TandC”, blnTandC);

  super.onSaveInstanceState(savedInstanceState);
}

تخزين بيانات التحكم في المتغير/واجهة المستخدم المحلية بين مثيلات التطبيق (أيبشكل دائم) باستخدام التفضيلات المشتركة

[Code sample – store state in SharedPreferences]
@Override
protected void onPause()
{
  super.onPause();

  // Store values between instances here
  SharedPreferences preferences = getPreferences(MODE_PRIVATE);
  SharedPreferences.Editor editor = preferences.edit();  // Put the values from the UI
  EditText txtName = (EditText)findViewById(R.id.txtName);
  String strName = txtName.getText().toString();

  EditText txtEmail = (EditText)findViewById(R.id.txtEmail);
  String strEmail = txtEmail.getText().toString();

  CheckBox chkTandC = (CheckBox)findViewById(R.id.chkTandC);
  boolean blnTandC = chkTandC.isChecked();

  editor.putString(“Name”, strName); // value to store
  editor.putString(“Email”, strEmail); // value to store
  editor.putBoolean(“TandC”, blnTandC); // value to store
  // Commit to storage
  editor.commit();
}

الحفاظ على مثيلات الكائن حية في الذاكرة بين الأنشطة خلال عمر التطبيق باستخدام مثيل غير تكوين تم الاحتفاظ به

[Code sample – store object instance]
private cMyClassType moInstanceOfAClass; // Store the instance of an object
@Override
public Object onRetainNonConfigurationInstance()
{
  if (moInstanceOfAClass != null) // Check that the object exists
      return(moInstanceOfAClass);
  return super.onRetainNonConfigurationInstance();
}

هذه مشكلة كلاسيكية في تطوير Android.هناك نوعان من القضايا هنا:

  • يوجد خطأ خفي في Android Framework مما يؤدي إلى تعقيد إدارة حزمة التطبيقات بشكل كبير أثناء التطوير، على الأقل في الإصدارات القديمة (لست متأكدًا تمامًا مما إذا كان/متى/كيف تم إصلاحه).سأناقش هذا الخطأ أدناه.
  • الطريقة "العادية" أو المقصودة لإدارة هذه المشكلة هي في حد ذاتها معقدة إلى حد ما بسبب ازدواجية onPause/onResume وonSaveInstanceState/onRestoreInstanceState

من خلال تصفح كل هذه المواضيع، أظن أن المطورين يتحدثون في كثير من الأحيان عن هاتين المسألتين المختلفتين في وقت واحد ...ومن هنا كل الالتباس والتقارير التي تقول "هذا لا يناسبني".

أولاً، لتوضيح السلوك "المقصود":onSaveInstance وonRestoreInstance هشتان ولا تستخدمان إلا للحالة العابرة.الاستخدام المقصود (afaict) هو التعامل مع إعادة النشاط عند تدوير الهاتف (تغيير الاتجاه).بمعنى آخر، الاستخدام المقصود هو عندما يكون نشاطك "في المقدمة" منطقيًا، ولكن لا يزال يتعين على النظام استعادته.لا تستمر الحزمة المحفوظة خارج العملية/الذاكرة/gc، لذلك لا يمكنك الاعتماد عليها حقًا إذا انتقل نشاطك إلى الخلفية.نعم، ربما ستنجو ذاكرة نشاطك من رحلتها إلى الخلفية وتهرب من GC، لكن هذا غير موثوق (ولا يمكن التنبؤ به).

لذا، إذا كان لديك سيناريو حيث يوجد "تقدم مستخدم" ذي معنى أو حالة يجب أن تستمر بين "عمليات تشغيل" التطبيق الخاص بك، فإن التوجيه هو استخدام onPause وonResume.يجب عليك اختيار وإعداد متجر دائم بنفسك.

ولكن - هناك خطأ مربك للغاية يزيد من تعقيد كل هذا.التفاصيل هنا:

http://code.google.com/p/android/issues/detail?id=2373

http://code.google.com/p/android/issues/detail?id=5277

في الأساس، إذا تم تشغيل التطبيق الخاص بك باستخدام علامة SingleTask، ثم قمت بتشغيله لاحقًا من الشاشة الرئيسية أو قائمة المشغل، فإن هذا الاستدعاء اللاحق سينشئ مهمة جديدة ...سيكون لديك بالفعل مثيلان مختلفان لتطبيقك يسكنان نفس المكدس ...والذي يصبح غريبًا جدًا وبسرعة كبيرة.يبدو أن هذا يحدث عند تشغيل تطبيقك أثناء التطوير (على سبيل المثال.من Eclipse أو Intellij)، لذلك يواجه المطورون هذا الأمر كثيرًا.ولكن أيضًا من خلال بعض آليات تحديث متجر التطبيقات (وبالتالي يؤثر ذلك على المستخدمين لديك أيضًا).

لقد تصارعت مع هذه المواضيع لساعات قبل أن أدرك أن مشكلتي الرئيسية كانت هذا الخطأ، وليس سلوك إطار العمل المقصود.كتابة عظيمة و الحل البديل (تحديث:انظر أدناه) يبدو أنه من المستخدمkaciula في هذه الإجابة:

سلوك الضغط على مفتاح الصفحة الرئيسية

تحديث يونيو 2013:وبعد أشهر، وجدت أخيرًا الحل "الصحيح".لا تحتاج إلى إدارة أي علامات بدء تشغيل ذات حالة بنفسك، يمكنك اكتشاف ذلك من إطار العمل والكفالة بشكل مناسب.أستخدم هذا بالقرب من بداية LauncherActivity.onCreate:

if (!isTaskRoot()) {
    Intent intent = getIntent();
    String action = intent.getAction();
    if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && action != null && action.equals(Intent.ACTION_MAIN)) {
        finish();
        return;
    }
}

onSaveInstanceState يتم استدعاؤه عندما يحتاج النظام إلى الذاكرة ويقتل أحد التطبيقات.ولا يتم استدعاؤه عندما يقوم المستخدم بإغلاق التطبيق للتو.لذلك أعتقد أنه يجب أيضًا حفظ حالة التطبيق onPause يجب أن يتم حفظه في بعض وحدات التخزين المستمرة مثل Preferences أو Sqlite

كلا الطريقتين مفيدتان وصالحتان وكلاهما مناسب لسيناريوهات مختلفة:

  1. يقوم المستخدم بإنهاء التطبيق وإعادة فتحه في وقت لاحق، ولكن يحتاج التطبيق إلى إعادة تحميل البيانات من الجلسة الأخيرة - وهذا يتطلب أسلوب تخزين مستمر مثل استخدام SQLite.
  2. يقوم المستخدم بتبديل التطبيق ثم يعود إلى التطبيق الأصلي ويريد المتابعة من حيث توقف - حفظ واستعادة بيانات الحزمة (مثل بيانات حالة التطبيق) في onSaveInstanceState() و onRestoreInstanceState() عادة ما تكون كافية.

إذا قمت بحفظ بيانات الحالة بطريقة مستمرة، فيمكن إعادة تحميلها بطريقة onResume() أو onCreate() (أو في الواقع على أي استدعاء دورة الحياة).قد يكون هذا السلوك مرغوبًا وقد لا يكون كذلك.إذا قمت بتخزينه في حزمة في InstanceState, ، فهي عابرة ومناسبة فقط لتخزين البيانات لاستخدامها في "جلسة" المستخدم نفسها (أستخدم مصطلح الجلسة بشكل فضفاض) ولكن ليس بين "الجلسات".

لا يعني ذلك أن أحد الأساليب أفضل من الآخر، مثل كل شيء، من المهم فقط فهم السلوك الذي تحتاجه واختيار النهج الأكثر ملاءمة.

إن توفير الحالة هو أمر مخادع في أحسن الأحوال بالنسبة لي.إذا كنت بحاجة إلى حفظ البيانات المستمرة، فما عليك سوى استخدام ملف سكليتي قاعدة البيانات.أندرويد يصنعها سوو سهل.

شيء من هذا القبيل:

import java.util.Date;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class dataHelper {

    private static final String DATABASE_NAME = "autoMate.db";
    private static final int DATABASE_VERSION = 1;

    private Context context;
    private SQLiteDatabase db;
    private OpenHelper oh ;

    public dataHelper(Context context) {
        this.context = context;
        this.oh = new OpenHelper(this.context);
        this.db = oh.getWritableDatabase();
    }

    public void close() {
        db.close();
        oh.close();
        db = null;
        oh = null;
        SQLiteDatabase.releaseMemory();
    }


    public void setCode(String codeName, Object codeValue, String codeDataType) {
        Cursor codeRow = db.rawQuery("SELECT * FROM code WHERE codeName = '"+  codeName + "'", null);
        String cv = "" ;

        if (codeDataType.toLowerCase().trim().equals("long") == true){
            cv = String.valueOf(codeValue);
        }
        else if (codeDataType.toLowerCase().trim().equals("int") == true)
        {
            cv = String.valueOf(codeValue);
        }
        else if (codeDataType.toLowerCase().trim().equals("date") == true)
        {
            cv = String.valueOf(((Date)codeValue).getTime());
        }
        else if (codeDataType.toLowerCase().trim().equals("boolean") == true)
        {
            String.valueOf(codeValue);
        }
        else
        {
            cv = String.valueOf(codeValue);
        }

        if(codeRow.getCount() > 0) //exists-- update
        {
            db.execSQL("update code set codeValue = '" + cv +
                "' where codeName = '" + codeName + "'");
        }
        else // does not exist, insert
        {
            db.execSQL("INSERT INTO code (codeName, codeValue, codeDataType) VALUES(" +
                    "'" + codeName + "'," +
                    "'" + cv + "'," +
                    "'" + codeDataType + "')" );
        }
    }

    public Object getCode(String codeName, Object defaultValue){

        //Check to see if it already exists
        String codeValue = "";
        String codeDataType = "";
        boolean found = false;
        Cursor codeRow  = db.rawQuery("SELECT * FROM code WHERE codeName = '"+  codeName + "'", null);
        if (codeRow.moveToFirst())
        {
            codeValue = codeRow.getString(codeRow.getColumnIndex("codeValue"));
            codeDataType = codeRow.getString(codeRow.getColumnIndex("codeDataType"));
            found = true;
        }

        if (found == false)
        {
            return defaultValue;
        }
        else if (codeDataType.toLowerCase().trim().equals("long") == true)
        {
            if (codeValue.equals("") == true)
            {
                return (long)0;
            }
            return Long.parseLong(codeValue);
        }
        else if (codeDataType.toLowerCase().trim().equals("int") == true)
        {
            if (codeValue.equals("") == true)
            {
                return (int)0;
            }
            return Integer.parseInt(codeValue);
        }
        else if (codeDataType.toLowerCase().trim().equals("date") == true)
        {
            if (codeValue.equals("") == true)
            {
                return null;
            }
            return new Date(Long.parseLong(codeValue));
        }
        else if (codeDataType.toLowerCase().trim().equals("boolean") == true)
        {
            if (codeValue.equals("") == true)
            {
                return false;
            }
            return Boolean.parseBoolean(codeValue);
        }
        else
        {
            return (String)codeValue;
        }
    }


    private static class OpenHelper extends SQLiteOpenHelper {

        OpenHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL("CREATE TABLE IF  NOT EXISTS code" +
            "(id INTEGER PRIMARY KEY, codeName TEXT, codeValue TEXT, codeDataType TEXT)");
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        }
    }
}

مكالمة بسيطة بعد ذلك

dataHelper dh = new dataHelper(getBaseContext());
String status = (String) dh.getCode("appState", "safetyDisabled");
Date serviceStart = (Date) dh.getCode("serviceStartTime", null);
dh.close();
dh = null;

أعتقد أنني وجدت الجواب.دعني أخبرك بما قمت به بكلمات بسيطة:

لنفترض أن لدي نشاطين، النشاط 1 والنشاط 2 وأنا أتنقل من النشاط 1 إلى النشاط 2 (لقد قمت ببعض الأعمال في النشاط 2) وأعود مرة أخرى إلى النشاط 1 من خلال النقر على زر في النشاط 1.الآن في هذه المرحلة، أردت العودة إلى النشاط 2 وأريد أن أرى نشاطي 2 في نفس الحالة عندما غادرت النشاط 2 آخر مرة.

بالنسبة للسيناريو أعلاه، ما قمت به هو أنني أجريت بعض التغييرات في البيان مثل هذا:

<activity android:name=".activity2"
          android:alwaysRetainTaskState="true"      
          android:launchMode="singleInstance">
</activity>

وفي النشاط 1 في حدث النقر على الزر، قمت بذلك على النحو التالي:

Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
intent.setClassName(this,"com.mainscreen.activity2");
startActivity(intent);

وفي النشاط 2 عند النقر على الزر، قمت بذلك على النحو التالي:

Intent intent=new Intent();
intent.setClassName(this,"com.mainscreen.activity1");
startActivity(intent);

ما سيحدث الآن هو أنه مهما كانت التغييرات التي أجريناها في النشاط 2 فلن تضيع، ويمكننا عرض النشاط 2 في نفس الحالة التي تركناها سابقًا.

أعتقد أن هذا هو الجواب وهذا يعمل بشكل جيد بالنسبة لي.صحح لي إن كنت مخطئ.

onSaveInstanceState() للبيانات العابرة (المستعادة في onCreate()/onRestoreInstanceState()), onPause() للبيانات المستمرة (المستعادة في onResume()).من الموارد التقنية لنظام Android:

onSaveInstanceState() يتم استدعاؤه بواسطة Android إذا تم إيقاف النشاط وقد يتم إيقافه قبل استئنافه!وهذا يعني أنه يجب عليه تخزين أي حالة ضرورية لإعادة التهيئة لنفس الحالة عند إعادة تشغيل النشاط.إنه النظير لطريقة onCreate()، وفي الواقع فإن حزمة saveInstanceState التي تم تمريرها إلى onCreate() هي نفس الحزمة التي تقوم بإنشائها كـ outState في طريقة onSaveInstanceState().

onPause() و onResume() هي أيضًا طرق مجانية.يتم استدعاء onPause() دائمًا عند انتهاء النشاط، حتى لو حرضنا على ذلك (من خلال استدعاء Finish() على سبيل المثال).سوف نستخدم هذا لحفظ الملاحظة الحالية مرة أخرى في قاعدة البيانات.الممارسة الجيدة هي تحرير أي موارد يمكن تحريرها أثناء onPause() أيضًا، لاستيعاب موارد أقل عندما تكون في الحالة الخاملة.

حقًا onSaveInstance استدعاء الحالة عندما ينتقل النشاط إلى الخلفية

مقتبس من المستندات:"طريقة onSaveInstanceState(Bundle) يتم استدعاؤه قبل وضع النشاط في حالة الخلفية هذه"

للمساعدة في تقليل النمط المتداول أستخدم ما يلي interface و class للقراءة/الكتابة إلى أ Bundle لحفظ حالة المثيل.


أولاً، قم بإنشاء واجهة سيتم استخدامها للتعليق على متغيرات المثيل الخاصة بك:

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({
        ElementType.FIELD
})
public @interface SaveInstance {

}

بعد ذلك، قم بإنشاء فصل حيث سيتم استخدام الانعكاس لحفظ القيم في الحزمة:

import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;

import java.io.Serializable;
import java.lang.reflect.Field;

/**
 * Save and load fields to/from a {@link Bundle}. All fields should be annotated with {@link
 * SaveInstance}.</p>
 */
public class Icicle {

    private static final String TAG = "Icicle";

    /**
     * Find all fields with the {@link SaveInstance} annotation and add them to the {@link Bundle}.
     *
     * @param outState
     *         The bundle from {@link Activity#onSaveInstanceState(Bundle)} or {@link
     *         Fragment#onSaveInstanceState(Bundle)}
     * @param classInstance
     *         The object to access the fields which have the {@link SaveInstance} annotation.
     * @see #load(Bundle, Object)
     */
    public static void save(Bundle outState, Object classInstance) {
        save(outState, classInstance, classInstance.getClass());
    }

    /**
     * Find all fields with the {@link SaveInstance} annotation and add them to the {@link Bundle}.
     *
     * @param outState
     *         The bundle from {@link Activity#onSaveInstanceState(Bundle)} or {@link
     *         Fragment#onSaveInstanceState(Bundle)}
     * @param classInstance
     *         The object to access the fields which have the {@link SaveInstance} annotation.
     * @param baseClass
     *         Base class, used to get all superclasses of the instance.
     * @see #load(Bundle, Object, Class)
     */
    public static void save(Bundle outState, Object classInstance, Class<?> baseClass) {
        if (outState == null) {
            return;
        }
        Class<?> clazz = classInstance.getClass();
        while (baseClass.isAssignableFrom(clazz)) {
            String className = clazz.getName();
            for (Field field : clazz.getDeclaredFields()) {
                if (field.isAnnotationPresent(SaveInstance.class)) {
                    field.setAccessible(true);
                    String key = className + "#" + field.getName();
                    try {
                        Object value = field.get(classInstance);
                        if (value instanceof Parcelable) {
                            outState.putParcelable(key, (Parcelable) value);
                        } else if (value instanceof Serializable) {
                            outState.putSerializable(key, (Serializable) value);
                        }
                    } catch (Throwable t) {
                        Log.d(TAG, "The field '" + key + "' was not added to the bundle");
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }
    }

    /**
     * Load all saved fields that have the {@link SaveInstance} annotation.
     *
     * @param savedInstanceState
     *         The saved-instance {@link Bundle} from an {@link Activity} or {@link Fragment}.
     * @param classInstance
     *         The object to access the fields which have the {@link SaveInstance} annotation.
     * @see #save(Bundle, Object)
     */
    public static void load(Bundle savedInstanceState, Object classInstance) {
        load(savedInstanceState, classInstance, classInstance.getClass());
    }

    /**
     * Load all saved fields that have the {@link SaveInstance} annotation.
     *
     * @param savedInstanceState
     *         The saved-instance {@link Bundle} from an {@link Activity} or {@link Fragment}.
     * @param classInstance
     *         The object to access the fields which have the {@link SaveInstance} annotation.
     * @param baseClass
     *         Base class, used to get all superclasses of the instance.
     * @see #save(Bundle, Object, Class)
     */
    public static void load(Bundle savedInstanceState, Object classInstance, Class<?> baseClass) {
        if (savedInstanceState == null) {
            return;
        }
        Class<?> clazz = classInstance.getClass();
        while (baseClass.isAssignableFrom(clazz)) {
            String className = clazz.getName();
            for (Field field : clazz.getDeclaredFields()) {
                if (field.isAnnotationPresent(SaveInstance.class)) {
                    String key = className + "#" + field.getName();
                    field.setAccessible(true);
                    try {
                        Object fieldVal = savedInstanceState.get(key);
                        if (fieldVal != null) {
                            field.set(classInstance, fieldVal);
                        }
                    } catch (Throwable t) {
                        Log.d(TAG, "The field '" + key + "' was not retrieved from the bundle");
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }
    }

}

مثال على الاستخدام:

public class MainActivity extends Activity {

    @SaveInstance
    private String foo;

    @SaveInstance
    private int bar;

    @SaveInstance
    private Intent baz;

    @SaveInstance
    private boolean qux;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Icicle.load(savedInstanceState, this);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Icicle.save(outState, this);
    }

}

ملحوظة: تم تعديل هذا الرمز من مشروع مكتبة اسمه androidAutowire والتي تم ترخيصها بموجب رخصة معهد ماساتشوستس للتكنولوجيا.

وفي الوقت نفسه أفعل بشكل عام لا مزيد من الاستخدام

Bundle savedInstanceState & Co

تعتبر دورة الحياة بالنسبة لمعظم الأنشطة معقدة للغاية وليست ضرورية.

وتقول جوجل نفسها إنها ليست موثوقة حتى.

طريقتي هي حفظ أي تغييرات على الفور في التفضيلات:

 SharedPreferences p;
 p.edit().put(..).commit()

بطريقة ما، تعمل SharedPreferences بشكل مشابه مثل الحزم.وبطبيعة الحال وفي البداية يجب قراءة هذه القيم من التفضيلات.

في حالة البيانات المعقدة، يمكنك استخدام SQLite بدلاً من استخدام التفضيلات.

عند تطبيق هذا المفهوم، يستمر النشاط في استخدام آخر حالة محفوظة، بغض النظر عما إذا كانت عملية فتح أولية مع عمليات إعادة تشغيل بينهما أو إعادة فتح بسبب المكدس الخلفي.

للإجابة على السؤال الأصلي مباشرة.saveInstancestate فارغ لأنه لن يتم إعادة إنشاء نشاطك مطلقًا.

لن تتم إعادة إنشاء نشاطك إلا باستخدام حزمة الحالة عندما:

  • تغييرات التكوين مثل تغيير الاتجاه أو لغة الهاتف مما قد يتطلب إنشاء مثيل نشاط جديد.
  • يمكنك العودة إلى التطبيق من الخلفية بعد أن يقوم نظام التشغيل بتدمير النشاط.

سيقوم Android بتدمير أنشطة الخلفية عندما تكون تحت ضغط الذاكرة أو بعد أن تظل في الخلفية لفترة طويلة من الوقت.

عند اختبار مثال عالم الترحيب الخاص بك، هناك عدة طرق لمغادرة النشاط والعودة إليه.

  • عند الضغط على زر الرجوع، ينتهي النشاط.تعد إعادة تشغيل التطبيق مثالًا جديدًا تمامًا.أنت لا تستأنف من الخلفية على الإطلاق.
  • عند الضغط على زر الصفحة الرئيسية أو استخدام مبدل المهام، سينتقل النشاط إلى الخلفية.عند العودة إلى التطبيق، لن يتم استدعاء onCreate إلا إذا كان من الضروري تدمير النشاط.

في معظم الحالات، إذا كنت تضغط على الصفحة الرئيسية ثم تقوم بتشغيل التطبيق مرة أخرى، فلن يلزم إعادة إنشاء النشاط.إنه موجود بالفعل في الذاكرة لذا لن يتم استدعاء onCreate().

يوجد خيار ضمن الإعدادات -> خيارات المطور يسمى "لا تحتفظ بالأنشطة".عندما يتم تمكينه، سيقوم Android دائمًا بتدمير الأنشطة وإعادة إنشائها عندما تكون في الخلفية.يعد هذا خيارًا رائعًا لتركه ممكّنًا عند التطوير لأنه يحاكي أسوأ السيناريوهات.(جهاز ذو ذاكرة منخفضة يعيد تدوير أنشطتك طوال الوقت).

تعتبر الإجابات الأخرى ذات قيمة لأنها تعلمك الطرق الصحيحة لتخزين الحالة ولكني لم أشعر أنهم أجابوا حقًا عن سبب عدم عمل الكود الخاص بك بالطريقة التي توقعتها.

ال onSaveInstanceState(bundle) و onRestoreInstanceState(bundle) تعتبر الطرق مفيدة لاستمرارية البيانات فقط أثناء تدوير الشاشة (تغيير الاتجاه).
إنها ليست جيدة حتى أثناء التبديل بين التطبيقات (نظرًا لـ onSaveInstanceState() تسمى الطريقة ولكن onCreate(bundle) و onRestoreInstanceState(bundle) لا يتم استدعاؤه مرة أخرى.
لمزيد من الثبات استخدم التفضيلات المشتركة. اقرأ هذه المقالة

كانت مشكلتي أنني كنت بحاجة إلى المثابرة فقط خلال عمر التطبيق (أي:تنفيذ واحد بما في ذلك بدء أنشطة فرعية أخرى داخل نفس التطبيق وتدوير الجهاز وما إلى ذلك).لقد جربت مجموعات مختلفة من الإجابات المذكورة أعلاه ولكني لم أحصل على ما أردت في جميع المواقف.في النهاية، ما نجح بالنسبة لي هو الحصول على مرجع إلى saveInstanceState أثناء onCreate:

mySavedInstanceState=savedInstanceState;

واستخدم ذلك للحصول على محتويات المتغير الخاص بي عندما أحتاج إليه، على غرار:

if (mySavedInstanceState !=null) {
   boolean myVariable = mySavedInstanceState.getBoolean("MyVariable");
}

أنا أستعمل onSaveInstanceStateو onRestoreInstanceState كما هو مقترح أعلاه ولكن أعتقد أنه يمكنني أيضًا أو بدلاً من ذلك استخدام طريقتي لحفظ المتغير عندما يتغير (على سبيل المثال.استخدام putBoolean)

على الرغم من أن الإجابة المقبولة صحيحة، إلا أن هناك طريقة أسرع وأسهل لحفظ حالة النشاط على نظام Android باستخدام مكتبة تسمى اختيار الجليد.Icepick هو معالج للتعليقات التوضيحية يعتني بجميع التعليمات البرمجية المعيارية المستخدمة في حفظ الحالة واستعادتها.

القيام بشيء مثل هذا مع Icepick:

class MainActivity extends Activity {
  @State String username; // These will be automatically saved and restored
  @State String password;
  @State int age;

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Icepick.restoreInstanceState(this, savedInstanceState);
  }

  @Override public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    Icepick.saveInstanceState(this, outState);
  }
}

هو نفس القيام بذلك:

class MainActivity extends Activity {
  String username;
  String password;
  int age;

  @Override
  public void onSaveInstanceState(Bundle savedInstanceState) {
    super.onSaveInstanceState(savedInstanceState);
    savedInstanceState.putString("MyString", username);
    savedInstanceState.putString("MyPassword", password);
    savedInstanceState.putInt("MyAge", age); 
    /* remember you would need to actually initialize these variables before putting it in the
    Bundle */
  }

  @Override
  public void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    username = savedInstanceState.getString("MyString");
    password = savedInstanceState.getString("MyPassword");
    age = savedInstanceState.getInt("MyAge");
  }
}

سيعمل Icepick مع أي كائن يحفظ حالته بامتداد Bundle.

عندما يتم إنشاء نشاط ما، يتم استدعاء أسلوب onCreate().

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

saveInstanceState هو كائن من فئة Bundle وهو فارغ لأول مرة، ولكنه يحتوي على قيم عند إعادة إنشائه.لحفظ حالة النشاط، عليك تجاوز onSaveInstanceState().

   @Override
    protected void onSaveInstanceState(Bundle outState) {
      outState.putString("key","Welcome Back")
        super.onSaveInstanceState(outState);       //save state
    }

ضع قيمك في كائن حزمة "outState" مثل outState.putString("key"، "Welcome Back") واحفظها عن طريق الاتصال بـ super.عندما يتم تدمير النشاط، يتم حفظ حالته في كائن Bundle ويمكن استعادتها بعد الإنشاء في onCreate() أو onRestoreInstanceState().الحزمة المستلمة في onCreate() وonRestoreInstanceState() متماثلة.

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

          //restore activity's state
         if(savedInstanceState!=null){
          String reStoredString=savedInstanceState.getString("key");
            }
    }

أو

  //restores activity's saved state
 @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
      String restoredMessage=savedInstanceState.getString("key");
    }

هناك طريقتان أساسيتان لتنفيذ هذا التغيير.

  1. استخدام onSaveInstanceState() و onRestoreInstanceState().
  2. في البيان android:configChanges="orientation|screenSize".

أنا حقا لا أنصح باستخدام الطريقة الثانية.حيث أنه في إحدى تجاربي كان يتسبب في تحول نصف شاشة الجهاز إلى اللون الأسود أثناء الدوران من الوضع الرأسي إلى الأفقي والعكس.

باستخدام الطريقة الأولى المذكورة أعلاه، يمكننا الاحتفاظ بالبيانات عند تغيير الاتجاه أو حدوث أي تغيير في التكوين.أعرف طريقة يمكنك من خلالها تخزين أي نوع من البيانات داخل كائن حالة saveInstance.

مثال:فكر في حالة إذا كنت تريد استمرار كائن Json.إنشاء فئة نموذجية باستخدام الحروف والأدوات.

class MyModel extends Serializable{
JSONObject obj;

setJsonObject(JsonObject obj)
{
this.obj=obj;
}

JSONObject getJsonObject()
return this.obj;
} 
}

الآن، في نشاطك في طريقة onCreate وonSaveInstanceState، قم بما يلي.سيبدو شيئا من هذا القبيل:

@override
onCreate(Bundle savedInstaceState){
MyModel data= (MyModel)savedInstaceState.getSerializable("yourkey")
JSONObject obj=data.getJsonObject();
//Here you have retained JSONObject and can use.
}


@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
//Obj is some json object 
MyModel dataToSave= new MyModel();
dataToSave.setJsonObject(obj);
oustate.putSerializable("yourkey",dataToSave); 

}

هنا تعليق من ستيف موسليإجابة (بواسطة صانع الأدواتستيف) الذي يضع الأمور في نصابها الصحيح (في مجمل onSaveInstanceState vs onPause، east cost vs west cost saga)

@VVK - أنا لا أوافق جزئيًا.بعض طرق الخروج من التطبيق لا تؤدي إلى onsaveinstancestate (oles).وهذا يحد من فائدة OSIS.إنه يستحق الدعم ، للحصول على الحد الأدنى من موارد نظام التشغيل ، ولكن إذا أراد التطبيق إعادة المستخدم إلى الحالة التي كانوا فيها ، بغض النظر عن كيفية خروج التطبيق ، فمن الضروري استخدام نهج التخزين المستمر بدلاً من ذلك. أستخدم onCreate للتحقق من وجود الحزمة، وإذا كانت مفقودة، فتحقق منها التخزين المستمر. وهذا يجعل عملية صنع القرار مركزية.يمكنني التعافي من خروج الزر "تعطل ، أو مخرج عن عنصر القائمة المخصصة ، أو العودة إلى مستخدم الشاشة كان في وقت لاحق بعد عدة أيام.- صانعة الأدوات 19 سبتمبر في الساعة 10:38

كود كوتلين:

يحفظ:

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState.apply {
        putInt("intKey", 1)
        putString("stringKey", "String Value")
        putParcelable("parcelableKey", parcelableObject)
    })
}

ومن ثم في onCreate() أو onRestoreInstanceState()

    val restoredInt = savedInstanceState?.getInt("intKey") ?: 1 //default int
    val restoredString = savedInstanceState?.getString("stringKey") ?: "default string"
    val restoredParcelable = savedInstanceState?.getParcelable<ParcelableClass>("parcelableKey") ?: ParcelableClass() //default parcelable

أضف قيمًا افتراضية إذا كنت لا تريد الحصول على خيارات اختيارية

للحصول على بيانات حالة النشاط المخزنة في onCreate(), ، عليك أولاً حفظ البيانات في saveInstanceState عن طريق التجاوز SaveInstanceState(Bundle savedInstanceState) طريقة.

عندما يدمر النشاط SaveInstanceState(Bundle savedInstanceState) يتم استدعاء الطريقة وهناك تقوم بحفظ البيانات التي تريد حفظها.وتحصل على نفس الشيء onCreate() عند إعادة تشغيل النشاط. (لن يكون saveInstanceState فارغًا نظرًا لأنك قمت بحفظ بعض البيانات فيه قبل تدمير النشاط)

بسيطة وسريعة لحل هذه المشكلة باستخدام اختيار الجليد

أولاً، قم بإعداد المكتبة في app/build.gradle

repositories {
  maven {url "https://clojars.org/repo/"}
}
dependencies {
  compile 'frankiesardo:icepick:3.2.0'
  provided 'frankiesardo:icepick-processor:3.2.0'
}

الآن، دعونا نتحقق من هذا المثال أدناه حول كيفية حفظ الحالة في النشاط

public class ExampleActivity extends Activity {
  @State String username; // This will be automatically saved and restored

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Icepick.restoreInstanceState(this, savedInstanceState);
  }

  @Override public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    Icepick.saveInstanceState(this, outState);
  }
}

إنه يعمل مع الأنشطة أو الأجزاء أو أي كائن يحتاج إلى إجراء تسلسل لحالته على حزمة (على سبيل المثال.عرض مقدمي الهاون)

يمكن لـ Icepick أيضًا إنشاء رمز حالة المثيل لطرق العرض المخصصة:

class CustomView extends View {
  @State int selectedPosition; // This will be automatically saved and restored

  @Override public Parcelable onSaveInstanceState() {
    return Icepick.saveInstanceState(this, super.onSaveInstanceState());
  }

  @Override public void onRestoreInstanceState(Parcelable state) {
    super.onRestoreInstanceState(Icepick.restoreInstanceState(this, state));
  }

  // You can put the calls to Icepick into a BaseCustomView and inherit from it
  // All Views extending this CustomView automatically have state saved/restored
}

لست متأكدًا مما إذا كان الحل الخاص بي مستهجنًا أم لا، ولكني أستخدم خدمة مقيدة للاستمرار في حالة ViewModel.يعتمد ما إذا كنت تقوم بتخزينه في الذاكرة في الخدمة أو الاستمرار فيه واسترجاعه من قاعدة بيانات SQLite على متطلباتك.هذا هو ما تفعله الخدمات من أي نوع، فهي تقدم خدمات مثل الحفاظ على حالة التطبيق وتجريد منطق العمل المشترك.

بسبب قيود الذاكرة والمعالجة المتأصلة في الأجهزة المحمولة، أتعامل مع مشاهدات Android بطريقة مشابهة لصفحة الويب.لا تحافظ الصفحة على الحالة، فهي مجرد مكون طبقة عرض هدفه الوحيد هو تقديم حالة التطبيق وقبول إدخال المستخدم.تستخدم الاتجاهات الحديثة في هندسة تطبيقات الويب استخدام النمط القديم للنموذج والعرض ووحدة التحكم (MVC)، حيث تكون الصفحة هي العرض، وبيانات المجال هي النموذج، وتقع وحدة التحكم خلف خدمة الويب.يمكن استخدام نفس النمط في Android مع عرض، حسنًا ...العرض، النموذج هو بيانات المجال الخاص بك، ويتم تنفيذ وحدة التحكم كخدمة مرتبطة بنظام Android.عندما تريد أن يتفاعل العرض مع وحدة التحكم، اربطه عند البدء/الاستئناف وفك الارتباط عند الإيقاف/الإيقاف المؤقت.

يمنحك هذا النهج ميزة إضافية تتمثل في فرض مبدأ تصميم فصل الاهتمام حيث يمكن نقل منطق أعمال التطبيق الخاص بك جميعًا إلى خدمتك مما يقلل المنطق المكرر عبر طرق عرض متعددة ويسمح للعرض بفرض مبدأ تصميم مهم آخر، وهو المسؤولية الفردية.

الآن يوفر Android ViewModels لحفظ الحالة، يجب أن تحاول استخدامها بدلاً من saveInstanceState.

ما يجب حفظه وما لا يجب حفظه؟

هل تساءلت يومًا عن سبب النص الموجود في EditText يتم حفظه تلقائيًا أثناء تغيير الاتجاه؟حسنًا، هذه الإجابة لك.

عندما يتم تدمير مثيل للنشاط ويقوم النظام بإعادة إنشاء مثيل جديد (على سبيل المثال، تغيير التكوين).يحاول إعادة إنشائه باستخدام مجموعة من البيانات المحفوظة لحالة النشاط القديمة (حالة المثيل).

حالة المثيل عبارة عن مجموعة من قيمة المفتاح الأزواج المخزنة في Bundle هدف.

افتراضيًا، يحفظ النظام كائنات العرض في الحزمة على سبيل المثال.

  • النص في EditText
  • وضع التمرير في أ ListView, ، إلخ.

إذا كنت بحاجة إلى حفظ متغير آخر كجزء من حالة المثيل، فيجب عليك ذلك تجاوز onSavedInstanceState(Bundle savedinstaneState) طريقة.

على سبيل المثال، int currentScore في نشاط اللعبة

مزيد من التفاصيل حول onSavedInstanceState(Bundle saveinstaneState) أثناء حفظ البيانات

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    // Save the user's current game state
    savedInstanceState.putInt(STATE_SCORE, mCurrentScore);

    // Always call the superclass so it can save the view hierarchy state
    super.onSaveInstanceState(savedInstanceState);
}

لذلك عن طريق الخطأ إذا نسيت الاتصال super.onSaveInstanceState(savedInstanceState);لن يعمل السلوك الافتراضي على أي نص في EditText لن ينقذ.

ما الذي يجب اختياره لاستعادة حالة النشاط؟

 onCreate(Bundle savedInstanceState)

أو

onRestoreInstanceState(Bundle savedInstanceState)

تحصل كلتا الطريقتين على نفس كائن الحزمة، لذلك لا يهم حقًا أين تكتب منطق الاستعادة الخاص بك.والفرق الوحيد هو أنه في onCreate(Bundle savedInstanceState) الطريقة التي سيتعين عليك فيها إعطاء فحص فارغ بينما لا تكون هناك حاجة إليه في الحالة الأخيرة.تحتوي الإجابات الأخرى بالفعل على مقتطفات من التعليمات البرمجية.يمكنك الرجوع إليهم.

مزيد من التفاصيل حول onRestoreInstanceState(Bundle saveinstaneState)

@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
    // Always call the superclass so it can restore the view hierarchy
    super.onRestoreInstanceState(savedInstanceState);

    // Restore state members from the saved instance
    mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
}

اتصل دائما super.onRestoreInstanceState(savedInstanceState); بحيث يقوم النظام باستعادة التسلسل الهرمي للعرض بشكل افتراضي

علاوة

ال onSaveInstanceState(Bundle savedInstanceState) يتم استدعاؤه بواسطة النظام فقط عندما ينوي المستخدم العودة إلى النشاط.على سبيل المثال، أنت تستخدم التطبيق X وفجأة تتلقى مكالمة.تنتقل إلى تطبيق المتصل وتعود إلى التطبيق X.في هذه الحالة onSaveInstanceState(Bundle savedInstanceState) سيتم استدعاء الطريقة.

ولكن ضع في اعتبارك هذا إذا قام المستخدم بالضغط على زر الرجوع.ومن المفترض أن المستخدم لا ينوي العودة إلى النشاط، وبالتالي في هذه الحالة onSaveInstanceState(Bundle savedInstanceState) لن يتم استدعاؤها من قبل النظام.النقطة المهمة هي أنه يجب عليك مراعاة جميع السيناريوهات أثناء حفظ البيانات.

الروابط ذات الصلة:

عرض توضيحي للسلوك الافتراضي
وثائق الروبوت الرسمية.

كوتلين

يجب عليك تجاوز onSaveInstanceState و onRestoreInstanceState لتخزين واسترداد المتغيرات الخاصة بك التي تريد أن تكون ثابتة

الرسم البياني لدورة الحياة

تخزين المتغيرات

public override fun onSaveInstanceState(savedInstanceState: Bundle) {
    super.onSaveInstanceState(savedInstanceState)

    // prepare variables here
    savedInstanceState.putInt("kInt", 10)
    savedInstanceState.putBoolean("kBool", true)
    savedInstanceState.putDouble("kDouble", 4.5)
    savedInstanceState.putString("kString", "Hello Kotlin")
}

استرجاع المتغيرات

public override fun onRestoreInstanceState(savedInstanceState: Bundle) {
    super.onRestoreInstanceState(savedInstanceState)

    val myInt = savedInstanceState.getInt("kInt")
    val myBoolean = savedInstanceState.getBoolean("kBool")
    val myDouble = savedInstanceState.getDouble("kDouble")
    val myString = savedInstanceState.getString("kString")
    // use variables here
}

لدي فكرة أفضل.سيكون هذا أفضل لحفظ بياناتك دون الاتصال بـ onCreate مرة أخرى.يمكنك تعطيله من النشاط عندما يتغير الاتجاه.

في البيان الخاص بك:

<activity android:name=".MainActivity"
        android:configChanges="orientation|screenSize">

يحرر:لن يعمل هذا الحل عندما يقوم Android بإنهاء العملية بسبب انخفاض الذاكرة.إذا كنت تريد الاحتفاظ بالبيانات بشكل مؤكد، فأنت بحاجة إلى استخدام SavedInstanceState في هذه الحالة، وإلا أقترح استخدام هذه الطريقة.رائع وسهل الاستخدام!

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top