מדריך לארכיטקטורת אפליקציות

ארכיטקטורת האפליקציה היא הבסיס לאפליקציה ל-Android איכותית. ארכיטקטורה מוגדרת היטב מאפשרת ליצור אפליקציה שניתנת להרחבה ולתחזוקה, ויכולה להתאים את עצמה למערכת האקולוגית המתרחבת של מכשירי Android, כולל טלפונים, טאבלטים, מכשירים מתקפלים, מכשירי ChromeOS, מסכים ברכב ו-XR.

הרכב האפליקציות

אפליקציית Android טיפוסית מורכבת מכמה רכיבי אפליקציה, כמו שירותים, ספקי תוכן ומקלט שידורים. מצהירים על הרכיבים האלה בקובץ מניפסט של אפליקציה.

ממשק המשתמש של אפליקציה הוא גם רכיב. בעבר, ממשקי משתמש נבנו באמצעות כמה פעילויות. עם זאת, אפליקציות מודרניות משתמשות בארכיטקטורה של פעילות יחידה. רכיב Activity יחיד משמש כקונטיינר למסכים או ליעדים של Jetpack Compose.

מספר גורמי צורה

אפליקציות יכולות לפעול במגוון מכשירים, כולל לא רק טלפונים, אלא גם טאבלטים, מכשירים מתקפלים, מכשירי ChromeOS ועוד. אל תניחו שהאפליקציה תמיד תישאר קבועה במצב אנכי או אופקי. שינויים בהגדרות, כמו סיבוב המכשיר או קיפול ופתיחה של מכשיר מתקפל, גורמים לאפליקציה להרכיב מחדש את ממשק המשתמש שלה, וזה משפיע על מצב האפליקציה.

משאבים מוגבלים

בניידים – גם במכשירים עם מסך גדול – יש מגבלות על משאבי המערכת, ולכן בכל שלב מערכת ההפעלה עשויה להפסיק את תהליך האפליקציה כדי להקצות את המשאבים שלה לתהליכים אחרים.

תנאי הפעלה של משתנה

בסביבה עם מגבלות על משאבים, יכול להיות שהרכיבים של האפליקציה יופעלו בנפרד ולא לפי הסדר, ויכול להיות שמערכת ההפעלה או המשתמש ישמידו אותם בכל שלב. לכן, אל תשמרו נתונים או מצב של אפליקציה ברכיבי האפליקציה. רכיבי האפליקציה צריכים להיות עצמאיים ולא תלויים זה בזה.

עקרונות אדריכליים נפוצים

אם אי אפשר להשתמש ברכיבי אפליקציה כדי לאחסן נתונים ומצב של אפליקציה, איך צריך לתכנן את האפליקציה?

ככל שאפליקציות ל-Android גדלות, חשוב להגדיר ארכיטקטורה שתאפשר את הרחבת האפליקציה. ארכיטקטורה מתוכננת היטב של אפליקציה מגדירה את הגבולות בין חלקי האפליקציה ואת האחריות של כל חלק.

הפרדה בין נושאים

כדאי לעצב את ארכיטקטורת האפליקציה לפי כמה עקרונות ספציפיים.

העיקרון הכי חשוב הוא הפרדה בין נושאים: הפרדה של האפליקציה לשיטות, מחלקות, קבצים, חבילות, מודולים ושכבות עם גבולות ואחריות מוגדרים בבירור.

טעות נפוצה היא לכתוב את כל הקוד ב-Activity.

התפקיד העיקרי של Activity הוא לארח את ממשק המשתמש של האפליקציה. מערכת ההפעלה Android שולטת במחזור החיים שלהם, ולעתים קרובות היא משמידה אותם ויוצרת אותם מחדש בתגובה לפעולות של המשתמשים, כמו סיבוב המסך, או לאירועים במערכת, כמו זיכרון נמוך.

האופי הארעי הזה הופך אותם ללא מתאימים לאחסון נתוני אפליקציה או מצב. אם מאחסנים נתונים ב-Activity, הנתונים האלה יאבדו כשהרכיב ייווצר מחדש. כדי להבטיח שהנתונים יישמרו ולספק חוויית משתמש יציבה, אל תסתמכו על רכיבי ממשק המשתמש האלה כדי לשמור את המצב.

פריסות מותאמות

צריך לפתח אפליקציות שמסוגלות לטפל בשינויים בהגדרות, כמו שינויים בכיוון ההצגה של המכשיר או שינויים בגודל של חלון האפליקציה. כדי לספק חוויית משתמש אופטימלית במגוון גורמים צורה, כדאי להטמיע פריסות קנוניות שמותאמות למכשירים שונים.

התאמה אישית של ממשק המשתמש ב-Drive ממודלים של נתונים

עוד עיקרון חשוב הוא להשתמש במודלים של נתונים כדי להניע את ממשק המשתמש, רצוי במודלים קבועים. מודלים של נתונים מייצגים את הנתונים של אפליקציה. הם בלתי תלויים ברכיבי ממשק המשתמש וברכיבים אחרים באפליקציה. כלומר, הם לא קשורים למחזור החיים של ממשק המשתמש ושל רכיבי האפליקציה, אבל הם עדיין ייהרסו כשהמערכת תסיר את התהליך של האפליקציה מהזיכרון.

מודלים קבועים הם פתרון אידיאלי מהסיבות הבאות:

  • המשתמשים לא מאבדים נתונים אם מערכת ההפעלה של Android משמידה את האפליקציה כדי לפנות משאבים.

  • האפליקציה ממשיכה לפעול במקרים שבהם החיבור לרשת הוא לסירוגין או לא זמין.

כדאי לבסס את ארכיטקטורת האפליקציה על מחלקות של מודל נתונים כדי שהאפליקציה תהיה חזקה וניתנת לבדיקה.

מקור מידע אמין

כשמגדירים סוג נתונים חדש באפליקציה, צריך להקצות לו מקור נתונים יחיד (SSOT). ה-SSOT הוא הבעלים של הנתונים האלה, ורק הוא יכול לשנות אותם. כדי לעשות את זה, ה-SSOT חושף את הנתונים באמצעות סוג נתונים שלא ניתן לשינוי. כדי לשנות את הנתונים, ה-SSOT חושף פונקציות או מקבל אירועים שסוגי נתונים אחרים יכולים להפעיל.

לדפוס הזה יש כמה יתרונות:

  • כל השינויים בסוג נתונים מסוים מרוכזים במקום אחד
  • ההגנה על הנתונים מונעת מסוגים אחרים של כלים לשנות אותם
  • השינוי מאפשר לעקוב אחרי הנתונים בצורה טובה יותר, וכך קל יותר לזהות באגים

באפליקציה שפועלת אופליין קודם, המקור המהימן לנתוני האפליקציה הוא בדרך כלל מסד נתונים. במקרים אחרים, מקור האמת יכול להיות ViewModel.

זרימת נתונים חד-כיוונית

עקרון המקור המרוכז האחד משמש לעיתים קרובות עם דפוס זרימת הנתונים החד-כיוונית (UDF). ב-UDF, state זורם רק בכיוון אחד, בדרך כלל מרכיב אב לרכיב צאצא. האירועים שמשנים את זרימת הנתונים בכיוון ההפוך.

ב-Android, בדרך כלל המצב או הנתונים זורמים מהסוגים בהיררכיה עם ההיקף הגבוה יותר לסוגים עם ההיקף הנמוך יותר. בדרך כלל, האירועים מופעלים מהסוגים עם ההיקף הנמוך יותר עד שהם מגיעים ל-SSOT של סוג הנתונים המתאים. לדוגמה, נתוני אפליקציות בדרך כלל זורמים ממקורות נתונים לממשק המשתמש. אירועים של משתמשים, כמו לחיצות על לחצנים, מועברים מממשק המשתמש אל ה-SSOT, שבו נתוני האפליקציה משתנים ומוצגים כסוג נתונים שלא ניתן לשנות.

הדפוס הזה שומר טוב יותר על עקביות הנתונים, פחות מועד לשגיאות, קל יותר לניפוי באגים ומספק את כל היתרונות של דפוס ה-SSOT.

מידע נוסף על UDF זמין במאמר זרימת נתונים חד-כיוונית ב-Jetpack Compose.

בהתאם לעקרונות ארכיטקטוניים נפוצים, צריך לעצב כל אפליקציה עם לפחות שתי שכבות:

  • שכבת ממשק המשתמש: מציגה נתונים של האפליקציה במסך
  • שכבת הנתונים: מכילה את הלוגיקה העסקית של האפליקציה ומציגה את נתוני האפליקציה

כדי לפשט את האינטראקציות בין שכבות ממשק המשתמש והנתונים ולעשות בהן שימוש חוזר, אפשר להוסיף שכבה נוספת שנקראת שכבת הדומיין.

בארכיטקטורה טיפוסית של אפליקציה, שכבת ממשק המשתמש מקבלת את נתוני האפליקציה משכבת הנתונים או משכבת הדומיין האופציונלית, שנמצאת בין שכבת ממשק המשתמש לבין שכבת הנתונים.
איור 1. תרשים של ארכיטקטורת אפליקציה טיפוסית.

ארכיטקטורה מודרנית של אפליקציות

ארכיטקטורה מודרנית של אפליקציות ל-Android משתמשת בטכניקות הבאות (בין היתר):

  • ארכיטקטורה מותאמת ושכבתית
  • זרימת נתונים חד-כיוונית (UDF) בכל השכבות של האפליקציה
  • שכבת ממשק משתמש עם מאחסני מצבים לניהול המורכבות של ממשק המשתמש
  • קורוטינות וזרימות
  • שיטות מומלצות להזרקת תלות
  • אופטימיזציה של הביצועים באמצעות R8 ופרופילים של Baseline, ומדידה באמצעות Macrobenchmark

מידע נוסף זמין במאמר בנושא המלצות לגבי ארכיטקטורת Android.

שכבת ממשק המשתמש

התפקיד של שכבת ממשק המשתמש (או שכבת ההצגה) הוא להציג את נתוני האפליקציה על המסך. בכל פעם שהנתונים משתנים, בגלל אינטראקציה של המשתמש (למשל לחיצה על לחצן) או קלט חיצוני (למשל תגובה של הרשת), ממשק המשתמש מתעדכן בהתאם לשינויים.

שכבת ממשק המשתמש מורכבת משני סוגים של מבנים:

  • רכיבי ממשק משתמש שמציגים את הנתונים במסך. כדי ליצור פריסות מותאמות, משתמשים בפונקציות של Jetpack Compose.
  • מאחסני מצבים (כמו ViewModel) שמכילים נתונים, חושפים אותם לממשק המשתמש ומטפלים בלוגיקה. מאחסני מצבים צריכים להתקיים למשך הזמן שבו קיים רכיב ממשק המשתמש שהם מספקים לו מצב. לדוגמה, אובייקט ViewModel של מסך צריך להישמר בזיכרון עד שהמסך יוסר ממקבץ הפעילויות הקודמות (back stack) של הניווט של האפליקציה. מידע נוסף זמין במאמר בנושא משך החיים של מצבים.
בארכיטקטורה טיפוסית, רכיבי ממשק המשתמש בשכבת ממשק המשתמש תלויים במחזיקי מצב, שבתורם תלויים במחלקות משכבת הנתונים או משכבת הדומיין האופציונלית.
איור 2. תפקיד שכבת ממשק המשתמש בארכיטקטורת האפליקציה.

בממשקי משתמש אדפטיביים, מאחסני מצבים כמו אובייקטים של ViewModel חושפים מצב של ממשק משתמש שמותאם לסוגים שונים של גודל חלון. אפשר להשתמש ב-currentWindowAdaptiveInfo() כדי להסיק את מצב ממשק המשתמש הזה. רכיבים כמו NavigationSuiteScaffold יכולים להשתמש במידע הזה כדי לעבור אוטומטית בין דפוסי ניווט שונים (לדוגמה, NavigationBar,‏ NavigationRail או NavigationDrawer) בהתאם לשטח המסך הזמין.

מידע נוסף זמין במאמרים שכבת ממשק המשתמש וארכיטקטורת ממשק המשתמש של Compose.

מידע נוסף על אפליקציות מותאמות וניווט זמין במאמרים פיתוח אפליקציות מותאמות ופיתוח ניווט מותאם.

שכבת נתונים

שכבת הנתונים של אפליקציה מכילה את הלוגיקה העסקית. הלוגיקה העסקית היא מה שנותן ערך לאפליקציה שלכם – היא כוללת כללים שקובעים איך האפליקציה יוצרת, מאחסנת ומשנה נתונים.

שכבת הנתונים מורכבת ממאגרי מידע, שכל אחד מהם יכול להכיל אפס עד הרבה מקורות נתונים. יוצרים מחלקת מאגר לכל סוג נתונים שמועבר באפליקציה. לדוגמה, אפשר ליצור מחלקה MoviesRepositoryלנתונים שקשורים לסרטים או מחלקה PaymentsRepositoryלנתונים שקשורים לתשלומים.

בארכיטקטורה טיפוסית, מאגרי המידע בשכבת הנתונים מספקים נתונים לשאר האפליקציה ותלויים במקורות הנתונים.
איור 3. תפקיד שכבת הנתונים בארכיטקטורת האפליקציה.

מחלקות המאגר אחראיות על הפעולות הבאות:

  • חשיפת הנתונים לשאר האפליקציה
  • ריכוז השינויים בנתונים
  • פתרון בעיות שקשורות לכמה מקורות נתונים
  • הפרדה של מקורות הנתונים משאר האפליקציה
  • מכיל לוגיקה עסקית

כל מחלקה של מקור נתונים אחראית לעבוד רק עם מקור נתונים אחד, שיכול להיות קובץ, מקור נתונים ברשת או מסד נתונים מקומי. מחלקות של מקורות נתונים הן הגשר בין האפליקציה לבין המערכת לפעולות שקשורות לנתונים.

מידע נוסף זמין בדף בנושא שכבת הנתונים.

שכבת הדומיין

שכבת הדומיין היא שכבה אופציונלית בין שכבת ממשק המשתמש לשכבת הנתונים.

שכבת הדומיין אחראית להצפנה של לוגיקה עסקית מורכבת או של לוגיקה עסקית פשוטה יותר שנעשה בה שימוש חוזר בכמה מודלים של תצוגות. שכבת הדומיין היא אופציונלית כי לא כל האפליקציות עומדות בדרישות האלה. משתמשים בו רק כשצריך – למשל, כדי לטפל במורכבות או כדי להעדיף שימוש חוזר.

כשכוללים אותה, שכבת הדומיין האופציונלית מספקת תלות לשכבת ממשק המשתמש ותלויה בשכבת הנתונים.
איור 4. תפקיד שכבת הדומיין בארכיטקטורת האפליקציה.

בדרך כלל קוראים למחלקות בשכבת הדומיין תרחישי שימוש או אינטראקטורים. כל תרחיש שימוש אחראי לפונקציונליות אחת. לדוגמה, יכול להיות שבאפליקציה שלכם יש מחלקה GetTimeZoneUseCase אם כמה מודלים של תצוגות מסתמכים על אזורי זמן כדי להציג את ההודעה המתאימה במסך.

מידע נוסף זמין בדף בנושא שכבת הדומיין.

ניהול יחסי תלות בין רכיבים

כדי שהכיתות באפליקציה יפעלו בצורה תקינה, הן צריכות להסתמך על כיתות אחרות. אפשר להשתמש באחד מדפוסי התכנון הבאים כדי לאסוף את התלויות של מחלקה מסוימת:

  • ‫Dependency injection (DI): ‏ Dependency injection מאפשרת למחלקות להגדיר את התלויות שלהן בלי לבנות אותן. בזמן הריצה, מחלקה אחרת אחראית לספק את יחסי התלות האלה.
  • איתור שירותים: התבנית של איתור שירותים מספקת מרשם שבו מחלקות יכולות לקבל את התלות שלהן במקום ליצור אותן.

הדפוסים האלה מאפשרים לכם להרחיב את הקוד כי הם מספקים דפוסים ברורים לניהול תלות בלי לשכפל קוד או להוסיף מורכבות. התבניות גם מאפשרות לעבור במהירות בין הטמעה של בדיקות לבין הטמעה של ייצור.

המלצות כלליות

תכנות הוא תחום יצירתי, ופיתוח אפליקציות ל-Android לא שונה. יש הרבה דרכים לפתור בעיה. למשל, אפשר להעביר נתונים בין כמה פעילויות או קטעי קוד, לאחזר נתונים מרחוק ולשמור אותם באופן מקומי לשימוש במצב אופליין, או לטפל בכל מספר של תרחישים נפוצים אחרים שקורים באפליקציות מורכבות.

ההמלצות הבאות לא מחייבות, אבל ברוב המקרים הן עוזרות ליצור בסיס קוד יציב יותר, שקל יותר לבדוק ולתחזק.

אל תשמרו נתונים ברכיבי האפליקציה.

אל תגדירו את נקודות הכניסה לאפליקציה – כמו פעילויות, שירותים ומקלטי שידורים – כמקורות נתונים. נקודות הכניסה צריכות לתאם עם רכיבים אחרים כדי לאחזר רק את קבוצת המשנה של הנתונים שרלוונטית לנקודת הכניסה. כל רכיב באפליקציה הוא קצר מועד, בהתאם לאינטראקציה של המשתמש עם המכשיר וליכולת של המערכת.

צמצום התלות בכיתות Android.

מוודאים שרכיבי האפליקציה הם המחלקות היחידות שמסתמכות על ממשקי API של Android framework SDK, כמו Context או Toast. הפשטה של מחלקות אחרות באפליקציה מרכיבי האפליקציה עוזרת בבדיקה ומפחיתה את הצימוד באפליקציה.

הגדרת גבולות ברורים של אחריות בין המודולים באפליקציה.

אל תפיצו את הקוד שמעמיס נתונים מהרשת על פני כמה מחלקות או חבילות בבסיס הקוד. באופן דומה, אל תגדירו באותה מחלקה כמה תחומי אחריות לא קשורים, כמו אחסון נתונים במטמון וקישור נתונים. פועלים לפי ארכיטקטורת האפליקציה המומלצת.

חשיפה של כמה שפחות נתונים מכל מודול.

לא ליצור קיצורי דרך שחושפים פרטי הטמעה פנימיים. יכול להיות שתרוויחו קצת זמן בטווח הקצר, אבל סביר להניח שתצברו חוב טכני גדול בהרבה ככל שבסיס הקוד יתפתח.

מתמקדים בליבה הייחודית של האפליקציה כדי שהיא תבלוט בין אפליקציות אחרות.

אל תמציאו את הגלגל מחדש ותכתבו את אותו קוד שחוזר על עצמו (boilerplate) שוב ושוב. במקום זאת, כדאי להשקיע את הזמן והאנרגיה במה שהופך את האפליקציה שלכם לייחודית. מאפשרים לספריות Jetpack ולספריות מומלצות אחרות לטפל בקוד הסטנדרטי החוזר על עצמו.

שימוש בפריסות קנוניות ובתבניות עיצוב של אפליקציות.

ספריות Jetpack Compose מספקות ממשקי API חזקים ליצירת ממשקי משתמש מותאמים. כדאי להשתמש בפריסות קנוניות באפליקציה כדי לשפר את חוויית המשתמש במגוון גורמי צורה וגדלים של מסכים. כדאי לעיין בגלריה של דפוסי עיצוב אפליקציות כדי לבחור את הפריסות שהכי מתאימות לתרחישי השימוש שלכם.

שמירת מצב ממשק המשתמש אחרי שינויים בהגדרות.

כשמעצבים פריסות מותאמות, חשוב לשמור את מצב ממשק המשתמש כשחלים שינויים בהגדרות, כמו שינוי גודל המסך, קיפול ושינויים בכיוון. הארכיטקטורה צריכה לוודא שהמצב הנוכחי של המשתמש נשמר, כדי לספק חוויה חלקה.

תכנון רכיבים קומפוזביליים של ממשק משתמש שאפשר לעשות בהם שימוש חוזר.

כדי לתמוך בעיצוב אדפטיבי, אפשר ליצור רכיבי ממשק משתמש שאפשר להשתמש בהם שוב וליצור מהם קומפוזיציות. כך תוכלו לשלב ולסדר מחדש רכיבים כדי שיתאימו לגדלים שונים של מסכים ולמצבי שימוש שונים בלי לבצע שינויים משמעותיים בקוד.

חשבו איך לבדוק כל חלק באפליקציה בנפרד.

ממשק API מוגדר היטב לאחזור נתונים מהרשת מאפשר לבדוק את המודול ששומר את הנתונים האלה במסד נתונים מקומי. לעומת זאת, אם תערבבו את הלוגיקה של שתי הפונקציות האלה במקום אחד, או אם תפיצו את קוד הרשת על פני כל בסיס הקוד, הבדיקה תהיה הרבה יותר קשה, ואולי אפילו בלתי אפשרית.

סוגים אחראים למדיניות המקבילות שלהם.

אם סוג מסוים מבצע עבודה חוסמת ממושכת, הסוג הזה צריך להיות אחראי להעברת החישוב לשרשור הנכון. הסוג יודע איזה חישוב הוא מבצע ובאיזה שרשור להריץ את החישוב. הסוגים צריכים להיות main‑safe, כלומר בטוח להפעיל אותם מה-thread הראשי בלי לחסום אותו.

כדאי לשמור כמה שיותר נתונים רלוונטיים ועדכניים.

כך המשתמשים יכולים ליהנות מהפונקציונליות של האפליקציה גם כשהמכשיר שלהם במצב אופליין. חשוב לזכור שלא לכל המשתמשים יש חיבור קבוע ומהיר, וגם אם יש להם, יכול להיות שהקליטה שלהם תהיה גרועה במקומות הומי אדם.

יתרונות הארכיטקטורה

הטמעה של ארכיטקטורה טובה באפליקציה מביאה הרבה יתרונות לפרויקט ולצוותי ההנדסה:

  • משפר את יכולת התחזוקה, האיכות והיציבות של האפליקציה הכוללת.
  • מאפשר לאפליקציה לשנות את הגודל שלה. יותר אנשים ויותר צוותים יכולים לתרום לאותו בסיס קוד עם מינימום קונפליקטים בקוד.
  • עזרה בתהליך ההצטרפות. הארכיטקטורה מוסיפה עקביות לפרויקט, כך שחברים חדשים בצוות יכולים להיכנס לעניינים במהירות ולהיות יעילים יותר תוך זמן קצר.
  • קל יותר לבדוק. ארכיטקטורה טובה מעודדת שימוש בסוגים פשוטים יותר, שבדרך כלל קל יותר לבדוק אותם.
  • מאפשר לכם לחקור באגים באופן שיטתי באמצעות תהליכים מוגדרים היטב.

למרות שארכיטקטורה טובה דורשת השקעה מראש של זמן, יש לה גם השפעה ישירה על המשתמשים. הם נהנים מאפליקציה יציבה יותר ומעוד תכונות, כי צוות ההנדסה פרודוקטיבי יותר.

דוגמאות

בדוגמאות הבאות אפשר לראות ארכיטקטורת אפליקציה טובה: