Veri katmanı

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

Endişelerin bu şekilde ayrılması; 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 iş mantığının kullanıcı arayüzünün dışında yeniden oluşturulmasına olanak tanır. Veri katmanının avantajları hakkında daha fazla bilgi için Mimariye Genel Bakış sayfasına göz atın.

Veri katmanı mimarisi

Veri katmanı, her biri sıfır ila birçok 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 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. Kullanıcı arayüzü katmanının uygulama mimarisindeki rolü.

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

  • Uygulamanın geri kalanına veriler gösteriliyor.
  • Verilerdeki değişiklikleri merkezileştirme.
  • Birden çok veri kaynağı arasındaki çakışmaları çözme.
  • Uygulamanın geri kalanındaki veri kaynaklarını soyutlama.
  • İş mantığını içerir.

Her veri kaynağı sınıfının; dosya, ağ kaynağı veya yerel veritabanı olabilecek tek bir veri kaynağıyla çalışma sorumluluğu taşıması gerekir. Veri kaynağı sınıfları, veri işlemleri için uygulama ve sistem arasındaki köprüdür.

Hiyerarşideki diğer katmanlar hiçbir zaman veri kaynaklarına doğrudan erişmemelidir. Veri katmanına giriş noktaları her zaman depo sınıflarıdır. Durum sahibi sınıfları (kullanıcı arayüzü katman kılavuzuna bakın) veya kullanım durumu sınıfları (alan katmanı kılavuzuna bakın), hiçbir zaman doğrudan bağımlılık olarak bir veri kaynağına sahip olmamalıdır. Depo sınıflarını giriş noktası olarak kullanmak, mimarinin farklı katmanlarının bağımsız olarak ölçeklenebilmesini sağlar.

Bu katmanın açığa çıkardığı veriler sabit olmalıdır. Böylece, diğer sınıflar tarafından oynanamaz. Bu durum, verilerin tutarsız bir duruma getirilmesiyle sonuçlanabilir. Sabit veriler ayrıca birden fazla iş parçacığı tarafından güvenli bir şekilde işlenebilir. Daha fazla ayrıntı için ileti dizisi bölümüne bakın.

Depo, bağımlılık yerleştirme en iyi uygulamalarını izleyerek veri kaynaklarını, oluşturucusunda bağımlılık olarak 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 seferlik Oluşturma, Okuma, Güncelleme ve Silme (CRUD) çağrıları gerçekleştirmek veya zaman içindeki veri değişikliklerinden haberdar olmak için işlevleri açığa çıkarır. Veri katmanı, bu durumların her biri için aşağıdakileri göstermelidir:

  • Tek seferlik işlemler: Veri katmanı, Kotlin'deki askıya alma işlevlerini açığa çıkarmalıdır. Java programlama dili için veri katmanı, işlemin sonucunu bildirmek için geri çağırma sağlayan işlevler veya RxJava Single, Maybe ya da Completable türlerini açığa çıkarmalıdır.
  • Zaman içindeki veri değişikliklerinden haberdar olmak için: Veri katmanı, Kotlin'deki akışları açığa çıkarmalıdır. Java programlama dili için ise veri katmanı, yeni verileri yayınlayan bir geri çağırma veya RxJava Observable ya da Flowable türünü göstermelidir.
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ılmıştır. Sistem 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. Sistem aşağıdaki gibidir:

veri türü + kaynak türü + Veri Kaynağı.

Uygulamalar değişebileceğinden daha genel olması için veri türü olarak Uzak veya Yerel değerini kullanın. Örneğin: NewsRemoteDataSource veya NewsLocalDataSource. Kaynağın önemli olduğu konusunda daha ayrıntılı bilgi vermek için kaynak türünü kullanın. Örneğin: NewsNetworkDataSource veya NewsDiskDataSource.

Veri kaynağını bir uygulama ayrıntılarına göre adlandırmayın (örneğin, UserSharedPreferencesDataSource). Bu veri kaynağını kullanan depolar, verilerin nasıl kaydedildiğini bilemez. Bu kuralı uygularsanız söz konusu kaynağı çağıran katmanı etkilemeden veri kaynağının uygulamasını (örneğin, SharedPreferences'tan DataStore'a taşıma) değiştirebilirsiniz.

Birden çok depo düzeyi

Daha karmaşık iş gereksinimleri içeren bazı durumlarda bir deponun diğer depolara bağımlı olması gerekebilir. Bunun nedeni, dahil edilen verilerin birden fazla veri kaynağından toplanmış olması veya sorumluluğun başka bir depo sınıfında yer alması gerekmesi olabilir.

Örneğin, kullanıcı kimlik doğrulama verilerini işleyen UserRepository bir depo, koşullarını karşılamak için LoginRepository ve RegistrationRepository gibi diğer depolara güvenebilir.

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

Bilgi kaynağı

Her deponun tek bir doğru kaynak tanımlaması önemlidir. Bilgilerin kaynağı her zaman tutarlı, doğru ve güncel veriler içerir. Aslında, depodan açığa çıkarılan veriler her zaman doğrudan doğrunun kaynağından gelen veriler olmalıdır.

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

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

Çevrimdışı öncelikli destek sağlamak için önerilen doğru kaynak, veritabanı gibi bir yerel veri kaynağıdır.

İplik işleme

Veri kaynaklarını ve depoları çağırmak ana güvenli olmalıdır. Bu durum, ana iş parçacığından kolayca çağrılabilir. Bu sınıflar, uzun süreli engelleme işlemleri gerçekleştirirken mantığının yürütülmesini uygun iş parçacığına taşımaktan sorumludur. Örneğin, veri kaynağının dosyadan okuması veya deponun büyük bir listede pahalı filtreleme işlemi yapması için "ana" güvenlik 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 güvenli API'ler zaten sağladığını unutmayın. Kod deponuz, kullanıma sunulduğunda bu API'lerden yararlanabilir.

İş parçacığı oluşturma hakkında daha fazla bilgi edinmek için arka plan işleme kılavuzuna bakın. Kotlin kullanıcıları için eş yordam önerilen seçenektir. Java programlama diliyle ilgili önerilen seçenekler için Android görevlerini arka plan iş parçacıklarında çalıştırma konusuna bakın.

Yaşam döngüsü

Veri katmanındaki sınıf örnekleri, genellikle uygulamanızdaki diğer nesnelerden referans alınarak çöp toplama kökünden erişildikleri sürece bellekte kalır.

Bir sınıfta bellek içi veriler (ör. önbellek) varsa bu sınıfın aynı örneğini belirli bir süre için 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 tüm uygulama için çok önemliyse söz konusu sınıfın bir örneğini Application sınıfı için kapsama alabilirsiniz. Böylece, örneğin uygulama yaşam döngüsünü takip etmesi sağlanır. Alternatif olarak, aynı örneği yalnızca uygulamanızdaki belirli bir akışta (ör. kayıt veya giriş akışında) yeniden kullanmanız gerekiyorsa örneği kapsamı söz konusu akışın yaşam döngüsüne sahip olan sınıfa ayarlamanız gerekir. Örneğin, bellek içi veriler içeren bir RegistrationRepository kapsamını RegistrationActivity veya kayıt akışının gezinme grafiğine uygulayabilirsiniz.

Her bir örneğin yaşam döngüsü, uygulamanızda bağımlılıkları nasıl sağlayacağınıza karar verirken kritik bir faktördür. Bağımlılıkların yönetildiği ve bağımlılık kapsayıcılarına alınabileceği bağımlılık ekleme ile ilgili en iyi uygulamaları takip etmeniz önerilir. Android'de kapsam oluşturma hakkında daha fazla bilgi edinmek için Android ve Hilt'de kapsam oluşturma blog yayınına bakın.

İş modellerini temsil etme

Veri katmanında göstermek istediğiniz veri modelleri, farklı veri kaynaklarından aldığınız bilgilerin bir alt kümesi olabilir. İdeal olarak, hem ağ hem de yerel gibi farklı veri kaynakları yalnızca uygulamanızın ihtiyaç duyduğu bilgileri döndürmelidir; ancak bu durum pek geçerli değildir.

Örneğin, yalnızca makale bilgilerinin yanı sıra düzenleme geçmişini, kullanıcı yorumlarını ve bazı meta verileri de döndüren bir News API sunucusu 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
)

Uygulamanın, makale hakkında çok fazla bilgiye ihtiyacı yoktur. Çünkü yalnızca makalenin içeriğini, yazarıyla ilgili temel bilgilerle birlikte ekranda gösterir. Model sınıflarını ayırmak ve depolarınızın yalnızca hiyerarşinin diğer katmanlarının gerektirdiği verileri göstermesini sağlamak iyi bir uygulamadır. Örneğin, Article model sınıfını alan ve kullanıcı arayüzü katmanlarına göstermek için ağdan ArticleApiModel kodunu şu şekilde kısaltabilirsiniz:

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 aşağıdaki açılardan faydalı olabilir:

  • Yalnızca gereken veri miktarını azaltarak uygulama belleğinden tasarruf sağlar.
  • Harici veri türlerini, uygulamanız tarafından kullanılan veri türlerine uyarlar. Örneğin, uygulamanız tarihleri temsil etmek için farklı bir veri türü kullanabilir.
  • Endişelerin 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 bireysel olarak çalışabilir.

Bu uygulamanın kapsamını genişletebilir ve uygulama mimarinizin diğer bölümlerinde de (örneğin, veri kaynağı sınıfları ve ViewModels) ayrı model sınıfları tanımlayabilirsiniz. Ancak bu, düzgün şekilde belgeleyip test etmeniz gereken ekstra sınıflar ve mantık tanımlamanızı gerektirir. En azından, bir veri kaynağının uygulamanızın geri kalanının beklediğinden eşleşmeyen veriler aldığı her durumda yeni modeller oluşturmanız önerilir.

Veri işlemi türleri

Veri katmanı, kritik öneme bağlı olarak değişen işlem türleriyle (kullanıcı arayüzü odaklı, uygulama odaklı ve iş odaklı operasyonlar) başa çıkabilir.

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. Bir örnek olarak, veritabanından elde edilen bazı veriler görüntülenmektedir.

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ü (örneğin, ViewModel'in yaşam döngüsü) takip eder. Kullanıcı arayüzü odaklı bir işlem örneği için Ağ isteği yapma bölümüne bakın.

Uygulama odaklı işlemler

Uygulama odaklı işlemler, uygulama açık olduğu sürece geçerlidir. Uygulama kapatılırsa veya işlem sonlandırılırsa bu işlemler iptal edilir. Buna örnek olarak, bir ağ isteğinin sonucunu daha sonra gerektiğinde kullanmak üzere önbelleğe almak gösterilebilir. Daha fazla bilgi edinmek için Bellek içi verileri ö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ü izler. Örnek için Bir işlemi ekrandan daha uzun süre canlı hale getirme bölümüne bakın.

İş odaklı operasyonlar

İş odaklı işlemler iptal edilemez. Süreçte ölümden kurtulmak zorundalar. Kullanıcının profilinde yayınlamak istediği bir fotoğrafın yüklenmesinin tamamlanması buna örnek olarak verilebilir.

İş odaklı operasyonlar için WorkManager kullanmanızı öneririz. Daha fazla bilgi edinmek için WorkManager kullanarak görev planlama bölümüne bakın.

Hataları göster

Depolar ve veri kaynaklarıyla etkileşimler başarılı olabilir veya bir hata oluştuğunda istisnai durum oluşabilir. İlişkiler ve akışlar için Kotlin'in yerleşik hata işleme mekanizmasını kullanmanız gerekir. Askıya alma işlevleri tarafından tetiklenebilecek hatalar için uygun olduğunda try/catch bloklarını ve akışlarda catch operatörünü kullanın. Bu yaklaşımda, kullanıcı arayüzü katmanının veri katmanı çağrılırken istisnaları işlemesi beklenir.

Veri katmanı, farklı hata türlerini anlayıp işleyebilir ve özel istisnalar (ör. UserNotAuthenticatedException) kullanarak bu hataları ortaya çıkarabilir.

Eş yordamlardaki hatalar hakkında daha fazla bilgi edinmek için İlişkilerdeki İstisnalar blog yayınına bakın.

Genel görevler

Aşağıdaki bölümlerde, Android uygulamalarında yaygın olarak görülen belirli görevleri gerçekleştirmek için veri katmanının nasıl kullanılacağı ve tasarlanacağına dair örnekler verilmektedir. Örnekler, rehberde daha önce bahsedilen tipik Haber uygulamasına dayanır.

Ağ isteği gönderin

Ağ isteği oluşturmak, bir Android uygulamasının gerçekleştirebileceği en yaygın görevlerden biridir. Haberler uygulamasının kullanıcıya ağdan alınan en son haberleri sunması gerekir. Dolayısıyla, uygulamanın ağ işlemlerini yönetmek için bir veri kaynağı sınıfına ihtiyacı vardır: NewsRemoteDataSource. Bilgilerin uygulamanın geri kalanına gösterilmesi için haber verileriyle ilgili işlemleri gerçekleştiren yeni bir depo oluşturulur: NewsRepository.

Buradaki şart, kullanıcı ekranı açtığında en son haberlerin her zaman güncellenmesidir. Bu nedenle, bu kullanıcı arayüzü odaklı bir işlemdir.

Veri kaynağını oluşturma

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

Ağ isteği yapmak, yeni bir fetchLatestNews() yöntemi ile gerçekleştirilen tek seferlik bir aramadı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 uygulanmasını gizler. Arayüzün Retrofit veya HttpURLConnection tarafından desteklenmesi fark etmez. Arayüzlerden yararlanmak, uygulamanızdaki API uygulamalarını değiştirilebilir hale getirir.

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 ekstra 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ın doğrudan kullanıcı arayüzü katmanından nasıl kullanılacağını öğrenmek için kullanıcı arayüzü katmanı kılavuzuna bakın.

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

Haberler uygulaması için yeni bir şart getirildiğini varsayalım: Kullanıcı ekranı açtığında, önceden istek yapılmışsa önbelleğe alınmış haberler kullanıcıya sunulmalıdır. Aksi takdirde, uygulama en son haberleri getirmek için ağ isteğinde bulunur.

Yeni şartla birlikte, kullanıcı açıkken uygulama, bellekteki en son haberleri saklamalıdır. Bu nedenle, bu uygulama odaklı bir işlemdir.

Önbellek

Bellek içi önbelleğe alma ekleyerek kullanıcı uygulamanızdayken verileri koruyabilirsiniz. Önbellekler, bazı bilgileri belirli bir süre boyunca (bu durumda, kullanıcı uygulamada bulunduğu sürece) bellekte saklamak için kullanılır. Önbellek uygulamaları farklı biçimlerde olabilir. Basit bir değişken değişkenden, birden fazla iş parçacığındaki okuma/yazma işlemlerinden koruyan daha gelişmiş bir sınıfa kadar değişiklik gösterebilir. Önbelleğe alma, kullanım alanına bağlı olarak depoda veya veri kaynağı sınıflarında uygulanabilir.

Ağ isteğinin sonucunu önbelleğe al

NewsRepository, en son haberleri önbelleğe almak için değişken bir değişken kullanır. Böylece kolaylık sağlar. Farklı iş parçacıklarından okuma ve yazma işlemlerini korumak için Mutex kullanılır. Paylaşılan değişken durum ve eşzamanlılık hakkında daha fazla bilgi edinmek için Kotlin belgelerini inceleyin.

Aşağıdaki uygulama, en son haber bilgilerini kod deposundaki Mutex ile yazma korumalı 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 }
    }
}

Bir işlemi ekrandan daha uzun süre canlı hale getirme

Kullanıcı, ağ isteği devam ederken ekrandan ayrılırsa istek iptal edilir ve sonuç önbelleğe alınmaz. NewsRepository, bu mantığı gerçekleştirmek için arayanın CoroutineScope özelliğini kullanmamalıdır. Bunun yerine, NewsRepository, yaşam döngüsüne bağlı bir CoroutineScope kullanmalıdır. En son haberleri getirme işleminin uygulama odaklı olması gerekir.

Bağımlılık yerleştirmeyle ilgili en iyi uygulamaların izlenmesi için NewsRepository, kendi CoroutineScope parametresini oluşturmak yerine oluşturucuda parametre olarak bir kapsam almalıdır. Depolar, çalışmalarının çoğunu arka plan iş parçacıklarında yapmalıdır. Bu nedenle, CoroutineScope öğesini Dispatchers.Default 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 uygulama odaklı işlemler gerçekleştirmeye hazır olduğundan, veri kaynağına yapılan çağrıyı gerçekleştirmesi ve sonucunu bu kapsam tarafından başlatılan yeni bir eş yordamla kaydetmesi gerekir:

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, harici kapsamdaki eş yordamı başlatmak için kullanılır. await, ağ isteği geri gelene ve sonuç önbelleğe kaydedilene kadar askıya almak için yeni eş yordasında çağrılır. Bu süre zarfında kullanıcı hâlâ ekrandaysa 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 kalıpları hakkında daha fazla bilgi edinmek için bu blog yayınına göz atın.

Diskteki verileri kaydet ve al

Yer işareti eklenen haberler ve kullanıcı tercihleri gibi verileri kaydetmek istediğinizi varsayalım. Bu tür verilerin, işlem bitiminden sonra hayatta kalabilmesi ve kullanıcı ağa bağlı olmasa bile erişilebilir olması gerekir.

Üzerinde çalıştığınız verilerin işlem ölümünden sonra hayatta kalabilmesi gerekiyorsa aşağıdaki yöntemlerden birini kullanarak verileri diskte depolamanız gerekir:

  • Sorgulanması gereken, referans bütünlüğü gerektiren veya kısmi güncelleme gerektiren büyük veri kümeleri için verileri bir Oda veritabanına kaydedin. Haber uygulaması örneğinde, haber makaleleri veya yazarlar veritabanına kaydedilebilir.
  • Yalnızca alınması ve ayarlanması gereken (sorgular veya kısmen güncellenenler için değil) 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'a kaydedilebilir.
  • JSON nesnesi gibi veri parçaları için file kullanın.

Gerçeğin 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 karşılık gelir (örneğin, News, Authors, NewsAndAuthors veya UserPreferences). Veri kaynağını kullanan sınıflar, verilerin nasıl kaydedildiğini bilmemelidir (örneğin, bir veritabanında veya dosyaya).

Veri kaynağı olarak oda

Her veri kaynağının belirli bir veri türü için yalnızca tek bir kaynakla çalışma sorumluluğu olması gerektiğinden, Oda veri kaynağı parametre olarak bir veri erişim nesnesi (DAO) veya veritabanının kendisini alır. Örneğin NewsLocalDataSource, parametre olarak NewsDao örneğini ve AuthorsLocalDataSource, AuthorsDao örneğini alabilir.

Bazı durumlarda, ekstra mantık gerekmiyorsa DAO, testlerde kolayca değiştirebileceğiniz bir arayüz olduğundan DAO'yu doğrudan depoya ekleyebilirsiniz.

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

Veri kaynağı olarak DataStore

DataStore, kullanıcı ayarları gibi anahtar/değer çiftlerini depolamak için mükemmeldir. Saat biçimi, bildirim tercihleri ve kullanıcı okuduktan sonra haber öğelerinin gösterilmesi veya gizlenmesi buna örnek olarak verilebilir. DataStore, yazılan nesneleri protokol arabellekleri ile de depolayabilir.

Diğer tüm nesnelerde olduğu gibi, DataStore tarafından desteklenen bir veri kaynağı da belirli bir türe veya uygulamanın belirli bir bölümüne karşılık gelen verileri içermelidir. DataStore için bu daha da doğrudur çünkü DataStore okumaları, her değer güncellendiğinde yayınlanan bir akış olarak ortaya çıkar. Bu nedenle, ilgili tercihleri aynı DataStore'da depolamanız gerekir.

Örneğin, yalnızca bildirimle ilgili tercihleri işleyen bir NotificationsDataStore ve yalnızca haber ekranıyla ilgili tercihleri işleyen bir NewsPreferencesDataStore'ınız olabilir. Bu şekilde, newsScreenPreferencesDataStore.data akışı yalnızca bu ekranla ilgili bir tercih değiştiğinde yayınladığından güncellemelerin kapsamını daha iyi belirleyebilirsiniz. Bu, aynı zamanda, yalnızca haber ekranı görüntülendiğinde yaşanabileceği için nesnenin yaşam döngüsünün daha kısa olabileceği anlamına da gelir.

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ı arasında geçiş yapmanız gerekir.

Dosya depolama alanıyla çalışma hakkında daha fazla bilgi edinmek için Depolama alanına genel bakış sayfasına göz atın.

WorkManager'ı kullanarak görevleri planlama

Haber uygulaması için başka bir yeni şartın uygulandığını varsayalım: Uygulama, cihaz şarj oluyor ve sınırsız bir ağa bağlı olduğu sürece kullanıcıya en son haberleri düzenli ve otomatik olarak getirme seçeneği sunmalıdır. Bu da bu işlemi işletme odaklı bir işlem haline getirir. Bu şart, kullanıcı uygulamayı açtığında cihaz bağlantısı olmasa bile son haberleri görmeye devam etmesini sağlar.

WorkManager, eşzamansız ve güvenilir çalışmaların planlanmasını kolaylaştırır ve kısıtlama yönetiminin halledilmesini sağlar. Kalıcı işler için önerilen kitaplıktır. Yukarıda tanımlanan görevi gerçekleştirmek için bir Worker sınıfı oluşturulur: RefreshLatestNewsWorker. Bu sınıf, en son haberleri getirmek ve bunları diskte önbelleğe almak için NewsRepository bağımlılığını 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 bir görev için iş mantığı, kendi sınıfına dahil edilmeli 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 kalıba bağlı kalarak, farklı ortamlardaki uygulamaları gerektiğinde hızlı bir şekilde değiştirebilirsiniz.

Bu örnekte, haberle ilgili bu görev NewsRepository konumundan çağrılmalıdır. Bu işlem, bağımlılık olarak yeni bir veri kaynağını alı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 sınıf türleri, 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 yer almalıdır.

Uygulama başlatılırken görevin tetiklenmesi gerekiyorsa depoyu bir Initializer üzerinden çağıran Uygulama Başlatma kitaplığını kullanarak WorkManager isteğini tetiklemeniz ö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 yerleştirme ile ilgili en iyi uygulamalar, uygulamanızı test ederken yardımcı olur. Ayrıca, harici kaynaklarla iletişim kuran sınıfların arayüzlerinden yararlanmak da faydalı olur. Bir birimi test ederken, testin belirleyici ve güvenilir olması için bağımlılıklarının sahte sürümlerini ekleyebilirsiniz.

Birim testleri

Genel test kılavuzu, veri katmanı test edilirken geçerlidir. Birim testleri için gerektiğinde gerçek nesneler kullanın ve bir dosyadan okuma veya ağdan okuma gibi harici kaynaklara ulaşan bağımlılıkları taklit edin.

Entegrasyon testleri

Harici kaynaklara erişen entegrasyon testleri, gerçek bir cihazda çalıştırılmaları gerektiğinden daha az belirleyici olur. Entegrasyon testlerini daha güvenilir hale getirmek için bu testleri kontrollü bir ortamda yürütmeniz önerilir.

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

Ağ iletişimi için sahte HTTP ve HTTPS çağrıları yapmanıza ve isteklerin beklendiği gibi yapıldığını doğrulamanıza olanak tanıyan WireMock veya MockWebServer gibi popüler kitaplıklar vardır.

Sana Özel

Aşağıdaki Google örnekleri, veri katmanının kullanımını göstermektedir. Bu kılavuzu inceleyerek örnekleri inceleyin: