שכבת ממשק המשתמש מכילה מצב שקשור לממשק המשתמש ולוגיקה של ממשק המשתמש, ושכבת הנתונים מכילה נתוני אפליקציה ולוגיקה עסקית. הלוגיקה העסקית היא מה שנותן ערך לאפליקציה – היא מורכבת מכללים עסקיים מהעולם האמיתי שקובעים איך צריך ליצור, לאחסן ולשנות את נתוני האפליקציה.
ההפרדה הזו בין הדאגות מאפשרת להשתמש בשכבת הנתונים בכמה מסכים, לשתף מידע בין חלקים שונים של האפליקציה ולשחזר לוגיקה עסקית מחוץ לממשק המשתמש לצורך בדיקות יחידה. מידע נוסף על היתרונות של שכבת הנתונים זמין בדף סקירה כללית של הארכיטקטורה.
ארכיטקטורה של שכבת נתונים
שכבת הנתונים מורכבת ממאגרים, שכל אחד מהם יכול להכיל אפס עד הרבה מקורות נתונים. מומלץ ליצור מחלקת מאגר לכל סוג נתונים שמועבר באפליקציה. לדוגמה, אפשר ליצור מחלקה MoviesRepositoryלנתונים שקשורים לסרטים או מחלקה PaymentsRepositoryלנתונים שקשורים לתשלומים.
מחלקות המאגר אחראיות למשימות הבאות:
- חשיפת הנתונים לשאר האפליקציה.
- ריכוז השינויים בנתונים.
- פתרון התנגשויות בין כמה מקורות נתונים.
- הפרדה של מקורות הנתונים משאר האפליקציה.
- מכיל לוגיקה עסקית.
כל מחלקה של מקור נתונים צריכה להיות אחראית לעבודה עם מקור נתונים אחד בלבד, שיכול להיות קובץ, מקור ברשת או מסד נתונים מקומי. מחלקות של מקורות נתונים הן הגשר בין האפליקציה למערכת לפעולות נתונים.
שכבות אחרות בהיררכיה לא אמורות לגשת למקורות נתונים ישירות. נקודות הכניסה לשכבת הנתונים הן תמיד מחלקות המאגר. למחלקות של מחזיקי מצב (ראו את המדריך לשכבת ממשק המשתמש) או למחלקות של תרחישי שימוש (ראו את המדריך לשכבת הדומיין) אף פעם לא צריכה להיות תלות ישירה במקור נתונים. שימוש במחלקות מאגר כנקודות כניסה מאפשר לשכבות השונות של הארכיטקטורה להתרחב באופן עצמאי.
הנתונים שמוצגים בשכבה הזו צריכים להיות בלתי ניתנים לשינוי כדי שמחלקות אחרות לא יוכלו לשנות אותם, כי אחרת יש סיכון שהערכים שלהם יהיו לא עקביים. אפשר גם לטפל בנתונים שלא ניתן לשנות באופן בטוח באמצעות כמה תהליכים. פרטים נוספים זמינים בקטע בנושא שרשורים.
בהתאם לשיטות המומלצות להזרקת תלות, המאגר מקבל מקורות נתונים כתלות בבונה שלו:
class ExampleRepository(
private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
private val exampleLocalDataSource: ExampleLocalDataSource // database
) { /* ... */ }
חשיפת ממשקי API
בדרך כלל, מחלקות בשכבת הנתונים חושפות פונקציות לביצוע קריאות חד-פעמיות של יצירה, קריאה, עדכון ומחיקה (CRUD), או לקבלת הודעה על שינויים בנתונים לאורך זמן. שכבת הנתונים צריכה לחשוף את הפרטים הבאים לגבי כל אחד מהמקרים האלה:
- לפעולות חד-פעמיות, חושפים פונקציות השעיה.
- כדי לקבל התראות על שינויים בנתונים לאורך זמן, צריך לחשוף זרימות.
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 כדי לעמוד בדרישות שלו.
מקור מידע אמין
חשוב שכל מאגר יגדיר מקור מהימן יחיד. מקור האמת תמיד מכיל נתונים עקביים, נכונים ועדכניים. למעשה, הנתונים שמוצגים מהמאגר צריכים להיות תמיד הנתונים שמגיעים ישירות מהמקור המהימן.
מקור האמת יכול להיות מקור נתונים – לדוגמה, מסד הנתונים – או אפילו מטמון בזיכרון שהמאגר עשוי להכיל. מאגרי מידע משלבים מקורות נתונים שונים ופותרים קונפליקטים פוטנציאליים בין מקורות הנתונים כדי לעדכן את המקור הקובע באופן קבוע או בעקבות אירוע של קלט של משתמשים.
יכול להיות שלמאגרים שונים באפליקציה יש מקורות שונים של נתונים מהימנים. לדוגמה, יכול להיות שהמחלקה LoginRepository תשתמש במטמון שלה כמקור האמת, והמחלקה PaymentsRepository תשתמש במקור הנתונים של הרשת.
כדי לספק תמיכה בגישה אופליין, מומלץ להשתמש במקור מהימן מקומי, כמו מסד נתונים.
הוספת שרשורים
הקריאה למקורות נתונים ולמאגרי מידע צריכה להיות main-safe – כלומר, בטוחה לקריאה מה-thread הראשי. המחלקות האלה אחראיות להעברת ההרצה של הלוגיקה שלהן לשרשור המתאים כשמבצעים פעולות חסימה ארוכות. לדוגמה, מקור נתונים צריך להיות בטוח לשימוש בשרשור הראשי כדי לקרוא מתוך קובץ, או כדי שמחסן יבצע סינון יקר ברשימה גדולה.
שימו לב: רוב מקורות הנתונים כבר מספקים ממשקי API בטוחים לשימוש בשרשור הראשי, כמו קריאות לשיטות השעיה שמסופקות על ידי Room, Retrofit או Ktor. אפשר להשתמש בממשקי ה-API האלה במאגר שלכם כשהם זמינים.
מידע נוסף על חלוקה לשרשורים זמין במדריך לעיבוד ברקע. למשתמשי Kotlin, מומלץ להשתמש בקורוטינות.
מחזור חיים
מופעים של מחלקות בשכבת הנתונים נשארים בזיכרון כל עוד אפשר להגיע אליהם משורש של garbage collection – בדרך כלל על ידי הפניה מאובייקטים אחרים באפליקציה.
אם כיתה מכילה נתונים בזיכרון – לדוגמה, מטמון – יכול להיות שתרצו להשתמש מחדש באותו מופע של הכיתה למשך תקופה מסוימת. התהליך הזה נקרא גם מחזור החיים של מופע המחלקה.
אם האחריות של המחלקה חשובה לכל האפליקציה, אפשר להגדיר את ההיקף של מופע של המחלקה הזו למחלקה Application. כך המופע פועל בהתאם למחזור החיים של האפליקציה. לחלופין, אם אתם צריכים להשתמש מחדש באותו מופע רק בתהליך מסוים באפליקציה – לדוגמה, בתהליך ההרשמה או הכניסה – אתם צריכים להגדיר את היקף המופע למחלקה שבבעלותה מחזור החיים של התהליך הזה. לדוגמה, אפשר להגדיר היקף של RegistrationRepository שמכיל נתונים בזיכרון ל-RegistrationActivity, או ל-backstack באמצעות NavEntryDecorator.
מחזור החיים של כל מופע הוא גורם חשוב בהחלטה איך לספק תלות בתוך האפליקציה. מומלץ לפעול לפי השיטות המומלצות של הזרקת תלות, שבהן התלויות מנוהלות ויכולות להיות מוגבלות למאגרי תלות. מידע נוסף על הגדרת היקף ב-Android זמין בפוסט בבלוג בנושא הגדרת היקף ב-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 לשכבות של הדומיין וממשק המשתמש:
data class Article(
val id: Long,
val title: String,
val content: String,
val publicationDate: Date,
val authorName: String,
val readTimeMin: Int
)
הפרדה בין מחלקות של מודלים מועילה בדרכים הבאות:
- הוא חוסך בזיכרון של האפליקציה כי הוא מצמצם את הנתונים רק למה שצריך.
- הוא מתאים סוגי נתונים חיצוניים לסוגי הנתונים שבהם האפליקציה משתמשת – לדוגמה, יכול להיות שהאפליקציה משתמשת בסוג נתונים שונה כדי לייצג תאריכים.
- הוא מאפשר הפרדה טובה יותר בין נושאים – לדוגמה, חברים בצוות גדול יכולים לעבוד בנפרד על שכבות הרשת וממשק המשתמש של תכונה מסוימת אם מחלקת המודל מוגדרת מראש.
אפשר להרחיב את השיטה הזו ולהגדיר מחלקות מודל נפרדות גם בחלקים אחרים של ארכיטקטורת האפליקציה – למשל, במחלקות של מקורות נתונים וב-ViewModels. עם זאת, כדי לעשות את זה צריך להגדיר מחלקות נוספות ולוגיקה נוספת, ולתעד ולבדוק אותן בצורה נכונה. מומלץ ליצור מודלים חדשים בכל מקרה שבו מקור נתונים מקבל נתונים שלא תואמים למה שהאפליקציה מצפה לו.
סוגים של פעולות על נתונים
שכבת הנתונים יכולה להתמודד עם סוגים שונים של פעולות, בהתאם לרמת הקריטיות שלהן: פעולות שקשורות לממשק המשתמש, פעולות שקשורות לאפליקציה ופעולות שקשורות לעסק.
פעולות שמתבצעות דרך ממשק המשתמש
פעולות שמתבצעות בממשק המשתמש רלוונטיות רק כשהמשתמש נמצא במסך מסוים, והן מבוטלות כשהמשתמש עובר למסך אחר. לדוגמה, הצגת נתונים מסוימים שהתקבלו ממסד הנתונים.
בדרך כלל, פעולות שקשורות לממשק המשתמש מופעלות על ידי שכבת ממשק המשתמש ופועלות לפי מחזור החיים של הרכיב שמפעיל אותן – לדוגמה, מחזור החיים של ViewModel. דוגמה לפעולה שמתבצעת דרך ממשק המשתמש מופיעה בקטע שליחת בקשה לרשת.
פעולות שקשורות לאפליקציה
פעולות שקשורות לאפליקציה רלוונטיות כל עוד האפליקציה פתוחה. אם האפליקציה נסגרת או שהתהליך מופסק, הפעולות האלה מבוטלות. דוגמה לכך היא שמירת התוצאה של בקשת רשת במטמון, כדי שאפשר יהיה להשתמש בה בהמשך אם יהיה צורך. מידע נוסף זמין בקטע הטמעה של שמירת נתונים במטמון בזיכרון.
בדרך כלל הפעולות האלה מתבצעות במהלך מחזור החיים של המחלקה Application או של שכבת הנתונים. לדוגמה, אפשר לעיין בקטע הארכת משך הפעולה מעבר למסך.
פעולות שקשורות לעסק
אי אפשר לבטל פעולות שקשורות לעסק. הם צריכים להמשיך להתקיים גם אחרי שהתהליך מסתיים. לדוגמה, השלמת ההעלאה של תמונה שהמשתמש רוצה לפרסם בפרופיל שלו.
ההמלצה לפעולות שקשורות לעסק היא להשתמש ב-WorkManager. מידע נוסף זמין בקטע תזמון משימות באמצעות WorkManager.
חשיפת שגיאות
אינטראקציות עם מאגרי מידע ומקורות נתונים יכולות להצליח או להחזיר חריגה אם מתרחש כשל. בקורוטינות וב-Flows, צריך להשתמש במנגנון מובנה לטיפול בשגיאות של Kotlin. כדי לטפל בשגיאות שיכולות להיגרם על ידי פונקציות השעיה, משתמשים בבלוקים של try/catch כשמתאים, ובזרימות משתמשים באופרטור catch. בגישה הזו, שכבת ממשק המשתמש אמורה לטפל בחריגים כשקוראים לשכבת הנתונים.
שכבת הנתונים יכולה להבין ולטפל בסוגים שונים של שגיאות ולחשוף אותן באמצעות חריגים מותאמים אישית – לדוגמה, UserNotAuthenticatedException.
מידע נוסף על שגיאות ב-coroutines זמין בפוסט בבלוג בנושא Exceptions in coroutines.
משימות נפוצות
בסעיפים הבאים מוצגות דוגמאות לשימוש בשכבת הנתונים ולתכנון שלה כדי לבצע משימות מסוימות שנפוצות באפליקציות ל-Android. הדוגמאות מבוססות על אפליקציית החדשות הטיפוסית שהוזכרה קודם במדריך.
שליחת בקשת רשת
ביצוע בקשת רשת הוא אחת המשימות הנפוצות שאפליקציית Android עשויה לבצע. אפליקציית החדשות צריכה להציג למשתמש את החדשות העדכניות ביותר שנשלפות מהרשת. לכן, האפליקציה צריכה מחלקה של מקור נתונים כדי לנהל פעולות ברשת: NewsRemoteDataSource. כדי לחשוף את המידע לשאר האפליקציה, נוצר מאגר חדש שמטפל בפעולות על נתוני חדשות: NewsRepository.
הדרישה היא שהחדשות העדכניות תמיד יתעדכנו כשהמשתמש יפתח את המסך. לכן, זו פעולה שמתבצעת דרך ממשק המשתמש.
יצירת מקור הנתונים
מקור הנתונים צריך לחשוף פונקציה שמחזירה את החדשות האחרונות: רשימה של מופעי ArticleHeadline. מקור הנתונים צריך לספק דרך בטוחה לשימוש ב-main כדי לקבל את החדשות האחרונות מהרשת. לשם כך, צריך ליצור תלות ב-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 משמש כשרת proxy למקור נתוני הרשת. היתרונות של הוספת שכבת ההפשטה הנוספת הזו מוסברים בקטע שמירת נתונים במטמון בזיכרון.
// NewsRepository is consumed from other layers of the hierarchy.
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource
) {
suspend fun fetchLatestNews(): List<ArticleHeadline> =
newsRemoteDataSource.fetchLatestNews()
}
במאמר שכבת ממשק המשתמש מוסבר איך להשתמש במחלקת המאגר ישירות משכבת ממשק המשתמש.
הטמעה של שמירת נתונים במטמון בזיכרון
נניח שנוספה דרישה חדשה לאפליקציית החדשות: כשהמשתמש פותח את המסך, צריך להציג לו חדשות שנשמרו במטמון אם נשלחה בקשה בעבר. אחרת, האפליקציה צריכה לשלוח בקשה לרשת כדי לאחזר את החדשות האחרונות.
בהתאם לדרישה החדשה, האפליקציה צריכה לשמור את החדשות האחרונות בזיכרון בזמן שהאפליקציה פתוחה למשתמש. לכן, זו פעולה שמתמקדת באפליקציה.
קובצי מטמון
כדי לשמור את הנתונים בזמן שהמשתמש נמצא באפליקציה, אפשר להוסיף שמירת נתונים במטמון בזיכרון. מטרת המטמון היא לשמור מידע מסוים בזיכרון למשך זמן מסוים – במקרה הזה, כל עוד המשתמש נמצא באפליקציה. הטמעות של מטמון יכולות להיות בצורות שונות. הם יכולים להיות משתנים פשוטים שניתנים לשינוי או מחלקות מתוחכמות יותר שמגנות מפני פעולות קריאה/כתיבה בכמה שרשורים. בהתאם לתרחיש השימוש, אפשר להטמיע שמירה במטמון במאגר או במחלקות של מקורות נתונים.
שמירת התוצאה של בקשת הרשת במטמון
כדי לפשט את התהליך, 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 או באמצעות מאגר שרשורים משלכם.
class NewsRepository(
...,
// This could be CoroutineScope(SupervisorJob() + Dispatchers.Default).
private val externalScope: CoroutineScope
) { ... }
מכיוון ש-NewsRepository מוכן לבצע פעולות שקשורות לאפליקציה עם CoroutineScope חיצוני, הוא צריך לבצע את הקריאה למקור הנתונים ולשמור את התוצאה שלו באמצעות שגרת המשך (coroutine) חדשה שהופעלה על ידי ההיקף הזה:
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 משמשת להפעלת שגרת המשך (coroutine) בהיקף החיצוני. הפונקציה await נקראת בשגרת ההמשך החדשה כדי להשהות עד שהבקשה לרשת חוזרת והתוצאה נשמרת במטמון. אם עד אז המשתמש עדיין נמצא במסך, הוא יראה את החדשות האחרונות. אם המשתמש עובר למקום אחר, הפעולה await מבוטלת אבל הלוגיקה בתוך async ממשיכה לפעול.
מידע נוסף על דפוסים ב-CoroutineScope
שמירה ואחזור של נתונים מהדיסק
נניח שאתם רוצים לשמור נתונים כמו חדשות שסומנו בסימנייה והעדפות משתמשים. הנתונים האלה צריכים להישמר גם אם התהליך נכשל, ולהיות נגישים גם אם המשתמש לא מחובר לרשת.
אם הנתונים שאתם עובדים איתם צריכים להישמר גם אחרי שהתהליך מסתיים, אתם צריכים לאחסן אותם בדיסק באחת מהדרכים הבאות:
- כדי לשמור קבוצות נתונים גדולות שצריך להריץ עליהן שאילתות, שנדרשת בהן שלמות הקשרים או שצריך לעדכן אותן באופן חלקי, צריך לשמור את הנתונים במסד נתונים של Room. בדוגמה של אפליקציית החדשות, אפשר לשמור את הכתבות או את שמות המחברים במסד הנתונים.
- אם יש לכם קבוצות נתונים קטנות שצריך רק לאחזר ולהגדיר (לא לבצע שאילתות או לעדכן חלקית), כדאי להשתמש ב-DataStore. בדוגמה של אפליקציית החדשות, יכול להיות שפורמט התאריך המועדף על המשתמש או העדפות תצוגה אחרות יישמרו ב-DataStore.
- כדי להעלות חלקים של נתונים כמו אובייקט JSON, משתמשים בקובץ.
כמו שצוין בקטע מקור האמת, כל מקור נתונים פועל רק עם מקור אחד ומתאים לסוג נתונים ספציפי (לדוגמה, News, Authors, NewsAndAuthors או UserPreferences). מחלקות שמשתמשות במקור הנתונים לא אמורות לדעת איך הנתונים נשמרים – לדוגמה, במסד נתונים או בקובץ.
הגדרת חדר כמקור נתונים
מכיוון שכל מקור נתונים צריך להיות אחראי לעבודה עם מקור אחד בלבד של סוג נתונים ספציפי, מקור נתונים של Room יקבל כפרמטר אובייקט גישה לנתונים (DAO) או את מסד הנתונים עצמו. לדוגמה, NewsLocalDataSource יכולה לקבל מופע של NewsDao כפרמטר, ו-AuthorsLocalDataSource יכולה לקבל מופע של AuthorsDao.
במקרים מסוימים, אם לא נדרשת לוגיקה נוספת, אפשר להחדיר את ה-DAO ישירות למאגר, כי ה-DAO הוא ממשק שאפשר להחליף בקלות בבדיקות.
מידע נוסף על עבודה עם ממשקי Room API זמין במדריכים בנושא Room.
DataStore כמקור נתונים
DataStore הוא פתרון מצוין לאחסון של צמדי מפתח-ערך כמו הגדרות משתמש. לדוגמה, פורמט השעה, העדפות ההתראות והאם להציג או להסתיר פריטי חדשות אחרי שהמשתמש קרא אותם. ב-DataStore אפשר גם לאחסן אובייקטים מוקלדים באמצעות מאגרי אחסון לפרוטוקולים.
בדומה לכל אובייקט אחר, מקור נתונים שמגובה על ידי DataStore צריך להכיל נתונים שתואמים לסוג מסוים או לחלק מסוים באפליקציה. זה נכון במיוחד לגבי DataStore, כי קריאות של DataStore מוצגות כזרימה שמופעלת בכל פעם שערך מתעדכן. לכן כדאי לאחסן העדפות קשורות באותו DataStore.
לדוגמה, יכול להיות שיש לכם NotificationsDataStore שמטפל רק בהעדפות שקשורות להתראות ו-NewsPreferencesDataStore שמטפל רק בהעדפות שקשורות למסך החדשות. כך תוכלו להבין טוב יותר את היקף העדכונים, כי הפונקציה newsScreenPreferencesDataStore.data מופעלת רק כשמשנים העדפה שקשורה למסך הזה. המשמעות היא גם שמחזור החיים של האובייקט יכול להיות קצר יותר, כי הוא יכול להתקיים רק כל עוד מסך החדשות מוצג.
מידע נוסף על עבודה עם ממשקי DataStore API זמין במדריכים בנושא DataStore.
קובץ כמקור נתונים
כשעובדים עם אובייקטים גדולים כמו אובייקט JSON או מפת סיביות, צריך לעבוד עם אובייקט File ולטפל במעבר בין השרשורים.
מידע נוסף על עבודה עם אחסון קבצים זמין בדף סקירה כללית על אחסון.
תזמון משימות באמצעות WorkManager
נניח שנוסף עוד דרישה חדשה לאפליקציית החדשות: האפליקציה צריכה לתת למשתמש את האפשרות לאחזר את החדשות האחרונות באופן קבוע ואוטומטי כל עוד המכשיר בטעינה ומחובר לרשת ללא הגבלת נפח. לכן מדובר בפעולה עסקית. הדרישה הזו מבטיחה שגם אם המכשיר לא מחובר לאינטרנט כשהמשתמש פותח את האפליקציה, הוא עדיין יוכל לראות חדשות מהזמן האחרון.
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 ולוודא שהבקשות בוצעו כמצופה.
מקורות מידע נוספים
דוגמאות
- Jetcaster
- תבנית התחלתית לארכיטקטורה (multi-module)
- ארכיטקטורה
- תבנית התחלה לארכיטקטורה (מודול יחיד)
- אפליקציית Now in Android
מומלץ בשבילכם
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- שכבת הדומיין
- פיתוח אפליקציה שפועלת אופליין
- ייצור מצבים לממשקי משתמש