Coroutine یک الگوی طراحی همزمان است که می توانید در اندروید برای ساده کردن کدهایی که به صورت ناهمزمان اجرا می شوند استفاده کنید. Coroutine ها در نسخه 1.3 به Kotlin اضافه شدند و بر اساس مفاهیم تثبیت شده از زبان های دیگر هستند.
در Android، برنامههای معمول به مدیریت کارهای طولانیمدت کمک میکنند که در غیر این صورت ممکن است رشته اصلی را مسدود کرده و باعث شود برنامه شما پاسخگو نباشد. بیش از 50 درصد از توسعه دهندگان حرفه ای که از کوروتین ها استفاده می کنند گزارش کرده اند که افزایش بهره وری را مشاهده کرده اند. این مبحث توضیح میدهد که چگونه میتوانید از کوروتینهای Kotlin برای رسیدگی به این مشکلات استفاده کنید، و به شما امکان میدهد کد برنامه را تمیزتر و مختصرتر بنویسید.
ویژگی ها
Coroutines راه حل پیشنهادی ما برای برنامه نویسی ناهمزمان در اندروید است. از ویژگی های قابل توجه می توان به موارد زیر اشاره کرد:
- سبک وزن : به دلیل پشتیبانی از تعلیق ، میتوانید بسیاری از کوروتینها را روی یک رشته اجرا کنید، که باعث مسدود شدن رشتهای نمیشود که کوروتین در حال اجراست. تعلیق باعث صرفه جویی در حافظه بیش از مسدود شدن می شود در حالی که از بسیاری از عملیات همزمان پشتیبانی می کند.
- نشت حافظه کمتر : از همزمانی ساختاریافته برای اجرای عملیات در یک محدوده استفاده کنید.
- پشتیبانی داخلی لغو : لغو به طور خودکار از طریق سلسله مراتب کوروتین در حال اجرا منتشر می شود.
- یکپارچهسازی 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()
را فراخوانی کند، که باعث می شود برنامه شما ثابت شود و به طور بالقوه به یک گفتگوی Application Not Responding (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>()
}
class LoginRepository(private val responseParser: LoginResponseParser) {
private const val loginUrl = "https://example.com/login"
// 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)
}
}
}
بیایید کد coroutines را در تابع login
تجزیه کنیم:
-
viewModelScope
یکCoroutineScope
از پیش تعریف شده است که با افزونه هایViewModel
KTX گنجانده شده است. توجه داشته باشید که همه کوروتین ها باید در یک محدوده اجرا شوند. یکCoroutineScope
یک یا چند کوروتین مرتبط را مدیریت می کند. -
launch
تابعی است که یک coroutine ایجاد می کند و اجرای بدنه تابع خود را به توزیع کننده مربوطه ارسال می کند. -
Dispatchers.IO
نشان می دهد که این coroutine باید روی رشته ای که برای عملیات I/O رزرو شده است اجرا شود.
تابع login
به صورت زیر اجرا می شود:
- برنامه
login
را از لایهView
در رشته اصلی فراخوانی می کند. -
launch
یک Coroutine جدید ایجاد می کند و درخواست شبکه به طور مستقل در یک رشته رزرو شده برای عملیات I/O انجام می شود. - در حالی که برنامه در حال اجرا است، عملکرد
login
به اجرا ادامه میدهد و احتمالاً قبل از اتمام درخواست شبکه، برمیگردد. توجه داشته باشید که برای سادگی، پاسخ شبکه در حال حاضر نادیده گرفته شده است.
از آنجایی که این برنامه با viewModelScope
شروع می شود، در محدوده ViewModel
اجرا می شود. اگر ViewModel
به دلیل دور شدن کاربر از صفحه از بین برود، viewModelScope
به طور خودکار لغو میشود و تمام برنامههای در حال اجرا نیز لغو میشوند.
یکی از مشکلات مثال قبلی این است که هر چیزی که makeLoginRequest
را فراخوانی می کند باید به خاطر داشته باشد که به صراحت اجرا را از رشته اصلی خارج کند. بیایید ببینیم چگونه می توانیم Repository
را تغییر دهیم تا این مشکل برای ما حل شود.
برای ایمنی اصلی از کوروتین ها استفاده کنید
زمانی که بهروزرسانیهای رابط کاربری را در رشته اصلی مسدود نمیکند، عملکردی را به عنوان اصلی امن در نظر میگیریم. تابع makeLoginRequest
ایمن اصلی نیست، زیرا فراخوانی makeLoginRequest
از رشته اصلی، رابط کاربری را مسدود می کند. از تابع withContext()
از کتابخانه coroutines برای انتقال اجرای یک coroutine به یک رشته دیگر استفاده کنید:
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)
اجرای کوروتین را به یک رشته ورودی/خروجی منتقل میکند و تابع فراخوانی ما را ایمن میکند و رابط کاربر را قادر میسازد تا در صورت نیاز بهروزرسانی شود.
makeLoginRequest
نیز با کلمه کلیدی suspend
مشخص شده است. این کلمه کلیدی راه کاتلین برای اعمال یک تابع برای فراخوانی از داخل یک کوروتین است.
در مثال زیر، Coroutine در 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
}
}
}
}
توجه داشته باشید که Coroutine هنوز در اینجا مورد نیاز است، زیرا makeLoginRequest
یک تابع suspend
است و همه توابع suspend
باید در یک coroutine اجرا شوند.
این کد از چند جهت با مثال login
قبلی متفاوت است:
-
launch
پارامترDispatchers.IO
را نمی گیرد. وقتی برایlaunch
از یکDispatcher
عبور نمیکنید، هر برنامهای که ازviewModelScope
راهاندازی میشود در رشته اصلی اجرا میشود. - اکنون نتیجه درخواست شبکه برای نمایش رابط کاربری موفقیت یا شکست مدیریت می شود.
اکنون تابع ورود به سیستم به صورت زیر اجرا می شود:
- برنامه
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()
پرتاب شود، به عنوان یک خطا در رابط کاربری مدیریت می شود.
منابع اضافی کوروتین
برای نگاه دقیقتر به کوروتینها در Android، به بهبود عملکرد برنامه با کوروتینهای Kotlin مراجعه کنید.
برای دریافت منابع بیشتر به پیوندهای زیر مراجعه کنید:
- مروری بر کوروتین ها (JetBrains)
- راهنمای Coroutines (JetBrains)
- منابع اضافی برای کوروتین ها و جریان کاتلین