روال‌های مشترک Kotlin در Android

روال همکار یک الگوی طراحی هم‌زمان است که می‌توانید در Android از آن برای ساده کردن کدی که به‌صورت ناهم‌زمان اجرا می‌شود استفاده کنید. روال‌های همکار در نسخه ۱.۳ به Kotlin اضافه شد و براساس مفاهیم ثابت‌شده از زبان‌های دیگر است.

در Android، روتین‌های همکار به مدیریت تکلیف‌های طولانی‌مدت کمک می‌کنند که درغیراین‌صورت ممکن است رشته اصلی را مسدود کنند و باعث شوند برنامه شما پاسخ‌گو نباشد. بیش‌از ۵۰٪ از توسعه‌دهندگان حرفه‌ای که از روتین‌های همکار استفاده می‌کنند گزارش کرده‌اند که بهره‌وری‌شان افزایش یافته است. این موضوع توضیح می‌دهد که چگونه می‌توانید از روال‌های هم‌زمان Kotlin برای پرداختن به این مشکلات استفاده کنید و به شما امکان می‌دهد کد برنامه پاک‌تر و مختصرتری بنویسید.

ویژگی‌ها

‫Coroutines راه‌حل پیشنهادی ما برای برنامه‌نویسی ناهمگام در Android است. ویژگی‌های قابل‌توجه شامل موارد زیر است:

  • سبک‌وزن: به‌دلیل پشتیبانی از تعلیق، می‌توانید بسیاری از روتین‌های همکار را در یک رشته اجرا کنید، که رشته‌ای را که روتین همکار در آن اجرا می‌شود مسدود نمی‌کند. تعلیق کردن در مقایسه با مسدود کردن، حافظه را ذخیره می‌کند و درعین‌حال از عملیات هم‌زمان زیادی پشتیبانی می‌کند.
  • نشت حافظه کمتر: از هم‌زمان‌سازی ساختاری برای اجرای عملیات در یک محدوده استفاده کنید.
  • پشتیبانی لغو داخلی: لغو به‌طور خودکار ازطریق سلسله‌مراتب روتین همکار درحال اجرا منتشر می‌شود.
  • یکپارچه‌سازی Jetpack: بسیاری از کتابخانه‌های Jetpack شامل افزونه‌هایی هستند که از روال‌های مشترک به‌طور کامل پشتیبانی می‌کنند. برخی‌از کتابخانه‌ها نیز دامنه روتین همکار خودشان را ارائه می‌دهند که می‌توانید از آن برای هم‌زمان‌سازی ساختاریافته استفاده کنید.

نمای کلی مثال‌ها

براساس راهنمای معماری برنامه، نمونه‌های این موضوع درخواست شبکه ایجاد می‌کنند و نتیجه را به رشته اصلی برمی‌گردانند، جایی که برنامه می‌تواند نتیجه را به کاربر نمایش دهد.

به‌طور دقیق، ViewModel عنصر «معماری» لایه مخزن را در رشته اصلی فرا می‌خواند تا درخواست شبکه را راه‌اندازی کند. این راهنما راه‌حل‌های مختلفی را که از روتین‌های همکار برای مسدود نشدن رشته اصلی استفاده می‌کنند تکرار می‌کند.

‫ViewModel شامل مجموعه‌ای از افزونه‌های KTX است که مستقیماً با روال‌های همکار کار می‌کنند. این افزونه‌ها کتابخانه lifecycle-viewmodel-ktx هستند و در این راهنما استفاده می‌شوند.

اطلاعات وابستگی

برای استفاده از روتین‌های همکار در پروژه Android، وابستگی زیر را به فایل 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 ازپیش‌تعریف‌شده است که در افزونه‌های ViewModel KTX گنجانده شده است. توجه داشته باشید که همه روتین‌های همکار باید در یک محدوده اجرا شوند. CoroutineScope یک یا چند روتین همکار مرتبط را مدیریت می‌کند.
  • ‫launch تابعی است که یک روتین همکار ایجاد می‌کند و اجرای بدنه تابع آن را به توزیع‌کننده مربوطه ارسال می‌کند.
  • ‫Dispatchers.IO نشان می‌دهد که این روتین هم‌زمان باید در رشته‌ای که برای عملیات ورودی/خروجی رزرو شده است اجرا شود.

تابع login به‌صورت زیر اجرا می‌شود:

  • برنامه تابع login را از لایه View در رشته اصلی فراخوانی می‌کند.
  • ‫launch یک روال همکار جدید ایجاد می‌کند و درخواست شبکه به‌طور مستقل در رشته‌ای که برای عملیات ورودی/خروجی رزرو شده است انجام می‌شود.
  • درحالی‌که روتین هم‌زمان درحال اجرا است، تابع login به اجرا ادامه می‌دهد و برمی‌گردد، احتمالاً قبل‌از اینکه درخواست شبکه تمام شود. توجه داشته باشید که برای ساده‌تر شدن، پاسخ شبکه فعلاً نادیده گرفته می‌شود.

ازآنجایی‌که این روتین هم‌زمان با viewModelScope شروع می‌شود، در محدوده ViewModel اجرا می‌شود. اگر ViewModel به‌دلیل اینکه کاربر از صفحه خارج می‌شود ازبین برود، viewModelScope به‌طور خودکار لغو می‌شود و همه روتین‌های هم‌زمان درحال اجرا نیز لغو می‌شوند.

یکی از مشکلات مثال قبلی این است که هر چیزی که makeLoginRequest را فراخوانی می‌کند باید به‌خاطر داشته باشد که اجرا را به‌طور صریح از رشته اصلی خارج کند. بیایید ببینیم چگونه می‌توانیم Repository را تغییر دهیم تا این مشکل را برایمان حل کند.

استفاده از روتین‌های همکار برای ایمنی اصلی

وقتی تابعی به‌روزرسانی‌های میانای کاربر را در رشته اصلی مسدود نکند، آن را امن برای اصلی درنظر می‌گیریم. تابع makeLoginRequest ایمن برای رشته اصلی نیست، زیرا فراخوانی makeLoginRequest از رشته اصلی باعث مسدود شدن میانای کاربری می‌شود. از تابع withContext() در کتابخانه روتین‌های همکار برای انتقال اجرای روتین همکار به رشته‌ای دیگر استفاده کنید:

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 نیز علامت‌گذاری شده است. این کلیدواژه روش Kotlin برای اجبار کردن تابع برای فراخوانی از درون یک روتین همکار است.

در مثال زیر، روتین همکار در 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() را از لایه View در رشته اصلی فراخوانی می‌کند.
  • ‫launch روتین همکار جدیدی در رشته اصلی ایجاد می‌کند و روتین همکار اجرا را شروع می‌کند.
  • درون روتین همکار، فراخوانی loginRepository.makeLoginRequest() اکنون اجرای بیشتر روتین همکار را تا زمانی که withContext بلوک در makeLoginRequest() اجرای آن تمام شود به‌تعلیق درمی‌آورد.
  • وقتی بلوک withContext تمام شد، اجرای روتین همکار در login() در رشته اصلی با نتیجه درخواست شبکه ازسر گرفته می‌شود.

مدیریت استثناها

برای مدیریت استثناهایی که لایه Repository می‌تواند ایجاد کند، از پشتیبانی داخلی Kotlin برای استثناها استفاده کنید. در مثال زیر، از یک بلوک 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 مراجعه کنید.

برای منابع بیشتر مربوط به روتین‌های همکار، پیوندهای زیر را ببینید: