Flow در کاتلین چیست؟ – آموزش به زبان ساده + نمونه کد
دنیای برنامه نویسی اندروید برای برنامه نویسان توسعهدهندگان اپلیکیشنهای اندرویدی بعد از معرفی زبان برنامه نویسی کاتلین به عنوان دومین زبان رسمی توسعه اندروید وارد فاز جدیدی شد. بسیاری از مفاهیم قدیمی موجود در زبان جاوا که برنامهنویسان اندروید از آنها برای توسعه اپلیکیشن استفاده میکردند، در زبان کاتلین با مفاهیم و ساختارهای جدید جایگزین شدند. یکی از ساختارهای قدیمی اندروید، RxJava بود که امکان «برنامه نویسی ناهمگام» (Asynchronous Programming) و رویدادگرا را فراهم میکرد. Flow در کاتلین به عنوان جایگزینی برای RxJava مطرح شد تا امکان برنامه نویسی ناهمگام با کاتلین نیز میسر شود.
بر همین اساس در این نوشته به زبان ساده مفهوم Flow در کاتلین مورد بررسی قرار گرفته و علاوه بر آن مزایای استفاده از این ساختار در برنامه نویسی اندروید نیز شرح داده شده است. در ادامه هم مباحث مهمی همچون چرخه حیات Flow در کاتلین، پیادهسازی و مفهوم تولید کننده و مصرف کننده در ساختار Flow مورد بررسی قرار گرفتهاند.
منظور از Flow در کاتلین چیست؟
Flow در کاتلین به عنوان افزونهای برای «کوروتینها» (Coroutines) شناخته میشود و در قالب فریم ورک «برنامه نویسی واکنشی» (Reactive Programming) عمل میکند. Flow در کاتلین برای مدیریت جریانی «غیر همزمان» (Asynchronous) از دادهها طراحی شده است.
شاید خواندن جملات بالا و برخورد با اصطلاحاتی همچون کوروتین، برنامه نویسی واکنشی و جریانی غیر همزمان از دادهها مفهوم Flow در کاتلین را برای شما با ابهامات بیشتری نیز روبرو کرده باشد. به همین دلیل، بررسی اصطلاحات فوق منجر به درک بهتر مفهوم Flow در کاتلین خواهد شد.
کوروتینها در برنامه نویسی اندروید با کاتلین به عنوان ابزاری برای مدیریت راحتتر «نخ | رشته» (Thread) شناخته میشوند. Thread نیز کوچکترین واحد اجرایی در سیستم عامل است و امکان اجرای چند «وظیفه» (Task) را فراهم میکند.
Flow در کاتلین را میتوان به عنوان یک کوروتین تعریف کرد که دارای مقادیر محاسبه شده غیر همزمان است. Flow در واقع جریانی از دادهها است که به صورت غیر همزمان محاسبه شده و برای ارسال چندین مقدار به صورت متوالی استفاده میشود. دادههای ارسال شده با Flow باید از نوع یکسانی باشند و به صورت متوالی منتشر شوند. Flow برای مدیریت ارسال این دادهها از توابع suspended
در کاتلین استفاده میکند.
مزیت استفاده از Flow در کاتلین چیست؟
«برنامه نویسی ناهمگام» (Asynchronous Programming) نقش بسیار مهمی در «واکنش گرا» (Responsive) کردن یک اپلیکیشن و بهبود عملکرد آن ایفا میکند. برای توسعه اپلیکیشن اندروید میتوان از RxJava استفاده کرد که امکان برنامه نویسی ناهمگام و رویدادگرا را فراهم میسازد. این کتابخانه با در اختیار قرار دادن امکانات و ابزارهای مختلف، نیاز به کد نویسی ناهمگام را کاهش میدهد و کار را بسیار آسان میکند.
در زبان برنامه نویسی کاتلین نیز Flow عملکردی مشابه با RxJava را ارائه میدهد. مزیت استفاده از Flow در کاتلین این است که نه تنها تمام عملگرها و عملکردهای مورد نیاز را دارد، بلکه از توابع suspended
نیز پشتیبانی میکند. توابع suspended
به انجام کارهای غیر همزمان به صورت متوالی کمک میکنند.
Flow در کاتلین چگونه کار میکند؟
اپلیکیشنی را در نظر بگیرید که در آن دادهها از سرور درخواست میشوند و از برنامه نویسی ناهمگام برای مدیریت این دادهها استفاده میشود. در این شرایط، Flow در کاتلین این دادهها را در پسزمینه به صورت ناهمگام مدیریت میکند، زیرا احتمال این مسئله وجود دارد که برخی از فرایندها برای واکشی و دریافت اطلاعات طولانی شوند.
با توجه به تصویر بالا، Flow در کاتلین دنبالهای از مقادیر است که از توابع suspend
برای تولید و به مصرف رساندن مقادیر به صورت ناهمگام استفاده میکند. Flow از ۳ موجودیت زیر تشکیل شده است.
- «تولید کننده» (Producer): دادههایی که باید به جریان اضافه شوند را منتشر میکند.
- «مداخله کننده» (Intermediary): امکان اصلاح و تغییر دادههای منتشر شده را فراهم میکند.
- «مصرف کننده» (Consumer): دادههای منتشر شده را دریافت میکند.
پیاده سازی Flow در کاتلین
بعد از آشنایی مقدماتی با Flow در کاتلین و مزیتهای استفاده از آن در برنامه نویسی اندروید در این بخش از نوشته به پیادهسازی Flow در کاتلین پرداخته شده است. با توجه به موجودیتهای تشکیل دهنده Flow در گام نخست باید اولین موجودیت یعنی «تولید کننده» (Producer) را ایجاد کرد.
موجودیت Producer چیست ؟
producer موجودیتی است که دادهها و مقادیر را در جریان منتشر میکند. برای ایجاد producer باید فایل Main.kt
را باز کرده و قبل از نام تابع main
از عبارت suspend fun
استفاده کرد. این تابع از نوع suspended
است، زیرا باید بتواند سایر توابع suspended
را فراخوانی کند.
suspend fun main () {
val flow = flow{
for(n in 0..9){
delay(2000)
emit(n)
}
}
}
با توجه به نمونه کد بالا برای ایجاد producer باید از متد flow
استفاده کرد. این متد مقادیری از نوع «عدد صحیح» (Integer) را از ۰ تا ۹ به ترتیب با تاخیر ۲۰۰۰ میلی ثانیه یا همان ۲ ثانیه منتشر میکند. در کد بالا از تابع emit()
برای ارسال مقادیر به «مصرف کننده» (Consumer) استفاده میشود. نکته مهمی که در خصوص Flow در کاتلین باید مد نظر داشت این است که Producer هیچ مقداری را منتشر نمیکند، تا زمانی که Consumer برای جمعآوری این مقادیر وجود داشته باشد.
پیاده سازی Consumer
برای پیادهسازی Consumer باید از کوروتین (Coroutine) استفاده کرد، زیرا متد collect
باید درون بدنه کوروتین نگهداری شود. تابع collect
برای جمعآوری مقادیر منتشر شده کاربرد دارد.
val job = GlobalScope.launch {
flow.collect{
delay(3000)
println(num)
}
}
در نمونه کد بالا از تابع delay(3000)
برای ایجاد ۳ هزار میلی ثانیه یا ۳ ثانیه تاخیر استفاده میشود، زیرا امکان وجود «فشار معکوس | فشار برگشتی» (Back Pressure) وجود دارد. فشار برگشتی زمانی رخ میدهد که مصرف کننده دادههای قبلی را هنوز مصرف نکرده است، ولی تولید کننده یا Producer همچنان در حال تولید دادههای جدید است که در نتیجه، امکان از دست رفتن دادهها وجود دارد.
بنابراین، برای جلوگیری از بروز چنین مشکلی، باید از تابع delay
برای ایجاد وقفه استفاده شود. در نهایت، میتوان تابع اصلی برنامه را اجرا و مقادیر به دست آمده را مشاهده کرد. با این روش میتوان پیادهسازی Flow در کاتلین را انجام داد.
چرخه حیات Flow در کاتلین
در این بخش از نوشته با بررسی چرخه حیات Flow در کاتلین و ساختارهای اصلی موجود در آن، سعی خواهد شد تا درک بهتری از این مفهوم کلیدی حاصل شود. هدف اصلی این است که سلسله مراتب موجود در Flow و نحوه عملکرد و زمان رخداد هر کدام مورد بررسی قرار گیرند.
سلسله مراتب Flow در کاتلین
در نمونه کد زیر، سلسله مراتب و توابع موجود در کاتلین مشخص شدهاند.
CoroutineScope(context = Dispatchers.Main.immediate).launch() {
doAction()
flowOf("Hey")
.onEach { doAction() }
.map { it.length }
.onStart { doAction() }
.flowOn(Dispatchers.Default)
.flatMapMerge {
doAction()
flowOf(1)
.flowOn(Dispatchers.Main)
.onEach { doAction() }
}
.flowOn(Dispatchers.IO)
.collect {
doAction()
}
}
}
در ادامه به صورت دقیق هر کدام از موارد فوق مورد بررسی قرار خواهند گرفت.
مبانی و چرخه حیات Flow در کاتلین
پیادهسازی Flow در کاتلین بر مبنای ۲ اینترفیس (Interface) زیر صورت میگیرد.
public interface Flow {
public suspend fun collect(collector: FlowCollector)
}
public fun interface FlowCollector {
public suspend fun emit(value: T)
}
این دو اینترفیس در کنار یکدیگر الگوی مصرف کننده و تولید کننده برای ساخت Flow در کاتلین را تشکیل میدهند. سلسله مراتب تشکیل دهنده Flow در کاتلین از مجموعهای از عملگرها و متدها تشکیل میشود. هر عملگر یک «موجودیت» (Instance) جدید از Flow را ایجاد میکند. این نکته را نیز باید در نظر گرفت که عملگرهای تشکیل دهنده Flow اجرا نمیشوند، تا زمانی که تابع collect
فراخوانی شود. چرخه حیات Flow در کاتلین از ۵ بخش مهم زیر تشکیل شده است.
- «راهاندازی» (Launching)
- «ساخت» (Creation)
- «جمعآوری عملگرها» (Operators Collection)
- «انتشار داده» (Data Emission)
- «لغو یا تکمیل زنجیره» (Cancellation | Completion)
در ادامه به بررسی کامل هر کدام از موارد فوق خواهیم پرداخت.
مرحله راه اندازی در چرخه حیات Flow در کاتلین
در مبحث پیادهسازی Flow در نگاه نخست همه چیز بسیار واضح به نظر میرسد. نمونه کد زیر نحوه راهاندازی Flow را به کمک فراخوانی توابع مربوطه نشان میدهد.
val job = CoroutineScope(Dispatchers.Main.immediate).launch {
doAction()
//....
}
در مثال بالا با استفاده از متد CoroutineScope
کوروتینی در Thread اصلی برنامه اجرا میشود. علاوه بر این، متد doAction()
داخل این کوروتین پیادهسازی شده است. خروجی حاصل از متد CoroutineScope
در متغیر job
ذخیره میشود که به کمک آن میتوان چرخه حیات Flow را مدیریت کرد. به عنوان مثال، با فراخوانی متد cancel()
میتوان تمامی عملکردهای در حال اجرا را متوقف کرد.
مرحله ساخت در چرخه حیات Flow در کاتلین
متد flowOf(“hey”)
اولین بخش از سلسله مراتب Flow در کاتلین است. در این مرحله موجودیت جدیدی از نوع Flow ساخته میشود که در مرحله جمعآوری عملگرها فراخوانی خواهد شد. در ادامه، نمونه کد مربوط به این بخش ارائه شده است.
import kotlinx.coroutines.flow.internal.unsafeFlow as flow
public fun flowOf(value: T): Flow = flow {
emit(value)
}
internal inline fun unsafeFlow(crossinline block: suspend FlowCollector.() -> Unit): Flow {
return object : Flow {
override suspend fun collect(collector: FlowCollector) {
collector.block()
}
}
}
عملگر onEach
نیز در ادامه به همین شکل ساخته میشود. سایر عملگرها هم در این مرحله به ترتیب فراخوانی میشوند و هیچ عملی را نیز انجام نمیدهند.
جمع آوری عملگرها
فرایند جمعآوری عملگرها به صورت سلسله مراتبی و از پایین به بالا آغاز و بلافاصله بعد از فراخوانی عملگرهای پایانه شروع میشود. در ادامه، عملگرهای پایانه موجود در Flow فهرست شده است.
- ()collect
- ()first
- ()toList
- ()toSet
- ()reduce
- ()fold
بعد از فراخوانی عملگرهای بالا، فرایند جمعآوری تنها برای عملگرهایی رخ میدهد که در سطوح بالاتری قرار دارند.
انتشار داده
فرایند انتشار داده در Flow به محض رسیدن به بالاترین سطح در سلسله مراتب Flow شروع میشود. این فرایند از بالاترین تا پایینترین سطح Flow جریان پیدا میکند.
سوالات متداول در خصوص Flow در کاتلین
تا این قسمت از نوشته مفاهیم، نحوه کار، مزایا و چرخه حیات Flow مورد بررسی قرار گرفت. در ادامه، به پرتکرارترین سوالات در این حوزه پاسخ داده شده است.
چه زمانی باید از Flow استفاده کرد؟
Flow گزینه خوبی برای بهروزرسانی اطلاعات در برنامه به حساب میآید. به عنوان مثال، میتوان Flow را به همراه پایگاه داده Room به کار برد تا از تغییرات به وقوع پیوسته در پایگاه داده مطلع شویم. علاوه بر این، یکی از مهمترین کاربردهای Flow در بحث دریافت اطلاعات از سرور است و میتوان از Flow برای تسهیل فرایند دریافت اطلاعات استفاده کرد.
چند نوع Flow وجود دارد؟
در یک دستهبندی کلی، میتوان گفت ۲ نوع Flow در کاتلین وجود دارد که عبارتند از «Hot Flow» و «Cold Flow» و در خصوص این دو نوع نیز باید عنوان کرد که Cold Flow زمانی شروع به تولید مقادیر میکند که در سمت مقابل مصرف کنندهای برای دریافت وجود داشته باشد. در نقطه مقابل، Hot Flow بدون توجه به وجود مصرف کننده به صورت پیوسته مقادیر جدید تولید میکند.
تفاوت Flow و کوروتین در کاتلین چیست؟
در واقع، Flow بخشی از کوروتین است که میتواند چندین مقدار را به صورت متوالی منتشر کند و از این لحاظ Flow دقیقاً در نقطه مقابل توابع suspend
قرار میگیرد، زیرا این توابع تنها یک مقدار را بر میگردانند. به عنوان مثال، میتوان از Flow برای دریافت آخرین بهروزرسانی پایگاه داده استفاده کرد.
آشنایی با Coroutine در کاتلین — از صفر تا صد
تفاوت Flow و LiveData در کاتلین چیست؟
هر دوی آنها به عنوان کلاسی برای نگهداری دادههای «قابل مشاهده» (Observable) کاربرد دارند و از الگوی مشابهی نیز پیروی میکنند. Flow و LiveData رفتار متفاوتی دارند، به این صورت که Flow برای ایجاد نیازمند مقداردهی اولیه است ولی LiveData این گونه نیست.
جمعبندی
ظهور زبان برنامه نویسی کاتلین و معرفی این زبان به عنوان زبان بومی توسعه اپلیکیشنهای اندرویدی، دنیای برنامه نویسی اندروید را دستخوش تغییرات زیادی کرد. قابلیتهای جذاب زبان کاتلین فرایند برنامه نویسی اندروید را برای توسعهدهندگان آسانتر و سریعتر ساخت. در عین حال بسیاری از کارهایی که در برنامه نویسی اندروید با جاوا امکانپذیر بودند نیز به شیوهای متفاوت و در عین حال راحتتر به کمک کاتلین قابل انجام بودند. Flow در کاتلین یکی از فناوریهایی به حساب میآید که جایگزین RxJava شده است. Flow در کاتلین امکان برنامه نویسی ناهمگام و رویدادگرا را فراهم کرده است. علاوه بر این، به کمک Flow میتوان دریافت اطلاعات از سرور را مدیریت و از بروز خطاها و مشکلات ناخواسته جلوگیری کرد.
یادگیری Flow در کاتلین و توانایی استفاده از آن کمک شایانی به برنامه نویسان اندروید در فرایند توسعه اپلیکیشن میکند. استفاده از Flow منجر به ساخت اپلیکیشنهایی سریعتر، روانتر و پایدارتر میشود. بر این اساس در این نوشته سعی شد تا مفهوم Flow در کاتلین به زبان ساده توضیح داده شود و علاوه بر آن نیز مزایای استفاده از این ساختار در برنامه نویسی اندروید شرح داده شد. در ادامه، نیز ضمن بررسی چرخه حیات Flow، نحوه پیادهسازی آن و مفاهیم مهمی همچون تولید کننده و مصرف کننده در Flow به طور کامل مورد بررسی قرار گرفت.