Sử dụng trình bổ trợ thư viện giao diện người dùng trên ô tô để tạo các phương thức triển khai hoàn chỉnh cho các chế độ tuỳ chỉnh thành phần trong thư viện giao diện người dùng trên ô tô thay vì sử dụng lớp phủ tài nguyên thời gian chạy (RRO). RRO chỉ cho phép bạn thay đổi tài nguyên XML của các thành phần trong thư viện giao diện người dùng trên ô tô, điều này giới hạn phạm vi tuỳ chỉnh.
Tạo trình bổ trợ
Trình bổ trợ thư viện giao diện người dùng trên ô tô là một APK chứa các lớp triển khai một tập hợp API trình bổ trợ. Bạn có thể biên dịch API trình bổ trợ thành trình bổ trợ dưới dạng thư viện tĩnh.
Xem các ví dụ trong Soong và Gradle:
Soong
Hãy xem ví dụ về Soong sau:
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",
}
Gradle
Hãy xem tệp build.gradle này:
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')
Trình bổ trợ phải có một nhà cung cấp nội dung được khai báo trong tệp kê khai có các thuộc tính sau:
android:authorities="com.android.car.ui.plugin"
android:enabled="true"
android:exported="true"
android:authorities="com.android.car.ui.plugin" giúp thư viện giao diện người dùng trên ô tô phát hiện được trình bổ trợ. Bạn phải xuất nhà cung cấp để có thể truy vấn nhà cung cấp này trong thời gian chạy. Ngoài ra, nếu bạn đặt thuộc tính enabled thành false, thì phương thức triển khai mặc định sẽ được sử dụng thay vì phương thức triển khai trình bổ trợ. Lớp nhà cung cấp nội dung không bắt buộc phải tồn tại. Trong trường hợp đó, hãy nhớ thêm
tools:ignore="MissingClass" vào định nghĩa nhà cung cấp. Hãy xem mục nhập tệp kê khai mẫu bên dưới:
<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>
Cuối cùng, hãy Ký ứng dụng của bạn để đảm bảo an toàn.
Trình bổ trợ dưới dạng thư viện dùng chung
Không giống như các thư viện tĩnh của Android được biên dịch trực tiếp vào ứng dụng, các thư viện dùng chung của Android được biên dịch vào một APK độc lập mà các ứng dụng khác tham chiếu trong thời gian chạy.
Các trình bổ trợ được triển khai dưới dạng thư viện dùng chung của Android sẽ tự động thêm các lớp vào trình tải lớp dùng chung giữa các ứng dụng. Khi một ứng dụng sử dụng thư viện giao diện người dùng trên ô tô chỉ định phần phụ thuộc thời gian chạy trên thư viện dùng chung của trình bổ trợ, trình tải lớp của ứng dụng đó có thể truy cập vào các lớp của thư viện dùng chung của trình bổ trợ. Các trình bổ trợ được triển khai dưới dạng ứng dụng Android thông thường (không phải thư viện dùng chung) có thể ảnh hưởng tiêu cực đến thời gian khởi động nguội của ứng dụng.
Triển khai và tạo thư viện dùng chung
Quá trình phát triển bằng thư viện dùng chung của Android tương tự như quá trình phát triển ứng dụng Android thông thường, chỉ có một vài điểm khác biệt chính.
- Sử dụng thẻ
librarytrong thẻapplicationcùng với tên gói trình bổ trợ trong tệp kê khai ứng dụng của trình bổ trợ:
<application>
<library android:name="com.chassis.car.ui.plugin" />
...
</application>
- Định cấu hình quy tắc tạo Soong
android_app(Android.bp) bằng cờ AAPTshared-libdùng để tạo thư viện dùng chung:
android_app {
...
aaptflags: ["--shared-lib"],
...
}
Phần phụ thuộc vào thư viện dùng chung
Đối với mỗi ứng dụng trên hệ thống sử dụng thư viện giao diện người dùng trên ô tô, hãy thêm thẻ
uses-library vào tệp kê khai ứng dụng trong thẻ
application cùng với tên gói trình bổ trợ:
<manifest>
<application
android:name=".MyApp"
...>
<uses-library android:name="com.chassis.car.ui.plugin" android:required="false"/>
...
</application>
</manifest>
Cài đặt trình bổ trợ
Bạn PHẢI cài đặt sẵn trình bổ trợ trên phân vùng hệ thống bằng cách thêm mô-đun
vào PRODUCT_PACKAGES. Bạn có thể cập nhật gói đã cài đặt sẵn tương tự như mọi ứng dụng cần cài đặt khác.
Nếu bạn đang cập nhật một trình bổ trợ hiện có trên hệ thống, thì mọi ứng dụng sử dụng trình bổ trợ đó sẽ tự động đóng. Sau khi người dùng mở lại, các ứng dụng đó sẽ có các thay đổi đã cập nhật. Nếu ứng dụng không chạy, thì lần tiếp theo ứng dụng này khởi động, ứng dụng sẽ có trình bổ trợ đã cập nhật.
Khi cài đặt trình bổ trợ bằng Android Studio, bạn cần cân nhắc thêm một số điểm. Tại thời điểm viết bài này, có một lỗi trong quá trình cài đặt ứng dụng Android Studio khiến các bản cập nhật cho trình bổ trợ không có hiệu lực. Bạn có thể khắc phục lỗi này bằng cách chọn tuỳ chọn Always install with package manager (disables deploy optimizations on Android 11 and later) (Luôn cài đặt bằng trình quản lý gói (vô hiệu hoá các chế độ tối ưu hoá việc triển khai trên Android 11 trở lên)) trong cấu hình bản dựng của trình bổ trợ.
Ngoài ra, khi cài đặt trình bổ trợ, Android Studio sẽ báo cáo lỗi rằng không tìm thấy hoạt động chính để khởi chạy. Đây là điều bình thường vì trình bổ trợ không có hoạt động nào (ngoại trừ ý định trống dùng để phân giải ý định). Để loại bỏ lỗi này, hãy thay đổi tuỳ chọn Launch (Khởi chạy) thành Nothing (Không có) trong cấu hình bản dựng.
Hình 1. Cấu hình trình bổ trợ Android Studio
Trình bổ trợ proxy
Để tuỳ chỉnh các ứng dụng sử dụng thư viện giao diện người dùng trên ô tô , bạn cần có một RRO nhắm đến từng ứng dụng cụ thể cần sửa đổi, kể cả khi các chế độ tuỳ chỉnh giống nhau trên các ứng dụng. Điều này có nghĩa là bạn cần có một RRO cho mỗi ứng dụng. Xem những ứng dụng sử dụng thư viện giao diện người dùng trên ô tô.
Trình bổ trợ proxy thư viện giao diện người dùng trên ô tô là một trình bổ trợ mẫu thư viện dùng chung uỷ quyền các phương thức triển khai thành phần cho phiên bản tĩnh của thư viện giao diện người dùng trên ô tô. Bạn có thể nhắm đến trình bổ trợ này bằng một RRO. RRO này có thể được dùng làm một điểm tuỳ chỉnh duy nhất cho các ứng dụng sử dụng thư viện giao diện người dùng trên ô tô mà không cần triển khai trình bổ trợ chức năng. Để biết thêm thông tin về RRO, hãy xem bài viết Thay đổi giá trị của tài nguyên ứng dụng trong thời gian chạy.
Trình bổ trợ proxy chỉ là một ví dụ và điểm xuất phát để tuỳ chỉnh bằng trình bổ trợ. Để tuỳ chỉnh ngoài RRO, bạn có thể triển khai một tập hợp con các thành phần trình bổ trợ và sử dụng trình bổ trợ proxy cho phần còn lại hoặc triển khai tất cả các thành phần trình bổ trợ hoàn toàn từ đầu.
Mặc dù trình bổ trợ proxy cung cấp một điểm tuỳ chỉnh RRO duy nhất cho các ứng dụng, nhưng các ứng dụng chọn không sử dụng trình bổ trợ này vẫn sẽ yêu cầu một RRO nhắm đến trực tiếp ứng dụng đó.
Triển khai API trình bổ trợ
Điểm truy cập chính vào trình bổ trợ là lớp com.android.car.ui.plugin.PluginVersionProviderImpl. Tất cả các trình bổ trợ phải có một lớp có chính xác tên và tên gói này. Lớp này phải có một hàm khởi tạo mặc định và triển khai giao diện PluginVersionProviderOEMV1.
Các trình bổ trợ CarUi phải hoạt động với các ứng dụng cũ hơn hoặc mới hơn trình bổ trợ. Để tạo điều kiện thuận lợi cho việc này, tất cả các API trình bổ trợ đều được phân phiên bản bằng V# ở cuối tên lớp. Nếu một phiên bản mới của thư viện giao diện người dùng trên ô tô được phát hành với các tính năng mới, thì các tính năng đó sẽ thuộc phiên bản V2 của thành phần. Thư viện giao diện người dùng trên ô tô sẽ cố gắng hết sức để các tính năng mới hoạt động trong phạm vi của thành phần trình bổ trợ cũ hơn.
Ví dụ: bằng cách chuyển đổi một loại nút mới trong thanh công cụ thành MenuItems.
Tuy nhiên, một ứng dụng có phiên bản cũ hơn của thư viện giao diện người dùng trên ô tô không thể thích ứng với một trình bổ trợ mới được viết dựa trên các API mới hơn. Để giải quyết vấn đề này, chúng tôi cho phép các trình bổ trợ trả về các phương thức triển khai khác nhau dựa trên phiên bản API của nhà sản xuất thiết bị gốc mà các ứng dụng hỗ trợ.
PluginVersionProviderOEMV1 có một phương thức trong đó:
Object getPluginFactory(int maxVersion, Context context, String packageName);
Phương thức này trả về một đối tượng triển khai phiên bản cao nhất của PluginFactoryOEMV# mà trình bổ trợ hỗ trợ, trong khi vẫn nhỏ hơn hoặc bằng maxVersion. Nếu một trình bổ trợ không có phương thức triển khai PluginFactory cũ, thì trình bổ trợ đó có thể trả về null. Trong trường hợp đó, phương thức triển khai được liên kết tĩnh của các thành phần CarUi sẽ được sử dụng.
Để duy trì khả năng tương thích ngược với các ứng dụng được biên dịch dựa trên các phiên bản cũ hơn của thư viện Car Ui tĩnh, bạn nên hỗ trợ maxVersion của 2, 5 trở lên trong phương thức triển khai lớp PluginVersionProvider của trình bổ trợ. Chúng tôi không hỗ trợ các phiên bản 1, 3 và 4. Để
biết thêm thông tin, hãy xem
PluginVersionProviderImpl.
PluginFactory là giao diện tạo tất cả các thành phần CarUi khác. Giao diện này cũng xác định phiên bản giao diện nào sẽ được sử dụng. Nếu trình bổ trợ không tìm cách triển khai bất kỳ thành phần nào trong số này, thì trình bổ trợ đó có thể trả về null trong hàm tạo (ngoại trừ thanh công cụ có hàm customizesBaseLayout() riêng).
pluginFactory giới hạn các phiên bản thành phần CarUi có thể được sử dụng cùng nhau. Ví dụ: sẽ không bao giờ có pluginFactory có thể tạo phiên bản 100 của Toolbar và cả phiên bản 1 của RecyclerView, vì không có gì đảm bảo rằng nhiều phiên bản thành phần sẽ hoạt động cùng nhau. Để sử dụng thanh công cụ phiên bản 100, nhà phát triển phải cung cấp phương thức triển khai phiên bản pluginFactory tạo thanh công cụ phiên bản 100. Phương thức này sẽ giới hạn các tuỳ chọn trên các phiên bản thành phần khác có thể được tạo. Các phiên bản thành phần khác có thể không bằng nhau, ví dụ: pluginFactoryOEMV100 có thể tạo ToolbarControllerOEMV100 và RecyclerViewOEMV70.
Thanh công cụ
Bố cục cơ bản
Thanh công cụ và "bố cục cơ bản" có mối quan hệ rất chặt chẽ, do đó, hàm tạo thanh công cụ có tên là installBaseLayoutAround. Bố cục cơ bản là một khái niệm cho phép đặt thanh công cụ ở bất kỳ vị trí nào xung quanh nội dung của ứng dụng, cho phép có thanh công cụ ở trên cùng/dưới cùng của ứng dụng, theo chiều dọc dọc theo các cạnh hoặc thậm chí là thanh công cụ hình tròn bao quanh toàn bộ ứng dụng. Điều này được thực hiện bằng cách truyền một thành phần hiển thị đến installBaseLayoutAround để thanh công cụ/bố cục cơ sở bao quanh.
Trình bổ trợ sẽ lấy thành phần hiển thị được cung cấp, tách thành phần đó khỏi thành phần hiển thị gốc, tăng bố cục riêng của trình bổ trợ ở cùng chỉ mục của thành phần hiển thị gốc và có cùng LayoutParams với thành phần hiển thị vừa tách, sau đó đính kèm lại thành phần hiển thị ở đâu đó bên trong bố cục vừa tăng. Bố cục đã tăng sẽ chứa thanh công cụ nếu ứng dụng yêu cầu.
Ứng dụng có thể yêu cầu bố cục cơ bản mà không có thanh công cụ. Nếu có, installBaseLayoutAround sẽ trả về giá trị rỗng. Đối với hầu hết các trình bổ trợ, đó là tất cả những gì cần thực hiện, nhưng nếu tác giả trình bổ trợ muốn áp dụng, chẳng hạn như trang trí xung quanh cạnh của ứng dụng, thì bạn vẫn có thể thực hiện việc đó bằng bố cục cơ bản. Các trang trí này đặc biệt hữu ích cho các thiết bị có màn hình không phải hình chữ nhật, vì chúng có thể đẩy ứng dụng vào không gian hình chữ nhật và thêm các chuyển đổi rõ ràng vào không gian không phải hình chữ nhật.
installBaseLayoutAround cũng được truyền một Consumer<InsetsOEMV1>. Bạn có thể sử dụng trình tiêu thụ này để thông báo cho ứng dụng rằng trình bổ trợ đang che phủ một phần nội dung của ứng dụng (bằng thanh công cụ hoặc cách khác). Sau đó, ứng dụng sẽ biết cách tiếp tục vẽ trong không gian này, nhưng giữ mọi thành phần quan trọng mà người dùng có thể tương tác ra khỏi không gian này. Hiệu ứng này được sử dụng trong thiết kế tham chiếu của chúng tôi để làm cho thanh công cụ bán trong suốt và có các danh sách cuộn bên dưới. Nếu tính năng này không được triển khai, thì mục đầu tiên trong danh sách sẽ bị kẹt bên dưới thanh công cụ và không nhấp được. Nếu không cần hiệu ứng này, trình bổ trợ có thể bỏ qua Trình tiêu thụ.
Hình 2. Nội dung cuộn bên dưới thanh công cụ
Theo quan điểm của ứng dụng, khi trình bổ trợ gửi các phần chèn mới, ứng dụng sẽ nhận các phần chèn đó từ mọi hoạt động hoặc mảnh triển khai InsetsChangedListener. Nếu
một hoạt động hoặc mảnh không triển khai InsetsChangedListener, thì thư viện Car Ui
sẽ xử lý các phần chèn theo mặc định bằng cách áp dụng các phần chèn làm phần đệm cho
Activity hoặc FragmentActivity chứa mảnh. Theo mặc định, thư viện không áp dụng các phần chèn cho các mảnh. Dưới đây là đoạn mã mẫu của phương thức triển khai áp dụng các phần chèn làm phần đệm trên RecyclerView trong ứng dụng:
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());
}
}
Cuối cùng, trình bổ trợ sẽ được cung cấp gợi ý fullscreen dùng để cho biết liệu thành phần hiển thị cần được bao bọc có chiếm toàn bộ ứng dụng hay chỉ một phần nhỏ.
Bạn có thể dùng gợi ý này để tránh áp dụng một số trang trí dọc theo cạnh mà chỉ có ý nghĩa nếu chúng xuất hiện dọc theo cạnh của toàn bộ màn hình. Ứng dụng mẫu sử dụng bố cục cơ bản không toàn màn hình là phần Cài đặt. Trong đó, mỗi ngăn của bố cục hai ngăn đều có thanh công cụ riêng.
Vì installBaseLayoutAround dự kiến sẽ trả về giá trị rỗng khi toolbarEnabled là false, nên để trình bổ trợ cho biết rằng trình bổ trợ không muốn tuỳ chỉnh bố cục cơ bản, trình bổ trợ phải trả về false từ customizesBaseLayout.
Bố cục cơ bản phải chứa FocusParkingView và FocusArea để hỗ trợ đầy đủ các nút điều khiển xoay. Bạn có thể bỏ qua các thành phần hiển thị này trên các thiết bị không hỗ trợ nút xoay. FocusParkingView/FocusAreas được triển khai trong thư viện CarUi tĩnh, vì vậy, setRotaryFactories được dùng để cung cấp các nhà máy tạo thành phần hiển thị từ ngữ cảnh.
Ngữ cảnh dùng để tạo thành phần hiển thị Tiêu điểm phải là ngữ cảnh nguồn, không phải ngữ cảnh của trình bổ trợ. FocusParkingView phải gần với thành phần hiển thị đầu tiên trong cây càng nhiều càng tốt, vì đây là thành phần được lấy tiêu điểm khi không có tiêu điểm nào hiển thị cho người dùng. FocusArea phải bao bọc thanh công cụ trong bố cục cơ bản để cho biết đây là vùng đẩy xoay. Nếu bạn không cung cấp FocusArea, người dùng sẽ không thể chuyển đến bất kỳ nút nào trong thanh công cụ bằng núm vặn điều khiển.
Bộ điều khiển thanh công cụ
Phương thức triển khai ToolbarController thực tế sẽ đơn giản hơn nhiều so với bố cục cơ bản. Phương thức này có nhiệm vụ lấy thông tin được truyền đến các trình thiết lập và hiển thị thông tin đó trong bố cục cơ bản. Hãy xem Javadoc để biết thông tin về hầu hết các phương thức. Một số phương thức phức tạp hơn sẽ được thảo luận bên dưới.
getImeSearchInterface được dùng để hiển thị kết quả tìm kiếm trong cửa sổ IME (bàn phím). Điều này có thể hữu ích để hiển thị/tạo hiệu ứng cho kết quả tìm kiếm cùng với bàn phím, chẳng hạn như nếu bàn phím chỉ chiếm một nửa màn hình. Hầu hết chức năng được triển khai trong thư viện CarUi tĩnh, giao diện tìm kiếm trong trình bổ trợ chỉ cung cấp các phương thức để thư viện tĩnh nhận các lệnh gọi lại TextView và onPrivateIMECommand. Để hỗ trợ việc này, trình bổ trợ nên sử dụng lớp con TextView ghi đè onPrivateIMECommand và truyền lệnh gọi đến trình nghe được cung cấp dưới dạng TextView của thanh tìm kiếm.
setMenuItems chỉ hiển thị MenuItems trên màn hình, nhưng sẽ được gọi thường xuyên một cách đáng ngạc nhiên. Vì API trình bổ trợ cho MenuItems là bất biến, nên bất cứ khi nào MenuItem thay đổi, một lệnh gọi setMenuItems hoàn toàn mới sẽ xảy ra. Điều này có thể xảy ra đối với một việc nhỏ nhặt như người dùng nhấp vào MenuItem chuyển đổi và thao tác nhấp đó khiến nút chuyển đổi bật/tắt. Vì lý do hiệu suất và hiệu ứng động, bạn nên tính toán sự khác biệt giữa danh sách MenuItems cũ và mới, đồng thời chỉ cập nhật các thành phần hiển thị thực sự thay đổi. MenuItems cung cấp trường key có thể giúp bạn thực hiện việc này, vì khoá phải giống nhau trên các lệnh gọi khác nhau đến setMenuItems cho cùng một MenuItem.
Ngữ cảnh
Trình bổ trợ phải cẩn thận khi sử dụng Ngữ cảnh, vì có cả ngữ cảnh trình bổ trợ và ngữ cảnh "nguồn". Ngữ cảnh trình bổ trợ được cung cấp dưới dạng đối số cho getPluginFactory và là ngữ cảnh duy nhất có tài nguyên của trình bổ trợ trong đó. Điều này có nghĩa là đây là ngữ cảnh duy nhất có thể được dùng để tăng bố cục trong trình bổ trợ.
Tuy nhiên, ngữ cảnh trình bổ trợ có thể không được đặt cấu hình chính xác. Để có được cấu hình chính xác, chúng tôi cung cấp ngữ cảnh nguồn trong các phương thức tạo thành phần. Ngữ cảnh nguồn thường là một hoạt động, nhưng trong một số trường hợp cũng có thể là Dịch vụ hoặc thành phần Android khác. Để sử dụng cấu hình từ ngữ cảnh nguồn với tài nguyên từ ngữ cảnh trình bổ trợ, bạn phải tạo ngữ cảnh mới bằng createConfigurationContext. Nếu bạn không sử dụng cấu hình chính xác, thì sẽ có lỗi vi phạm chế độ nghiêm ngặt của Android và các thành phần hiển thị đã tăng có thể không có kích thước chính xác.
Context layoutInflationContext = pluginContext.createConfigurationContext(
sourceContext.getResources().getConfiguration());
Thay đổi chế độ
Một số trình bổ trợ có thể hỗ trợ nhiều chế độ cho các thành phần của chúng, chẳng hạn như chế độ thể thao hoặc chế độ tiết kiệm năng lượng có giao diện khác biệt. CarUi không hỗ trợ sẵn chức năng này, nhưng không có gì ngăn trình bổ trợ triển khai chức năng này hoàn toàn ở bên trong. Trình bổ trợ có thể theo dõi mọi điều kiện mà trình bổ trợ muốn tìm hiểu thời điểm chuyển đổi chế độ, chẳng hạn như nghe các chương trình phát. Trình bổ trợ không thể kích hoạt thay đổi cấu hình để thay đổi chế độ, nhưng bạn không nên dựa vào các thay đổi về cấu hình, vì việc cập nhật giao diện của từng thành phần theo cách thủ công sẽ mượt mà hơn đối với người dùng và cũng cho phép các chuyển đổi không thể thực hiện được bằng các thay đổi về cấu hình.
Jetpack Compose
Bạn có thể triển khai trình bổ trợ bằng Jetpack Compose, nhưng đây là tính năng ở giai đoạn alpha và không nên coi là ổn định.
Trình bổ trợ có thể sử dụng
ComposeView
để tạo một vùng có hỗ trợ Compose để kết xuất. ComposeView này sẽ là thành phần được trả về từ ứng dụng từ phương thức getView trong các thành phần.
Một vấn đề chính khi sử dụng ComposeView là vấn đề này đặt các thẻ trên thành phần hiển thị gốc trong bố cục để lưu trữ các biến toàn cục được chia sẻ trên các ComposeView khác nhau trong hệ phân cấp. Vì mã tài nguyên của trình bổ trợ không được đặt trong không gian tên riêng biệt với mã tài nguyên của ứng dụng, nên điều này có thể gây ra xung đột khi cả ứng dụng và trình bổ trợ đều đặt thẻ trên cùng một thành phần hiển thị. `ComposeViewWithLifecycle` tuỳ chỉnh di chuyển các biến toàn cục này xuống `ComposeView` được cung cấp bên dưới.ComposeViewWithLifecycleComposeView Xin nhắc lại rằng bạn không nên coi đây là tính năng ổn định.
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)
// }
}