مهاجرت از LiveData به Kotlin Flow
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 11 دقیقه

مهاجرت از LiveData به Kotlin Flow

Livedata چیزی بود که در سال 2017 ما به آن نیاز داشتیم. الگوی observer زندگی ما را آسان‌تر کرد، اما گزینه‌هایی مانند RxJava در آن زمان برای مبتدیان بسیار پیچیده بودند. تیم Architecture Component، livedata را ایجاد کردند: یک کلاس نگه‌دارنده داده قابل مشاهده بسیار متمایز، که برای اندروید طراحی شده است. استفاده از آن بسیار ساده بود و توصیه می‌شد از RxJava برای جریان‌های واکنشی پیچیده استفاده کنید، و از مزیت هر دو، همزمان استفاده کنید.

آیا LiveData مرده است؟

Livedata هنوز راه حل مناسب برای توسعه دهندگان جاوا، مبتدیان و شرایط ساده است. برای بقیه انتقال به Kotlin Flow گزینه خوبی است. flowها هنوز یک منحنی شیب‌دار دارند اما بخشی از زبان کاتلین است که توسط jetbrains پشتیبانی می‌شود، و همچنین compose در حال آمدن است که متناسب با مدل واکنشی است.

قبلاً در مورد استفاده از Flow برای اتصال قسمت‌های مختلف برنامه به جز view و ViewModel در این پست صحبت شده است. اکنون راه حل ایمن‌تری برای جمع‌ آوری جریان از رابط کاربری اندروید داریم، و می‌توانیم یک راهنمای کامل برای مهاجرت ایجاد کنیم.

در این پست خواهید آموخت که چگونه Flow را در معرض دید قرار دهید، چگونه آن‌ها را جمع آوری کنید و چگونه آن را متناسب با نیازهای خاص تنظیم کنید.

Flow کارهای ساده را سخت‌تر و کارهای پیچیده را ساده‌تر می‌کند

Livedata یک کار انجام می‌دهد و آن کار را بسیار خوب انجام می‌دهد: داده‌ها را در معرض دید قرار می‌دهد درحالی که آخرین مقدار را cache می‌کند و درک خوبی از چرخه زندگی اندروید دارد. بعداً فهمیدیم که می‌تواند کوروتین‌ها را شروع کند و تبدیلات پیچیده‌ای را نیز انجام دهد، اما این موراد آن را یکم بیشتر درگیر کرد.

بیایید برخی از الگوهای LiveData و معادل‌های Flow آن را نیز بررسی کنیم:

1: نتیجه یک عملیات one-shot را با نگه دارنده داده قابل تغییر نمایش دهید

این یک الگوی کلاسیک است، جایی که شما یک نگه دارنده حالت را با نتیجه یک کوروتین نمایش می‌دهید:

برای انجام همان کار با Flow‌ها از MutableStateFlow استفاده می‌کنیم:

StateFlow نوع خاصی از SharedFlow است(که خود آن نوع خاصی از Flow است)، که نزدیک‌ترین حالت به LiveData است:

  • همیشه یک مقدار دارد.
  • فقط یک مقدار دارد.
  • از چندید ناظر(Observer) پشتیبانی می‌کند(بنابراین flow مشترک است)
  • همیشه مستقل از تعداد ناظران فعال، آخرین مقدار اشتراک را برمی‌گرداند.

برای نمایش وضعیت UI، همیشه از StateFlow استفاده کنید. این یک ناظر ایمن و کارامد است که برای نگه داشتن حالت UI طراحی شده است.

2: نتیجه یک عملیات one-shot را نشان دهید

این مشابه بخش قبل است با این تفاوت که نمایش نتیجه فراخوانی کوروتین بدون خاصیت قابل تغییر بودن است.

با LiveData ما از سازنده livedata coroutine برای این کار استفاده کردیم:

از آنجا که دارندگان حالت فقط یک مقدار دارند، بهتر است حالت UI خود را در نوعی از کلاس Result قرار دهیم که از حالت‌هایی مانند Loading, Success و Error پشتیبانی می‌کند.

معادل Flow آن کمی بیشتر درگیر می‌شود، زیرا باید برای آن پیکربندی انجام دهید:

stateIn یک عملگر Flow است که یک Flow را به StateFlow تبدیل می‌کند. اکنون به این پارامترها اعتماد کنیم، زیرا بعداً برای توضیح آن به پیچیدگی بیشتری نیاز داریم.

3: بارگذاری دادهای one-shot با پارامتر

در نظر بگیرید شما می‌خواهید برخی از داده‌ها را که به شناسه کاربر بستگی دارد بارگیری کنید و این اطلاعات را از یک AuthManager دریافت می‌کنید که یک Flow را نشان می‌دهد:

با LiveData می‌توانید کاری مشابه این انجام دهید:

switchMap یک تبدیل است که بدنه آن اجرا می‌شود و نتیجه آن هنگام تغییر userId به اشتراک گذاشته می‌شود.

اگر دلیلی برای تبدیل userId به LiveData وجود ندارد، یک گزینه بهتر برای این کار ترکیب جریان‌ها با Flow و در نهایت تبدیل نتیجه نمایش داده شده به LiveData است.

انجام این کار با Flow نیز بسیار شببیه است:

توجه داشته باشید که اگر به انعطاف پذیری بیشتری نیاز دارید می‌توانید از transformationLatest نیز استفاده کنید و موارد را به طور صریح منتشر کنید:

4: مشاهده یک جریان داده با پارامتر

حالا بیاید مثال را واکنشی‌تر بکنیم. داده‌ه‌ها واکشی نمی‌شوند، اما مشاهده می‌شوند، بنابراین ما تغییرات منبع داده را به طور خودکار به UI منتقل می‌کنیم.

ادامه مثال: به جای فراخوانی fetchItemبر روی منبع داده از یک عملگر فرضی ObservItem استفاده می‌کنیم که یک Flow را برمی‌گرداند.

با LiveData می‌توانید flow را به livedata تبدیل کنید و همه بروزرسانی‌ها را emitSource کنید:

یا ترجیحاً هر دو flow را با استفاده از flatMapLatest ترکیب کنید و فقط خروجی را به LiveData تبدیل کنید:

پیاده سازی در flow مشابه است اما تبدیل به livedata ندارد:

هرگاه کاربر تغییر کند یا داده‌های کاربر در مخزن تغییر کند StateFlow بروز می‌شود.

5- ترکیب چندین منبع: MediatorLiveData -> Flow.combine

MediatorLiveData به شما امکان می‌دهد یک یا چند منبع بروزرسانی را مشاهده کنید(LiveData Observers) و هنگامی که آن‌ها داده جدید می‌گیرند کاری را انجام دهید. معمولاً مقدار MediatorLiveData را بروز می‌کنید.

معادل آن در Flow بسیار ساده‌تر است:

همچنین می‌توانید از تابع combileTransform یا zip نیز استفاده کنید.

پیکربندی StateFlow به نمایش گذاشته شده(اپراتور stateIn)

ما قبلاً از stateIn برای تبدیل flow عادی به StateFlow استفاده می‌کردیم، اما یک سری تنظیمات هم نیاز داشت. اگر نمی‌خواهید اکنون به جزئیات بپردازید فقط copy-paste کنید، که من هم این را توصیه می‌کنم.

با این حال اگر از آن پارامتر 5 ثانیه‌ای مطمئن نیستید، ادامه مطلب را بخوانید.

stateIn سه پارامتر دارد(براساس مستندات):

@param scope the coroutine scope in which sharing is started.
@param started the strategy that controls when sharing is started and stopped.
@param initialValue the initial value of the state flow.
This value is also used when the state flow is reset using the [SharingStarted.WhileSubscribed] strategy with the `replayExpirationMillis` parameter.

stateIn می‌تواند 3 مقدار داشته باشد:

  • Lazily: با ظاهر شدن اولین مشترک شروع کنید و با لغو scope متوقف شوید.
  • Eagerly: بلافاصله شروع کنید و هنگام لغو scope متوقف شوید.
  • WhileSubscribed: این مورد پیچیده است.

برای عملیات one-shot می‌توانید از Lazily یا Eagerly استفاده کنید. با این حال اگر Flowهای دیگری را مشاهده می‌کنید، باید از WhileSubscribed برای انجام بهینه‌ سازی‌های کوچک اما مهم استفاده کنید.

استراتژی WhileSubscribed

WhileSubscribed هنگامی که جمع کننده وجود ندارد upstream flow را لغو می‌کند. StateFlow با استفاده از stateIn ایجاد شده است، داده‌ها را در view در معرض دید قرار می‌دهد، اما همچنین flowهای لایه‌‌ها یا برنامه‌های دیگر را نیز مشاهده می‌کند(upstream). فعال نگه داشتن این flowها ممکن است منجر به هدر رفتن منابع شود، به عنوان مثال اگر آن‌ها به خواندن داده از منابع دیگر مانند اتصال پایگاه داده، حسگرهای سخت‌افزاری و غیره ادمه دهند.وقتی برنامه شما به پس‌زمینه می‌رود شما باید این کوروتین را متوقف کنید.

WhileSubscribed دو پارامتر می‌گیرد:

public fun WhileSubscribed(
    stopTimeoutMillis: Long = 0,
    replayExpirationMillis: Long = Long.MAX_VALUE
)

Stop timeout

طبق مستندات:

stopTimeoutMillies تاخیری(بر حسب میلی ثانیه) بین ناپدید شدن آخرین مشترک و توقف upstream flow(جریان بالایی) را تنظیم می‌کند. به طور پیش فرض صفر است(بلافاصله متوقف می‌شود).

این مورد وقتی مفید است که view برای کسری از ثانیه متوقف شود و نمی‌خواهید جریان‌های بالایی را لغو کنید. این اتفاق همیشه می‌افتد، به عنوان مثال هنگامی که کاربر دستگاه را می‌چرخاند و view از بین می‌رود و دوباره ایجاد می‌شود.

راه حل آن در livedata coroutine builder اضافه کردن 5 ثانیه تاخیر و درصورت عدم حضور مشترک(subscriber) آن را متوقف کنید. WhileSubscribed دقیقاً همین کار را انجام می‌دهد:

این روش همه این موارد را بررسی می‌کند:

  • هنگامی که کاربر برنامه شما را به پس زمینه منتقل کند، بروزرسانی‌های سایر لایه‌ها پس از 5 ثانیه متوقف می‌شود و باتری صرفه جویی می‌شود.
  • آخرین مقدار در cache ذخیره می‌شود تا وقتی کاربر به برنامه بازگشت، بلافاصله داده داشته باشد.
  • مشترک‌ها مجدداً راه اندازی می‌شوند و مقادیر جدید وارد می‌شوند و صفحه را هنگام بالا آمدن refresh می‌کند.

Reply Expiration

اگر نمی‌خواهید کاربر داده‌های قدیمی را ببیند و ترجیح می‌دهید یک صفحه loading را نشان دهید، باید از پارامتر replayExpirationMillies در WhileSubscribed استفاده کنید. این پارامتر در این شرایط بسیار مفید است و همچنین در حافطه نیز صرفه جویی می‌کند، زیرا مقادیر ذخیره شده داخل cache به مقدار اولیه داخل stateIn بازیابی می‌شود. با این کار هنگام بازگشت به برنامه داده‌های قدیمی را نشان نمی‌دهید.

replayExpirationMillies یک تاخیر(بر حسب میلی ثانیه) بین توقف کوروتین اشتراک گذاری شده و تنظیم مجدد cache است(که cache را برای اپراتور shareIn خالی می‌کند و مقدار cache را به initialValue برای عملگر stateIn مجدد قرار می‌دهد). مقدار پیش فرض آن Long.MAX_VALUE(cache را برای همیشه نگه دارید، بافر را هرگز تنظیم مجدد نکنید). از مقدار صفر برای اینکه cache بلافاصله منقضی شود می‌توانید استفاده کنید.

مشاهده StateFlow از view

همانطور که تا کنون مشاهده کرده‌اید برای view بسیار مهم است که به StateFlow در ViewModel اطلاع داده شود که آن‌ها دیگر گوش نمی‌دهند. با این حال مانند هرچیزی که به چرخه زندگی مربوط می‌شود، به همین سادگی نیست.

برای جمع آوری flow، شما به یک کوروتین نیاز دارید. اکتیویتی‌ها و فرگمنت‌ها، تعدادی coroutine builder ارائه می‌دهند:

  • Activity.lifecycleScope.launch: بلافاصله کوروتین را شروع می‌کند و با از بین رفتن اکتیویتی آن را لغو می‌کند.
  • Fragment.lifecyclescope.launch: بلافاصله کوروتین را شروع کرده و با از بین رفتن فرگمنت آن را لغو می‌کند.
  • Fragment.lifecycleOwner.lifecycleScope.launch: بلافاصله کوروتین را شروع کرده و با ازبین رفتن چرخه زندگی فرگمنت آن را لغو می‌کند. اگر Ui را تغییر می‌دهید باید از چرخه زندگی view استفاده کنید.

LaunchWhenStarted, launchWhenResumed…

نسخه‌های خاص launch به نام launchWhenX هستند که منتظر می‌مانند تا lifecycleOwner در حالت X قرار بگیرد و وقتی که lifecycleOwner در زیر حالت X قرار می‌گیرد، کوروتین معلق(suspend) می‌شود. لازم به ذکر است که آن‌ها تا زمانی که lifecycleOwner از بین نرود، کوروتین را لغو نمی‌کنند.

دریافت بروزرسانی در حالی که برنامه در پس‌زمینه است، منجر به crash برنامه می‌شود، که با تعلیق مجموعه در view برطرف می‌شود. با این حال flow‌های بالایی تا زمانی که برنامه در پس‌زمینه است، فعال نگه داشته می‌شوند و باعث هدر رفتن منابع می‌شوند.

این یعنی که هر کاری که تاکنون برای پیکربندی StateFlow انجام داده‌ایم کاملاً بی‌فایده است. با این حال یک api جدید برای این کار وجود دارد.

Lifecycle.repeatOnLifecycle برای نجات

این سازنده جدید کوروتین(از نسخه lifecycle-runtime-ktx 2.4.0-alpha01 به بعد وجود دارد) دقیقاً همان چیزی را که ما نیاز داریم انجام می‌دهد: که کوروتین‌ها را در یک حالت خاص شروع می‌کند و وقتی lifecycle owner به زیر آن حالت می‌رسد، آن‌ها را متوقف می‌کند.

برای مثال در فرکمنت:

این کار وقتی view فرگمنت در حالت start قرار می‌گیرد شروع می‌شود، و وقتی در حالت resume قرار می‌گیرد ادامه پیدا می‌کند، و وقتی به حالت stop برگردد متوقف خواهد شد. یک راه امن‌تر برای جمع آوری داده از ui را می‌توانید در این مقاله بخوانید.

ترکیب repeatOnLifeCycle با StateFlow ضمن استفاده خوب از منابع دستگاه، بهترین عملکرد را برای شما به ارمغان می‌آورد.

هشدارها

  • به تازگی پشتیبانی از StateFlow به DataBinding اضافه شده است که برای جمع آوری بروزرسانی‌ها از startWhenCreate استفاده می‌کند، و هنگامی که به حالت ثبات رسید از repeatOnLifecycle به جای آن استفاده می‌کند.
  • برای DataBinding باید از Flowها در همه جا استفاده کنید و asLiveData را اضافه کنید تا در view بتوان آن‌ها را دریافت کرد. DataBinding با stable شدن نسخه lifecycle-runtime-ktx 2.4.0 بروزرسانی خواهد شد.

بهترین روش برای نشان دادن داده‌ها از ViewModel و دریافت آن‌ها از view:

  • با استفاده از استراتژی WhileSubscribed می‌توان StateFlow را با یک وقفه مشخص در معرض دید قرار داد. ]مثال[
  • می‌توان با استفاده از repeatOnLifecycle داده‌ها را دریافت کرد. ]مثال[

هر ترکیب دیگری flowهای بالایی را فعال نگه می‌دارد و باعث هدر رفتن منابع می‌شود:

  • در معرض دید قرار دادن داده با whileSubscribed و دریافت با استفاده از lifecycleScope.launch/launchWhenX
  • در معرض دید قرار دادن داده با استفاده از Lazily/Eagerly و دریافت با استفاده از repeatOnLifecycle

البته اگر شما به تمام قدرت Flow نیاز ندارید، فقط از LiveData استفاده کنید.

منبع

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
در انتظار ثبت رای

3 سال پیش
/@pouryasharifi78
پوریا شریفی
توسعه‌دهنده‌ی اندروید

ابتدا که با برنامه‌نویسی آشنا شدم به سمت php و طراحی وب رفتم، بعد از اون به توسعه‌ی اندروید علاقه‌مند شدم و تقریبا ۲ سال است که مشغول به برنامه‌نویسی اندروید هستم، همچنین عاشق یادگیری چیزهای جدید هستم.

دیدگاه و پرسش

برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید ورود یا ثبت‌نام

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید