Kotlin coroutines-এর সাহায্যে অ্যাপ পারফর্ম্যান্স উন্নত করা

Kotlin কোরাউটিন আপনাকে পরিষ্কার, সরলীকৃত অ্যাসিঙ্ক্রোনাস কোড লিখতে সাহায্য করে যা নেটওয়ার্ক কল বা ডিস্ক অপারেশনের মতো দীর্ঘ-সময় ধরে চলতে থাকা টাস্ক ম্যানেজ করার সময়ও আপনার অ্যাপকে রেসপন্সিভ রাখে।

এই বিষয় Android-এ কোরাউটিন সম্পর্কে বিস্তারিত তথ্য প্রদান করে। আপনি কোরাউটিন সম্পর্কে অপরিচিত হলে, এই বিষয় পড়ার আগে Android-এ Kotlin কোরাউটিন সম্পর্কে পড়তে ভুলবেন না।

দীর্ঘ সময় ধরে চলা টাস্ক ম্যানেজ করা

দীর্ঘক্ষণ ধরে চলতে থাকা টাস্ক ম্যানেজ করার জন্য দুটি অপারেশন যোগ করে কোরাউটিন সাধারণ ফাংশনের উপর ভিত্তি করে তৈরি করা হয়। invoke (বা call) ও return ছাড়াও, করুটিন suspend ও resume যোগ করে:

  • suspend বর্তমান কোরাউটিনের এক্সিকিউশন পজ করে এবং সব লোকাল ভেরিয়েবল সেভ করে।
  • resume কোনও সাসপেন্ড করা কোরাউটিনকে সেই জায়গা থেকেই এক্সিকিউট করা চালিয়ে যায় যেখানে সেটি সাসপেন্ড করা হয়েছিল।

আপনি শুধুমাত্র অন্য suspend ফাংশন থেকে suspend ফাংশন কল করতে পারবেন অথবা নতুন কোরাউটিন শুরু করার জন্য launch-এর মতো কোরাউটিন বিল্ডার ব্যবহার করতে পারবেন।

নিম্নলিখিত উদাহরণে একটি কাল্পনিক দীর্ঘমেয়াদী টাস্কের জন্য সহজ কোরাউটিন প্রয়োগ দেখানো হয়েছে:

suspend fun fetchDocs() { // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result) // Dispatchers.Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) {
    /* ... */
}

এই উদাহরণে, get() এখনও মূল থ্রেডে রান করে, কিন্তু এটি নেটওয়ার্ক অনুরোধ শুরু করার আগে করুটিন সাসপেন্ড করে। নেটওয়ার্ক অনুরোধ সম্পূর্ণ হলে, get মূল থ্রেডকে জানানোর জন্য কলব্যাক ব্যবহার করার পরিবর্তে সাসপেন্ড করা কোরাউটিন আবার চালু করে।

কোন ফাংশন চলছে তা ম্যানেজ করার জন্য Kotlin স্ট্যাক ফ্রেম ব্যবহার করে এর সাথে কোনও লোকাল ভেরিয়েবলও থাকে। কোনও কোরাউটিন সাসপেন্ড করার সময়, বর্তমান স্ট্যাক ফ্রেম কপি করে পরে ব্যবহারের জন্য সেভ করা হয়। আবার চালু করার সময়, স্ট্যাক ফ্রেমটি যেখানে সেভ করা হয়েছিল সেখান থেকে কপি করা হয় এবং ফাংশনটি আবার চলতে শুরু করে। কোডটি সাধারণ সিকোয়েন্সিয়াল ব্লকিং অনুরোধের মতো মনে হলেও, কোরাউটিন নিশ্চিত করে যে নেটওয়ার্ক অনুরোধটি মূল থ্রেডকে ব্লক করা এড়িয়ে চলে।

থ্রেড-সেফটির জন্য কোরাউটিন ব্যবহার করা

কোন থ্রেড কোরাউটিন এক্সিকিউশনের জন্য ব্যবহার করা হবে তা নির্ধারণ করতে Kotlin কোরাউটিন ডিসপ্যাচার ব্যবহার করে। মূল থ্রেডের বাইরে কোড চালানোর জন্য, আপনি Kotlin করুটিনকে Default বা IO ডিসপ্যাচার ব্যবহার করে কাজ করতে বলতে পারেন। Kotlin-এ সব কোরাউটিনকে অবশ্যই একটি ডিসপ্যাচারে রান করতে হবে, এমনকি সেগুলি যখন মূল থ্রেডে রান করছে তখনও। কোরুটিন নিজেকে সাসপেন্ড করতে পারে এবং ডিসপ্যাচার সেগুলিকে আবার চালু করার জন্য দায়ী।

কোথায় কোরুটিন রান করবে তা নির্দিষ্ট করতে, Kotlin তিনটি ডিসপ্যাচার প্রদান করে যা আপনি ব্যবহার করতে পারবেন:

  • Dispatchers.Main - Android-এর মূল থ্রেডে কোরাউটিন রান করাতে এই ডিসপ্যাচার ব্যবহার করুন। এটি শুধুমাত্র UI-এর সাথে ইন্টার‍্যাক্ট করা এবং দ্রুত কাজ করার জন্য ব্যবহার করা উচিত। suspend ফাংশন কল করা, Android UI ফ্রেমওয়ার্ক অপারেশন চালানো এবং LiveData অবজেক্ট আপডেট করার মতো উদাহরণ।
  • Dispatchers.IO - এই ডিসপ্যাচারটি মূল থ্রেডের বাইরে ডিস্ক বা নেটওয়ার্ক I/O পারফর্ম করার জন্য অপ্টিমাইজ করা হয়েছে। যেমন, এর মধ্যে রয়েছে রুম কম্পোনেন্ট ব্যবহার করা, ফাইল থেকে পড়া বা ফাইলে লেখা এবং যেকোনও নেটওয়ার্ক অপারেশন চালানো।
  • Dispatchers.Default - এই ডিসপ্যাচারটি মূল থ্রেডের বাইরে CPU-ইনটেনসিভ কাজ করার জন্য অপ্টিমাইজ করা হয়েছে। ব্যবহারের উদাহরণের মধ্যে রয়েছে তালিকা সাজানো এবং JSON পার্স করা।

আগের উদাহরণটি চালিয়ে গেলে, আপনি ডিসপ্যাচার ব্যবহার করে get ফাংশনটি আবার নতুন করে ডিফাইন করতে পারবেন। get-এর বডির মধ্যে, IO থ্রেড পুলে রান করে এমন ব্লক তৈরি করতে withContext(Dispatchers.IO)-কে কল করুন। আপনি সেই ব্লকের মধ্যে যে কোডই রাখুন না কেন, সেটি সবসময় IO ডিসপ্যাচারের মাধ্যমে এক্সিকিউট হয়। যেহেতু withContext নিজেই একটি সাসপেন্ড ফাংশন, তাই get ফাংশনটিও একটি সাসপেন্ড ফাংশন।

suspend fun fetchDocs() { // Dispatchers.Main
    val result = get("developer.android.com") // Dispatchers.Main
    show(result) // Dispatchers.Main
}

suspend fun get(url: String) = // Dispatchers.Main
    withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
        /* perform network IO here */ // Dispatchers.IO (main-safety block)
    } // Dispatchers.Main

কোরুটিনের সাহায্যে, আপনি ফাইন-গ্রেন কন্ট্রোল সহ থ্রেড ডিসপ্যাচ করতে পারবেন। কারণ, withContext() আপনাকে কলব্যাক যোগ না করেই যেকোনও কোডের থ্রেড পুল কন্ট্রোল করতে দেয়। আপনি এটি ডেটাবেস থেকে রিড করা বা নেটওয়ার্ক অনুরোধের মতো খুব ছোট ফাংশনে প্রয়োগ করতে পারেন। ভাল প্র্যাক্টিস হল, প্রতিটি ফাংশন main-safe কিনা তা নিশ্চিত করতে withContext() ব্যবহার করা। এর অর্থ হল, আপনি মূল থ্রেড থেকে ফাংশন কল করতে পারবেন। এইভাবে, ফাংশনটি এক্সিকিউট করার জন্য কোন থ্রেড ব্যবহার করা উচিত তা কলারকে কখনও ভাবতে হবে না।

আগের উদাহরণে, fetchDocs() প্রধান থ্রেডে এক্সিকিউট হয়; তবে, এটি নিরাপদে get-কে কল করতে পারে, যা ব্যাকগ্রাউন্ডে নেটওয়ার্ক অনুরোধ পারফর্ম করে। কারণ কোরাউটিন suspend ও resume-এ কাজ করে, তাই withContext ব্লকটি সম্পূর্ণ হওয়ার সাথে সাথেই get ফলাফল সহ মূল থ্রেডে কোরাউটিন আবার চালু করা হয়।

withContext()-এর পারফর্ম্যান্স

withContext() তুলনামূলক কলব্যাক-ভিত্তিক ইমপ্লিমেন্টেশনের তুলনায় অতিরিক্ত ওভারহেড যোগ করে না। এছাড়াও, কিছু পরিস্থিতিতে, কলব্যাক-ভিত্তিক প্রয়োগের সমতুল্য withContext() কল অপ্টিমাইজ করা সম্ভব। যেমন, কোনও ফাংশন যদি নেটওয়ার্কে দশটি কল করে, তাহলে আপনি Kotlin-কে বাইরের withContext() ব্যবহার করে থ্রেড একবারই পরিবর্তন করতে বলতে পারেন। তারপরে, নেটওয়ার্ক লাইব্রেরি withContext() একাধিকবার ব্যবহার করলেও, এটি একই ডিসপ্যাচারেই থাকে এবং থ্রেড পরিবর্তন করা এড়ায়। এছাড়াও, Kotlin, থ্রেড সুইচিং এড়াতে যখনই সম্ভব Dispatchers.Default ও Dispatchers.IO-এর মধ্যে সুইচিং অপ্টিমাইজ করে।

করুটিন শুরু করা

আপনি দুটি উপায়ের মধ্যে একটি উপায়ে কোরাউটিন শুরু করতে পারবেন:

  • launch নতুন কোরাউটিন শুরু করে এবং কলারকে ফলাফল ফেরত দেয় না। launch ব্যবহার করে "ফায়ার অ্যান্ড ফরগেট" হিসেবে বিবেচিত যেকোনও কাজ শুরু করা যেতে পারে।
  • async নতুন কোরাউটিন শুরু করে এবং await নামের সাসপেন্ড ফাংশন কল করে ফলাফল রিটার্ন করার অনুমতি দেয়।

সাধারণত, আপনাকে একটি সাধারণ ফাংশন থেকে একটি নতুন কোরাউটিন launch করতে হবে, কারণ একটি সাধারণ ফাংশন await কল করতে পারে না। async শুধুমাত্র অন্য কোনও কোরাউটিনের মধ্যে অথবা সাসপেন্ড ফাংশনের মধ্যে থাকলে এবং প্যারালাল ডিকম্পোজিশন পারফর্ম করলে ব্যবহার করুন।

প্যারালাল ডিকম্পোজিশন

suspend ফাংশনের মধ্যে শুরু করা সবকটি কোরাউটিন সেই ফাংশন রিটার্ন করার সময় অবশ্যই বন্ধ করতে হবে, তাই আপনাকে সম্ভবত এটি নিশ্চিত করতে হবে যে সেইসব কোরাউটিন রিটার্ন করার আগে শেষ হয়ে গেছে। Kotlin-এ স্ট্রাকচার্ড কনকারেন্সি-এর সাহায্যে, আপনি এমন coroutineScope নির্ধারণ করতে পারবেন যা এক বা একাধিক কোরাউটিন শুরু করে। তারপর, await() (একটি কোরাউটিনের জন্য) বা awaitAll() (একাধিক কোরাউটিনের জন্য) ব্যবহার করে, আপনি নিশ্চিত করতে পারবেন যে ফাংশন থেকে রিটার্ন করার আগে এইসব কোরাউটিন শেষ হয়ে যাবে।

উদাহরণস্বরূপ, আসুন আমরা এমন একটি coroutineScope নির্ধারণ করি যা দুটি ডকুমেন্ট অ্যাসিঙ্ক্রোনাসভাবে ফেচ করে। প্রতিটি ডিফার করা রেফারেন্সে await() কল করে, আমরা এটি নিশ্চিত করি যে ভ্যালু রিটার্ন করার আগে async অপারেশন সম্পূর্ণ হয়:

suspend fun fetchTwoDocs() =
    coroutineScope {
        val deferredOne = async { fetchDoc(1) }
        val deferredTwo = async { fetchDoc(2) }
        deferredOne.await()
        deferredTwo.await()
    }

এছাড়াও, আপনি নিম্নলিখিত উদাহরণে দেখানো পদ্ধতিতে awaitAll() সংগ্রহে ব্যবহার করতে পারবেন:

suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf( // fetch two docs at the same time
            async { fetchDoc(1) }, // async returns a result for the first doc
            async { fetchDoc(2) } // async returns a result for the second doc
        )
        deferreds.awaitAll() // use awaitAll to wait for both network requests
    }

fetchTwoDocs() async-এর সাথে নতুন কোরাউটিন লঞ্চ করলেও, ফাংশনটি awaitAll() ব্যবহার করে লঞ্চ করা কোরাউটিন সম্পূর্ণ হওয়ার জন্য অপেক্ষা করে, তারপরে রিটার্ন করে। তবে, মনে রাখবেন, আমরা awaitAll() কল না করলেও, সব নতুন কোরাউটিন সম্পূর্ণ না হওয়া পর্যন্ত coroutineScope বিল্ডার, যে কোরাউটিন কল করেছে fetchTwoDocs সেটি আবার চালু করে না।

এছাড়াও, coroutineScope কোরাউটিন থেকে পাওয়া যেকোনও ব্যতিক্রমী ঘটনা ক্যাচ করে এবং সেগুলিকে কলারের কাছে ফেরত পাঠায়।

প্যারালাল ডিকম্পোজিশন সম্পর্কে আরও জানতে, সাসপেন্ড করা ফাংশন কম্পোজ করা দেখুন।

Coroutines-এর ধারণা

CoroutineScope

CoroutineScope launch বা async ব্যবহার করে তৈরি করা যেকোনও কোরাউটিনের ট্র্যাক রাখে। চলমান কাজ (অর্থাৎ, রানিং কোরাউটিন) যেকোনও সময় scope.cancel() কল করে বাতিল করা যেতে পারে। Android-এ, কিছু KTX লাইব্রেরি নির্দিষ্ট লাইফসাইকেল ক্লাসের জন্য নিজস্ব CoroutineScope প্রদান করে। যেমন, ViewModel-এর viewModelScope আছে, এবং Lifecycle-এর lifecycleScope আছে। তবে, ডিসপ্যাচারের মতো, CoroutineScope কোরাউটিন রান করে না।

viewModelScope-এর ব্যবহার কোরুটিন সহ Android-এ ব্যাকগ্রাউন্ড থ্রেডিং-এ পাওয়া উদাহরণেও দেখা যায়। তবে, আপনার অ্যাপের কোনও নির্দিষ্ট লেয়ারে কোরাউটিনের লাইফসাইকেল কন্ট্রোল করার জন্য নিজের CoroutineScope তৈরি করতে চাইলে, আপনি নিম্নলিখিত উপায়ে তা করতে পারবেন:

class ExampleClass {

    // Job and Dispatcher are combined into a CoroutineContext which
    // will be discussed shortly
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine within the scope
        scope.launch {
            // New coroutine that can call suspend functions
            fetchDocs()
        }
    }

    fun cleanUp() {
        // Cancel the scope to cancel ongoing coroutines work
        scope.cancel()
    }
}

বাতিল করা স্কোপ আর কোরাউটিন তৈরি করতে পারে না। তাই, যে ক্লাস এর লাইফসাইকেল কন্ট্রোল করে সেটি ধ্বংস হওয়ার সময় শুধুমাত্র scope.cancel()-কে কল করা উচিত। viewModelScope ব্যবহার করার সময়, ViewModel-এর onCleared() পদ্ধতিতে ViewModel ক্লাস আপনার জন্য স্কোপ অটোমেটিক বাতিল করে দেয়।

চাকরি

Job হল একটি কোরাউটিনের হ্যান্ডেল। launch বা async ব্যবহার করে তৈরি করা প্রতিটি কোরাউটিন একটি Job ইনস্ট্যান্স রিটার্ন করে যা কোরাউটিনকে অনন্যভাবে শনাক্ত করে এবং এর লাইফসাইকেল ম্যানেজ করে। এছাড়াও, আপনি এর লাইফসাইকেল আরও ম্যানেজ করতে Job-এ CoroutineScope পাস করতে পারেন, যেমনটি নিম্নলিখিত উদাহরণে দেখানো হয়েছে:

class ExampleClass {
    // ...
    fun exampleMethod() {
        // Handle to the coroutine, you can control its lifecycle
        val job = scope.launch {
            // New coroutine
        }

        if (condition) {
            // Cancel the coroutine started above, this doesn't affect the scope
            // this coroutine was launched in
            job.cancel()
        }
    }
}

CoroutineContext

CoroutineContext নিচে উল্লেখ করা এলিমেন্টের সেট ব্যবহার করে কোরাউটিনের আচরণকে সংজ্ঞায়িত করে:

  • Job: কোরুটিনের লাইফসাইকেল নিয়ন্ত্রণ করে।
  • CoroutineDispatcher: ডিসপ্যাচ উপযুক্ত থ্রেডে কাজ করে।
  • CoroutineName: করুটিনের নাম, যা ডিবাগিংয়ের জন্য সহায়ক।
  • CoroutineExceptionHandler: ক্যাচ না করা ব্যতিক্রম হ্যান্ডেল করে।

স্কোপের মধ্যে তৈরি করা নতুন কোরাউটিনের জন্য, নতুন কোরাউটিনে একটি নতুন Job ইনস্ট্যান্স অ্যাসাইন করা হয় এবং অন্যান্য CoroutineContext এলিমেন্ট যে স্কোপে কোরাউটিন আছে সেখান থেকে ইনহেরিট করা হয়। launch বা async ফাংশনে নতুন CoroutineContext পাস করে আপনি ইনহেরিট করা এলিমেন্ট ওভাররাইড করতে পারবেন। মনে রাখবেন, Job থেকে launch বা async-এ পাস করলে কোনও প্রভাব পড়ে না, কারণ Job-এর একটি নতুন ইনস্ট্যান্স সবসময় একটি নতুন কোরাউটিনে অ্যাসাইন করা হয়।

class ExampleClass {
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine on Dispatchers.Main as it's the scope's default
        val job1 = scope.launch {
            // New coroutine with CoroutineName = "coroutine" (default)
        }

        // Starts a new coroutine on Dispatchers.Default
        val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
            // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
        }
    }
}

অতিরিক্ত কোরাউটিন রিসোর্স

আরও কোরাউটিন রিসোর্সের জন্য, নিম্নলিখিত লিঙ্ক দেখুন: