Android Developer Roadmap: Sử dụng Fragments, Dialogs, Bottom Sheets và Drawers – Xây dựng Giao diện Linh hoạt và Tương tá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ảnxâ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 khi onCreateView() 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ức onCreate() 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:

  1. 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.
  2. 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 FragmentManagerFragmentTransaction. Đâ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:

  1. 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).
  2. 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ính layout_gravitystart (hoặc end) để 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 FragmentManagerFragmentTransaction (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”:

Chỉ mục