App có nút mở tài liệu, nhưng trên một thiết bị nút không làm gì vì code kiểm tra resolveActivity trả null rồi bỏ qua. Với Android có giới hạn package visibility, việc truy vấn app có thể nhìn thấy và việc gửi intent để mở Activity không phải cùng một hợp đồng.
Bài đối chiếu Android documentation ngày 30/10/2026, tập trung vào thao tác mở một liên kết HTTPS từ Activity sau khi người dùng bấm nút. Phần package visibility đặc biệt liên quan app target Android 11/API 30 trở lên. Code được rà theo API và XML được kiểm tra cú pháp; chưa build Kotlin/Android hoặc chạy trên thiết bị.
Xác định hành động và kết quả mong đợi
ACTION_VIEW mô tả yêu cầu xem dữ liệu ở URI. Với liên kết HTTPS, hệ thống có thể mở trình duyệt, một ứng dụng hỗ trợ deep link, hoặc hiển thị hộp chọn. Không nên đặt tên flow là “luôn mở đúng browser X” nếu bạn chưa chỉ định và kiểm tra hợp đồng khác.
Android common intents mô tả cách mở web URL bằng intent. Khi gửi được intent thành công, bạn mới biết hệ thống đã tiếp nhận yêu cầu mở Activity; điều đó không xác nhận trang đã tải thành công hoặc thiết bị có mạng.
Ví dụ dùng URL tài liệu cố định, không nhận scheme tùy ý từ input. Nếu chuyển sang URL do người dùng hoặc server cung cấp, cần định nghĩa các URL được phép mở riêng, thay vì chỉ thay hằng chuỗi rồi coi mọi URI đều tương đương.
Chuẩn bị chuỗi giao diện
Thêm các chuỗi sau vào resources của project. Giữ thông báo lỗi có hành động dễ hiểu, không hiển thị stack trace cho người dùng:
<resources>
<string name="open_docs">Mở tài liệu Android</string>
<string name="no_link_handler">Không có ứng dụng phù hợp để mở liên kết.</string>
</resources>
Nếu file strings.xml đã có thẻ resources, chỉ thêm hai string bên trong thẻ đó. Bài không yêu cầu tạo hai root resources trong cùng file. Khi thêm bản dịch, giữ nguyên tên resource để code không phải đổi theo ngôn ngữ.
Gửi intent và bắt đúng ca không có Activity
Đặt extension sau trong package của project để truy cập R của ứng dụng. Caller phải là một Activity đang hoạt động:
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.widget.Toast
fun Activity.openDeveloperDocs() {
val link = Uri.parse("https://developer.android.com/")
val request = Intent(Intent.ACTION_VIEW, link)
try {
startActivity(request)
} catch (_: ActivityNotFoundException) {
Toast.makeText(this, R.string.no_link_handler, Toast.LENGTH_LONG).show()
}
}
Trong Activity có sẵn một Button tên helpButton, gắn hành động vào thao tác bấm:
helpButton.setText(R.string.open_docs)
helpButton.setOnClickListener { openDeveloperDocs() }
helpButton là Button của màn hình đang có, không phải biến global do Android tự tạo. Cách lấy view tùy layout hoặc View Binding của project. Extension nhận Activity nên không cần thêm NEW_TASK chỉ để bù cho một application context bị dùng nhầm.
ActivityNotFoundException API xác nhận exception này dùng khi không tìm được Activity thực hiện intent. Không mở rộng catch thành mọi Exception để làm im cả lỗi lập trình hoặc lỗi quyền có nguyên nhân khác.
Vì sao không bắt buộc resolveActivity trước?
Hướng dẫn package visibility cho mở URL khuyến nghị gọi intent và xử lý ActivityNotFoundException trong flow mở thông thường. startActivity không cần package visibility để khởi chạy Activity của app khác theo cách được tài liệu mô tả.
Vì vậy, không cần tự thêm queries chỉ để làm điều kiện “có nhìn thấy app thì mới được thử mở URL” cho flow này. Null từ một phép truy vấn không nên bị diễn giải máy móc thành “chắc chắn không thể mở”.
Nếu UI thực sự cần biết khả năng xử lý trước khi người dùng bấm, đó là nhu cầu truy vấn riêng. Khi ấy khai báo queries phù hợp với use case và đọc giới hạn của kết quả. Đừng thêm QUERY_ALL_PACKAGES chỉ để sửa một nút mở tài liệu.
Kiểm tra các nhánh có ý nghĩa
Trên thiết bị thử, kiểm tra trường hợp có browser mặc định, có nhiều app nhận URL và môi trường không có Activity phù hợp nếu bạn có thể tạo fixture đó. Nhánh cuối phải cho phản hồi thay vì làm app crash. Bài chưa thực hiện những phép thử thiết bị này.
Tắt mạng rồi bấm nút là một ca khác: app nhận có thể vẫn mở bình thường và sau đó báo lỗi mạng. Đừng dùng việc startActivity không ném lỗi làm phép kiểm tra URL online.
Kiểm tra nút có tên đọc được bằng screen reader, có thể bấm lại sau khi quay về app và không tự mở liên kết khi màn hình vừa xuất hiện. Không đưa token hoặc dữ liệu riêng tư vào URL tài liệu; ví dụ chỉ dùng trang công khai.
Lỗi thường gặp và giới hạn
- Gộp query visibility với quyền launch Activity thành một điều kiện duy nhất.
- Coi ACTION_VIEW bảo đảm luôn mở browser thay vì app hỗ trợ liên kết.
- Dùng application context rồi thêm cờ mà không xem lại ngữ cảnh điều hướng.
- Bắt mọi exception và báo “không có browser” cho mọi lỗi.
Bài này xử lý liên kết đi ra khỏi app, không cấu hình app nhận deep link từ bên ngoài và không thiết lập Custom Tabs. Hãy chốt yêu cầu mở trước; với thao tác xem URL thông thường, gửi intent rõ ràng và xử lý đúng ca không có app nhận giúp flow dễ hiểu hơn.

