آموزش LiveData در اندروید به زبان ساده با کاتلین

سلام رفقا . من تصمیم گرفتنم بخاطر اینکه منابع فارسی برای آموزش LiveData خیلی کمه ، این مقاله رو که بنظرم خیلی خوب و آسونه رو به فارسی ترجمه کنم :) اگه آدرس گیت هاب یا توییتر و .. نویسنده رو میخواید به همونجا مراجعه کنید . پس بزن بریم . این LiveData اصلا چی هست ؟؟live dataخب LiveData در واقع یه Class هست که یه اطلاعات قابل مشاهده ای (که باید بهش بگین Observable) رو نگه میداره . برخلاف Observable های عادی ، LiveData از Lifecycle آگاه هست ، این یعنی به چرخه حیات بقیه کامپوننت ها مثل اکتیویتی ها ، فرگمنت ها یا سرویس ها احترام میزاره . این ویژگی تضمین میکنه که LiveData فقط اون Observer (ناظر) هایی از اپ رو بروزرسانی میکنه که در چرخه حیات حالت فعال دارنبریم یه نگاهی بندازیم و ببینیم چی شد که LiveData به وجود اومد ؟وقتی که اندروید برای اولین بار معرفی شد ، همه اومدن کل کد هاشون رو توی یه اکتیویتی نوشتن .کل کدها درون یک اکتیویتی به هر حال این خوب نیست که کل کد ها رو توی یه اکتیویتی بنویسیم ، همچنین اکتیویتی های اندروید برای Unit Testing هم خیلی سخت هستن .بعد این ماجرا ، همه با مدل های معماری مختلف پا به میدون گذاشتن مثل MVP , MVC , MVVM و بقیه . منظورم از این معماری ها جایی هستن که قسمت منطقی برنامه توسط قسمت های جداگانه ای مثل Presenter , ViewModel , Controller و ... انجام میشن .اکتیویتی کل چرخه حیات رو میدونه و اونو به Presenter یا ViewModel میگه این روش بهتریه بخاطر اینکه قسمت منطقی رو از View جدا میکنه ولی خب این روش هم مشکلات خودشو داره . Presenter , ViewModel , Controller و غیره از چرخه حیات اکتیویتی آگاه نیستن . بخاطر همین نیاز دارن که اکتیویتی چرخه حیات رو بهشون بگه .پس گوگل شروع کرد به فکر کردن که نذاره هر کی هر کاری دوست داره بکنه ، پس Architecture Components رو معرفی کرد .کامپوننت هایی مثل ViewModel یه قابلیت ویژه دارن . اونا میتونن که از چرخه حیات اکتیویتی آگاه باشن و دیگه لازم نیست که اکتیویتی اینو بهشون بگه .منظورم اینه که این برای ارتباط برقرار کردن اطلاعاتیه که از ViewModel به اکتیویتی میرن . این اطلاعات در واقع این قابلیت رو دارن که از چرخه حیات اکتیویتی مطلع باشن . بخاطر همین بهش میگن LiveData ، اطلاعاتی که از چرخه حیات ناظر هاشون ( مثلا اکتیویتی ) آگاه هستن .شکل کامل تر ...برای ترسیم بهتر ، LiveData رو وسط نقشه میزاریم .ترسیم مکان و نقش LiveData توی این نقشه شما میتونین ببینید که LiveData میتونه توسط ViewModel ها تغییر پیدا کنه ( یا هر جایی که برای تغییر LiveData استفاده میکنین )این نکته رو در نظر بگیرید که بعد از تغییر ، LiveData دوست داره که Observer هاش ( مثلا اکتیویتی ، فرگمنت ، سرویس و ... ) رو مطلع بکنه . به هر حال LiveData چشم بسته همه رو مطلع نمیکنه ولی بجاش اول وضعیت اونا رو بررسی میکنه .اگه Observer فعال باشه بعدش میتونه اطلاعاتی که داخل LiveData تغییر داده شدن رو اطلاع بده . به هر حال اگه Observer درحالت Paused یا Destroyed باشه ، اون وقت مطلع نمیکنه .این خیلی خوبه چون لازم نیس ما درباره و onDestroy نگران باشیم :)علاوه بر این وقتی Observer میره توی حالت Resume ، یهویی از اخرین اطلاعات LiveData مطلع میشه .انواع LiveDataدرواقع LiveData یک abstract class هستش بخاطر همین نمیشه از خودش استفاده کرد ولی خوشبختانه گوگل چند تا کلاس implement کرده که میتونیم از اونا استفاده کنیم . ا MutableLiveDataاین یکی از ساده ترین LiveData ها هستش که فقط اطلاعات اپدیت شده رو دریافت میکنه و به Observer هایی که داره اطلاع میده . به همین راحتی میتونین MutableLiveData رو تعریف کرد //c - تعریف کردن val liveDataA = MutableLiveData<String>() // تغییر دادن مقدار liveDataA.value = someValue // میتونید از این هم استفاده کنید liveDataA.postValue(value) // انجام بشه UI tread تا دربرای Observe کردن اطلاعات هم کار ساده ای رو باید انجام بدیم . من یه فرگمنت دارم که liveDataA رو به صورت زیر Observe میکنه class MutableLiveDataFragment : Fragment() { privateval changeObserver = Observer<String> { value -> value?.let { txt_fragment.text = it } } override fun onAttach(context: Context?) { super.onAttach(context) getLiveDataA().observe(this, changeObserver) } // بقیه ی کد های فرگمنت ... }نتیجه به شکل زیره . وقتی که اطلاعات مثلا 7567 و 6269 در liveDataA ذخیره میشن ، توسط فرگمنت تشخیص داده میشن داخل کد ، شما میتونید این قسمت رو ببینید getLiveDataA().observe(this, changeObserver)ولی هیچ کدی وجود نداره که وقتی فرگمنت pause یا terminate میشه ، LiveData اونو بیخیال بشه . حتی بدون انجام اینکار ، هیچ مشکلی برای ما پیش نمیاد :)گیف زیر رو نگاه کنید ، حتی وقتی که فرگمنت حذف میشه ، مقدار liveDataA یعنی 1428 هیچ خطایی بخاطر قرار دادن یک مقدار توی یه فرگمنت غیرفعال نمیده . این نکته رو هم در نظر داشته باشید که وقتی فرگمنت فعال میشه ، اخرین اطلاعات رو از liveDataA دریافت میکنه . ا Transformations.Mapفرض کنید که شما یه اطلاعاتی رو از یک Repository دریافت میکنید و قبل از اینکه اطلاعات رو به View بفرستید ، میخواید که اونو تغییر بدید . ما هنوزم میتونیم از LiveData برای انتقال اطلاعات بین موجودیت های مختلف استفاده کنیم . انتقال اطلاعات از repository به viewModel و بعدش به view توسط LiveDataما میتونیم اطلاعات رو از یک LiveData به یک LiveData ی دیگه بفرستیم ، اونم با استفاده از فانکشن ()transformations.mapclass TransformationMapFragment : Fragment() { privateval changeObserver = Observer<String> { value -> value?.let { txt_fragment.text = it } } override fun onAttach(context: Context?) { super.onAttach(context) val transformedLiveData = Transformations.map( getLiveDataA()) { &quotA:$it&quot } transformedLiveData.observe(this, changeObserver) } // بقیه ی کد های فرگمنت ... }و نتیجه میشه گیف زیر . اطلاعات که در اینجا 5116 هستش تبدیل میشه به A:5116 و در فرگمنت نمایش داده میشه . یکی از مزایای استفاده از ()transformations.map اینه که مطمئن میشیم که وقتی مقصد ( مثلا ViewModel و View ) غیرفعال هستش ، LiveData اطلاعات رو منتقل نمیکنه . بهتره یه نگاهی به کد های خود ()transformations.map بندازیم و ببینیم که داره چیکار میکنه ...@MainThread public static <X, Y> LiveData<Y> map(@NonNull LiveData<X> source, @NonNull final Function<X, Y> func) { final MediatorLiveData<Y> result = new MediatorLiveData<>(); result.addSource(source, new Observer<X>() { @Override public void d(@Nullable X x) { result.setValue(func.apply(x)); } }); return result; }عه ! توی این کد ها میبینیم که از یه نوع LiveData دیگه استفاده شده که اسمش MediatorLiveData هست . بریم ببینیم این چطوریه ...ا MediatorLiveDataاگه کد های بالا رو بررسی کنید ، میفهمید که جذاب ترین قسمت MediatorLiveData ، قابلیت اضافه کردن منبع اطلاعات به اون و کدی که محتوای اطلاعات رو تغییر میده هستش .این به این معنی هستش که ما میتونیم چند LiveData به یک مقصد از طریق MediatorLiveData رو داشته باشیم . دو LiveData که از طریق MediatorLiveData به یک مقصد میرنما میتونیم به صورت مستقیم از MediatorLiveData به شکل زیر استفاده کنیم . class MediatorLiveDataFragment : Fragment() { privateval changeObserver = Observer<String> { value -> value?.let { txt_fragment.text = it } } override fun onAttach(context: Context?) { super.onAttach(context) val mediatorLiveData = MediatorLiveData<String>() mediatorLiveData.addSource(getliveDataA()) { mediatorLiveData.value = &quotA:$it&quot } mediatorLiveData.addSource(getliveDataB()) { mediatorLiveData.value = &quotB:$it&quot } mediatorLiveData.observe(this, changeObserver) } // بقیه ی کد های فرگمنت ... }با استفاده از کدهای بالا ، ما نتیجه زیر رو داریم . فرگمنت میتونه اطلاعات تغییر داده شده ی liveDataA و liveDataB دریافت کنه . اینجا یه نکته وجود داره . وقتی فرگمنت فعال نیس و اطلاعات liveDataA و liveDataB تغییر پیدا میکنه ، وقتی فرگمنت فعال میشه ، MediatorLiveData اطلاعات اون LiveData ای رو میگیره که اخرین بار اون اضافه شده باشه ، که توی گیف زیر liveDataB هستش .همونطور که دیدید ، فرگمنتی که دوباره فعال شده همش داره اطلاعات liveDataB رو میگیره ، بی توجه به اینکه کدوم LiveData اخرین بار اطلاعاتش تغییر کرده . این بخاطر اینه که توی کد میتونید ببینید که liveDataB اخرین source ای هستش که به MediatorLiveData اضافه شده . ا Transformations.SwitchMapاینکه ما این قابلیت رو داشته باشیم که اطلاعات دو تا منبع رو داشته باشیم خیلی خوبه ولی اگه بخوایم فقط یکی رو کنترل کنیم و اگه نیاز بود بین اون ها switch کنیم باید چیکار کنیم ؟ آیا باید یه کد منطقی براش بنویسیم ؟ :( ما میتونیم این کارو انجام بدیم بدون اینکه نیاز به منطق باشه D: گوگل یه فانکشن خیلی باحال دیگه ارائه کرده به اسم ()Transformations.switchMap . @MainThread public static <X, Y> LiveData<Y> switchMap(@NonNull LiveData<X> trigger, @NonNull final Function<X, LiveData<Y>> func) { final MediatorLiveData<Y> result = new MediatorLiveData<>(); result.addSource(trigger, new Observer<X>() { LiveData<Y> mSource; @Override public void d(@Nullable X x) { LiveData<Y> newLiveData = func.apply(x); if (mSource == newLiveData) { return; } if (mSource != null) { result.removeSource(mSource); } mSource = newLiveData; if (mSource != null) { result.addSource(mSource, new Observer<Y>() { @Override public void d(@Nullable Y y) { result.setValue(y); } }); } } }); return result; }این فانکشن یه source رو اضافه میکنه و source قبلی رو حذف میکنه . پس ما همیشه یه دونه source برای MediatorLiveData داریم . پس دیاگرام ما این شکلی میشه :و کدش هم این مدلی میشه : class TransformationSwitchMapFragment : Fragment() { privateval changeObserver = Observer<String> { value -> value?.let { txt_fragment.text = it } } override fun onAttach(context: Context?) { super.onAttach(context) val transformSwitchedLiveData = Transformations.switchMap(getLiveDataSwitch()) { switchToB -> if (switchToB) { getLiveDataB() } else { getLiveDataA() } } transformSwitchedLiveData.observe(this, changeObserver) } // بقیه ی کد های فرگمنت ... }با استفاده از این ما به راحتی میتونیم کنترل کنیم که کدوم اطلاعات در View نمایش داده بشه . این خیلی برای برنامه هایی بدرد میخوره که اطلاعاتی رو از منابع مختلف دریافت میکنن و توسط یه تنظیم معینی کنترل میشن ( مثل قسمت login کردن کاربر ) . منابع نویسنده شما میتونین سورس کد ها رو از این گیت هاب ببینید. https://github.com/elye/demo_android_livedata_illustration اجراش کنید و باهاش بازی کنید تا بهتر بفهمیدش و درک کنید :) دمتون گرم که اینو خوندید و امیداورم یادگرفته باشید . اگه خوشتون اومد لایک یادتون نره و همچنین میتونید با استفاده از این لینک برام یه قهوه بخرید :) ❤️ بدرود .