داخل پروژه ای که الان دارم روش کار میکنم به یه مشکل مربوط به ذخیره و بازیابی وضعیت View در CustomView های که خودمون ایجاد کرده بودیم برخورد کردم. من راه حل های مختلف را داخل یه پروژه جداگانه آزمایش کردم تا به این راه حل رسیدم.در ابتدا بیاید سعی کنیم به این سوال پاسخ بدیم.
چرا باید وضعیت View رو ذخیره کنیم؟
موقعیتی رو تصور کنید که یه پرسشنامه طولانی رو داخل یه اپلیکیشن دارید پر میکنید. تو یه لحظه، شما به طور تصادفی صفحه گوشی رو می چرخونید یا لازم دارید موردی رو داخل برنامه دیگه ای بررسی کنید و پس از بازگشت به پرسشنامه، معلوم میشه که همه قسمت ها خالی شدن!!! چنین وضعیت بدی به طور قابل توجه ای کاربرو از استفاده از برنامه منصرف می کنه.
برای مثال، با من همراه باشید تا یه CustomView ایجاد کنیم
کد مربوط به فایل 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:orientation="horizontal"> <androidx.appcompat.widget.SwitchCompat android:layout_width="wrap_content" android:layout_height="wrap_content" /> <EditText android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" /> </LinearLayout>
کد مربوط به کلاس :
class CustomSwitchViewNoId: FrameLayout { // constructors init { LayoutInflater.from(context).inflate(R.layout.view_custom_switch_no_id, this) } }
به جای اینکه هر بار صفحه رو بچرخونیم تا نتیجه رو ببینیم، میتونیم گزینه زیر رو تو تنظیمات Developer options گوشی فعال کنیم
(Settings -> Developer options -> Don’t keep activities -> این گزینه رو روشن کنیم)
حالا وقشه پروژه رو اجاره میکنیم و دکمه home گوشی رو بزنیم تا عملیات ریستو داشته باشیم.
همانطور که می بینید با ریست شدن اکتیویتی یا فرگمنت وضعیت View ذخیره نمیشه و ما متن و حالت انتخاب شده SwitchCompat رو از دست میدیم.
خوب مشکل اصلی همین جاست !
اجازه بدید سلسله مراتبی رو که توسط اندروید برای ذخیره و بازیابی وضعیتView فراخوانی میشه رو بررسی کنیم:
Save state
Restore state
پیاده سازی State Saving
حالا میرسیم به اصل موضوع. در قدم اول فرآیند ذخیره و بازیابی ویژگی ها در یک View :
برای انجام این کار، باید دو متد onSaveInstanceState و onRestoreInstanceState را Override کنیم. onSaveInstanceState یک parcelable را برمیگردونه که حالت ذخیره شده view ماست و به onRestoreInstanceState یک parcelable ارسال میشه که می تونیم وضعیت ذخیره شده view رو از اون بازیابی کنیم.
در onSaveInstanceState، باید super.onSaveInstanceState را فراخوانی کنیم تا به کلاس super اجازه بدیم ذخیره وضعیت رو داشته باشه و parcelable ای که نشون دهنده وضعیت view هستش رو برگردونه. ما نمیتونیم مقداری به parcelable برای کلاس super اضافه کنیم، بنابراین به راهی برای کپسولهسازی وضعیت ذخیرهشده کلاس super، و همچنین ذخیره وضعیت CustomView نیاز داریم. اندروید کلاس View.BaseSavedState رو ارائه می ده که می تونیم برای این منظور گسترشش بدیم.
میشه بیشتر هم توضیح داد ولی به نظرم تا همین جا هم کافی باشه !!!
خوب بریم تو کد :
class SavedState(superState: Parcelable?) : BaseSavedState(superState) {
val childrenStates by lazy {
SparseArray<Parcelable>()
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeSparseArray(childrenStates)
}
}
در مورد SparseArray داخل یه پست دیگه توضیح میدم فعلا فقط اینو بدونید که جایگزینی برای Map هستش و ما وضعیت View ها رو داخل متد writeToParcel ای که override کردیم داخل آرایه ای از جنس SparceArray ذخیره میکنیم.
public override fun onSaveInstanceState(): Parcelable? {
val saveState = SavedState(super.onSaveInstanceState())
for (i in 0 until childCount) {
getChildAt(i).saveHierarchyState(saveState.childrenStates)
}
return saveState
}
public override fun onRestoreInstanceState(state: Parcelable) {
when (state) {
is SavedState -> {
super.onRestoreInstanceState(state.superState)
dispatchSaveInstanceState(state.childrenStates)
}
else -> super.onRestoreInstanceState(state)
}
}
در مورد متدهای onSaveInstanceState و onRestoreInstanceState و نحوه ی عملکردشون چند خط بالاتر توضیح دادم.
نکته : با وجود تمام کدهایی که ایجاد کردیم هنوز هم عملیات ذخیره و بازیابی وضعیت View انجام نمیشه !!!
اگر برای View آی دی ست نکرده باشیم، اندروید برای ذخیره وضعیت view هیچ تلاشی نمیکنه پس حتما به این نکته باید توجه داشت که برای هر view ای در CustomView برای ذخیره وضعیت نیاز به تعریف id داریم.
<?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:orientation="horizontal"> <androidx.appcompat.widget.SwitchCompat android:id="@+id/customViewSwitchCompat" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <EditText android:id="@+id/customViewEditText" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" /> </LinearLayout>
خروجی کار به صورت زیر میشه :
تا به این جای کار میتونیم وضعیت view رو حفظ و بازیابی کنیم اما بازهم یه مشکل وجود داره!
همانطور که می بینید، تگ های id/customViewSwitch+@ و id/customViewEditText+@ به تعداد View های ما تکرار می شن، بنابراین SparseArray تولید شده فرزندان id/switch1+@، سپسid/switch2+@ و در نهایت id/switch3+@ را ذخیره می کنه.
رفتار SparseArray برای ذخیره دیتا به صورت زیر هستش (حتما پست مربوط به SparseArray رو بخونید)
val array = SparseArray() array.put(1, "test1") array.put(1, "test2") array.put(1, "test3") Log.i("TAG", array.toString())
نتیجه به صورت زیر هستش :
I/TAG: {1=test3}
بنابراین مقادیر جدید مقادیر قبلی رو override می کنن. ،تصویر زیر کامل میتونه نحوه ی ذخیره سازی view هارو نشون بده که چرا هر یک از viewها حالت آخرین فرزندو دریافت میکنن.
با این حال، چگونه می تونیم جلوی این اتفاقو بگیریم؟ راه حل اینجاست !
در ابتدا، ما باید دو callback ( dispatchSaveInstanceState و dispatchRestoreInstanceState) رو override کنیم:
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>){ dispatchFreezeSelfOnly(container) }
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) { dispatchThawSelfOnly(container) }
به این ترتیب()super.onSaveInstanceState تنها حالت super رو برمیگردونه و از مشاهده فرزندان اجتناب میکنه. ما اکنون آماده هستیم تا وضعیت را در داخل onSaveInstanceState و onRestoreInstanceState مدیریت کنیم. حالا وقتشه با فرآیند ذخیره وضعیت شروع کنیم:
بیایید بررسی کنیم که الان سلسله مراتب ذخیره سازی داخل SparceArray به چه شکلی شده:
و در نهایت خروجی کار میشه :
وظیفه متد ()saveHierarchyState
ریشه (root) اپلیکیشن اندرویدی یه پنجره به نام PhoneWindow هستش ، که این پنجره حاوی یه ریشه view برای تمام رابط کاربری خودشه که بهش mContentParent می گن. در زمان مناسب، اندروید به پنجره PhoneWindow دستور میده تا ()saveHierarchyState که به معنای ذخیره وضعیت رابط کاربری هست اجرا بشه ، سپس متدی با همین نام در ریشه view خودش فراخوانی می کنه. از این نقطه به بعد، ما در فرآیند ذخیره سازی وضعیت view هستیم ،ما نمی دونیم چه نوع view ای در ریشه پنجره PhoneWindow قرار داره، اما اجازه بدید برای لحظه ای فرض کنیم که فقط یه view اولیه داریم. در این صورت، زمانی که ()saveHierarchyState فراخوانی می شه، بلافاصله ()dispatchSaveInstanceState رو داخل خودش فراخوانی می کنه.سپس ()dispatchSaveInstanceState دو کار جالب انجام می ده:
اول، ()onSaveInstanceState را داخل خودش فراخوانی می کنه .
دوم: parcelable ای که از()onSaveInstanceState گرفته رو داخل یه SparseAarray به عنوان کانتینر قرار میده که توسط PhoneWindow در اختیارش قرار گرفته است.سپس PhoneWindow آرایه SparseArray برگشتی رو می گیره و در یک Bundle قرار میده، و در نهایت PhoneWindow اون Bundle را به سیستم اندروید برمی گردونه.
خیلی خوشحال میشم در صورتی که این نوشته رو دوست داشتید ❤️ کنید یا نظرتون را از طریق کامنت برام بنویسید.
سورس مربوط به همین مقاله رو داخل گیت هابم اضافه کردم میتونید ازش استفاده کنید و اگه مشکلی داشت کمک کنید باهم درستش کنیم .(روی متن کلیک کنید)