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

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

חוויות משתמש באפליקציות לנייד

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

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

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

הגבלות על משאבים

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

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

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

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

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

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

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

הפרדה בין תחומים

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

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

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

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

הפעלת ממשק המשתמש של Drive ממודלים של נתונים

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

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

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

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

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

מקור יחיד למידע מהימן

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

שכבת ממשק המשתמש מורכבת משני דברים:

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

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

שכבת נתונים

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

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

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

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

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

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

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

שכבת הדומיין

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

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

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

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

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

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

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

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

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

שיטות מומלצות כלליות

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

ההמלצות הבאות לא מחייבות, אבל ברוב המקרים הן עוזרות להפוך את בסיס הקוד לחזק יותר, קל יותר לבדיקה ולתחזוקה בטווח הארוך:

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

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

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

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

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

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

צריך לחשוף כמה שפחות מכל מודול.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

טעימות

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