ארכיטקטורת האפליקציה היא הבסיס לאפליקציית 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 אם כמה מודלים של תצוגות מסתמכים על אזורי זמן כדי להציג את ההודעה המתאימה במסך.
מידע נוסף זמין בדף בנושא שכבת הדומיין.
ניהול יחסי תלות בין רכיבים
כדי שהכיתות באפליקציה יפעלו בצורה תקינה, הן תלויות בכיתות אחרות. אפשר להשתמש באחד מדפוסי העיצוב הבאים כדי לאסוף את התלויות של מחלקה מסוימת:
- הזרקת תלות (DI): הזרקת תלות מאפשרת למחלקות להגדיר את התלויות שלהן בלי לבנות אותן. בזמן הריצה, מחלקה אחרת אחראית לספק את יחסי התלות האלה.
- איתור שירותים: התבנית של איתור שירותים מספקת מרשם שבו מחלקות יכולות לקבל את התלות שלהן במקום ליצור אותן.
הדפוסים האלה מאפשרים לכם להרחיב את הקוד, כי הם מספקים דפוסים ברורים לניהול תלות בלי לשכפל קוד או להוסיף מורכבות. התבניות גם מאפשרות לכם לעבור במהירות בין הטמעות של בדיקות ושל ייצור.
שיטות מומלצות כלליות
תכנות הוא תחום יצירתי, ופיתוח אפליקציות ל-Android הוא לא יוצא דופן. יש הרבה דרכים לפתור בעיה. למשל, אפשר להעביר נתונים בין כמה פעילויות או קטעים, לאחזר נתונים מרחוק ולשמור אותם באופן מקומי לשימוש במצב אופליין, או לטפל בכל מספר של תרחישים נפוצים אחרים שקורים באפליקציות מורכבות.
ההמלצות הבאות לא מחייבות, אבל ברוב המקרים הן עוזרות ליצור בסיס קוד חזק יותר, שקל יותר לבדוק ולתחזק.
אל תשמרו נתונים ברכיבי האפליקציה.
אל תגדירו את נקודות הכניסה של האפליקציה – כמו פעילויות, שירותים ומקלטי שידורים – כמקורות נתונים. נקודות הכניסה צריכות לתאם רק עם רכיבים אחרים כדי לאחזר את קבוצת המשנה של הנתונים שרלוונטית לנקודת הכניסה הזו. כל רכיב באפליקציה הוא קצר מועד, בהתאם לאינטראקציה של המשתמש עם המכשיר וליכולת של המערכת.
צמצום יחסי התלות במחלקות Android.
רכיבי האפליקציה צריכים להיות המחלקות היחידות שמסתמכות על ממשקי API של Android framework SDK, כמו Context או Toast. הפשטה של מחלקות אחרות באפליקציה מרכיבי האפליקציה עוזרת בבדיקה ומפחיתה את הצימוד באפליקציה.
הגדרת גבולות ברורים של אחריות בין המודולים באפליקציה.
אל תפיצו את הקוד שמעמיס נתונים מהרשת על פני כמה מחלקות או חבילות בבסיס הקוד. באופן דומה, לא כדאי להגדיר באותה מחלקה כמה תחומי אחריות לא קשורים, כמו שמירת נתונים במטמון וקישור נתונים. כדאי לפעול לפי ארכיטקטורת האפליקציה המומלצת.
צריך לחשוף כמה שפחות מכל מודול.
לא ליצור קיצורי דרך שחושפים פרטי הטמעה פנימיים. יכול להיות שתרוויחו קצת זמן בטווח הקצר, אבל סביר להניח שתצברו חוב טכני גדול בהרבה ככל שבסיס הקוד יתפתח.
התמקדו בליבת האפליקציה הייחודית שלכם כדי שהיא תבלוט בין אפליקציות אחרות.
אל תמציאו את הגלגל מחדש ותכתבו שוב ושוב את אותו קוד סטנדרטי. במקום זאת, כדאי להשקיע את הזמן והאנרגיה במה שמייחד את האפליקציה שלכם. מאפשרים לספריות Jetpack ולספריות מומלצות אחרות לטפל בקוד חוזר.
שימוש בפריסות קנוניות ובתבניות עיצוב של אפליקציות.
ספריות Jetpack Compose מספקות ממשקי API חזקים לבניית ממשקי משתמש מותאמים. כדאי להשתמש בפריסות קנוניות באפליקציה כדי לשפר את חוויית המשתמש במגוון גורמי צורה וגדלים של מסכים. כדאי לעיין בגלריה של דפוסי עיצוב אפליקציות כדי לבחור את הפריסות שהכי מתאימות לתרחישי השימוש שלכם.
שמירה של מצב ממשק המשתמש אחרי שינויים בהגדרות.
כשמעצבים פריסות מותאמות, חשוב לשמור על מצב ממשק המשתמש כשחלים שינויים בהגדרות, כמו שינוי גודל התצוגה, קיפול ושינויים בכיוון. הארכיטקטורה צריכה לוודא שהמצב הנוכחי של המשתמש נשמר, כדי לספק חוויה חלקה.
תכנון רכיבי ממשק משתמש קומפוזביליים לשימוש חוזר.
פיתוח רכיבי ממשק משתמש שאפשר לעשות בהם שימוש חוזר ולשלב אותם כדי לתמוך בעיצוב אדפטיבי. כך תוכלו לשלב ולסדר מחדש רכיבים כדי להתאים אותם לגדלים שונים של מסכים ולכיוונים שונים של המכשיר, בלי שתצטרכו לבצע שינויים משמעותיים בקוד.
חשבו איך לבדוק כל חלק באפליקציה בנפרד.
ממשק API מוגדר היטב לאחזור נתונים מהרשת מאפשר לבדוק את המודול ששומר את הנתונים האלה במסד נתונים מקומי. לעומת זאת, אם תערבבו את הלוגיקה של שתי הפונקציות האלה במקום אחד, או אם תפיצו את קוד הרשת על פני כל בסיס הקוד, יהיה הרבה יותר קשה לבצע בדיקות, ואולי אפילו בלתי אפשרי.
סוגים אחראים למדיניות המקבילות שלהם.
אם סוג מבצע עבודה חוסמת ממושכת, הסוג צריך להיות אחראי להעברת החישוב הזה לשרשור הנכון. הסוג יודע איזה חישוב הוא מבצע ובאיזה שרשור החישוב צריך להתבצע. הסוגים צריכים להיות main-safe, כלומר בטוח להפעיל אותם מה-thread הראשי בלי לחסום אותו.
חשוב לשמור כמה שיותר נתונים רלוונטיים ועדכניים.
כך המשתמשים יכולים ליהנות מהפונקציונליות של האפליקציה גם כשהמכשיר שלהם במצב אופליין. חשוב לזכור שלא לכל המשתמשים יש חיבור מהיר וקבוע, וגם אם יש להם, יכול להיות שהקליטה שלהם לא טובה במקומות הומי אדם.
יתרונות הארכיטקטורה
הטמעה של ארכיטקטורה טובה באפליקציה מביאה הרבה יתרונות לפרויקט ולצוותי ההנדסה:
- משפר את יכולת התחזוקה, האיכות והיציבות של האפליקציה הכוללת.
- מאפשר לאפליקציה לשנות את הגודל. יותר אנשים ויותר צוותים יכולים לתרום לאותו בסיס קוד עם מינימום התנגשויות קוד.
- עזרה בתהליך ההצטרפות. הארכיטקטורה מספקת עקביות לפרויקט, כך שחברים חדשים בצוות יכולים להיכנס לעניינים במהירות ולהיות יעילים יותר תוך זמן קצר.
- קל יותר לבצע בדיקות. ארכיטקטורה טובה מעודדת שימוש בסוגים פשוטים יותר, שבדרך כלל קל יותר לבדוק אותם.
- אפשר לחקור באגים באופן שיטתי באמצעות תהליכים מוגדרים היטב.
השקעה בארכיטקטורה משפיעה גם באופן ישיר על המשתמשים. הם נהנים מאפליקציה יציבה יותר ומפיצ'רים נוספים בזכות צוות הנדסה פרודוקטיבי יותר. עם זאת, הארכיטקטורה דורשת גם השקעה מוקדמת של זמן. כדי לעזור לכם להסביר לארגון למה כדאי להשקיע את הזמן הזה, כדאי לעיין במקרים לדוגמה שבהם חברות אחרות משתפות את סיפורי ההצלחה שלהן בנוגע לארכיטקטורה טובה באפליקציה שלהן.
דוגמיות
בדוגמאות הבאות אפשר לראות ארכיטקטורת אפליקציה טובה: