به جای استفاده از همپوشانی منابع زمان اجرا (RRO) از پلاگین های کتابخانه UI برای ایجاد پیاده سازی کامل از سفارشی سازی مؤلفه ها در کتابخانه Car UI استفاده کنید. RRO ها شما را قادر می سازند که فقط منابع XML مؤلفه های کتابخانه Car UI را تغییر دهید، که میزان سفارشی سازی را محدود می کند.
یک افزونه ایجاد کنید
پلاگین کتابخانه UI Car یک 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')
این افزونه باید یک ارائه دهنده محتوا در مانیفست خود داشته باشد که دارای ویژگی های زیر باشد:
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 مستقل کامپایل میشوند که در زمان اجرا توسط سایر برنامهها ارجاع داده میشود.
افزونههایی که بهعنوان یک کتابخانه مشترک اندروید پیادهسازی میشوند، کلاسهایشان بهطور خودکار به کلاسلودر مشترک بین برنامهها اضافه میشوند. وقتی برنامهای که از کتابخانه Car UI استفاده میکند، وابستگی زمان اجرا را به کتابخانه مشترک افزونه مشخص میکند، کلاسلودر آن میتواند به کلاسهای کتابخانه مشترک افزونه دسترسی داشته باشد. افزونههایی که بهعنوان برنامههای عادی اندروید (نه یک کتابخانه مشترک) پیادهسازی میشوند، میتوانند بر زمان شروع سرد برنامه تأثیر منفی بگذارند.
پیاده سازی و ساخت کتابخانه های مشترک
توسعه با کتابخانه های مشترک اندروید بسیار شبیه برنامه های معمولی اندروید است، با چند تفاوت کلیدی.
- از تگ
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 (غیرفعال کردن استقرار بهینه سازی در اندروید 11 به بعد) در پیکربندی ساخت افزونه برطرف کرد.
علاوه بر این، هنگام نصب افزونه، اندروید استودیو خطایی را گزارش می دهد که نمی تواند فعالیت اصلی را برای راه اندازی پیدا کند. این مورد انتظار است، زیرا افزونه هیچ فعالیتی ندارد (به جز هدف خالی که برای حل یک intent استفاده می شود). برای رفع خطا، گزینه Launch را به Nothing در تنظیمات ساخت تغییر دهید.
شکل 1. پیکربندی پلاگین Android Studio
افزونه پروکسی
سفارشیسازی برنامهها با استفاده از کتابخانه Car UI به یک RRO نیاز دارد که هر برنامه خاصی را که قرار است اصلاح شود، هدف قرار میدهد، از جمله زمانی که سفارشیسازیها در بین برنامهها یکسان هستند. این بدان معنی است که یک RRO برای هر برنامه مورد نیاز است. ببینید کدام برنامهها از کتابخانه Car UI استفاده میکنند.
افزونه پروکسی کتابخانه UI Car نمونه ای از کتابخانه مشترک پلاگین است که اجرای اجزای خود را به نسخه ایستا کتابخانه UI Car واگذار می کند. این افزونه را می توان با RRO هدف قرار داد، که می تواند به عنوان یک نقطه سفارشی سازی برای برنامه هایی که از کتابخانه Car UI بدون نیاز به پیاده سازی یک افزونه کاربردی استفاده می کنند، استفاده شود. برای اطلاعات بیشتر در مورد RRO ها، به تغییر مقدار منابع برنامه در زمان اجرا مراجعه کنید.
افزونه پروکسی تنها یک مثال و نقطه شروع برای انجام سفارشی سازی با استفاده از یک افزونه است. برای سفارشیسازی فراتر از RRO، میتوان زیرمجموعهای از اجزای افزونه را پیادهسازی کرد و از افزونه پروکسی برای بقیه استفاده کرد، یا همه اجزای افزونه را به طور کامل از ابتدا پیادهسازی کرد.
اگرچه پلاگین پراکسی یک نقطه از سفارشیسازی RRO را برای برنامهها فراهم میکند، برنامههایی که از استفاده از افزونه منصرف میشوند همچنان به یک RRO نیاز دارند که مستقیماً خود برنامه را هدف قرار دهد.
API های افزونه را پیاده سازی کنید
نقطه ورود اصلی به پلاگین کلاس com.android.car.ui.plugin.PluginVersionProviderImpl
است. همه افزونه ها باید دارای کلاسی با این نام و نام بسته باشند. این کلاس باید یک سازنده پیش فرض داشته باشد و رابط PluginVersionProviderOEMV1
را پیاده سازی کند.
افزونههای CarUi باید با برنامههایی که قدیمیتر یا جدیدتر از افزونه هستند کار کنند. برای تسهیل این امر، همه API های افزونه با یک V#
در انتهای نام کلاس خود نسخه می شوند. اگر نسخه جدیدی از کتابخانه Car UI با ویژگی های جدید منتشر شود، آنها بخشی از نسخه V2
این مؤلفه هستند. کتابخانه Car UI تمام تلاش خود را می کند تا ویژگی های جدید در محدوده یک جزء افزونه قدیمی کار کند. برای مثال، با تبدیل یک نوع دکمه جدید در نوار ابزار به MenuItems
.
با این حال، برنامهای با نسخه قدیمیتر کتابخانه Car UI نمیتواند با پلاگین جدیدی که در برابر APIهای جدیدتر نوشته شده است، سازگار شود. برای حل این مشکل، به افزونهها اجازه میدهیم پیادهسازیهای مختلف خود را بر اساس نسخه OEM API که توسط برنامهها پشتیبانی میشود، برگردانند.
PluginVersionProviderOEMV1
یک روش در خود دارد:
Object getPluginFactory(int maxVersion, Context context, String packageName);
این روش یک شی را برمیگرداند که بالاترین نسخه PluginFactoryOEMV#
را که توسط افزونه پشتیبانی میشود، پیادهسازی میکند، در حالی که هنوز کمتر یا مساوی maxVersion
است. اگر یک پلاگین پیادهسازی یک PluginFactory
قدیمی نداشته باشد، ممکن است null
برگرداند، در این صورت از پیادهسازی استاتیکی مرتبط اجزای CarUi استفاده میشود.
برای حفظ سازگاری به عقب با برنامههایی که در برابر نسخههای قدیمیتر کتابخانه استاتیک Car 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>
ارسال می شود. از این مصرف کننده می توان برای برقراری ارتباط با برنامه استفاده کرد که افزونه تا حدی محتوای برنامه را پوشش می دهد (با نوار ابزار یا موارد دیگر). سپس برنامه میداند که به کشیدن در این فضا ادامه میدهد، اما اجزای مهم قابل تعامل کاربر را از آن دور نگه میدارد. این افکت در طراحی مرجع ما استفاده می شود تا نوار ابزار نیمه شفاف شود و لیست هایی در زیر آن حرکت کنند. اگر این ویژگی اجرا نمی شد، اولین مورد از یک لیست زیر نوار ابزار گیر می کرد و قابل کلیک نبود. اگر این افکت مورد نیاز نباشد، افزونه می تواند Consumer را نادیده بگیرد.
شکل 2. پیمایش محتوا در زیر نوار ابزار
از دیدگاه برنامه، وقتی افزونه ورودیهای جدید ارسال میکند، آنها را از هر فعالیت یا قطعهای که InsetsChangedListener
پیادهسازی میکند، دریافت میکند. اگر یک اکتیویتی یا قطعه InsetsChangedListener
پیادهسازی نکند، کتابخانه Car Ui بهطور پیشفرض با اعمال اینستها بهعنوان بالشتک روی Activity
یا FragmentActivity
حاوی قطعه، insetها را مدیریت میکند. کتابخانه به طور پیشفرض ورودیها را روی قطعات اعمال نمیکند. در اینجا یک قطعه نمونه از یک پیاده سازی است که 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 را برگرداند، برای اینکه افزونه نشان دهد که نمیخواهد طرحبندی پایه را سفارشی کند، باید false
از customizesBaseLayout
برگرداند.
طرح پایه باید دارای یک FocusParkingView
و یک FocusArea
باشد تا به طور کامل از کنترل های چرخشی پشتیبانی کند. این نماها را می توان در دستگاه هایی که از چرخش پشتیبانی نمی کنند حذف کرد. FocusParkingView/FocusAreas
در کتابخانه استاتیک CarUi پیاده سازی شده است، بنابراین یک setRotaryFactories
برای ارائه کارخانه ها برای ایجاد نماها از زمینه ها استفاده می شود.
زمینه های مورد استفاده برای ایجاد نمای فوکوس باید زمینه منبع باشد، نه زمینه افزونه. 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
وارد سلسله مراتب view نمی کند، بلکه فقط نمای خود را از طریق getView
به کتابخانه ایستا برمی گرداند، که سپس درج را انجام می دهد. موقعیت و اندازه AppStyledView
را نیز می توان با پیاده سازی getDialogWindowLayoutParam
کنترل کرد.
زمینه ها
افزونه هنگام استفاده از Contexts باید مراقب باشد، زیرا هر دو زمینه افزونه و منبع وجود دارد. زمینه افزونه به عنوان یک آرگومان برای getPluginFactory
ارائه شده است و تنها زمینه ای است که منابع افزونه را در خود دارد. این بدان معناست که این تنها زمینهای است که میتوان از آن برای افزایش طرحبندی در افزونه استفاده کرد.
با این حال، زمینه افزونه ممکن است پیکربندی صحیحی روی آن تنظیم نشده باشد. برای به دست آوردن پیکربندی صحیح، زمینههای منبع را در روشهایی که مؤلفهها را ایجاد میکنند، ارائه میکنیم. متن منبع معمولاً یک فعالیت است، اما در برخی موارد ممکن است یک سرویس یا سایر مؤلفههای Android نیز باشد. برای استفاده از پیکربندی از زمینه منبع با منابع از زمینه افزونه، باید یک زمینه جدید با استفاده از 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)
// }
}