יצירת ארכיטקטורה של ממשק המשתמש לכתיבה

אי אפשר לשנות את ממשק המשתמש בקטע 'פיתוח' – אי אפשר לעדכן אותו אחרי שהוא כבר וצייר. מה שאפשר לשלוט בו הוא מצב ממשק המשתמש. בכל פעם שמצב השינויים בממשק המשתמש, 'כתיבה' יוצר מחדש את החלקים של עץ ממשק המשתמש אשר שונה. תכנים קומפוזביליים יכולים לקבל במצב ולחשוף אירועים. לדוגמה, TextField מקבל ערך וחושף קריאה חוזרת (callback) onValueChange שמבקשת את ה-handler של הקריאה החוזרת לשנות את עם ערך מסוים.

var name by remember { mutableStateOf("") }
OutlinedTextField(
    value = name,
    onValueChange = { name = it },
    label = { Text("Name") }
)

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

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

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

לולאת העדכון של ממשק המשתמש של אפליקציה שמשתמשת בזרם נתונים חד-כיווני נראית כך:

  • אירוע: חלק מממשק המשתמש יוצר אירוע ומעביר אותו למעלה, למשל הלחיצה על הלחצן מועברת אל ViewModel לצורך טיפול; או שאירוע מועבר או שכבות אחרות של האפליקציה, כמו ציון שהסשן של המשתמש פג תוקף.
  • עדכון מצב: הגורם המטפל באירועים עשוי לשנות את המצב.
  • Display state (מצב התצוגה): בעל המדינה מעביר את המדינה (State), וממשק המשתמש מוצג את זה.

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

יש כמה יתרונות לשימוש ב-Jetpack פיתוח נייטיב בהתאם לדפוס הזה:

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

תהליך חד-כיווני של נתונים ב-Jetpack פיתוח נייטיב

תכנים קומפוזביליים פועלים על סמך מצב ואירועים. לדוגמה, TextField הוא רק מתעדכן כשפרמטר value מתעדכן והוא חושף onValueChange קריאה חוזרת (callback) — אירוע שמבקש לשנות את הערך לערך חדש. אימייל חדש מגדיר את האובייקט State כמחזיק ערך, ומשנה את ערך המצב (State) יובילו להרכבת מחדש. אפשר לשמור את המדינה remember { mutableStateOf(value) } או rememberSaveable { mutableStateOf(value), בהתאם למשך הזמן הרצוי תזכור את הערך של

סוג הערך של התוכן הקומפוזבילי TextField הוא String, לכן יכול להיות מכל מקום - מערך שרשום בתוך הקוד, מ-ViewModel או שמועבר תוכן קומפוזבילי הורה. לא חובה להחזיק אותו באובייקט State, אבל צריך כדי לעדכן את הערך כשמתבצעת קריאה אל onValueChange.

הגדרת פרמטרים קומפוזביליים

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

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

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

@Composable
fun Header(title: String, subtitle: String) {
    // Recomposes when title or subtitle have changed.
}

@Composable
fun Header(news: News) {
    // Recomposes when a new instance of News is passed in.
}

לפעמים, השימוש בפרמטרים בודדים גם משפר את הביצועים. News מכיל יותר מידע מאשר title ו-subtitle, בכל פעם המופע החדש של News מועבר אל Header(news), התוכן הקומפוזבילי לכתוב מחדש, גם אם title ו-subtitle לא השתנו.

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

אירועים בכתיבה

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

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

עדיף להעביר ערכים לא ניתנים לשינוי ל-lambdas של מצב ומטפל באירועים. הזה יש לה את היתרונות הבאים:

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

לדוגמה, תוכן קומפוזבילי שמקבל String ו-lambda בתור פרמטרים לקבל קריאה מהקשרים רבים, והוא ניתן לשימוש חוזר. נניח שהאפליקציה המובילה בסרגל באפליקציה תמיד מציג טקסט ויש בו לחצן 'הקודם'. אפשר להגדיר תוכן קומפוזבילי גנרי יותר של MyAppTopAppBar שמקבל את הטקסט ומחזיר את הטקסט הכינוי של הלחצן:

@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
    TopAppBar(
        title = {
            Text(
                text = topAppBarText,
                textAlign = TextAlign.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .wrapContentSize(Alignment.Center)
            )
        },
        navigationIcon = {
            IconButton(onClick = onBackPressed) {
                Icon(
                    Icons.Filled.ArrowBack,
                    contentDescription = localizedString
                )
            }
        },
        // ...
    )
}

הצגת מודלים, מצבים ואירועים: דוגמה

בעזרת ViewModel ו-mutableStateOf, אפשר גם להציג נתונים חד-כיווניים באפליקציה שלך אם מתקיים אחד מהתנאים הבאים:

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

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

במסך יש ארבעה מצבים:

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

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

class MyViewModel : ViewModel() {
    private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
    val uiState: State<UiState>
        get() = _uiState

    // ...
}

בנוסף ל-API של mutableStateOf, 'כתיבה' מאפשרת תוספים עבור LiveData, Flow, ו- Observable כדי להירשם כמאזינים ולייצג את הערך כמצב.

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
    val uiState: LiveData<UiState>
        get() = _uiState

    // ...
}

@Composable
fun MyComposable(viewModel: MyViewModel) {
    val uiState = viewModel.uiState.observeAsState()
    // ...
}

מידע נוסף

מידע נוסף על ארכיטקטורה ב-Jetpack פיתוח נייטיב זמין במקורות המידע הבאים:

דוגמיות