Trình bổ trợ giao diện người dùng trên ô tô

Sử dụng trình bổ trợ thư viện giao diện người dùng trên ô tô để tạo các hoạt động triển khai hoàn chỉnh của tính năng 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 thư viện giao diện người dùng trên ô tô, điều này giới hạn phạm vi bạn có thể 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 tệp APK chứa các lớp triển khai một nhóm API Trình bổ trợ. Bạn có thể biên dịch API Trình bổ trợ thành một trình bổ trợ dưới dạng thư viện tĩnh.

Xem ví dụ trong Soong và Gradle:

Soong

Hãy xem xét 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

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 trình cung cấp để có thể truy vấn trình cung cấp đó trong thời gian chạy. Ngoài ra, nếu thuộc tính enabled được đặt 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 trì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, để bảo mật, hãy Ký ứng dụng.

Trình bổ trợ dưới dạng thư viện dùng chung

Không giống như thư viện tĩnh Android được biên dịch trực tiếp vào ứng dụng, thư viện dùng chung Android được biên dịch thành một tệp 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 một phần phụ thuộc thời gian chạy trên thư viện chia sẻ 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 chia sẻ 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

Việc phát triển bằng thư viện dùng chung của Android cũng giống như phát triển ứng dụng Android thông thường, nhưng có một vài điểm khác biệt chính.

  • Sử dụng thẻ library trong thẻ application 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 bản dựng android_app Soong (Android.bp) bằng cờ AAPT shared-lib, dùng để tạo thư viện dùng chung:
android_app {
  ...
  aaptflags: ["--shared-lib"],
  ...
}

Phần phụ thuộc trên 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 ô tô, hãy thêm thẻ uses-library vào tệp kê khai ứng dụng trong thẻ application 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 đưa mô-đun vào PRODUCT_PACKAGES. Bạn có thể cập nhật gói được cài đặt sẵn tương tự như mọi ứng dụng đã 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, họ sẽ thấy 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 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ều. Tại thời điểm viết bài, 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 vấn đề 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 (tắt tính năng tối ưu hoá 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 không tìm thấy hoạt động chính để khởi chạy. Điều này là dự kiến 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ó gì) trong cấu hình bản dựng.

Cấu hình trình bổ trợ Android Studio Hình 1. Cấu hình trình bổ trợ Android Studio

Trình bổ trợ proxy

Việc tuỳ chỉnh ứng dụng bằng thư viện Giao diện người dùng ô tô yêu cầu một RRO nhắm đến từng ứng dụng cụ thể cần sửa đổi, bao gồm cả khi các tuỳ chỉnh giống nhau trên các ứng dụng. Điều này có nghĩa là bạn bắt buộc phải có một RRO cho mỗi ứng dụng. Xem những ứng dụng nào sử dụng thư viện Giao diện người dùng ô tô.

Trình bổ trợ proxy thư viện giao diện người dùng trên ô tô là một thư viện dùng chung trình bổ trợ mẫu uỷ quyền việ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 mục tiêu trình bổ trợ này bằng RRO. RRO 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 phần 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ợ 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 trực tiếp nhắm đến chính ứ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ả trình bổ trợ phải bao gồm một lớp có tên và tên gói chính xác như vậy. Lớp này phải có hàm khởi tạo mặc định và triển khai giao diện PluginVersionProviderOEMV1.

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ợ. Để hỗ trợ việc này, tất cả API trình bổ trợ đều được tạo 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 ô 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 một thành phần trình bổ trợ cũ. 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 thư viện Giao diện người dùng ô tô cũ hơn 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 OEM mà ứng dụng hỗ trợ.

PluginVersionProviderOEMV1 có một phương thức:

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 PluginFactoryOEMV# cao nhất 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 triển khai PluginFactory cũ, thì trình bổ trợ đó có thể trả về null. Trong trường hợp này, quá trình triển khai 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 theo các phiên bản cũ của thư viện Giao diện người dùng ô tô tĩnh, bạn nên hỗ trợ maxVersion từ 2, 5 trở lên trong quá trình triển khai trình bổ trợ của lớp PluginVersionProvider. Phiên bản 1, 3 và 4 không được hỗ trợ. Để 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. Tệp này cũng xác định phiên bản giao diện nào sẽ được sử dụng. Nếu không tìm cách triển khai bất kỳ thành phần nào trong số này, trình bổ trợ có thể trả về null trong hàm tạo của các thành phần đó (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à phiên bản 1 của RecyclerView, vì sẽ 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 cần cung cấp cách triển khai phiên bản pluginFactory để tạo thanh công cụ phiên bản 100. Sau đó, phiên bản 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ể tạo. Các phiên bản của các thành phần khác có thể không bằng nhau, ví dụ: pluginFactoryOEMV100 có thể tạo ToolbarControllerOEMV100RecyclerViewOEMV70.

Thanh công cụ

Bố cục cơ sở

Thanh công cụ và "bố cục cơ sở" có liên quan chặt chẽ với nhau, do đó, hàm tạo thanh công cụ được gọi là installBaseLayoutAround. Bố cục cơ sở là một khái niệm cho phép thanh công cụ được đặt ở bất kỳ vị trí nào xung quanh nội dung của ứng dụng, để cho phép thanh công cụ nằm ở đầu/đuôi ứ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 bọc xung 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 hiển thị đó khỏi thành phần mẹ, tăng cường bố cục của chính trình bổ trợ trong cùng một chỉ mục của thành phần mẹ và với cùng một LayoutParams như thành phần hiển thị vừa được tách, sau đó đính kèm lại thành phần hiển thị ở một vị trí nào đó bên trong bố cục vừa được tăng cường. Bố cục tăng cườ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ơ sở 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 xảy ra, nhưng nếu tác giả trình bổ trợ muốn áp dụng, ví dụ: một phần trang trí xung quanh cạnh của ứng dụng, thì bạn vẫn có thể thực hiện việc này bằng bố cục cơ sở. Các phần 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 hiệu ứng 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 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 tiếp tục vẽ trong không gian này, nhưng loại bỏ mọi thành phần quan trọng mà người dùng có thể tương tác. Hiệu ứng này được sử dụng trong thiết kế tham khảo của chúng tôi để làm cho thanh công cụ có độ mờ và có các danh sách cuộn bên dưới. Nếu bạn không triển khai tính năng này, mục đầu tiên trong danh sách sẽ bị kẹt bên dưới thanh công cụ và không thể nhấp vào. Nếu không cần hiệu ứng này, trình bổ trợ có thể bỏ qua Consumer.

Nội dung cuộn bên dưới thanh công cụ Hình 2. Nội dung cuộn bên dưới thanh công cụ

Từ quan điểm của ứng dụng, khi trình bổ trợ gửi các phần lồng ghép mới, trình bổ trợ sẽ nhận được các phần lồng ghép đó 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 lồng ghép theo mặc định bằng cách áp dụng các phần lồng ghép làm khoảng đệm cho Activity hoặc FragmentActivity chứa mảnh đó. Theo mặc định, thư viện không áp dụng phần lồng ghép cho các mảnh. Dưới đây là đoạn mã mẫu về cách triển khai áp dụng phần lồng ghép làm khoảng đệ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ẽ nhận được gợi ý fullscreen. Gợi ý này dùng để cho biết liệu thành phần hiển thị cần được gói có chiếm toàn bộ ứng dụng hay chỉ một phần nhỏ. Bạn có thể dùng tính năng này để tránh áp dụng một số phần trang trí dọc theo cạnh 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. Một ứng dụng mẫu sử dụng bố cục cơ sở không toàn màn hình là Cài đặt, trong đó mỗi ngăn của bố cục ngăn đôi có thanh công cụ riêng.

Vì dự kiến installBaseLayoutAround sẽ trả về giá trị rỗng khi toolbarEnabledfalse, để trình bổ trợ cho biết rằng trình bổ trợ không muốn tuỳ chỉnh bố cục cơ sở, trình bổ trợ phải trả về false từ customizesBaseLayout.

Bố cục cơ sở phải chứa FocusParkingViewFocusArea để 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ợ màn hình xoay. FocusParkingView/FocusAreas được triển khai trong thư viện CarUi tĩnh, vì vậy, setRotaryFactories được dùng để cung cấp nhà máy tạo để 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ị Focus phải là ngữ cảnh nguồn, chứ không phải ngữ cảnh của trình bổ trợ. FocusParkingView phải gần nhất với thành phần hiển thị đầu tiên trong cây một cách hợp lý nhất có thể, vì đó là thành phần được lấy làm tiêu điểm khi người dùng không thấy tiêu điểm nào. FocusArea phải gói thanh công cụ trong bố cục cơ sở để cho biết đó là vùng đẩy xoay. Nếu không cung cấp FocusArea, người dùng sẽ không thể điều hướng đến bất kỳ nút nào trong thanh công cụ bằng bộ điều khiển xoay.

Trình điều khiển thanh công cụ

ToolbarController thực tế được trả về sẽ dễ triển khai hơn nhiều so với bố cục cơ sở. Nhiệm vụ của lớp này là lấy thông tin được truyền đến phương thức setter và hiển thị thông tin đó trong bố cục cơ sở. 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 được thảo luận dưới đây.

getImeSearchInterface 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 khi hiển thị/tạo ảnh độ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 cho thư viện tĩnh để nhận lệnh gọi lại TextViewonPrivateIMECommand. Để hỗ trợ việc này, trình bổ trợ phải sử dụng một 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 MenuItem không thể thay đổi, nên mỗi khi một 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 với một thao tác nhỏ như người dùng nhấp vào một MenuItem nút chuyển và thao tác nhấp đó đã khiến nút chuyển bật/tắt. Vì cả lý do hiệu suất và ảnh độ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 những thành phần hiển thị thực sự thay đổi. MenuItems cung cấp một trường key có thể giúp giải quyết vấn đề này, vì khoá phải giống nhau trên các lệnh gọi đến setMenuItems cho cùng một MenuItem.

AppStyledView

AppStyledView là vùng chứa cho một thành phần hiển thị không được tuỳ chỉnh. Bạn có thể sử dụng lớp này để tạo đường viền xung quanh thành phần hiển thị đó, giúp thành phần hiển thị đó nổi bật so với phần còn lại của ứng dụng và cho người dùng biết rằng đây là một loại giao diện khác. Thành phần hiển thị được AppStyledView bao bọc được cung cấp trong setContent. AppStyledView cũng có thể có nút quay lại hoặc đóng như yêu cầu của ứng dụng.

AppStyledView không chèn các thành phần hiển thị của nó vào hệ phân cấp thành phần hiển thị ngay lập tức như installBaseLayoutAround, mà chỉ trả về thành phần hiển thị của nó cho thư viện tĩnh thông qua getView, sau đó thực hiện thao tác chèn. Bạn cũng có thể kiểm soát vị trí và kích thước của AppStyledView bằng cách triển khai getDialogWindowLayoutParam.

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à "nguồn". Bối cảnh trình bổ trợ được cung cấp dưới dạng đối số cho getPluginFactory và là bối cảnh duy nhất có tài nguyên của trình bổ trợ. Điều này có nghĩa là đây là ngữ cảnh duy nhất có thể dùng để tăng cường bố cục trong trình bổ trợ.

Tuy nhiên, ngữ cảnh trình bổ trợ có thể không được thiết lập cấu hình chính xác. Để có cấu hình chính xác, chúng ta 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à một 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 một 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, 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ườ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 trình bổ trợ, chẳng hạn như chế độ thể thao hoặc chế độ tiết kiệm có giao diện khác biệt. Không có tính năng hỗ trợ tích hợp cho chức năng như vậy trong CarUi, nhưng không có gì ngăn cản trình bổ trợ triển khai chức năng đó hoàn toàn trong nội bộ. Trình bổ trợ có thể theo dõi bất kỳ điều kiện nào 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 thông báo truyền tin. 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 chuyển đổi không thể thực hiện được với 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à một tính năng cấp alpha và không được coi là ổn định.

Trình bổ trợ có thể sử dụng ComposeView để tạo một nền tảng hỗ trợ Compose để kết xuất. ComposeView này sẽ là giá trị được trả về cho ứng dụng từ phương thức getView trong các thành phần.

Một vấn đề lớn khi sử dụng ComposeView là việc đặt 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 nhiều ComposeViews trong hệ phân cấp. Vì mã nhận dạng tài nguyên của trình bổ trợ không được phân vùng riêng biệt với mã nhận dạng 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ợ đặt thẻ trên cùng một thành phần hiển thị. Dưới đây là một ComposeViewWithLifecycle tuỳ chỉnh di chuyển các biến toàn cục này xuống ComposeView. Xin nhắc lại rằng đây không phải là phiên bản ổ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)
//  }
}