অ্যান্ড্রয়েডে কোরোটিনের জন্য সেরা অনুশীলন

এই পৃষ্ঠাটি বেশ কয়েকটি সর্বোত্তম অনুশীলন উপস্থাপন করে যা আপনার অ্যাপটিকে আরও মাপযোগ্য এবং পরীক্ষাযোগ্য করে coroutines ব্যবহার করার মাধ্যমে ইতিবাচক প্রভাব ফেলে।

প্রেরকদের ইনজেক্ট করুন

নতুন কোরোটিন তৈরি করার সময় বা 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(): List<ArticleWithAuthor> {
        val news = newsRepository.fetchLatestNews()

        val response: List<ArticleWithAuthor> = mutableEmptyList()
        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 সুযোগের বাইরে থাকতে হয়, ব্যবসায় এবং ডেটা স্তর বিভাগে কোরোটিন তৈরি করা দেখুন।

পরিবর্তনযোগ্য প্রকারগুলি প্রকাশ করবেন না

অপরিবর্তনীয় প্রকারগুলিকে অন্য শ্রেণীর কাছে প্রকাশ করা পছন্দ করুন। এইভাবে, মিউটেবল টাইপের সমস্ত পরিবর্তন একটি ক্লাসে কেন্দ্রীভূত করা হয় যাতে কিছু ভুল হয়ে গেলে ডিবাগ করা সহজ হয়।

// 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)

    /* ... */
}

ডেটা এবং ব্যবসায়িক স্তরটি সাসপেন্ড ফাংশন এবং প্রবাহ প্রকাশ করবে

ডেটা এবং ব্যবসায়িক স্তরগুলির ক্লাসগুলি সাধারণত ওয়ান-শট কলগুলি সম্পাদন করতে বা সময়ের সাথে ডেটা পরিবর্তনের বিষয়ে অবহিত করার জন্য ফাংশনগুলিকে প্রকাশ করে৷ এই স্তরগুলির ক্লাসগুলিকে এক-শট কলের জন্য সাসপেন্ড ফাংশন এবং ডেটা পরিবর্তন সম্পর্কে অবহিত করার জন্য ফ্লোকে প্রকাশ করা উচিত।

// Classes in the data and business layer expose
// either suspend functions or Flows
class ExampleRepository {
    suspend fun makeNetworkRequest() { /* ... */ }

    fun getExamples(): Flow<Example> { /* ... */ }
}

এই সর্বোত্তম অনুশীলনটি কলারকে, সাধারণত উপস্থাপনা স্তরকে, সেই স্তরগুলিতে ঘটতে থাকা কাজের সম্পাদন এবং জীবনচক্র নিয়ন্ত্রণ করতে সক্ষম করে এবং প্রয়োজনে বাতিল করতে সক্ষম করে।

ব্যবসা এবং ডেটা স্তরে কোরোটিন তৈরি করা

ডেটা বা ব্যবসায়িক স্তরের ক্লাসগুলির জন্য যেগুলি বিভিন্ন কারণে কোরোটিন তৈরি করতে হবে, সেখানে বিভিন্ন বিকল্প রয়েছে।

যদি এই কোরোটিনে করা কাজটি শুধুমাত্র তখনই প্রাসঙ্গিক হয় যখন ব্যবহারকারী বর্তমান স্ক্রিনে উপস্থিত থাকে, তবে এটি কলারের জীবনচক্র অনুসরণ করা উচিত। বেশিরভাগ ক্ষেত্রে, কলকারী হবেন ভিউমডেল, এবং যখন ব্যবহারকারী স্ক্রীন থেকে দূরে নেভিগেট করবে এবং ভিউমডেল সাফ হয়ে যাবে তখন কলটি বাতিল হয়ে যাবে। এই ক্ষেত্রে, 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 এবং প্যাটার্নে ব্যাখ্যা করা হয়েছে।

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 দ্বারা পরিচালিত হতে পারে।

পরীক্ষায় TestDispatchers ইনজেক্ট করুন

TestDispatcher এর একটি উদাহরণ পরীক্ষায় আপনার ক্লাসে ইনজেকশন করা উচিত। kotlinx-coroutines-test লাইব্রেরিতে দুটি উপলব্ধ বাস্তবায়ন রয়েছে:

  • StandardTestDispatcher : একটি শিডিউলারের মাধ্যমে শুরু হওয়া কোরোটিনগুলিকে সারিবদ্ধ করে, এবং পরীক্ষার থ্রেড ব্যস্ত না থাকলে সেগুলি কার্যকর করে৷ আপনি advanceUntilIdle এর মতো পদ্ধতি ব্যবহার করে অন্যান্য সারিবদ্ধ কোরোটিনগুলি চালানোর জন্য পরীক্ষার থ্রেডটি স্থগিত করতে পারেন।

  • UnconfinedTestDispatcher : একটি ব্লকিং উপায়ে, সাগ্রহে নতুন coroutines চালায়। এটি সাধারণত লেখার পরীক্ষাগুলিকে সহজ করে তোলে, তবে পরীক্ষার সময় কোরোটিনগুলি কীভাবে কার্যকর করা হয় তার উপর আপনাকে কম নিয়ন্ত্রণ দেয়।

অতিরিক্ত বিবরণের জন্য প্রতিটি প্রেরণকারী বাস্তবায়নের ডকুমেন্টেশন দেখুন।

coroutines পরীক্ষা করতে, runTest coroutine বিল্ডার ব্যবহার করুন. 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,
            testDispatcher
        )
        val article = Article()
        repository.bookmarkArticle(article)
        assertThat(articlesDataSource.isBookmarked(article)).isTrue()
    }
}

সমস্ত TestDispatchers একই সময়সূচী শেয়ার করা উচিত. এটি আপনাকে আপনার পরীক্ষাগুলিকে নির্ধারক করতে একক পরীক্ষার থ্রেডে আপনার সমস্ত করোটিন কোড চালানোর অনুমতি দেয়। runTest একই শিডিউলারে থাকা সমস্ত কোরোটিনগুলির জন্য অপেক্ষা করবে বা ফিরে আসার আগে পরীক্ষার কোরোটিন সম্পূর্ণ করার জন্য অপেক্ষা করবে৷

গ্লোবালস্কোপ এড়িয়ে চলুন

এটি ইনজেক্ট ডিসপ্যাচার সেরা অনুশীলনের অনুরূপ। GlobalScope ব্যবহার করে, আপনি CoroutineScope হার্ডকোড করছেন যা একটি ক্লাস ব্যবহার করে এর সাথে কিছু খারাপ দিক নিয়ে আসে:

  • হার্ড-কোডিং মান প্রচার করে। আপনি GlobalScope হার্ডকোড করলে, আপনি হার্ড-কোডিং Dispatchers হতে পারেন।

  • আপনার কোড একটি অনিয়ন্ত্রিত সুযোগে নির্বাহ করা হয় বলে পরীক্ষা করা খুব কঠিন করে তোলে, আপনি এটির সম্পাদন নিয়ন্ত্রণ করতে পারবেন না।

  • স্কোপের মধ্যেই তৈরি সমস্ত কোরোটিনের জন্য কার্যকর করার জন্য আপনার কাছে একটি সাধারণ CoroutineContext থাকতে পারে না।

পরিবর্তে, বর্তমান সুযোগের বাইরে থাকা প্রয়োজন এমন কাজের জন্য একটি CoroutineScope ইনজেকশন দেওয়ার কথা বিবেচনা করুন। এই বিষয় সম্পর্কে আরও জানতে ব্যবসা এবং ডেটা স্তর বিভাগে কোরোটিন তৈরি করা দেখুন।

// 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
    }
}

ব্লগ পোস্ট বাতিল করা উচিত নয় এমন কাজের জন্য Coroutines এবং প্যাটার্নে GlobalScope এবং এর বিকল্প সম্পর্কে আরও জানুন।

আপনার coroutine বাতিলযোগ্য করুন

coroutines-এ বাতিলকরণ হল সমবায়, যার মানে হল যখন একটি coroutine এর Job বাতিল করা হয়, coroutine বাতিল করা হয় না যতক্ষণ না এটি স্থগিত বা বাতিলের জন্য চেক করা হয়। আপনি যদি একটি করোটিনে ব্লকিং অপারেশন করেন তবে নিশ্চিত করুন যে করোটিন বাতিলযোগ্য

উদাহরণস্বরূপ, আপনি যদি ডিস্ক থেকে একাধিক ফাইল পড়ছেন, প্রতিটি ফাইল পড়া শুরু করার আগে, করুটিন বাতিল করা হয়েছে কিনা তা পরীক্ষা করে দেখুন। বাতিলকরণ পরীক্ষা করার একটি উপায় হল ensureActive ফাংশন কল করা।

someScope.launch {
    for(file in files) {
        ensureActive() // Check for cancellation
        readFile(file)
    }
}

kotlinx.coroutines থেকে সমস্ত সাসপেন্ড ফাংশন যেমন withContext এবং delay বাতিলযোগ্য। যদি আপনার কোরোটিন তাদের কল করে, তাহলে আপনাকে কোনো অতিরিক্ত কাজ করতে হবে না।

coroutines-এ বাতিলকরণ সম্পর্কে আরও তথ্যের জন্য, coroutines ব্লগ পোস্টে বাতিলকরণ দেখুন।

ব্যতিক্রমের জন্য সতর্ক থাকুন

কোরোটিনে নিক্ষিপ্ত অ-হ্যান্ডেল ব্যতিক্রমগুলি আপনার অ্যাপ ক্র্যাশ করতে পারে। ব্যতিক্রম ঘটার সম্ভাবনা থাকলে, viewModelScope বা lifecycleScope দিয়ে তৈরি যেকোনও কোরোটিনের বডিতে সেগুলি ধরুন।

class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    fun login(username: String, token: String) {
        viewModelScope.launch {
            try {
                loginRepository.login(username, token)
                // Notify view user logged in successfully
            } catch (exception: IOException) {
                // Notify view login attempt failed
            }
        }
    }
}

আরও তথ্যের জন্য, কোটলিন ডকুমেন্টেশনে coroutines এর ব্যতিক্রমসমূহ বা Coroutine ব্যতিক্রম হ্যান্ডলিং ব্লগ পোস্টটি দেখুন।

coroutines সম্পর্কে আরও জানুন

আরও coroutines সম্পদের জন্য, Kotlin coroutines এবং ফ্লো পৃষ্ঠার জন্য অতিরিক্ত সম্পদ দেখুন।