کوروتین یک الگوی طراحی همزمانی است که میتوانید در اندروید برای سادهسازی کدی که به صورت ناهمگام اجرا میشود، از آن استفاده کنید. کوروتینها در نسخه ۱.۳ به کاتلین اضافه شدند و بر اساس مفاهیم تثبیتشده از زبانهای دیگر ساخته شدهاند.
در اندروید، کوروتینها به مدیریت وظایف طولانیمدت کمک میکنند که در غیر این صورت ممکن است نخ اصلی را مسدود کرده و باعث عدم پاسخگویی برنامه شما شوند. بیش از 50٪ از توسعهدهندگان حرفهای که از کوروتینها استفاده میکنند، افزایش بهرهوری را گزارش کردهاند. این مبحث نحوه استفاده از کوروتینهای کاتلین را برای رفع این مشکلات شرح میدهد و شما را قادر میسازد کد برنامه تمیزتر و مختصرتری بنویسید.
ویژگیها
کوروتینها راهحل پیشنهادی ما برای برنامهنویسی ناهمگام در اندروید هستند. ویژگیهای قابل توجه آنها شامل موارد زیر است:
- سبک : به دلیل پشتیبانی از قابلیت تعلیق (suspend) ، میتوانید چندین کوروتین را روی یک نخ واحد اجرا کنید، که نخی را که کوروتین در آن اجرا میشود مسدود نمیکند. تعلیق در عین پشتیبانی از بسیاری از عملیات همزمان، در مقایسه با مسدود کردن، در مصرف حافظه صرفهجویی میکند.
- نشت حافظه کمتر : از همزمانی ساختاریافته برای اجرای عملیات در یک محدوده استفاده کنید.
- پشتیبانی از لغو داخلی : لغو به طور خودکار از طریق سلسله مراتب کوروتین در حال اجرا منتشر میشود.
- ادغام با Jetpack : بسیاری از کتابخانههای Jetpack شامل افزونههایی هستند که پشتیبانی کامل از کوروتینها را ارائه میدهند. برخی از کتابخانهها همچنین دامنه کوروتین مخصوص به خود را ارائه میدهند که میتوانید برای همزمانی ساختاریافته از آن استفاده کنید.
مرور کلی مثالها
بر اساس راهنمای معماری برنامه ، مثالهای این مبحث یک درخواست شبکه ارسال میکنند و نتیجه را به نخ اصلی برمیگردانند، جایی که برنامه میتواند نتیجه را به کاربر نمایش دهد.
به طور خاص، کامپوننت معماری ViewModel لایه مخزن را در نخ اصلی فراخوانی میکند تا درخواست شبکه را فعال کند. این راهنما از طریق راهحلهای مختلفی که از کوروتینها برای جلوگیری از انسداد نخ اصلی استفاده میکنند، تکرار میشود.
ViewModel شامل مجموعهای از افزونههای KTX است که مستقیماً با کوروتینها کار میکنند. این افزونهها کتابخانه lifecycle-viewmodel-ktx هستند و در این راهنما استفاده میشوند.
اطلاعات وابستگی
برای استفاده از کوروتینها در پروژه اندروید خود، وابستگی زیر را به فایل build.gradle برنامه خود اضافه کنید:
شیار
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' }
کاتلین
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") }
اجرا در یک نخ پسزمینه
ارسال درخواست شبکه در نخ اصلی باعث میشود که تا زمان دریافت پاسخ، منتظر بماند یا مسدود شود . از آنجایی که نخ مسدود شده است، سیستم عامل قادر به فراخوانی onDraw() نیست، که باعث میشود برنامه شما متوقف شود و به طور بالقوه منجر به خطای عدم پاسخگویی برنامه (ANR) شود. برای تجربه کاربری بهتر، بیایید این عملیات را در یک نخ پسزمینه اجرا کنیم.
ابتدا، بیایید نگاهی به کلاس Repository خود بیندازیم و ببینیم که چگونه درخواست شبکه را ارسال میکند:
sealed class Result<out R> { data class Success<out T>(val data: T) : Result<T>() data class Error(val exception: Exception) : Result<Nothing>() } private const val loginUrl = "https://example.com/login" class LoginRepository(private val responseParser: LoginResponseParser) { // Function that makes the network request, blocking the current thread fun makeLoginRequest( jsonBody: String ): Result<LoginResponse> { val url = URL(loginUrl) (url.openConnection() as? HttpURLConnection)?.run { requestMethod = "POST" setRequestProperty("Content-Type", "application/json; utf-8") setRequestProperty("Accept", "application/json") doOutput = true outputStream.write(jsonBody.toByteArray()) return Result.Success(responseParser.parse(inputStream)) } return Result.Error(Exception("Cannot open HttpURLConnection")) } }
makeLoginRequest همگام است و نخ فراخوانی را مسدود میکند. برای مدلسازی پاسخ درخواست شبکه، کلاس Result خودمان را داریم.
ViewModel زمانی که کاربر مثلاً روی یک دکمه کلیک میکند، درخواست شبکه را فعال میکند:
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { val jsonBody = "{ username: \"$username\", token: \"$token\"}" loginRepository.makeLoginRequest(jsonBody) } }
با کد قبلی، LoginViewModel هنگام ایجاد درخواست شبکه، نخ رابط کاربری را مسدود میکند. سادهترین راه حل برای انتقال اجرا از نخ اصلی، ایجاد یک کوروتین جدید و اجرای درخواست شبکه روی یک نخ ورودی/خروجی است:
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { // Create a new coroutine to move the execution off the UI thread viewModelScope.launch(Dispatchers.IO) { val jsonBody = "{ username: \"$username\", token: \"$token\"}" loginRepository.makeLoginRequest(jsonBody) } } }
بیایید کد کوروتینها را در تابع login بررسی کنیم:
-
viewModelScopeیکCoroutineScopeاز پیش تعریف شده است که در افزونههایViewModelKTX گنجانده شده است. توجه داشته باشید که همه Coroutineها باید در یک scope اجرا شوند. یکCoroutineScopeیک یا چند Coroutine مرتبط را مدیریت میکند. -
launchتابعی است که یک کوروتین ایجاد میکند و اجرای بدنه تابع خود را به توزیعکننده مربوطه ارسال میکند. -
Dispatchers.IOنشان میدهد که این کوروتین باید روی نخی که برای عملیات ورودی/خروجی رزرو شده است، اجرا شود.
تابع login به صورت زیر اجرا میشود:
- برنامه تابع
loginرا از لایهViewدر thread اصلی فراخوانی میکند. -
launchیک کوروتین جدید ایجاد میکند و درخواست شبکه به طور مستقل روی نخی که برای عملیات ورودی/خروجی رزرو شده است، انجام میشود. - در حالی که کوروتین در حال اجرا است، تابع
loginبه اجرا ادامه میدهد و احتمالاً قبل از پایان درخواست شبکه، نتیجه را برمیگرداند. توجه داشته باشید که برای سادگی، پاسخ شبکه فعلاً نادیده گرفته میشود.
از آنجایی که این کوروتین با viewModelScope شروع شده است، در محدوده ViewModel اجرا میشود. اگر ViewModel به دلیل خارج شدن کاربر از صفحه نمایش از بین برود، viewModelScope به طور خودکار لغو میشود و تمام کوروتینهای در حال اجرا نیز لغو میشوند.
یکی از مشکلات مثال قبلی این است که هر چیزی که makeLoginRequest را فراخوانی میکند، باید به خاطر داشته باشد که اجرا را به طور صریح از نخ اصلی خارج کند. بیایید ببینیم چگونه میتوانیم Repository تغییر دهیم تا این مشکل را برای ما حل کند.
استفاده از کوروتینها برای ایمنی اصلی
ما یک تابع را زمانی main-safe در نظر میگیریم که بهروزرسانیهای رابط کاربری را در نخ اصلی مسدود نکند. تابع makeLoginRequest main-safe نیست، زیرا فراخوانی makeLoginRequest از نخ اصلی، رابط کاربری را مسدود میکند. از withContext() از کتابخانه coroutines برای انتقال اجرای یک کوروتین به نخ دیگری استفاده کنید:
class LoginRepository( // ... ) { // ... suspend fun makeLoginRequest( jsonBody: String ): Result<LoginResponse> { // Move the execution of the coroutine to the I/O dispatcher return withContext(Dispatchers.IO) { // Blocking network request code } } }
withContext(Dispatchers.IO) اجرای کوروتین را به یک نخ ورودی/خروجی منتقل میکند، تابع فراخوانی ما را از حالت main-safe خارج میکند و به رابط کاربری اجازه میدهد در صورت نیاز بهروزرسانی شود.
makeLoginRequest همچنین با کلمه کلیدی suspend مشخص شده است. این کلمه کلیدی روش کاتلین برای وادار کردن یک تابع به فراخوانی از داخل یک کوروتین است.
در مثال زیر، کوروتین در LoginViewModel ایجاد میشود. از آنجایی که makeLoginRequest اجرا را از نخ اصلی خارج میکند، کوروتین موجود در تابع login اکنون میتواند در نخ اصلی اجرا شود:
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { // Create a new coroutine on the UI thread viewModelScope.launch { val jsonBody = "{ username: \"$username\", token: \"$token\"}" // Make the network call and suspend execution until it finishes val result = loginRepository.makeLoginRequest(jsonBody) // Display result of the network request to the user when (result) { is Result.Success<LoginResponse> -> { /* Happy path */ } else -> { /* Show error in UI */ } } } } }
توجه داشته باشید که در اینجا هنوز به کوروتین نیاز است، زیرا makeLoginRequest یک تابع suspend است و همه توابع suspend باید در یک کوروتین اجرا شوند.
این کد از چند جهت با مثال login قبلی متفاوت است:
-
launchپارامترDispatchers.IOرا دریافت نمیکند. وقتیDispatcherبهlaunchارسال نمیکنید، هر کوروتینی که ازviewModelScopeاجرا شود، در نخ اصلی اجرا میشود. - اکنون نتیجه درخواست شبکه برای نمایش رابط کاربری موفقیت یا شکست مدیریت میشود.
تابع login اکنون به صورت زیر اجرا میشود:
- برنامه تابع
login()را از لایهViewدر نخ اصلی فراخوانی میکند. -
launchیک کوروتین جدید روی نخ اصلی ایجاد میکند و کوروتین شروع به اجرا میکند. - درون کوروتین، فراخوانی
loginRepository.makeLoginRequest()اکنون اجرای بیشتر کوروتین را تا زمان اتمام اجرای بلوکwithContextدرmakeLoginRequest()به حالت تعلیق در میآورد . - پس از اتمام بلوک
withContext، کوروتین موجود درlogin()اجرای خود را در نخ اصلی با نتیجه درخواست شبکه از سر میگیرد.
مدیریت استثنائات
برای مدیریت استثناهایی که لایه Repository میتواند ایجاد کند، از پشتیبانی داخلی کاتلین برای استثناها استفاده کنید. در مثال زیر، از یک بلوک try-catch استفاده میکنیم:
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { viewModelScope.launch { val jsonBody = "{ username: \"$username\", token: \"$token\"}" val result = try { loginRepository.makeLoginRequest(jsonBody) } catch (e: Exception) { Result.Error(Exception("Network request failed")) } when (result) { is Result.Success<LoginResponse> -> { /* Happy path */ } else -> { /* Show error in UI */ } } } } }
در این مثال، هر خطای غیرمنتظرهای که توسط فراخوانی makeLoginRequest() رخ دهد، به عنوان یک خطا در رابط کاربری مدیریت میشود.
منابع اضافی کوروتین
برای بررسی دقیقتر کوروتینها در اندروید، به بهبود عملکرد برنامه با کوروتینهای کاتلین مراجعه کنید.
برای منابع بیشتر در مورد کوروتینها، به لینکهای زیر مراجعه کنید:
- مرور کلی کوروتینها (JetBrains)
- راهنمای کوروتینها (JetBrains)
- منابع اضافی برای کوروتینها و جریان کاتلین