השתמש בתוספים של ספריית ממשק המשתמש של רכב כדי ליצור יישומים מלאים של התאמות אישיות של רכיבים בספריית ממשק המשתמש של רכב במקום להשתמש בשכבות-על של משאבי זמן ריצה (RRO). RROs מאפשרים לך לשנות רק את משאבי ה-XML של רכיבי ספריית Car UI, מה שמגביל את מידת ההתאמה האישית.
צור תוסף
תוסף ספריית ממשק משתמש לרכב הוא APK המכיל מחלקות שמיישמות קבוצה של ממשקי API של פלאגין . ניתן להרכיב את ממשקי ה-API של הפלאגין לתוך תוסף כספרייה סטטית.
ראה דוגמאות ב- Soong and Gradle:
בקרוב
שקול את הדוגמה הזו של Soong:
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>
לבסוף, כאמצעי אבטחה, חתום על האפליקציה שלך .
תוספים כספרייה משותפת
שלא כמו ספריות סטטיות של אנדרואיד אשר מורכבות ישירות לתוך אפליקציות, ספריות משותפות של אנדרואיד ממולאות ל-APK עצמאי שאליו מתייחסים אפליקציות אחרות בזמן ריצה.
תוספים המיושמים כספרייה משותפת של אנדרואיד, השיעורים שלהם מתווספים אוטומטית למטען הכיתה המשותף בין אפליקציות. כאשר אפליקציה שמשתמשת בספריית ממשק המשתמש של רכב מציינת תלות בזמן ריצה בספרייה המשותפת של הפלאגין, טוען הכיתה שלה יכול לגשת למחלקות של הספרייה המשותפת של הפלאגין. תוספים המיושמים כאפליקציות אנדרואיד רגילות (לא ספרייה משותפת) יכולים להשפיע לרעה על זמני ההתחלה הקרה של האפליקציה.
יישום ובניית ספריות משותפות
הפיתוח עם ספריות משותפות של אנדרואיד דומה מאוד לזה של אפליקציות אנדרואיד רגילות, עם כמה הבדלים עיקריים.
- השתמש בתג
library
מתחת לתגapplication
עם שם חבילת הפלאגין במניפסט האפליקציה של הפלאגין שלך:
<application>
<library android:name="com.chassis.car.ui.plugin" />
...
</application>
- הגדר את כלל הבנייה של Soong
android_app
(Android.bp
) עם דגל AAPTshared-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 של התוסף.
בנוסף, בעת התקנת התוסף, אנדרואיד סטודיו מדווח על שגיאה שאינה מוצאת פעילות עיקרית להפעלה. זה צפוי, מכיוון שלפלאגין אין פעילויות כלשהן (למעט הכוונה הריקה המשמשת לפתרון כוונה). כדי לבטל את השגיאה, שנה את אפשרות ההפעלה ל- Nothing בתצורת ה-build.
איור 1. תצורת Plugin Android Studio
תוסף פרוקסי
התאמה אישית של אפליקציות באמצעות ספריית ממשק המשתמש של רכב מחייבת RRO המכוון לכל אפליקציה ספציפית שיש לשנות, כולל כאשר ההתאמות האישיות זהות בין האפליקציות. המשמעות היא שנדרש RRO לכל אפליקציה. ראה אילו אפליקציות משתמשות בספריית ממשק המשתמש של רכב.
תוסף ה-Proxy של ספריית ממשק המשתמש של רכב הוא ספריית פלאגין משותפת לדוגמה, המאצילה את יישומי הרכיבים שלה לגרסה הסטטית של ספריית ממשק המשתמש של רכב. תוסף זה יכול להיות ממוקד עם RRO, אשר יכול לשמש כנקודה אחת להתאמה אישית עבור אפליקציות המשתמשות בספריית ממשק המשתמש של רכב ללא צורך ביישום תוסף פונקציונלי. למידע נוסף על RROs, ראה שינוי הערך של משאבי אפליקציה בזמן ריצה .
תוסף ה-proxy הוא רק דוגמה ונקודת התחלה לביצוע התאמה אישית באמצעות תוסף. להתאמה אישית מעבר ל-RROs, אפשר ליישם תת-קבוצה של רכיבי תוסף ולהשתמש בתוסף ה-proxy לשאר, או ליישם את כל רכיבי הפלאגין לגמרי מאפס.
למרות שתוסף ה-proxy מספק נקודה אחת של התאמה אישית של RRO עבור אפליקציות, אפליקציות שיבטלו את השימוש בתוסף עדיין ידרשו RRO שמכוון ישירות לאפליקציה עצמה.
הטמע את ממשקי ה-API של הפלאגין
נקודת הכניסה העיקרית לפלאגין היא מחלקת com.android.car.ui.plugin.PluginVersionProviderImpl
. כל התוספים חייבים לכלול מחלקה עם השם המדויק הזה ושם החבילה. למחלקה זו חייב להיות בנאי ברירת מחדל ולהטמיע את ממשק PluginVersionProviderOEMV1
.
תוספים של CarUi חייבים לעבוד עם אפליקציות ישנות או חדשות יותר מהפלאגין. כדי להקל על זה, כל ממשקי ה-API של הפלאגין מגויסים עם V#
בסוף שם הכיתה שלהם. אם יוצאת גרסה חדשה של ספריית ממשק המשתמש של רכב עם תכונות חדשות, הן חלק מגרסת V2
של הרכיב. ספריית ממשק המשתמש של הרכב עושה כמיטב יכולתה כדי לגרום לתכונות חדשות לעבוד בהיקף של רכיב תוסף ישן יותר. לדוגמה, על ידי המרת סוג חדש של כפתור בסרגל הכלים ל- MenuItems
.
עם זאת, אפליקציה עם גרסה ישנה יותר של ספריית ממשק המשתמש של רכב לא יכולה להסתגל לפלאגין חדש שנכתב כנגד ממשקי API חדשים יותר. כדי לפתור בעיה זו, אנו מאפשרים לפלאגינים להחזיר יישומים שונים של עצמם בהתבסס על הגרסה של OEM API הנתמכת על ידי האפליקציות.
PluginVersionProviderOEMV1
כולל שיטה אחת:
Object getPluginFactory(int maxVersion, Context context, String packageName);
שיטה זו מחזירה אובייקט שמיישם את הגרסה הגבוהה ביותר של PluginFactoryOEMV#
הנתמכת על ידי הפלאגין, תוך שהוא עדיין קטן או שווה ל- maxVersion
. אם לפלאגין אין מימוש של PluginFactory
כל כך ישן, הוא עשוי להחזיר null
, ובמקרה זה נעשה שימוש ביישום המקושר סטטית של רכיבי CarUi.
כדי לשמור על תאימות לאחור עם יישומים אשר מורכבים כנגד גרסאות ישנות יותר של ספריית Car Ui הסטטית, מומלץ לתמוך ב- maxVersion
s של 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. גלילה של תוכן מתחת לסרגל הכלים
מנקודת המבט של האפליקציה, כאשר הפלאגין שולח הוספות חדשות, הוא יקבל אותם מכל פעילות או פרגמנט שמיישמים את InsetsChangedListener
. אם פעילות או מקטע אינם מיישמים את InsetsChangedListener
, ספריית Car Ui תטפל בהוספות כברירת מחדל על ידי החלת ההוספות כריפוד על Activity
או FragmentActivity
המכילים את הפרגמנט. הספרייה אינה מיישמת את ההוספות כברירת מחדל על פרגמנטים. הנה קטע לדוגמה של יישום שמחיל את ההוספות כריפוד ב- RecyclerView
באפליקציה:
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
משמש כדי לספק מפעלים ליצירת התצוגות מהקשרים.
ההקשרים המשמשים ליצירת תצוגות Focus חייבים להיות ההקשר של המקור, לא ההקשר של הפלאגין. ה- FocusParkingView
צריך להיות הכי קרוב לתצוגה הראשונה בעץ ככל שניתן באופן סביר, מכיוון שהוא מה שממוקד כאשר לא אמור להיות מיקוד גלוי למשתמש. ה- FocusArea
חייב לעטוף את סרגל הכלים בפריסת הבסיס כדי לציין שזהו אזור דחיפה סיבובית. אם ה- FocusArea
לא מסופק, המשתמש אינו יכול לנווט ללחצנים כלשהם בסרגל הכלים עם הבקר הסיבובי.
בקר סרגל הכלים
ה- ToolbarController
המוחזר בפועל צריך להיות הרבה יותר פשוט ליישום מאשר פריסת הבסיס. תפקידו הוא לקחת מידע המועבר לקובעים שלו ולהציג אותו בפריסת הבסיס. עיין ב-Javadoc למידע על רוב השיטות. כמה מהשיטות המורכבות יותר נדון להלן.
getImeSearchInterface
משמש להצגת תוצאות חיפוש בחלון IME (מקלדת). זה יכול להיות שימושי להצגת/הנפשת תוצאות חיפוש לצד המקלדת, למשל אם המקלדת תפסה רק חצי מהמסך. רוב הפונקציונליות מיושמת בספריית CarUi הסטטית, ממשק החיפוש בתוסף רק מספק שיטות עבור הספרייה הסטטית לקבל את ההתקשרות של TextView
ו- onPrivateIMECommand
. כדי לתמוך בכך, התוסף צריך להשתמש בתת-מחלקה TextView
שעוקפת את onPrivateIMECommand
ומעבירה את הקריאה למאזין המסופק בתור TextView
של שורת החיפוש שלו.
setMenuItems
פשוט מציג MenuItems על המסך, אבל זה ייקרא לעתים קרובות באופן מפתיע. מכיוון שה-API של הפלאגין עבור MenuItems אינו ניתן לשינוי, בכל פעם שמשנה MenuItem, תתרחש קריאה חדשה לגמרי של setMenuItems
. זה יכול לקרות עבור משהו טריוויאלי כמו שמשתמש לחץ על מתג MenuItem, והלחיצה הזו גרמה למתג לעבור. הן מסיבות ביצועים והן מסיבות אנימציה, לכן מומלץ לחשב את ההפרש בין רשימת MenuItems הישנה לחדשה, ולעדכן רק את התצוגות שהשתנו בפועל. ה-MenuItems מספקים שדה key
שיכול לעזור בכך, שכן המפתח צריך להיות זהה בקריאות שונות ל- setMenuItems
עבור אותו MenuItem.
AppStyledView
ה- AppStyledView
הוא מיכל לתצוגה שאינה מותאמת אישית כלל. ניתן להשתמש בו כדי לספק גבול מסביב לאותה תצוגה שמבליטה אותה משאר האפליקציה, ולציין למשתמש שמדובר בממשק מסוג אחר. התצוגה שעוטפת ה-AppStyledView ניתנת ב- setContent
. ל- AppStyledView
יכול להיות גם כפתור חזרה או סגירה לפי בקשת האפליקציה.
ה- AppStyledView
לא מכניס מיד את התצוגות שלו להיררכיית התצוגות כמו ש- installBaseLayoutAround
עושה, הוא פשוט מחזיר את התצוגה שלו לספרייה הסטטית דרך getView
, אשר לאחר מכן מבצעת את ההוספה. ניתן לשלוט במיקום ובגודל של AppStyledView
גם על ידי יישום getDialogWindowLayoutParam
.
הקשרים
התוסף חייב להיות זהיר בעת שימוש בהקשרים, מכיוון שיש גם הקשרים של תוסף וגם "מקור". ההקשר של הפלאגין ניתן כארגומנט ל- getPluginFactory
, והוא ההקשר היחיד שיש בו את משאבי התוסף. המשמעות היא שזהו ההקשר היחיד שניתן להשתמש בו כדי לנפח פריסות בתוסף.
עם זאת, ייתכן שהקונפיגורציה הנכונה של הפלאגין אינה מוגדרת עליו. כדי לקבל את התצורה הנכונה, אנו מספקים הקשרי מקור בשיטות היוצרות רכיבים. הקשר המקור הוא בדרך כלל פעילות, אך במקרים מסוימים עשוי להיות גם שירות או רכיב אנדרואיד אחר. כדי להשתמש בתצורה מהקשר המקור עם המשאבים מהקשר הפלאגין, יש ליצור הקשר חדש באמצעות createConfigurationContext
. אם לא נעשה שימוש בתצורה הנכונה, תהיה הפרת מצב קפדנית של אנדרואיד, וייתכן שלתצוגות המנופחות לא יהיו המידות הנכונות.
Context layoutInflationContext = pluginContext.createConfigurationContext(
sourceContext.getResources().getConfiguration());
שינויים במצב
תוספים מסוימים יכולים לתמוך במספר מצבים עבור הרכיבים שלהם, כגון מצב ספורט או מצב אקולוגי שנראים מובחנים ויזואלית. אין תמיכה מובנית בפונקציונליות כזו ב-CarUi, אבל אין שום דבר שמפריע לתוסף ליישם אותו באופן פנימי לחלוטין. התוסף יכול לעקוב אחר כל התנאים שהוא רוצה כדי להבין מתי להחליף מצבים, כגון האזנה לשידורים. התוסף לא יכול להפעיל שינוי תצורה לשינוי מצבים, אבל בכל מקרה לא מומלץ להסתמך על שינויי תצורה, שכן עדכון ידני של המראה של כל רכיב חלק יותר למשתמש ומאפשר גם מעברים שאינם אפשריים בשינויי תצורה.
Jetpack Compose
ניתן ליישם תוספים באמצעות Jetpack Compose, אך זוהי תכונה ברמת אלפא ואינה אמורה להיחשב יציבה.
תוספים יכולים להשתמש ComposeView
כדי ליצור משטח התומך ב-Compose לעיבוד. ComposeView
זה יהיה מה שמוחזר לאפליקציה משיטת getView
ברכיבים.
בעיה מרכזית אחת בשימוש ComposeView
היא שהוא מגדיר תגים בתצוגת השורש בפריסה על מנת לאחסן משתנים גלובליים המשותפים בין תצוגות 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)
// }
}