AndroidView trong Jetpack Compose
1. Giới thiệu
AndroidView là một Composable đặc biệt trong Jetpack Compose cho phép bạn nhúng các View truyền thống (XML-based Views) vào trong giao diện Compose. Đây là cầu nối quan trọng giữa hệ thống View cũ và Compose mới.
Tại sao cần AndroidView?
| Trường hợp | Ví dụ |
|---|---|
| View chưa có trong Compose | WebView, MapView, AdView |
| Thư viện bên thứ ba | Video player (ExoPlayer), Chart libraries |
| Migration dần dần | Chuyển đổi từ XML sang Compose từng bước |
| Custom Views phức tạp | Các view tự tạo đã có sẵn |
Lưu ý: Compose đang phát triển nhanh và nhiều components mới được thêm vào. Tuy nhiên, AndroidView vẫn cần thiết cho các trường hợp đặc biệt như WebView.
2. Cú pháp cơ bản
@Composable
fun <T : View> AndroidView(
factory: (Context) -> T,
modifier: Modifier = Modifier,
update: (T) -> Unit = { }
)Các tham số:
| Tham số | Mô tả |
|---|---|
factory | Lambda được gọi một lần để tạo View. Nhận Context và trả về instance của View |
modifier | Modifier để điều chỉnh kích thước, padding, căn chỉnh… |
update | Lambda được gọi mỗi khi recomposition xảy ra. Dùng để cập nhật View theo state |
3. Ví dụ: WebView trong Compose
@Composable
fun WebViewScreen(url: String) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
WebView(context).apply {
settings.javaScriptEnabled = true
webViewClient = WebViewClient() // Giữ navigation trong app
}
},
update = { webView ->
webView.loadUrl(url)
}
)
}
// Sử dụng
@Composable
fun MyScreen() {
WebViewScreen(url = "https://www.google.com")
}Giải thích:
factory: Chỉ chạy một lần khi Composable được tạo. Khởi tạo và cấu hình WebView tại đây.update: Chạy mỗi khiurlthay đổi, cho phép cập nhật mà không tạo lại WebView.webViewClient: Giữ điều hướng bên trong WebView thay vì mở trình duyệt ngoài.
4. Các ví dụ thực tế
4.1 VideoView
@Composable
fun VideoPlayerView(videoUri: Uri) {
AndroidView(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
factory = { context ->
VideoView(context).apply {
setMediaController(MediaController(context))
}
},
update = { videoView ->
videoView.setVideoURI(videoUri)
videoView.start()
}
)
}4.2 CalendarView
@Composable
fun CalendarPicker(
onDateSelected: (Long) -> Unit
) {
AndroidView(
modifier = Modifier.wrapContentSize(),
factory = { context ->
CalendarView(context).apply {
setOnDateChangeListener { _, year, month, dayOfMonth ->
val calendar = Calendar.getInstance()
calendar.set(year, month, dayOfMonth)
onDateSelected(calendar.timeInMillis)
}
}
}
)
}4.3 Custom EditText với TextWatcher
@Composable
fun LegacyEditText(
text: String,
onTextChange: (String) -> Unit
) {
AndroidView(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
factory = { context ->
EditText(context).apply {
hint = "Nhập text..."
addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
onTextChange(s?.toString() ?: "")
}
override fun afterTextChanged(s: Editable?) {}
})
}
},
update = { editText ->
if (editText.text.toString() != text) {
editText.setText(text)
}
}
)
}5. WebView nâng cao
5.1 WebView với Back Navigation
@Composable
fun AdvancedWebView(url: String) {
var webView by remember { mutableStateOf<WebView?>(null) }
BackHandler(enabled = webView?.canGoBack() == true) {
webView?.goBack()
}
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
WebView(context).apply {
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
loadWithOverviewMode = true
useWideViewPort = true
}
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
return false
}
}
}.also { webView = it }
},
update = { view ->
view.loadUrl(url)
}
)
}5.2 WebView với Loading State
@Composable
fun WebViewWithLoading(url: String) {
var isLoading by remember { mutableStateOf(true) }
Box(modifier = Modifier.fillMaxSize()) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
WebView(context).apply {
settings.javaScriptEnabled = true
webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
isLoading = true
}
override fun onPageFinished(view: WebView?, url: String?) {
isLoading = false
}
}
loadUrl(url)
}
}
)
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
}
}6. AndroidViewBinding - Sử dụng View Binding
Nếu bạn có layout XML đã định nghĩa sẵn, có thể dùng AndroidViewBinding:
@Composable
fun LegacyLayoutInCompose() {
AndroidViewBinding(MyLegacyLayoutBinding::inflate) { binding ->
binding.textView.text = "Hello from Compose!"
binding.button.setOnClickListener {
// Handle click
}
}
}Cần thêm dependency:
implementation("androidx.compose.ui:ui-viewbinding")7. Lifecycle và Memory Management
7.1 DisposableEffect cho Cleanup
@Composable
fun WebViewWithLifecycle(url: String) {
val context = LocalContext.current
val webView = remember {
WebView(context).apply {
settings.javaScriptEnabled = true
}
}
DisposableEffect(Unit) {
onDispose {
webView.stopLoading()
webView.destroy()
}
}
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { webView },
update = { it.loadUrl(url) }
)
}7.2 Lifecycle Observer
@Composable
fun LifecycleAwareWebView(url: String) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val webView = remember { WebView(context) }
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> webView.onResume()
Lifecycle.Event.ON_PAUSE -> webView.onPause()
Lifecycle.Event.ON_DESTROY -> webView.destroy()
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { webView },
update = { it.loadUrl(url) }
)
}8. Best Practices
✅ Nên làm
| Practice | Lý do |
|---|---|
Khởi tạo View trong factory | Đảm bảo View chỉ được tạo một lần |
Cập nhật trong update | Phản ứng với state changes |
Dùng remember khi cần giữ reference | Tránh tạo lại khi recomposition |
Cleanup trong DisposableEffect | Giải phóng resources đúng cách |
Thêm webViewClient | Kiểm soát navigation |
❌ Không nên
| Anti-pattern | Vấn đề |
|---|---|
Tạo View ngoài factory | View có thể bị tạo lại nhiều lần |
Load URL trong factory | Không thể cập nhật URL khi state thay đổi |
| Quên destroy WebView | Memory leak |
| Không handle back press | UX kém khi dùng WebView |
9. So sánh AndroidView vs Compose Native
| Tiêu chí | AndroidView | Compose Native |
|---|---|---|
| Performance | Overhead nhỏ do bridge | Tối ưu nhất |
| Complexity | Đơn giản hơn nếu đã có View | Cần viết mới |
| Maintenance | Phụ thuộc View system | Thuần Compose |
| Use case | WebView, Maps, 3rd party | Mọi thứ khác |
10. Permissions cần thiết
Với WebView, cần thêm permission trong AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />11. Tổng kết
- AndroidView là cầu nối giữa View System truyền thống và Jetpack Compose
- Sử dụng
factoryđể tạo View một lần - Sử dụng
updateđể cập nhật View khi state thay đổi - Luôn xử lý lifecycle và cleanup đúng cách với
DisposableEffect - Ưu tiên dùng Compose native components khi có thể
- AndroidView hữu ích cho: WebView, MapView, VideoView, và các thư viện bên thứ ba
Last updated on