এই পৃষ্ঠায় এমন কিছু সেরা অনুশীলন তুলে ধরা হয়েছে, যা কো-রুটিন ব্যবহারের মাধ্যমে আপনার অ্যাপকে আরও স্কেলেবল ও টেস্টেবল করে তুলে ইতিবাচক প্রভাব ফেলে।
ইনজেক্ট ডিসপ্যাচার
নতুন কো-রুটিন তৈরি করার সময় বা withContext কল করার সময় Dispatchers হার্ডকোড করবেন না।
// DO inject Dispatchers class NewsRepository( private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default ) { suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ } } // DO NOT hardcode Dispatchers class NewsRepository { // DO NOT use Dispatchers.Default directly, inject it instead suspend fun loadNews() = withContext(Dispatchers.Default) { /* ... */ } }
এই ডিপেন্ডেন্সি ইনজেকশন প্যাটার্ন টেস্টিং সহজ করে তোলে, কারণ আপনি ইউনিট এবং ইন্সট্রুমেন্টেশন টেস্টের ডিসপ্যাচারগুলোকে একটি টেস্ট ডিসপ্যাচার দিয়ে প্রতিস্থাপন করে আপনার টেস্টগুলোকে আরও ডিটারমিনিস্টিক করতে পারেন।
সাসপেন্ড ফাংশনগুলো মেইন থ্রেড থেকে নিরাপদে কল করা উচিত।
সাসপেন্ড ফাংশনগুলো মেইন-সেফ হওয়া উচিত, অর্থাৎ এগুলো মেইন থ্রেড থেকে নিরাপদে কল করা যায়। যদি কোনো ক্লাস একটি কো-রুটিনে দীর্ঘ সময় ধরে চলা ব্লকিং অপারেশন সম্পাদন করে, তবে withContext ব্যবহার করে এক্সিকিউশনকে মেইন থ্রেড থেকে সরিয়ে নেওয়ার দায়িত্ব তারই থাকে। আপনার অ্যাপের সমস্ত ক্লাসের ক্ষেত্রেই এটি প্রযোজ্য, ক্লাসটি আর্কিটেকচারের যে অংশেই থাকুক না কেন।
class NewsRepository(private val ioDispatcher: CoroutineDispatcher) { // As this operation is manually retrieving the news from the server // using a blocking HttpURLConnection, it needs to move the execution // to an IO dispatcher to make it main-safe suspend fun fetchLatestNews(): List<Article> { withContext(ioDispatcher) { /* ... implementation ... */ } } } // This use case fetches the latest news and the associated author. class GetLatestNewsWithAuthorsUseCase( private val newsRepository: NewsRepository, private val authorsRepository: AuthorsRepository ) { // This method doesn't need to worry about moving the execution of the // coroutine to a different thread as newsRepository is main-safe. // The work done in the coroutine is lightweight as it only creates // a list and add elements to it suspend operator fun invoke(): Result<List<ArticleWithAuthor>> { val news = newsRepository.fetchLatestNews() val response = mutableListOf<ArticleWithAuthor>() for (article in news) { val author = authorsRepository.getAuthor(article.author) response.add(ArticleWithAuthor(article, author)) } return Result.Success(response) } }
এই প্যাটার্নটি আপনার অ্যাপকে আরও স্কেলেবল করে তোলে, কারণ যে ক্লাসগুলো সাসপেন্ড ফাংশন কল করে, তাদের কোন ধরনের কাজের জন্য কোন Dispatcher ব্যবহার করতে হবে তা নিয়ে চিন্তা করতে হয় না। এই দায়িত্বটি সেই ক্লাসের উপর বর্তায়, যেটি কাজটি করে।
ViewModel-এর উচিত কোরাউটিন তৈরি করা।
বিজনেস লজিক সম্পাদনের জন্য সাসপেন্ড ফাংশন ব্যবহার করার পরিবর্তে ViewModel ক্লাসগুলোতে কো-রুটিন তৈরি করাই শ্রেয়। ViewModel এ সাসপেন্ড ফাংশন তখনো কার্যকর হতে পারে, যখন ডেটার স্ট্রিম ব্যবহার করে স্টেট প্রকাশ করার পরিবর্তে শুধুমাত্র একটিমাত্র ভ্যালু নির্গত করার প্রয়োজন হয়।
// DO create coroutines in the ViewModel class LatestNewsViewModel( private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase ) : ViewModel() { private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading) val uiState: StateFlow<LatestNewsUiState> = _uiState fun loadNews() { viewModelScope.launch { val latestNewsWithAuthors = getLatestNewsWithAuthors() _uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors) } } }
// Prefer observable state rather than suspend functions from the ViewModel class LatestNewsViewModel( private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase ) : ViewModel() { // DO NOT do this. News would probably need to be refreshed as well. // Instead of exposing a single value with a suspend function, news should // be exposed using a stream of data as in the code snippet above. suspend fun loadNews() = getLatestNewsWithAuthors() }
বিজনেস লজিক সম্পাদনের জন্য ভিউয়ের সরাসরি কোনো কো-রুটিন ট্রিগার করা উচিত নয়। এর পরিবর্তে, এই দায়িত্বটি ViewModel এর ওপর ছেড়ে দিন। এতে আপনার বিজনেস লজিক পরীক্ষা করা সহজ হয়, কারণ ভিউ পরীক্ষা করার জন্য প্রয়োজনীয় ইন্সট্রুমেন্টেশন টেস্টের পরিবর্তে ViewModel অবজেক্টগুলোকে ইউনিট টেস্ট করা যায়।
এর পাশাপাশি, যদি কাজটি viewModelScope এ শুরু করা হয়, তাহলে আপনার কো-রুটিনগুলো কনফিগারেশন পরিবর্তন সত্ত্বেও স্বয়ংক্রিয়ভাবে টিকে থাকবে। এর পরিবর্তে যদি আপনি lifecycleScope ব্যবহার করে কো-রুটিন তৈরি করেন, তবে আপনাকে সেটি ম্যানুয়ালি পরিচালনা করতে হবে। যদি কো-রুটিনটিকে ViewModel এর স্কোপের চেয়েও বেশি সময় ধরে কার্যকর থাকার প্রয়োজন হয়, তাহলে "Creating coroutines in the business and data layer" অংশটি দেখুন।
পরিবর্তনযোগ্য প্রকার প্রকাশ করবেন না
অন্যান্য ক্লাসের কাছে অপরিবর্তনশীল টাইপ প্রকাশ করা শ্রেয়। এভাবে, পরিবর্তনযোগ্য টাইপের সমস্ত পরিবর্তন একটি ক্লাসের মধ্যেই কেন্দ্রীভূত থাকে, ফলে কোনো সমস্যা হলে ডিবাগ করা সহজ হয়।
// DO expose immutable types class LatestNewsViewModel : ViewModel() { private val _uiState = MutableStateFlow(LatestNewsUiState.Loading) val uiState: StateFlow<LatestNewsUiState> = _uiState /* ... */ }
class LatestNewsViewModel : ViewModel() { // DO NOT expose mutable types val uiState = MutableStateFlow(LatestNewsUiState.Loading) /* ... */ }
ডেটা এবং বিজনেস লেয়ারে সাসপেন্ড ফাংশন এবং ফ্লো উন্মুক্ত করা উচিত।
ডেটা এবং বিজনেস লেয়ারের ক্লাসগুলো সাধারণত ওয়ান-শট কল সম্পাদন করার জন্য অথবা সময়ের সাথে সাথে ডেটার পরিবর্তন সম্পর্কে অবহিত হওয়ার জন্য ফাংশন সরবরাহ করে। এই লেয়ারের ক্লাসগুলোতে ওয়ান-শট কলের জন্য সাসপেন্ড ফাংশন এবং ডেটার পরিবর্তন সম্পর্কে অবহিত করার জন্য ফ্লো (Flow) সরবরাহ করা উচিত।
// Classes in the data and business layer expose // either suspend functions or Flows class ExampleRepository { suspend fun makeNetworkRequest() { /* ... */ } fun getExamples(): Flow<Example> { /* ... */ } }
এই উত্তম অনুশীলনটি কলারকে (সাধারণত প্রেজেন্টেশন লেয়ার) ঐ লেয়ারগুলোতে চলমান কাজের সম্পাদন ও জীবনচক্র নিয়ন্ত্রণ করতে এবং প্রয়োজনে তা বাতিল করতে সক্ষম করে।
বিজনেস এবং ডেটা লেয়ারে কো-রুটিন তৈরি করা
ডেটা বা বিজনেস লেয়ারের যে ক্লাসগুলোতে বিভিন্ন কারণে কো-রুটিন তৈরি করার প্রয়োজন হয়, সেগুলোর জন্য বিভিন্ন বিকল্প রয়েছে।
যদি ওই কো-রুটিনগুলিতে সম্পাদিতব্য কাজটি শুধুমাত্র ব্যবহারকারী বর্তমান স্ক্রিনে উপস্থিত থাকলেই প্রাসঙ্গিক হয়, তবে সেটিকে কলারের লাইফসাইকেল অনুসরণ করা উচিত। বেশিরভাগ ক্ষেত্রে, কলার হবে ভিউমডেল (ViewModel), এবং যখন ব্যবহারকারী স্ক্রিন থেকে অন্য স্ক্রিনে চলে যায় ও ভিউমডেলটি খালি হয়ে যায়, তখন কলটি বাতিল হয়ে যায়। এই ক্ষেত্রে, coroutineScope বা supervisorScope ব্যবহার করা উচিত।
class GetAllBooksAndAuthorsUseCase( private val booksRepository: BooksRepository, private val authorsRepository: AuthorsRepository, ) { suspend fun getBookAndAuthors(): BookAndAuthors { // In parallel, fetch books and authors and return when both requests // complete and the data is ready return coroutineScope { val books = async { booksRepository.getAllBooks() } val authors = async { authorsRepository.getAllAuthors() } BookAndAuthors(books.await(), authors.await()) } } }
যদি করণীয় কাজটি অ্যাপটি খোলা থাকা পর্যন্ত প্রাসঙ্গিক থাকে এবং কাজটি কোনো নির্দিষ্ট স্ক্রিনের সাথে আবদ্ধ না থাকে, তাহলে কাজটি কলকারীর লাইফসাইকেলকে ছাড়িয়ে যাওয়া উচিত। এই পরিস্থিতির জন্য, একটি এক্সটার্নাল CoroutineScope ব্যবহার করা উচিত, যেমনটি "Coroutines & Patterns for work that should't be cancelled" ব্লগ পোস্টে ব্যাখ্যা করা হয়েছে।
class ArticlesRepository( private val articlesDataSource: ArticlesDataSource, private val externalScope: CoroutineScope, ) { // As we want to complete bookmarking the article even if the user moves // away from the screen, the work is done creating a new coroutine // from an external scope suspend fun bookmarkArticle(article: Article) { externalScope.launch { articlesDataSource.bookmarkArticle(article) } .join() // Wait for the coroutine to complete } }
externalScope এমন একটি ক্লাস দ্বারা তৈরি ও পরিচালিত হওয়া উচিত যা বর্তমান স্ক্রিনের চেয়ে বেশি সময় ধরে টিকে থাকে; এটি Application ক্লাস অথবা নেভিগেশন গ্রাফের আওতাভুক্ত কোনো ViewModel দ্বারা পরিচালিত হতে পারে।
টেস্টে টেস্টডিসপ্যাচার ইনজেক্ট করুন
টেস্টের সময় আপনার ক্লাসগুলোতে TestDispatcher এর একটি ইনস্ট্যান্স ইনজেক্ট করতে হবে। kotlinx-coroutines-test লাইব্রেরিতে এর দুটি ইমপ্লিমেন্টেশন পাওয়া যায়:
StandardTestDispatcher: একটি শিডিউলারের সাহায্যে এতে শুরু হওয়া কো-রুটিনগুলোকে কিউতে রাখে এবং টেস্ট থ্রেড ব্যস্ত না থাকলে সেগুলোকে এক্সিকিউট করে।advanceUntilIdleমতো মেথড ব্যবহার করে আপনি টেস্ট থ্রেডটিকে সাসপেন্ড করতে পারেন, যাতে কিউতে থাকা অন্যান্য কো-রুটিনগুলো চলতে পারে।UnconfinedTestDispatcher: নতুন কো-রুটিনগুলোকে আগ্রহের সাথে এবং ব্লকিং পদ্ধতিতে রান করে। এটি সাধারণত টেস্ট লেখা সহজ করে তোলে, কিন্তু টেস্ট চলাকালীন কো-রুটিনগুলো কীভাবে এক্সিকিউট হবে তার উপর আপনার নিয়ন্ত্রণ কম থাকে।
অতিরিক্ত বিবরণের জন্য প্রতিটি ডিসপ্যাচার বাস্তবায়নের ডকুমেন্টেশন দেখুন।
কো-রুটিন পরীক্ষা করার জন্য, runTest কো-রুটিন বিল্ডার ব্যবহার করুন। runTest একটি TestCoroutineScheduler ব্যবহার করে, যা পরীক্ষার সময় বিলম্ব এড়িয়ে যেতে এবং ভার্চুয়াল সময় নিয়ন্ত্রণ করতে সাহায্য করে। প্রয়োজন অনুযায়ী অতিরিক্ত টেস্ট ডিসপ্যাচার তৈরি করতেও আপনি এই শিডিউলারটি ব্যবহার করতে পারেন।
class ArticlesRepositoryTest { @Test fun testBookmarkArticle() = runTest { // Pass the testScheduler provided by runTest's coroutine scope to // the test dispatcher val testDispatcher = UnconfinedTestDispatcher(testScheduler) val articlesDataSource = FakeArticlesDataSource() val repository = ArticlesRepository( articlesDataSource, defaultDispatcher = testDispatcher ) val article = Article() repository.bookmarkArticle(article) assertThat(articlesDataSource.isBookmarked(article)).isTrue() } }
সমস্ত TestDispatchers একই শিডিউলার থাকা উচিত। এটি আপনাকে আপনার সমস্ত কো-রুটিন কোড একটিমাত্র টেস্ট থ্রেডে চালানোর সুযোগ দেয়, যা আপনার টেস্টগুলোকে ডিটারমিনিস্টিক করে তোলে। runTest রিটার্ন করার আগে, একই শিডিউলারে থাকা অথবা টেস্ট কো-রুটিনের চাইল্ড হিসেবে থাকা সমস্ত কো-রুটিন সম্পূর্ণ হওয়ার জন্য অপেক্ষা করবে।
গ্লোবালস্কোপ এড়িয়ে চলুন
এটি ইনজেক্ট ডিসপ্যাচার (Inject Dispatchers) ব্যবহারের সেরা পদ্ধতির অনুরূপ। GlobalScope ব্যবহার করার মাধ্যমে, আপনি একটি ক্লাসের ব্যবহৃত CoroutineScope হার্ডকোড করে দেন, যা কিছু অসুবিধাও নিয়ে আসে:
এটি ভ্যালু হার্ড-কোডিংকে উৎসাহিত করে। আপনি যদি
GlobalScopeহার্ডকোড করেন, তাহলেDispatchersও হার্ডকোড হয়ে যেতে পারে।এটি টেস্টিং খুব কঠিন করে তোলে, কারণ আপনার কোড একটি অনিয়ন্ত্রিত পরিসরে এক্সিকিউট হয়, ফলে আপনি এর এক্সিকিউশন নিয়ন্ত্রণ করতে পারবেন না।
একই স্কোপের মধ্যে নির্মিত সমস্ত কো-রুটিন কার্যকর করার জন্য একটি সাধারণ
CoroutineContextব্যবহার করা যায় না।
এর পরিবর্তে, যে কাজগুলোর জন্য বর্তমান স্কোপের চেয়ে বেশি সময় ধরে কাজ করা প্রয়োজন, সেগুলোর জন্য একটি CoroutineScope যুক্ত করার কথা বিবেচনা করুন। এই বিষয়ে আরও জানতে "Creating coroutines in the business and data layer" অংশটি দেখুন।
// DO inject an external scope instead of using GlobalScope. // GlobalScope can be used indirectly. Here as a default parameter makes sense. class ArticlesRepository( private val articlesDataSource: ArticlesDataSource, private val externalScope: CoroutineScope = GlobalScope, private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default ) { // As we want to complete bookmarking the article even if the user moves // away from the screen, the work is done creating a new coroutine // from an external scope suspend fun bookmarkArticle(article: Article) { externalScope.launch(defaultDispatcher) { articlesDataSource.bookmarkArticle(article) } .join() // Wait for the coroutine to complete } }
// DO NOT use GlobalScope directly class ArticlesRepository( private val articlesDataSource: ArticlesDataSource, ) { // As we want to complete bookmarking the article even if the user moves away // from the screen, the work is done creating a new coroutine with GlobalScope suspend fun bookmarkArticle(article: Article) { GlobalScope.launch { articlesDataSource.bookmarkArticle(article) } .join() // Wait for the coroutine to complete } }
GlobalScope এবং এর বিকল্পগুলো সম্পর্কে আরও জানতে "Coroutines & Patterns for work that should't be cancelled" ব্লগ পোস্টটি পড়ুন ।
আপনার কো-রুটিন বাতিলযোগ্য করুন
কো-রুটিনে বাতিলকরণ সহযোগিতামূলক, যার অর্থ হলো, যখন একটি কো-রুটিনের Job বাতিল করা হয়, তখন কো-রুটিনটি ততক্ষণ পর্যন্ত বাতিল হয় না যতক্ষণ না এটি সাসপেন্ড হয় বা বাতিলকরণের জন্য পরীক্ষা করে। আপনি যদি একটি কো-রুটিনে ব্লকিং অপারেশন করেন, তবে নিশ্চিত করুন যে কো-রুটিনটি বাতিলযোগ্য ।
উদাহরণস্বরূপ, আপনি যদি ডিস্ক থেকে একাধিক ফাইল পড়েন, তবে প্রতিটি ফাইল পড়া শুরু করার আগে, কো-রুটিনটি বাতিল করা হয়েছে কিনা তা পরীক্ষা করে দেখুন। বাতিলকরণ পরীক্ষা করার একটি উপায় হলো ensureActive ফাংশনটি কল করা।
someScope.launch { for (file in files) { ensureActive() // Check for cancellation readFile(file) } }
kotlinx.coroutines এর withContext এবং delay মতো সমস্ত সাসপেন্ড ফাংশন বাতিলযোগ্য। আপনার কো-রুটিন যদি এগুলোকে কল করে, তবে আপনার কোনো অতিরিক্ত কাজ করার প্রয়োজন হবে না।
কো-রুটিনে ক্যান্সেলেশন (cancellation) সম্পর্কে আরও তথ্যের জন্য, “ Cancellation in coroutines” ব্লগ পোস্টটি দেখুন।
ব্যতিক্রমের ব্যাপারে সতর্ক থাকুন
কো-রুটিনে থ্রো করা আনহ্যান্ডেলড এক্সেপশন আপনার অ্যাপ ক্র্যাশ করে দিতে পারে। যদি এক্সেপশন ঘটার সম্ভাবনা থাকে, তাহলে viewModelScope বা lifecycleScope দিয়ে তৈরি করা যেকোনো কো-রুটিনের বডিতে সেগুলোকে ক্যাচ করুন।
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { viewModelScope.launch { try { loginRepository.login(username, token) // Update UI, user logged in successfully } catch (exception: IOException) { // Update UI, login attempt failed } } } }
আরও তথ্যের জন্য, “Exceptions in coroutines” ব্লগ পোস্টটি, অথবা কোটলিন ডকুমেন্টেশনে “Coroutine exceptions handling” দেখুন।
কো-রুটিন সম্পর্কে আরও জানুন
কো-রুটিন সম্পর্কিত আরও তথ্যের জন্য, "Additional resources for Kotlin coroutines and flow" পৃষ্ঠাটি দেখুন।