Parcelable و Serializable در اندروید
چطور میشه که بعضی اپلیکیشنهای اندروید سریع و بینقص کار میکنند؟ تو این مقاله، با تفاوت Parcelable و Serializable آشنا میشیم؛ دو تکنیکی که میتونن اپلیکیشن شما رو به یه سطح دیگه ببرن.
معرفی Parcelable و Serializable
سلام دوستان! امروز میخوایم در مورد دو تا از مهمترین روشهای سریالیزه کردن دادهها در اندروید صحبت کنیم: Parcelable و Serializable. این دو تا مفهوم خیلی مهم و کاربردی هستند، خصوصاً وقتی که میخوایم دادهها رو بین اکتیویتیها یا فرگمنتها منتقل کنیم. حالا بیایید یکم بیشتر باهاشون آشنا بشیم.
Parcelable چیست؟
خب، اول بریم سراغ Parcelable. شاید شنیده باشین که میگن Parcelable خیلی سریعتر و بهینهتر از Serializable هست. راستش رو بخواین، کاملاً درسته! Parcelable یه اینترفیسه که توسط اندروید ارائه شده (نه جاوا یا کاتلین!) و مخصوص کار با دادهها توی محیط اندروید طراحی شده. وقتی که یه کلاس رو Parcelable میکنیم، به اندروید میگیم که چطور این کلاس رو به یکسری بایت تبدیل کنه و بعداً دوباره از اون بایتها به شی اصلی برگردونه.
بیایید با یه مثال ساده شروع کنیم. فرض کنید ما یک سگ داریم و میخوایم اون رو به ذرات اتمیش تجزیه کنیم تا بتونیم تلهپورتش کنیم. برای این کار، باید دقیقاً بدونیم چطور این سگ رو تجزیه کنیم و بعداً دوباره سرهمش کنیم. Parcelable همین کار رو با دادههای ما انجام میده.
فرض کنید یه کلاس داریم به اسم Dog که اطلاعات سگ رو نگه میداره:
import android.os.Parcel import android.os.Parcelable data class Dog(val name: String, val age: Int) : Parcelable { // کانستراکتور که اطلاعات رو از Parcel میخونه constructor(parcel: Parcel) : this( parcel.readString() ?: "", parcel.readInt() ) // این تابع مقادیر کلاس رو داخل Parcel مینویسه override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeString(name) parcel.writeInt(age) } // این تابع محتویات ویژه این Parcelable رو توضیح میده (معمولاً 0 برمیگردونه) override fun describeContents(): Int { return 0 } // این شیء کمک میکنه تا Dog دوباره از Parcel خونده بشه companion object CREATOR : Parcelable.Creator<Dog> { override fun createFromParcel(parcel: Parcel): Dog { return Dog(parcel) } override fun newArray(size: Int): Array<Dog?> { return arrayOfNulls(size) } } }
بیایید خط به خط این کد رو توضیح بدم بهتون:
- تعریف کلاس و ارثبری از Parcelable:
- کلاس Dog از Parcelable ارثبری میکنه. این یعنی ما داریم اینترفیس Parcelable رو پیادهسازی میکنیم.
- کانستراکتور Parcel:
- این کانستراکتور جدید اطلاعات رو از شی Parcel میگیره و مقادیر رو به متغیرهای کلاس اختصاص میده. parsel.readString و parsel.readInt دادهها رو از Parcel میخونن.
- تابع writeToParcel:
- این تابع برای نوشتن دادههای کلاس به داخل Parcel استفاده میشه. parcel.writeString و parcel.writeInt دادهها رو داخل Parcel مینویسن.
- تابع describeContents:
- این تابع معمولاً 0 برمیگردونه و برای موارد خاص استفاده میشه که نیاز به توضیحات اضافی داره و در ادامه مقاله بهش اشاره میکنم.
- CREATOR:
- این شیء یک پیادهسازی از Parcelable.Creator است که به اندروید کمک میکنه تا کلاس Dog رو از Parcel بازسازی کنه.
- createFromParcel یک شیء Dog رو از Parcel میسازه.
- newArray یک آرایه از شیء Dog میسازه.
- در ادامه مقاله توضیح کامل تری از CREATOR آوردهام.
حالا با این توضیحات، ما میتونیم از کلاس Dog در انتقال دادهها بین اکتیویتیها استفاده کنیم. مثلاً:
val dog = Dog("Rex", 5) val intent = Intent(this, AnotherActivity::class.java) intent.putExtra("dog", dog) startActivity(intent)
و در اکتیویتی مقصد:
val dog = intent.getParcelableExtra<Dog>("dog")
به همین سادگی! حالا میتونید داده های خودتون رو بهینه و سریع بین بخشهای مختلف برنامه انتقال بدید.
خب، بذارید بیشتر درمورد دوتا از مهم ترین قسمتها یعنی Creator و describeContnts توضیح بدم.
Creator چیست؟
همونطور که گفتیم، Parcelable یه اینترفیسه که توسط اندروید ارائه شده و به ما کمک میکنه دادهها رو بهینهتر منتقل کنیم. یکی از مهمترین قسمتهای Parcelable، شیء Creator هست که نقش اساسی توی بازسازی دادهها داره.
وقتی که یه کلاس رو Parcelable میکنیم، باید حتماً یه شیء از نوع Parcelable.Creator رو هم پیادهسازی کنیم. این Creator دو تا تابع مهم داره: createFromParcel و newArray. بیایید با مثال Dog بریم سراغ توضیحات دقیقتر.
فرض کنید ما یه کلاس Dog داریم که اطلاعات یه سگ مثل نام و سن رو نگه میداره. توی این کلاس، Parcelable.Creator به این صورت تعریف شده:
companion object CREATOR : Parcelable.Creator < Dog > { override fun createFromParcel(parcel: Parcel) : Dog { return Dog(parcel) } override fun newArray(size: Int) : Array < Dog ? > { return arrayOfNulls(size) } }
تابع createFromParcel مسئول اینه که یه شیء جدید از کلاس ما بسازه و دادهها رو از Parcel بخونه. فرض کنید میخوایم سگمون رو تلهپورت کنیم و اون رو به ذرات اتمیش تبدیل کردیم. حالا با استفاده از createFromParcel میتونیم دوباره این سگ رو از حالت ذرات به حالت اصلی برگردونیم. این تابع به این صورت عمل میکنه:
override fun createFromParcel(parcel: Parcel): Dog { return Dog(parcel) }
توی اینجا، ما کانستراکتور Dog که یه Parcel میگیره رو فراخوانی میکنیم و دادهها رو از Parcel میخونیم. مثلا:
constructor(parcel: Parcel) : this( parcel.readString() ?: "", parcel.readInt() )
با استفاده از این کد، دادههای نام و سن از Parcel (همون پارامتری که در متد create from parcel میگیریم ) خونده میشن و به متغیرهای name و age اختصاص داده میشن.
تابع newArray که در Parcelable.Creator پیادهسازی میکنیم، وظیفهاش ایجاد آرایهای از اشیاء کلاس ماست. این تابع زمانی استفاده میشه که اندروید نیاز داشته باشه تا آرایهای از اشیاء پارسلیبل (مثل کلاس Dog) رو مدیریت کنه. اما سوال اینه که کاربرد این آرایه چیه و چرا بهش نیاز داریم؟
فرض کنید ما میخوایم مجموعهای از اشیاء Dog رو بین دو اکتیویتی یا فرگمنت منتقل کنیم. در اینجا newArray به کمک ما میاد تا بتونیم یه آرایه از اشیاء Dog بسازیم و اون رو به صورت Parcelable منتقل کنیم.
// تعریف آرایهای از اشیاء Dog val dogs: Array<Dog> = arrayOf( Dog("Rex", 5), Dog("Bella", 3), Dog("Max", 7) ) // قرار دادن آرایه در Intent val intent = Intent(this, AnotherActivity::class.java) intent.putParcelableArrayListExtra("dogs", dogs.toList() as ArrayList<Dog>) startActivity(intent)
و در اکتیویتی مقصد:
val dogs = intent.getParcelableArrayListExtra<Dog>("dogs")
اینجاست که newArray به کار میاد. اندروید از این تابع استفاده میکنه تا یه آرایه جدید از کلاس ما بسازه و دادهها رو داخلش قرار بده. این تابع یه آرایه از اشیاء Dog با اندازه مشخصی (size) میسازه. این اندازه به تعداد اشیائی که میخوایم منتقل کنیم بستگی داره. در واقع، newArray به اندروید کمک میکنه تا فضای لازم برای نگهداری اشیاء ما رو در نظر بگیره.
describeContents چیست؟
تابع describeContents برای توصیف محتویات Parcelable استفاده میشه. بیشتر وقتها این تابع 0 برمیگردونه، ولی اگه بخوایم از انواع خاصی از اشیاء مثل فایلها یا descriptorها استفاده کنیم، میتونیم مقادیر دیگهای برگردونیم.
override fun describeContents(): Int { return 0 }
این تابع به سیستم کمک میکنه تا نوع محتویاتی که داریم رو بهتر بفهمه. ولی بیایید با یه مثال ساده این قضیه رو روشن کنیم.
مثال عملی از describeContents
بیایید یه مثال بزنم که توش تابع describeContents عددی غیر از 0 برگردونه. فرض کنین ما یک کلاس به نام FileDescriptor داریم که شامل یک فایل است و نیاز داریم تا این فایل رو موقع Parcelable کردن، به درستی توصیف کنیم. تو این حالت میتونیم از describeContents استفاده کنیم تا مشخص کنیم که این Parcelable شامل یک فایل است.
import android.os.Parcel import android.os.Parcelable import android.os.ParcelFileDescriptor data class FileDescriptor(val file: ParcelFileDescriptor) : Parcelable { constructor(parcel: Parcel) : this( parcel.readParcelable(ParcelFileDescriptor::class.java.classLoader)!! ) override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeParcelable(file, flags) } override fun describeContents(): Int { // در اینجا 1 برمیگردونیم تا نشان دهیم که این Parcelable شامل یک فایل است return Parcelable.CONTENTS_FILE_DESCRIPTOR } companion object CREATOR : Parcelable.Creator<FileDescriptor> { override fun createFromParcel(parcel: Parcel): FileDescriptor { return FileDescriptor(parcel) } override fun newArray(size: Int): Array<FileDescriptor?> { return arrayOfNulls(size) } } }
اینجا، کلاس FileDescriptor شامل یک ParcelFileDescriptor هست که میتونه نمایانگر یه فایل باز شده باشه. وقتی که ما Parcelable رو پیادهسازی میکنیم، تابع describeContents به جای 0، مقدار Parcelable.CONTENTS_FILE_DESCRIPTOR رو برمیگردونه. این مقدار به سیستم اندروید میگه که این Parcelable شامل یک فایل هست و باید به صورت خاصی با اون برخورد بشه.
Serializable چیست؟
حالا بریم سراغ Serializable. Serializable یه اینترفیس از جاواست و خیلی راحتتر میتونیم ازش استفاده کنیم. فقط کافیه که کلاس مورد نظرمون Serializable باشه. به مثال زیر توجه کنید:
import java.io.Serializable data class Dog(val name: String, val age: Int) : Serializable
همین! دیگه نیاز نیست کار خاصی بکنیم. حالا میتونیم این کلاس رو به راحتی بین اکتیویتیها منتقل کنیم، دقیقاً مثل Parcelable:
val dog = Dog("Rex", 5) val intent = Intent(this, AnotherActivity::class.java) intent.putExtra("dog", dog) startActivity(intent)
و توی AnotherActivity میتونیم اون رو دریافت کنیم:
val dog = intent.getSerializableExtra("dog") as Dog
خب، تا اینجا دیدیم که استفاده از Serializable خیلی راحتتر و سریعتره. اما چرا میگن Parcelable بهتره؟ دلیلش اینه که Serializable از روش reflection استفاده میکنه (در ادامه بیشتر توضیحش دادم ) که نسبت به Parcelable خیلی کندتره و همچنین حافظه بیشتری مصرف میکنه.
تفاوت Parcelable و Serializable در عملکرد و کارایی
حالا بیایید یه نگاهی به تفاوتهای اصلی بین Parcelable و Serializable بندازیم:
- سرعت: Parcelable خیلی سریعتر از Serializable هست. چون Parcelable مستقیماً با بایتها کار میکنه و نیاز به reflection نداره، در حالی که Serializable از reflection استفاده میکنه که خیلی کندتره.
- مصرف حافظه: Parcelable کمتر حافظه مصرف میکنه. چون توی Parcelable دقیقاً میدونیم که چه دادههایی رو داریم منتقل میکنیم، میتونیم بهینهتر این کار رو انجام بدیم.
- پیچیدگی کد: Serializable خیلی راحتتر استفاده میشه. فقط کافیه که کلاس مورد نظرمون Serializable باشه، اما برای Parcelable باید یه سری توابع اضافی بنویسیم.
- کاربرد: Parcelable بیشتر توی اندروید استفاده میشه، چون برای این محیط بهینهسازی شده. در حالی که Serializable یه روش عمومیتره و میتونیم توی هر جایی که جاوا استفاده میشه، ازش استفاده کنیم.
در نهایت، انتخاب بین Parcelable و Serializable به نیاز و شرایط پروژهتون بستگی داره. اگه سرعت و بهینه بودن حافظه براتون مهمه و مشکلی با نوشتن کد اضافی ندارید، Parcelable گزینه بهتریه. اما اگه سادگی و راحتی کدنویسی براتون اولویته، Serializable کارتون رو راه میاندازه.
Reflection در Serializable
در پشت صحنه، جاوا از Reflection استفاده میکنه تا این سریالیزهسازی به راحتی انجام بشه. وقتی یه شیء Serializable رو سریالیزه میکنید، جاوا از Reflection برای خوندن مقادیر فیلدها استفاده میکنه و اونا رو به صورت بایتاستریم ذخیره میکنه. بعداً، وقتی که میخواید شیء رو دوباره از بایتاستریم بخونید (دیسریالیزه کنید)، باز هم جاوا از Reflection استفاده میکنه تا فیلدها رو به درستی تنظیم کنه.
Reflection این امکان رو میده که بدون نیاز به دانستن نوع دقیق شیء، به فیلدها و متدهای اون دسترسی پیدا کنیم. این قابلیت توی Serializable خیلی مفیده چون:
- انعطافپذیری: با استفاده از Reflection، میتونیم کلاسهای مختلفی رو بدون نیاز به تغییر کد، سریالیزه کنیم. این یعنی اگه کلاس جدیدی اضافه کنیم یا فیلدهای جدیدی به کلاسهای موجود اضافه کنیم، نیازی به تغییر در منطق سریالیزه کردن نداریم.
- سادگی پیادهسازی: استفاده از Serializable خیلی سادهست. فقط کافیه که کلاسمون از Serializable ارثبری کنه و جاوا بقیه کارها رو با استفاده از Reflection انجام میده. این کار باعث میشه که سریالیزه کردن اشیاء پیچیده خیلی آسونتر بشه.
- کد کمتر: نیازی نیست برای هر فیلد کد دستی بنویسیم تا مقادیر رو بخونه یا بنویسه. جاوا خودش این کار رو با استفاده از Reflection انجام میده، که این باعث میشه کد ما کوتاهتر و خواناتر باشه.
کاربردهای Parcelable و Serializable در اندروید
حالا که با مفاهیم Parcelable و Serializable آشنا شدیم، بیایید ببینیم Parcelable و Serializable توی اندروید چه کاربردهایی دارن و کجاها میتونیم ازشون استفاده کنیم.
انتقال داده بین اکتیویتیها
یکی از رایجترین کاربردهای Parcelable و Serializable انتقال داده بین اکتیویتیهاست این مثال رو در کدهای قبلی بهش اشاره کردیم پس اینجا ازش زود رد میشیم.
ذخیرهسازی دادهها در Bundle
وقتی که وضعیت اکتیویتی یا فرگمنت رو ذخیره میکنیم، معمولاً از Bundle استفاده میکنیم. Parcelable و Serializable به ما کمک میکنن تا اشیاء پیچیده رو توی Bundle ذخیره کنیم و بعداً دوباره بازیابیشون کنیم.
override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putParcelable("user", user) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) user = savedInstanceState?.getParcelable("user") }
استفاده در فریمورکهای اندرویدی
خیلی از فریمورکها و کتابخونههای اندرویدی از Parcelable و Serializable پشتیبانی میکنن. مثلاً توی Room Database وقتی که میخوایم دادهها رو به صورت شیء ذخیره کنیم، میتونیم از Parcelable و Serializable استفاده کنیم. راستی اگه میخواید به طور کامل با دیتابیس روم و معماریهای استفاده از روم در اندروید آشنا بشید پیشنهاد می کنم دوره جامع اندروید ما رو ببینید.
انتقال دادهها در سرویسها
وقتی که با سرویسها کار میکنیم و میخوایم دادهها رو بین اکتیویتی و سرویس انتقال بدیم، میتونیم از Parcelable استفاده کنیم. سرویسها معمولاً نیاز به کارایی بالا دارن، بنابراین Parcelable گزینه بهتریه.
مزایا و معایب
هر دوی اینها روشهایی برای سریالیزه کردن دادهها هستن، اما هر کدوم مزایا و معایب خودشون رو دارن. Parcelable مخصوص اندروید طراحی شده و کارایی بالاتری داره، در حالی که Serializable یه روش عمومیتره و پیادهسازی آسونتری داره. بیایید یه نگاهی به مزایا و معایب Parcelable و Serializable بندازیم.
ویژگی | Parcelable | Serializable |
پیاده سازی | پیچیدهتر، نیازمند پیادهسازی کد دستی | سادهتر، فقط با پیادهسازی اینترفیس |
کارایی | بسیار سریعتر | کندتر |
مصرف حافظه | مصرف حافظه کمتر | مصرف حافظه بیشتر |
استفاده در اندروید | بهینهسازی شده برای اندروید | عمومیتر، قابل استفاده در جاوا |
انعطاف پذیری | کمتر، محدود به اندروید | بیشتر، قابل استفاده در محیطهای مختلف |
تبدیل به بایت | دستی، نیاز به کدگذاری | اتوماتیک توسط JVM |
سرعت اجرا | بالاتر | پایینتر |
مناسب برای | اپلیکیشنهای اندرویدی با نیاز به کارایی بالا | پروژههایی با نیاز به پیادهسازی آسانتر |
همونطور که توی جدول بالا میبینید، تفاوت Parcelable و Serializable توی عملکرد، مصرف حافظه و پیچیدگی پیادهسازی کاملاً مشخصه. برای پروژههای اندرویدی که نیاز به کارایی و سرعت بالا دارن، Parcelable گزینه بهتریه. اما اگه دنبال یه پیادهسازی آسونتر و انعطافپذیری بیشتری هستید، Serializable میتونه مناسبتر باشه. با توجه به این تفاوتها، انتخاب روش مناسب بستگی به نیازهای خاص پروژه شما داره. امیدوارم این مقایسه بهتون کمک کنه تا انتخاب بهتری داشته باشید. هر سوالی داشتید، خوشحال میشم که جواب بدم. موفق باشید!