Çevrimdışı öncelikli bir uygulama geliştirme

Çevrimdışı öncelikli uygulama, temel işlevlerinin tamamını veya kritik bir alt kümesini internet erişimi olmadan gerçekleştirebilen bir uygulamadır. Yani, iş mantığının bir kısmını veya tamamını çevrimdışı olarak gerçekleştirebilir.

Çevrimdışı öncelikli uygulama oluştururken dikkat edilmesi gerekenler, uygulama verilerine ve iş mantığına erişim sağlayan veri katmanında başlar. Uygulamanın zaman zaman bu verileri cihaz dışındaki kaynaklardan yenilemesi gerekebilir. Bu işlem sırasında güncel kalmak için ağ kaynaklarını kullanması gerekebilir.

Ağ kullanılabilirliği her zaman garanti edilmez. Cihazlar genellikle kesintili veya yavaş ağ bağlantısı dönemleri yaşar. Kullanıcılar aşağıdakileri yaşayabilir:

  • Sınırlı internet bant genişliği
  • Asansör veya tünel gibi yerlerde geçici bağlantı kesintileri
  • Ara sıra veri erişimi (ör. yalnızca kablosuz ağ özellikli tabletler)

Nedeni ne olursa olsun, bir uygulamanın bu koşullarda yeterli düzeyde çalışması genellikle mümkündür. Uygulamanızın çevrimdışı olarak düzgün çalışması için aşağıdakileri yapabilmesi gerekir:

  • Güvenilir bir ağ bağlantısı olmadan kullanılabilir durumda kalma
  • İlk ağ çağrısının tamamlanmasını veya başarısız olmasını beklemek yerine kullanıcılara yerel verileri hemen sunun.
  • Verileri, pil ve veri durumunu dikkate alarak getirin. Örneğin, veri getirme isteklerini yalnızca şarj olurken veya kablosuz ağa bağlıyken olduğu gibi en uygun koşullarda gönderin.

Bu ölçütleri karşılayan uygulamalar genellikle çevrimdışı öncelikli uygulama olarak adlandırılır.

Öncelikle çevrimdışı çalışan bir uygulama tasarlama

Çevrimdışı öncelikli bir uygulama tasarlarken veri katmanından ve uygulama verileri üzerinde gerçekleştirebileceğiniz iki ana işlemden başlayın:

  • Okuma: Uygulamanın diğer bölümlerinde kullanılmak üzere verileri alma (ör. kullanıcıya bilgi gösterme). Compose'da bunu genellikle durumu gözlemleyerek yaparsınız. Kullanıcı arayüzünüz yerel veri kaynağını durum olarak gözlemlediğinde ekran, en son yerel verileri otomatik olarak yansıtır.
  • Yazma: Kullanıcı girişini daha sonra almak üzere saklama. Compose'da bunu genellikle kullanıcı arayüzünden ViewModel'e gönderilen etkinlikleri ve işlemleri kullanarak yaparsınız.

Veri katmanındaki depolar, uygulama verilerini sağlamak için veri kaynaklarını birleştirmekten sorumludur. Öncelikli olarak çevrimdışı çalışan bir uygulamada, en kritik görevlerini yerine getirmek için ağ erişimine ihtiyaç duymayan en az bir veri kaynağı olmalıdır. Bu kritik görevlerden biri veri okumadır.

Çevrimdışı öncelikli bir uygulamada verileri modelleme

Çevrimdışı öncelikli bir uygulamada, ağ kaynaklarını kullanan her depo için en az 2 veri kaynağı bulunur:

  • Yerel veri kaynağı
  • Ağ veri kaynağı
Önce çevrimdışı veri katmanı, hem yerel hem de ağ veri kaynaklarından oluşur.
Şekil 1: Çevrimdışı öncelikli bir depo.

Yerel veri kaynağı

Yerel veri kaynağı, uygulama için standart doğruluk kaynağıdır. Uygulamanın daha üst katmanlarının okuduğu tüm verilerin tek kaynağı olmalıdır. Bu sayede, bağlantı durumları arasında veri tutarlılığı sağlanır. Yerel veri kaynağı genellikle diske kaydedilen depolama alanıyla desteklenir. Verileri diske kaydetmek için yaygın olarak kullanılan yöntemlerden bazıları şunlardır:

  • Room gibi ilişkisel veritabanları gibi yapılandırılmış veri kaynakları
  • Yapılandırılmamış veri kaynakları (ör. DataStore ile protokol arabellekleri)
  • Basit dosyalar

Ağ veri kaynağı

Ağ veri kaynağı, uygulamanın gerçek durumudur. En iyi ihtimalle yerel veri kaynağı, ağ veri kaynağıyla senkronize edilir. Yerel veri kaynağı, ağ veri kaynağının gerisinde de kalabilir. Bu durumda, tekrar internete bağlanıldığında uygulamanın güncellenmesi gerekir. Tersine, ağ veri kaynağı, bağlantı geri geldiğinde uygulama tarafından güncellenene kadar yerel veri kaynağının gerisinde kalabilir. Uygulamanın alan ve kullanıcı arayüzü katmanları, ağ katmanıyla hiçbir zaman doğrudan iletişim kurmamalıdır. Barındıran repository, bu bileşenle iletişim kurmak ve yerel veri kaynağını güncellemek için bu bileşeni kullanmakla sorumludur.

Özel ve yönetilen senkronizasyon

Çevrimdışı öncelikli bir veri katmanı oluşturmak için iki temel yaklaşım kullanılabilir:

  • Özel senkronizasyon yığını: Room ile yerel veritabanlarını veya yerel depolama alanındaki dosyaları yönetir, özel ağ istemcileri oluşturur ve WorkManager ile mutabakat çalışanlarını planlarsınız. Bu yaklaşım, şemalar ve özel çakışma çözümü algoritmaları üzerinde maksimum kontrol sağlar.
  • Yönetilen bulut hizmetleri: Cloud Firestore gibi çözümler, hepsi bir arada yerel önbellek ve anlık bulut akışı sağlayabilir. SDK; disk önbelleğe alma, çevrimdışı yazma sıraya alma ve çift yönlü senkronizasyonu otomatik olarak gerçekleştirerek gerçek zamanlı veri akışlarını doğrudan deponuza iletir. İlişkisel PostgreSQL arka uçları için Firebase SQL Connect, depolarınızla sorunsuz bir şekilde entegre olan, tip güvenliği sağlayan Kotlin istemci SDK'ları oluşturur. Yapılandırılmamış veriler (ör. medya, büyük dosyalar veya yedekler) için Cloud Storage for Firebase, ağ kesintileri sırasında otomatik olarak duraklatılan ve devam ettirilen yükleme ve indirme işlemleriyle bulutta barındırılan nesne depolama alanı sağlar.

Kaynakları kullanıma sunma

Yerel ve ağ veri kaynakları, uygulamanızın bu kaynakları okuma ve yazma şekli açısından temel farklılıklar gösterebilir. Yerel bir veri kaynağını sorgulamak, SQL sorguları kullanırken olduğu gibi hızlı ve esnek olabilir. Buna karşılık, ağ veri kaynakları yavaş ve kısıtlı olabilir. Örneğin, RESTful kaynaklarına kimliğe göre aşamalı olarak erişirken bu durumla karşılaşabilirsiniz. Bu nedenle, her veri kaynağının genellikle sağladığı verilerin kendi temsiline ihtiyacı vardır. Bu nedenle, yerel veri kaynağı ve ağ veri kaynağının kendi modelleri olabilir.

Aşağıdaki dizin yapısı bu kavramı görselleştirmeye yardımcı olur. AuthorEntity, uygulamanın yerel veritabanından okunan bir yazarı, NetworkAuthor ise ağ üzerinden serileştirilen bir yazarı temsil eder:

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

AuthorEntity ve NetworkAuthor ile ilgili ayrıntılar aşağıda verilmiştir:

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

Hem AuthorEntity hem de NetworkAuthor türünü veri katmanında tutmak ve harici katmanların kullanması için üçüncü bir türü kullanıma sunmak iyi bir uygulamadır. Bu sayede, yerel ve ağ veri kaynaklarında yapılan ve uygulamanın davranışını temelden değiştirmeyen küçük değişikliklerden harici katmanlar korunur. Bu durum aşağıdaki snippet'te gösterilmiştir:

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

Ağ modeli, yerel modele dönüştürmek için bir uzantı yöntemi tanımlayabilir. Yerel model de benzer şekilde, aşağıdaki snippet'te gösterildiği gibi harici temsile dönüştürmek için bir uzantı yöntemine sahiptir:

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

Okumalar

Okuma, çevrimdışı öncelikli bir uygulamadaki uygulama verileri üzerinde yapılan temel işlemdir. Bu nedenle, uygulamanızın verileri okuyabildiğinden ve yeni veriler kullanıma sunulur sunulmaz uygulamanın bu verileri gösterebildiğinden emin olmanız gerekir. Bunu yapabilen bir uygulama, gözlemlenebilir türlere sahip okuma API'lerini kullanıma sunduğu için reaktif bir uygulamadır.

Aşağıdaki snippet'te OfflineFirstTopicRepository, tüm okuma API'leri için Flow döndürür. Bu sayede, ağ veri kaynağından güncelleme aldığında okuyucularını güncelleyebilir. Başka bir deyişle, yerel veri kaynağı geçersiz kılındığında OfflineFirstTopicRepository öğesinin değişiklikleri aktarmasına olanak tanır. Bu nedenle, OfflineFirstTopicRepository okuyucularının her biri, uygulamaya ağ bağlantısı yeniden sağlandığında tetiklenebilecek veri değişikliklerini işlemeye hazır olmalıdır. Ayrıca OfflineFirstTopicRepository, verileri doğrudan yerel veri kaynağından okur. Okuyucularını veri değişiklikleri hakkında bilgilendirmek için önce yerel veri kaynağını güncellemesi gerekir.

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

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

Jetpack Compose uygulamasında, veri katmanı ile kullanıcı arayüzü arasında köprü oluşturmak için ViewModel kullanın. ViewModel'de, stateIn operatörünü kullanarak Flow değerini StateFlow değerine dönüştürün. Ardından, composable'lar collectAsStateWithLifecycle() kullanarak bu durumları toplar ve abonelikleri yaşam döngüsüne duyarlı bir şekilde otomatik olarak yönetir.

collectAsStateWithLifecycle() hakkında daha fazla bilgi için State ve Jetpack Compose başlıklı makaleyi inceleyin.

Hata işleme stratejileri

Çevrimdışı öncelikli uygulamalarda hataları ele almanın, hataların oluşabileceği veri kaynaklarına bağlı olarak farklı yolları vardır. Aşağıdaki alt bölümlerde bu stratejiler açıklanmaktadır.

Yerel veri kaynağı

Yerel veri kaynağından okuma yaparken hataları en aza indirmeye çalışın. Okuyucuları hatalardan korumak için okuyucunun veri topladığı Flow'lerde catch operatörünü kullanın.

catch operatörünü ViewModel içinde aşağıdaki gibi kullanabilirsiniz:

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

Daha esnek bir yaklaşım için LCE (Yükleme İçeriği Hatası) çözümünü kullanabilirsiniz. LCE'de, okuma sırasında bir hata olduğunda hata durumu gösterirsiniz. Genellikle kullanıcı arayüzü durumlarını Kotlin sealed sınıfları olarak modelleyerek LCE'yi elde edersiniz.

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

Ağ veri kaynağı

Ağ veri kaynağından veri okunurken hata oluşursa uygulamanın, verileri getirmeyi yeniden denemek için bir buluşsal yöntem kullanması gerekir. Yaygın sezgisel yöntemler şunlardır:

Eksponansiyel geri yükleme

Eksponansiyel geri yükleme yönteminde uygulama, başarılı olana veya diğer koşullar durması gerektiğini belirtene kadar ağ veri kaynağından okuma yapmayı artan zaman aralıklarıyla dener.

Eksponansiyel geri yükleme ile verileri okuma
Şekil 2: Eksponansiyel geri yükleme ile veri okuma.

Uygulamanın geri çekilmeye devam edip etmediğini değerlendirmek için kullanılan ölçütler şunlardır:

  • Ağ veri kaynağının belirttiği hata türü. Örneğin, bağlantı eksikliğini gösteren bir hata döndüren ağ çağrılarını yeniden deneyin. Uygun kimlik bilgileri sağlanana kadar, yetkilendirilmemiş HTTP isteklerini yeniden denemeyin.
  • İzin verilen maksimum yeniden deneme sayısı.
Ağ bağlantısı izleme

Bu yaklaşımda, okuma istekleri uygulama ağ veri kaynağına bağlanabileceğinden emin olana kadar sıraya alınır. Bağlantı kurulduktan sonra okuma isteği sıradan çıkarılır, veriler okunur ve yerel veri kaynağı güncellenir. Android'de bu sıra, bir Room veritabanıyla yönetilebilir ve WorkManager kullanılarak kalıcı çalışma olarak boşaltılabilir.

Ağ izleyicileri ve sıralarla veri okuma
Şekil 3: Ağ izleme ile okuma sıraları.

Yazma işlemleri

Çevrimdışı öncelikli bir uygulamada veri okumanın önerilen yolu gözlemlenebilir türleri kullanmak olsa da yazma API'lerinin eşdeğeri, askıya alma işlevleri gibi eşzamansız API'lerdir. Bu sayede kullanıcı arayüzü iş parçacığının engellenmesi önlenir ve hata işleme konusunda yardımcı olunur. Çünkü çevrimdışı öncelikli uygulamalardaki yazma işlemleri, ağ sınırını aşarken başarısız olabilir.

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

Yukarıdaki snippet'te, yöntem askıya alındığı için tercih edilen eşzamansız API Coroutines'dir.

Yazma stratejileri

Çevrimdışı öncelikli uygulamalarda veri yazarken göz önünde bulundurulması gereken üç strateji vardır. Hangisini seçeceğiniz, yazılan verilerin türüne ve uygulamanın gereksinimlerine bağlıdır:

Yalnızca online yazma işlemleri

Verileri ağ sınırının ötesine yazma girişimi. İşlem başarılı olursa yerel veri kaynağını güncelleyin. Aksi takdirde bir istisna oluşturun ve uygun şekilde yanıt vermeyi çağırana bırakın.

Yalnızca online yazma işlemleri
Şekil 4: Yalnızca online yazma işlemleri.

Bu strateji genellikle neredeyse gerçek zamanlı olarak online gerçekleşmesi gereken yazma işlemleri (ör. banka havalesi) için kullanılır. Yazma işlemleri başarısız olabileceğinden, kullanıcıya yazma işleminin başarısız olduğunu bildirmek veya kullanıcının veri yazmaya çalışmasını engellemek genellikle gereklidir. Bu senaryolarda uygulayabileceğiniz bazı stratejiler şunlardır:

  • Bir uygulama veri yazmak için internet erişimi gerektiriyorsa kullanıcıya veri yazmasına olanak tanıyan bir kullanıcı arayüzü sunmamayı tercih edebilir veya en azından bu arayüzü devre dışı bırakabilirsiniz.
  • Kullanıcıya çevrimdışı olduğunu bildirmek için kullanıcının kapatamayacağı bir AlertDialog veya Snackbar kullanabilirsiniz.

Sıraya alınan yazma işlemleri

Yazmak istediğiniz bir nesne olduğunda bunu bir sıraya ekleyin. Uygulama tekrar internete bağlandığında eksponansiyel geri yükleme ile sırayı boşaltın. Android'de, çevrimdışı sırayı boşaltma işlemi genellikle WorkManager'a devredilen kalıcı bir iştir.

Yeniden denemelerle yazma kuyrukları
5. şekil: Yeniden denemelerle yazma kuyrukları.

Bu yaklaşım aşağıdaki senaryolarda iyi bir seçimdir:

  • Verilerin ağa yazılması şart değildir.
  • İşlem zamana duyarlı değildir.
  • İşlem başarısız olursa kullanıcının bilgilendirilmesi zorunlu değildir.

Bu yaklaşımın kullanım alanları arasında analiz etkinlikleri ve günlük kaydı yer alır.

Geç yazmalar

Önce yerel veri kaynağına yazın, ardından ağı en kısa sürede bilgilendirmek için yazma işlemini sıraya alın. Uygulama tekrar internete bağlandığında ağ ve yerel veri kaynakları arasında çakışmalar olabileceğinden bu işlem kolay değildir. Çatışma çözümüyle ilgili bir sonraki bölümde daha fazla ayrıntı bulabilirsiniz.

Ağ izleme ile geç yazma
Şekil 6: Gecikmeli yazma işlemleri.

Veriler uygulama için kritik öneme sahipse bu yaklaşım doğru seçimdir. Örneğin, çevrimdışı öncelikli bir yapılacaklar listesi uygulamasında, kullanıcının çevrimdışıyken eklediği görevlerin veri kaybı riskini önlemek için yerel olarak depolanması gerekir.

Senkronizasyon ve çakışma çözümü

Önce çevrimdışı uygulaması bağlantısını geri yüklediğinde yerel veri kaynağındaki verileri ağ veri kaynağındaki verilerle mutabık kılması gerekir. Bu işleme senkronizasyon denir. Bir uygulamanın ağ veri kaynağıyla senkronize olmasının iki ana yolu vardır:

  • Çekme tabanlı senkronizasyon
  • Push tabanlı senkronizasyon

Çekme tabanlı senkronizasyon

Çekme tabanlı senkronizasyonda uygulama, en son uygulama verilerini isteğe bağlı olarak okumak için ağa ulaşır. Bu yaklaşım için yaygın bir buluşsal yöntem, uygulamanın yalnızca kullanıcıya sunmadan hemen önce veri getirdiği gezinmeye dayalı yöntemdir.

Bu yaklaşım, uygulamanın kısa ila orta süreli ağ bağlantısı kesintileri beklediği durumlarda en iyi sonucu verir. Bunun nedeni, veri yenilemenin fırsatçı olması ve uzun süre bağlantı olmamasının, kullanıcının eski veya boş bir önbellekle uygulama hedeflerini ziyaret etmeye çalışma olasılığını artırmasıdır.

Çekme tabanlı senkronizasyon
Şekil 7: Çekme tabanlı senkronizasyon: A cihazı yalnızca A ve B ekranlarının kaynaklarına erişirken B cihazı yalnızca B, C ve D ekranlarının kaynaklarına erişir.

Belirli bir ekran için sonsuz kaydırma listesindeki öğeleri getirmek üzere sayfa jetonlarının kullanıldığı bir uygulama düşünün. Uygulama, ağa geç ulaşabilir, verileri yerel veri kaynağına kaydedebilir ve ardından kullanıcıya bilgileri geri sunmak için yerel veri kaynağından okuyabilir. Ağ bağlantısı yoksa depo, yalnızca yerel veri kaynağından veri isteyebilir. Bu, Jetpack Paging Library'nin RemoteMediator API'si ile kullandığı kalıptır.

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
}

Çekme tabanlı senkronizasyonun avantajları ve dezavantajları aşağıdaki tabloda özetlenmiştir:

Avantajları Dezavantajları
Uygulaması nispeten kolaydır. Yoğun veri kullanımına yatkındır. Bunun nedeni, bir navigasyon hedefine yapılan tekrarlanan ziyaretlerin, değişmeyen bilgilerin gereksiz yere yeniden getirilmesini tetiklemesidir. Doğru önbelleğe alma işlemiyle bu sorunu azaltabilirsiniz. Bu işlem, kullanıcı arayüzü katmanında cachedIn operatörüyle veya ağ katmanında bir HTTP önbelleğiyle yapılabilir.
Gerekli olmayan veriler hiçbir zaman alınmaz. Alınan modelin kendi kendine yeterli olması gerektiğinden ilişkisel verilerle iyi ölçeklenmez. Senkronize edilen model, kendisini doldurmak için diğer modellerin getirilmesine bağlıysa daha önce bahsedilen yoğun veri kullanımı sorunu daha da önemli hale gelir. Ayrıca, üst modelin depoları ile iç içe yerleştirilmiş modelin depoları arasında bağımlılıklara neden olabilir.

Push tabanlı senkronizasyon

Push tabanlı senkronizasyonda yerel veri kaynağı, ağ veri kaynağının replika grubunu elinden geldiğince taklit etmeye çalışır. İlk başlatmada, bir referans belirlemek için proaktif olarak uygun miktarda veri getirir. Ardından, veriler güncelliğini yitirdiğinde uyarı almak için sunucudan gelen bildirimleri kullanır.

Push tabanlı senkronizasyon
Şekil 8: Push tabanlı senkronizasyon: Ağ, veriler değiştiğinde uygulamayı bilgilendirir ve uygulama, değişen verileri getirerek yanıt verir.

Uygulama, eski bildirimini aldıktan sonra yalnızca eski olarak işaretlenen verileri güncellemek için ağa ulaşır. Bu iş, ağ veri kaynağına ulaşan ve getirilen verileri yerel veri kaynağına kaydeden Repository sınıfına devredilir. Depo, verilerini gözlemlenebilir türlerle sunduğu için okuyucular tüm değişikliklerden haberdar edilir.

class UserDataRepository(...) {

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

Bu yaklaşımda uygulama, ağ veri kaynağına çok daha az bağımlıdır ve uzun süreler boyunca ağ veri kaynağı olmadan çalışabilir. Ağ veri kaynağından en son bilgileri yerel olarak aldığını varsaydığı için çevrimdışıyken hem okuma hem de yazma erişimi sunar.

Push tabanlı senkronizasyonun avantajları ve dezavantajları aşağıdaki tabloda özetlenmiştir:

Avantajları Dezavantajları
Uygulama süresiz olarak çevrimdışı kalabilir. Çakışma çözümü için sürüm oluşturma verileri önemsiz değildir.
Minimum veri kullanımı. Uygulama yalnızca değişen verileri getirir. Senkronizasyon sırasında yazma endişelerini dikkate almanız gerekir.
İlişkisel veriler için idealdir. Her depo, yalnızca desteklediği model için veri getirmekten sorumludur. Ağ veri kaynağının senkronizasyonu desteklemesi gerekir.

Karma senkronizasyon

Bazı uygulamalar, verilere bağlı olarak çekme veya gönderme tabanlı karma bir yaklaşım kullanır. Örneğin, bir sosyal medya uygulaması, feed güncellemelerinin sıklığı nedeniyle kullanıcının takip ettiği feed'i isteğe bağlı olarak getirmek için çekme tabanlı senkronizasyon kullanabilir. Aynı uygulama, oturum açmış kullanıcının kullanıcı adı, profil resmi vb. verileri için push tabanlı senkronizasyonu kullanmayı tercih edebilir.

Sonuç olarak, çevrimdışı öncelikli senkronizasyon seçimi, ürün gereksinimlerine ve mevcut teknik altyapıya bağlıdır.

Çatışma çözümü

Uygulama çevrimdışıyken yerel olarak ağ veri kaynağıyla uyumlu olmayan veriler yazarsa senkronizasyonun gerçekleşebilmesi için çakışmayı çözmeniz gerekir.

Çakışma çözümü genellikle sürüm oluşturmayı gerektirir. Uygulamanın, değişikliklerin ne zaman gerçekleştiğini takip etmek için bazı kayıtlar tutması gerekir. Böylece meta verileri ağ veri kaynağına iletebilir. Ağ veri kaynağı, mutlak doğruluk kaynağını sağlama sorumluluğuna sahiptir. Uygulamanın ihtiyaçlarına bağlı olarak, çakışma çözümü için dikkate alınması gereken birçok strateji vardır. Mobil uygulamalarda yaygın bir yaklaşım, "son yazan kazanır" yaklaşımıdır.

Son yazma işlemi geçerli olur

Bu yaklaşımda cihazlar, ağa yazdıkları verilere zaman damgası meta verileri ekler. Ağ veri kaynağı bu verileri aldığında, mevcut durumundan daha eski olan tüm verileri siler ve mevcut durumundan daha yeni olanları kabul eder.

Son yazma kazanır çakışma çözümü
Şekil 9: "Son yazan kazanır" Verilerin doğru kaynağı, verileri yazan son varlık tarafından belirlenir.

Şekil 9'da her iki cihaz da çevrimdışı ve başlangıçta ağ veri kaynağıyla senkronize durumda. Çevrimdışıyken her ikisi de verileri yerel olarak yazar ve verilerini yazdıkları zamanı takip eder. Her iki cihaz da tekrar internete bağlanıp ağ veri kaynağıyla senkronize edildiğinde ağ, B cihazının verilerini daha sonra yazdığı için bu verileri koruyarak çakışmayı çözer.

Çevrimdışı öncelikli uygulamalarda WorkManager

Daha önce ele alınan hem okuma hem de yazma stratejilerinde iki yaygın yardımcı program vardır:

  • Sıralar
    • Okuma: Ağ bağlantısı kullanılabilir olana kadar okuma işlemlerini ertelemek için kullanılır.
    • Yazma işlemleri: Ağ bağlantısı sağlanana kadar yazma işlemlerini ertelemek ve yeniden denemeler için yazma işlemlerini yeniden sıraya koymak amacıyla kullanılır.
  • Ağ bağlantısı monitörleri
    • Okuma: Uygulama bağlandığında okuma sırasını boşaltmak için bir sinyal olarak ve senkronizasyon için kullanılır.
    • Yazma: Uygulama bağlıyken yazma sırasını boşaltmak için bir sinyal olarak ve senkronizasyon için kullanılır.

Her iki durum da WorkManager'ın başarılı olduğu kalıcı çalışma örnekleridir.

Örneğin, Now in Android örnek uygulamasında WorkManager, yerel veri kaynağı senkronize edilirken hem okuma sırası hem de ağ monitörü olarak kullanılır. Uygulama, başlatıldığında şunları yapar:

  1. Yerel veri kaynağı ile ağ veri kaynağı arasında eşitlik olduğundan emin olmak için okuma senkronizasyonu işini sıraya alır.
  2. Okuma senkronizasyonu sırasını boşaltır ve uygulama internete bağlıyken senkronizasyonu başlatır.
  3. Eksponansiyel geri yükleme kullanarak ağ veri kaynağından okuma işlemi gerçekleştirir.
  4. Okuma sonuçlarını yerel veri kaynağına kaydeder ve ortaya çıkan tüm çakışmaları çözer.
  5. Yerel veri kaynağından gelen verileri, uygulamanın diğer katmanlarının kullanması için sunar.

Bu işlemler aşağıdaki diyagramda gösterilmektedir:

Now in Android uygulamasında veri senkronizasyonu
Şekil 10: Now in Android uygulamasında veri senkronizasyonu.

Senkronizasyon işinin WorkManager ile sıraya alınması, KEEP ExistingWorkPolicy ile benzersiz iş olarak belirtilerek yapılır:

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() şu şekilde tanımlanır:


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

Özellikle, SyncConstraints tarafından tanımlanan Constraints, NetworkType değerinin NetworkType.CONNECTED olmasını gerektirir. Yani çalışmadan önce ağın kullanılabilir olmasını bekler.

Ağ kullanılabilir olduğunda Worker, uygun Repository örneklerine yetki vererek SyncWorkName tarafından belirtilen benzersiz iş sırasını boşaltır. Senkronizasyon başarısız olursa doWork() yöntemi Result.retry() ile döndürülür. WorkManager, eksponansiyel geri yükleme ile senkronizasyonu otomatik olarak yeniden dener. Aksi takdirde Result.success() döndürülür ve senkronizasyon tamamlanır.

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

Örnekler

Aşağıdaki Google örneklerinde, çevrimdışı öncelikli uygulamalar gösterilmektedir. Bu kılavuzun pratikteki işleyişini görmek için bu kaynakları inceleyin: