להשתמש ביישומי הפלאגין בספרייה של ממשק המשתמש ברכב כדי ליצור הטמעות מלאות של הרכיב התאמות אישיות בספריית ממשק המשתמש של הרכב במקום להשתמש בשכבות-על של משאבים בזמן ריצה (RRO). מסמכי RRO מאפשרים לשנות רק את משאבי ה-XML של ספריית ממשק המשתמש של הרכב ולכן מגביל את היקף ההתאמה האישית של נתונים.
יצירת פלאגין
פלאגין של ספריית ממשק המשתמש ברכב הוא APK שמכיל מחלקות שמטמיעות קבוצה של ממשקי API של יישומי פלאגין. אפשר להדר את ממשקי ה-API של הפלאגין בתור ספרייה סטטית.
ראו דוגמאות ב-Song וב-Gradle:
סונג
נבחן את הדוגמה הזו של סונג:
android_app {
name: "my-plugin",
min_sdk_version: "28",
target_sdk_version: "30",
aaptflags: ["--shared-lib"],
sdk_version: "current",
manifest: "src/main/AndroidManifest.xml",
srcs: ["src/main/java/**/*.java"],
resource_dirs: ["src/main/res"],
static_libs: [
"car-ui-lib-oem-apis",
],
// Disable optimization is mandatory to prevent R.java class from being
// stripped out
optimize: {
enabled: false,
},
certificate: ":my-plugin-certificate",
}
גרדל
הצגת קובץ build.gradle
הזה:
apply plugin: 'com.android.application'
android {
compileSdkVersion 30
defaultConfig {
minSdkVersion 28
targetSdkVersion 30
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
signingConfigs {
debug {
storeFile file('chassis_upload_key.jks')
storePassword 'chassis'
keyAlias 'chassis'
keyPassword 'chassis'
}
}
}
dependencies {
implementation project(':oem-apis')
// Or use the following if you'd like to use the maven artifact
// implementation 'com.android.car.ui:car-ui-lib-plugin-apis:1.0.0'
}
Settings.gradle
:
// You can remove the ':oem-apis' if you're using the maven artifact.
include ':oem-apis'
project(':oem-apis').projectDir = new File('./path/to/oem-apis')
include ':my-plugin'
project(':my-plugin').projectDir = new File('./my-plugin')
במניפסט של הפלאגין חייב להיות הצהרה על ספק תוכן שכולל את המאפיינים הבאים:
android:authorities="com.android.car.ui.plugin"
android:enabled="true"
android:exported="true"
android:authorities="com.android.car.ui.plugin"
הופך את הפלאגין לגלוי
לספריית ממשק המשתמש של הרכב. צריך לייצא את הספק כדי שתהיה אפשרות לשלוח שאילתות לגביו
בסביבת זמן ריצה. כמו כן, אם המאפיין enabled
מוגדר כ-false
, ברירת המחדל
ייעשה שימוש בהטמעה של הפלאגין, במקום בהטמעה של הפלאגין. התוכן
מחלקת הספק לא חייבת להיות קיימת. במקרה כזה, הקפידו להוסיף
tools:ignore="MissingClass"
להגדרת הספק. לצפייה בדוגמה
ערך המניפסט הבא:
<application>
<provider
android:name="com.android.car.ui.plugin.PluginNameProvider"
android:authorities="com.android.car.ui.plugin"
android:enabled="false"
android:exported="true"
tools:ignore="MissingClass"/>
</application>
לבסוף, כאמצעי אבטחה, לחתום על האפליקציה.
יישומי פלאגין כספרייה משותפת
שלא כמו ספריות סטטיות של Android שמשולבות ישירות באפליקציות, הספריות המשותפות ל-Android מורכבות מחבילת APK עצמאית שיש הפניה אליה על ידי אפליקציות אחרות בזמן הריצה.
ליישומי פלאגין שמוטמעים כספרייה משותפת ב-Android יש את הכיתות שלהם נוסף באופן אוטומטי ל-classload המשותף בין אפליקציות. כאשר אפליקציה משתמש בספרייה של ממשק המשתמש ברכב מציין תלות זמן ריצה בספרייה המשותפת של יישומי הפלאגין, classloader יכול לגשת לכיתות של הספרייה המשותפת של הפלאגין. יישומי פלאגין הוטמעו כי אפליקציות רגילות ל-Android (לא ספרייה משותפת) עלולות להשפיע לרעה על הקור של האפליקציה בשעות ההתחלה.
הטמעה ויצירה של ספריות משותפות
הפיתוח באמצעות ספריות משותפות ב-Android דומה מאוד לפיתוח ב-Android רגיל יש כמה הבדלים עיקריים.
- משתמשים בתג
library
מתחת לתגapplication
עם חבילת יישומי הפלאגין בקובץ המניפסט של האפליקציה:
<application>
<library android:name="com.chassis.car.ui.plugin" />
...
</application>
- צריך להגדיר את כלל ה-build של
android_app
ב-Sung (Android.bp
) באמצעות AAPT דגלshared-lib
, המשמש לבניית ספרייה משותפת:
android_app {
...
aaptflags: ["--shared-lib"],
...
}
יחסי תלות בספריות משותפות
עבור כל אפליקציה במערכת שמשתמשת בספריית ממשק המשתמש של הרכב, צריך לכלול את הקוד
את התג uses-library
בקובץ המניפסט של האפליקציה מתחת לקטע
תג application
עם שם החבילה של הפלאגין:
<manifest>
<application
android:name=".MyApp"
...>
<uses-library android:name="com.chassis.car.ui.plugin" android:required="false"/>
...
</application>
</manifest>
התקנת פלאגין
חובה להתקין מראש יישומי פלאגין במחיצת המערכת על ידי הכללת המודול
ב-PRODUCT_PACKAGES
. אפשר לעדכן את החבילה שהותקנה מראש באופן דומה
אפליקציה מותקנת אחרת.
אם אתם מעדכנים פלאגין קיים במערכת, כל האפליקציות שמשתמשות בפלאגין הזה ייסגרו אוטומטית. אחרי שהמשתמש פותח מחדש, הוא יכול לבצע את השינויים המעודכנים. אם האפליקציה לא פעלה, בפעם הבאה שהיא תתחיל היא תעודכן יישומי פלאגין.
כשמתקינים פלאגין עם Android Studio, יש עוד כמה שצריך להביא בחשבון. בזמן הכתיבה, יש באג תהליך ההתקנה של אפליקציית Android Studio שגורם לעדכונים בפלאגין. לא נכנס לתוקף. כדי לפתור את הבעיה, אפשר לבחור באפשרות אני רוצה להתקין תמיד עם מנהל חבילות (השבתת האופטימיזציות של הפריסה ב-Android 11 ואילך) בהגדרות ה-build של הפלאגין.
בנוסף, כשמתקינים את הפלאגין, Android Studio מדווח על שגיאה לא מצאת פעילות ראשית להפעלה. במצב כזה, הפלאגין לא כולל פעילויות כלשהן (למעט הכוונה הריקה שמשמשת לפתרון כוונה). שפת תרגום ביטול השגיאה, משנים את האפשרות Launch (הפעלה) ל-Nothing (שום דבר) ב-build. הגדרה אישית.
איור 1. הגדרות אישיות לפלאגין של Android Studio
פלאגין של שרת Proxy
התאמה אישית של אפליקציות שמשתמשות בספריית ממשק המשתמש של הרכב דורש RRO שמטרגט כל אפליקציה ספציפית שצריך לשנות, כולל מקרים שבהם ההתאמות האישיות זהות בכל האפליקציות. כלומר, ב-RRO נדרשת אפליקציה. הצגת האפליקציות שמשתמשות בספרייה של ממשק המשתמש ברכב.
דוגמה: הפלאגין של שרת ה-Proxy לספריית ממשק המשתמש של המכונית של הפלאגין המשותף של ספריית ממשק המשתמש ברכב. ניתן לטרגט את הפלאגין הזה באמצעות RRO, שניתן משמש כנקודת התאמה אחת בלבד של אפליקציות שמשתמשות בספריית ממשק המשתמש של הרכב. ללא צורך בהטמעה של פלאגין תקין. מידע נוסף על RROs, ניתן לעיין במאמר שינוי הערך של משאבים של אפליקציה בכתובת סביבת זמן ריצה.
הפלאגין של שרת ה-proxy הוא רק דוגמה ונקודת התחלה לביצוע התאמה אישית באמצעות פלאגין. כדי לבצע התאמה אישית מעבר לקובצי RRO, אפשר להטמיע קבוצת משנה של יישומי פלאגין ולהשתמש בפלאגין של שרת proxy לכל השאר, או להטמיע את כל יישומי הפלאגין מההתחלה ועד הסוף.
אמנם הפלאגין של שרת ה-proxy מספק נקודה אחת של התאמה אישית של RRO לאפליקציות, אבל עבור אפליקציות שביטלו את השימוש בפלאגין עדיין יידרשו RRO שיש מטרגט את האפליקציה עצמה.
הטמעת ממשקי ה-API של יישומי הפלאגין
נקודת הכניסה העיקרית לפלאגין היא
כיתה אחת (com.android.car.ui.plugin.PluginVersionProviderImpl
). כל יישומי הפלאגין חייבים
כוללים מחלקה עם השם ושם החבילה המדויקים האלה. לכיתה הזו צריך להיות
ב-buildor שמוגדר כברירת מחדל, ולהטמיע את הממשק של PluginVersionProviderOEMV1
.
יישומי הפלאגין של CarUi חייבים לפעול עם אפליקציות ישנות או חדשות יותר מהפלאגין. שפת תרגום
לצורך כך, כל ממשקי ה-API של יישומי הפלאגין מדורגים עם V#
בסוף
שם הכיתה. אם תפורסם גרסה חדשה של ספריית ממשק המשתמש של הרכב עם תכונות חדשות,
הם חלק מגרסת V2
של הרכיב. ספריית ממשק המשתמש של הרכב עושה
הכי טוב לגרום לתכונות חדשות לפעול במסגרת רכיב ישן של פלאגין.
לדוגמה, אפשר להמיר סוג חדש של לחצן בסרגל הכלים לMenuItems
.
עם זאת, אפליקציה עם גרסה ישנה יותר של ספריית ממשק המשתמש של הרכב לא יכולה להסתגל לגרסה חדשה של הפלאגין שנכתב מול ממשקי API חדשים יותר. כדי לפתור את הבעיה הזו, אנחנו מאפשרים ליישומי פלאגין להחזיר יישומים שונים של עצמם בהתאם לגרסת ממשק ה-API של ה-OEM. שנתמכות על ידי האפליקציות.
ב-PluginVersionProviderOEMV1
יש שיטה אחת:
Object getPluginFactory(int maxVersion, Context context, String packageName);
השיטה הזו מחזירה אובייקט שמיישם את הגרסה הגבוהה ביותר של
הפלאגין PluginFactoryOEMV#
נתמך, אבל הוא עדיין נמוך מ- או
שווה ל-maxVersion
. אם פלאגין לא כולל הטמעה של
ב-PluginFactory
הישן, יכול להיות שהוא יחזיר null
, ובמקרה כזה באופן סטטי-
נעשה שימוש בהטמעה מקושרת של רכיבי CarUi.
כדי לשמור על תאימות לאחור עם אפליקציות שנאספו מול
בגרסאות הישנות של ספריית ה-Ui הסטטית של הרכב, מומלץ לתמוך.
maxVersion
מתוך 2, 5 ומעלה מתוך ההטמעה של הפלאגין
הכיתה PluginVersionProvider
. גרסאות 1, 3 ו-4 לא נתמכות. עבור
מידע נוסף:
PluginVersionProviderImpl
PluginFactory
הוא הממשק שיוצר את כל שאר ה-CarUi
רכיבים. הוא גם מגדיר באיזו גרסה של הממשקים שלהם להשתמש. אם המיקום
הפלאגין לא מנסה להטמיע אף אחד מהרכיבים האלו, הוא עשוי להחזיר
null
בפונקציית היצירה שלהם (למעט סרגל הכלים, שכולל
פונקציית customizesBaseLayout()
נפרדת).
ב-pluginFactory
יש הגבלה על הגרסאות של רכיבי CarUi שבהם אפשר להשתמש
את כל החלקים. לדוגמה, אף פעם לא יהיה pluginFactory
שיכול ליצור
את גרסה 100 של Toolbar
וגם את גרסה 1 של RecyclerView
,
יש מעט מאוד ערובה לכך שמגוון רחב של גרסאות של רכיבים
פועלים יחד. כדי להשתמש בגרסה 100 של סרגל הכלים, אנחנו מצפים ממפתחים
לספק יישום של גרסה של pluginFactory
שיוצרת
בסרגל הכלים גרסה 100, שמגביל את האפשרויות בגרסאות של
שאפשר ליצור. ייתכן שהגרסאות של רכיבים אחרים לא
שווה ל-, לדוגמה pluginFactoryOEMV100
יכול ליצור
ToolbarControllerOEMV100
וגם RecyclerViewOEMV70
.
סרגל כלים
פריסת בסיס
סרגל הכלים וה"פריסה הבסיסית" קשורים מאוד,
שיצר את סרגל הכלים נקרא installBaseLayoutAround
.
פריסת בסיס
היא מושג שמאפשר למקם את סרגל הכלים בכל מקום סביב האפליקציה
תוכן, כדי לאפשר סרגל כלים לאורך החלק העליון/התחתון של האפליקציה, באופן אנכי
לאורך הצדדים, או אפילו סרגל כלים עגול שמקיף את כל האפליקציה. הדבר
ניתן לעשות זאת על ידי העברת תצוגה אל installBaseLayoutAround
של סרגל הכלים או הבסיס
פריסה מסביב.
הפלאגין צריך לקחת את התצוגה שסופקה, לנתק אותה מהורה, לניפוח
הפריסה של הפלאגין עצמו באותו אינדקס של ההורה ועם אותה
LayoutParams
בתור התצוגה המפורטת שהתנתקה, ואז מחברים מחדש את התצוגה
במקום כלשהו בתוך הפריסה שפשוט התנפחה. הפריסה המנופחת
להכיל את סרגל הכלים, אם האפליקציה ביקשה זאת.
האפליקציה יכולה לבקש פריסה בסיסית בלי סרגל כלים. אם כן,
הערך של installBaseLayoutAround
צריך להחזיר את הערך null. זה כל מה שצריך לעשות לרוב יישומי הפלאגין
צריך לקרות, אבל אם מחבר הפלאגין ירצה להשתמש בהטמעה. למשל, קישוט
מסביב לקצה האפליקציה, שאפשר עדיין לבצע באמצעות פריסה בסיסית. האלה
קישוטים שימושיים במיוחד למכשירים עם מסכים לא מלבניים, כמו
הם יכולים לדחוף את האפליקציה לשטח מלבני ולהוסיף מעברים נקיים אל
במקום המלבני.
גם installBaseLayoutAround
מועבר Consumer<InsetsOEMV1>
. הזה
יכול לשמש את הצרכן כדי להודיע לאפליקציה שהפלאגין
שמכסה את תוכן האפליקציה (בסרגל הכלים או בדרך אחרת). האפליקציה
ואז לדעת להמשיך לשרטט במרחב הזה, אבל לשמור על יכולת
את הרכיבים שלו. האפקט הזה משמש בעיצוב העזר שלנו כדי להפוך את
סרגל כלים חצי שקוף למחצה, ורשימות נגללות מתחתיו. אם התכונה
לא הוטמע, הפריט הראשון ברשימה ייתקע מתחת לסרגל הכלים.
ולא קליקביליות. אם אין צורך באפקט הזה, הפלאגין יכול להתעלם
צרכן.
איור 2. גלילה בתוכן מתחת לסרגל הכלים
מבחינת האפליקציה, כשהפלאגין שולח רכיבי inset חדשים, הוא יקבל
אותם מפעילויות או מקטעים שמטמיעים את InsetsChangedListener
. אם המיקום
פעילות או מקטע לא מטמיעים את InsetsChangedListener
, ממשק המשתמש של הרכב
הספרייה תטפל בכניסות פנימיות כברירת מחדל על ידי החלת הרכיבים הפנימיים כמרווח פנימי
Activity
או FragmentActivity
שמכילים את המקטע. הספרייה לא
להחיל את inset כברירת מחדל על מקטעים. הנה קטע קוד לדוגמה
שמחילה את הרכיבים הפנימיים בתור מרווח פנימי ב-RecyclerView
app:
public class MainActivity extends Activity implements InsetsChangedListener {
@Override
public void onCarUiInsetsChanged(Insets insets) {
CarUiRecyclerView rv = requireViewById(R.id.recyclerview);
rv.setPadding(insets.getLeft(), insets.getTop(),
insets.getRight(), insets.getBottom());
}
}
לסיום, הפלאגין מקבל רמז fullscreen
, שמשמש כדי לציין
התצוגה שאמורה למכסה תופסת את כל האפליקציה או רק חלק קטן.
אפשר להשתמש בשיטה הזו כדי למנוע החלה של קישוטים לאורך הקצה
הגיוני רק אם הן מופיעות לאורך קצה המסך כולו. דוגמה
באפליקציה שמשתמשת בפריסות בסיסיות שלא מתאימות למסך מלא היא 'הגדרות', שבה כל חלונית
לפריסה של שתי חלוניות יש סרגל כלים משלה.
מכיוון ש-installBaseLayoutAround
צפוי להחזיר את הערך null כאשר
toolbarEnabled
הוא false
, כדי שהפלאגין יציין שהוא לא
כשרוצים להתאים אישית את הפריסה הבסיסית, צריך להחזיר false
customizesBaseLayout
הפריסה הבסיסית חייבת להכיל FocusParkingView
ו-FocusArea
כדי לקבל אותה במלואה
תמיכה בחוגה לפקדים. ניתן להשמיט את הצפיות האלה במכשירים
לא תומכים בחוגה. הערכים של FocusParkingView/FocusAreas
מוטמעים
ספריית CarUi הסטטית, כך ש-setRotaryFactories
משמש לאספקת מפעלים
יוצר את התצוגות מהקשרים.
ההקשרים שמשמשים ליצירת תצוגות מיקוד חייבים להיות בהקשר של המקור, ולא
של הפלאגין. הערך FocusParkingView
צריך להיות המיקום הקרוב ביותר לתצוגה הראשונה
בעץ, באופן סביר ככל האפשר, כי זה מה שמתמקד
שהמשתמש לא יוכל לראות אותן. השדה FocusArea
חייב להקיף את סרגל הכלים
כדי לציין שמדובר בתחום של נדנוד סיבובי. אם FocusArea
לא
שצוין, המשתמש לא יכול לנווט ללחצנים בסרגל הכלים עם
חוגה.
בקר סרגל הכלים
הערך של 'ToolbarController
' שהוחזר בפועל צריך להיות הרבה יותר פשוט
מהפריסה הבסיסית. התפקיד שלו הוא לקחת את המידע שמועבר
ולהגדיר אותו בפריסה הבסיסית. ב-Javadoc יש מידע על
את רוב השיטות. בהמשך מתוארות כמה מהשיטות המורכבות יותר.
getImeSearchInterface
משמש להצגת תוצאות חיפוש ב-IME (מקלדת)
חלון. הדבר יכול להיות שימושי להצגה או להנפשה של תוצאות חיפוש לצד
במקלדת, לדוגמה אם המקלדת תפסה רק חצי מהמסך. רוב
את הפונקציונליות מוטמעת בספריית CarUi הסטטית,
הממשק בפלאגין פשוט מספק שיטות לספרייה הסטטית
TextView
ו-onPrivateIMECommand
התקשרות חזרה. כדי לתמוך בכך, הפלאגין
צריך להשתמש במחלקה משנית מסוג TextView
שמחליפה את onPrivateIMECommand
ועוברת את הבדיקה
את הקריאה ל-listener שסופק כ-TextView
של סרגל החיפוש שלו.
ב-setMenuItems
מוצגים פריטים בתפריט, אבל הם ייקראו
באופן מפתיע. מכיוון שה-API של הפלאגין עבור ListItems אינו ניתן לשינוי, בכל פעם
הפריט בתפריט השתנה. תתבצע הפעלה חדשה של setMenuItems
. הפעולה הזו יכולה
מתרחשת במקרה טריוויאלי כאשר משתמש לחץ על פריט בתפריט מתג, והפעולה הזאת
הקליק גרם למעבר למצב מופעל. מסיבות שקשורות לביצועים וגם לאנימציה,
לכן מומלץ לחשב את ההבדל בין
רשימת פריטים בתפריט, ורק את התצוגות שהשתנו בפועל. הפריטים בתפריט
צריך לספק שדה key
שיכול לעזור בעניין הזה, כי המפתח צריך להיות זהה.
בקריאות שונות ל-setMenuItems
עבור אותו פריט בתפריט.
AppStyledView
AppStyledView
הוא מאגר של תצוגה שלא מותאמת אישית בכלל. הוא
יכול לשמש לציון גבול מסביב לתצוגה, שמבליט אותה
את שאר האפליקציות, ולציין למשתמש שמדובר
גרפי. התצוגה שגולמת על ידי AppStyledView ניתנת
setContent
אפשר גם להוסיף לחצן 'הקודם' או 'סגירה' בAppStyledView
שנדרשה על ידי האפליקציה.
השדה AppStyledView
לא יוסיף מיד את התצוגות המפורטות שלו בהיררכיית התצוגות
כמו installBaseLayoutAround
, אלא פשוט מחזיר את התצוגה
הספרייה הסטטית דרך getView
, ואז מתבצעת ההוספה. המיקום וגם
אפשר גם לשנות את הגודל של המאפיין AppStyledView
באמצעות
getDialogWindowLayoutParam
.
הקשרים
חשוב להפעיל שיקול דעת בעת השימוש בפלאגין, כי יש גם פלאגין
"source" הקשרים שונים. ההקשר של הפלאגין ניתן כארגומנט
getPluginFactory
, והוא ההקשר היחיד שכולל את
של יישומי הפלאגין שבהם. המשמעות היא שזה ההקשר היחיד שבו אפשר להשתמש
הגדלת הפריסה בפלאגין.
עם זאת, יכול להיות שההגדרה של הפלאגין לא נכונה. שפת תרגום
מקבלים את ההגדרה הנכונה, אנחנו מספקים הקשרי מקור בשיטות
רכיבים. הקשר המקור הוא בדרך כלל פעילות, אבל במקרים מסוימים
להיות גם שירות או רכיב Android אחר. כדי להשתמש בהגדרות האישיות
הקשר מקור עם המשאבים מההקשר של הפלאגין, הקשר חדש חייב
נוצר באמצעות createConfigurationContext
. אם ההגדרות האישיות הנכונות לא
תהיה הפרה של המצב המחמיר של Android, והצפיות המנופחות עלולות
שאינן במידות הנכונות.
Context layoutInflationContext = pluginContext.createConfigurationContext(
sourceContext.getResources().getConfiguration());
שינויים במצב
יישומי פלאגין מסוימים יכולים לתמוך במספר מצבים עבור הרכיבים שלהם, כמו מצב ספורט או מצב אקולוגי שנראים ייחודיים מבחינה חזותית. אין ב-CarUi יש תמיכה מובנית בפונקציונליות כזו, אבל שום דבר לא עוצר להטמיע אותו באופן פנימי בלבד. הפלאגין יכול לעקוב אחר בתנאים מסוימים שהוא רוצה לקבוע מתי להחליף מצבים, כמו להאזין לשידורים. הפלאגין לא יכול להפעיל שינוי הגדרה כדי לשנות מצבים, אבל לא מומלץ להסתמך על שינויים בהגדרות בכל מקרה, מכיוון שעדכון ידני של המראה של כל רכיב הוא חלק יותר למשתמש ומאפשרות גם מעברים שלא אפשריים עם שינויים בתצורה.
Jetpack פיתוח נייטיב
אפשר להטמיע יישומי פלאגין באמצעות 'Jetpack פיתוח נייטיב', אבל מדובר ברמת אלפא ולא להיחשב כיציבות.
יישומי פלאגין יכולים להשתמש
ComposeView
כדי ליצור משטח שמיועד לכתיבה. הComposeView
הזה יהיה
של המידע שמוחזר לאפליקציה מהשיטה getView
ברכיבים.
אחת הבעיות העיקריות בשימוש ב-ComposeView
היא שהגדרת תגים בתצוגת הרמה הבסיסית (root)
בפריסה כדי לאחסן משתנים גלובליים שמשותפים בין
תצוגות ComposeView שונות בהיררכיה. כי מזהי המשאבים של הפלאגין לא
ממרחב שמות בנפרד ממרחבי האפליקציה, הדבר עלול לגרום להתנגשויות כאשר
האפליקציה והפלאגין מגדירים תגים באותה תצוגה. בהתאמה אישית
ComposeViewWithLifecycle
שמעביר את המשתנים הגלובליים האלה
המאפיין ComposeView
מצוין למטה. שוב, המצב הזה לא צריך להיחשב כיציב.
ComposeViewWithLifecycle
:
class ComposeViewWithLifecycle @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr),
LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
private val lifeCycle = LifecycleRegistry(this)
private val modelStore = ViewModelStore()
private val savedStateRegistryController = SavedStateRegistryController.create(this)
private var composeView: ComposeView? = null
private var content = @Composable {}
init {
ViewTreeLifecycleOwner.set(this, this)
ViewTreeViewModelStoreOwner.set(this, this)
ViewTreeSavedStateRegistryOwner.set(this, this)
compositionContext = createCompositionContext()
}
fun setContent(content: @Composable () -> Unit) {
this.content = content
composeView?.setContent(content)
}
override fun getLifecycle(): Lifecycle {
return lifeCycle
}
override fun getViewModelStore(): ViewModelStore {
return modelStore
}
override fun getSavedStateRegistry(): SavedStateRegistry {
return savedStateRegistryController.savedStateRegistry
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
savedStateRegistryController.performRestore(Bundle())
lifeCycle.currentState = Lifecycle.State.RESUMED
composeView = ComposeView(context)
composeView?.setContent(content)
addView(composeView, LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
lifeCycle.currentState = Lifecycle.State.DESTROYED
modelStore.clear()
removeAllViews()
composeView = null
}
// Exact copy of View.createCompositionContext() in androidx's WindowRecomposer.android.kt
private fun createCompositionContext(): CompositionContext {
val currentThreadContext = AndroidUiDispatcher.CurrentThread
val pausableClock = currentThreadContext[MonotonicFrameClock]?.let {
PausableMonotonicFrameClock(it).apply { pause() }
}
val contextWithClock = currentThreadContext + (pausableClock ?: EmptyCoroutineContext)
val recomposer = Recomposer(contextWithClock)
val runRecomposeScope = CoroutineScope(contextWithClock)
val viewTreeLifecycleOwner = checkNotNull(ViewTreeLifecycleOwner.get(this)) {
"ViewTreeLifecycleOwner not found from $this"
}
viewTreeLifecycleOwner.lifecycle.addObserver(
LifecycleEventObserver { _, event ->
@Suppress("NON_EXHAUSTIVE_WHEN")
when (event) {
Lifecycle.Event.ON_CREATE ->
// Undispatched launch since we've configured this scope
// to be on the UI thread
runRecomposeScope.launch(start = CoroutineStart.UNDISPATCHED) {
recomposer.runRecomposeAndApplyChanges()
}
Lifecycle.Event.ON_START -> pausableClock?.resume()
Lifecycle.Event.ON_STOP -> pausableClock?.pause()
Lifecycle.Event.ON_DESTROY -> {
recomposer.cancel()
}
}
}
)
return recomposer
}
// TODO: ComposeViewWithLifecycle should handle saving state and other lifecycle things
// override fun onSaveInstanceState(): Parcelable? {
// val superState = super.onSaveInstanceState()
// val bundle = Bundle()
// savedStateRegistryController.performSave(bundle)
// }
}