Veri katmanı

Kullanıcı arayüzü katmanı, kullanıcı arayüzüyle ilgili durumu ve kullanıcı arayüzü mantığını içerirken veri katmanı uygulama verilerini ve iş mantığını içerir. İş mantığı, uygulamanıza değer katan unsurdur. Uygulama verilerinin nasıl oluşturulması, depolanması ve değiştirilmesi gerektiğini belirleyen gerçek dünya iş kurallarından oluşur.

Bu ayrım, veri katmanının birden fazla ekranda kullanılmasına, uygulamanın farklı bölümleri arasında bilgi paylaşılmasına ve birim testi için kullanıcı arayüzü dışında iş mantığının yeniden üretilmesine olanak tanır. Veri katmanının avantajları hakkında daha fazla bilgi edinmek için Mimariye Genel Bakış sayfasını inceleyin.

Veri katmanı mimarisi

Veri katmanı, her biri sıfır ila çok sayıda veri kaynağı içerebilen depolardan oluşur. Uygulamanızda işlediğiniz her farklı veri türü için bir depo sınıfı oluşturmanız gerekir. Örneğin, filmlerle ilgili veriler için bir MoviesRepository sınıfı veya ödemelerle ilgili veriler için bir PaymentsRepository sınıfı oluşturabilirsiniz.

Tipik bir mimaride, veri katmanının depoları uygulamanın geri kalanına veri sağlar ve veri kaynaklarına bağlıdır.
Şekil 1. Veri katmanının uygulama mimarisindeki rolü.

Depo sınıfları aşağıdaki görevlerden sorumludur:

  • Verileri uygulamanın geri kalanına sunma
  • Verilerde yapılan değişiklikleri merkezileştirme.
  • Birden fazla veri kaynağı arasındaki çakışmaları çözme
  • Veri kaynaklarını uygulamanın geri kalanından ayırma
  • İş mantığı içerir.

Her veri kaynağı sınıfı, yalnızca bir veri kaynağıyla (dosya, ağ kaynağı veya yerel veritabanı olabilir) çalışma sorumluluğuna sahip olmalıdır. Veri kaynağı sınıfları, veri işlemleri için uygulama ile sistem arasındaki köprüdür.

Hiyerarşideki diğer katmanlar hiçbir zaman doğrudan veri kaynaklarına erişmemelidir. Veri katmanına giriş noktaları her zaman depo sınıflarıdır. Durum bilgisi depolayıcı sınıflar (UI katmanı kılavuzuna bakın) veya kullanım alanı sınıfları (alan katmanı kılavuzuna bakın) hiçbir zaman doğrudan bağımlılık olarak veri kaynağına sahip olmamalıdır. Giriş noktası olarak depo sınıflarını kullanmak, mimarinin farklı katmanlarının bağımsız olarak ölçeklenmesine olanak tanır.

Bu katman tarafından kullanıma sunulan veriler değişmez olmalıdır. Böylece, değerlerini tutarsız bir duruma getirme riski taşıyan diğer sınıflar tarafından değiştirilemez. Değiştirilemez veriler, birden fazla iş parçacığı tarafından da güvenli bir şekilde işlenebilir. Daha fazla ayrıntı için iş parçacığı oluşturma bölümüne bakın.

Bağımlılık ekleme ile ilgili en iyi uygulamalara uygun olarak, depo oluşturucusunda bağımlılık olarak veri kaynaklarını alır:

class ExampleRepository(
    private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
    private val exampleLocalDataSource: ExampleLocalDataSource // database
) { /* ... */ }

API'leri kullanıma sunma

Veri katmanındaki sınıflar genellikle tek seferluk oluşturma, okuma, güncelleme ve silme (CRUD) çağrıları yapmak veya zaman içinde veri değişikliklerinden haberdar olmak için işlevler sunar. Veri katmanı, bu durumların her biri için aşağıdakileri göstermelidir:

  • Tek seferlik işlemler için askıya alma işlevlerini kullanıma sunun.
  • Zaman içindeki veri değişikliklerinden haberdar olmak için akışları kullanıma sunun.
class ExampleRepository(
    private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
    private val exampleLocalDataSource: ExampleLocalDataSource // database
) {

    val data: Flow<Example> = ...

    suspend fun modifyData(example: Example) { ... }
}

Bu kılavuzdaki adlandırma kuralları

Bu kılavuzda, depo sınıfları sorumlu oldukları verilere göre adlandırılır. Kural aşağıdaki gibidir:

Veri türü + Depo.

Örneğin: NewsRepository, MoviesRepository veya PaymentsRepository.

Veri kaynağı sınıfları, sorumlu oldukları verilere ve kullandıkları kaynağa göre adlandırılır. Kural aşağıdaki gibidir:

Veri türü + Kaynak türü + DataSource.

Veri türü için Uzak veya Yerel'i kullanın. Uygulamalar değişebileceğinden bu terimler daha geneldir. Örneğin: NewsRemoteDataSource veya NewsLocalDataSource. Kaynak önemliyse daha ayrıntılı bilgi vermek için kaynağın türünü kullanın. Örneğin: NewsNetworkDataSource veya NewsDiskDataSource.

Veri kaynağını bir uygulama ayrıntısına göre adlandırmayın. Örneğin, UserSharedPreferencesDataSource. Çünkü bu veri kaynağını kullanan depolar, verilerin nasıl kaydedildiğini bilmemelidir. Bu kurala uyarsanız veri kaynağının uygulamasını (örneğin, SharedPreferences'tan DataStore'a geçiş) bu kaynağı çağıran katmanı etkilemeden değiştirebilirsiniz.

Birden fazla depo düzeyi

Daha karmaşık iş gereksinimlerinin olduğu bazı durumlarda, bir deponun diğer depolara bağlı olması gerekebilir. Bunun nedeni, söz konusu verilerin birden fazla veri kaynağının birleştirilmesiyle elde edilmiş olması veya sorumluluğun başka bir depo sınıfında kapsüllenmesi gerekmesi olabilir.

Örneğin, kullanıcı kimlik doğrulama verilerini işleyen bir depo (UserRepository), gereksinimlerini karşılamak için LoginRepository ve RegistrationRepository gibi diğer depolara bağlı olabilir.

Örnekte UserRepository, diğer iki depo sınıfına bağlıdır:
    Diğer giriş veri kaynaklarına bağlı olan LoginRepository ve
    Diğer kayıt veri kaynaklarına bağlı olan RegistrationRepository.
Şekil 2. Diğer depolara bağlı bir deponun bağımlılık grafiği.

Veri kaynağı

Her depoda tek bir doğru kaynağın tanımlanması önemlidir. Doğruluk kaynağı her zaman tutarlı, doğru ve güncel veriler içerir. Hatta, havuzdan sunulan veriler her zaman doğrudan doğruluk kaynağından gelen veriler olmalıdır.

Doğruluk kaynağı, bir veri kaynağı (ör. veritabanı) veya hatta depoda bulunabilecek bir bellek içi önbellek olabilir. Depolar, farklı veri kaynaklarını birleştirir ve tek doğru kaynağı düzenli olarak ya da kullanıcı girişi etkinliği nedeniyle güncellemek için veri kaynakları arasındaki olası çakışmaları çözer.

Uygulamanızdaki farklı depoların farklı doğruluk kaynakları olabilir. Örneğin, LoginRepository sınıfı, doğru kaynağı olarak önbelleğini kullanabilir ve PaymentsRepository sınıfı, ağ veri kaynağını kullanabilir.

Çevrimdışı öncelikli destek sağlamak için veritabanı gibi yerel bir veri kaynağının doğru kaynak olarak kullanılması önerilir.

İş parçacığı oluşturma

Veri kaynaklarını ve depoları çağırma ana ileti dizisinde güvenli olmalıdır. Bu sınıflar, uzun süren engelleme işlemleri gerçekleştirirken mantıklarının yürütülmesini uygun iş parçacığına taşımaktan sorumludur. Örneğin, bir veri kaynağının bir dosyadan okuma yapması veya bir deponun büyük bir listede maliyetli filtreleme yapması için ana iş parçacığı güvenli olmalıdır.

Çoğu veri kaynağının, Room, Retrofit veya Ktor tarafından sağlanan askıya alma yöntemi çağrıları gibi ana iş parçacığına güvenli API'ler sunduğunu unutmayın. Deponuz, bu API'ler kullanıma sunulduğunda bunlardan yararlanabilir.

İş parçacığı oluşturma hakkında daha fazla bilgi edinmek için Arka planda işleme kılavuzu'na bakın. Kotlin kullanıcıları için önerilen seçenek coroutines'dir.

Yaşam döngüsü

Veri katmanındaki sınıf örnekleri, bir atık toplama kökünden erişilebildiği sürece (genellikle uygulamanızdaki diğer nesnelerden referans verilerek) bellekte kalır.

Bir sınıf, bellekteki verileri (ör. önbellek) içeriyorsa bu sınıfın aynı örneğini belirli bir süre boyunca yeniden kullanmak isteyebilirsiniz. Bu, sınıf örneğinin yaşam döngüsü olarak da adlandırılır.

Sınıfın sorumluluğu uygulamanın tamamı için kritik öneme sahipse bu sınıfın bir örneğini Application sınıfıyla kapsayabilirsiniz. Bu sayede örnek, uygulamanın yaşam döngüsünü takip eder. Alternatif olarak, uygulamanızdaki belirli bir akışta (ör. kayıt veya giriş akışı) aynı örneği yeniden kullanmanız gerekiyorsa örneği, bu akışın yaşam döngüsüne sahip olan sınıfla sınırlandırmanız gerekir. Örneğin, bellekteki verileri içeren bir RegistrationRepository öğesini RegistrationActivity kapsamına veya NavEntryDecorator kullanarak bir geri yığın kapsamına alabilirsiniz.

Her örneğin yaşam döngüsü, uygulamanızdaki bağımlılıkların nasıl sağlanacağına karar vermede kritik bir faktördür. Bağımlılıkların yönetildiği ve bağımlılık kapsayıcılarına göre kapsamlandırıldığı bağımlılık ekleme ile ilgili en iyi uygulamaları kullanmanız önerilir. Android'de kapsam belirleme hakkında daha fazla bilgi edinmek için Android ve Hilt'te Kapsam Belirleme başlıklı blog yayınını inceleyin.

İş modellerini temsil etme

Veri katmanından kullanıma sunmak istediğiniz veri modelleri, farklı veri kaynaklarından aldığınız bilgilerin bir alt kümesi olabilir. İdeal olarak, hem ağ hem de yerel olan farklı veri kaynakları yalnızca uygulamanızın ihtiyaç duyduğu bilgileri döndürmelidir ancak bu durum genellikle böyle değildir.

Örneğin, yalnızca makale bilgilerini değil, düzenleme geçmişini, kullanıcı yorumlarını ve bazı meta verileri de döndüren bir Haber API'si sunucusu olduğunu düşünün:

data class ArticleApiModel(
    val id: Long,
    val title: String,
    val content: String,
    val publicationDate: Date,
    val modifications: Array<ArticleApiModel>,
    val comments: Array<CommentApiModel>,
    val lastModificationDate: Date,
    val authorId: Long,
    val authorName: String,
    val authorDateOfBirth: Date,
    val readTimeMin: Int
)

Uygulama, makalenin yalnızca içeriğini ve yazarıyla ilgili temel bilgileri ekranda gösterdiğinden makaleyle ilgili bu kadar çok bilgiye ihtiyaç duymaz. Model sınıflarını ayırmak ve depolarınızın yalnızca hiyerarşinin diğer katmanlarının ihtiyaç duyduğu verileri göstermesini sağlamak iyi bir uygulamadır. Örneğin, ArticleApiModel model sınıfını alan ve kullanıcı arayüzü katmanlarına göstermek için ağdaki ArticleApiModel öğesini nasıl küçültebileceğiniz aşağıda açıklanmıştır:Article

data class Article(
    val id: Long,
    val title: String,
    val content: String,
    val publicationDate: Date,
    val authorName: String,
    val readTimeMin: Int
)

Model sınıflarını ayırmak şu açılardan faydalıdır:

  • Yalnızca gerekli verileri kullanarak uygulama belleğinden tasarruf sağlar.
  • Harici veri türlerini uygulamanızın kullandığı veri türlerine uyarlar. Örneğin, uygulamanız tarihleri temsil etmek için farklı bir veri türü kullanabilir.
  • İlgi alanlarının daha iyi ayrılmasını sağlar. Örneğin, model sınıfı önceden tanımlanmışsa büyük bir ekibin üyeleri bir özelliğin ağ ve kullanıcı arayüzü katmanlarında ayrı ayrı çalışabilir.

Bu uygulamayı genişletebilir ve uygulama mimarinizin diğer bölümlerinde (ör. veri kaynağı sınıfları ve ViewModel'ler) ayrı model sınıfları da tanımlayabilirsiniz. Ancak bu, düzgün bir şekilde belgelemeniz ve test etmeniz gereken ek sınıflar ve mantık tanımlamanızı gerektirir. En azından, bir veri kaynağı uygulamanızın geri kalanının beklediğiyle eşleşmeyen veriler aldığında yeni modeller oluşturmanız önerilir.

Veri işlemleri türleri

Veri katmanı, önem derecesine göre değişen işlem türleriyle (kullanıcı arayüzü odaklı, uygulama odaklı ve işletme odaklı işlemler) ilgilenebilir.

Kullanıcı arayüzü odaklı işlemler

Kullanıcı arayüzü odaklı işlemler yalnızca kullanıcı belirli bir ekrandayken geçerlidir ve kullanıcı bu ekrandan ayrıldığında iptal edilir. Örneğin, veri tabanından alınan bazı verilerin gösterilmesi.

Kullanıcı arayüzü odaklı işlemler genellikle kullanıcı arayüzü katmanı tarafından tetiklenir ve arayanın yaşam döngüsünü (ör. ViewModel'in yaşam döngüsü) takip eder. Kullanıcı arayüzü odaklı bir işlem örneği için Ağ isteğinde bulunma bölümüne bakın.

Uygulama odaklı işlemler

Uygulama açık olduğu sürece uygulamaya yönelik işlemler geçerlidir. Uygulama kapatılırsa veya işlem sonlandırılırsa bu işlemler iptal edilir. Örneğin, bir ağ isteğinin sonucunu gerektiğinde daha sonra kullanabilmek için önbelleğe alma işlemi yapılabilir. Daha fazla bilgi edinmek için Bellek içi veri önbelleğe almayı uygulama bölümüne bakın.

Bu işlemler genellikle Application sınıfının veya veri katmanının yaşam döngüsünü takip eder. Örnek için Bir işlemi ekrandan daha uzun süre canlı tutma bölümüne bakın.

İşletmeye yönelik işlemler

İşletmeye yönelik işlemler iptal edilemez. İşlem sonlandırmadan etkilenmemelidir. Örneğin, kullanıcının profilinde yayınlamak istediği bir fotoğrafın yüklenmesini tamamlamak.

İşletmeye yönelik işlemler için WorkManager kullanılması önerilir. Daha fazla bilgi edinmek için WorkManager'ı kullanarak görevleri planlama bölümüne bakın.

Hataları ortaya çıkarma

Depolar ve veri kaynaklarıyla etkileşimler başarılı olabilir veya bir hata oluştuğunda istisna oluşturabilir. Coroutine'ler ve akışlar için Kotlin'in yerleşik hata işleme mekanizmasını kullanmanız gerekir. Askıya alma işlevlerinin tetikleyebileceği hatalar için uygun olduğunda try/catch bloklarını kullanın. Akışlarda ise catch operatörünü kullanın. Bu yaklaşımla, veri katmanı çağrılırken kullanıcı arayüzü katmanının istisnaları işlemesi beklenir.

Veri katmanı, farklı hata türlerini anlayıp işleyebilir ve bunları özel istisnalar kullanarak (ör. UserNotAuthenticatedException) gösterebilir.

Coroutine'lerdeki hatalar hakkında daha fazla bilgi edinmek için Coroutine'lerdeki istisnalar blog yayınını inceleyin.

Genel görevler

Aşağıdaki bölümlerde, Android uygulamalarında yaygın olarak kullanılan belirli görevleri gerçekleştirmek için veri katmanının nasıl kullanılacağına ve tasarlanacağına dair örnekler verilmiştir. Örnekler, kılavuzun önceki bölümlerinde bahsedilen tipik Haber uygulamasına dayanmaktadır.

Ağ isteğinde bulunma

Ağ isteğinde bulunmak, bir Android uygulamasının gerçekleştirebileceği en yaygın görevlerden biridir. Haber uygulaması, kullanıcılara ağdan alınan en son haberleri göstermelidir. Bu nedenle, uygulamanın ağ işlemlerini yönetmek için bir veri kaynağı sınıfına ihtiyacı vardır: NewsRemoteDataSource. Bilgileri uygulamanın geri kalanına sunmak için haber verileri üzerindeki işlemleri yöneten yeni bir depo oluşturulur: NewsRepository.

Kullanıcı ekranı açtığında en son haberlerin her zaman güncellenmesi gerekir. Bu nedenle, kullanıcı arayüzü odaklı bir işlemdir.

Veri kaynağını oluşturma

Veri kaynağı, en son haberleri döndüren bir işlev sunmalıdır: ArticleHeadline örneklerinin listesi. Veri kaynağı, ağdan en son haberleri almanın güvenli bir yolunu sağlamalıdır. Bunun için görevi çalıştırmak üzere CoroutineDispatcher veya Executor öğesine bağımlı olması gerekir.

Ağ isteğinde bulunmak, yeni bir fetchLatestNews() yöntemiyle işlenen tek seferlik bir çağrıdır:

class NewsRemoteDataSource(
  private val newsApi: NewsApi,
  private val ioDispatcher: CoroutineDispatcher
) {
    /**
     * Fetches the latest news from the network and returns the result.
     * This executes on an IO-optimized thread pool, the function is main-safe.
     */
    suspend fun fetchLatestNews(): List<ArticleHeadline> =
        // Move the execution to an IO-optimized thread since the ApiService
        // doesn't support coroutines and makes synchronous requests.
        withContext(ioDispatcher) {
            newsApi.fetchLatestNews()
        }
    }

// Makes news-related network synchronous requests.
interface NewsApi {
    fun fetchLatestNews(): List<ArticleHeadline>
}

NewsApi arayüzü, ağ API istemcisinin uygulamasını gizler. Arayüzün Retrofit veya HttpURLConnection tarafından desteklenmesi fark etmez. Arayüzleri kullanmak, API uygulamalarının uygulamanızda değiştirilebilir olmasını sağlar.

Depoyu oluşturma

Bu görev için depo sınıfında ek mantık gerekmediğinden, NewsRepository ağ veri kaynağı için proxy görevi görür. Bu ek soyutlama katmanını eklemenin avantajları bellek içi önbelleğe alma bölümünde açıklanmıştır.

// NewsRepository is consumed from other layers of the hierarchy.
class NewsRepository(
    private val newsRemoteDataSource: NewsRemoteDataSource
) {
    suspend fun fetchLatestNews(): List<ArticleHeadline> =
        newsRemoteDataSource.fetchLatestNews()
}

Depo sınıfını doğrudan kullanıcı arayüzü katmanından nasıl kullanacağınızı öğrenmek için Kullanıcı arayüzü katmanı kılavuzuna bakın.

Bellek içi veri önbelleğe almayı uygulama

Haber uygulaması için yeni bir şart getirildiğini varsayalım: Kullanıcı ekranı açtığında daha önce istekte bulunulmuşsa kullanıcılara önbelleğe alınmış haberler sunulmalıdır. Aksi takdirde, uygulamanın en son haberleri getirmek için bir ağ isteğinde bulunması gerekir.

Yeni şart uyarınca, kullanıcı uygulamayı açık tuttuğu sürece uygulama en son haberleri bellekte saklamalıdır. Dolayısıyla bu, uygulama odaklı bir işlemdir.

Önbellekler

Bellek içi veri önbelleğe alma özelliği ekleyerek kullanıcı uygulamanızdayken verileri koruyabilirsiniz. Önbellekler, belirli bir süre boyunca (bu durumda, kullanıcı uygulamada kaldığı sürece) bazı bilgileri bellekte saklamak için tasarlanmıştır. Önbellek uygulamaları farklı biçimlerde olabilir. Basit değiştirilebilir değişkenlerden, birden fazla iş parçacığında okuma/yazma işlemlerine karşı koruma sağlayan daha karmaşık sınıflara kadar değişebilirler. Kullanım alanına bağlı olarak, önbelleğe alma işlemi depoda veya veri kaynağı sınıflarında uygulanabilir.

Ağ isteğinin sonucunu önbelleğe alma

NewsRepository, son haberleri önbelleğe almak için basitlik adına değiştirilebilir bir değişken kullanır. Okuma ve yazma işlemlerini farklı iş parçacıklarından korumak için Mutex kullanılır. Paylaşılan değiştirilebilir durum ve eşzamanlılık hakkında daha fazla bilgi edinmek için Kotlin dokümanlarına bakın.

Aşağıdaki uygulama, en son haber bilgilerini Mutex ile yazmaya karşı korunan depodaki bir değişkene önbelleğe alır. Ağ isteğinin sonucu başarılı olursa veriler latestNews değişkenine atanır.

class NewsRepository(
  private val newsRemoteDataSource: NewsRemoteDataSource
) {
    // Mutex to make writes to cached values thread-safe.
    private val latestNewsMutex = Mutex()

    // Cache of the latest news got from the network.
    private var latestNews: List<ArticleHeadline> = emptyList()

    suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
        if (refresh || latestNews.isEmpty()) {
            val networkResult = newsRemoteDataSource.fetchLatestNews()
            // Thread-safe write to latestNews
            latestNewsMutex.withLock {
                this.latestNews = networkResult
            }
        }

        return latestNewsMutex.withLock { this.latestNews }
    }
}

Ekranın süresinden daha uzun süren işlemler yapma

Kullanıcı, ağ isteği devam ederken ekrandan ayrılırsa istek iptal edilir ve sonuç önbelleğe alınmaz. NewsRepository bu mantığı uygulamak için arayanın CoroutineScope bilgisini kullanmamalıdır. Bunun yerine, NewsRepository yaşam döngüsüne bağlı bir CoroutineScope kullanmalıdır. En son haberlerin getirilmesi, uygulamaya yönelik bir işlem olmalıdır.

Bağımlılık ekleme ile ilgili en iyi uygulamalara uymak için NewsRepository, kendi CoroutineScope öğesini oluşturmak yerine oluşturucusunda parametre olarak bir kapsam almalıdır. Depoların işlerinin çoğunu arka plan iş parçacıklarında yapması gerektiğinden CoroutineScope, Dispatchers.Default ile veya kendi iş parçacığı havuzunuzla yapılandırmanız gerekir.

class NewsRepository(
    ...,
    // This could be CoroutineScope(SupervisorJob() + Dispatchers.Default).
    private val externalScope: CoroutineScope
) { ... }

NewsRepository, harici CoroutineScope ile uygulamaya yönelik işlemler yapmaya hazır olduğundan veri kaynağına çağrı yapmalı ve sonucunu bu kapsam tarafından başlatılan yeni bir eş yordamla kaydetmelidir:

class NewsRepository(
    private val newsRemoteDataSource: NewsRemoteDataSource,
    private val externalScope: CoroutineScope
) {
    /* ... */

    suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
        return if (refresh) {
            externalScope.async {
                newsRemoteDataSource.fetchLatestNews().also { networkResult ->
                    // Thread-safe write to latestNews.
                    latestNewsMutex.withLock {
                        latestNews = networkResult
                    }
                }
            }.await()
        } else {
            return latestNewsMutex.withLock { this.latestNews }
        }
    }
}

async, eş yordamı harici kapsamda başlatmak için kullanılır. await, ağ isteği geri gelene ve sonuç önbelleğe kaydedilene kadar yeni eş yordamda askıya alınmak üzere çağrılır. Kullanıcı o zamana kadar ekranda kalırsa en son haberleri görür. Kullanıcı ekrandan uzaklaşırsa await iptal edilir ancak async içindeki mantık yürütülmeye devam eder.

CoroutineScope için desenler hakkında daha fazla bilgi edinin.

Diske veri kaydetme ve diskten veri alma

Örneğin, yer işaretli haberler ve kullanıcı tercihleri gibi verileri kaydetmek istediğinizi varsayalım. Bu tür verilerin işlem sonlandırmadan sonra da varlığını sürdürmesi ve kullanıcı ağa bağlı olmasa bile erişilebilir olması gerekir.

Üzerinde çalıştığınız verilerin işlem sonlandırmadan sonra da kullanılmaya devam etmesi gerekiyorsa bu verileri aşağıdaki yöntemlerden biriyle diskte saklamanız gerekir:

  • Sorgulanması, referans bütünlüğü gerektiren veya kısmi güncellemeler gerektiren büyük veri kümeleri için verileri Room veritabanına kaydedin. Haber uygulaması örneğinde, haber makaleleri veya yazarlar veritabanına kaydedilebilir.
  • Yalnızca alınması ve ayarlanması gereken (kısmen sorgulanmayan veya güncellenmeyen) küçük veri kümeleri için DataStore'u kullanın. Haberler uygulaması örneğinde, kullanıcının tercih ettiği tarih biçimi veya diğer görüntüleme tercihleri DataStore'da kaydedilebilir.
  • JSON nesnesi gibi veri parçaları için dosya kullanın.

Doğruluk kaynağı bölümünde belirtildiği gibi, her veri kaynağı yalnızca tek bir kaynakla çalışır ve belirli bir veri türüne (örneğin, News, Authors, NewsAndAuthors veya UserPreferences) karşılık gelir. Veri kaynağını kullanan sınıflar, verilerin nasıl kaydedildiğini (örneğin, bir veritabanında veya bir dosyada) bilmemelidir.

Room'u veri kaynağı olarak kullanma

Her veri kaynağı, belirli bir veri türü için yalnızca bir kaynakla çalışmaktan sorumlu olmalıdır. Bu nedenle, bir Room veri kaynağı parametre olarak veri erişim nesnesi (DAO) veya veritabanının kendisini alır. Örneğin, NewsLocalDataSource, parametre olarak NewsDao örneğini, AuthorsLocalDataSource ise AuthorsDao örneğini alabilir.

Bazı durumlarda, ek mantık gerekmiyorsa DAO'yu doğrudan depoya yerleştirebilirsiniz. Bunun nedeni, DAO'nun testlerde kolayca değiştirebileceğiniz bir arayüz olmasıdır.

Room API'leri ile çalışma hakkında daha fazla bilgi edinmek için Room kılavuzlarına bakın.

Veri kaynağı olarak DataStore

DataStore, kullanıcı ayarları gibi anahtar/değer çiftlerini depolamak için idealdir. Örnek olarak saat biçimi, bildirim tercihleri ve kullanıcının okuduktan sonra haber öğelerinin gösterilip gösterilmeyeceği verilebilir. DataStore, protokol arabellekleri ile yazılmış nesneleri de depolayabilir.

Diğer tüm nesnelerde olduğu gibi, DataStore tarafından desteklenen bir veri kaynağı belirli bir türe veya uygulamanın belirli bir bölümüne karşılık gelen veriler içermelidir. Bu durum, DataStore okumaları her değer güncellendiğinde yayın yapan bir akış olarak sunulduğundan DataStore için daha da geçerlidir. Bu nedenle, ilgili tercihleri aynı DataStore'da saklamanız gerekir.

Örneğin, yalnızca bildirimlerle ilgili tercihleri işleyen bir NotificationsDataStore ve yalnızca haber ekranıyla ilgili tercihleri işleyen bir NewsPreferencesDataStore olabilir. Bu sayede, newsScreenPreferencesDataStore.data akışı yalnızca o ekranla ilgili bir tercih değiştirildiğinde yayın yaptığından güncellemeleri daha iyi kapsamlandırabilirsiniz. Ayrıca, nesnenin yaşam döngüsü yalnızca haber ekranı görüntülendiği sürece devam edebileceğinden daha kısa olabilir.

DataStore API'leriyle çalışma hakkında daha fazla bilgi edinmek için DataStore kılavuzlarına bakın.

Veri kaynağı olarak dosya

JSON nesnesi veya bit eşlem gibi büyük nesnelerle çalışırken File nesnesiyle çalışmanız ve iş parçacıklarını değiştirmeniz gerekir.

Dosya depolama alanıyla çalışma hakkında daha fazla bilgi edinmek için Depolamaya genel bakış sayfasını inceleyin.

WorkManager kullanarak görev planlama

Haberler uygulaması için yeni bir şart getirildiğini varsayalım: Uygulama, cihaz şarj olurken ve sınırsız bir ağa bağlıyken kullanıcılara en son haberleri düzenli ve otomatik olarak getirme seçeneği sunmalıdır. Bu nedenle bu işlem işletmeye yönelik bir işlemdir. Bu koşul, kullanıcı uygulamayı açtığında cihazın bağlantısı olmasa bile kullanıcının son haberleri görmesini sağlar.

WorkManager, eşzamansız ve güvenilir işleri planlamayı kolaylaştırır ve kısıtlama yönetimini halledebilir. Kararlı çalışma için önerilen kitaplıktır. Yukarıda tanımlanan görevi gerçekleştirmek için Worker sınıfı oluşturulur: RefreshLatestNewsWorker. Bu sınıf, en son haberleri getirmek ve diske önbelleğe almak için NewsRepository sınıfını bağımlılık olarak kullanır.

class RefreshLatestNewsWorker(
    private val newsRepository: NewsRepository,
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result = try {
        newsRepository.refreshLatestNews()
        Result.success()
    } catch (error: Throwable) {
        Result.failure()
    }
}

Bu tür görevlerin iş mantığı kendi sınıfında kapsüllenmeli ve ayrı bir veri kaynağı olarak ele alınmalıdır. Bu durumda WorkManager yalnızca tüm kısıtlamalar karşılandığında işin arka plan iş parçacığında yürütülmesini sağlamaktan sorumlu olur. Bu düzene uyarak gerektiğinde farklı ortamlardaki uygulamaları hızlıca değiştirebilirsiniz.

Bu örnekte, haberlerle ilgili bu görevin NewsRepository adresinden çağrılması gerekir. Bu durumda, yeni bir veri kaynağı bağımlılık olarak alınır. NewsTasksDataSource, aşağıdaki şekilde uygulanır:

private const val REFRESH_RATE_HOURS = 4L
private const val FETCH_LATEST_NEWS_TASK = "FetchLatestNewsTask"
private const val TAG_FETCH_LATEST_NEWS = "FetchLatestNewsTaskTag"

class NewsTasksDataSource(
    private val workManager: WorkManager
) {
    fun fetchNewsPeriodically() {
        val fetchNewsRequest = PeriodicWorkRequestBuilder<RefreshLatestNewsWorker>(
            REFRESH_RATE_HOURS, TimeUnit.HOURS
        ).setConstraints(
            Constraints.Builder()
                .setRequiredNetworkType(NetworkType.TEMPORARILY_UNMETERED)
                .setRequiresCharging(true)
                .build()
        )
            .addTag(TAG_FETCH_LATEST_NEWS)

        workManager.enqueueUniquePeriodicWork(
            FETCH_LATEST_NEWS_TASK,
            ExistingPeriodicWorkPolicy.KEEP,
            fetchNewsRequest.build()
        )
    }

    fun cancelFetchingNewsPeriodically() {
        workManager.cancelAllWorkByTag(TAG_FETCH_LATEST_NEWS)
    }
}

Bu tür sınıflar, sorumlu oldukları verilere göre adlandırılır. Örneğin, NewsTasksDataSource veya PaymentsTasksDataSource. Belirli bir veri türüyle ilgili tüm görevler aynı sınıfta kapsüllenmelidir.

Görevin uygulama başlatılırken tetiklenmesi gerekiyorsa Initializer içinden depoyu çağıran App Startup kitaplığı kullanılarak WorkManager isteğinin tetiklenmesi önerilir.

WorkManager API'leriyle çalışma hakkında daha fazla bilgi edinmek için WorkManager kılavuzlarına bakın.

Test

Bağımlılık ekleme ile ilgili en iyi uygulamalar, uygulamanızı test ederken yardımcı olur. Ayrıca, harici kaynaklarla iletişim kuran sınıflar için arayüzleri kullanmak da faydalıdır. Bir birimi test ederken testi deterministik ve güvenilir hale getirmek için bağımlılıklarının sahte sürümlerini ekleyebilirsiniz.

Birim testleri

Veri katmanı test edilirken genel test yönergeleri geçerlidir. Birim testleri için gerektiğinde gerçek nesneleri kullanın ve bir dosyadan okuma veya ağdan okuma gibi harici kaynaklara ulaşan tüm bağımlılıkları sahteleyin.

Entegrasyon testleri

Harici kaynaklara erişen entegrasyon testleri, gerçek bir cihazda çalıştırılmaları gerektiğinden daha az determinist olma eğilimindedir. Entegrasyon testlerinin daha güvenilir olması için bu testleri kontrollü bir ortamda yürütmeniz önerilir.

Veritabanları için Room, testlerinizde tamamen kontrol edebileceğiniz bir bellek içi veritabanı oluşturmanıza olanak tanır. Daha fazla bilgi edinmek için Veritabanınızı test etme ve hatalarını ayıklama sayfasını inceleyin.

Ağ oluşturma için WireMock veya MockWebServer gibi popüler kitaplıklar vardır. Bu kitaplıklar, HTTP ve HTTPS çağrılarını taklit etmenize ve isteklerin beklendiği gibi yapıldığını doğrulamanıza olanak tanır.

Ek kaynaklar

Örnekler