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