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

Sử dụng các trình bổ trợ thư viện Giao diện người dùng trên ô tô để tạo hoạt động triển khai hoàn chỉnh cho các thành phần các tính năng tuỳ chỉnh 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 trong thời gian chạy (RRO). RRO cho phép bạn chỉ thay đổi các tài nguyên XML của thư viện Giao diện người dùng cho ô tô thành phần, giới hạn phạm vi nội dung 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 cho ô tô là một tệp APK chứa các lớp triển khai một tập hợp API trình bổ trợ. API Trình bổ trợ có thể được biên dịch thành một plugin dưới dạng thư viện tĩnh.

Hãy xem ví dụ trong Soong và Gradle:

Tiếng Soong

Hãy xem ví dụ về phiên bản Soong sau đây:

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ợ này phải có một trình cung cấp nội dung được khai báo trong tệp kê khai có phần tử 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 người dùng dễ dàng tìm thấy trình bổ trợ này vào thư viện Giao diện người dùng trên ô tô. Bạn phải xuất trình cung cấp này để có thể truy vấn nhà cung cấp này tại thời gian chạy. Ngoài ra, nếu bạn đặt thuộc tính enabled thành false thì giá trị mặc định phương thức triển khai này sẽ được dùng thay vì triển khai trình bổ trợ. Nội dung lớp nhà cung cấp không phải tồn tại. Trong trường hợp đó, hãy nhớ thêm tools:ignore="MissingClass" cho định nghĩa trình cung cấp. Xem mẫu mục kê khai 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, như một biện pháp bảo mật, Ký ứng dụng.

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 thành ứ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 được tham chiếu bởi các ứng dụng khác trong thời gian chạy.

Các trình bổ trợ được triển khai dưới dạng thư viện chia sẻ Android có các lớp riêng tự động được thêm 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ẻ trình bổ trợ, trình tải lớp có thể truy cập vào các lớp của thư viện chia sẻ của trình bổ trợ. Đã triển khai trình bổ trợ như các ứ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 tình trạng nguội ứng dụng thời gian bắt đầu.

Triển khai và xây dựng thư viện dùng chung

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

  • Dùng thẻ library trong thẻ application cùng với gói trình bổ trợ tên 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 Soong android_app (Android.bp) với AAPT gắn cờ 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 cho ô tô, hãy thêm phần tử uses-library trong tệp kê khai ứng dụng trong Thẻ application có 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ợ

Trình bổ trợ PHẢI được cài đặt trước trên phân vùng hệ thống bằng cách thêm mô-đun trong PRODUCT_PACKAGES. Gói cài đặt sẵn có thể được cập nhật tương tự như bất kỳ ứng dụng đã cài đặt nào khác.

Nếu bạn đang cập nhật trình bổ trợ hiện có trên hệ thống, mọi ứng dụng đang sử dụng trình bổ trợ đó tự động đóng. Sau khi người dùng mở lại, họ sẽ có các thay đổi được cập nhật. Nếu ứng dụng không chạy, vào lần khởi động tiếp theo, ứng dụng sẽ có .

Khi cài đặt trình bổ trợ với Android Studio, sẽ có một số cần cân nhắc. Tại thời điểm viết, có một lỗi trong quy trình cài đặt ứng dụng Android Studio dẫn đến việc cập nhật một trình bổ trợ không có hiệu lực. Điều này có thể được khắc phục bằng cách chọn tuỳ chọn Luôn cài đặt bằng trình quản lý gói (tắt tính năng triển khai tính năng tối ưu hoá 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ợ này, Android Studio sẽ báo cáo một lỗi không tìm thấy một hoạt động chính để khởi chạy. Điều này nằm trong dự kiến, vì trình bổ trợ không có bất kỳ hoạt động nào (ngoại trừ ý định trống dùng để phân giải một ý định). Người nhận loại bỏ lỗi này, hãy thay đổi tuỳ chọn Launch (Khởi chạy) thành None (Không có) trong 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

Tuỳ chỉnh ứng dụng dùng thư viện Giao diện người dùng trên ô tô yêu cầu RRO nhắm mục tiêu đến từng ứng dụng cụ thể cần được sửa đổi, kể cả khi các yếu tố tuỳ chỉnh giống hệt nhau trên các ứng dụng. Điều này có nghĩa là RRO trên mỗi bắt buộc. Xem ứng dụng nào 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 ví dụ thư viện chia sẻ trình bổ trợ uỷ quyền triển khai thành phần cho thư viện tĩnh phiên bản thư viện Giao diện người dùng trên ô tô. Trình bổ trợ này có thể được nhắm mục tiêu bằng RRO, có thể là được dùng làm điểm tuỳ chỉnh duy nhất cho các ứng dụng 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, xem phần Thay đổi giá trị của tài nguyên ứng dụng tại thời gian chạy.

Plugin proxy chỉ là một ví dụ và là điểm bắt đầu để thực hiện tùy chỉnh bằng một trình bổ trợ. Để tuỳ chỉnh ngoài RRO, bạn có thể triển khai một tập hợp con trình bổ trợ các thành phần khác và sử dụng plugin proxy cho phần còn lại hoặc triển khai tất cả các plugin thành phần hoàn toàn từ đầu.

Mặc dù plugin proxy cung cấp một điểm tuỳ chỉnh RRO duy nhất cho ứng dụng, các ứng dụng chọn không sử dụng trình bổ trợ vẫn sẽ yêu cầu RRO trực tiếp nhắm mục tiêu đến chính ứng dụng đó.

Triển khai các API trình bổ trợ

Điểm truy cập chính cho trình bổ trợ là Lớp com.android.car.ui.plugin.PluginVersionProviderImpl. Tất cả trình bổ trợ phải hãy thê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ó 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ợ. Người nhận 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 thư viện Giao diện người dùng ô tô ra mắt phiên bản mới kèm theo các tính năng mới, chúng là một phần của phiên bản V2 của thành phần. Thư viện giao diện người dùng trên ô tô thực hiện tốt nhất để làm cho 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ũ. Ví dụ: bằng cách chuyển đổi một loại nút mới trên thanh công cụ thành MenuItems.

Tuy nhiên, ứng dụng có phiên bản cũ của thư viện Giao diện người dùng ô tô không thể thích ứng vớ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 trình bổ trợ trả về nhiều cách triển khai của chính nó dựa trên phiên bản API OEM được ứng dụng hỗ trợ.

PluginVersionProviderOEMV1 chứa 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 cao nhất của PluginFactoryOEMV# được trình bổ trợ hỗ trợ, mặc dù vẫn thấp hơn hoặc bằng maxVersion. Nếu một trình bổ trợ không triển khai PluginFactory cũ, giá trị này có thể trả về null, trong trường hợp đó phương thức tĩnh được liên kết triển khai các thành phần CarUi.

Để 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 phiên bản cũ hơn của thư viện Car UI tĩnh, bạn nên hỗ trợ maxVersion là 2, 5 trở lên trong quá trình triển khai trình bổ trợ lớp PluginVersionProvider. Phiên bản 1, 3 và 4 không được hỗ trợ. Cho thông tin khác, xem PluginVersionProviderImpl.

PluginFactory là giao diện tạo tất cả CarUi khác thành phần. Tệp này cũng xác định phiên bản giao diện 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, nó có thể trả về null trong chức năng tạo của mình (ngoại trừ thanh công cụ có một hàm customizesBaseLayout() riêng).

pluginFactory giới hạn những phiên bản thành phần CarUi có thể sử dụng khi kết hợp cùng nhau. Ví dụ: sẽ không bao giờ có một pluginFactory có thể tạo phiên bản 100 của Toolbar và phiên bản 1 của RecyclerView, như ở đó sẽ ít có thể đảm bảo rằng nhiều phiên bản thành phần khác nhau sẽ làm việc cùng nhau. Để sử dụng thanh công cụ phiên bản 100, nhà phát triển cần phải cung cấp phương thức triển khai phiên bản pluginFactory sẽ tạo thanh công cụ phiên bản 100, sau đó giới hạn các tuỳ chọn trên các phiên bản của thành phần có thể được tạo. Phiên bản của các thành phần khác có thể không được 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 rất chặt chẽ, do đó hàm này để tạo thanh công cụ có tên là installBaseLayoutAround. Chiến lược phát hành đĩa đơn bố cục cơ sở là một khái niệm cho phép thanh công cụ được đặt ở bất cứ đâu xung quanh ứng dụng nội dung, để cho phép thanh công cụ chạy dọc ở đầu/cuối ứng dụng dọc theo các cạnh hoặc thậm chí là một thanh công cụ hình tròn bao quanh toàn bộ ứng dụng. Đây là được thực hiện bằng cách chuyển thành phần hiển thị đến installBaseLayoutAround cho thanh công cụ/cơ sở để bao bọc.

Trình bổ trợ sẽ lấy khung hiển thị được cung cấp, tách khỏi khung hiển thị gốc, tăng cường bố cục riêng của 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 làm thành phần hiển thị vừa được tách rời, rồi đính kèm lại thành phần hiển thị này ở đâu đó 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ở mà không cần thanh công cụ. Nếu có, installBaseLayoutAround phải 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 làm cần xảy ra, nhưng nếu tác giả trình bổ trợ muốn áp dụng, ví dụ: trang trí xung quanh cạnh ứng dụng, bạn vẫn có thể thực hiện việc đó với bố cục cơ sở. Các trang trí đặ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ì họ 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 tiếp rõ ràng vào không gian không phải hình chữ nhật.

installBaseLayoutAround cũng nhận được một Consumer<InsetsOEMV1>. Chiến dịch này đối tượng tiêu dùng có thể được dùng để cho ứng dụng biết rằng trình bổ trợ chỉ sử dụng một phần che khuất nội dung của ứng dụng (bằng thanh công cụ hoặc cách khác). Ứng dụng này sẽ thì cần tiếp tục vẽ trong không gian này, nhưng vẫn giữ mọi tính năng tối quan trọng mà người dùng có thể tương tác thành phần khác. Hiệu ứng này được dùng trong thiết kế tham chiếu để làm cho thanh công cụ có bán trong suốt và có danh sách cuộn bên dưới. Nếu tính năng này chưa được triển khai, 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 vào được. Nếu không cần hiệu ứng này, trình bổ trợ có thể bỏ qua Người tiêu dùng.

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

Từ góc độ của ứng dụng, khi trình bổ trợ gửi các phần lồng ghép mới, nó sẽ nhận chúng qua 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, Giao diện người dùng ô tô thư viện 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. Thư viện không áp dụng phần lồng ghép theo mặc định cho các mảnh. Dưới đây là một đoạn mã mẫu của phương thức triển khai áp dụng các 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ợ này sẽ được cung cấp một gợi ý fullscreen để cho biết liệu khung hiển thị cần được bao bọc sẽ chiếm toàn bộ ứng dụng hoặc 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ố đồ 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 mẫu ứng dụng 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 kép có thanh công cụ riêng.

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

Bố cục cơ sở phải chứa FocusParkingViewFocusArea để đầy đủ hỗ trợ điều khiển xoay. Bạn có thể bỏ qua các chế độ xem này trên các thiết bị không hỗ trợ xoay. FocusParkingView/FocusAreas được triển khai trong thư viện CarUi tĩnh, nên setRotaryFactories được dùng để cung cấp cho các nhà máy tạo chế độ xem từ ngữ cảnh.

Ngữ cảnh dùng để tạo Chế độ xem tập trung phải là ngữ cảnh nguồn, chứ không phải là 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 nhất vào cây một cách hợp lý nhất có thể, vì đó là yếu tố được tập trung vào khi cần người dùng không nhìn thấy tiêu điểm. FocusArea phải gói thanh công cụ trong bố cục cơ sở để cho biết đó là vùng nhắc nhở hoạt động xoay. Nếu FocusArea không phải là được cung cấp, người dùng không thể điều hướng đến bất kỳ nút nào trên thanh công cụ bằng núm vặn điều khiển.

Bộ điều khiển thanh công cụ

ToolbarController thực tế được trả về sẽ đơn giản hơn nhiều để triển khai so với bố cục cơ sở. Nhiệm vụ của mã này là đưa thông tin đến phương thức setter và hiển thị phương thức này trong bố cục cơ sở. Xem Javadoc để biết thông tin về hầu hết các phương pháp. Một số phương pháp phức tạp hơn sẽ được thảo luận dưới đây.

getImeSearchInterface dùng để hiện kết quả tìm kiếm trong IME (bàn phím) cửa sổ. Đ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, ví dụ: 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 này được triển khai trong thư viện CarUi tĩnh, công cụ tìm kiếm giao diện trong trình bổ trợ chỉ cung cấp các phương thức cho thư viện tĩnh để lấy Lệnh gọi lại TextViewonPrivateIMECommand. Để hỗ trợ điều này, trình bổ trợ nên sử dụng một lớp con TextView ghi đè onPrivateIMECommand và truyền lệnh gọi đến trình nghe đã cung cấp làm 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 đến mức bất ngờ. Vì API trình bổ trợ cho MenuItems là không thể thay đổi, bất cứ khi nào một Mục trình đơn đã thay đổi, một lệnh gọi setMenuItems hoàn toàn mới sẽ diễn ra. Điều này có thể xảy ra cho một việc không đáng kể như khi người dùng nhấp vào một mục trình đơn chuyển đổi và điều đó nhấp vào khiến nút chuyển này chuyển sang trạng thái bật/tắt. Vì cả lý do liên quan đến hiệu suất và ảnh động, do đó, bạn nên tính toán sự khác biệt giữa quảng cáo cũ và mới Danh sách MenuItems và chỉ cập nhật các thành phần hiển thị thực sự thay đổi. Mục trong trình đơn hãy cung cấp trường key có thể trợ giúp 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 Mục trình đơn.

Chế độ xem AppStyled

AppStyledView là vùng chứa một khung hiển thị không được tuỳ chỉnh. Nó có thể được dùng để 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 phần còn lại của ứng dụng và chỉ cho người dùng rằng đây là một loại . Thành phần hiển thị được gói bằng AppStyledView được cung cấp trong setContent. AppStyledView cũng có thể có nút quay lại hoặc nút đóng như mà ứng dụng yêu cầu.

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

Ngữ cảnh

Trình bổ trợ này phải cẩn thận khi sử dụng Ngữ cảnh, vì có cả trình bổ trợ và "nguồn" ngữ cảnh. Ngữ cảnh trình bổ trợ được cung cấp dưới dạng một đối số cho getPluginFactory và là ngữ cảnh duy nhất có trình bổ trợ trong đó. Đây có nghĩa là đây là ngữ cảnh duy nhất có thể được sử 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 đúng cấu hình. Người nhận có được cấu hình chính xác, chúng ta sẽ 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ó thể cũng 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 cùng với tài nguyên từ ngữ cảnh trình bổ trợ, thì ngữ cảnh mới phải là được tạo bằng createConfigurationContext. Nếu cấu hình không chính xác thì sẽ vi phạm chế độ nghiêm ngặt của Android và chế độ xem tăng cường có thể không có phương diện chính xác.

Context layoutInflationContext = pluginContext.createConfigurationContext(
        sourceContext.getResources().getConfiguration());

Các thay đổi về 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ế độ sinh thái trông khác biệt về mặt trực quan. Không có hỗ trợ tích hợp sẵn cho chức năng như vậy trong CarUi, nhưng không có gì dừng lại trình bổ trợ triển khai hoàn toàn nội bộ. Trình bổ trợ có thể giám sát bất kỳ điều kiện nào nó muốn xác định thời điểm chuyển đổi chế độ, chẳng hạn như đang 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 dù sao thì việc cập nhật giao diện của mỗi thành phần theo cách thủ công sẽ mượt mà hơn cho 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

Có thể triển khai trình bổ trợ bằng Jetpack Compose, nhưng đây là cấp độ alpha và không nên được xem là ổn định.

Các trình bổ trợ có thể sử dụng ComposeView để tạo một bề mặt hỗ trợ Compose để kết xuất. ComposeView này sẽ là dữ liệu được trả về từ ứ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à đặt thẻ trong chế độ xem gốc trong bố cục để lưu trữ các biến toàn cục được chia sẻ qua ComposeView khác nhau trong hệ phân cấp. Vì mã tài nguyên của trình bổ trợ không phải là không gian tên tách biệt với ứng dụng, điều này có thể gây ra xung đột khi cả hai và trình bổ trợ đặt các thẻ trên cùng một chế độ xem. Tùy chỉnh ComposeViewWithLifecycle để chuyển các biến toàn cục này xuống ComposeView được cung cấp dưới đây. Xin nhắc lại rằng bạn không nên xem thuộc tính này là ổ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)
//  }
}