Flow کاتلین در اندروید — راهنمای کاربردی

آخرین به‌روزرسانی: ۱۷ فروردین ۱۳۹۹
زمان مطالعه: ۵ دقیقه

RxJava سال‌ها است که در پروژه‌های اندرویدی وجود دارد و اینک به یکی از بلوک‌های اصلی تشکیل‌دهنده اغلب این پروژه‌ها تبدیل شده است. Flow کاتلین گام‌های بلندی در جهت کسب این جایگاه برداشته است، اما آیا می‌تواند جایگزین RxJava شود یا هنوز مسیر درازی در پیش دارد؟ در این مقاله به بررسی Flow کاتلین در اندروید می‌پردازیم و با مبانی آن و شیوه رسیدن از فراخوانی‌های API به UI را صرفاً با استفاده از فلو و بهره‌گیری از LiveData ،ViewModel و دیگر الگوهای ریپازیتوری بررسی می‌کنیم.

Flow چیست؟

Flow کاتلین یک افزونه برای Coroutine-های کاتلین محسوب می‌شود. کوروتین‌ها ما را از جهنم Callback رها می‌سازند و امکان اجرای کد ناهمگام را به شیوه مشابه کدهای همگام فراهم می‌سازند. Flow با افزودن stream به این مجموعه، موجب پیشرفت هر چه بیشتر آن شده است. شاید از خود بپرسید چرا از کانال (Channel) کوروتین بهره نگیریم؟ کانال‌ها ناپایدار هستند، یعنی اگر یک کانال را فراخوانی کنیم، اما داده‌های آن را مصرف نکنیم، از دست می‌رود. Flow ثبات بیشتری دارد و از این رو داده‌ها صرفاً زمانی که شروع به گردآوری آن‌ها بکنیم دریافت می‌شوند. این وضعیت شبیه به اشتراک در stream-های RxJava است.

معماری

کار بررسی خود را از معماری آغاز می‌کنیم. در این جا از الگوی MVVM استفاده می‌کنیم که همراه با الگوی ریپازیتوری برای لایه داده استفاده می‌شود. به طور معمول در RxJava با Observable-ها در سراسر لایه داده مواجه هستیم که در ViewModel مدیریت‌شده و با استفاده از LiveData به UI ارسال می‌شوند. در زمان استفاده از Flow کاتلین از Observable-ها فاصله می‌گیریم و از تابع‌های suspend همراه با Flow-ها استفاده می‌کنیم. این به آن معنی است که معماری ما اینک به صورت زیر در می‌آید:

Flow کاتلین در اندروید

فراخوانی‌های شبکه با Retrofit

Retrofit کتابخانه آماده ما برای مصرف REST API محسوب می‌شود. به طور معمول از Flow در لایه داده استفاده می‌شود و به هیچ وابستگی دیگری برای اجرایی کردن آن نیاز نداریم. Retrofit از نسخه 2.6.0 به بعد از تابع‌های suspend پشتیبانی می‌کند و این دقیقاً همان چیزی است که نیاز داریم. دیگر نیازی به بازگشت Observables‌ ،Singles ،‌Flowables ،Completables یا حتی Maybes وجود ندارد. کافی است شیء یا پاسخ مطلوب را بازگشت دهید و suspend را به تابع اضافه کنید:

@GET("foo")
suspend fun getFoo(): List<Foo>

بدین ترتیب کار لایه شبکه پایان می‌یابد.

ریپازیتوری

فرض کنید می‌خواهیم پاسخی را از فراخوانی شبکه از طریق ریپازیتوری خود در یک Flow بازگشت دهیم. این کار با قرار دادن آن مورد درون {…}flow  ممکن است. در این جا می‌توانیم کارهایی که لازم است را انجام دهیم و از emit برای ارسال مقدار جدید بهره بگیریم.

به طور جایگزین می‌توان از تابع‌هایی مانند ()flowOf یا تابع‌های اکستنشن، مانند ()adFlow نیز استفاده کرد که اشیا را به یک Flow تبدیل می‌کنند و محتوای آن را مستقیماً ارسال می‌کنند. این که از کدام یک استفاده می‌کنید به نیازهای شما بستگی دارد.

زمانی که Flow آماده شد، می‌توانیم نوعی نگاشت در ریپازیتوری خود با استفاده از عملگر {…}map. داشته باشیم و با استفاده از (…)flowOn. نوعی «نخ‌بندی» (threading) اضافه کنیم.

class FlowRepository(private val api: FlowApiService) {

  fun getFoo(): Flow<List<Foo>> {
      return flow {
        // exectute API call and map to UI object
        val fooList = api.getFoo()
              .map { fooFromApi ->
                  FooUI.fromResponse(fooFromApi)
              }

          // Emit the list to the stream
          emit(fooList)
      }.flowOn(Dispatchers.IO) // Use the IO thread for this Flow
  }
  
  fun getFooAlternative(): Flow<List<Foo>> {
        return api.getFoo()
                .map { fooFromApi -> FooUI.fromResponse(fooFromApi)}
                .asFlow()
                .flowOn(Dispatchers.IO)
  }
}

اینک ریپازیتوری ما یک فراخوانی API اجرا می‌کند و آن را به اشیای UI روی نخ IO نگاشت می‌کند. همه این موارد از طریق Flow مدیریت می‌شوند.

ViewModel

در ViewModel چندین گزینه برای مدیریت Flow داریم. کار خود را با یک مدل ساده آغاز می‌کنیم که به طرز کار RxJava شباهت دارد. برای استفاده از liveData builder باید به بخش پایین مراجعه کنید.

اینک یک ریپازیتوری داریم که یک Flow بازگشت می‌دهد، اما چطور می‌توانیم این داده‌ها را مدیریت کرده و به UI بفرستیم؟

کار خود را از موارد معمول آغاز می‌کنیم که شیءهای تغییرپذیر و تغییرناپذیر liveData هستند. ما می‌خواهیم مقادیر را از طریق ریپازیتوری ارسال کنیم، بنابراین نخستین سؤال این است که چطور باید داده‌ها را از ریپازیتوری خودمان بازیابی کنیم؟ دریافت داده‌های یک Flow کار آسانی است، کافی است از تابع { … } collect استفاده کنید. به خاطر داشته باشید که لیست نگاشت‌یافته را با استفاده از emit به ریپازیتوری ارسال کردیم. این داده‌ها در نهایت سر از collect درمی‌آورند.

از آنجا که collect را تنها می‌توانیم از یک کوروتین یا تابع تعلیق یافته فراخوانی کنیم، از viewModelScope برای اجرای Flow گرداوری داده‌ها استفاده می‌کنیم:

private val _foo = MutableLiveData<List<FooUI>>()
val foo: LiveData<List<FooUI>> get() = _foo

fun loadFoo() {
  viewModelScope.launch {
    fooRepository.getFoo()
      .collect { fooItems ->
        _foo.value = fooItems
      }
  }
}

viewModelScope ما را مطمئن می‌سازد که داده‌ها را از فراخوانی API تا LiveData ارسال کرده‌ایم تا در UI دیده شود. اما در صورتی که خطایی رخ دهد یا اگر بخواهیم کار خود را با داده‌های متفاوتی آغاز کنیم، Flow همچنان به کمک ما می‌آید. می‌توانیم یک مقدار را در زمانی که Flow شروع به گرداوری داده‌ها از طریق { … }onStart می‌کند emit کنیم و در صورتی که خطایی رخ داده باشد، می‌توانیم آن را با { … }catch. به دام بیندازیم.

private val _foo = MutableLiveData<List<FooUI>>()
val foo: LiveData<List<FooUI>> get() = _foo

fun loadFoo() {
  viewModelScope.launch {
    fooRepository.getFoo()
      .onStart { /* _foo.value = loading state */ }
      .catch { exception -> /* _foo.value = error state */ }
      .collect { fooItems ->
        _foo.value = fooItems
      }
  }
}

اکنون می‌توانیم داده‌ها را مدیریت کنیم و مقادیر چندگانه را به UI بفرستیم.

Catch استثنا را مصرف می‌کند، اما می‌توان از onCompletion نیز برای مدیریت استثنا استفاده کرد و همچنان حرکت به سمت پایین را ادامه داد. onCompletionto زمانی تحریک می‌شود که Flow کامل شود و یک استثنا زمانی بازگشت می‌یابد که خطایی رخ داده باشد یا در صورتی که با موفقیت کامل شود null بازگشت می‌یابد.

LiveData builder

به جای تعیین مقدار LiveData به صورت دستی، چنان که در مثال قبلی دیدیم، می‌توان از LiveData builder نیز به این منظور کمک گرفت. آن را می‌توان همچون یک دامنه برای شیء LiveData در نظر گرفت. LiveData builder زمانی تحریک می‌شود که بخشی از UI شروع به مشاهده LiveData بکند و زمانی لغو می‌شود که observer دیگری پیش از آن تکمیل نشده باشد. استفاده از LiveData builder آسان است و دو گزینه در اختیار داریم: کدی را که می‌خواهیم اجرا کنیم درون { … } liveData  قرار دهیم و یک مقدار emit کنیم یا از تابع اکستنشن ()asLiveData برای یک Flow بهره بگیریم که اساساً همان کار را انجام می‌دهد:

val foo: LiveData<List<FooUI>> = liveData {
                                    fooRepository.getFoo()
                                        .onStart { /* emit loading state */ }
                                        .catch { exception -> /* emit error state */ }
                                        .collect { fooItems ->
                                          emit(fooItems)
                                        }
                                  }

val foo2: LiveData<List<FooUI>> = fooRepository.getFoo()
                                    .onStart { /* emit loading state */ }
                                    .catch { exception -> /* emit error state */ }
                                    .asLiveData()

بدین ترتیب با روش استفاده از Flow به همراه LiveData builder آشنا شدیم. توجه کنید که وقتی بلوک کد درون LiveData builder اجرا شده و با موفقیت پایان یابد، در وهله‌های بعدی ری‌استارت نمی‌شود. این کد تنها در صورتی ری‌استارت می‌شود که بیلدر خودش اجرا را لغو کند. برای مثال ممکن است به دلیل وجود observer غیر فعال این کار را انجام دهد.

نکته: LiveData builder مقدار timeout قابل پیکربندی دارد، یعنی می‌توانید تعیین کنید بلوک کد درون بیلدر تا چه مدت پس از آن که هیچ observer فعالی وجود نداشت صبر کند و بعد خود را لغو کند.

سخن پایانی

در این مقاله با روش استفاده از Kotlin Flow در یک پروژه اندروید به اختصار آشنا شدیم. داشتن اندکی تجربه کار با RxJava به پیاده‌سازی Flow کمک می‌کند، اما ممکن است در آغاز کار یادگیری آن برایتان دشوار باشد. در هر حال Flow کاتلین موجب حذف بسیاری از وابستگی‌های پروژه می‌شود. از این رو استفاده از آن کاملاً توصیه می‌شود و دست کم ارزش یک بار تجربه کردن را دارد.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۵ نفر
آیا این مطلب برای شما مفید بود؟
شما قبلا رای داده‌اید!
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
proandroiddev

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *