Nền tảng ứng dụng

Mở rộng quy mô ứng dụng cho mọi màn hình bằng Compose

Loa

Giới thiệu

Giới thiệu về bài thuyết trình

Hệ sinh thái Android rất rộng lớn, người dùng có thể chuyển đổi liền mạch giữa điện thoại, máy tính bảng, thiết bị gập và môi trường máy tính. Giờ đây, khi chúng ta ưu tiên Compose, phiên này sẽ khám phá cách thiết kế ứng dụng cho các cửa sổ có thể đổi kích thước và tỷ lệ khung hình đa dạng – nơi các giả định bố cục truyền thống bị phá vỡ, dẫn đến giao diện người dùng bị kéo giãn và mất trạng thái.

Khám phá cách các tính năng mới nhất trong Jetpack Compose, Navigation 3 và WindowManager có thể giúp bạn xây dựng bố cục động, tự động điều chỉnh theo tỷ lệ từ màn hình nhỏ gọn đến màn hình hai ngăn. Bạn sẽ nắm được các chiến lược để đẩy nhanh quy trình phát triển bằng Gemini và các kỹ năng của chúng tôi, chẳng hạn như kỹ năng thích ứng, để tự động tái cấu trúc cơ sở mã cũ cho màn hình lớn hơn.

Tóm tắt

  • Nền tảng Android yêu cầu ứng dụng thích ứng vì hệ thống bỏ qua các chế độ khoá hướng và tỷ lệ khung hình trên màn hình lớn kể từ Android 16, đồng thời loại bỏ các lựa chọn không tham gia trong tệp kê khai kể từ Android 17.
  • API MediaQuery thử nghiệm theo dõi một cách rõ ràng các chức năng giao diện người dùng đang hoạt động, chẳng hạn như kích thước cửa sổ và tư thế gập. Hàm này thay thế việc kết hợp nhiều trình quản lý nền tảng cũ thành trạng thái Compose.
  • Các thành phần cơ bản ổn định Grid và FlexBox cung cấp các thành phần bố cục thích ứng. Grid cho phép sắp xếp nhiều chiều theo tham số trong một composable, còn FlexBox cung cấp tính năng phân phối và gói không gian một chiều.
  • Jetpack Navigation 3 cấu trúc các luồng nhiều ngăn ở cấp ứng dụng xung quanh trạng thái và cảnh. Chiến lược cảnh tuỳ chỉnh đánh giá các chức năng giao diện người dùng và lớp kích thước cửa sổ để chuyển đổi linh hoạt giữa bản trình bày một ngăn và nhiều ngăn.
  • Kỹ năng Thích ứng của Compose cho Gemini tự động di chuyển ứng dụng sang các thành phần thích ứng hiện đại. Kỹ năng mới này sẽ cải tiến bố cục, giải quyết lỗi mất trạng thái và định cấu hình các phương thức nhập trên máy tính, chẳng hạn như phím tắt và con trỏ.

Tài nguyên

Gemini Notebook

Chủ đề

  • jetpack-compose
  • adaptive-layouts
  • mediaquery
  • bố cục dạng lưới
  • flexbox
  • navigation-3
  • android-skills

Bản chép lời

Giới thiệu về cách điều chỉnh ứng dụng theo màn hình

  • Francesco Romano

    Chào các bạn.

  • Hôm nay, chúng ta sẽ tìm hiểu sâu về kỹ thuật điều chỉnh ứng dụng cho phù hợp với mọi màn hình bằng Jetpack Compose.

  • Tại sao cần điều chỉnh tỷ lệ cho mọi màn hình?

  • Vì hệ sinh thái Android đã mở rộng trên nhiều kiểu dáng khác nhau.

  • APK của bạn có thể chạy trên điện thoại, thiết bị có thể gập lại, máy tính bảng, Chromebook và Googlebook, TV, hệ thống thông tin giải trí trên ô tô, thiết bị XR, v.v.

  • Vì vậy, điều đầu tiên là tiếp cận người dùng ở nơi họ đang ở.

  • Thứ hai, sự chuyển dịch của nền tảng Android sang các ứng dụng thích ứng không phải là tạm thời.

  • Chúng ta sẽ không quay lại thời kỳ chỉ dùng điện thoại, mà AI sẽ tiếp tục phát triển.

  • Và tôi có một dòng thời gian nhanh ở đây.

  • Năm 2022, chúng tôi bắt đầu đặt nền móng.

  • Chúng tôi đã ra mắt Android 12L, bản phát hành đầu tiên của Jetpack WindowManager, cung cấp cho bạn các API như lớp kích thước cửa sổ và xuất bản phiên bản đầu tiên của nguyên tắc về chất lượng ứng dụng thích ứng.

  • Sau đó, chúng tôi cũng cập nhật Google Play để điểm xếp hạng được chia theo loại thiết bị, đồng thời bắt đầu đưa nguyên tắc về chất lượng ứng dụng thích ứng vào các quyết định xếp hạng.

  • Trong Android 15, chúng tôi cũng đã đưa tính năng cửa sổ kiểu máy tính vào AOSP và phát hành phiên bản ổn định của Compose Material 3 Adaptive.

  • Sau đó, kể từ Android 16, SDK 36, hệ thống sẽ bỏ qua các khoá hướng, khoá tỷ lệ khung hình, cờ khả năng đổi kích thước trên màn hình lớn.

  • Cùng với việc phát hành màn hình kết nối, giờ đây, tệp APK của bạn có thể chạy ở hầu hết mọi kích thước cửa sổ.

  • SDK 36 cung cấp cho bạn một cờ tệp kê khai tạm thời để chọn không sử dụng tính năng ghi đè hướng và tỷ lệ khung hình.

  • Trong Android 17, lựa chọn không tham gia đó sẽ không còn nữa.

  • Vì vậy, việc xây dựng ứng dụng thích ứng là một yêu cầu cơ bản của nền tảng.

  • Trong thập kỷ qua, chúng tôi đã hợp tác chặt chẽ với các nhóm trong hệ sinh thái và hiểu rõ lý do khiến việc xây dựng ứng dụng thích ứng có thể gặp khó khăn.

  • Bạn bị mất một số trạng thái, API cửa sổ bị phân mảnh, tín hiệu không rõ ràng về thời điểm chuyển đổi bố cục và một số giả định cũ được đưa vào sâu trong toàn bộ mã nguồn.

  • Vì vậy, chúng tôi đã lấy mọi thứ học được từ những thách thức của nhà phát triển trong thập kỷ này và tích hợp trực tiếp vào hai nơi: thứ nhất, vào chính bộ công cụ giao diện người dùng.

  • Hôm nay, chúng ta sẽ tìm hiểu một số tính năng mới quan trọng trong Jetpack Compose: truy vấn phương tiện, lưới, FlexBox và Navigation 3.

  • Ngoài ra, chúng tôi cũng tích hợp các công cụ tác nhân giúp bạn viết mã thông qua các kỹ năng Android chính thức cho Gemini trong Android Studio và Android CLI.

  • Ví dụ: chúng ta sẽ xem xét các kỹ năng thích ứng của Compose được phát hành tại Google I/O vào đầu năm nay.

  • Thư viện này đã có thể di chuyển bố cục điện thoại sang kích thước cửa sổ lớn hơn.

  • Giờ đây, ứng dụng cũng có thể khắc phục tình trạng mất trạng thái, triển khai các trải nghiệm dành riêng cho thiết bị có thể gập lại như chế độ trên mặt bàn, đồng thời cải thiện khả năng hỗ trợ nhập liệu bằng cách khám phá và triển khai phím tắt, con trỏ tuỳ chỉnh, chú giải công cụ, v.v.

  • Được rồi, hãy bắt đầu với Jetpack Compose, nền tảng cho tất cả những điều này.

  • Jetpack Compose ra mắt phiên bản chính thức vào năm 2021, nên năm nay là sinh nhật lần thứ 5 của bộ công cụ này.

  • Trong 5 năm qua, chúng tôi đã thấy hàng nghìn ứng dụng chính thức được xây dựng dễ dàng và đáng tin cậy hơn nhờ Compose.

  • Giờ đây, quá trình phát triển Android chính thức ưu tiên Compose.

  • Điều đó có nghĩa là tất cả các bố cục thích ứng, kiểu dáng và thành phần điều hướng hiện đại mới đều được xây dựng cho Compose.

  • Và từ góc độ cấu trúc hệ thống, lợi thế lớn nhất của Compose đối với các ứng dụng thích ứng so với hệ thống khung hiển thị cũ là cách trạng thái và giao diện người dùng tương tác.

  • Để giải thích điều này, tôi muốn so sánh nhanh với hệ thống khung hiển thị.

  • Vì vậy, hãy nghĩ về cách hoạt động của hệ thống khung hiển thị.

  • Giao diện người dùng của bạn là một cây gồm các đối tượng có thể thay đổi mà bạn đã tăng cường một lần từ các tệp XML tĩnh.

  • Bạn có nhiều bộ hạn định tài nguyên và hãy bắt đầu với vấn đề chính ở đây: sự trùng lặp giữa các bộ hạn định tài nguyên.

  • Khi tạo giao diện người dùng thích ứng trong XML, bạn không có bất kỳ logic có điều kiện nào bên trong tệp bố cục.

  • Thay vào đó, bạn chỉ cần sao chép toàn bộ hệ phân cấp trên các thư mục tài nguyên, chẳng hạn như bố cục, bố cục cửa sổ nhỏ nhất 600 dp, bố cục ngang, v.v.

  • Được rồi, hãy xem ví dụ về EditText ở bên phải.

    <LinearLayout android:orientation="vertical">
      <EditText
          android:id="@+id/text_msg"
          android:inputType="textCapCharacters"
          android:maxLength="12" />
    </LinearLayout>
    <ConstraintLayout>
      <TextInputEditText
        android:id="@+id/text_input"
        android:inputType="text" />
    </ConstraintLayout>
  • Đó chính là thành phần logic xử lý hoạt động nhập văn bản và thành phần này nằm rải rác trong nhiều tệp XML.

  • Nếu bạn cập nhật một thuộc tính như inputType hoặc maxLength trong bố cục điện thoại, thì trình biên dịch sẽ không bắt buộc bạn phải cập nhật thuộc tính đó trong bố cục máy tính bảng.

  • Và vì các thành phần được tách riêng trên nhiều tệp, nên cấu trúc gần như chắc chắn sẽ bị lệch theo thời gian.

  • Và việc trùng lặp đó cũng liên quan đến một vấn đề khác, đó là mất trạng thái trong quá trình thay đổi cấu hình.

  • Hãy nhớ rằng trong hệ thống Khung hiển thị, các thành phần giao diện người dùng đều có trạng thái.

  • EditText sở hữu vùng đệm Editable nội bộ, vị trí con trỏ tuỳ chỉnh, trạng thái lấy tiêu điểm và một số trạng thái.

  • Vì vậy, khi người dùng mở thiết bị hoặc đổi kích thước cửa sổ vượt quá một ngưỡng nhất định, Android sẽ hủy bỏ toàn bộ hệ phân cấp view và tăng cường các tệp XML mới từ đầu.

  • Để lưu dữ liệu đầu vào của người dùng, khung này dựa vào onSaveInstanceState. Thao tác này sẽ đóng gói trạng thái được liên kết với mã nhận dạng của thành phần hiển thị đó.

  • Nhưng điều đó tạo ra ba chế độ lỗi có thể xảy ra.

  • Trước tiên, nếu ai đó vô tình đặt tên cho mã nhận dạng là text_input thay vì text_msg trong XML khác, thì quá trình khôi phục trạng thái sẽ tự động loại bỏ văn bản đầu vào của người dùng mà không đưa ra cảnh báo.

  • Thứ hai, onRestoreInstanceState chạy sau onViewCreated, có thể thường xuyên xung đột với trình quan sát ViewModel, có thể gây ra vòng lặp gọi lại.

  • Thứ ba, nếu bạn hoán đổi loại tiện ích trên các bộ hạn định, giả sử trên màn hình nhỏ gọn, bạn muốn có một vòng quay và trên màn hình rộng hơn, bạn muốn có RadioGroup, nếu chúng có cùng mã nhận dạng, thì sẽ gặp sự cố trong thời gian chạy với ClassCastException.

  • Trong Jetpack Compose, giao diện người dùng không phải là một đối tượng mà bạn tạo một lần.

  • Giao diện người dùng là một hàm thuần tuý của trạng thái.

  • Và điều đó thay đổi hoàn toàn định nghĩa về thay đổi cấu hình.

  • Thay đổi về cấu hình trong Compose là gì?

  • Tôi có một gợi ý.

Xử lý các thay đổi về cấu hình và chức năng giao diện người dùng

  • Francesco Romano

    Vâng.

  • Trong Compose, thay đổi cấu hình chỉ là một quá trình kết hợp lại khác và quá trình kết hợp lại luôn diễn ra.

  • Khi cửa sổ mở rộng hơn, có thể là do người dùng mở thiết bị, các thành phần kết hợp của bạn sẽ đọc kích thước mới và những thành phần có kết quả phụ thuộc vào kích thước đó sẽ chạy lại.

  • Đó là toàn bộ sự kiện.

  • Nhưng bây giờ, hãy cùng tìm hiểu ý nghĩa của việc viết ứng dụng thích ứng hiện đại bằng các công cụ và thành phần mới nhất của Compose.

  • Vì vậy, thế giới XML khiến bạn đặt ra những câu hỏi như "Tôi đang chạy trên điện thoại, máy tính bảng hay máy tính để bàn?", sau đó phân nhánh giao diện người dùng dựa trên danh mục thiết bị.

  • Tuy nhiên, có một lý do cơ bản để tránh làm như vậy.

  • Ngay cả khi chạy trên cùng một thiết bị thực, bối cảnh ứng dụng và chức năng giao diện người dùng vẫn có thể thay đổi linh động trong thời gian chạy.

  • Vì vậy, thông điệp mà tôi muốn gửi gắm là các quyết định về giao diện người dùng nên được đưa ra dựa trên các chức năng hiện đang hoạt động: kích thước cửa sổ hiện tại, tư thế, một số đặc điểm phần cứng.

  • Hãy xem một ví dụ điển hình, đó là cửa sổ kiểu máy tính trên màn hình ngoài.

  • Nếu bạn kết nối điện thoại Pixel với màn hình ngoài trên bàn làm việc, thì chính quy trình ứng dụng vừa chạy trên màn hình dọc 6 inch giờ đây sẽ chạy trên màn hình rộng 32 inch có gắn bàn phím và chuột vật lý.

  • Vì vậy, việc kiểm tra loại thiết bị mà ứng dụng đang chạy không đảm bảo được điều gì về màn hình mà ứng dụng thực sự đang hiển thị hoặc phương thức nhập.

  • Ngoài ra, Android còn hỗ trợ chế độ nhiều cửa sổ, nhờ đó bạn có thể đổi kích thước cửa sổ.

  • Ngay cả khi màn hình của Googlebook lớn hơn điện thoại, ứng dụng của bạn có thể đang chạy trên cửa sổ rộng 400 dp cùng với các ứng dụng khác.

  • Do đó, việc truy vấn các chỉ số màn hình thực tế tĩnh cũng có thể không có ý nghĩa.

  • Vì vậy, để thiết kế ứng dụng cho Compose trước tiên, chúng ta cần hiểu cách truy vấn môi trường.

  • Và tôi muốn bắt đầu bằng cách cố gắng chính thức hoá khái niệm về các chức năng của giao diện người dùng.

  • Các chức năng của giao diện người dùng không chỉ bao gồm chiều rộng và chiều cao của cửa sổ hoặc tư thế gập, mà còn bao gồm cả các phương thức đầu vào và đầu ra hiện đang hoạt động.

  • Chúng tôi nhận thấy rằng trong môi trường máy tính, người dùng không phải lúc nào cũng sử dụng màn hình cảm ứng.

  • Họ có thể dùng bàn phím và chuột.

  • Trong hệ thống ô tô, một số xe có núm xoay hoặc phương thức nhập khác, cách nhập tín hiệu khác.

  • Ngoài ra, ngoài kích thước màn hình và phương thức nhập, khoảng cách giữa người dùng và màn hình, chẳng hạn như khoảng cách xem hoặc liệu máy ảnh hoặc micrô phần cứng có đang được kết nối hay không.

  • Đó là tất cả các chức năng của giao diện người dùng.

Phát hiện tư thế bằng API mediaQuery

  • Francesco Romano

    Giờ đây, nếu muốn truy xuất tất cả tín hiệu, bạn phải kết hợp 5, 6 API khác nhau từ nhiều nguồn.

  • Bạn cần có đối tượng cấu hình, WindowMetricsCalculator, WindowInfoTracker, InputManager, PackageManager, sau đó chuyển đổi từng đối tượng theo cách thủ công thành một trạng thái, một trạng thái Compose.

  • Bạn có thích làm việc đó không?

  • Tôi không làm vậy.

  • Để giải quyết vấn đề này, chúng tôi đã ra mắt API MediaQuery trong androidx.compose.ui.

    mediaQuery { ... }
    derivedMediaQuery { ... }
  • API này được lấy cảm hứng từ các truy vấn đa phương tiện CSS.

  • Và từ "media" (nội dung nghe nhìn) không có nghĩa là phát âm thanh hoặc video.

  • Nó đề cập đến phương tiện, môi trường mà giao diện người dùng của bạn đang được hiển thị.

  • MediaQuery cung cấp cho bạn một cách khai báo duy nhất, gốc trong Compose để truy vấn các chức năng của giao diện người dùng.

  • Hãy xem các thuộc tính được hiển thị trong UiMediaScope.

    sealed interface UiMediaScope {
        @get:FrequentlyChangingValue val windowWidth: Dp
        @get:FrequentlyChangingValue val windowHeight: Dp
    
        val windowPosture: Posture // Flat, Tabletop, Book
        val pointerPrecision: PointerPrecision // Fine, Coarse, Blunt, None
        val keyboardKind: KeyboardKind // Physical, Virtual, None
    
        val hasCamera: Boolean
        val hasMicrophone: Boolean
        val viewingDistance: ViewingDistance // Near, Medium, Far
    }
  • windowWidth và windowHeight tính bằng dp.

  • windowPosture, hiện dành cho thiết bị có thể gập lại và có thể là Flat (Phẳng), Tabletop (Mặt bàn) hoặc Book (Sách).

  • pointerPrecision cho bạn biết độ chính xác của thiết bị trỏ chính: Fine (Chính xác) cho chuột, Bút cảm ứng hoặc Bàn di chuột; Coarse (Tương đối) cho Thao tác chạm; Blunt (Không chính xác) cho cần điều khiển, tay điều khiển hoặc Không có.

  • keyboardKind có thể là thẻ vật lý, thẻ ảo hoặc không có thẻ.

  • hasCamera và hasMicrophone tự động theo dõi xem phần cứng này có sẵn hay không.

  • Hãy cân nhắc sử dụng camera ngoài có thể cắm vào.

  • Và viewingDistance báo cáo Gần, Trung bình hoặc Xa.

  • Nhưng tôi nghĩ sẽ dễ dàng hơn nếu chúng ta xem xét một ví dụ thực tế, đó là phát hiện xem thiết bị có đang ở chế độ Mặt bàn hay không.

  • Chế độ mặt bàn là một tư thế cho phép người dùng gập thiết bị một nửa và thường đặt thiết bị trên một bề mặt.

  • Chế độ này cũng rất phù hợp để phát video trong thời gian dài, gọi video và chụp ảnh tua nhanh thời gian.

  • Tính năng này cũng rất hữu ích khi bạn muốn làm theo công thức nấu ăn hoặc xem bản trình chiếu.

  • Giờ đây, nếu muốn phát hiện tư thế Để trên bàn trước MediaQuery, bạn phải viết tất cả đoạn mã này.

    @Composable
    fun currentVideoPlayerLayout(): VideoPlayerLayout {
       val context = LocalContext.current
       val windowLayoutInfo by WindowInfoTracker.getOrCreate(context).windowLayoutInfo(context)
           .collectAsStateWithLifecycle(null)
       val isTabletopPosture = windowLayoutInfo?.displayFeatures?.any { displayFeature ->
           displayFeature is FoldingFeature &&
                   displayFeature.state == FoldingFeature.State.HALF_OPENED &&
                   displayFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
       } ?: false
       return if (isTabletopPosture) {
           VideoPlayerLayout.Tabletop
       } else {
           VideoPlayerLayout.Default
       }
    }
  • Bạn phải lấy bối cảnh, lấy một thực thể của WindowInfoTracker, thu thập windowLayoutInfo (là một luồng, một trạng thái Compose nhận biết vòng đời), sau đó lặp lại qua displayFeatures để kiểm tra xem có một thực thể là FoldingFeature, đang mở một nửa và nằm ngang hay không.

  • Và đó chỉ là để phát hiện tư thế.

  • Đó là rất nhiều mã nguyên mẫu.

  • Với MediaQuery API mới, tất cả những điều đó được thu gọn thành chỉ vài dòng mã.

    @Composable
    fun currentVideoPlayerLayout(): VideoPlayerLayout {
       return mediaQuery {
           when(windowPosture) {
               UiMediaScope.Posture.Tabletop -> VideoPlayerLayout.Tabletop
               else -> VideoPlayerLayout.Default
           }
       }
    }
  • Hàm mediaQuery nhận một tham số lambda duy nhất và trong phạm vi nhận của lambda đó, hàm này hiển thị tập hợp các thuộc tính mà chúng ta vừa thấy.

  • Trong ví dụ này, chỉ cần kiểm tra windowPosture trong biểu thức when, chúng ta có thể trả về ngay composable phù hợp.

  • Sau đây là hai ví dụ khác về cách sử dụng API.

    @Composable
    fun AdaptiveChatControls(
        onStartVideoCall: () -> Unit,
        onSendMessage: () -> Unit,
    ) {
        // 1. Return a Dp value directly based on pointer precision
        val chipPadding = mediaQuery {
            if (pointerPrecision == PointerPrecision.Fine) 2.dp else 8.dp
        }
    
        // 2. Return a Boolean combining hardware capabilities
        val canVideoCall = mediaQuery { hasCamera && hasMicrophone }
        val hasPhysicalKeyboard = mediaQuery {
            keyboardKind == KeyboardKind.Physical
        }
    
        Row(modifier = Modifier.padding(chipPadding)) {
            if (canVideoCall) {
                IconButton(onClick = onStartVideoCall) {
                    Icon(Icons.Default.Videocam, contentDescription = "Video call")
                }
            }
            Button(onClick = onSendMessage) {
                Text(if (hasPhysicalKeyboard) "Send (Ctrl+Enter)" else "Send")
            }
        }
    }
  • Đầu tiên là mật độ thông tin.

  • Vì vậy, khi người dùng sử dụng máy tính xách tay, màn hình máy tính có chuột và độ chính xác của con trỏ là Fine (Chính xác), chúng ta có thể thắt chặt khoảng đệm trên các thành phần giao diện người dùng từ 8 Dp xuống 2 Dp (trong ví dụ này) để hiển thị nhiều nội dung hơn một cách thoải mái, vì người dùng không sử dụng ngón tay nên mục tiêu chạm có thể nhỏ hơn.

  • Ngoài ra, còn có các hành động dựa trên chức năng.

  • Giả sử chúng ta có một thanh nhập nội dung trò chuyện, chúng ta có thể kiểm tra hasCamera và hasMicrophone để hiển thị hoặc ẩn nút hành động cuộc gọi video một cách linh động, hoặc chúng ta có thể kết hợp loại bàn phím là bàn phím thực để hiển thị hoặc không hiển thị một số gợi ý về phím tắt như nhấn Ctrl+Enter để gửi.

  • Tất cả các thông tin này đều được truy xuất trong thời gian chạy.

  • MediaQuery cũng rất phù hợp để thử nghiệm và tạo bản xem trước.

    @Preview(name = "Tabletop + Fine Pointer", widthDp = 840, heightDp = 700)
    @Composable
    fun TabletopPlayerPreview() {
        val simulatedMediaScope = object : UiMediaScope {
            override val windowWidth = 840.dp
            override val windowHeight = 700.dp
            override val windowPosture = UiMediaScope.Posture.Tabletop
            override val pointerPrecision = UiMediaScope.PointerPrecision.Fine
            override val keyboardKind = UiMediaScope.KeyboardKind.Physical
            override val hasCamera = true
            override val hasMicrophone = true
            override val viewingDistance = UiMediaScope.ViewingDistance.Medium
        }
    
        CompositionLocalProvider(LocalUiMediaScope provides simulatedMediaScope) {
            VideoPlayerScreen()
        }
    }
  • Vì vậy, về cơ bản, MediaQuery phân giải trình nhận thông qua một thành phần cục bộ có tên là LocalUiMediaScope.

  • Vì vậy, bạn chỉ cần cung cấp một UiMediaScope giả thông qua CompositionLocalProvider trong bản xem trước mà không cần phải khởi động thiết bị có thể gập lại và đặt góc 90° hoặc gắn chuột chỉ để chạy kiểm thử giao diện người dùng Compose.

Bố cục màn hình thích ứng với Grid và FlexBox

  • Francesco Romano

    Giờ thì chúng ta đã biết cách truy vấn các chức năng của giao diện người dùng.

  • Bây giờ, hãy xem cách tạo bố cục thực tế.

  • Vì vậy, tôi muốn giới thiệu Grid, một thành phần gốc mới cho bố cục trong androidx.compose.foundation.layout, hiện đã ổn định.

    Grid(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        config = {
            val maxWidthDp = constraints.maxWidth.toDp()
            val columnCount = if (maxWidthDp < 600.dp) 2 else 4
    
            repeat(columnCount) {
                column(1.fr)
            }
            row(GridTrackSize.Auto)
            row(1.fr)
    
            gap(row = 16.dp, column = 8.dp)
            flow = GridFlow.Row
        }
    ) {
        // GridScope children...
    }
  • Lưới cho phép bạn triển khai bố cục hai chiều bằng cách hiển thị các mục trong các cột và hàng thích ứng với kích thước vùng chứa có sẵn.

  • Hãy xem một số việc bạn có thể làm trong khối cấu hình.

  • Bạn có thể thêm các phần tử hoặc bản nhạc mới bằng cách gọi cột hoặc hàng, đồng thời có thể sử dụng các vòng lặp Kotlin chuẩn như lặp lại.

  • Bạn có thể đặt rãnh giữa các dải.

  • Bạn có thể kiểm soát hướng đặt tự động thông qua flow – GridFlow.Row hoặc GridFlow.Column.

  • Và đây là một chi tiết quan trọng về cấu trúc.

  • Khối cấu hình chạy trong quá trình đo lường, còn phạm vi cấu hình lưới triển khai mật độ và hiển thị các quy tắc ràng buộc.

  • Tức là bạn có thể kiểm tra constraints.maxWidth.toDp() ngay trong cấu hình để tính toán các bản nhạc mà không cần thêm lượt kết hợp lại.

  • Trong Grid, khi bạn thêm một thành phần con mà không có đối tượng sửa đổi, thành phần đó sẽ tự động chuyển sang ô tiếp theo.

    Grid(
        config = {
            repeat(3) { column(1.fr) }
            repeat(3) { row(100.dp) }
            gap(8.dp)
        }
    ) {
        // Spans 2x2 area (rows 1..2, columns 1..2)
        HeroCard(modifier = Modifier.gridItem(rows = 1..2, columns = 1..2))
    
        // Overlaps on top of HeroCard in cell (1, 1) aligned to TopStart!
        LiveBadge(
            modifier = Modifier.gridItem(
                row = 1,
                column = 1,
                alignment = Alignment.TopStart
            )
        )
    
        // Negative index: placed in last row (-1), spanning all 3 columns
        FooterBar(modifier = Modifier.gridItem(row = -1, column = 1, columnSpan = 3))
    }
  • Khi muốn có quyền kiểm soát rõ ràng, bạn có thể sử dụng Modifier.gridItem và bạn có toạ độ dựa trên một.

  • Vì vậy, hàng 1, cột 1 là ô trên cùng bên trái.

  • Hàm này hỗ trợ lập chỉ mục âm, vì vậy, hàng -1 là hàng cuối cùng, v.v.

  • Bạn có thể sử dụng rowSpan để kéo dài một mục trên một vùng 2D, sau đó cũng có sự chồng chéo.

  • Vì vậy, nếu hai mục được đặt vào các ô chồng chéo, thì Grid sẽ xếp chồng chúng lên nhau trong các giới hạn đó và thứ tự Z được xác định theo thứ tự thành phần.

  • Vì vậy, các mục sau sẽ vẽ lên trên các mục khác giống như Box.

  • Giờ thì chúng ta đã biết những kiến thức cơ bản về Grid.

  • Hãy xem ví dụ về trình phát video.

  • Tuỳ thuộc vào việc thiết bị đang ở trạng thái phẳng hay mở một nửa, chúng ta cần chuyển đổi giữa bố cục mặc định hoặc bố cục trên mặt bàn.

  • Trước khi có Grid, chúng ta phải tạo hai thành phần kết hợp khác nhau là DefaultLayout và TabletopLayout, sau đó phân nhánh mã trên biểu thức when.

    @Composable
    fun VideoPlayerLayout(
       player: @Composable () -> Unit,
       controller: @Composable () -> Unit,
       modifier: Modifier = Modifier
    ) {
       when(currentVideoPlayerLayout()) {
           VideoPlayerLayout.Default -> DefaultLayout(player, controller, modifier)
           VideoPlayerLayout.Tabletop -> TabletopLayout(player, controller, modifier)
       }
    }
  • Và bạn có thể nghĩ, "Có gì sai đâu?"

  • Nếu xem các bố cục đó, bạn sẽ thấy nội dung của chúng giống hệt nhau.

  • Cả hai đều hiển thị cùng một trình phát và cùng một bộ điều khiển.

  • Điểm khác biệt duy nhất giữa hai thành phần này là cách sắp xếp chúng trên màn hình.

  • Cho đến nay, vì không có cách nào để truyền chính cách sắp xếp bố cục dưới dạng tham số, nên chúng ta buộc phải xác định hai thành phần kết hợp riêng biệt.

  • Giờ đây, chúng ta có thể tham số hoá cấu hình bố cục và xử lý mọi thứ trong một hàm composable duy nhất.

  • Chúng ta sẽ cùng tìm hiểu cách thực hiện.

  • Vì vậy, với Grid API, chúng ta có thể xác định các vùng được đặt tên dưới dạng enum.

    enum class VideoPlayerArea { Player, Controller }
    
    Grid(
        config = { ... },
        modifier = modifier
    ) {
        Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
            player()
        }
        Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
           controller()
       }
    }
  • Sau đó, chúng ta có thể chỉ định các thành phần kết hợp con, trình phát và bộ điều khiển để đặt bên trong lưới, đồng thời có thể liên kết từng thành phần con với một vùng được đặt tên bằng cách gọi Modifier.gridItem và truyền areaId.

  • Và tại đây, chúng ta sẽ yêu cầu Grid đặt trình phát vào vùng có tên là VideoPlayerArea.Player và bộ điều khiển vào VideoPlayerArea.Controller.

  • Và tại đây, chúng ta xác định hai lambda cấu hình trong một VideoPlayerGridConfiguration.

    object VideoPlayerGridConfiguration {
       val Default: GridConfigurationScope.() -> Unit = {
           row(1f)
           column(1f)
           area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
           area(areaId = VideoPlayerArea.Controller, row = 1, column = 1)
       }
    
       val Tabletop: GridConfigurationScope.() -> Unit = {
           row(0.5f)
           row(0.5f)
           column(1f)
           area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
           area(areaId = VideoPlayerArea.Controller, row = 2, column = 1)
       }
    }
  • Đối với bố cục trình phát video mặc định, chúng ta định cấu hình một lưới gồm một hàng và một cột, nhưng xin lưu ý rằng ngay cả khi chỉ có một ô, chúng ta vẫn có thể chỉ định hai tên vùng cho cùng một ô.

  • Trong trường hợp này, như chúng ta đã thấy trước đó, bộ điều khiển được phủ lên trên trình phát.

  • Đối với chế độ trên mặt bàn, chúng ta chỉ cần xác định hai hàng và một cột, đặt trình phát ở nửa trên và bộ điều khiển ở nửa dưới, về cơ bản là như thế này.

  • Sau khi xác định các lambda cấu hình đó, chúng ta có thể sử dụng MediaQuery trực tiếp trong cấu hình của lưới.

    @Composable
    fun VideoPlayerLayout(
       player: @Composable () -> Unit,
       controller: @Composable () -> Unit,
       modifier: Modifier = Modifier
    ) {
       Grid(
           config = { mediaQuery {
           if (isTabletopPosture) { VideoPlayerGridConfiguration.Tabletop }
     else { VideoPlayerGridConfiguration.Default }
    }
       }
           modifier = modifier
       ) {
           Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
               player()
           }
           Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
               controller()
           }
       }
    }
  • Vì vậy, giờ đây, chúng ta có thể có một bố cục trình phát video duy nhất có thể kết hợp với giao diện người dùng, giúp chuyển đổi linh động giữa hai cấu hình.

  • Kiểu dữ liệu gốc tiếp theo mà tôi muốn nói đến là FlexBox.

    @Composable
    fun AdaptiveActionHeader(tags: List<String>) {
            FlexBox(
            config = {
                direction( if (isCompact)
    FlexDirection.Column else FlexDirection.Row )
                wrap(FlexWrap.Wrap)
                alignItems(FlexAlignItems.Center)
                gap(row = 8.dp, column = 12.dp)
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(
                text = "Home",
                modifier = Modifier.flex {
                    basis(FlexBasis.Auto)
                    grow(1f)
                    shrink(1f)
                }
            )
            tags.forEach { tag ->
                FilterChipItem(
                    label = tag,
                    modifier = Modifier.flex { shrink(0f) }
                )
            }
        }
    }
  • Vì vậy, trong khi Grid xử lý bố cục hàng và cột 2D, thì FlexBox (hiện cũng đã ổn định) xử lý bố cục một chiều nhưng vẫn cần phân phối không gian, thu nhỏ và xuống dòng nhiều dòng một cách thông minh.

  • Đây là một bố cục thích ứng, tiện lợi mà bạn có thể sử dụng thay vì hai thành phần kết hợp riêng biệt, chẳng hạn như FlowRow và FlowColumn, vì đây là một vùng chứa duy nhất có hướng có thể thay đổi linh động bên trong khối cấu hình mà không cần tạo lại các thành phần kết hợp con.

  • Và nó triển khai thuật toán CSS FlexBox đầy đủ, vì vậy bạn có wrap, justify-content, align-items, gap.

  • Và trên mỗi phần tử con, bạn có Modifier.flex cung cấp cho bạn cơ sở, hệ số tăng, thu nhỏ tương đối, alignSelf, v.v.

Cấu trúc bố cục ở cấp ứng dụng và Navigation 3

  • Francesco Romano

    Bây giờ, hãy chuyển từ định nghĩa cấp màn hình sang bố cục cấp ứng dụng, cách chúng ta cấu trúc thành phần điều hướng cấp cao nhất và cách sắp xếp màn hình nhiều ngăn trên toàn bộ ứng dụng, chứ không chỉ trên một màn hình.

  • Mặc dù MediaQuery cung cấp cho bạn chiều rộng và chiều cao chính xác của cửa sổ theo Dp, nhưng đối với cấu trúc cấp ứng dụng, bạn cần có các điểm ngắt khung nhìn được chuẩn hoá và có chủ đích, đó là các lớp kích thước cửa sổ.

  • Về cơ bản, bạn muốn mọi màn hình đều thích ứng trong từng nhóm này, đồng thời bạn có thể có nhiều thay đổi và cập nhật về cấu trúc cũng như sự khác biệt giữa các nhóm.

  • Ví dụ: bằng cách chỉ hiển thị danh sách trên màn hình thu gọn hoặc cả danh sách và thông tin chi tiết trong cửa sổ mở rộng.

  • Bạn có thể tạo các lớp kích thước cửa sổ ngay trên UiMediaScope theo cách rất rõ ràng, tức là xác định một thuộc tính tiện ích trên UiMediaScope.

    val UiMediaScope.windowSizeClass: WindowSizeClass
        @Composable get() = derivedMediaQuery {
            WindowSizeClass.BREAKPOINTS_V2.computeWindowSizeClass(
                widthDp = windowWidth.value,
                heightDp = windowHeight.value
            )
        }
    
    @Composable
    fun AppRoot() {
        val sizeClass = mediaQuery { windowSizeClass }
        val isExpandedWidth = sizeClass.isWidthAtLeastBreakpoint(
            WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND // 600dp
        )
        // Drive app-level navigation and scene strategies...
    }
  • Một điều cần lưu ý là nó đang sử dụng derivedMediaQuery, chứ không phải MediaQuery tiêu chuẩn.

  • Tại sao?

  • Vì chiều rộng và chiều cao của cửa sổ là các giá trị thường xuyên thay đổi.

  • Khi người dùng đổi kích thước ứng dụng của bạn, chẳng hạn như trên máy tính, các giá trị này có thể thay đổi theo từng khung hình.

  • Và có thể bạn không muốn kết hợp lại scaffold ở cấp ứng dụng trên mọi khung hình, mà chỉ khi cửa sổ thực sự vượt qua một điểm ngắt.

  • Và khi đã có các lớp kích thước cửa sổ, làm cách nào để chuyển đổi ứng dụng từ luồng một ngăn sang bố cục nhiều ngăn khi có nhiều không gian hơn?

  • Đây là điểm nổi bật của Jetpack Navigation 3, vì Nav 3 được thiết kế từ đầu dựa trên trạng thái và cảnh của Compose.

  • Đầu tiên, bạn sở hữu ngăn xếp lui, đây chỉ đơn giản là danh sách NavKey, vì vậy, ngăn xếp này rất linh hoạt và dễ lưu dưới dạng trạng thái.

  • Mỗi khoá trên ngăn xếp phân giải thành một NavEntry chứa thành phần kết hợp và siêu dữ liệu của khoá đó.

  • NavDisplay không cố định giá trị trong mã về cách vẽ các mục; thay vào đó, NavDisplay đánh giá danh sách các chiến lược cảnh.

  • Trong chiến lược này, bạn có thể kiểm tra các chức năng của giao diện người dùng, chẳng hạn như các lớp kích thước cửa sổ.

    if (isExpandedWidth) {
        /* return a multi-pane scene */
    }
    else {
        /* return a single pane scene */
    }
  • Bạn có thể kiểm tra ngăn xếp lui, sau đó quyết định xem nên hiển thị một NavEntry trong một ngăn hay nhiều mục cùng lúc, hoặc mục này chồng lên mục kia.

  • Cách này rất linh hoạt.

  • Bên cạnh chiến lược nhiều ngăn, các lớp kích thước cửa sổ cũng thúc đẩy những thành phần điều hướng mà bạn muốn sử dụng để tối đa hoá tính công thái học và việc sử dụng không gian màn hình.

  • Ví dụ: trên chiều rộng thu gọn, bạn muốn có một thanh điều hướng phía dưới, trên chiều rộng trung bình, bạn muốn có một thanh điều hướng dọc, trên chiều rộng mở rộng, bạn muốn có một ngăn điều hướng đầy đủ.

  • Khi kết hợp Nav3 cho các quyết định về bố cục ở cấp ứng dụng và các thành phần thích ứng cho định nghĩa ở cấp màn hình, bạn sẽ có được những ứng dụng trông như thế này và hoạt động hiệu quả trên hầu hết mọi kích thước cửa sổ và môi trường.

  • Riêng đối với Nav 3, nếu bạn muốn tìm hiểu thêm về các bản cập nhật mới nhất, bao gồm cả việc hỗ trợ đường liên kết sâu, hãy xem bài nói chuyện Xây dựng đường liên kết sâu an toàn về kiểu bằng Jetpack Navigation 3.

Bản minh hoạ về quy trình phát triển và tự động hoá dựa trên tác nhân

  • Francesco Romano

    Hôm nay, chúng ta đã tìm hiểu nhiều API mới: MediaQuery, Grid, FlexBox, Nav3.

  • Đó là cách Jetpack Compose giúp bạn tạo ứng dụng thích ứng.

  • Nhưng trong kỷ nguyên phát triển dựa trên tác nhân, có một lợi thế cực kỳ quan trọng khác khi sử dụng Jetpack Compose: hiệu quả hơn nhiều.

  • Có thể bạn đã nghe trong bài phát biểu chính rằng chúng tôi nhận thấy các ứng dụng sử dụng ít hơn 30% mã thông báo sau khi di chuyển sang Compose và chúng tôi đang nỗ lực tối ưu hoá quy trình làm việc dựa trên tác nhân cho các ứng dụng Compose hơn nữa.

  • Hôm nay, tôi muốn nêu bật những điểm cập nhật chính về kỹ năng Thích ứng trong Compose.

    android skills list
    android skills add adaptive
  • Như tôi đã nói, ngoài việc di chuyển bố cục sang Grid, Flexbox và MediaQuery, giờ đây, Gemini còn xử lý rõ ràng việc quản lý trạng thái toàn diện trên toàn ứng dụng và chúng tôi kiểm thử kỹ năng này dựa trên các lỗi mất trạng thái trong thực tế: duy trì thao tác điều hướng, vị trí cuộn, phát nội dung nghe nhìn đang hoạt động, nhập văn bản chưa hoàn tất, v.v.

  • Kỹ năng này hiện cũng bao gồm tính năng tối ưu hoá tư thế gập, chẳng hạn như tự động tái cấu trúc màn hình nội dung nghe nhìn và camera sang chế độ Mặt bàn. Tuy nhiên, khi ứng dụng của bạn chạy trên Googlebook hoặc màn hình ngoài bằng chuột và bàn phím, bố cục chỉ là một phần của câu chuyện.

  • Giờ đây, kỹ năng Thích ứng có thể kiểm tra giao diện người dùng để tìm và triển khai phím tắt, con trỏ tuỳ chỉnh, trạng thái di chuột, chú thích, v.v.

  • Hãy xem ví dụ thực tế.

  • Tôi đã ghi lại một bản minh hoạ về việc này.

  • Vậy là chúng ta đã có ứng dụng.

  • Chúng ta có thể ngắm nhìn thú cưng.

  • Chúng ta có thể nhắn tin với họ.

  • Chúng ta có thể xem video về chúng.

  • Đúng vậy, chúng ta có thể thấy những thú cưng hàng đầu.

  • Nhưng bạn đã thấy rằng điều đó không tốt.

  • Giờ hãy xem, tôi chỉ muốn xem một bức ảnh có chút kịch tính và có mèo.

  • Được rồi, tôi muốn xem ảnh lớn hơn.

  • Dah, that's back to home.

  • Được rồi, để tôi nói với bạn tôi về việc này.

  • Ừ, "Buddy" à, hừm.

  • Ứng dụng này không tốt.

  • Nó cũng quá rộng.

  • Bạn thấy rằng nó quá rộng.

  • Hãy thử đổi kích thước.

  • Loại hình minh hoạ nào?

  • Ừ, được rồi.

  • Và văn bản tôi đã nhập bị mất.

  • Được rồi, giờ là lúc hỏi Gemini sửa lỗi này.

  • Nhờ kỹ năng Thích ứng, câu lệnh có thể đơn giản như "làm cho ứng dụng thích ứng".

  • Như bạn có thể thấy, tôi đang sử dụng Gemini 3.0 Flash và mô hình này có thể làm được điều đó nhờ kỹ năng này.

  • Trợ lý đã tiếp thu kỹ năng và hiện đang bắt đầu thực hiện các thay đổi.

  • Công cụ này sẽ đặt một số câu hỏi, chẳng hạn như "Ồ, MediaQuery đang trong giai đoạn thử nghiệm.

  • Bạn có thực sự muốn sử dụng không?"

  • Và tiếp tục.

  • Tôi phải tăng tốc để cắt bớt một số phần, nhưng nếu đến khu vực minh hoạ, bạn có thể tự mình thử các kỹ năng.

  • Và đây chỉ là một trong những kỹ năng mà chúng tôi đã phát hành.

  • Ví dụ: chúng tôi cũng có một kỹ năng mới giúp bạn xây dựng trải nghiệm bút cảm ứng tuyệt vời.

  • Vì vậy, hãy đến khu vực này để thử.

  • Vậy là xong.

  • Nó đã sửa đổi 11 tệp.

  • Hãy xem ví dụ thực tế.

  • Được rồi.

  • Grid đã thực hiện công việc của mình.

  • Thanh điều hướng đã được chuyển sang bên cạnh.

  • Hãy thử dùng bộ lọc.

  • Tôi nghĩ giờ thì chúng sẽ hoạt động.

  • Có.

  • Được rồi, hãy nhắn tin cho bạn tôi và nói rằng ứng dụng này hiện rất tuyệt.

  • Hãy xem giao diện khi có nhiều không gian hơn.

  • Đúng vậy, ứng dụng đã chuyển sang bố cục danh sách-chi tiết.

  • Quá trình chạy này mất khoảng 20 phút và khoảng 200.000 mã thông báo.

  • Được rồi.

Lợi tức đầu tư (ROI) của ứng dụng thích ứng và những điểm chính cần ghi nhớ

  • Francesco Romano

    Bây giờ, tôi thường bắt đầu bài nói chuyện bằng các chỉ số kinh doanh và trường hợp thành công, nhưng đối với ADS, tôi muốn đi thẳng vào mã.

  • Tuy nhiên, tôi muốn dành vài phút để chia sẻ về lợi tức đầu tư của kỹ thuật.

  • Hiện nay, ứng dụng Android của bạn có thể chạy trên hơn 580 triệu thiết bị màn hình lớn đang hoạt động. Chúng tôi cũng biết rằng người dùng thiết bị gập và nhiều thiết bị là một trong những phân khúc có mức độ tương tác cao nhất trên Android.

  • Hãy xem xét hai nghiên cứu điển hình cụ thể chứng minh điều gì sẽ xảy ra khi bạn cung cấp một bản dựng thích ứng.

  • Đầu tiên là Gratitude, một ứng dụng tự chăm sóc và viết nhật ký hằng ngày phổ biến.

  • Bằng cách áp dụng thiết kế màn hình thích ứng, Gratitude đã biến máy tính bảng và thiết bị có thể gập lại thành một kênh thu nạp người dùng có mức tăng trưởng cao.

  • Đầu tiên, họ tăng gấp đôi tốc độ thu nạp người dùng trên màn hình lớn.

  • Số lượt cài đặt hằng ngày từ máy tính bảng và thiết bị có thể gập lại tăng 100%.

  • Họ đã tăng tỷ lệ người dùng màn hình lớn trong tổng số cơ sở người dùng từ 7% lên 10% tổng số lượt cài đặt.

  • Thứ hai là BoxBoxClub, một ứng dụng đồng hành về Công thức 1 và đua xe thể thao.Sau khi cập nhật bản dựng cho thiết bị có thể gập lại và màn hình lớn, dữ liệu đo từ xa của họ cho thấy mức tăng đáng kể: số người dùng đang hoạt động nói chung tăng 7, 6% và thời gian tương tác tăng 54%.

  • Được rồi.

  • Chúng ta đã đến phần cuối của bài nói chuyện và tôi muốn chia sẻ với bạn 3 điểm quan trọng cần ghi nhớ.

  • Việc xây dựng bố cục thích ứng giúp ứng dụng của bạn phục vụ nhiều thiết bị và người dùng hơn, đồng thời giúp ứng dụng của bạn sẵn sàng cho tương lai trong hệ sinh thái đang mở rộng này.

  • Quá trình phát triển Android ưu tiên Compose.

  • Tất cả công cụ, thư viện và kỹ năng mới đều được xây dựng cho Compose.

  • Compose là công cụ tuyệt vời cho bố cục thích ứng và phát triển dựa trên tác nhân.

  • Toàn bộ mã nguồn nhỏ hơn và dễ hiểu hơn, tác nhân sử dụng ít mã thông báo hơn khi xử lý mã Compose.

  • Vì vậy, hãy tiếp tục, thử tất cả các công cụ, kỹ năng, API mới và tận hưởng niềm vui.

  • Cảm ơn bạn.

Mã

Đoạn mã

Đoạn mã XML ở phút 05:17 trong video "Giới thiệu về cách điều chỉnh ứng dụng theo tỷ lệ trên nhiều màn hình"
<LinearLayout android:orientation="vertical">
  <EditText
      android:id="@+id/text_msg"
      android:inputType="textCapCharacters"
      android:maxLength="12" />
</LinearLayout>
Đoạn mã XML ở phút 05:17 trong video "Giới thiệu về cách điều chỉnh ứng dụng cho phù hợp với nhiều màn hình"
<ConstraintLayout>
  <TextInputEditText
    android:id="@+id/text_input"
    android:inputType="text" />
</ConstraintLayout>
Đoạn mã Kotlin ở phút 11:31 trong video "Phát hiện tư thế bằng API mediaQuery"
mediaQuery { ... }
derivedMediaQuery { ... }
Đoạn mã Kotlin ở phút 11:57 trong video "Phát hiện tư thế bằng API mediaQuery"
sealed interface UiMediaScope {
    @get:FrequentlyChangingValue val windowWidth: Dp
    @get:FrequentlyChangingValue val windowHeight: Dp

    val windowPosture: Posture // Flat, Tabletop, Book
    val pointerPrecision: PointerPrecision // Fine, Coarse, Blunt, None
    val keyboardKind: KeyboardKind // Physical, Virtual, None

    val hasCamera: Boolean
    val hasMicrophone: Boolean
    val viewingDistance: ViewingDistance // Near, Medium, Far
}
Đoạn mã Kotlin ở phút 13:11 trong phần "Phát hiện tư thế bằng API mediaQuery"
@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
   val context = LocalContext.current
   val windowLayoutInfo by WindowInfoTracker.getOrCreate(context).windowLayoutInfo(context)
       .collectAsStateWithLifecycle(null)
   val isTabletopPosture = windowLayoutInfo?.displayFeatures?.any { displayFeature ->
       displayFeature is FoldingFeature &&
               displayFeature.state == FoldingFeature.State.HALF_OPENED &&
               displayFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
   } ?: false
   return if (isTabletopPosture) {
       VideoPlayerLayout.Tabletop
   } else {
       VideoPlayerLayout.Default
   }
}
Đoạn mã Kotlin ở phút 13:42 trong video "Phát hiện tư thế bằng API mediaQuery"
@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
   return mediaQuery {
       when(windowPosture) {
           UiMediaScope.Posture.Tabletop -> VideoPlayerLayout.Tabletop
           else -> VideoPlayerLayout.Default
       }
   }
}
Đoạn mã Kotlin tại 14:07 trong phần "Phát hiện tư thế bằng API mediaQuery"
@Composable
fun AdaptiveChatControls(
    onStartVideoCall: () -> Unit,
    onSendMessage: () -> Unit,
) {
    // 1. Return a Dp value directly based on pointer precision
    val chipPadding = mediaQuery {
        if (pointerPrecision == PointerPrecision.Fine) 2.dp else 8.dp
    }

    // 2. Return a Boolean combining hardware capabilities
    val canVideoCall = mediaQuery { hasCamera && hasMicrophone }
    val hasPhysicalKeyboard = mediaQuery {
        keyboardKind == KeyboardKind.Physical
    }

    Row(modifier = Modifier.padding(chipPadding)) {
        if (canVideoCall) {
            IconButton(onClick = onStartVideoCall) {
                Icon(Icons.Default.Videocam, contentDescription = "Video call")
            }
        }
        Button(onClick = onSendMessage) {
            Text(if (hasPhysicalKeyboard) "Send (Ctrl+Enter)" else "Send")
        }
    }
}
Đoạn mã Kotlin ở phút 14:59 trong video "Detecting postures with the mediaQuery API" (Phát hiện tư thế bằng API mediaQuery)
@Preview(name = "Tabletop + Fine Pointer", widthDp = 840, heightDp = 700)
@Composable
fun TabletopPlayerPreview() {
    val simulatedMediaScope = object : UiMediaScope {
        override val windowWidth = 840.dp
        override val windowHeight = 700.dp
        override val windowPosture = UiMediaScope.Posture.Tabletop
        override val pointerPrecision = UiMediaScope.PointerPrecision.Fine
        override val keyboardKind = UiMediaScope.KeyboardKind.Physical
        override val hasCamera = true
        override val hasMicrophone = true
        override val viewingDistance = UiMediaScope.ViewingDistance.Medium
    }

    CompositionLocalProvider(LocalUiMediaScope provides simulatedMediaScope) {
        VideoPlayerScreen()
    }
}
Đoạn mã Kotlin ở phút 15:40 trong video "Bố cục màn hình thích ứng với Lưới và FlexBox"
Grid(
    modifier = Modifier.fillMaxSize().padding(16.dp),
    config = {
        val maxWidthDp = constraints.maxWidth.toDp()
        val columnCount = if (maxWidthDp < 600.dp) 2 else 4

        repeat(columnCount) {
            column(1.fr)
        }
        row(GridTrackSize.Auto)
        row(1.fr)

        gap(row = 16.dp, column = 8.dp)
        flow = GridFlow.Row
    }
) {
    // GridScope children...
}
Đoạn mã Kotlin ở phút 16:50 trong video "Bố cục màn hình thích ứng với Grid và FlexBox"
Grid(
    config = {
        repeat(3) { column(1.fr) }
        repeat(3) { row(100.dp) }
        gap(8.dp)
    }
) {
    // Spans 2x2 area (rows 1..2, columns 1..2)
    HeroCard(modifier = Modifier.gridItem(rows = 1..2, columns = 1..2))

    // Overlaps on top of HeroCard in cell (1, 1) aligned to TopStart!
    LiveBadge(
        modifier = Modifier.gridItem(
            row = 1,
            column = 1,
            alignment = Alignment.TopStart
        )
    )

    // Negative index: placed in last row (-1), spanning all 3 columns
    FooterBar(modifier = Modifier.gridItem(row = -1, column = 1, columnSpan = 3))
}
Đoạn mã Kotlin ở phút 17:59 trong video "Bố cục màn hình thích ứng với Grid và FlexBox"
@Composable
fun VideoPlayerLayout(
   player: @Composable () -> Unit,
   controller: @Composable () -> Unit,
   modifier: Modifier = Modifier
) {
   when(currentVideoPlayerLayout()) {
       VideoPlayerLayout.Default -> DefaultLayout(player, controller, modifier)
       VideoPlayerLayout.Tabletop -> TabletopLayout(player, controller, modifier)
   }
}
Đoạn mã Kotlin ở phút 18:51 trong video "Bố cục màn hình thích ứng bằng Grid và FlexBox"
enum class VideoPlayerArea { Player, Controller }

Grid(
    config = { ... },
    modifier = modifier
) {
    Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
        player()
    }
    Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
       controller()
   }
}
Đoạn mã Kotlin ở phút 19:20 trong video "Bố cục màn hình thích ứng với Grid và FlexBox"
object VideoPlayerGridConfiguration {
   val Default: GridConfigurationScope.() -> Unit = {
       row(1f)
       column(1f)
       area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
       area(areaId = VideoPlayerArea.Controller, row = 1, column = 1)
   }

   val Tabletop: GridConfigurationScope.() -> Unit = {
       row(0.5f)
       row(0.5f)
       column(1f)
       area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
       area(areaId = VideoPlayerArea.Controller, row = 2, column = 1)
   }
}
Đoạn mã Kotlin ở phút 20:00 trong video "Bố cục màn hình thích ứng bằng Grid và FlexBox"
@Composable
fun VideoPlayerLayout(
   player: @Composable () -> Unit,
   controller: @Composable () -> Unit,
   modifier: Modifier = Modifier
) {
   Grid(
       config = { mediaQuery {
       if (isTabletopPosture) { VideoPlayerGridConfiguration.Tabletop }
 else { VideoPlayerGridConfiguration.Default }
}
   }
       modifier = modifier
   ) {
       Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
           player()
       }
       Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
           controller()
       }
   }
}
Đoạn mã Kotlin ở phút 20:16 trong video "Bố cục màn hình thích ứng với Grid và FlexBox"
@Composable
fun AdaptiveActionHeader(tags: List<String>) {
        FlexBox(
        config = {
            direction( if (isCompact)
FlexDirection.Column else FlexDirection.Row )
            wrap(FlexWrap.Wrap)
            alignItems(FlexAlignItems.Center)
            gap(row = 8.dp, column = 12.dp)
        },
        modifier = Modifier.fillMaxWidth()
    ) {
        Text(
            text = "Home",
            modifier = Modifier.flex {
                basis(FlexBasis.Auto)
                grow(1f)
                shrink(1f)
            }
        )
        tags.forEach { tag ->
            FilterChipItem(
                label = tag,
                modifier = Modifier.flex { shrink(0f) }
            )
        }
    }
}
Đoạn mã Kotlin ở phút 22:09 trong video "Cấu trúc bố cục cấp ứng dụng và Navigation 3"
val UiMediaScope.windowSizeClass: WindowSizeClass
    @Composable get() = derivedMediaQuery {
        WindowSizeClass.BREAKPOINTS_V2.computeWindowSizeClass(
            widthDp = windowWidth.value,
            heightDp = windowHeight.value
        )
    }

@Composable
fun AppRoot() {
    val sizeClass = mediaQuery { windowSizeClass }
    val isExpandedWidth = sizeClass.isWidthAtLeastBreakpoint(
        WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND // 600dp
    )
    // Drive app-level navigation and scene strategies...
}
Đoạn mã Kotlin ở phút 23:33 trong video "Cấu trúc bố cục cấp ứng dụng và Navigation 3"
if (isExpandedWidth) {
    /* return a multi-pane scene */
}
else {
    /* return a single pane scene */
}
Đoạn mã shell ở phút 25:18 trong video "Bản minh hoạ về quy trình phát triển và tự động hoá dựa trên tác nhân"
android skills list
android skills add adaptive