استفاده از کوروتین‌های کاتلین با کامپوننت‌های آگاه از چرخه عمر (Views)

مفاهیم و پیاده‌سازی Jetpack Compose

کوروتین‌های کاتلین یک API ارائه می‌دهند که به شما امکان می‌دهد کد ناهمزمان بنویسید. با کوروتین‌های کاتلین، می‌توانید یک CoroutineScope تعریف کنید که به شما کمک می‌کند زمان اجرای کوروتین‌های خود را مدیریت کنید. هر عملیات ناهمزمان در یک محدوده خاص اجرا می‌شود.

کامپوننت‌های آگاه از چرخه عمر، پشتیبانی درجه یکی از کوروتین‌ها را برای حوزه‌های منطقی در برنامه شما به همراه یک لایه قابلیت همکاری با LiveData ارائه می‌دهند. این مبحث نحوه استفاده مؤثر از کوروتین‌ها را با کامپوننت‌های آگاه از چرخه عمر توضیح می‌دهد.

وابستگی‌های KTX را اضافه کنید

محدوده‌های کوروتین داخلی که در این مبحث توضیح داده شده‌اند، در افزونه‌های KTX برای هر کامپوننت مربوطه موجود هستند. هنگام استفاده از این محدوده‌ها، حتماً وابستگی‌های مناسب را اضافه کنید.

  • برای ViewModelScope ، از androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 یا بالاتر استفاده کنید.
  • برای LifecycleScope ، androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 یا بالاتر استفاده کنید.
  • برای liveData ، از androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 یا بالاتر استفاده کنید.

محدوده‌های کوروتین آگاه از چرخه حیات

کامپوننت‌های آگاه از چرخه عمر، محدوده‌های داخلی زیر را تعریف می‌کنند که می‌توانید در برنامه خود از آنها استفاده کنید.

محدوده مدل نمایش

برای هر ViewModel در برنامه شما، یک ViewModelScope تعریف می‌شود. هر کوروتینی که در این scope راه‌اندازی شود، در صورت پاک شدن ViewModel به طور خودکار لغو می‌شود. کوروتین‌ها در اینجا برای زمانی مفید هستند که کاری دارید که فقط در صورت فعال بودن ViewModel باید انجام شود. به عنوان مثال، اگر در حال محاسبه برخی داده‌ها برای یک طرح‌بندی هستید، باید کار را به ViewModel محدود کنید تا اگر ViewModel پاک شد، کار به طور خودکار لغو شود تا از مصرف منابع جلوگیری شود.

شما می‌توانید از طریق ویژگی viewModelScope مربوط به ViewModel ، همانطور که در مثال زیر نشان داده شده است، به CoroutineScope مربوط به یک ViewModel دسترسی داشته باشید:

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}

محدوده چرخه حیات

برای هر شیء Lifecycle یک LifecycleScope تعریف می‌شود. هر کوروتینی که در این scope اجرا شود، با از بین رفتن Lifecycle لغو می‌شود. می‌توانید از طریق ویژگی‌های lifecycle.coroutineScope یا lifecycleOwner.lifecycleScope به CoroutineScope مربوط به Lifecycle دسترسی داشته باشید.

مثال زیر نحوه استفاده از lifecycleOwner.lifecycleScope را برای ایجاد متن از پیش محاسبه شده به صورت غیرهمزمان نشان می‌دهد:

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) {
                PrecomputedTextCompat.create(longTextContent, params)
            }
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        }
    }
}

کوروتین‌های آگاه از چرخه عمر با قابلیت راه‌اندازی مجدد

اگرچه lifecycleScope روشی مناسب برای لغو خودکار عملیات طولانی مدت هنگام DESTROYED Lifecycle ارائه می‌دهد، اما ممکن است موارد دیگری داشته باشید که بخواهید اجرای یک بلوک کد را هنگامی که Lifecycle در حالت خاصی است شروع کنید و هنگامی که در حالت دیگری است لغو کنید. به عنوان مثال، ممکن است بخواهید یک جریان را هنگامی که Lifecycle STARTED است جمع‌آوری کنید و هنگامی که STOPPED است، جمع‌آوری را لغو کنید. این رویکرد، انتشار جریان را فقط زمانی که رابط کاربری روی صفحه قابل مشاهده است پردازش می‌کند و در منابع صرفه‌جویی می‌کند و به طور بالقوه از خرابی برنامه جلوگیری می‌کند.

برای این موارد، Lifecycle و LifecycleOwner رابط برنامه‌نویسی کاربردی suspend repeatOnLifecycle را ارائه می‌دهند که دقیقاً همین کار را انجام می‌دهد. مثال زیر شامل یک بلوک کد است که هر بار که Lifecycle مرتبط حداقل در حالت STARTED باشد، اجرا می‌شود و هنگامی که Lifecycle STOPPED ، لغو می‌شود:

class MyFragment : Fragment() {

    val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Create a new coroutine in the lifecycleScope
        viewLifecycleOwner.lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // This happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                viewModel.someDataFlow.collect {
                    // Process item
                }
            }
        }
    }
}

جمع‌آوری جریان آگاه از چرخه حیات

اگر فقط نیاز به انجام جمع‌آوری آگاهانه از چرخه حیات روی یک جریان داده دارید، می‌توانید از متد Flow.flowWithLifecycle() برای ساده‌سازی کد خود استفاده کنید:

viewLifecycleOwner.lifecycleScope.launch {
    exampleProvider.exampleFlow()
        .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        .collect {
            // Process the value.
        }
}

با این حال، اگر نیاز دارید که جمع‌آوری آگاهانه از چرخه حیات را روی چندین جریان به صورت موازی انجام دهید، باید هر جریان را در کوروتین‌های مختلف جمع‌آوری کنید. در این صورت، استفاده مستقیم repeatOnLifecycle() کارآمدتر است:

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        // Because collect is a suspend function, if you want to
        // collect multiple flows in parallel, you need to do so in
        // different coroutines.
        launch {
            flow1.collect { /* Process the value. */ }
        }

        launch {
            flow2.collect { /* Process the value. */ }
        }
    }
}

تعلیق کوروتین‌های آگاه از چرخه حیات

اگرچه CoroutineScope روشی مناسب برای لغو خودکار عملیات طولانی مدت ارائه می‌دهد، اما ممکن است موارد دیگری داشته باشید که بخواهید اجرای یک بلوک کد را به حالت تعلیق درآورید، مگر اینکه Lifecycle در وضعیت خاصی باشد. به عنوان مثال، برای اجرای یک FragmentTransaction ، باید صبر کنید تا Lifecycle حداقل STARTED شود. برای این موارد، Lifecycle متدهای اضافی ارائه می‌دهد: lifecycle.whenCreated, lifecycle.whenStarted و lifecycle.whenResumed . هر کوروتینی که درون این بلوک‌ها اجرا شود، در صورتی که Lifecycle حداقل در وضعیت مطلوب نباشد، به حالت تعلیق در می‌آید.

مثال زیر شامل یک بلوک کد است که فقط زمانی اجرا می‌شود که Lifecycle مرتبط حداقل در حالت STARTED ) باشد:

class MyFragment: Fragment {
    init { // Notice that we can safely launch in the constructor of the Fragment.
        lifecycleScope.launch {
            whenStarted {
                // The block inside will run only when Lifecycle is at least STARTED.
                // It will start executing when fragment is started and
                // can call other suspend methods.
                loadingView.visibility = View.VISIBLE
                val canAccess = withContext(Dispatchers.IO) {
                    checkUserAccess()
                }

                // When checkUserAccess returns, the next line is automatically
                // suspended if the Lifecycle is not *at least* STARTED.
                // We could safely run fragment transactions because we know the
                // code won't run unless the lifecycle is at least STARTED.
                loadingView.visibility = View.GONE
                if (canAccess == false) {
                    findNavController().popBackStack()
                } else {
                    showContent()
                }
            }

            // This line runs only after the whenStarted block above has completed.

        }
    }
}

اگر Lifecycle در حین فعال بودن یک کوروتین از طریق یکی از متدهای when از بین برود، کوروتین به طور خودکار لغو می‌شود. در مثال زیر، بلوک finally پس از DESTROYED وضعیت Lifecycle اجرا می‌شود:

class MyFragment: Fragment {
    init {
        lifecycleScope.launchWhenStarted {
            try {
                // Call some suspend functions.
            } finally {
                // This line might execute after Lifecycle is DESTROYED.
                if (lifecycle.state >= STARTED) {
                    // Here, since we've checked, it is safe to run any
                    // Fragment transactions.
                }
            }
        }
    }
}

استفاده از کوروتین‌ها با LiveData

هنگام استفاده از LiveData ، ممکن است نیاز به محاسبه مقادیر به صورت غیرهمزمان داشته باشید. برای مثال، ممکن است بخواهید تنظیمات کاربر را بازیابی کرده و آنها را به رابط کاربری خود ارائه دهید. در این موارد، می‌توانید از تابع سازنده liveData برای فراخوانی یک تابع suspend استفاده کنید و نتیجه را به عنوان یک شیء LiveData ارائه دهید.

در مثال زیر، loadUser() یک تابع suspend است که در جای دیگری تعریف شده است. از تابع liveData builder برای فراخوانی loadUser() به صورت غیرهمزمان استفاده کنید و سپس emit() برای انتشار نتیجه استفاده کنید:

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

بلوک سازنده‌ی liveData به عنوان یک عنصر اولیه‌ی همزمانی ساختاریافته بین کوروتین‌ها و LiveData عمل می‌کند. این بلوک کد با فعال شدن LiveData شروع به اجرا می‌کند و پس از یک زمان‌بندی قابل تنظیم، زمانی که LiveData غیرفعال می‌شود، به طور خودکار لغو می‌شود. اگر قبل از اتمام لغو شود، در صورتی که LiveData دوباره فعال شود، مجدداً راه‌اندازی می‌شود. اگر در اجرای قبلی با موفقیت تکمیل شده باشد، مجدداً راه‌اندازی نمی‌شود. توجه داشته باشید که فقط در صورت لغو خودکار مجدداً راه‌اندازی می‌شود. اگر بلوک به هر دلیل دیگری (مثلاً ایجاد CancellationException ) لغو شود، مجدداً راه‌اندازی نمی‌شود .

همچنین می‌توانید چندین مقدار را از بلوک ارسال کنید. هر فراخوانی emit() اجرای بلوک را تا زمانی که مقدار LiveData در نخ اصلی تنظیم شود، به حالت تعلیق در می‌آورد.

val user: LiveData<Result> = liveData {
    emit(Result.loading())
    try {
        emit(Result.success(fetchUser()))
    } catch(ioException: Exception) {
        emit(Result.error(ioException))
    }
}

همچنین می‌توانید liveData با Transformations ترکیب کنید، همانطور که در مثال زیر نشان داده شده است:

class MyViewModel: ViewModel() {
    private val userId: LiveData<String> = MutableLiveData()
    val user = userId.switchMap { id ->
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(database.loadUserById(id))
        }
    }
}

شما می‌توانید با فراخوانی تابع emitSource() هر زمان که می‌خواهید مقدار جدیدی را منتشر کنید، چندین مقدار را از یک LiveData منتشر کنید. توجه داشته باشید که هر فراخوانی emit() یا emitSource() منبع اضافه شده قبلی را حذف می‌کند.

class UserDao: Dao {
    @Query("SELECT * FROM User WHERE id = :id")
    fun getUser(id: String): LiveData<User>
}

class MyRepository {
    fun getUser(id: String) = liveData<User> {
        val disposable = emitSource(
            userDao.getUser(id).map {
                Result.loading(it)
            }
        )
        try {
            val user = webservice.fetchUser(id)
            // Stop the previous emission to avoid dispatching the updated user
            // as `loading`.
            disposable.dispose()
            // Update the database.
            userDao.insert(user)
            // Re-establish the emission with success type.
            emitSource(
                userDao.getUser(id).map {
                    Result.success(it)
                }
            )
        } catch(exception: IOException) {
            // Any call to `emit` disposes the previous one automatically so we don't
            // need to dispose it here as we didn't get an updated value.
            emitSource(
                userDao.getUser(id).map {
                    Result.error(exception, it)
                }
            )
        }
    }
}

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

منابع اضافی

برای کسب اطلاعات بیشتر در مورد استفاده از کوروتین‌ها با کامپوننت‌های آگاه از چرخه عمر، به منابع اضافی زیر مراجعه کنید.

نمونه‌ها

وبلاگ‌ها