لایه داده

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

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

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

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

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

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

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

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

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

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

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

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

افشای APIها

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

  • برای عملیات یک‌باره ، توابع suspend را فعال کنید.
  • برای اطلاع از تغییرات داده‌ها در طول زمان ، از دستور expose flows استفاده کنید.
class ExampleRepository(
    private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
    private val exampleLocalDataSource: ExampleLocalDataSource // database
) {

    val data: Flow<Example> = ...

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

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

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

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

برای مثال: NewsRepository ، MoviesRepository یا PaymentsRepository .

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

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

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

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

سطوح مختلف مخازن

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

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

In the example, UserRepository depends on two other repository classes:
    LoginRepository, which depends on other login data sources; and
    RegistrationRepository, which depends on other registration data sources.
شکل ۲. نمودار وابستگی یک مخزن که به مخازن دیگر وابسته است.

منبع حقیقت

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

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

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

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

رزوه کاری

فراخوانی منابع داده و مخازن باید به صورت main-safe باشد - فراخوانی از نخ اصلی ایمن باشد. این کلاس‌ها مسئول انتقال اجرای منطق خود به نخ مناسب هنگام انجام عملیات مسدودسازی طولانی مدت هستند. به عنوان مثال، برای یک منبع داده خواندن از یک فایل یا برای یک مخزن انجام فیلترینگ پرهزینه روی یک لیست بزرگ، باید main-safe باشد.

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

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

چرخه حیات

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

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

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

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

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

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

برای مثال، یک سرور 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 در لایه‌های دامنه و رابط کاربری آورده شده است:

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

جداسازی کلاس‌های مدل از جهات زیر مفید است:

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

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

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

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

عملیات‌های مبتنی بر رابط کاربری

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

عملیات‌های مبتنی بر رابط کاربری معمولاً توسط لایه رابط کاربری آغاز می‌شوند و چرخه حیات فراخواننده را دنبال می‌کنند - برای مثال، چرخه حیات ViewModel. برای مثالی از یک عملیات مبتنی بر رابط کاربری، به بخش «ایجاد درخواست شبکه» مراجعه کنید.

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

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

این عملیات معمولاً از چرخه حیات کلاس Application یا لایه داده پیروی می‌کنند. برای مثال، به بخش «طول عمر یک عملیات را از صفحه نمایش بیشتر کنید» مراجعه کنید.

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

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

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

نمایش خطاها

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

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

برای کسب اطلاعات بیشتر در مورد خطاها در کوروتین‌ها، به پست وبلاگ « استثنائات در کوروتین‌ها» مراجعه کنید.

وظایف مشترک

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

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

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

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

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

منبع داده باید تابعی را نمایش دهد که آخرین اخبار را برمی‌گرداند: فهرستی از نمونه‌های ArticleHeadline . منبع داده باید یک روش main-safe برای دریافت آخرین اخبار از شبکه ارائه دهد. برای این کار، باید به 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 را در برنامه شما قابل تعویض می‌کند.

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

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

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

برای یادگیری نحوه‌ی استفاده‌ی مستقیم از کلاس repository از لایه‌ی 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 مخصوص به خود، یک scope را به عنوان پارامتر در سازنده خود دریافت کند. از آنجا که مخازن باید بیشتر کار خود را در نخ‌های پس‌زمینه انجام دهند، باید CoroutineScope را یا با Dispatchers.Default یا با استخر نخ خودتان پیکربندی کنید.

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

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

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

درباره الگوهای CoroutineScope بیشتر بخوانید.

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

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

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

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

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

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

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

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

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

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

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

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

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

برای کسب اطلاعات بیشتر در مورد کار با APIهای DataStore، به راهنماهای 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 فراخوانی می‌کند، فعال کنید.

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

آزمایش

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

تست‌های واحد

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

آزمون‌های یکپارچه‌سازی

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

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

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

منابع اضافی

نمونه‌ها

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}