Android में कोरूटीन इस्तेमाल करने के सबसे सही तरीके

इस पेज पर, कई सबसे सही तरीके बताए गए हैं. इनसे को-रूटीन का इस्तेमाल करते समय, आपके ऐप्लिकेशन को ज़्यादा बेहतर तरीके से स्केल किया जा सकता है और उसकी टेस्टिंग की जा सकती है.

डिस्पैचर इंजेक्ट करना

नई को-रूटीन बनाते समय या Dispatchers को कॉल करते समय, Dispatchers को हार्डकोड न करें.withContext

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

इस पैटर्न से आपका ऐप्लिकेशन ज़्यादा स्केलेबल बनता है. ऐसा इसलिए, क्योंकि suspend फ़ंक्शन को कॉल करने वाली क्लास को यह चिंता नहीं करनी पड़ती कि किस तरह के काम के लिए Dispatcher का इस्तेमाल करना है. यह ज़िम्मेदारी उस क्लास की होती है जो काम करती है.

ViewModel को कोरूटीन बनाने चाहिए

ViewModel क्लास को, कारोबार के लॉजिक को लागू करने के लिए सस्पेंड फ़ंक्शन को दिखाने के बजाय, कोरूटीन बनाने को प्राथमिकता देनी चाहिए. अगर डेटा की स्ट्रीम का इस्तेमाल करके स्थिति को दिखाने के बजाय, सिर्फ़ एक वैल्यू को दिखाना है, तो ViewModel में मौजूद Suspend फ़ंक्शन काम के हो सकते हैं.

// 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> {
        /* ... */
    }
}

इस सबसे सही तरीके से, कॉलर (आम तौर पर, प्रज़ेंटेशन लेयर) को इन लेयर में हो रहे काम के एक्ज़ीक्यूशन और लाइफ़साइकल को कंट्रोल करने की सुविधा मिलती है. साथ ही, ज़रूरत पड़ने पर इसे रद्द करने की सुविधा भी मिलती है.

कारोबार और डेटा लेयर में को-रूटीन बनाना

डेटा या कारोबार की लेयर में मौजूद ऐसी क्लास जिनके लिए अलग-अलग वजहों से को-रूटीन बनाने की ज़रूरत होती है उनके लिए अलग-अलग विकल्प उपलब्ध हैं.

अगर उन कोरूटीन में किया जाने वाला काम सिर्फ़ तब ज़रूरी होता है, जब उपयोगकर्ता मौजूदा स्क्रीन पर मौजूद हो, तो उसे कॉलर के लाइफ़साइकल का पालन करना चाहिए. ज़्यादातर मामलों में, कॉलर ViewModel होगा. जब उपयोगकर्ता स्क्रीन से हट जाएगा और 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 का इस्तेमाल किया जाना चाहिए. इसके बारे में ऐसे काम के लिए कोरूटीन और पैटर्न जिन्हें रद्द नहीं किया जाना चाहिए ब्लॉग पोस्ट में बताया गया है.

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: यह नई कोरूटीन को तुरंत शुरू करता है. इससे आम तौर पर, टेस्ट लिखना आसान हो जाता है. हालांकि, इससे आपको यह कंट्रोल कम मिलता है कि टेस्ट के दौरान कोरूटीन कैसे एक्ज़ीक्यूट किए जाते हैं.

ज़्यादा जानकारी के लिए, हर डिसपैचर को लागू करने से जुड़ा दस्तावेज़ देखें.

कोरूटीन की जांच करने के लिए, 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, एक ही शेड्यूलर पर मौजूद या टेस्ट कोरूटीन के चाइल्ड कोरूटीन के पूरे होने का इंतज़ार करेगा. इसके बाद ही, यह वैल्यू दिखाएगा.

GlobalScope से बचें

यह इंजेक्ट डिस्पैचर के सबसे सही तरीके जैसा ही है. 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
    }
}

GlobalScope और इसके विकल्पों के बारे में ज़्यादा जानने के लिए, ऐसे काम के लिए को-रूटीन और पैटर्न जिन्हें रद्द नहीं किया जाना चाहिए ब्लॉग पोस्ट पढ़ें.

अपनी को-रूटीन को रद्द करने लायक बनाना

कोरूटीन में रद्द करने की सुविधा, कोऑपरेटिव होती है. इसका मतलब है कि जब किसी कोरूटीन के Job को रद्द किया जाता है, तो कोरूटीन तब तक रद्द नहीं होता, जब तक कि वह सस्पेंड न हो जाए या रद्द करने की स्थिति की जांच न कर ले. अगर आपको किसी कोरूटीन में ब्लॉकिंग ऑपरेशन करने हैं, तो पक्का करें कि कोरूटीन रद्द की जा सकती हो.

उदाहरण के लिए, अगर आपको डिस्क से कई फ़ाइलें पढ़नी हैं, तो हर फ़ाइल को पढ़ना शुरू करने से पहले, देखें कि कोरूटीन रद्द तो नहीं कर दिया गया है. रद्द करने की स्थिति की जांच करने का एक तरीका, ensureActive फ़ंक्शन को कॉल करना है.

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

kotlinx.coroutines के सभी सस्पेंड फ़ंक्शन, जैसे कि withContext और delay रद्द किए जा सकते हैं. अगर आपकी को-रूटीन उन्हें कॉल करती है, तो आपको कोई अतिरिक्त काम करने की ज़रूरत नहीं है.

कोरूटीन में रद्द करने के बारे में ज़्यादा जानने के लिए, कोरूटीन में रद्द करने के बारे में ब्लॉग पोस्ट देखें.

अपवादों पर नज़र रखें

कोरूटीन में थ्रो किए गए ऐसे अपवादों को हैंडल न करने पर, आपका ऐप्लिकेशन क्रैश हो सकता है. अगर अपवाद होने की संभावना है, तो उन्हें 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
            }
        }
    }
}

ज़्यादा जानकारी के लिए, कोरूटीन में अपवाद वाली ब्लॉग पोस्ट पढ़ें. इसके अलावा, Kotlin के दस्तावेज़ में कोरूटीन में अपवादों को हैंडल करना लेख पढ़ें.

को-रूटीन के बारे में ज़्यादा जानें

कोरूटीन के बारे में ज़्यादा जानने के लिए, Kotlin कोरूटीन और फ़्लो के बारे में अतिरिक्त संसाधन पेज देखें.