खास तौर पर ऑफ़लाइन ऐप्लिकेशन बनाएं

ऑफ़लाइन-फ़र्स्ट ऐप्लिकेशन, ऐसा ऐप्लिकेशन होता है जो इंटरनेट ऐक्सेस के बिना, अपने सभी या मुख्य फ़ंक्शन के सबसे ज़रूरी सबसेट को परफ़ॉर्म कर सकता है. इसका मतलब है कि यह अपने कुछ या सभी कारोबारी नियमों को ऑफ़लाइन लागू कर सकता है.

ऑफ़लाइन-फ़र्स्ट ऐप्लिकेशन बनाने के लिए, सबसे पहले डेटा लेयर पर ध्यान देना ज़रूरी है. इससे ऐप्लिकेशन के डेटा और कारोबारी नियम को ऐक्सेस किया जा सकता है. समय-समय पर, ऐप्लिकेशन को डिवाइस से बाहर के सोर्स से इस डेटा को रीफ़्रेश करने की ज़रूरत पड़ सकती है. ऐसा करने के लिए, इसे अप-टू-डेट रहने के लिए नेटवर्क संसाधनों को कॉल करने की ज़रूरत पड़ सकती है.

नेटवर्क की उपलब्धता की गारंटी हमेशा नहीं दी जाती. डिवाइसों में अक्सर कुछ समय के लिए, इंटरनेट कनेक्शन धीमा या रुक-रुककर चलता है. उपयोगकर्ताओं को ये समस्याएं दिख सकती हैं:

  • इंटरनेट बैंडविथ कम होना
  • कनेक्शन में कुछ समय के लिए रुकावटें आना, जैसे कि लिफ़्ट या सुरंग में होने पर
  • कभी-कभी डेटा ऐक्सेस करने वाले डिवाइस—उदाहरण के लिए, सिर्फ़ वाई-फ़ाई से कनेक्ट होने वाले टैबलेट

वजह चाहे जो भी हो, ऐसी स्थितियों में भी ऐप्लिकेशन को ठीक से काम करना चाहिए. यह पक्का करने के लिए कि आपका ऐप्लिकेशन ऑफ़लाइन सही तरीके से काम करे, उसमें ये सुविधाएं होनी चाहिए:

  • इंटरनेट कनेक्शन के बिना भी इस्तेमाल किया जा सकता है
  • उपयोगकर्ताओं को तुरंत स्थानीय डेटा दिखाएं. इसके लिए, पहले नेटवर्क कॉल के पूरा होने या फ़ेल होने का इंतज़ार न करें
  • डेटा को इस तरह फ़ेच करें कि बैटरी और डेटा की स्थिति पर ध्यान दिया जाए. उदाहरण के लिए, डेटा फ़ेच करने का अनुरोध सिर्फ़ तब करें, जब डिवाइस चार्ज हो रहा हो या वाई-फ़ाई से कनेक्ट हो

इन शर्तों को पूरा करने वाले ऐप्लिकेशन को अक्सर ऑफ़लाइन-फ़र्स्ट ऐप्लिकेशन कहा जाता है.

ऑफ़लाइन-फ़र्स्ट ऐप्लिकेशन डिज़ाइन करना

ऑफ़लाइन-फ़र्स्ट ऐप्लिकेशन डिज़ाइन करते समय, डेटा लेयर से शुरुआत करें. साथ ही, ऐप्लिकेशन के डेटा पर ये दो मुख्य कार्रवाइयां की जा सकती हैं:

  • रीड: ऐप्लिकेशन के अन्य हिस्सों में इस्तेमाल करने के लिए डेटा को वापस पाना. जैसे, उपयोगकर्ता को जानकारी दिखाना. Compose में, आम तौर पर स्टेट को ऑब्ज़र्व करके ऐसा किया जाता है. जब आपका यूज़र इंटरफ़ेस (यूआई), स्थानीय डेटा सोर्स को स्टेट के तौर पर देखता है, तो स्क्रीन पर नया स्थानीय डेटा अपने-आप दिखने लगता है.
  • लिखना: उपयोगकर्ता के इनपुट को सेव करना, ताकि उसे बाद में वापस पाया जा सके. Compose में, आम तौर पर यूज़र इंटरफ़ेस (यूआई) से ViewModel को भेजे गए इवेंट और कार्रवाइयों का इस्तेमाल करके ऐसा किया जाता है.

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

ऑफ़लाइन-फ़र्स्ट ऐप्लिकेशन में मॉडल डेटा

ऑफ़लाइन-फ़र्स्ट ऐप्लिकेशन में, नेटवर्क के संसाधनों का इस्तेमाल करने वाली हर रिपॉज़िटरी के लिए, कम से कम दो डेटा सोर्स होते हैं:

  • स्थानीय डेटा सोर्स
  • नेटवर्क डेटा सोर्स
ऑफ़लाइन-फ़र्स्ट डेटा लेयर में, लोकल और नेटवर्क डेटा सोर्स, दोनों शामिल होते हैं
पहली इमेज: ऑफ़लाइन-फ़र्स्ट रिपॉज़िटरी.

स्थानीय डेटा सोर्स

स्थानीय डेटा सोर्स, ऐप्लिकेशन के लिए कैननिकल सोर्स ऑफ़ ट्रुथ होता है. यह किसी भी ऐसे डेटा का खास सोर्स होना चाहिए जिसे ऐप्लिकेशन की ऊपरी लेयर पढ़ती हैं. इससे यह पक्का होता है कि कनेक्शन की स्थितियों के बीच डेटा एक जैसा रहे. स्थानीय डेटा सोर्स को अक्सर स्टोरेज से बैक अप किया जाता है. यह स्टोरेज, डिस्क में सेव रहता है. डिस्क पर डेटा सेव करने के कुछ सामान्य तरीके ये हैं:

  • स्ट्रक्चर्ड डेटा सोर्स, जैसे कि Room जैसे रिलेशनल डेटाबेस
  • अनस्ट्रक्चर्ड डेटा सोर्स—उदाहरण के लिए, DataStore के साथ प्रोटोकॉल बफ़र
  • सामान्य फ़ाइलें

नेटवर्क डेटा सोर्स

नेटवर्क डेटा सोर्स, ऐप्लिकेशन की असल स्थिति होती है. सबसे सही तरीका यह है कि स्थानीय डेटा सोर्स को नेटवर्क डेटा सोर्स के साथ सिंक किया जाए. स्थानीय डेटा सोर्स, नेटवर्क डेटा सोर्स से पीछे भी रह सकता है. ऐसे में, ऑनलाइन होने पर ऐप्लिकेशन को अपडेट करना ज़रूरी है. इसके उलट, नेटवर्क डेटा सोर्स, स्थानीय डेटा सोर्स से पीछे रह सकता है. ऐसा तब तक हो सकता है, जब तक इंटरनेट कनेक्टिविटी वापस आने पर ऐप्लिकेशन उसे अपडेट न कर दे. ऐप्लिकेशन के डोमेन और यूज़र इंटरफ़ेस (यूआई) लेयर को नेटवर्क लेयर के साथ सीधे तौर पर कम्यूनिकेट नहीं करना चाहिए. होस्टिंग repository की ज़िम्मेदारी है कि वह इससे कम्यूनिकेट करे और स्थानीय डेटा सोर्स को अपडेट करने के लिए इसका इस्तेमाल करे.

कस्टम बनाम मैनेज किया गया सिंक्रनाइज़ेशन

ऑफ़लाइन-फ़र्स्ट डेटा लेयर बनाने के दो मुख्य तरीके हैं:

  • कस्टम सिंक्रनाइज़ेशन स्टैक: Room या लोकल स्टोरेज में मौजूद फ़ाइलों की मदद से, लोकल डेटाबेस को मैनेज किया जाता है. साथ ही, कस्टम नेटवर्क क्लाइंट बनाए जाते हैं और WorkManager की मदद से, डेटा को सिंक करने वाले वर्कर शेड्यूल किए जाते हैं. इस तरीके से, स्कीमा और कस्टम कॉन्फ़्लिक्ट रिज़ॉल्यूशन एल्गोरिदम पर ज़्यादा से ज़्यादा कंट्रोल मिलता है.
  • मैनेज की जाने वाली क्लाउड सेवाएं: Cloud Firestore जैसे समाधान, ऑल-इन-वन लोकल कैश और रीयल-टाइम क्लाउड स्ट्रीम उपलब्ध करा सकते हैं. यह एसडीके, डिस्क की कैश मेमोरी, ऑफ़लाइन राइट क्यूइंग, और दोनों तरफ़ से सिंक करने की प्रोसेस को अपने-आप मैनेज करता है. इससे, रीयल-टाइम डेटा स्ट्रीम सीधे आपके डेटा स्टोर करने की जगह पर पहुंचती हैं. रिलेशनल PostgreSQL बैकएंड के लिए, Firebase SQL Connect, टाइप-सेफ़ Kotlin क्लाइंट SDK टूल जनरेट करता है. ये SDK टूल, आपकी रिपॉज़िटरी के साथ आसानी से इंटिग्रेट हो जाते हैं. बिना स्ट्रक्चर वाले डेटा (जैसे, मीडिया, बड़ी फ़ाइलें या बैकअप) के लिए, Firebase के लिए Cloud Storage, क्लाउड पर होस्ट किया गया ऑब्जेक्ट स्टोरेज उपलब्ध कराता है. इसमें अपलोड और डाउनलोड करने की ऐसी सुविधाएं होती हैं जो नेटवर्क में रुकावट आने पर अपने-आप रुक जाती हैं और नेटवर्क वापस आने पर फिर से शुरू हो जाती हैं.

संसाधनों को एक्सपोज़ करना

लोकल और नेटवर्क डेटा सोर्स में, इस बात का बुनियादी अंतर हो सकता है कि आपका ऐप्लिकेशन उन्हें कैसे पढ़ सकता है और उनमें कैसे डेटा लिख सकता है. स्थानीय डेटा सोर्स से क्वेरी करना तेज़ और आसान हो सकता है. जैसे, SQL क्वेरी का इस्तेमाल करते समय. इसके उलट, नेटवर्क डेटा सोर्स धीमे और सीमित हो सकते हैं. जैसे, आईडी के हिसाब से RESTful रिसॉर्स को धीरे-धीरे ऐक्सेस करना. इस वजह से, हर डेटा सोर्स को अक्सर अपने डेटा को अलग तरीके से दिखाने की ज़रूरत होती है. इसलिए, हो सकता है कि स्थानीय डेटा सोर्स और नेटवर्क डेटा सोर्स के अपने मॉडल हों.

इस डायरेक्ट्री स्ट्रक्चर से, इस कॉन्सेप्ट को समझने में मदद मिलती है. AuthorEntity, ऐप्लिकेशन के लोकल डेटाबेस से पढ़े गए लेखक को दिखाता है. वहीं, NetworkAuthor, नेटवर्क पर सीरियलाइज़ किए गए लेखक को दिखाता है:

data/
├─ local/
│ ├─ entities/
│ │ ├─ AuthorEntity
│ ├─ dao/
│ ├─ NiADatabase
├─ network/
│ ├─ NiANetwork
│ ├─ models/
│ │ ├─ NetworkAuthor
├─ model/
│ ├─ Author
├─ repository/

AuthorEntity और NetworkAuthor की जानकारी यहां दी गई है:

/**
 * Network representation of [Author]
 */
@Serializable
data class NetworkAuthor(
    val id: String,
    val name: String,
    val imageUrl: String,
    val twitter: String,
    val mediumPage: String,
    val bio: String,
)

/**
 * Defines an author for either an [EpisodeEntity] or [NewsResourceEntity].
 * It has a many-to-many relationship with both entities
 */
@Entity(tableName = "authors")
data class AuthorEntity(
    @PrimaryKey
    val id: String,
    val name: String,
    @ColumnInfo(name = "image_url")
    val imageUrl: String,
    @ColumnInfo(defaultValue = "")
    val twitter: String,
    @ColumnInfo(name = "medium_page", defaultValue = "")
    val mediumPage: String,
    @ColumnInfo(defaultValue = "")
    val bio: String,
)

AuthorEntity और NetworkAuthor, दोनों को डेटा लेयर के अंदर रखना और बाहरी लेयर के लिए तीसरे टाइप को एक्सपोज़ करना एक अच्छा तरीका है. इससे बाहरी लेयर, लोकल और नेटवर्क डेटा सोर्स में होने वाले छोटे-मोटे बदलावों से सुरक्षित रहती हैं. इन बदलावों से ऐप्लिकेशन के काम करने के तरीके में कोई बुनियादी बदलाव नहीं होता. इसे यहां दिए गए स्निपेट में दिखाया गया है:

/**
 * External data layer representation of a "Now in Android" Author
 */
data class Author(
    val id: String,
    val name: String,
    val imageUrl: String,
    val twitter: String,
    val mediumPage: String,
    val bio: String,
)

इसके बाद, नेटवर्क मॉडल, एक्सटेंशन मेथड को तय कर सकता है, ताकि उसे लोकल मॉडल में बदला जा सके. इसी तरह, लोकल मॉडल में भी एक एक्सटेंशन मेथड होता है, ताकि उसे बाहरी मॉडल में बदला जा सके. इसके बारे में यहां दिए गए स्निपेट में बताया गया है:

/**
 * Converts the network model to the local model for persisting
 * by the local data source
 */
fun NetworkAuthor.asEntity() = AuthorEntity(
    id = id,
    name = name,
    imageUrl = imageUrl,
    twitter = twitter,
    mediumPage = mediumPage,
    bio = bio,
)

/**
 * Converts the local model to the external model for use
 * by layers external to the data layer
 */
fun AuthorEntity.asExternalModel() = Author(
    id = id,
    name = name,
    imageUrl = imageUrl,
    twitter = twitter,
    mediumPage = mediumPage,
    bio = bio,
)

पढ़ा गया

ऑफ़लाइन-फ़र्स्ट ऐप्लिकेशन में, ऐप्लिकेशन के डेटा को पढ़ना एक बुनियादी कार्रवाई है. इसलिए, आपको यह पक्का करना होगा कि आपका ऐप्लिकेशन डेटा को पढ़ सके. साथ ही, नया डेटा उपलब्ध होते ही ऐप्लिकेशन उसे दिखा सके. ऐसा करने वाला ऐप्लिकेशन, रिएक्टिव ऐप्लिकेशन होता है. ऐसा इसलिए, क्योंकि यह मॉनिटर किए जा सकने वाले टाइप के साथ, रीड एपीआई को एक्सपोज़ करता है.

यहां दिए गए स्निपेट में, OfflineFirstTopicRepository अपने सभी रीड एपीआई के लिए Flows दिखाता है. इससे, नेटवर्क डेटा सोर्स से अपडेट मिलने पर, यह अपने रीडर को अपडेट कर पाता है. दूसरे शब्दों में, यह OfflineFirstTopicRepository को बदलावों को पुश करने की अनुमति देता है, जब उसका स्थानीय डेटा सोर्स अमान्य हो जाता है. इसलिए, OfflineFirstTopicRepository के हर रीडर को डेटा में होने वाले उन बदलावों को मैनेज करने के लिए तैयार रहना चाहिए जो ऐप्लिकेशन के इंटरनेट कनेक्शन से फिर से कनेक्ट होने पर ट्रिगर हो सकते हैं. इसके अलावा, OfflineFirstTopicRepository सीधे स्थानीय डेटा सोर्स से डेटा को रीड करता है. यह अपने पाठकों को डेटा में हुए बदलावों की सूचना सिर्फ़ तब दे सकता है, जब यह पहले अपने स्थानीय डेटा सोर्स को अपडेट करे.

class TopicsViewModel(
    offlineFirstTopicsRepository: OfflineFirstTopicsRepository
) : ViewModel() {

    val topics: StateFlow<List<Topic>> = offlineFirstTopicsRepository.getTopicsStream()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList()
        )
}

Jetpack Compose ऐप्लिकेशन में, डेटा लेयर और यूज़र इंटरफ़ेस (यूआई) को जोड़ने के लिए, ViewModel का इस्तेमाल करें. ViewModel में, stateIn ऑपरेटर का इस्तेमाल करके Flow को StateFlow में बदलें. इसके बाद, कंपोज़ेबल collectAsStateWithLifecycle() का इस्तेमाल करके उन स्थितियों को इकट्ठा करते हैं. साथ ही, लाइफ़साइकल के हिसाब से सदस्यताएं अपने-आप मैनेज करते हैं.

collectAsStateWithLifecycle() के बारे में ज़्यादा जानने के लिए, स्टेट और Jetpack Compose देखें.

गड़बड़ी ठीक करने की रणनीतियां

ऑफ़लाइन-फ़र्स्ट ऐप्लिकेशन में गड़बड़ियों को मैनेज करने के अलग-अलग तरीके होते हैं. यह उन डेटा सोर्स पर निर्भर करता है जहां गड़बड़ियां हो सकती हैं. यहां दिए गए सब-सेक्शन में, इन रणनीतियों के बारे में बताया गया है.

स्थानीय डेटा सोर्स

स्थानीय डेटा सोर्स से डेटा पढ़ते समय, गड़बड़ियों को कम करने की कोशिश करें. रीडर को गड़बड़ियों से बचाने के लिए, उन Flow पर catch ऑपरेटर का इस्तेमाल करें जिनसे रीडर डेटा इकट्ठा कर रहा है.

ViewModel में catch ऑपरेटर का इस्तेमाल इस तरह किया जा सकता है:

class AuthorViewModel(
    authorsRepository: AuthorsRepository,
    ...
) : ViewModel() {
   private val authorId: String = ...

   // Observe author information
    private val authorStream: Flow<Author> =
        authorsRepository.getAuthorStream(
            id = authorId
        )
        .catch { emit(Author.empty()) }
}

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

// Define the LCE UI state
sealed interface AuthorUiState {
    data object Loading : AuthorUiState
    data class Success(val author: Author) : AuthorUiState
    data object Error : AuthorUiState
}

class AuthorViewModel(
    authorsRepository: AuthorsRepository,
    ...
) : ViewModel() {
    private val authorId: String = ...

    // Observe author information and map to LCE state
    val authorUiState: StateFlow<AuthorUiState> =
        authorsRepository.getAuthorStream(id = authorId)
            .map<Author, AuthorUiState> { author ->
                AuthorUiState.Success(author)
            }
            .catch { emit(AuthorUiState.Error) }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = AuthorUiState.Loading
            )
}

नेटवर्क डेटा सोर्स

अगर नेटवर्क डेटा सोर्स से डेटा ऐक्सेस करते समय गड़बड़ियां होती हैं, तो ऐप्लिकेशन को डेटा फ़ेच करने की फिर से कोशिश करने के लिए, ह्यूरिस्टिक का इस्तेमाल करना होगा. आम तौर पर, ह्यूरिस्टिक में ये शामिल होते हैं:

एक्स्पोनेंशियल बैकऑफ़

एक्स्पोनेंशियल बैकऑफ़ में, ऐप्लिकेशन नेटवर्क डेटा सोर्स से डेटा को पढ़ने की कोशिश करता रहता है. इसके लिए, वह समय अंतराल को तब तक बढ़ाता रहता है, जब तक कि उसे सफलता नहीं मिल जाती या अन्य शर्तें उसे रोकने के लिए नहीं कहतीं.

एक्स्पोनेंशियल बैकऑफ़ की मदद से डेटा पढ़ना
दूसरी इमेज: एक्स्पोनेंशियल बैकऑफ़ की मदद से डेटा पढ़ना.

ऐप्लिकेशन बार-बार बैकऑफ़ कर रहा है या नहीं, इसका आकलन करने के लिए ये शर्तें शामिल हैं:

  • नेटवर्क डेटा सोर्स से मिली गड़बड़ी का टाइप. उदाहरण के लिए, उन नेटवर्क कॉल को फिर से करें जो कनेक्टिविटी की कमी की वजह से गड़बड़ी दिखाती हैं. जब तक सही क्रेडेंशियल उपलब्ध न हों, तब तक उन एचटीटीपी अनुरोधों को फिर से न भेजें जिन्हें अनुमति नहीं मिली है.
  • ज़्यादा से ज़्यादा कितनी बार कोशिश की जा सकती है.
इंटरनेट कनेक्टिविटी की निगरानी करना

इस तरीके में, डेटा पढ़ने के अनुरोधों को तब तक सूची में रखा जाता है, जब तक ऐप्लिकेशन को यह पक्का नहीं हो जाता कि वह नेटवर्क डेटा सोर्स से कनेक्ट हो सकता है. कनेक्शन बन जाने के बाद, डेटा पढ़ने का अनुरोध सूची से हटा दिया जाता है. इसके बाद, डेटा पढ़ा जाता है और स्थानीय डेटा सोर्स को अपडेट किया जाता है. Android पर, इस सूची को Room डेटाबेस की मदद से मैनेज किया जा सकता है. साथ ही, WorkManager का इस्तेमाल करके, इसे लगातार काम के तौर पर खाली किया जा सकता है.

नेटवर्क मॉनिटर और सूची की मदद से डेटा पढ़ना
तीसरी इमेज: नेटवर्क मॉनिटरिंग की मदद से, रीड क्यू की जानकारी.

लिखना

ऑफ़लाइन-फ़र्स्ट ऐप्लिकेशन में डेटा पढ़ने के लिए, ऑब्ज़र्वेबल टाइप का इस्तेमाल करने का सुझाव दिया जाता है. वहीं, डेटा लिखने के लिए, एसिंक्रोनस एपीआई का इस्तेमाल किया जाता है. जैसे, सस्पेंड फ़ंक्शन. इससे यूज़र इंटरफ़ेस (यूआई) थ्रेड को ब्लॉक होने से रोका जा सकता है. साथ ही, गड़बड़ी को मैनेज करने में मदद मिलती है. ऐसा इसलिए, क्योंकि ऑफ़लाइन-फ़र्स्ट ऐप्लिकेशन में डेटा सेव करने की प्रोसेस, नेटवर्क की सीमा पार करने पर फ़ेल हो सकती है.

interface UserDataRepository {
    /**
     * Updates the bookmarked status for a news resource
     */
    suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean)
}

ऊपर दिए गए स्निपेट में, एसिंक्रोनस एपीआई के तौर पर Coroutines का इस्तेमाल किया गया है, क्योंकि यह तरीका निलंबित है.

लेखन से जुड़ी रणनीतियां

ऑफ़लाइन-फ़र्स्ट ऐप्लिकेशन में डेटा सेव करते समय, इन तीन रणनीतियों पर विचार किया जा सकता है. इनमें से कौनसा विकल्प चुनना है, यह इस बात पर निर्भर करता है कि किस तरह का डेटा सेव किया जा रहा है और ऐप्लिकेशन की ज़रूरतें क्या हैं:

सिर्फ़ ऑनलाइन मोड में लिखने की सुविधा

नेटवर्क की सीमा के बाहर डेटा लिखने की कोशिश करना. अगर यह प्रोसेस पूरी हो जाती है, तो स्थानीय डेटा सोर्स को अपडेट करें. अगर ऐसा नहीं होता है, तो अपवाद को थ्रो करें और कॉलर को सही तरीके से जवाब देने दें.

सिर्फ़ ऑनलाइन होने पर डेटा सेव करना
चौथी इमेज: सिर्फ़ ऑनलाइन मोड में डेटा सेव करने की सुविधा.

इस रणनीति का इस्तेमाल अक्सर उन ट्रांज़ैक्शन के लिए किया जाता है जो रीयल टाइम में ऑनलाइन होने चाहिए. जैसे, बैंक ट्रांसफ़र. डेटा सेव करने की प्रोसेस फ़ेल हो सकती है. इसलिए, उपयोगकर्ता को यह बताना ज़रूरी है कि डेटा सेव नहीं हो सका. इसके अलावा, उपयोगकर्ता को डेटा सेव करने से रोका जा सकता है. इन स्थितियों में, यहां दी गई रणनीतियों का इस्तेमाल किया जा सकता है:

  • अगर किसी ऐप्लिकेशन को डेटा लिखने के लिए इंटरनेट ऐक्सेस की ज़रूरत है, तो आपके पास उपयोगकर्ता को डेटा लिखने की अनुमति देने वाला यूज़र इंटरफ़ेस (यूआई) न दिखाने का विकल्प होता है. इसके अलावा, आपके पास इसे बंद करने का विकल्प भी होता है.
  • उपयोगकर्ता को यह बताने के लिए कि वह ऑफ़लाइन है, AlertDialog का इस्तेमाल किया जा सकता है. इसे उपयोगकर्ता खारिज नहीं कर सकता. इसके अलावा, Snackbar का इस्तेमाल भी किया जा सकता है.

सूची में जोड़े गए राइट

जब आपके पास कोई ऐसा ऑब्जेक्ट हो जिसे आपको लिखना है, तो उसे सूची में शामिल करें. जब ऐप्लिकेशन फिर से ऑनलाइन हो जाए, तो एक्स्पोनेंशियल बैकऑफ़ की मदद से सूची को खाली करें. Android पर, ऑफ़लाइन सूची को खाली करना एक ऐसा काम है जो लगातार चलता रहता है. इसे अक्सर WorkManager को सौंप दिया जाता है.

फिर से कोशिश करने की सुविधा वाली सूची बनाना
पांचवीं इमेज: फिर से कोशिश करने की सुविधा के साथ, सूची में डेटा सेव करना.

यह तरीका इन स्थितियों में काम आता है:

  • यह ज़रूरी नहीं है कि डेटा को कभी नेटवर्क पर सेव किया जाए.
  • लेन-देन को तय समय में पूरा करना ज़रूरी नहीं है.
  • अगर कार्रवाई पूरी नहीं हो पाती है, तो उपयोगकर्ता को इसकी जानकारी देना ज़रूरी नहीं है.

इस तरीके के इस्तेमाल के उदाहरणों में, आंकड़ों से जुड़े इवेंट और लॉगिंग शामिल हैं.

लेज़ी राइट्स

सबसे पहले, स्थानीय डेटा सोर्स में लिखें. इसके बाद, नेटवर्क को जल्द से जल्द सूचना देने के लिए, लिखने की प्रोसेस को सूची में जोड़ें. यह आसान नहीं है, क्योंकि ऐप्लिकेशन के फिर से ऑनलाइन होने पर, नेटवर्क और स्थानीय डेटा सोर्स के बीच टकराव हो सकता है. विवादों को सुलझाने के बारे में अगले सेक्शन में ज़्यादा जानकारी दी गई है.

नेटवर्क मॉनिटरिंग के साथ लेज़ी राइट्स
छठी इमेज: लेज़ी राइट्स.

यह तरीका तब सही होता है, जब डेटा ऐप्लिकेशन के लिए ज़रूरी हो. उदाहरण के लिए, ऑफ़लाइन काम करने वाले किसी टू-डू लिस्ट ऐप्लिकेशन में, यह ज़रूरी है कि उपयोगकर्ता जो भी टास्क ऑफ़लाइन जोड़े वह डिवाइस में सेव हो जाए, ताकि डेटा खोने का जोखिम न रहे.

सिंक करना और विरोध का हल

जब ऑफ़लाइन-फ़र्स्ट ऐप्लिकेशन अपनी कनेक्टिविटी वापस पाता है, तो उसे अपने स्थानीय डेटा सोर्स में मौजूद डेटा को नेटवर्क डेटा सोर्स में मौजूद डेटा के साथ मैच करना होता है. इस प्रोसेस को सिंक करना कहते हैं. कोई ऐप्लिकेशन, अपने नेटवर्क डेटा सोर्स के साथ दो मुख्य तरीकों से सिंक हो सकता है:

  • पुल-बेस्ड सिंक्रनाइज़ेशन
  • पुश-आधारित सिंक

पुल-बेस्ड सिंक्रनाइज़ेशन

पुल-आधारित सिंक्रनाइज़ेशन में, ऐप्लिकेशन नेटवर्क से संपर्क करके, मांग के हिसाब से ऐप्लिकेशन का नया डेटा पढ़ता है. इस तरीके के लिए, आम तौर पर नेविगेशन पर आधारित ह्यूरिस्टिक का इस्तेमाल किया जाता है. इसमें ऐप्लिकेशन, उपयोगकर्ता को डेटा दिखाने से ठीक पहले उसे फ़ेच करता है.

यह तरीका तब सबसे अच्छा काम करता है, जब ऐप्लिकेशन को कुछ समय के लिए इंटरनेट कनेक्शन न मिलने की उम्मीद हो. ऐसा इसलिए होता है, क्योंकि डेटा रीफ़्रेश करने की सुविधा, इंटरनेट कनेक्टिविटी उपलब्ध होने पर ही काम करती है. लंबे समय तक इंटरनेट कनेक्टिविटी न होने पर, इस बात की संभावना बढ़ जाती है कि उपयोगकर्ता, ऐप्लिकेशन के डेस्टिनेशन पर जाने की कोशिश करे. हालांकि, ऐसा करने पर उसे पुराना या खाली कैश मेमोरी वाला डेटा दिखेगा.

पुल पर आधारित सिंक्रनाइज़ेशन
सातवीं इमेज: पुल-बेस्ड सिंक्रनाइज़ेशन: डिवाइस A सिर्फ़ स्क्रीन A और B के लिए रिसॉर्स ऐक्सेस करता है, जबकि डिवाइस B सिर्फ़ स्क्रीन B, C, और D के लिए रिसॉर्स ऐक्सेस करता है.

मान लें कि कोई ऐप्लिकेशन, किसी स्क्रीन पर लगातार स्क्रोल होने वाली सूची में आइटम फ़ेच करने के लिए, पेज टोकन का इस्तेमाल करता है. ऐसा हो सकता है कि लागू करने की प्रोसेस, नेटवर्क तक पहुंचने में समय ले, डेटा को स्थानीय डेटा सोर्स में सेव करे, और फिर उपयोगकर्ता को जानकारी दिखाने के लिए, स्थानीय डेटा सोर्स से डेटा पढ़े. अगर नेटवर्क कनेक्टिविटी नहीं है, तो हो सकता है कि रिपॉज़िटरी सिर्फ़ लोकल डेटा सोर्स से डेटा का अनुरोध करे. यह पैटर्न, Jetpack Paging Library अपने RemoteMediator एपीआई के साथ इस्तेमाल करती है.

class FeedRepository(...) {

    fun feedPagingSource(): PagingSource<FeedItem> { ... }
}

class FeedViewModel(
    private val repository: FeedRepository
) : ViewModel() {
    private val pager = Pager(
        config = PagingConfig(
            pageSize = NETWORK_PAGE_SIZE,
            enablePlaceholders = false
        ),
        remoteMediator = FeedRemoteMediator(...),
        pagingSourceFactory = feedRepository::feedPagingSource
    )

    val feedPagingData = pager.flow
}

पुल-आधारित सिंक करने के फ़ायदे और नुकसान के बारे में यहां दी गई टेबल में बताया गया है:

फ़ायदे नुकसान
लागू करना आसान है. इसमें ज़्यादा डेटा खर्च होता है. ऐसा इसलिए होता है, क्योंकि किसी नेविगेशन डेस्टिनेशन पर बार-बार जाने से, ऐसी जानकारी को फिर से फ़ेच किया जाता है जिसमें कोई बदलाव नहीं हुआ है. सही तरीके से कैश मेमोरी का इस्तेमाल करके, इस समस्या को कम किया जा सकता है. ऐसा यूज़र इंटरफ़ेस (यूआई) लेयर में cachedIn ऑपरेटर की मदद से या नेटवर्क लेयर में एचटीटीपी कैश की मदद से किया जा सकता है.
जिस डेटा की ज़रूरत नहीं होती उसे कभी फ़ेच नहीं किया जाता. रिलेशनल डेटा के साथ अच्छी तरह से स्केल नहीं होता, क्योंकि पुल किए गए मॉडल को आत्मनिर्भर होना चाहिए. अगर सिंक किया जा रहा मॉडल, खुद को पॉप्युलेट करने के लिए अन्य मॉडल पर निर्भर है, तो डेटा के ज़्यादा इस्तेमाल की समस्या और भी बढ़ जाती है. इसके अलावा, इसकी वजह से पैरंट मॉडल और नेस्ट किए गए मॉडल की रिपॉज़िटरी के बीच डिपेंडेंसी हो सकती है.

पुश-आधारित सिंक

पुश-आधारित सिंक्रनाइज़ेशन में, स्थानीय डेटा सोर्स, नेटवर्क डेटा सोर्स के रेप्लिका सेट की नकल करने की पूरी कोशिश करता है. यह पहली बार चालू होने पर, ज़रूरी डेटा को अपने-आप फ़ेच करता है, ताकि बेसलाइन सेट की जा सके. इसके बाद, डेटा पुराना होने पर, यह सर्वर से मिलने वाली सूचनाओं पर निर्भर करता है.

पुश-आधारित सिंक
आठवीं इमेज: पुश-आधारित सिंक्रनाइज़ेशन: डेटा में बदलाव होने पर, नेटवर्क ऐप्लिकेशन को सूचना देता है. इसके बाद, ऐप्लिकेशन बदले हुए डेटा को फ़ेच करके जवाब देता है.

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

class UserDataRepository(...) {

    suspend fun synchronize() {
        val userData = networkDataSource.fetchUserData()
        localDataSource.saveUserData(userData)
    }
}

इस तरीके में, ऐप्लिकेशन नेटवर्क डेटा सोर्स पर बहुत कम निर्भर होता है. साथ ही, यह लंबे समय तक इसके बिना काम कर सकता है. यह ऑफ़लाइन होने पर, पढ़ने और लिखने, दोनों का ऐक्सेस देता है. ऐसा इसलिए, क्योंकि यह मान लेता है कि इसके पास नेटवर्क डेटा सोर्स से मिली नई जानकारी स्थानीय तौर पर मौजूद है.

पुश-आधारित सिंक करने के फ़ायदे और नुकसान के बारे में यहां दी गई टेबल में बताया गया है:

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

हाइब्रिड सिंक्रनाइज़ेशन

कुछ ऐप्लिकेशन, हाइब्रिड तरीके का इस्तेमाल करते हैं. यह तरीका, डेटा के आधार पर पुल या पुश पर आधारित होता है. उदाहरण के लिए, कोई सोशल मीडिया ऐप्लिकेशन, पुल-आधारित सिंक्रनाइज़ेशन का इस्तेमाल करके, उपयोगकर्ता के फ़ॉलो किए जा रहे फ़ीड को मांग पर फ़ेच कर सकता है. ऐसा इसलिए, क्योंकि फ़ीड में बार-बार अपडेट होते रहते हैं. ऐसा हो सकता है कि वही ऐप्लिकेशन, साइन इन किए हुए उपयोगकर्ता के डेटा के लिए, पुश-आधारित सिंक्रोनाइज़ेशन का इस्तेमाल करे. इसमें उपयोगकर्ता का नाम, प्रोफ़ाइल फ़ोटो वगैरह शामिल हैं.

ऑफ़लाइन-फ़र्स्ट सिंक्रनाइज़ेशन का विकल्प चुनना, प्रॉडक्ट की ज़रूरतों और उपलब्ध तकनीकी इन्फ़्रास्ट्रक्चर पर निर्भर करता है.

विवाद का हल

अगर ऑफ़लाइन होने पर, ऐप्लिकेशन स्थानीय तौर पर ऐसा डेटा सेव करता है जो नेटवर्क डेटा सोर्स से मेल नहीं खाता, तो सिंक करने से पहले आपको इस समस्या को हल करना होगा.

विवादों को सुलझाने के लिए, अक्सर वर्शन बनाने की ज़रूरत होती है. ऐप्लिकेशन को बदलावों का ट्रैक रखने के लिए, कुछ बुककीपिंग करनी होगी, ताकि वह नेटवर्क डेटा सोर्स को मेटाडेटा भेज सके. इसके बाद, नेटवर्क डेटा सोर्स की ज़िम्मेदारी होती है कि वह सटीक जानकारी उपलब्ध कराए. ऐप्लिकेशन की ज़रूरतों के हिसाब से, डेटा के टकराव को हल करने के लिए कई रणनीतियां अपनाई जा सकती हैं. मोबाइल ऐप्लिकेशन के लिए, "लास्ट राइट विंस" एक सामान्य तरीका है.

Last write wins

इस तरीके में, डिवाइस उस डेटा में टाइमस्टैंप मेटाडेटा अटैच करते हैं जिसे वे नेटवर्क पर लिखते हैं. जब नेटवर्क डेटा सोर्स को ये मिलते हैं, तो वह अपनी मौजूदा स्थिति से पुराने डेटा को खारिज कर देता है. साथ ही, अपनी मौजूदा स्थिति से नए डेटा को स्वीकार कर लेता है.

लास्ट राइट विंस कॉन्फ़्लिक्ट रिज़ॉल्यूशन
नौवीं इमेज: "लास्ट राइट विंस" डेटा के लिए सोर्स ऑफ़ ट्रूथ, डेटा लिखने वाली आखिरी इकाई से तय होता है.

इमेज 9 में, दोनों डिवाइस ऑफ़लाइन हैं और शुरुआत में नेटवर्क डेटा सोर्स के साथ सिंक किए गए हैं. ऑफ़लाइन होने पर, दोनों ही डिवाइसों पर डेटा सेव होता है. साथ ही, डेटा सेव होने का समय भी ट्रैक किया जाता है. जब दोनों डिवाइस फिर से ऑनलाइन आते हैं और नेटवर्क डेटा सोर्स के साथ सिंक होते हैं, तो नेटवर्क इस समस्या को हल करता है. इसके लिए, वह डिवाइस B का डेटा सेव रखता है, क्योंकि उसने अपना डेटा बाद में लिखा था.

ऑफ़लाइन-फ़र्स्ट ऐप्लिकेशन में WorkManager

रीड और राइट, दोनों रणनीतियों में दो सामान्य यूटिलिटी होती हैं:

  • क्यू की लिस्ट
    • रीड: इसका इस्तेमाल, नेटवर्क कनेक्टिविटी उपलब्ध होने तक रीड को रोकने के लिए किया जाता है.
    • राइट: इसका इस्तेमाल, नेटवर्क कनेक्टिविटी उपलब्ध होने तक राइट को रोकने और फिर से कोशिश करने के लिए, राइट को फिर से सूची में जोड़ने के लिए किया जाता है.
  • नेटवर्क कनेक्टिविटी मॉनिटर
    • रीड: इसका इस्तेमाल, ऐप्लिकेशन के कनेक्ट होने पर रीड सूची को खाली करने और सिंक करने के लिए किया जाता है.
    • Writes: इसका इस्तेमाल, ऐप्लिकेशन के कनेक्ट होने पर राइट क्यू को खाली करने और सिंक करने के लिए किया जाता है.

दोनों ही मामलों में, लगातार काम करने के उदाहरण दिए गए हैं. WorkManager इस तरह के काम को बेहतर तरीके से करता है.

उदाहरण के लिए, Now in Android सैंपल के तौर पर मिले ऐप्लिकेशन में, स्थानीय डेटा सोर्स को सिंक करते समय, WorkManager का इस्तेमाल रीड क्यू और नेटवर्क मॉनिटर, दोनों के तौर पर किया जाता है. चालू होने पर, ऐप्लिकेशन ये काम करता है:

  1. यह पक्का करने के लिए कि स्थानीय डेटा सोर्स और नेटवर्क डेटा सोर्स के बीच समानता हो, डेटा सिंक करने के काम को सूची में जोड़ता है.
  2. यह 'पढ़ा गया' के तौर पर मार्क किए गए मैसेज को सिंक करने की सूची को खाली कर देता है. साथ ही, ऐप्लिकेशन के ऑनलाइन होने पर सिंक करना शुरू कर देता है.
  3. एक्स्पोनेंशियल बैकऑफ़ का इस्तेमाल करके, नेटवर्क डेटा सोर्स से डेटा को पढ़ता है.
  4. यह स्थानीय डेटा सोर्स में रीड के नतीजों को सेव रखता है और होने वाली किसी भी समस्या को हल करता है.
  5. यह स्थानीय डेटा सोर्स से डेटा को एक्सपोज़ करता है, ताकि ऐप्लिकेशन की अन्य लेयर इसका इस्तेमाल कर सकें.

इन कार्रवाइयों को इस डायग्राम में दिखाया गया है:

Now in Android ऐप्लिकेशन में डेटा सिंक करना
दसवीं इमेज: Now in Android ऐप्लिकेशन में डेटा सिंक करना.

WorkManager की मदद से, सिंक करने के काम को सूची में शामिल करने के लिए, KEEP ExistingWorkPolicy का इस्तेमाल करके, इसे यूनीक वर्क के तौर पर सेट करें:

class SyncInitializer : Initializer<Sync> {
   override fun create(context: Context): Sync {
       WorkManager.getInstance(context).apply {
           // Queue sync on app startup and ensure only one
           // sync worker runs at any time
           enqueueUniqueWork(
               SyncWorkName,
               ExistingWorkPolicy.KEEP,
               SyncWorker.startUpSyncWork()
           )
       }
       return Sync
   }
}

SyncWorker.startupSyncWork() को इस तरह परिभाषित किया गया है:


/**
 Create a WorkRequest to call the SyncWorker using a DelegatingWorker.
 This allows for dependency injection into the SyncWorker in a different
 module than the app module without having to create a custom WorkManager
 configuration.
*/
fun startUpSyncWork() = OneTimeWorkRequestBuilder<DelegatingWorker>()
    // Run sync as expedited work if the app is able to.
    // If not, it runs as regular work.
   .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
   .setConstraints(SyncConstraints)
    // Delegate to the SyncWorker.
   .setInputData(SyncWorker::class.delegatedData())
   .build()

val SyncConstraints
   get() = Constraints.Builder()
       .setRequiredNetworkType(NetworkType.CONNECTED)
       .build()

खास तौर पर, SyncConstraints के मुताबिक Constraints के लिए ज़रूरी है कि NetworkType को NetworkType.CONNECTED किया जाए. इसका मतलब है कि यह तब तक इंतज़ार करता है, जब तक नेटवर्क उपलब्ध नहीं हो जाता.

नेटवर्क उपलब्ध होने पर, वर्कर, SyncWorkName की ओर से तय की गई यूनीक वर्क सूची को खाली कर देता है. इसके लिए, वह सही Repository इंस्टेंस को काम सौंपता है. अगर सिंक नहीं हो पाता है, तो doWork() तरीका Result.retry() के साथ वापस आता है. WorkManager, एक्स्पोनेंशियल बैकऑफ़ के साथ सिंक्रनाइज़ेशन की कोशिश अपने-आप करेगा. ऐसा न होने पर, यह Result.success() दिखाता है और सिंक करने की प्रोसेस पूरी हो जाती है.

class SyncWorker(...) : CoroutineWorker(appContext, workerParams), Synchronizer {

    override suspend fun doWork(): Result = withContext(ioDispatcher) {
        // First sync the repositories in parallel
        val syncedSuccessfully = awaitAll(
            async { topicRepository.sync() },
            async { authorsRepository.sync() },
            async { newsRepository.sync() },
        ).all { it }

        if (syncedSuccessfully) Result.success()
        else Result.retry()
    }
}

सैंपल

Google के ये सैंपल, ऑफ़लाइन-फ़र्स्ट ऐप्लिकेशन दिखाते हैं. इन दिशा-निर्देशों को व्यवहार में देखने के लिए, इन्हें एक्सप्लोर करें: