به جای استفاده از پوششهای منابع زمان اجرا (RRO)، از افزونههای کتابخانه رابط کاربری خودرو برای ایجاد پیادهسازیهای کامل سفارشیسازی اجزا در کتابخانه رابط کاربری خودرو استفاده کنید. RROها به شما این امکان را میدهند که فقط منابع XML اجزای کتابخانه رابط کاربری خودرو را تغییر دهید، که این امر میزان سفارشیسازی شما را محدود میکند.
ایجاد افزونه
یک افزونه کتابخانه رابط کاربری خودرو، یک فایل APK است که شامل کلاسهایی است که مجموعهای از APIهای افزونه را پیادهسازی میکنند. APIهای افزونه را میتوان به صورت یک کتابخانه استاتیک در یک افزونه کامپایل کرد.
مثالها را در Soong و 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')
افزونه باید یک ارائه دهنده محتوا (content provider) در مانیفست خود داشته باشد که دارای ویژگیهای زیر باشد:
android:authorities="com.android.car.ui.plugin"
android:enabled="true"
android:exported="true"
android:authorities="com.android.car.ui.plugin" باعث میشود افزونه برای کتابخانه Car UI قابل کشف باشد. ارائهدهنده باید صادر شود تا بتوان در زمان اجرا از آن پرسوجو کرد. همچنین، اگر ویژگی 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 مستقل کامپایل میشوند که در زمان اجرا توسط سایر برنامهها ارجاع داده میشود.
افزونههایی که به عنوان یک کتابخانه مشترک اندروید پیادهسازی میشوند، کلاسهایشان به طور خودکار به classloader مشترک بین برنامهها اضافه میشود. وقتی برنامهای که از کتابخانه رابط کاربری خودرو استفاده میکند، یک وابستگی زمان اجرا به کتابخانه مشترک افزونه مشخص میکند، classloader آن میتواند به کلاسهای کتابخانه مشترک افزونه دسترسی داشته باشد. افزونههایی که به عنوان برنامههای معمولی اندروید (نه یک کتابخانه مشترک) پیادهسازی میشوند، میتوانند بر زمان شروع سرد برنامه تأثیر منفی بگذارند.
پیادهسازی و ساخت کتابخانههای اشتراکی
توسعه با کتابخانههای اشتراکی اندروید بسیار شبیه به توسعه برنامههای معمولی اندروید است، البته با چند تفاوت کلیدی.
- از تگ
libraryزیر تگapplicationبه همراه نام بسته افزونه در مانیفست برنامه افزونه خود استفاده کنید:
<application>
<library android:name="com.chassis.car.ui.plugin" />
...
</application>
- قانون ساخت Soong
android_app(Android.bp) خود را با پرچم AAPTshared-libکه برای ساخت یک کتابخانه مشترک استفاده میشود، پیکربندی کنید:
android_app {
...
aaptflags: ["--shared-lib"],
...
}
وابستگیها به کتابخانههای اشتراکی
برای هر برنامهای که روی سیستم از کتابخانه Car UI استفاده میکند، تگ uses-library را در فایل مانیفست برنامه، زیر تگ application به همراه نام بسته افزونه، قرار دهید:
<manifest>
<application
android:name=".MyApp"
...>
<uses-library android:name="com.chassis.car.ui.plugin" android:required="false"/>
...
</application>
</manifest>
نصب یک افزونه
افزونهها باید با قرار دادن ماژول در PRODUCT_PACKAGES ، از قبل روی پارتیشن سیستم نصب شده باشند. بسته از پیش نصب شده را میتوان مانند هر برنامه نصب شده دیگری بهروزرسانی کرد.
اگر در حال بهروزرسانی یک افزونهی موجود در سیستم هستید، هر برنامهای که از آن افزونه استفاده میکند بهطور خودکار بسته میشود. پس از بازگشایی توسط کاربر، تغییرات بهروزرسانیشده روی آن برنامه اعمال میشود. اگر برنامه در حال اجرا نبوده باشد، دفعهی بعدی که شروع به کار کند، افزونهی بهروزرسانیشده را دارد.
هنگام نصب یک افزونه با اندروید استودیو، باید ملاحظات دیگری را نیز در نظر گرفت. در زمان نگارش این مطلب، اشکالی در فرآیند نصب برنامه اندروید استودیو وجود دارد که باعث میشود بهروزرسانیهای یک افزونه اعمال نشوند. این مشکل را میتوان با انتخاب گزینه Always install with package manager (disables deploy optimizations on Android 11 and later) در پیکربندی ساخت افزونه برطرف کرد.
علاوه بر این، هنگام نصب افزونه، اندروید استودیو خطایی مبنی بر عدم یافتن فعالیت اصلی برای راهاندازی گزارش میدهد. این مورد قابل پیشبینی بود، زیرا افزونه هیچ فعالیتی ندارد (به جز intent خالی که برای حل یک intent استفاده میشود). برای رفع این خطا، گزینه Launch را در پیکربندی ساخت به Nothing تغییر دهید.
شکل 1. پیکربندی افزونه اندروید استودیو
افزونه پروکسی
سفارشیسازی برنامهها با استفاده از کتابخانه رابط کاربری خودرو (Car UI Library) نیازمند یک RRO است که هر برنامه خاص که قرار است تغییر کند را هدف قرار دهد، از جمله زمانی که سفارشیسازیها در بین برنامهها یکسان هستند. این بدان معناست که یک RRO برای هر برنامه مورد نیاز است. ببینید کدام برنامهها از کتابخانه رابط کاربری خودرو (Car UI Library) استفاده میکنند.
افزونه پروکسی کتابخانه Car UI یک کتابخانه اشتراکی افزونه نمونه است که پیادهسازیهای اجزای خود را به نسخه استاتیک کتابخانه Car UI واگذار میکند. این افزونه میتواند با یک RRO هدفگیری شود که میتواند به عنوان یک نقطه واحد سفارشیسازی برای برنامههایی که از کتابخانه Car UI استفاده میکنند، بدون نیاز به پیادهسازی یک افزونه کاربردی، مورد استفاده قرار گیرد. برای اطلاعات بیشتر در مورد RROها، به بخش تغییر مقدار منابع یک برنامه در زمان اجرا مراجعه کنید.
افزونه پروکسی تنها یک مثال و نقطه شروع برای انجام سفارشیسازی با استفاده از یک افزونه است. برای سفارشیسازی فراتر از RROها، میتوان زیرمجموعهای از اجزای افزونه را پیادهسازی کرد و برای بقیه از افزونه پروکسی استفاده کرد، یا تمام اجزای افزونه را کاملاً از ابتدا پیادهسازی کرد.
اگرچه افزونه پروکسی یک نقطه واحد برای سفارشیسازی RRO برای برنامهها فراهم میکند، برنامههایی که از استفاده از این افزونه انصراف میدهند، همچنان به RRO نیاز دارند که مستقیماً خود برنامه را هدف قرار دهد.
پیادهسازی APIهای افزونه
نقطه ورود اصلی به افزونه، کلاس com.android.car.ui.plugin.PluginVersionProviderImpl است. همه افزونهها باید شامل کلاسی با همین نام و نام بسته باشند. این کلاس باید یک سازنده پیشفرض داشته باشد و رابط PluginVersionProviderOEMV1 را پیادهسازی کند.
افزونههای CarUi باید با برنامههایی که قدیمیتر یا جدیدتر از افزونه هستند، کار کنند. برای تسهیل این امر، تمام APIهای افزونه با یک V# در انتهای نام کلاس خود نسخهبندی میشوند. اگر نسخه جدیدی از کتابخانه Car UI با ویژگیهای جدید منتشر شود، آنها بخشی از نسخه V2 کامپوننت هستند. کتابخانه Car UI تمام تلاش خود را میکند تا ویژگیهای جدید در محدوده یک کامپوننت افزونه قدیمیتر کار کنند. به عنوان مثال، با تبدیل یک نوع دکمه جدید در نوار ابزار به MenuItems .
با این حال، برنامهای با نسخه قدیمیتر کتابخانه رابط کاربری خودرو نمیتواند با افزونه جدیدی که برای APIهای جدیدتر نوشته شده است، سازگار شود. برای حل این مشکل، ما به افزونهها اجازه میدهیم تا پیادهسازیهای مختلفی از خود را بر اساس نسخه API OEM پشتیبانی شده توسط برنامهها، برگردانند.
PluginVersionProviderOEMV1 یک متد در خود دارد:
Object getPluginFactory(int maxVersion, Context context, String packageName);
این متد شیءای را برمیگرداند که بالاترین نسخه PluginFactoryOEMV# پشتیبانیشده توسط افزونه را پیادهسازی میکند، در حالی که همچنان کمتر یا مساوی maxVersion است. اگر افزونهای پیادهسازی PluginFactory قدیمی نداشته باشد، ممکن است null برگرداند، که در این صورت از پیادهسازی استاتیک-لینکشده کامپوننتهای CarUi استفاده میشود.
برای حفظ سازگاری با برنامههایی که با نسخههای قدیمیتر کتابخانه رابط کاربری استاتیک Car کامپایل شدهاند، توصیه میشود از maxVersion های ۲، ۵ و بالاتر از طریق پیادهسازی کلاس PluginVersionProvider در افزونه خود پشتیبانی کنید. نسخههای ۱، ۳ و ۴ پشتیبانی نمیشوند. برای اطلاعات بیشتر، به PluginVersionProviderImpl مراجعه کنید.
PluginFactory رابطی است که تمام کامپوننتهای CarUi دیگر را ایجاد میکند. همچنین مشخص میکند که کدام نسخه از رابطهای آنها باید استفاده شود. اگر افزونه به دنبال پیادهسازی هیچ یک از این کامپوننتها نباشد، ممکن است در تابع ایجاد آنها null را برگرداند (به استثنای نوار ابزار که تابع customizesBaseLayout() جداگانهای دارد).
pluginFactory نسخههایی از کامپوننتهای CarUi را که میتوانند با هم استفاده شوند، محدود میکند. برای مثال، هرگز pluginFactory وجود نخواهد داشت که بتواند نسخه ۱۰۰ یک Toolbar و همچنین نسخه ۱ یک RecyclerView ایجاد کند، زیرا تضمین کمی وجود دارد که نسخههای متنوعی از کامپوننتها با هم کار کنند. برای استفاده از toolbar نسخه ۱۰۰، از توسعهدهندگان انتظار میرود پیادهسازی از نسخه pluginFactory را ارائه دهند که یک toolbar نسخه ۱۰۰ ایجاد میکند، که سپس گزینههای مربوط به نسخههای سایر کامپوننتهایی را که میتوانند ایجاد شوند، محدود میکند. نسخههای سایر کامپوننتها ممکن است برابر نباشند، برای مثال، pluginFactoryOEMV100 میتواند یک ToolbarControllerOEMV100 و یک RecyclerViewOEMV70 ایجاد کند.
نوار ابزار
طرح پایه
نوار ابزار و "طرح پایه" بسیار به هم نزدیک هستند، از این رو تابعی که نوار ابزار را ایجاد میکند installBaseLayoutAround نام دارد. طرح پایه مفهومی است که به نوار ابزار اجازه میدهد تا در هر جایی از محتوای برنامه قرار گیرد، تا یک نوار ابزار در بالا/پایین برنامه، به صورت عمودی در امتداد کنارهها یا حتی یک نوار ابزار دایرهای که کل برنامه را در بر میگیرد، ایجاد شود. این کار با ارسال یک view به installBaseLayoutAround برای قرار گرفتن طرح نوار ابزار/پایه در اطراف آن انجام میشود.
این افزونه باید نمای ارائه شده را بگیرد، آن را از والدش جدا کند، طرحبندی افزونه را در همان اندیس والد و با همان LayoutParams به عنوان نمای جدا شده، تغییر دهد و سپس نمای را در جایی درون طرحبندی که تغییر داده شده است، دوباره متصل کند. طرحبندی تغییر یافته، در صورت درخواست برنامه، شامل نوار ابزار نیز خواهد بود.
برنامه میتواند بدون نوار ابزار، طرحبندی پایه را درخواست کند. اگر این اتفاق بیفتد، installBaseLayoutAround باید مقدار null را برگرداند. برای اکثر افزونهها، این تمام کاری است که باید انجام شود، اما اگر نویسنده افزونه بخواهد مثلاً یک تزئین در اطراف لبه برنامه اعمال کند، باز هم میتواند با یک طرحبندی پایه انجام شود. این تزئینات به ویژه برای دستگاههایی با صفحه نمایش غیرمستطیلی مفید هستند، زیرا میتوانند برنامه را به یک فضای مستطیلی سوق دهند و انتقالهای تمیزی را به فضای غیرمستطیلی اضافه کنند.
همچنین به installBaseLayoutAround یک Consumer<InsetsOEMV1> ارسال میشود. این consumer میتواند برای اطلاعرسانی به برنامه استفاده شود که افزونه تا حدی محتوای برنامه را پوشش میدهد (با نوار ابزار یا موارد دیگر). سپس برنامه متوجه میشود که باید به ترسیم در این فضا ادامه دهد، اما اجزای مهم قابل تعامل با کاربر را از آن دور نگه دارد. این افکت در طراحی مرجع ما استفاده میشود تا نوار ابزار نیمه شفاف شود و لیستها زیر آن اسکرول شوند. اگر این ویژگی پیادهسازی نشده بود، اولین آیتم در یک لیست زیر نوار ابزار گیر میکرد و قابل کلیک نبود. اگر این افکت مورد نیاز نباشد، افزونه میتواند Consumer را نادیده بگیرد.
شکل ۲. پیمایش محتوا در زیر نوار ابزار
از دیدگاه برنامه، وقتی افزونه insetهای جدید ارسال میکند، آنها را از هر activity یا fragment که InsetsChangedListener را پیادهسازی میکند، دریافت میکند. اگر یک activity یا fragment InsetsChangedListener پیادهسازی نکند، کتابخانه Car UI به طور پیشفرض insetها را با اعمال insetها به عنوان padding به Activity یا FragmentActivity حاوی fragment، مدیریت میکند. این کتابخانه insetها را به طور پیشفرض به fragmentها اعمال نمیکند. در اینجا یک قطعه نمونه از پیادهسازی که insetها را به عنوان padding روی 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 وقتی toolbarEnabled false دارد، مقدار null را برگرداند، برای اینکه افزونه نشان دهد که نمیخواهد طرح پایه را سفارشی کند، باید از customizesBaseLayout false برگرداند.
طرح پایه باید شامل یک FocusParkingView و یک FocusArea باشد تا بتواند به طور کامل از کنترلهای چرخشی پشتیبانی کند. این نماها را میتوان در دستگاههایی که از چرخش پشتیبانی نمیکنند، حذف کرد. FocusParkingView/FocusAreas در کتابخانه استاتیک CarUi پیادهسازی شدهاند، بنابراین از setRotaryFactories برای فراهم کردن کارخانههایی جهت ایجاد نماها از زمینهها استفاده میشود.
زمینههایی که برای ایجاد نماهای Focus استفاده میشوند باید زمینه منبع باشند، نه زمینه افزونه. FocusParkingView باید تا حد امکان به اولین نمای درخت نزدیک باشد، زیرا زمانی که نباید فوکوس برای کاربر قابل مشاهده باشد، این همان چیزی است که فوکوس میشود. FocusArea باید نوار ابزار را در طرح پایه قرار دهد تا نشان دهد که یک منطقه چرخشی است. اگر FocusArea ارائه نشود، کاربر نمیتواند با کنترلر چرخشی به هیچ دکمهای در نوار ابزار پیمایش کند.
کنترل کننده نوار ابزار
پیادهسازی ToolbarController واقعی که برگردانده میشود، باید بسیار سرراستتر از طرح پایه باشد. وظیفه آن دریافت اطلاعات ارسالی به setterها و نمایش آنها در طرح پایه است. برای اطلاعات بیشتر در مورد اکثر متدها به Javadoc مراجعه کنید. برخی از متدهای پیچیدهتر در زیر مورد بحث قرار گرفتهاند.
getImeSearchInterface برای نمایش نتایج جستجو در پنجره IME (صفحه کلید) استفاده میشود. این میتواند برای نمایش/متحرکسازی نتایج جستجو در کنار صفحه کلید مفید باشد، به عنوان مثال اگر صفحه کلید فقط نیمی از صفحه نمایش را اشغال کرده باشد. بیشتر عملکردها در کتابخانه استاتیک CarUi پیادهسازی شدهاند، رابط جستجو در افزونه فقط متدهایی را برای کتابخانه استاتیک فراهم میکند تا فراخوانیهای TextView و onPrivateIMECommand را دریافت کند. برای پشتیبانی از این، افزونه باید از یک زیرکلاس TextView استفاده کند که onPrivateIMECommand را لغو میکند و فراخوانی را به شنونده ارائه شده به عنوان TextView نوار جستجوی خود منتقل میکند.
setMenuItems به سادگی MenuItems را روی صفحه نمایش میدهد، اما به طرز شگفتآوری اغلب فراخوانی میشود. از آنجایی که API افزونه برای MenuItems تغییرناپذیر است، هر زمان که یک MenuItem تغییر کند، یک فراخوانی کاملاً جدید setMenuItems اتفاق میافتد. این میتواند برای چیزی به سادگی کلیک کاربر روی یک سوئیچ MenuItem اتفاق بیفتد و آن کلیک باعث تغییر وضعیت سوئیچ شود. بنابراین، به دلایل عملکرد و انیمیشن، توصیه میشود تفاوت بین لیست قدیمی و جدید MenuItems محاسبه شود و فقط نماهایی که واقعاً تغییر کردهاند بهروزرسانی شوند. MenuItems یک فیلد key ارائه میدهد که میتواند در این امر کمک کند، زیرا کلید باید در فراخوانیهای مختلف setMenuItems برای یک MenuItem یکسان باشد.
زمینهها
افزونه باید هنگام استفاده از Contexts دقت کند، زیرا هم context های plugin و هم "source" وجود دارد. context افزونه به عنوان آرگومان به getPluginFactory داده میشود و تنها context ای است که منابع افزونه را در خود دارد. این بدان معناست که تنها context ای است که میتواند برای inflate کردن layout ها در افزونه استفاده شود.
با این حال، ممکن است پیکربندی صحیح روی زمینه افزونه تنظیم نشده باشد. برای دریافت پیکربندی صحیح، زمینههای منبع را در متدهایی که اجزا را ایجاد میکنند، ارائه میدهیم. زمینه منبع معمولاً یک فعالیت است، اما در برخی موارد ممکن است یک سرویس یا سایر اجزای اندروید نیز باشد. برای استفاده از پیکربندی از زمینه منبع با منابع از زمینه افزونه، باید یک زمینه جدید با استفاده از createConfigurationContext ایجاد شود. اگر پیکربندی صحیح استفاده نشود، نقض حالت سخت اندروید رخ خواهد داد و نماهای متورم ممکن است ابعاد صحیحی نداشته باشند.
Context layoutInflationContext = pluginContext.createConfigurationContext(
sourceContext.getResources().getConfiguration());
تغییرات حالت
برخی از افزونهها میتوانند از چندین حالت برای اجزای خود پشتیبانی کنند، مانند حالت اسپرت یا حالت اکو که از نظر بصری متمایز به نظر میرسند. هیچ پشتیبانی داخلی برای چنین عملکردی در CarUi وجود ندارد، اما هیچ چیز مانع از پیادهسازی کامل داخلی افزونه نمیشود. این افزونه میتواند هر شرایطی را که میخواهد رصد کند تا زمان تغییر حالتها را تشخیص دهد، مانند گوش دادن به پخشها. این افزونه نمیتواند تغییر پیکربندی را برای تغییر حالتها ایجاد کند، اما به هر حال توصیه نمیشود که به تغییرات پیکربندی تکیه کنید، زیرا بهروزرسانی دستی ظاهر هر جزء برای کاربر روانتر است و همچنین امکان انتقالهایی را فراهم میکند که با تغییرات پیکربندی امکانپذیر نیستند.
جتپک آهنگسازی
افزونهها را میتوان با استفاده از Jetpack Compose پیادهسازی کرد، اما این یک ویژگی سطح آلفا است و نباید آن را پایدار در نظر گرفت.
افزونهها میتوانند از ComposeView برای ایجاد یک سطح با قابلیت Compose برای رندر کردن استفاده کنند. این ComposeView همان چیزی است که از متد getView در کامپوننتها به app برگردانده میشود.
یکی از مشکلات اصلی استفاده از 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)
// }
}