برنامه های کاتلین در اندروید

Coroutine یک الگوی طراحی همزمان است که می توانید در اندروید برای ساده کردن کدهایی که به صورت ناهمزمان اجرا می شوند استفاده کنید. Coroutine ها در نسخه 1.3 به Kotlin اضافه شدند و بر اساس مفاهیم تثبیت شده از زبان های دیگر هستند.

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

ویژگی ها

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

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

بررسی اجمالی نمونه ها

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

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

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

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

برای استفاده از کوروتین ها در پروژه اندروید خود، وابستگی زیر را به فایل build.gradle برنامه خود اضافه کنید:

Groovy

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

Kotlin

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 مراجعه کنید.

برای دریافت منابع بیشتر به پیوندهای زیر مراجعه کنید: