لایه داده

در حالی که لایه UI شامل حالت و منطق UI مربوط به UI است، لایه داده حاوی داده های برنامه و منطق تجاری است. منطق کسب‌وکار چیزی است که به برنامه شما ارزش می‌دهد—این برنامه از قوانین کسب‌وکار در دنیای واقعی ساخته شده است که تعیین می‌کند چگونه داده‌های برنامه باید ایجاد، ذخیره و تغییر شوند.

این جداسازی نگرانی‌ها به لایه داده اجازه می‌دهد تا در چند صفحه نمایش استفاده شود، اطلاعات بین بخش‌های مختلف برنامه به اشتراک گذاشته شود و منطق تجاری خارج از رابط کاربری برای آزمایش واحد بازتولید شود. برای اطلاعات بیشتر در مورد مزایای لایه داده، صفحه نمای کلی معماری را بررسی کنید.

معماری لایه داده

لایه داده از مخازنی ساخته شده است که هر کدام می توانند حاوی صفر تا بسیاری از منابع داده باشند. شما باید برای هر نوع داده متفاوتی که در برنامه خود مدیریت می کنید، یک کلاس مخزن ایجاد کنید. به عنوان مثال، ممکن است یک کلاس MoviesRepository برای داده های مربوط به فیلم ها، یا یک کلاس PaymentsRepository برای داده های مربوط به پرداخت ها ایجاد کنید.

در یک معماری معمولی، مخازن لایه داده داده ها را در اختیار بقیه برنامه قرار می دهند و به منابع داده بستگی دارند.
شکل 1. نقش لایه داده در معماری برنامه.

کلاس های مخزن وظایف زیر را بر عهده دارند:

  • نمایش داده‌ها به بقیه برنامه.
  • متمرکز کردن تغییرات در داده ها
  • حل تضاد بین منابع داده چندگانه
  • انتزاع منابع داده از بقیه برنامه.
  • حاوی منطق تجاری

هر کلاس منبع داده باید مسئولیت کار با یک منبع داده را داشته باشد که می تواند یک فایل، یک منبع شبکه یا یک پایگاه داده محلی باشد. کلاس های منبع داده پل بین برنامه و سیستم برای عملیات داده ها هستند.

لایه های دیگر در سلسله مراتب هرگز نباید مستقیماً به منابع داده دسترسی داشته باشند. نقاط ورود به لایه داده همیشه کلاس های مخزن هستند. کلاس های دارنده حالت (به راهنمای لایه UI مراجعه کنید) یا کلاس های مورد استفاده (به راهنمای لایه دامنه مراجعه کنید) هرگز نباید منبع داده به عنوان یک وابستگی مستقیم داشته باشند. استفاده از کلاس های مخزن به عنوان نقاط ورودی به لایه های مختلف معماری اجازه می دهد تا به طور مستقل مقیاس شوند.

داده‌هایی که توسط این لایه در معرض دید قرار می‌گیرند باید تغییرناپذیر باشند تا نتوانند توسط کلاس‌های دیگر دستکاری شوند، که ممکن است مقادیر آن را در حالت ناسازگار قرار دهد. داده های تغییرناپذیر را می توان به طور ایمن توسط رشته های متعدد مدیریت کرد. برای جزئیات بیشتر به بخش threading مراجعه کنید.

به دنبال بهترین روش‌های تزریق وابستگی ، مخزن منابع داده را به عنوان وابستگی در سازنده خود می‌گیرد:

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

API ها را در معرض دید قرار دهید

کلاس‌های لایه داده عموماً عملکردهایی را برای انجام تماس‌های یک‌شات ایجاد، خواندن، به‌روزرسانی و حذف (CRUD) یا اطلاع از تغییرات داده‌ها در طول زمان نشان می‌دهند. لایه داده باید موارد زیر را برای هر یک از این موارد نشان دهد:

  • عملیات تک شات: لایه داده باید توابع تعلیق در Kotlin را نشان دهد. و برای زبان برنامه نویسی جاوا، لایه داده باید توابعی را نشان دهد که یک فراخوان برای اطلاع از نتیجه عملیات یا انواع RxJava Single ، Maybe یا Completable ارائه می دهد.
  • برای اطلاع از تغییرات داده ها در طول زمان: لایه داده باید جریان های موجود در Kotlin را نشان دهد. و برای زبان برنامه نویسی جاوا، لایه داده باید یک فراخوانی را نشان دهد که داده های جدید یا نوع RxJava Observable یا Flowable را منتشر می کند.
class ExampleRepository(
    private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
    private val exampleLocalDataSource: ExampleLocalDataSource // database
) {

    val data: Flow<Example> = ...

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

قراردادهای نامگذاری در این راهنما

در این راهنما، کلاس های مخزن بر اساس داده هایی که مسئولیت آنها را بر عهده دارند نامگذاری می شوند. کنوانسیون به شرح زیر است:

نوع داده + مخزن .

به عنوان مثال: NewsRepository ، MoviesRepository ، یا PaymentsRepository .

کلاس های منبع داده بر اساس داده هایی که مسئولیت آنها را بر عهده دارند و منبعی که استفاده می کنند نامگذاری می شوند. کنوانسیون به شرح زیر است:

نوع داده + نوع منبع + DataSource .

برای نوع داده، از Remote یا Local استفاده کنید تا عمومی‌تر باشد زیرا پیاده‌سازی‌ها می‌توانند تغییر کنند. به عنوان مثال: NewsRemoteDataSource یا NewsLocalDataSource . برای دقیق تر بودن در مورد مهم بودن منبع، از نوع منبع استفاده کنید. به عنوان مثال: NewsNetworkDataSource یا NewsDiskDataSource .

منبع داده را بر اساس جزئیات پیاده سازی نامگذاری نکنید - به عنوان مثال، UserSharedPreferencesDataSource - زیرا مخازن که از آن منبع داده استفاده می کنند نباید بدانند داده چگونه ذخیره می شود. اگر از این قانون پیروی کنید، می‌توانید اجرای منبع داده را تغییر دهید (مثلاً انتقال از SharedPreferences به DataStore ) بدون تأثیر بر لایه‌ای که آن منبع را فراخوانی می‌کند.

چندین سطح از مخازن

در برخی موارد که شامل الزامات تجاری پیچیده تر است، ممکن است یک مخزن به مخازن دیگر وابسته باشد. دلیل این امر می‌تواند به این دلیل باشد که داده‌های درگیر انباشته‌ای از منابع داده‌های متعدد است، یا به این دلیل که مسئولیت باید در کلاس مخزن دیگری محصور شود.

برای مثال، مخزنی که داده‌های احراز هویت کاربر را مدیریت می‌کند، UserRepository ، می‌تواند به مخازن دیگری مانند LoginRepository و RegistrationRepository برای برآوردن نیازهای خود وابسته باشد.

در مثال، UserRepository به دو کلاس مخزن دیگر بستگی دارد: LoginRepository، که به سایر منابع داده ورود بستگی دارد. و RegistrationRepository که به سایر منابع داده ثبتی بستگی دارد.
شکل 2. نمودار وابستگی یک مخزن که به مخازن دیگر بستگی دارد.

منبع حقیقت

مهم است که هر مخزن یک منبع حقیقت را تعریف کند. منبع حقیقت همیشه حاوی داده هایی است که سازگار، صحیح و به روز هستند. در واقع، داده‌های افشا شده از مخزن باید همیشه داده‌هایی باشند که مستقیماً از منبع حقیقت می‌آیند.

منبع حقیقت می تواند یک منبع داده باشد - برای مثال، پایگاه داده - یا حتی یک کش در حافظه که ممکن است مخزن حاوی آن باشد. مخازن منابع داده های مختلف را ترکیب می کنند و هرگونه تضاد احتمالی بین منابع داده را حل می کنند تا منبع منفرد حقیقت را به طور منظم یا به دلیل یک رویداد ورودی کاربر به روز کنند.

مخازن مختلف در برنامه شما ممکن است منابع مختلفی از حقیقت داشته باشند. به عنوان مثال، کلاس LoginRepository ممکن است از حافظه پنهان خود به عنوان منبع حقیقت و کلاس PaymentsRepository از منبع داده شبکه استفاده کند.

به منظور ارائه پشتیبانی آفلاین اول، یک منبع داده محلی - مانند پایگاه داده - منبع توصیه شده حقیقت است .

نخ زنی

فراخوانی منابع داده و مخازن باید ایمن اصلی باشد — برای تماس از رشته اصلی امن است. این کلاس ها وظیفه انتقال اجرای منطق خود را به رشته مناسب در هنگام انجام عملیات مسدودسازی طولانی مدت دارند. به عنوان مثال، برای خواندن یک منبع داده از یک فایل یا برای یک مخزن برای انجام فیلترهای گران قیمت در یک لیست بزرگ، باید امن باشد.

توجه داشته باشید که اکثر منابع داده قبلاً APIهای ایمن اصلی مانند فراخوانی روش تعلیق ارائه شده توسط Room ، Retrofit یا Ktor را ارائه می دهند. مخزن شما می تواند از مزایای این API ها در صورت در دسترس بودن استفاده کند.

برای کسب اطلاعات بیشتر در مورد threading، به راهنمای پردازش پس‌زمینه مراجعه کنید. برای کاربران Kotlin، کوروتین ها گزینه پیشنهادی هستند. برای گزینه های توصیه شده برای زبان برنامه نویسی جاوا ، اجرای وظایف اندروید در رشته های پس زمینه را ببینید.

چرخه زندگی

نمونه‌هایی از کلاس‌ها در لایه داده تا زمانی که از ریشه جمع‌آوری زباله قابل دسترسی باشند در حافظه باقی می‌مانند - معمولاً با ارجاع به اشیاء دیگر در برنامه شما.

اگر یک کلاس حاوی داده‌های درون حافظه‌ای باشد - مثلاً یک حافظه پنهان - ممکن است بخواهید از همان نمونه آن کلاس برای یک دوره زمانی خاص دوباره استفاده کنید. از این به عنوان چرخه حیات نمونه کلاس نیز یاد می شود.

اگر مسئولیت کلاس برای کل برنامه بسیار مهم است، می توانید نمونه ای از آن کلاس را به کلاس Application اختصاص دهید . این باعث می شود که نمونه از چرخه عمر برنامه پیروی کند. متناوبا، اگر فقط نیاز به استفاده مجدد از همان نمونه در یک جریان خاص در برنامه خود دارید - به عنوان مثال، جریان ثبت نام یا ورود به سیستم -، باید نمونه را به کلاسی که دارای چرخه حیات آن جریان است اختصاص دهید. به عنوان مثال، می‌توانید یک RegistrationRepository که حاوی داده‌های درون حافظه است را به RegistrationActivity یا نمودار ناوبری جریان ثبت محدود کنید.

چرخه عمر هر نمونه یک عامل مهم در تصمیم گیری در مورد نحوه ارائه وابستگی ها در برنامه شما است. توصیه می‌شود که بهترین شیوه‌های تزریق وابستگی را در جایی که وابستگی‌ها مدیریت می‌شوند و می‌توان به ظروف وابستگی محدود کرد، دنبال کنید. برای کسب اطلاعات بیشتر در مورد محدوده در اندروید، به پست وبلاگ Scoping در Android و Hilt مراجعه کنید.

نشان دهنده مدل های کسب و کار

مدل‌های داده‌ای که می‌خواهید از لایه داده در معرض نمایش قرار دهید، ممکن است زیرمجموعه‌ای از اطلاعاتی باشند که از منابع مختلف داده دریافت می‌کنید. در حالت ایده‌آل، منابع داده مختلف – اعم از شبکه و محلی – باید فقط اطلاعات مورد نیاز برنامه شما را برگردانند. اما اغلب اینطور نیست.

به عنوان مثال، یک سرور News API را تصور کنید که نه تنها اطلاعات مقاله، بلکه تاریخچه ویرایش، نظرات کاربر و برخی ابرداده ها را نیز برمی گرداند:

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
)

این برنامه به اطلاعات زیادی در مورد مقاله نیاز ندارد زیرا فقط محتوای مقاله را به همراه اطلاعات اولیه درباره نویسنده آن بر روی صفحه نمایش می دهد. این یک تمرین خوب است که کلاس‌های مدل را جدا کنید و مخازن شما فقط داده‌هایی را که سایر لایه‌های سلسله مراتب نیاز دارند، در معرض دید قرار دهند. برای مثال، در اینجا نحوه حذف ArticleApiModel از شبکه به منظور نمایش یک کلاس مدل Article در لایه های دامنه و UI آمده است:

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

جداسازی کلاس های مدل به روش های زیر مفید است:

  • با کاهش داده‌ها فقط به موارد مورد نیاز، حافظه برنامه را ذخیره می‌کند.
  • انواع داده های خارجی را با انواع داده های مورد استفاده برنامه شما تطبیق می دهد - برای مثال، برنامه شما ممکن است از نوع داده دیگری برای نمایش تاریخ ها استفاده کند.
  • جداسازی بهتر نگرانی‌ها را فراهم می‌کند - برای مثال، اگر کلاس مدل از قبل تعریف شده باشد، اعضای یک تیم بزرگ می‌توانند به صورت جداگانه روی شبکه و لایه‌های رابط کاربری یک ویژگی کار کنند.

می‌توانید این عمل را گسترش دهید و کلاس‌های مدل جداگانه را در بخش‌های دیگر معماری برنامه‌تان نیز تعریف کنید - برای مثال، در کلاس‌های منبع داده و ViewModels. با این حال، این مستلزم تعریف کلاس‌ها و منطق اضافی است که باید به درستی مستند و آزمایش کنید. حداقل، توصیه می‌شود در هر صورت که منبع داده داده‌هایی را دریافت می‌کند که با آنچه بقیه برنامه‌های شما انتظار دارند مطابقت ندارد، مدل‌های جدیدی ایجاد کنید.

انواع عملیات داده

لایه داده می تواند با انواع عملیاتی که بر اساس میزان حیاتی بودن آنها متفاوت است سروکار داشته باشد: عملیات UI-گرا، برنامه گرا و کسب و کار.

عملیات UI-گرا

عملیات UI-گرا فقط زمانی مرتبط هستند که کاربر روی یک صفحه خاص باشد و زمانی که کاربر از آن صفحه دور می‌شود لغو می‌شوند. یک مثال نمایش برخی از داده های به دست آمده از پایگاه داده است.

عملیات UI-گرا معمولاً توسط لایه UI راه اندازی می شود و از چرخه عمر تماس گیرنده پیروی می کند - به عنوان مثال، چرخه عمر ViewModel. برای مثالی از عملیات مبتنی بر رابط کاربری، بخش ساخت درخواست شبکه را ببینید.

عملیات برنامه محور

تا زمانی که برنامه باز است، عملیات برنامه محور مرتبط هستند. اگر برنامه بسته شود یا فرآیند از بین برود، این عملیات لغو می شود. به عنوان مثال، نتیجه یک درخواست شبکه را در حافظه پنهان ذخیره کنید تا در صورت نیاز بعداً از آن استفاده شود. برای کسب اطلاعات بیشتر به بخش Implement in-memory data caching مراجعه کنید.

این عملیات معمولاً از چرخه حیات کلاس Application یا لایه داده پیروی می کند. برای مثال، به بخش Make an operation live more than the screen مراجعه کنید.

عملیات تجاری محور

عملیات تجاری محور را نمی توان لغو کرد. آنها باید از مرگ فرآیندی جان سالم به در ببرند. یک مثال تکمیل آپلود عکسی است که کاربر می خواهد در نمایه خود پست کند.

توصیه برای عملیات تجاری گرا استفاده از WorkManager است. برای کسب اطلاعات بیشتر به بخش زمانبندی وظایف با استفاده از WorkManager مراجعه کنید.

خطاها را فاش کنید

تعامل با مخازن و منابع داده می تواند موفقیت آمیز باشد یا در صورت بروز یک شکست، استثنا ایجاد کند. برای کوروتین ها و جریان ها، باید از مکانیزم داخلی رسیدگی به خطای کاتلین استفاده کنید. برای خطاهایی که ممکن است توسط توابع تعلیق ایجاد شوند، در صورت لزوم از بلوک‌های try/catch استفاده کنید. و در جریان ها از عملگر catch استفاده کنید. با این رویکرد، انتظار می رود که لایه UI در هنگام فراخوانی لایه داده، استثنائات را مدیریت کند.

لایه داده می تواند انواع مختلف خطاها را درک کرده و مدیریت کند و آنها را با استفاده از استثناهای سفارشی آشکار کند - به عنوان مثال، یک UserNotAuthenticatedException .

برای کسب اطلاعات بیشتر در مورد خطاها در برنامه های مشترک، به پست وبلاگ Exceptions in coroutines مراجعه کنید.

وظایف مشترک

در بخش‌های زیر نمونه‌هایی از نحوه استفاده و معماری لایه داده برای انجام برخی وظایف رایج در برنامه‌های اندرویدی ارائه شده است. مثال‌ها بر اساس برنامه خبری معمولی است که قبلاً در راهنما ذکر شد.

درخواست شبکه بدهید

درخواست شبکه یکی از رایج ترین کارهایی است که یک برنامه اندرویدی ممکن است انجام دهد. برنامه News باید آخرین اخباری را که از شبکه واکشی می شود به کاربر ارائه دهد. بنابراین، برنامه برای مدیریت عملیات شبکه به یک کلاس منبع داده نیاز دارد: NewsRemoteDataSource . برای نمایش اطلاعات به بقیه برنامه، یک مخزن جدید ایجاد می‌شود که عملیات روی داده‌های خبری را مدیریت می‌کند: NewsRepository .

لازمه این است که آخرین اخبار همیشه باید هنگام باز کردن صفحه توسط کاربر به روز شود. بنابراین، این یک عملیات UI-گرا است.

منبع داده را ایجاد کنید

منبع داده باید تابعی را نشان دهد که آخرین اخبار را برمی گرداند: فهرستی از نمونه های ArticleHeadline . منبع داده نیاز به ارائه یک راه امن اصلی برای دریافت آخرین اخبار از شبکه دارد. برای این کار باید به CoroutineDispatcher یا Executor وابستگی داشته باشد تا کار را روی آن اجرا کند.

ایجاد یک درخواست شبکه یک تماس تک شات است که توسط متد جدید fetchLatestNews() انجام می شود:

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 اجرای سرویس گیرنده API شبکه را پنهان می کند. فرقی نمی‌کند که رابط توسط Retrofit یا HttpURLConnection پشتیبانی شود. تکیه بر رابط ها باعث می شود که پیاده سازی های API در برنامه شما قابل تعویض باشند.

مخزن را ایجاد کنید

از آنجا که هیچ منطق اضافی در کلاس مخزن برای این کار مورد نیاز نیست، NewsRepository به عنوان یک پروکسی برای منبع داده شبکه عمل می کند. مزایای افزودن این لایه اضافی از انتزاع در بخش کش در حافظه توضیح داده شده است.

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

برای یادگیری نحوه مصرف مستقیم کلاس مخزن از لایه UI، راهنمای لایه UI را ببینید.

اجرای کش کردن داده ها در حافظه

فرض کنید یک نیاز جدید برای برنامه News معرفی شده است: وقتی کاربر صفحه را باز می کند، اگر قبلاً درخواستی داده شده باشد، باید اخبار ذخیره شده در حافظه پنهان به کاربر ارائه شود. در غیر این صورت، برنامه باید یک درخواست شبکه برای دریافت آخرین اخبار ارائه دهد.

با توجه به نیاز جدید، برنامه باید آخرین اخبار را در حافظه خود حفظ کند تا زمانی که کاربر برنامه را باز کرده باشد. بنابراین، این یک عملیات برنامه محور است.

حافظه های پنهان

می‌توانید با افزودن حافظه پنهان داده‌های درون حافظه، داده‌ها را زمانی که کاربر در برنامه شما است حفظ کنید. منظور از کش برای ذخیره برخی اطلاعات در حافظه برای مدت زمان مشخصی است - در این مورد، تا زمانی که کاربر در برنامه است. پیاده سازی کش می تواند اشکال مختلفی داشته باشد. این می تواند از یک متغیر ساده تغییرپذیر تا یک کلاس پیچیده تر که از عملیات خواندن/نوشتن در چندین رشته محافظت می کند متفاوت باشد. بسته به مورد استفاده، کش را می توان در مخزن یا در کلاس های منبع داده پیاده سازی کرد.

نتیجه درخواست شبکه را کش کنید

برای سادگی، NewsRepository از یک متغیر قابل تغییر برای ذخیره آخرین اخبار استفاده می کند. برای محافظت از خواندن و نوشتن از موضوعات مختلف، از Mutex استفاده می شود. برای کسب اطلاعات بیشتر در مورد وضعیت تغییرپذیر مشترک و همزمانی، به مستندات Kotlin مراجعه کنید.

پیاده سازی زیر آخرین اطلاعات اخبار را در یک متغیر در مخزن ذخیره می کند که با Mutex محافظت شده است. اگر نتیجه درخواست شبکه موفقیت آمیز باشد، داده ها به متغیر latestNews اختصاص داده می شود.

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

عملیات را طولانی تر از صفحه نمایش زنده کنید

اگر زمانی که درخواست شبکه در حال انجام است، کاربر از صفحه دور شود، این درخواست لغو می‌شود و نتیجه در حافظه پنهان ذخیره نمی‌شود. NewsRepository نباید از CoroutineScope تماس گیرنده برای اجرای این منطق استفاده کند. در عوض، NewsRepository باید از CoroutineScope استفاده کند که به چرخه حیات آن متصل است. واکشی آخرین اخبار باید یک عملیات برنامه محور باشد.

برای پیروی از بهترین شیوه‌های تزریق وابستگی، NewsRepository باید به‌جای ایجاد CoroutineScope خود، یک محدوده به‌عنوان پارامتر در سازنده‌اش دریافت کند. از آنجا که مخازن باید بیشتر کار خود را در رشته های پس زمینه انجام دهند، باید CoroutineScope با Dispatchers.Default یا با Thread Pool خود پیکربندی کنید.

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

از آنجایی که NewsRepository آماده انجام عملیات برنامه محور با CoroutineScope خارجی است، باید تماس با منبع داده را انجام دهد و نتیجه خود را با یک کوروتین جدید که توسط آن محدوده شروع شده است ذخیره کند:

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 برای شروع کوروتین در محدوده خارجی استفاده می شود. await در coroutine جدید فراخوانی می شود تا زمانی که درخواست شبکه بازگردد و نتیجه در حافظه پنهان ذخیره شود، به حالت تعلیق درآید. اگر تا آن زمان کاربر هنوز روی صفحه باشد، آخرین اخبار را مشاهده خواهد کرد. اگر کاربر از صفحه دور شود، await لغو می شود اما منطق داخل async به اجرا ادامه می دهد.

برای کسب اطلاعات بیشتر در مورد الگوهای CoroutineScope ، این پست وبلاگ را ببینید.

ذخیره و بازیابی اطلاعات از دیسک

فرض کنید که می خواهید داده هایی مانند اخبار نشانک شده و تنظیمات برگزیده کاربر را ذخیره کنید. این نوع داده ها باید از مرگ فرآیند جان سالم به در ببرند و حتی اگر کاربر به شبکه متصل نباشد قابل دسترسی باشند.

اگر داده‌هایی که با آنها کار می‌کنید نیاز به زنده ماندن از مرگ دارند، باید آن‌ها را به یکی از روش‌های زیر روی دیسک ذخیره کنید:

  • برای مجموعه داده های بزرگی که نیاز به پرس و جو دارند، نیاز به یکپارچگی ارجاعی دارند یا نیاز به به روز رسانی جزئی دارند، داده ها را در پایگاه داده اتاق ذخیره کنید. در مثال برنامه News، مقالات یا نویسندگان خبری را می توان در پایگاه داده ذخیره کرد.
  • برای مجموعه داده‌های کوچکی که فقط نیاز به بازیابی و تنظیم دارند (نه پرس و جو یا به‌روزرسانی جزئی)، از DataStore استفاده کنید. در مثال برنامه News، قالب تاریخ برگزیده کاربر یا سایر تنظیمات برگزیده نمایش را می توان در DataStore ذخیره کرد.
  • برای تکه‌های داده مانند یک شی JSON، از یک فایل استفاده کنید.

همانطور که در بخش منبع حقیقت ذکر شد، هر منبع داده تنها با یک منبع کار می کند و مربوط به یک نوع داده خاص است (به عنوان مثال، News ، Authors ، NewsAndAuthors ، یا UserPreferences ). کلاس هایی که از منبع داده استفاده می کنند نباید بدانند که چگونه داده ها ذخیره می شوند - به عنوان مثال، در یک پایگاه داده یا در یک فایل.

اتاق به عنوان منبع داده

از آنجا که هر منبع داده باید مسئولیت کار با تنها یک منبع برای نوع خاصی از داده را داشته باشد، منبع داده اتاق یا یک شی دسترسی به داده (DAO) یا خود پایگاه داده را به عنوان یک پارامتر دریافت می کند. برای مثال، NewsLocalDataSource ممکن است نمونه‌ای از NewsDao به عنوان پارامتر و AuthorsLocalDataSource نمونه‌ای از AuthorsDao بگیرد.

در برخی موارد، اگر منطق اضافی مورد نیاز نباشد، می‌توانید DAO را مستقیماً به مخزن تزریق کنید، زیرا DAO یک رابط است که می‌توانید به راحتی آن را در آزمایش‌ها جایگزین کنید.

برای کسب اطلاعات بیشتر در مورد کار با API های اتاق، به راهنمای اتاق مراجعه کنید.

DataStore به عنوان منبع داده

DataStore برای ذخیره جفت های کلید-مقدار مانند تنظیمات کاربر عالی است. مثال‌ها ممکن است شامل قالب زمان، تنظیمات برگزیده اعلان و نمایش یا پنهان کردن موارد اخبار پس از خواندن کاربر باشد. DataStore همچنین می تواند اشیاء تایپ شده را با بافرهای پروتکل ذخیره کند.

مانند هر شی دیگری، منبع داده ای که توسط DataStore پشتیبانی می شود باید حاوی داده های مربوط به نوع خاصی یا قسمت خاصی از برنامه باشد. این در مورد DataStore حتی بیشتر صادق است، زیرا خواندن های DataStore به صورت جریانی در معرض دید قرار می گیرند که هر بار که یک مقدار به روز می شود منتشر می شود. به همین دلیل، شما باید ترجیحات مرتبط را در همان DataStore ذخیره کنید.

برای مثال، می‌توانید یک NotificationsDataStore داشته باشید که فقط تنظیمات برگزیده مربوط به اعلان را مدیریت می‌کند و یک NewsPreferencesDataStore که فقط تنظیمات برگزیده مربوط به صفحه اخبار را مدیریت می‌کند. به این ترتیب، می‌توانید دامنه به‌روزرسانی‌ها را بهتر انجام دهید، زیرا جریان newsScreenPreferencesDataStore.data تنها زمانی منتشر می‌شود که یک اولویت مربوط به آن صفحه تغییر کند. همچنین به این معنی است که چرخه حیات شی می تواند کوتاهتر باشد زیرا فقط تا زمانی که صفحه اخبار نمایش داده می شود می تواند زنده بماند.

برای کسب اطلاعات بیشتر در مورد کار با DataStore API، به راهنمای DataStore مراجعه کنید.

یک فایل به عنوان منبع داده

هنگام کار با اشیاء بزرگ مانند یک شی JSON یا یک بیت مپ، باید با یک شی File کار کنید و رشته های سوئیچینگ را مدیریت کنید.

برای کسب اطلاعات بیشتر در مورد کار با ذخیره سازی فایل، به صفحه نمای کلی فضای ذخیره سازی مراجعه کنید.

کارها را با استفاده از WorkManager زمان بندی کنید

فرض کنید یک نیاز جدید دیگر برای برنامه News معرفی شده است: این برنامه باید به کاربر این امکان را بدهد که تا زمانی که دستگاه شارژ می‌شود و به یک شبکه بدون اندازه‌گیری متصل است، آخرین اخبار را به طور منظم و خودکار دریافت کند. این باعث می شود این عملیات تجاری محور باشد. این الزام باعث می‌شود حتی اگر وقتی کاربر برنامه را باز می‌کند، دستگاه اتصال نداشته باشد، کاربر همچنان می‌تواند اخبار اخیر را ببیند.

WorkManager برنامه ریزی کار ناهمزمان و قابل اعتماد را آسان می کند و می تواند مدیریت محدودیت ها را انجام دهد. این کتابخانه توصیه شده برای کار مداوم است. برای انجام وظیفه تعریف شده در بالا، یک کلاس Worker ایجاد می شود: RefreshLatestNewsWorker . این کلاس NewsRepository را به عنوان یک وابستگی می گیرد تا آخرین اخبار را واکشی کند و آن را روی دیسک ذخیره کند.

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

منطق کسب و کار برای این نوع کار باید در کلاس خودش محصور شود و به عنوان یک منبع داده جداگانه در نظر گرفته شود. پس از آن WorkManager تنها مسئول حصول اطمینان از اجرای کار بر روی یک رشته پس‌زمینه زمانی است که تمام محدودیت‌ها برآورده شوند. با رعایت این الگو، می توانید به سرعت پیاده سازی ها را در محیط های مختلف در صورت نیاز تعویض کنید.

در این مثال، این وظیفه مرتبط با اخبار باید از NewsRepository فراخوانی شود، که یک منبع داده جدید را به عنوان یک وابستگی در نظر می گیرد: NewsTasksDataSource ، که به صورت زیر پیاده سازی می شود:

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

این نوع کلاس‌ها بر اساس داده‌هایی که مسئولیت آن‌ها را بر عهده دارند نام‌گذاری می‌شوند - برای مثال NewsTasksDataSource یا PaymentsTasksDataSource . تمام وظایف مربوط به نوع خاصی از داده ها باید در یک کلاس کپسوله شوند.

اگر کار باید هنگام راه‌اندازی برنامه راه‌اندازی شود، توصیه می‌شود درخواست WorkManager را با استفاده از کتابخانه App Startup که مخزن را از یک Initializer فراخوانی می‌کند، راه‌اندازی کنید.

برای کسب اطلاعات بیشتر در مورد کار با WorkManager API، به راهنمای WorkManager مراجعه کنید.

تست کردن

بهترین روش‌های تزریق وابستگی هنگام آزمایش برنامه شما کمک می‌کند. همچنین تکیه بر رابط های کلاس هایی که با منابع خارجی ارتباط برقرار می کنند مفید است. هنگامی که یک واحد را آزمایش می کنید، می توانید نسخه های جعلی وابستگی های آن را تزریق کنید تا آزمایش قطعی و قابل اعتماد باشد.

تست های واحد

دستورالعمل آزمایش عمومی هنگام آزمایش لایه داده اعمال می شود. برای تست های واحد، در صورت نیاز از اشیاء واقعی استفاده کنید و وابستگی هایی که به منابع خارجی مانند خواندن از یک فایل یا خواندن از شبکه می رسد را جعل کنید.

تست های یکپارچه سازی

تست‌های یکپارچه‌سازی که به منابع خارجی دسترسی دارند، قطعی‌تر نیستند، زیرا باید روی یک دستگاه واقعی اجرا شوند. توصیه می شود که آن تست ها را در یک محیط کنترل شده اجرا کنید تا تست های یکپارچه سازی قابل اعتمادتر شود.

برای پایگاه های داده، Room اجازه می دهد تا یک پایگاه داده در حافظه ایجاد کنید که می توانید به طور کامل در آزمایش های خود کنترل کنید. برای کسب اطلاعات بیشتر، به صفحه تست و دیباگ خود مراجعه کنید.

برای شبکه، کتابخانه‌های محبوبی مانند WireMock یا MockWebServer وجود دارند که به شما امکان می‌دهند تماس‌های HTTP و HTTPS جعلی را انجام دهید و تأیید کنید که درخواست‌ها مطابق انتظار انجام شده‌اند.

نمونه ها

نمونه های گوگل زیر استفاده از لایه داده را نشان می دهد. برای دیدن این راهنمایی در عمل، آنها را کاوش کنید:

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}