Những điểm cần cân nhắc khác

Mặc dù việc di chuyển từ Khung hiển thị sang Compose chỉ liên quan đến giao diện người dùng, nhưng bạn cần xem xét nhiều thứ để có thể di chuyển dần và an toàn. Trang này trình bày một số điểm cần cân nhắc trong khi di chuyển ứng dụng dựa trên Khung hiển thị sang Compose.

Di chuyển giao diện của ứng dụng

Bạn nên sử dụng hệ thống thiết kế Material Design để thiết kế giao diện cho các ứng dụng Android.

Có 3 phiên bản Material dành cho các ứng dụng dựa trên Khung hiển thị:

  • Material Design 1 dùng thư viện AppCompat (cụ thể là Theme.AppCompat.*)
  • Material Design 2 dùng thư viện MDC-Android (cụ thể là Theme.MaterialComponents.*)
  • Material Design 3 dùng thư viện MDC-Android (cụ thể là Theme.Material3.*)

Có 2 phiên bản Material dành cho các ứng dụng Compose:

  • Material Design 2 dùng thư viện Compose Material (cụ thể là androidx.compose.material.MaterialTheme)
  • Material Design 3 dùng thư viện Compose Material 3 (cụ thể là androidx.compose.material3.MaterialTheme)

Nếu có thể, bạn nên sử dụng phiên bản mới nhất (Material 3) trong trường hợp hệ thống thiết kế của ứng dụng cho phép. Bạn có thể tham khảo hướng dẫn di chuyển cho cả Khung hiển thị và Compose theo đường liên kết dưới đây:

Khi tạo màn hình mới trong ứng dụng Compose, bất kể bạn đang sử dụng phiên bản Material Design nào, hãy đảm bảo rằng bạn áp dụng MaterialTheme trước mọi thành phần kết hợp tạo ra giao diện người dùng từ thư viện Compose Material. Các thành phần Material (Button, Text, v.v.) phụ thuộc vào việc có sẵn MaterialTheme hay không. Nếu không có, thì hành vi của các thành phần đó sẽ không được xác định.

Tất cả các mẫu Jetpack Compose đều sử dụng giao diện Compose tuỳ chỉnh dựa trên MaterialTheme.

Hãy xem các bài viết Hệ thống thiết kế trong ComposeDi chuyển giao diện XML sang Compose để tìm hiểu thêm.

Nếu sử dụng thành phần Điều hướng trong ứng dụng, hãy xem phần Khả năng tương tác trong hướng dẫn Điều hướng với Compose.

Kiểm thử giao diện người dùng hỗn hợp cho Compose/Khung hiển thị

Sau khi di chuyển các phần của ứng dụng sang Compose, việc kiểm thử là rất quan trọng để đảm bảo không có bất cứ vấn đề gì.

Khi một hoạt động hoặc mảnh sử dụng Compose, bạn cần dùng createAndroidComposeRule thay vì sử dụng ActivityScenarioRule. createAndroidComposeRule tích hợp ActivityScenarioRule với ComposeTestRule cho phép bạn kiểm thử cùng lúc mã Compose và Khung hiển thị.

class MyActivityTest {
    @Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule<MyActivity>()

    @Test
    fun testGreeting() {
        val greeting = InstrumentationRegistry.getInstrumentation()
            .targetContext.resources.getString(R.string.greeting)

        composeTestRule.onNodeWithText(greeting).assertIsDisplayed()
    }
}

Hãy xem bài viết Kiểm thử bố cục Compose để tìm hiểu thêm về việc kiểm thử và khả năng tương tác với Espresso.

Tích hợp Compose vào cấu trúc ứng dụng hiện có

Mô hình cấu trúc Luồng dữ liệu một chiều (UDF) hoạt động liền mạch với Compose. Nếu ứng dụng dùng các loại mô hình cấu trúc khác, chẳng hạn như Mô hình – Khung hiển thị – Thành phần trình bày (MVP), bạn nên di chuyển phần giao diện người dùng đó sang UDF trước hoặc trong khi sử dụng Compose.

Sử dụng ViewModel trong Compose

Nếu sử dụng thư viện Thành phần cấu trúcViewModel, bạn có thể truy cập vào ViewModel từ bất kỳ thành phần kết hợp nào bằng cách gọi hàm viewModel() như giải thích trong bài viết Compose và các thư viện khác.

Khi dùng Compose, hãy cẩn thận về việc sử dụng cùng một loại ViewModel trong nhiều thành phần kết hợp dưới dạng phần tử ViewModel tuân theo phạm vi vòng đời của Khung hiển thị. Phạm vi này sẽ là hoạt động lưu trữ, mảnh hoặc biểu đồ điều hướng nếu sử dụng thư viện Điều hướng.

Ví dụ: nếu các thành phần kết hợp được lưu trữ trong một hoạt động, thì viewModel() luôn trả về cùng một thực thể, thực thể này chỉ bị xoá khi hoạt động đó kết thúc. Ở ví dụ sau, cùng một người dùng ("user1") sẽ được chào hai lần vì cùng một thực thể GreetingViewModel được sử dụng lại trong mọi thành phần kết hợp dưới hoạt động lưu trữ. Thực thể ViewModel đầu tiên đã tạo được sử dụng lại trong các thành phần kết hợp khác.

class GreetingActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MaterialTheme {
                Column {
                    GreetingScreen("user1")
                    GreetingScreen("user2")
                }
            }
        }
    }
}

@Composable
fun GreetingScreen(
    userId: String,
    viewModel: GreetingViewModel = viewModel(
        factory = GreetingViewModelFactory(userId)
    )
) {
    val messageUser by viewModel.message.observeAsState("")
    Text(messageUser)
}

class GreetingViewModel(private val userId: String) : ViewModel() {
    private val _message = MutableLiveData("Hi $userId")
    val message: LiveData<String> = _message
}

class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return GreetingViewModel(userId) as T
    }
}

Biểu đồ điều hướng cũng xác định phạm vi các phần tử ViewModel. Vì thế, những thành phần kết hợp là đích đến trong biểu đồ điều hướng sẽ có một thực thể ViewModel khác. Trong trường hợp này, ViewModel nằm trong phạm vi vòng đời của đích đến và sẽ bị xoá khi đích đến bị xoá khỏi ngăn xếp lùi. Trong ví dụ sau đây, khi người dùng chuyển đến màn hình Hồ sơ, thao tác này sẽ tạo một thực thể mới của GreetingViewModel.

@Composable
fun MyApp() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

Nguồn trạng thái đáng tin

Khi bạn sử dụng Compose trong một phần của giao diện người dùng, có thể Compose và mã hệ thống Khung hiển thị sẽ phải chia sẻ dữ liệu. Khi có thể, bạn nên đóng gói trạng thái được chia sẻ đó trong một lớp khác tuân theo các phương pháp hay nhất của UDF mà cả hai nền tảng sử dụng, chẳng hạn như trong ViewModel hiển thị luồng dữ liệu được chia sẻ để phát các nội dung cập nhật dữ liệu.

Tuy nhiên, không phải lúc nào điều đó cũng có thể xảy ra nếu dữ liệu được chia sẻ có thể thay đổi hoặc liên kết chặt chẽ với một phần tử trên giao diện người dùng. Trong trường hợp đó, một hệ thống phải là nguồn đáng tin và hệ thống đó cần chia sẻ mọi nội dung cập nhật dữ liệu cho hệ thống kia. Theo quy tắc chung, nguồn đáng tin phải thuộc sở hữu của thành phần gần hơn với gốc của hệ phân cấp giao diện người dùng.

Compose là nguồn đáng tin

Sử dụng thành phần kết hợp SideEffect để xuất bản trạng thái Compose thành các mã không phải Compose. Trong trường hợp này, nguồn đáng tin sẽ được giữ lại trong một thành phần kết hợp có nhiệm vụ gửi các lượt cập nhật trạng thái.

Ví dụ: thư viện phân tích của bạn có thể cho phép bạn phân đoạn toàn bộ số người dùng bằng cách đính kèm siêu dữ liệu tuỳ chỉnh (thuộc tính người dùng trong ví dụ này) vào mọi sự kiện phân tích tiếp theo. Để truyền đạt thông tin loại người dùng của người dùng hiện tại cho thư viện phân tích, hãy sử dụng SideEffect để cập nhật giá trị.

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        /* ... */
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

Để biết thêm thông tin, hãy xem trang Hiệu ứng phụ trong Compose.

Hệ thống khung hiển thị là nguồn đáng tin

Nếu hệ thống chế độ xem sở hữu trạng thái và chia sẻ trạng thái đó với Compose, bạn nên bao gồm trạng thái đó trong các đối tượng mutableStateOf để trạng thái an toàn theo luồng cho Compose. Nếu bạn sử dụng cách tiếp cận này, các hàm có khả năng kết hợp được đơn giản hoá vì các hàm đó không còn nguồn đáng tin nữa. Tuy nhiên, hệ thống Khung hiển thị cần cập nhật trạng thái có thể thay đổi và các Khung hiển thị sử dụng trạng thái đó.

Trong ví dụ sau, một CustomViewGroup chứa TextViewComposeView có thành phần kết hợp TextField bên trong. TextView cần hiển thị nội dung mà người dùng nhập vào trong TextField.

class CustomViewGroup @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {

    // Source of truth in the View system as mutableStateOf
    // to make it thread-safe for Compose
    private var text by mutableStateOf("")

    private val textView: TextView

    init {
        orientation = VERTICAL

        textView = TextView(context)
        val composeView = ComposeView(context).apply {
            setContent {
                MaterialTheme {
                    TextField(value = text, onValueChange = { updateState(it) })
                }
            }
        }

        addView(textView)
        addView(composeView)
    }

    // Update both the source of truth and the TextView
    private fun updateState(newValue: String) {
        text = newValue
        textView.text = newValue
    }
}

Di chuyển giao diện người dùng dùng chung

Nếu đang chuyển dần sang ứng dụng Compose, bạn có thể cần sử dụng các phần tử dùng chung trên giao diện người dùng trong cả Compose và hệ thống Khung hiển thị. Ví dụ: nếu ứng dụng của bạn có một thành phần CallToActionButton tuỳ chỉnh, thì bạn có thể cần sử dụng thành phần đó trong cả màn hình Compose và màn hình dựa trên Khung hiển thị.

Trong Compose, các phần tử dùng chung trên giao diện người dùng sẽ trở thành các thành phần kết hợp có thể dùng lại trên ứng dụng, bất kể phần tử đó được tạo kiểu bằng XML hay khung hiển thị tuỳ chỉnh. Ví dụ: bạn sẽ tạo một thành phần kết hợp CallToActionButton cho thành phần Button kêu gọi hành động tuỳ chỉnh.

Để sử dụng thành phần kết hợp này trong màn hình dựa trên Khung hiển thị, hãy tạo một trình bao bọc khung hiển thị tuỳ chỉnh mở rộng từ AbstractComposeView. Trong thành phần kết hợp Content bị ghi đè, hãy đặt thành phần kết hợp mà bạn tạo trong giao diện Compose như minh hoạ ở ví dụ dưới đây:

@Composable
fun CallToActionButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = MaterialTheme.colorScheme.secondary
        ),
        onClick = onClick,
        modifier = modifier,
    ) {
        Text(text)
    }
}

class CallToActionViewButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {

    var text by mutableStateOf("")
    var onClick by mutableStateOf({})

    @Composable
    override fun Content() {
        YourAppTheme {
            CallToActionButton(text, onClick)
        }
    }
}

Lưu ý rằng các tham số của thành phần kết hợp đó sẽ trở thành biến có thể thay đổi bên trong khung hiển thị tuỳ chỉnh. Nhờ vậy, khung hiển thị CallToActionViewButton tuỳ chỉnh có thể tăng cường và sử dụng được, như khung hiển thị truyền thống. Hãy xem một ví dụ về trường hợp này với Liên kết khung hiển thị ở bên dưới:

class ViewBindingActivity : ComponentActivity() {

    private lateinit var binding: ActivityExampleBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityExampleBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.callToAction.apply {
            text = getString(R.string.greeting)
            onClick = { /* Do something */ }
        }
    }
}

Nếu thành phần tuỳ chỉnh chứa trạng thái có thể thay đổi, hãy xem phần Nguồn trạng thái đáng tin.

WindowInsets và Ảnh động IME

Kể từ Compose 1.2.0, bạn có thể sử dụng đối tượng sửa đổi để xử lý WindowInsets trong bố cục. Ảnh động IME cũng được hỗ trợ.

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
              MyScreen()
            }
        }
    }
}

@Composable
fun MyScreen() {
    Box {
        LazyColumn(
            modifier = Modifier
                .fillMaxSize() // fill the entire window
                .imePadding() // padding for the bottom for the IME
                .imeNestedScroll(), // scroll IME at the bottom
            content = { }
        )
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding() // padding for navigation bar
                .imePadding(), // padding for when IME appears
            onClick = { }
        ) {
            Icon( /* ... */)
        }
    }
}

Ảnh động hiển thị phần tử trên giao diện người dùng cuộn lên và xuống để nhường chỗ cho bàn phím

Hình 1. Ảnh động IME

Ưu tiên phân chia trạng thái khỏi bản trình bày

Theo truyền thống, View là một trạng thái. View quản lý các trường mô tả nội dung cần hiển thị, ngoài cách thức hiển thị nội dung đó. Khi bạn chuyển đổi View sang Compose, hãy tìm cách tách riêng dữ liệu đang hiển thị sang luồng dữ liệu một chiều, như đã giải thích thêm ở phần chuyển trạng thái lên trên (state hoisting).

Ví dụ: View có thuộc tính visibility mô tả liệu thuộc tính này đang hiện, ẩn hay đã biến mất. Đây là một thuộc tính vốn có của View. Mặc dù các đoạn mã khác có thể làm thay đổi chế độ hiển thị của View, nhưng bản thân View chỉ thực sự biết chế độ hiển thị hiện tại của mã đó. Logic để đảm bảo rằng View hiển thị có thể dễ gặp lỗi và thường liên quan đến chính View.

Ngược lại, Compose giúp bạn dễ dàng hiển thị các thành phần kết hợp hoàn toàn khác nhau bằng cách sử dụng logic có điều kiện trong Kotlin:

if (showCautionIcon) {
    CautionIcon(/* ... */)
}

Theo thiết kế, CautionIcon không cần phải biết hoặc quan tâm đến lý do tại sao nội dung đó hiển thị và không có khái niệm về visibility: nội dung này nằm trong Thành phần kết hợp hoặc không.

Bằng cách tách biệt hoạt động quản lý trạng thái và logic trình bày, bạn có thể tự do thay đổi cách hiển thị nội dung dưới dạng lượt chuyển đổi trạng thái thành giao diện người dùng. Nhờ khả năng chuyển trạng thái lên trên khi cần, bạn cũng có thể sử dụng lại các thành phần kết hợp nhiều lần hơn vì quyền sở hữu trạng thái linh hoạt hơn.

Tăng cấp các thành phần đã đóng gói và có thể sử dụng lại

Các phần tử View thường biết vị trí tồn tại của chính mình: bên trong Activity, Dialog, Fragment hoặc vị trí nào đó bên trong một hệ phân cấp View khác. Do những thành phần này thường được tăng cường từ các tệp bố cục tĩnh, nên cấu trúc tổng thể của View có xu hướng rất cứng nhắc. Điều này dẫn đến việc ghép nối chặt chẽ hơn và khiến View khó thay đổi hoặc sử dụng lại hơn.

Ví dụ: View tuỳ chỉnh có thể giả định rằng khung hiển thị này có khung hiển thị con thuộc một loại nhất định với một mã nhận dạng nhất định, đồng thời trực tiếp thay đổi các thuộc tính của loại đó để phản hồi một số hành động. Cách này giúp ghép nối chặt chẽ các phần tử View đó lại với nhau: thành phần View tuỳ chỉnh có thể gặp sự cố hoặc bị lỗi nếu không tìm thấy thành phần con và có thể không sử dụng lại được nếu không có thành phần mẹ View tuỳ chỉnh.

Cách này ít gặp vấn đề hơn trong Compose nhờ các thành phần kết hợp có thể sử dụng lại. Thành phần mẹ có thể dễ dàng chỉ định trạng thái và lệnh gọi lại, nhờ đó, bạn có thể viết các thành phần kết hợp có thể sử dụng lại mà không cần phải biết chính xác vị trí sẽ sử dụng chúng.

var isEnabled by rememberSaveable { mutableStateOf(false) }

Column {
    ImageWithEnabledOverlay(isEnabled)
    ControlPanelWithToggle(
        isEnabled = isEnabled,
        onEnabledChanged = { isEnabled = it }
    )
}

Trong ví dụ trên, cả ba phần đều được đóng gói nhiều hơn và ghép nối ít hơn:

  • ImageWithEnabledOverlay chỉ cần biết trạng thái hiện tại của isEnabled chứ không cần biết ControlPanelWithToggle có tồn tại hay không hoặc thậm chí là làm thế nào để kiểm soát.

  • ControlPanelWithToggle không biết rằng ImageWithEnabledOverlay tồn tại. Có thể không có, một hoặc nhiều cách để isEnabled hiển thị và ControlPanelWithToggle sẽ không phải thay đổi.

  • Đối với thành phần mẹ, bạn không cần quan tâm ImageWithEnabledOverlay hoặc ControlPanelWithToggle lồng nhau ở mức độ nào. Các thành phần con đó có thể đang tạo ảnh động cho các thay đổi, hoán đổi nội dung hoặc chuyển nội dung cho các thành phần con khác.

Mẫu này còn được gọi là đảo ngược quyền kiểm soát mà bạn có thể đọc thêm trong tài liệu về CompositionLocal.

Xử lý các thay đổi về kích thước màn hình

Một trong những cách chính để tạo bố cục View thích ứng là sử dụng nhiều tài nguyên cho các kích thước cửa sổ khác nhau. Mặc dù bạn vẫn có thể lựa chọn các tài nguyên đủ điều kiện khi đưa ra các quyết định về bố cục ở cấp màn hình, nhưng Compose sẽ giúp bạn thay đổi toàn bộ bố cục dễ dàng hơn chỉ bằng mã với logic có điều kiện thông thường. Hãy xem phần Hỗ trợ nhiều kích thước màn hình để tìm hiểu thêm.

Ngoài ra, hãy tham khảo bài viết Tạo bố cục thích ứng nếu bạn muốn tìm hiểu về các kỹ thuật mà Compose cung cấp để tạo giao diện người dùng thích ứng.

Cuộn dạng lồng với Khung hiển thị

Để biết thêm thông tin về cách hỗ trợ khả năng tương tác cuộn dạng lồng giữa các phần tử Khung hiển thị có thể cuộn và thành phần kết hợp có thể cuộn, được lồng theo cả hai hướng, hãy đọc qua phần Khả năng tương tác cuộn dạng lồng.

Compose trong RecyclerView

Các thành phần kết hợp trong RecyclerView hoạt động hiệu quả kể từ RecyclerView phiên bản 1.3.0-alpha02. Hãy đảm bảo bạn đang sử dụng ít nhất là phiên bản 1.3.0-alpha02 của RecyclerView để thấy được những lợi ích đó.