Mục lục
Lời mở đầu: Tầm quan trọng của các thành phần UI linh hoạt
Chào mừng các bạn quay trở lại với chuỗi bài viết “Android Developer Roadmap”! Sau khi đã tìm hiểu về các View cơ bản và xây dựng danh sách động với RecyclerView, bạn đã có nền tảng vững chắc để tạo ra các màn hình ứng dụng. Tuy nhiên, giao diện người dùng hiện đại không chỉ dừng lại ở việc hiển thị thông tin tĩnh. Chúng cần sự linh hoạt để thích ứng với các kích thước màn hình khác nhau, cung cấp các tương tác nhanh gọn, và hiển thị thông tin bổ sung mà không làm gián đoạn luồng chính.
Đây chính là lúc các thành phần như Fragments, Dialogs, Bottom Sheets và Drawers phát huy sức mạnh của mình. Chúng cho phép bạn xây dựng các khối UI có thể tái sử dụng, tạo ra các cửa sổ pop-up hoặc bảng điều khiển tạm thời, và tổ chức điều hướng một cách hiệu quả. Nắm vững cách sử dụng chúng là một bước tiến quan trọng trên con đường trở thành một Lập trình viên Android chuyên nghiệp.
Bài viết này sẽ đi sâu vào từng thành phần, giải thích mục đích, cách sử dụng và các trường hợp áp dụng điển hình. Hãy cùng bắt đầu nhé!
Fragments: Những “mảnh ghép” linh hoạt của giao diện
Fragment là gì?
Theo định nghĩa chính thức của Android, Fragment đại diện cho một phần hành vi hoặc giao diện người dùng trong một Activity. Bạn có thể coi Fragment như một mô-đun UI hoặc hành vi có thể tái sử dụng được đặt bên trong một Activity. Một Activity có thể chứa nhiều Fragment, và bạn có thể quản lý chúng trong Activity’s back stack, giống như cách bạn quản lý các Activity trong back stack của ứng dụng.
Tại sao lại cần Fragments?
Ban đầu, Fragment được giới thiệu để hỗ trợ thiết kế ứng dụng cho máy tính bảng, nơi bạn thường cần hiển thị nhiều nội dung cùng lúc trên màn hình lớn. Ví dụ, một ứng dụng tin tức có thể hiển thị danh sách các tiêu đề ở một bên (một Fragment) và chi tiết bài báo khi một tiêu đề được chọn ở bên còn lại (một Fragment khác). Cả hai Fragment này cùng tồn tại trong một Activity duy nhất.
Ngày nay, Fragment không chỉ dành cho máy tính bảng. Chúng là công cụ mạnh mẽ để:
- Tái sử dụng UI: Tạo một Fragment cho một chức năng cụ thể (ví dụ: bộ chọn ngày, form đăng nhập) và sử dụng nó trong nhiều Activity khác nhau.
- Hỗ trợ các kích thước màn hình khác nhau: Sử dụng các layout thay thế (alternative layouts) để định nghĩa cách các Fragment được kết hợp trong Activity tùy thuộc vào kích thước màn hình (điện thoại, máy tính bảng).
- Quản lý giao diện phức tạp: Chia nhỏ giao diện người dùng phức tạp của một màn hình lớn thành các Fragment nhỏ hơn, giúp quản lý mã nguồn dễ dàng hơn.
- Điều hướng: Kết hợp với Navigation Component (sẽ nói trong bài viết sau) để quản lý luồng điều hướng giữa các màn hình/Fragment một cách hiệu quả.
Vòng đời của Fragment (Fragment Lifecycle)
Giống như Activity, Fragment cũng có vòng đời riêng, nhưng vòng đời này phụ thuộc vào Activity chứa nó. Các phương thức callback chính trong vòng đời của Fragment bao gồm:
onAttach()
: Fragment được gắn (attach) vào Activity.onCreate()
: Được gọi để khởi tạo Fragment.onCreateView()
: Được gọi để tạo ra View hierarchical cho Fragment. Đây là nơi bạn inflate layout XML của Fragment.onViewCreated()
: Được gọi ngay sau khionCreateView()
trả về, đảm bảo rằng View đã được tạo.onActivityCreated()
: Được gọi khi Activity chứa Fragment đã hoàn tất phương thứconCreate()
của nó.onStart()
: Fragment trở nên hiển thị với người dùng.onResume()
: Fragment hoạt động và sẵn sàng tương tác với người dùng.onPause()
: Fragment đang tạm dừng, thường là vì một Fragment khác đang được hiển thị đè lên.onStop()
: Fragment không còn hiển thị với người dùng.onDestroyView()
: View hierarchical của Fragment bị hủy.onDestroy()
: Fragment đang bị hủy.onDetach()
: Fragment bị tách (detach) khỏi Activity.
Điều quan trọng là hiểu rằng trạng thái của Fragment luôn đồng bộ với trạng thái của Activity chứa nó. Ví dụ, khi Activity bị dừng (stop), tất cả Fragment trong nó cũng sẽ bị dừng.
Sử dụng Fragment: Static vs Dynamic
Có hai cách chính để thêm Fragment vào Activity:
- Static (Tĩnh): Khai báo Fragment trực tiếp trong layout XML của Activity bằng thẻ
<fragment>
. Cách này đơn giản nhưng kém linh hoạt hơn vì bạn không thể thay thế hoặc xóa Fragment này trong thời gian chạy. - Dynamic (Động): Thêm, xóa, thay thế hoặc hiển thị/ẩn Fragment trong thời gian chạy bằng cách sử dụng
FragmentManager
vàFragmentTransaction
. Đây là cách phổ biến và linh hoạt nhất.
Ví dụ về thêm Fragment động:
Giả sử bạn có một Activity với một FrameLayout là container cho Fragment:
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Và một lớp Fragment đơn giản:
class MyFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_my, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Setup views or logic here
// access views like view.findViewById(...) or using view binding
}
}
Để thêm Fragment này vào FrameLayout trong Activity:
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
// Add the fragment dynamically
if (savedInstanceState == null) { // Only add if not restoring state
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, MyFragment())
.commit()
}
}
}
Các thao tác khác như replace()
hoặc remove()
cũng được thực hiện tương tự trên FragmentTransaction
. Bạn có thể thêm transaction vào back stack bằng cách gọi addToBackStack(null)
trước khi commit, cho phép người dùng nhấn nút back để quay lại trạng thái Fragment trước đó. Việc xử lý backstack với Fragment là một khía cạnh quan trọng cần nắm vững.
Giao tiếp giữa các Fragment và Activity
Fragment không nên trực tiếp gọi các phương thức hoặc truy cập các View của Activity hay Fragment khác. Thay vào đó, nên sử dụng các pattern giao tiếp an toàn như:
- Interfaces: Fragment định nghĩa một interface và Activity (hoặc Fragment cha) implement interface đó. Fragment gọi các phương thức của interface để gửi sự kiện.
- ViewModels: Sử dụng ViewModel chung được chia sẻ giữa Activity và Fragment (hoặc giữa các Fragment thông qua Activity) để chia sẻ dữ liệu và trạng thái.
- LiveData/Flow: Sử dụng LiveData hoặc StateFlow/SharedFlow trong ViewModel để Fragment có thể observe dữ liệu hoặc sự kiện từ ViewModel.
Cách tiếp cận thông qua ViewModel/LiveData/Flow là phương pháp được khuyến khích hiện nay, phù hợp với kiến trúc OOP và các nguyên tắc thiết kế hiện đại.
Dialogs: Cửa sổ pop-up cho thông tin quan trọng
Dialog là gì?
Dialog là một cửa sổ pop-up nhỏ xuất hiện đè lên Activity hiện tại, yêu cầu người dùng đưa ra quyết định hoặc nhập thông tin ngắn gọn. Dialog thường làm gián đoạn luồng công việc của người dùng cho đến khi họ tương tác với nó.
Các loại Dialog phổ biến:
- AlertDialog: Dialog linh hoạt nhất, có thể hiển thị tiêu đề, nội dung thông báo, danh sách các mục hoặc layout tùy chỉnh. Thường được sử dụng để hỏi xác nhận (Yes/No, OK/Cancel) hoặc hiển thị thông báo lỗi.
- DatePickerDialog/TimePickerDialog: Cung cấp giao diện chuẩn để chọn ngày hoặc giờ.
- Custom Dialogs: Tạo Dialog với layout hoàn toàn tùy chỉnh của riêng bạn.
Sử dụng DialogFragment
Cách được khuyến nghị để quản lý Dialog trong Android là sử dụng DialogFragment
. DialogFragment
là một Fragment đặc biệt được thiết kế để quản lý vòng đời của Dialog. Điều này giúp Dialog tồn tại qua các thay đổi cấu hình (như xoay màn hình) và quản lý trạng thái của nó một cách đúng đắn, phù hợp với quá trình xử lý trạng thái trong Activity.
Ví dụ về sử dụng AlertDialog với DialogFragment:
Đầu tiên, tạo một lớp kế thừa từ `DialogFragment`:
class MyAlertDialogFragment : DialogFragment() {
interface MyAlertDialogListener {
fun onPositiveClick(dialog: DialogFragment)
fun onNegativeClick(dialog: DialogFragment)
}
// Use this instance of the interface to deliver action events
var listener: MyAlertDialogListener? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(requireContext()).apply {
setTitle("Xác nhận hành động")
setMessage("Bạn có chắc chắn muốn thực hiện hành động này?")
setPositiveButton("Đồng ý") { dialog, id ->
// Send the positive button event back to the host activity
listener?.onPositiveClick(this@MyAlertDialogFragment)
}
setNegativeButton("Hủy") { dialog, id ->
// Send the negative button event back to the host activity
listener?.onNegativeClick(this@MyAlertDialogFragment)
}
}.create()
}
// Override the Fragment.onAttach() method to instantiate the MyAlertDialogListener
override fun onAttach(context: Context) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
try {
// Instantiate the MyAlertDialogListener so we can send events to the host
listener = context as MyAlertDialogListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException((context.toString() +
" must implement MyAlertDialogListener"))
}
}
// Override the Fragment.onDetach() method to clean up the listener
override fun onDetach() {
super.onDetach()
listener = null
}
}
Trong Activity hoặc Fragment muốn hiển thị Dialog:
class MyActivity : AppCompatActivity(), MyAlertDialogFragment.MyAlertDialogListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
// Example button click to show dialog
findViewById<Button>(R.id.show_dialog_button).setOnClickListener {
showMyDialog()
}
}
fun showMyDialog() {
MyAlertDialogFragment().show(supportFragmentManager, "MyAlertDialogTag")
}
// Implement the interface methods to receive events
override fun onPositiveClick(dialog: DialogFragment) {
// User tapped the dialog's positive button
Toast.makeText(this, "Đồng ý được nhấn", Toast.LENGTH_SHORT).show()
}
override fun onNegativeClick(dialog: DialogFragment) {
// User tapped the dialog's negative button
Toast.makeText(this, "Hủy được nhấn", Toast.LENGTH_SHORT).show()
}
}
Sử dụng DialogFragment
giúp bạn quản lý Dialog một cách chính xác hơn so với việc tạo Dialog trực tiếp trong Activity, đặc biệt khi xử lý các sự kiện vòng đời và trạng thái.
Bottom Sheets: Hiển thị nội dung từ dưới màn hình
Bottom Sheet là gì?
Bottom Sheet là một panel xuất hiện từ dưới cùng của màn hình, thường chứa các thao tác bổ sung hoặc hiển thị nội dung liên quan đến tác vụ hiện tại của người dùng. Bottom Sheet ít gây gián đoạn hơn Dialog và thường được sử dụng cho:
- Các tùy chọn bổ sung liên quan đến một mục (ví dụ: menu ngữ cảnh khi nhấn giữ một item).
- Hiển thị thông tin chi tiết ngắn gọn.
- Bộ lọc hoặc tùy chọn sắp xếp.
Có hai loại Bottom Sheet chính:
- Modal Bottom Sheet: Đây là loại phổ biến nhất, xuất hiện đè lên nội dung chính và làm tối (dim) phần còn lại của màn hình. Người dùng phải tương tác với sheet hoặc chạm ra ngoài vùng sheet để đóng nó.
- Persistent Bottom Sheet: Sheet này là một phần của layout và có thể kéo lên/xuống. Nó không làm tối phần còn lại của màn hình và thường được sử dụng để hiển thị nội dung bổ sung luôn có sẵn, chẳng hạn như bảng điều khiển nhạc đang phát.
Sử dụng Bottom Sheets
Đối với Modal Bottom Sheet, bạn nên sử dụng BottomSheetDialogFragment
, tương tự như DialogFragment
. Điều này giúp quản lý vòng đời và trạng thái của sheet một cách chính xác.
Đối với Persistent Bottom Sheet, bạn sử dụng BottomSheetBehavior
kết hợp với một View (ví dụ: LinearLayout
, FrameLayout
) trong layout của Activity hoặc Fragment.
Ví dụ về sử dụng Modal Bottom Sheet với BottomSheetDialogFragment:
Tạo một lớp kế thừa từ `BottomSheetDialogFragment`:
class MyBottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this bottom sheet
return inflater.inflate(R.layout.fragment_bottom_sheet, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Setup views or add listeners here
// view.findViewById<Button>(R.id.button_action).setOnClickListener {
// // Perform action and dismiss the sheet
// dismiss()
// }
}
}
Và layout cho Bottom Sheet (fragment_bottom_sheet.xml
):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
tools:context=".MyBottomSheetDialogFragment">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Tùy chọn khác"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6" />
<Button
android:id="@+id/button_action_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Hành động 1" />
<Button
android:id="@+id/button_action_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Hành động 2" />
</LinearLayout>
Để hiển thị Modal Bottom Sheet trong Activity hoặc Fragment:
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
findViewById<Button>(R.id.show_bottom_sheet_button).setOnClickListener {
showMyBottomSheet()
}
}
fun showMyBottomSheet() {
MyBottomSheetDialogFragment().show(supportFragmentManager, "MyBottomSheetTag")
}
}
Việc sử dụng BottomSheetDialogFragment
giúp bạn tận dụng được các tính năng quản lý trạng thái và vòng đời của Fragment cho Bottom Sheet.
Drawers: Bảng điều khiển điều hướng trượt
Navigation Drawer là gì?
Navigation Drawer (hay Drawer) là một panel trượt từ cạnh màn hình (thường là cạnh trái, hoặc phải trong các ngôn ngữ RTL) để hiển thị các tùy chọn điều hướng chính của ứng dụng. Đây là một pattern UI rất phổ biến để cung cấp quyền truy cập vào nhiều đích đến (destinations) mà không làm chiếm không gian màn hình chính.
Sử dụng DrawerLayout
Bạn triển khai Navigation Drawer bằng cách sử dụng DrawerLayout
làm View root của layout Activity của bạn. DrawerLayout
chứa ít nhất hai View con:
- Main Content View: View hiển thị nội dung chính của màn hình (ví dụ: một
FragmentContainerView
hoặc một layout chứa các Fragment). - Drawer View: View chứa nội dung của drawer (thường là danh sách các mục điều hướng, có thể là
NavigationView
hoặc một layout tùy chỉnh chứa RecyclerView). View này phải chỉ định thuộc tínhlayout_gravity
làstart
(hoặcend
) để chỉ định cạnh mà nó trượt ra.
Ví dụ cấu trúc layout với DrawerLayout:
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">
<!-- Main content view -->
<FrameLayout
android:id="@+id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- Or typically a FragmentContainerView if using Navigation Component -->
<!-- Container for drawer's contents -->
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header"
app:menu="@menu/activity_main_drawer" />
<!-- Or a custom layout: <ListView android:layout_gravity="start" ... /> -->
</androidx.drawerlayout.widget.DrawerLayout>
Trong Activity, bạn cần xử lý việc mở/đóng drawer và xử lý các sự kiện click vào các mục trong drawer. Thường sử dụng ActionBarDrawerToggle
để đồng bộ trạng thái của drawer với icon “hamburger” trên Toolbar/ActionBar và xử lý các cử chỉ vuốt.
Việc quản lý điều hướng trong Navigation Drawer thường được thực hiện hiệu quả nhất khi kết hợp với Navigation Component, một thư viện quản lý điều hướng trong ứng dụng Android (sẽ được trình bày chi tiết trong một bài viết khác).
Bảng so sánh các thành phần UI linh hoạt
Để giúp bạn dễ dàng phân biệt và lựa chọn thành phần phù hợp, dưới đây là bảng so sánh tóm tắt:
Thành phần | Mục đích chính | Ngữ cảnh sử dụng điển hình | Phương pháp triển khai phổ biến | Mối quan hệ với Activity |
---|---|---|---|---|
Fragment | Mô-đun hóa UI/Hành vi, hỗ trợ đa màn hình, tái sử dụng UI. | Một phần của màn hình, chia nhỏ Activity, các tab, vuốt ngang giữa các màn hình con. | Kế thừa Fragment , sử dụng FragmentManager và FragmentTransaction (Dynamic). |
Sống bên trong một Activity (host), quản lý vòng đời bởi Activity. |
Dialog | Yêu cầu tương tác quan trọng, hiển thị thông báo, xác nhận. | Thông báo lỗi, cảnh báo, hỏi xác nhận (OK/Cancel), chọn ngày/giờ. | Kế thừa DialogFragment , sử dụng AlertDialog.Builder hoặc layout tùy chỉnh. |
Xuất hiện đè lên Activity, làm gián đoạn luồng chính. |
Bottom Sheet | Hiển thị tùy chọn/thông tin bổ sung từ dưới màn hình. | Menu ngữ cảnh, bộ lọc, tùy chọn chia sẻ, bảng điều khiển nhạc (persistent). | Modal: Kế thừa BottomSheetDialogFragment .Persistent: Sử dụng BottomSheetBehavior trong layout. |
Modal: Xuất hiện đè lên Activity/Fragment. Persistent: Một phần của layout Activity/Fragment. |
Navigation Drawer | Cung cấp menu điều hướng chính cho ứng dụng. | Truy cập nhanh đến các phần khác nhau của ứng dụng (ví dụ: Home, Settings, Profile). | Sử dụng DrawerLayout làm View root, kết hợp NavigationView hoặc layout tùy chỉnh. |
Là một phần của layout Activity, quản lý bởi DrawerLayout . |
Kết luận
Fragments, Dialogs, Bottom Sheets và Drawers là những công cụ không thể thiếu trong bộ kỹ năng của một Lập trình viên Android. Chúng giúp bạn xây dựng giao diện người dùng linh hoạt, dễ quản lý và mang lại trải nghiệm tốt hơn cho người dùng trên nhiều thiết bị khác nhau.
Việc nắm vững cách sử dụng Fragment, hiểu rõ vòng đời của nó và cách giao tiếp an toàn giữa các thành phần là nền tảng để xây dựng các ứng dụng phức tạp. Dialog và Bottom Sheet cung cấp các cách khác nhau để hiển thị thông tin hoặc yêu cầu tương tác mà không làm thay đổi hoàn toàn màn hình, trong khi Navigation Drawer là giải pháp chuẩn cho việc tổ chức điều hướng chính.
Hãy thực hành sử dụng các thành phần này trong các dự án của bạn. Bạn có thể bắt đầu bằng cách thử chuyển đổi một số màn hình Activity đơn giản thành các Fragment được quản lý bởi một Activity chứa, hoặc thêm một Dialog xác nhận vào một hành động quan trọng.
Tiếp theo trong chuỗi bài “Android Developer Roadmap”, chúng ta sẽ cùng nhau khám phá sâu hơn về Navigation Component – một thư viện hiện đại giúp đơn giản hóa việc quản lý điều hướng giữa các Fragment và màn hình.
Chúc bạn học tốt và hẹn gặp lại trong bài viết tiếp theo! Nếu có bất kỳ câu hỏi nào, đừng ngần ngại để lại bình luận nhé.
Các bài viết trước trong chuỗi “Android Developer Roadmap”:
- Android Developer Roadmap – Lộ trình học Lập trình viên Android 2025
- Kotlin vs Java: Ngôn ngữ nào nên chọn cho Android vào năm 2025?
- Lộ trình Học Lập trình viên Android 2025: Thiết lập Môi trường Phát triển Android
- Học Kotlin cho Android: Cú pháp và Khái niệm Cốt lõi Bạn Cần Nắm Vững
- Lập trình Hướng đối tượng (OOP) trong Android
- Android Developer Roadmap: Cấu trúc Dữ liệu và Giải thuật
- Gradle là gì? Cách sử dụng trong Phát triển Android
- Android Developer Roadmap: Ứng dụng Android Đầu Tiên Của Bạn – Tạo “Hello World”
- Android Developer Roadmap: Bắt đầu với Git và Hệ thống Quản lý Phiên bản
- Android Developer Roadmap: GitHub, GitLab, Bitbucket
- Hiểu Rõ Vòng Đời Activity trong Android
- Android Developer Roadmap: Xử lý Thay đổi Trạng thái và Điều hướng Backstack
- Android Developer Roadmap: Implicit vs Explicit Intents
- Android Developer Roadmap: Intent Filter
- Android Developer Roadmap: Khám phá Services, Content Providers, và Broadcast Receivers
- Android Developer Roadmap: Xây dựng Danh sách Động với RecyclerView
- Android Developer Roadmap: Xây dựng Giao diện người dùng với các View cơ bản