1. مقدمه
2. مشکل از کجا شروع شد؟ (چرا کوروتین؟)
3. تعریف کوروتین؟
3.1 استفاده از کوروتین برای تسک های طولانی
3.2 Main-Safty با کوروتین
4. ردیابی کوروتین ها
4.1 کنسل کردن کار با socps
4.2 ردیابی یک کار تا زمانی که در حال اجراست
4.3 ارسال خطاها هنگام عدم موفقیت یک کوروتین
5. استفاده از structured concurrency
6. خلاصه
7. منابع
دور هم جمع شدن یک عده از اندروید دولاپرها که تصمیم داشتن با پیاده سازی یک پروژه اوپن سورس موزیک پلیر، مطالب جدیدی رو در اندروید یاد بگیرن و تمرین کنن، باعث شد من در مورد کاتلین کوروتین چندتا مقاله بخونم و ویدئو ببینم.
حاصل این مطالعه دو روزه شد این مقاله که پیش روی شماست.
اگه مفهوم کوروتین برای شما هم گنگه، یا تازه میخواین شروع به یادگیری این مفهوم نوظهور در کاتلین بکنین در ادامه مقاله با من همراه باشین.
توجه: اگرچه کوروتین مفهوم جدیدی در کاتلین ۱.۳ میباشد، اما پیش از این در زبانهای زیادی استفاده میشده. ما برای اشاره به مفهوم کوروتین در کاتلین از عبارت کاتلین کوروتین استفاده خواهیم کرد.
هر اپ در اندروید شامل یکmain thread یا ترد اصلی می باشد که مسیولیت به روز رسانی های ui و تعامل با کاربر را بعهده دارد. اگر کارهای بسیار زیاد و یا سنگین روی این ترد انجام شود، اپ بسیار کند شده و یا هنگ خواهد کرد. چنین تسک هایی باید بدون بلاک کردن ترد اصلی انجام شود.
به این مثال توجه کنید:
همواره کد رویایی ما در برنامه نویسی به این شکل می باشد:
اما ما نمیتوانیم درخواستهای سرور را روی ترد اصلی پیاده کنیم. به همین خاطر در این کد، با NetworkOnMainThreadExeption مواجه خواهیم شد.
حال شاید بگوئید میتوانیم از ترد استفاده کنیم. به این شکل:
اما این کد نیز بخاطر خط دوم دچار CalledFromWrongThreadExeption خواهد شد.
اما راه حل چیست؟
استفاده از callback!
استفاده از callback ها برای اجتناب از بلاک شدن ترد اصلی بسیار مفید است اما callback ها نیز مشکلات خود را دارند:
برای حل این مشکلات می توان از کوروتین استفاده کرد.
کوروتین در حقیقت شیوه جدیدی برای ساده کردن کدهای async است. کوروتین کدهای ما را ساده تر و خواناتر می کند. کوروتین ها امکان نوشتن کد غیر همزمان (asynchronous) را به روشی همزمان (synchronous) امکانپذیر می سازد. نوشتن کدها بصورت متوالی در کوروتین به خوانایی کدها کمک می نماید.
کوروتین ها مشابه تردها میباشند با این تفاوت که:
۱. چندین کوروتین میتواند داخل یک ترد اجرا شود.
۲. کوروتین ها بسیار سبکتر از تردها میباشند.
۳. تردها توسط os مدیریت می شوند، در حالی که کوروتین ها توسط کاربر.
نکته: کوروتین ها جایگزین تردها نیستند، بلکه بیشتر شبیه یک فریم ورک برای مدیریت آنها می باشند.
کوروتین ها در اندروید دو مشکل اساسی را حل میکنند:
1. کارهای طولانی که main thread را برای مدت طولانی بلاک میکنند.
2. Main-safety که به شما این اجازه را میدهد که توابع suspend را در ترد اصلی فراخوانی کنید.
(مشابه دریم کد!!?✌️)
3.1 استفاده از کاتلین برای تسک های طولانی:
ارتباط با یک api، خواندن داده ها از دیتابیس، لود کردن یک عکس از دیسک، چیزهایی هستند که ما به آنها long running task یا تسک های طولانی می گوییم.
کوروتین ها در حقیقت راهی برای ساده کردن کدها، برای مدیریت کارهای طولانی هستند.
به کد پایین که با کمک کوروتین نوشته شده است دقت کنید:
suspend fun fetchDocs() { // Dispatchers.Main val result = get("developer.android.com") // Dispatchers.Main show(result) // Dispatchers.Main } suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}
کوروتین ها این امکان را فراهم میکنند که این کد بدون بلاک کردن ترد اصلی اجرا شود.
برای استفاده از کوروتین در کاتلین کافی است کلمه کلیدی suspend را به ابتدای توابع اضافه کنیم. هنگامی که یک کوروتین یک تابع suspend را به حالت تعلیق در می آورد، ترد اصلی را بلاک نکرده، و هنگامی که نتیجه آماده شد، تابع تعلیق شده را از همان نقطه از سر میگیرد (resume).
کوروتین ها شامل دو عملیات جدید می باشند:
suspend و resume با یکدیگر، جایگزین callback ها می شوند.
نکته: توابعsuspend تنها از داخل توابع suspend دیگر، و یا یک کوروتین بیلدر مانند launch قابل فراخوانی هستند.
در مثال بالا تابع get قبل از نتورک ریکوئست بحالت معلق درخواهد آمد. و پس از دریافت پاسخ از سرور، کار خود را از همان جایی کهpause شده بود از سر میگیرد.
هرگاه یک کوروتین معلق شود، استک فریم جاری (جایی که کاتلین برای نگه داشتن اطلاعات توابع معلق شده استفاده میکند) کپی و ذخیره میگردد. و هنگام resume، استک فریم باز خوانی شده و از همان نقطه معلق شده از سر گرفته خواهد شد.
این انیمیشن نحوه suspend و resume شدن تابع را بخوبی نشان میدهد.
در اواسط انیمیشن، وقتی همه کوروتین ها در ترد اصلی معلق شدند، ترد اصلی برای انجام سایر کارهای خود، مثل آپدیت ui و هندل کردن User event آزاد خواهد شد.
3.2 Main-safety با کوروتین ها
در کاتلین کوروتین، توابعsuspend برای فراخوانی از ترد اصلی، همیشه امن هستند.
نکته: عبارت suspend بمعنای اجرای تابع در ترد backgroundنیست، بلکه ممکن است این تابع در ترد اصلی و یا ترد background اجرا شود.
در کاتلین همه کوروتین ها باید در یک دیسپچر اجرا شوند. دیسپچر مدیریت اجرای کوروتینها را بر عهده دارد. کوروتین ها میتوانند خود را ساسپند کنند، و دیسپچر آنها را resumeخواهد کرد.
یک دیسپچر مدیریت میکند که کدام ترد کوروتین را اجرا کند.
در کاتلین سه نوع دیسپچر وجود دارد:
1. Dispatchers.Main: ترید اصلی، تعامل با کاربر و انجام کارهای سبک
2. Dispatchers.IO: بهینه شده برای ورودی/خروجی دیسک و شبکه، خارج از main thread
3. Dispatchers.Default: بهینه شده برای کارهای سنگین cpu، خارج از main thread
برای کامل کردن کد بالا بیایید از dispatcher ها استفاده کنیم. داخل تابع get برای ایجاد یک بلوک که در دیسپچر ioاجرا شود از withContext()استفاده میکنیم.
withContext(Dispatchers.IO) یک بلاک کوروتین می باشد که تمامی کدهای داخل این بلاک در دیسپچر ioاجرا خواهد شد. از آنجایی که تابع withContet(Dispatchers.IO) خود یک تابع suspend می باشد، با استفاده از کوروتین main safety را فراهم خواهد کرد.
suspend fun fetchDocs() { // Dispatchers.Main val result = get("developer.android.com") // Dispatchers.Main show(result) // Dispatchers.Main } // Dispatchers.Main suspend fun get(url: String) = // Dispatchers.Main withContext(Dispatchers.IO) { /* perform blocking network IO here */ // Dispatchers.IO } // Dispatchers.Main
در این مثال fetchDocs
در ترد اصلی اجرا می شود، اما می تواند تابع get را که در آن یک درخواست سرور وجود دارد، فراخوانی نماید. زیرا هر دو تابع suspend می باشند.
در حقیقت ما با استفاده از suspend، withContext(Dispatchers.IO)توانستیم چنین توابعی را در ترد اصلی فراخوانی کنیم.
اگرچه کوروتین ها مزایای زیادی دارند، اما نمی توانیم آنها را پیگیری نماییم. شرایطی را در نظر بگیرید که صدها یا حتی هزاران کوروتین داشته باشیم! اگر همه آنها را ردگیری نکرده و به موقع لغو یا کامل نکنیم، برنامه ما ممکن است دچار work leak شود.
یک کوروتین رها شده می تواند cpu یا دیسک را هدر داده یا حتی درخواست شبکه ای که دیگر لازم نیست را اجرا کند.
برای جلوگیری از این امر کوروتین Structured concurrency را معرفی می نماید.
ما از structured concurrency در اندروید برای انجام سه کار استفاده میکنیم:
1. کنسل کردن یک کار وقتی دیگر نیازی به آن نداریم
2. ردیابی یک کار تا زمانی که در حال اجراست
3. ارسال خطاها هنگام عدم موفقیت یک کوروتین
4.1 کنسل کردن کار با scope
در کاتلین، کوروتین ها باید داخل CoroutineScope اجرا شوند. یک CoroutineScope همه کوروتین ها، حتی کوروتین های suspend شده را ردیابی میکند. CoroutineScope کوروتین ها را اجرا نمیکند، و تنها وظیفه ردگیری آنها را دارد.
برای اطمینان از اینکه همه کوروتین ها ردیابی می شوند، کاتلین به شما اجازه شروع یک کوروتین جدید بدونCoroutineScope را نمیدهد.
نکته: یک CoroutineScope تمام کوروتین های ما را ردیابی می کند، و می تواند تمام کوروتین های شروع شده در آن را لغو کند.
4.1.1 شروع یک کوروتین جدید
دو راه برای شروع کوروتین ها وجود دارد:
1. launch کوروتین هایی که این کوروتین بیلدر آغاز میکند نیازی به برگرداندن نتیجه ندارند.
2. async کوروتین هایی که این کوروتین بیلدر آغاز میکند به شما اجازه دریافت نتیجه بازگشتی با کمک await را میدهد.
می توان launch را پلی میان توابع عمومی و کوروتین ها دانست.
از آنجایی که launch و async تنها در CoroutineScope در دسترس هستند، در نتیجه قابل ردگیری می باشند.
4.1.2 شروع در ViewModel
دقیقا کجا باید launch را فراخوانی کرده و scope ها را قرار دهیم؟ و چه زمانی باید همه کوروتین های داخل scope را کنسل کنیم؟
منطقی است که یک CoroutineScope
را متناظر با یک صفحه کاربری در نظر بگیریم. این امر از نشت کوروتین ها و یا انجام کارهای اضافی برای اکتیویتی و فرگمنتهایی که دیگر نمایش داده نمی شود، جلوگیری میکند. وقتی کاربر از آن صفحه خارج میشود، CoroutineScope
متناظر می تواند همه کارها را کنسل کند.
هنگام ادغام کوروتین ها با Android Architecture Componentsمنطقی است که کوروتین ها را در ViewModel راه اندازی کنیم. با این کار دیگر نگران چرخش صفحه موبایل نخواهیم بود.
برای استفاده از کوروتین ها در ViewModel میتوانیم از viewModelScope استفاده کنیم.
lifecycle-viewmodel-ktx:2.1.0-alpha04
.viewModelScope
به مثال زیر توجه کنید:
class MyViewModel(): ViewModel() { fun userNeedsDocs() { // Start a new coroutine in a ViewModel viewModelScope.launch { fetchDocs() } } }
4.2 ردیابی کارها
شما می توانید laounch و async را طبق نیاز خود باهم ترکیب و هماهنگ کنید. مثل کد زیر:
suspend fun fetchTwoDocs() { coroutineScope { launch { fetchDoc(1) } async { fetchDoc(2) } } }
تفاوت اصلی coroutineScope و supervisorScope
در این است که هرگاه هر یک از فرزندان coroutineScope شکست بخورند،coroutineScope لغو خواهد شد. بنابراین اگر یک درخواست شبکه ناموفق باشد، همه درخواست های دیگر بلافاصله لغو می شوند. اگر در عوض می خواهید درخواست های دیگر را حتی در صورت عدم موفقیت ادامه دهید، می توانید از یک supervisorScope
استفاده کنید. در supervisorScope
وقتی یکی از فرزندان ناکام باشد، یک supervisorScope
مابقی را لغو نخواهد کرد.
4.3 ارسال خطاها هنگام شکست خوردن یک کوروتین
در مورد ارسال خطاها، کوروتین مشابه توابع عادی عمل میکند.
از آنجا که coroutineScope
منتظر تمام فرزندان خود میماند، در صورت شکست هر کدام از آنها مطلع خواهد شد.
اگر از structured concurrency استفاده کنیم از work leak اجتناب خواهد شد.
ما از structured concurrency در اندروید برای انجام سه کار استفاده میکنیم:
1. کنسل کردن یک کار وقتی دیگر نیازی به آن نداریم
2. ردیابی یک کار تا زمانی که در حال اجراست
3. ارسال خطاها هنگام عدم موفقیت یک کوروتین
تضمین هایی که structured concurrency به ما میدهد شامل موارد زیر است:
1. هنگام کنسل شدن یک scopeهمه کوروتین های آن کنسل می شوند.
2. وقتی یک تابع suspendبازگشت داده میشود، تمام کارهای آن انجام شده است.
3. وقتی یک کوروتین خطا میدهد، به callerیا scope او اطلاع داده می شود.
تضمین هایی که structured concurrency میدهد، کد ما را تمیزتر و امن تر کرده و به ما اجازه میدهد از work leak جلوگیری کنیم.
کوروتین در حقیقت شیوه جدیدی برای ساده کردن کدهای async است. کوروتین کدهای ما را ساده تر و خواناتر می کند. کوروتین ها امکان نوشتن کد غیر همزمان (asynchronous) را به روشی همزمان (synchronous) امکانپذیر می سازد.
کوروتین ها در اندروید دو مشکل اساسی را حل میکنند:
1. کارهای طولانی که main thread را برای مدت طولانی بلاک میکنند.
2. Main-safety که به شما این اجازه را میدهد که توابع suspend را در ترد اصلی فراخوانی کنید.
توابع suspend تنها از داخل توابع suspend دیگر، و یا یک کوروتین بیلدر مانند launch قابل فراخوانی هستند.
وقتی همه کوروتین ها در ترد اصلی معلق شدند، ترد اصلی برای انجام سایر کارهای خود آزاد خواهد شد.
عبارت suspend بمعنای اجرای تابع در ترد background نیست، بلکه ممکن است این تابع در ترد اصلی و یا ترد background اجرا شود.
در کاتلین همه کوروتین ها باید در یک دیسپچر اجرا شوند. دیسپچر مدیریت اجرای کوروتینها را بر عهده دارد. کوروتین ها میتوانند خود را ساسپند کنند، و دیسپچر آنها را resumeخواهد کرد. یک دیسپچر مدیریت میکند که کدام ترد کوروتین را اجرا کند.
در کاتلین سه نوع دیسپچر وجود دارد:
1. Dispatchers.Main
2. Dispatchers.IO
3. Dispatchers.Default
ما از structured concurrency در اندروید برای انجام سه کار استفاده میکنیم:
1. کنسل کردن یک کار وقتی دیگر نیازی به آن نداریم
2. ردیابی یک کار تا زمانی که در حال اجراست
3. ارسال خطاها هنگام عدم موفقیت یک کوروتین
یک CoroutineScope تمام کوروتین های ما را ردیابی می کند، و می تواند تمام کوروتین های شروع شده در آن را لغو کند.
https://medium.com/androiddevelopers/coroutines-on-android-part-i-getting-the-background-3e0e54d20bb<br/>https://blog.mindorks.com/mastering-kotlin-coroutines-in-android-step-by-step-guide
https://developer.android.com/kotlin/coroutines<br/>https://codelabs.developers.google.com/codelabs/kotlin-coroutines/#4<br/>https://www.youtube.com/watch?v=BOHK_w09pVA