ארכיטקטורת האפליקציה היא הבסיס לאפליקציה ל-Android איכותית. ארכיטקטורה מוגדרת היטב מאפשרת ליצור אפליקציה שניתנת להרחבה ולתחזוקה, שיכולה להתאים את עצמה למערכת האקולוגית המתפתחת של מכשירי Android, כולל טלפונים, טאבלטים, מכשירים מתקפלים, מכשירי ChromeOS, מסכים ברכב ו-XR.
הרכב האפליקציות
אפליקציית Android טיפוסית מורכבת מכמה רכיבי אפליקציה, כמו שירותים, ספקי תוכן ומקלט שידורים. מצהירים על הרכיבים האלה בקובץ מניפסט של אפליקציה.
ממשק המשתמש של אפליקציה הוא גם רכיב. בעבר, ממשקי משתמש נבנו באמצעות כמה פעילויות. עם זאת, אפליקציות מודרניות משתמשות בארכיטקטורה של פעילות יחידה. רכיב Activity יחיד משמש כקונטיינר למסכים או ליעדים של Jetpack פיתוח נייטיב.
מספר גורמי צורה
אפליקציות יכולות לפעול במגוון מכשירים, כולל לא רק טלפונים, אלא גם טאבלטים, מכשירים מתקפלים, מכשירי ChromeOS ועוד. אל תניחו שהאפליקציה שלכם תמיד תישאר קבועה במצב אנכי או אופקי. שינויים בהגדרות, כמו סיבוב המכשיר או קיפול ופתיחה של מכשיר מתקפל, גורמים לאפליקציה להרכיב מחדש את ממשק המשתמש שלה, וזה משפיע על מצב האפליקציה.
הגבלות על משאבים
בניידים – גם במכשירים עם מסך גדול – יש מגבלות על המשאבים, ולכן בכל רגע נתון מערכת ההפעלה עשויה להפסיק את תהליך האפליקציה כדי להקצות את המשאבים שלה לתהליכים אחרים.
תנאי הפעלה של משתנה
בסביבה עם מגבלות על משאבים, יכול להיות שהרכיבים של האפליקציה יופעלו בנפרד ולא לפי הסדר. בנוסף, מערכת ההפעלה או המשתמש יכולים להשמיד אותם בכל שלב. לכן, אל תאחסנו נתונים או מצב של אפליקציה ברכיבי האפליקציה. כדאי ליצור רכיבים עצמאיים באפליקציה, שלא תלויים זה בזה.
עקרונות אדריכליים נפוצים
אם אי אפשר להשתמש ברכיבי אפליקציה כדי לאחסן נתונים ומצב של אפליקציה, איך צריך לתכנן את האפליקציה?
ככל שאפליקציות ל-Android גדלות בגודלן, חשוב להגדיר ארכיטקטורה שתאפשר לאפליקציה להתרחב. ארכיטקטורה של אפליקציה שתוכננה היטב מגדירה את הגבולות בין חלקי האפליקציה ואת האחריות של כל חלק.
הפרדה בין תחומים
כדאי לעצב את ארכיטקטורת האפליקציה לפי כמה עקרונות ספציפיים.
העיקרון הכי חשוב הוא הפרדה בין תחומים: הפרדת האפליקציה לשיטות, למחלקות, לקבצים, לחבילות, למודולים ולשכבות עם גבולות ותחומי אחריות מוגדרים בבירור.
טעות נפוצה היא לכתוב את כל הקוד בתוך Activity.
התפקיד העיקרי של Activity הוא לארח את ממשק המשתמש של האפליקציה. מערכת ההפעלה Android שולטת במחזור החיים שלהם, ולעתים קרובות משמידה אותם ויוצרת אותם מחדש בתגובה לפעולות משתמשים כמו סיבוב המסך או אירועי מערכת כמו זיכרון נמוך.
האופי הארעי הזה הופך אותם ללא מתאימים להחזקת נתוני אפליקציה או מצב. אם מאחסנים נתונים ב-Activity, הנתונים האלה יימחקו כשהרכיב ייווצר מחדש. כדי להבטיח שהנתונים יישמרו ולספק חוויית משתמש יציבה, אל תסתמכו על רכיבי ממשק המשתמש האלה כדי לשמור את המצב.
פריסות מותאמות
פיתוח אפליקציות שמסוגלות לטפל בשינויים בהגדרות, כמו שינויים בכיוון המכשיר או שינויים בגודל חלון האפליקציה. כדי לספק חוויית משתמש אופטימלית במגוון גורמי צורה, צריך להטמיע פריסות קנוניות שמותאמות למצבים שונים.
הפעלת ממשק המשתמש של Drive ממודלים של נתונים
עוד עיקרון חשוב הוא להשתמש במודלים של נתונים כדי להניע את ממשק המשתמש, ועדיף במודלים קבועים. מודלים של נתונים מייצגים את הנתונים של אפליקציה. הם בלתי תלויים ברכיבי ממשק המשתמש וברכיבים אחרים באפליקציה. כלומר, הם לא קשורים למחזור החיים של ממשק המשתמש ושל רכיבי האפליקציה, אבל הם עדיין ייהרסו כשהמערכת תסיר את התהליך של האפליקציה מהזיכרון.
מודלים קבועים הם פתרון אידיאלי מהסיבות הבאות:
המשתמשים לא מאבדים נתונים אם מערכת ההפעלה של Android משמידה את האפליקציה כדי לפנות משאבים.
האפליקציה ממשיכה לפעול במקרים שבהם החיבור לרשת הוא לסירוגין או לא זמין.
כדאי לבסס את ארכיטקטורת האפליקציה על מחלקות של מודל נתונים כדי שהאפליקציה תהיה חזקה וניתנת לבדיקה.
מקור מידע אמין
כשמגדירים סוג נתונים חדש באפליקציה, צריך להקצות לו מקור אחד של נתוני אמת (SSOT). ה-SSOT הוא הבעלים של הנתונים האלה, ורק הוא יכול לשנות אותם. כדי לעשות את זה, ה-SSOT חושף את הנתונים באמצעות סוג בלתי ניתן לשינוי. כדי לשנות את הנתונים, ה-SSOT חושף פונקציות או מקבל אירועים שסוגים אחרים יכולים לקרוא להם.
לדפוס הזה יש כמה יתרונות:
- כל השינויים בסוג נתונים מסוים מרוכזים במקום אחד
- ההגדרה מגנה על הנתונים כך שסוגים אחרים לא יוכלו לשנות אותם
- השינויים בנתונים קלים יותר למעקב, ולכן קל יותר לזהות באגים
באפליקציה שפועלת אופליין קודם, המקור האמיתי של נתוני האפליקציה הוא בדרך כלל מסד נתונים. במקרים אחרים, מקור האמת יכול להיות ViewModel.
זרימת נתונים חד-כיוונית
העיקרון של מקור מרוכז אחד משמש לעיתים קרובות עם דפוס זרימת הנתונים החד-כיוונית (UDF). ב-UDF, state זורם רק בכיוון אחד, בדרך כלל מרכיב אב לרכיב צאצא. האירועים שמשנים את זרימת הנתונים בכיוון ההפוך.
ב-Android, בדרך כלל המצב או הנתונים זורמים מהסוגים בהיררכיה עם ההיקף הרחב יותר לסוגים עם ההיקף הצר יותר. בדרך כלל, האירועים מופעלים מהסוגים עם ההיקף הנמוך יותר עד שהם מגיעים ל-SSOT של סוג הנתונים המתאים. לדוגמה, נתוני אפליקציות בדרך כלל זורמים ממקורות נתונים לממשק המשתמש. אירועים של משתמשים, כמו לחיצות על לחצנים, עוברים מממשק המשתמש אל SSOT, שם הנתונים של האפליקציה משתנים ומוצגים כסוג נתונים שלא ניתן לשינוי.
הדפוס הזה שומר טוב יותר על עקביות הנתונים, פחות נוטה לשגיאות, קל יותר לניפוי באגים ומספק את כל היתרונות של דפוס ה-SSOT.
מידע נוסף על UDF זמין במאמר Unidirectional data flow in Jetpack פיתוח נייטיב (זרימת נתונים חד-כיוונית ב-Jetpack פיתוח נייטיב).
ארכיטקטורה מומלצת של אפליקציות
בהתאם לעקרונות ארכיטקטוניים נפוצים, צריך לתכנן כל אפליקציה עם לפחות שתי שכבות:
- שכבת ממשק המשתמש: מציגה את נתוני האפליקציה במסך
- שכבת הנתונים: מכילה את הלוגיקה העסקית של האפליקציה ומציגה את נתוני האפליקציה
אפשר להוסיף שכבה נוספת שנקראת שכבת הדומיין כדי לפשט את האינטראקציות בין ממשק המשתמש ושכבות הנתונים, ולעשות בהן שימוש חוזר.
ארכיטקטורה מודרנית של אפליקציות
ארכיטקטורה מודרנית של אפליקציות ל-Android משתמשת בטכניקות הבאות (בין היתר):
- ארכיטקטורה אדפטיבית ושכבתית
- זרימת נתונים חד-כיוונית (UDF) בכל השכבות של האפליקציה
- שכבת ממשק משתמש עם מחזיקי מצב לניהול המורכבות של ממשק המשתמש
- קורוטינות וזרימות
- שיטות מומלצות להזרקת תלות
מידע נוסף זמין במאמר בנושא המלצות לגבי ארכיטקטורת Android.
שכבת ממשק המשתמש
התפקיד של שכבת ממשק המשתמש (או שכבת ההצגה) הוא להציג את נתוני האפליקציה במסך. בכל פעם שהנתונים משתנים, בגלל אינטראקציה של המשתמש (למשל, לחיצה על כפתור) או קלט חיצוני (למשל, תגובה של הרשת), ממשק המשתמש מתעדכן בהתאם לשינויים.
שכבת ממשק המשתמש מורכבת משני סוגים של מבנים:
- רכיבי ממשק משתמש שמציגים את הנתונים במסך. כדי ליצור פריסות מותאמות, משתמשים בפונקציות של Jetpack פיתוח נייטיב.
- מאחסני מצב (כמו
ViewModel) שמאחסנים נתונים, חושפים אותם לממשק המשתמש ומטפלים בלוגיקה. הערכים של המאפיין state צריכים להיות בתוקף למשך אותו פרק זמן כמו הרכיב בממשק המשתמש שעבורו הם מספקים את המצב. לדוגמה, צריך לשמור בזיכרון ViewModel של מסך עד שהמסך מוסר ממקבץ פעילויות קודמות (back stack) של האפליקציה. מידע נוסף מופיע במאמר בנושא משך החיים של מצבים.
בממשקי משתמש אדפטיביים, מחזיקי מצב כמו אובייקטים של ViewModel חושפים את מצב ממשק המשתמש שמותאם לסיווגים שונים של גודל חלון. אפשר להשתמש ב-currentWindowAdaptiveInfo() כדי להסיק את מצב ממשק המשתמש הזה. רכיבים כמו NavigationSuiteScaffold יכולים להשתמש במידע הזה כדי לעבור אוטומטית בין דפוסי ניווט שונים (לדוגמה, NavigationBar, NavigationRail או NavigationDrawer) בהתאם לשטח המסך הזמין.
מידע נוסף זמין במאמרים שכבת ממשק המשתמש וארכיטקטורת ממשק המשתמש של Compose.
מידע נוסף על אפליקציות מותאמות ועל ניווט זמין במאמרים יצירת אפליקציות מותאמות ויצירת ניווט מותאם.
שכבת נתונים
שכבת הנתונים של אפליקציה מכילה את הלוגיקה העסקית. הלוגיקה העסקית היא מה שנותן ערך לאפליקציה שלכם – היא כוללת כללים שקובעים איך האפליקציה יוצרת, מאחסנת ומשנה נתונים.
שכבת הנתונים מורכבת ממאגרי מידע, שכל אחד מהם יכול להכיל אפס עד הרבה מקורות נתונים. יוצרים מחלקה של מאגר לכל סוג נתונים שמועבר באפליקציה. לדוגמה, אפשר ליצור מחלקה MoviesRepositoryלנתונים שקשורים לסרטים או מחלקה PaymentsRepositoryלנתונים שקשורים לתשלומים.
האחריות של מחלקות המאגר היא:
- חשיפת נתונים לשאר האפליקציה
- ריכוז השינויים בנתונים
- פתרון התנגשויות בין כמה מקורות נתונים
- הפרדה של מקורות הנתונים משאר האפליקציה
- מכיל לוגיקה עסקית
כל מחלקה של מקור נתונים אחראית לעבוד רק עם מקור נתונים אחד, שיכול להיות קובץ, מקור ברשת או מסד נתונים מקומי. מחלקות של מקורות נתונים הן הגשר בין האפליקציה לבין המערכת לפעולות שקשורות לנתונים.
מידע נוסף זמין בדף בנושא שכבת הנתונים.
שכבת הדומיין
שכבת הדומיין היא שכבה אופציונלית בין שכבות ממשק המשתמש והנתונים.
שכבת הדומיין אחראית להצפנת לוגיקה עסקית מורכבת או לוגיקה עסקית פשוטה יותר שנעשה בה שימוש חוזר בכמה מודלים של תצוגה. שכבת הדומיין היא אופציונלית כי לא כל האפליקציות עומדות בדרישות האלה. משתמשים בו רק כשצריך – למשל, כדי לטפל במורכבות או כדי להעדיף שימוש חוזר.
בדרך כלל קוראים למחלקות בשכבת הדומיין תרחישי שימוש או אינטראקטורים.
כל תרחיש לדוגמה אחראי לפונקציונליות אחת. לדוגמה, יכול להיות שבאפליקציה שלכם יש מחלקה GetTimeZoneUseCase אם כמה מודלים של תצוגות מסתמכים על אזורי זמן כדי להציג את ההודעה המתאימה במסך.
מידע נוסף זמין בדף בנושא שכבת הדומיין.
ניהול יחסי תלות בין רכיבים
כדי שהכיתות באפליקציה יפעלו בצורה תקינה, הן צריכות להסתמך על כיתות אחרות. אתם יכולים להשתמש באחת מתבניות העיצוב הבאות כדי לאסוף את התלויות של מחלקה מסוימת:
- הזרקת תלות (DI): הזרקת תלות מאפשרת למחלקות להגדיר את התלויות שלהן בלי לבנות אותן. בזמן הריצה, מחלקה אחרת אחראית לספק את יחסי התלות האלה.
- איתור שירותים: התבנית של איתור שירותים מספקת מרשם שבו מחלקות יכולות לקבל את התלות שלהן במקום ליצור אותן.
הדפוסים האלה מאפשרים לכם להרחיב את הקוד, כי הם מספקים דפוסים ברורים לניהול תלות בלי לשכפל קוד או להוסיף מורכבות. התבניות מאפשרות גם לעבור במהירות בין הטמעות של בדיקות ושל ייצור.
שיטות מומלצות כלליות
תכנות הוא תחום יצירתי, ופיתוח אפליקציות ל-Android הוא לא יוצא מן הכלל. יש הרבה דרכים לפתור בעיה. למשל, אפשר להעביר נתונים בין כמה פעילויות או קטעים, לאחזר נתונים מרחוק ולשמור אותם באופן מקומי לשימוש במצב אופליין, או לטפל בכל מספר של תרחישים נפוצים אחרים שקורים באפליקציות מורכבות.
למרות שההמלצות הבאות לא מחייבות, ברוב המקרים הן עוזרות להפוך את בסיס הקוד ליציב יותר, ניתן לבדיקה וקל לתחזוקה.
אל תשמרו נתונים ברכיבי האפליקציה.
אל תגדירו את נקודות הכניסה של האפליקציה – כמו פעילויות, שירותים ומקלטי שידורים – כמקורות נתונים. נקודות הכניסה צריכות לתאם עם רכיבים אחרים כדי לאחזר רק את קבוצת המשנה של הנתונים שרלוונטית לנקודת הכניסה הזו. כל רכיב באפליקציה הוא קצר טווח, בהתאם לאינטראקציה של המשתמש עם המכשיר וליכולת של המערכת.
צמצום התלות בכיתות Android.
מוודאים שרכיבי האפליקציה הם המחלקות היחידות שמסתמכות על ממשקי API של Android framework SDK, כמו Context או Toast. הפשטה של מחלקות אחרות באפליקציה מרכיבי האפליקציה עוזרת בבדיקה ומפחיתה את הצימוד באפליקציה.
הגדירו גבולות ברורים של אחריות בין המודולים באפליקציה.
אל תפיצו את הקוד שמעמיס נתונים מהרשת על פני כמה מחלקות או חבילות בבסיס הקוד. באופן דומה, לא כדאי להגדיר באותה מחלקה כמה תחומי אחריות לא קשורים, כמו שמירת נתונים במטמון וקישור נתונים. פועלים לפי ארכיטקטורת האפליקציה המומלצת.
כדאי לחשוף כמה שפחות מכל מודול.
אל תיצרו קיצורי דרך שחושפים פרטי הטמעה פנימיים. יכול להיות שתרוויחו קצת זמן בטווח הקצר, אבל סביר להניח שתצברו חוב טכני גדול בהרבה ככל שבסיס הקוד יתפתח.
התמקדו בליבת האפליקציה הייחודית שלכם כדי שהיא תבלוט בין אפליקציות אחרות.
אל תמציאו את הגלגל מחדש ותכתבו את אותו קוד שחוזר על עצמו (boilerplate) שוב ושוב. במקום זאת, כדאי להשקיע את הזמן והאנרגיה במה שמייחד את האפליקציה שלכם. מאפשרים לספריות Jetpack ולספריות מומלצות אחרות לטפל בקוד הסטנדרטי החוזר על עצמו.
שימוש בפריסות קנוניות ובתבניות עיצוב של אפליקציות.
ספריות Jetpack פיתוח נייטיב מספקות ממשקי API חזקים ליצירת ממשקי משתמש מותאמים. כדאי להשתמש בפריסות קנוניות באפליקציה כדי לשפר את חוויית המשתמש במגוון גורמי צורה וגדלים של מסכים. כדאי לעיין בגלריה של דפוסי עיצוב אפליקציות כדי לבחור את הפריסות שהכי מתאימות לתרחישי השימוש שלכם.
שמירה של מצב ממשק המשתמש אחרי שינויים בהגדרות.
כשמעצבים פריסות מותאמות, חשוב לשמור על מצב ממשק המשתמש כשחלים שינויים בהגדרות, כמו שינוי גודל התצוגה, קיפול ושינויים בכיוון. הארכיטקטורה שלכם צריכה לוודא שהמצב הנוכחי של המשתמש נשמר, כדי לספק חוויה חלקה.
תכנון רכיבי ממשק משתמש לשימוש חוזר וניתנים להרכבה.
פיתוח רכיבי ממשק משתמש שאפשר לעשות בהם שימוש חוזר וליצור מהם קומפוזיציות, כדי לתמוך בעיצוב אדפטיבי. כך אפשר לשלב ולסדר מחדש רכיבים כדי להתאים אותם לגדלים שונים של מסכים ולכיוונים שונים של המכשיר בלי לבצע ארגון הקוד מחדש משמעותי.
חשבו איך לבדוק כל חלק באפליקציה בנפרד.
ממשק API מוגדר היטב לאחזור נתונים מהרשת מאפשר לבדוק את המודול ששומר את הנתונים האלה במסד נתונים מקומי. לעומת זאת, אם תערבבו את הלוגיקה של שתי הפונקציות האלה במקום אחד, או אם תפיצו את קוד הרשת על פני כל בסיס הקוד, הבדיקה תהיה הרבה יותר קשה, ואולי אפילו בלתי אפשרית.
סוגים אחראים למדיניות המקבילות שלהם.
אם סוג מבצע עבודה חוסמת ממושכת, הסוג צריך להיות אחראי להעברת החישוב הזה לשרשור הנכון. הסוג יודע איזה חישוב הוא מבצע ובאיזה שרשור להריץ את החישוב. הסוגים צריכים להיות main‑safe, כלומר בטוח להפעיל אותם מה-thread הראשי בלי לחסום אותו.
חשוב לשמור כמה שיותר נתונים רלוונטיים ועדכניים.
כך המשתמשים יכולים ליהנות מהפונקציונליות של האפליקציה גם כשהמכשיר שלהם במצב אופליין. חשוב לזכור שלא לכל המשתמשים יש חיבור מהיר וקבוע, וגם אם יש להם, יכול להיות שהקליטה שלהם תהיה חלשה במקומות הומי אדם.
יתרונות הארכיטקטורה
יישום ארכיטקטורה טובה באפליקציה מביא הרבה יתרונות לפרויקט ולצוותי ההנדסה:
- משפר את יכולת התחזוקה, האיכות והיציבות של האפליקציה הכוללת.
- מאפשרת לאפליקציה לשנות את הגודל שלה. יותר אנשים ויותר צוותים יכולים לתרום לאותו בסיס קוד עם מינימום התנגשויות קוד.
- עזרה בהצטרפות. הארכיטקטורה מספקת עקביות לפרויקט, כך שחברים חדשים בצוות יכולים להיכנס לעניינים במהירות ולהיות יעילים יותר תוך זמן קצר.
- קל יותר לבדוק. ארכיטקטורה טובה מעודדת שימוש בסוגים פשוטים יותר, שבדרך כלל קל יותר לבדוק אותם.
- מאפשר לכם לחקור באגים באופן שיטתי באמצעות תהליכים מוגדרים היטב.
ארכיטקטורה טובה דורשת השקעה מראש של זמן, אבל יש לה גם השפעה ישירה על המשתמשים. הם נהנים מאפליקציה יציבה יותר ומפיצ'רים נוספים בזכות צוות הנדסה פרודוקטיבי יותר.
דוגמיות
בדוגמאות הבאות אפשר לראות ארכיטקטורת אפליקציה טובה: