Tạo ứng dụng Android bằng khung hiển thị

1. Trước khi bắt đầu

Giới thiệu

Vậy là bạn đã tìm hiểu mọi kiến thức về cách tạo ứng dụng Android bằng Compose. Điều đó thật tuyệt! Compose là một công cụ vô cùng mạnh mẽ giúp đơn giản hoá quá trình phát triển ứng dụng. Tuy nhiên, không phải lúc nào các ứng dụng Android cũng được tạo bằng giao diện người dùng mang tính khai báo. Compose là công cụ còn rất mới trong lịch sử phát triển Ứng dụng Android. Ban đầu, giao diện người dùng Android được tạo bằng Khung hiển thị. Do đó, rất có thể bạn sẽ gặp phải Khung hiển thị khi tiếp tục hành trình của một nhà phát triển Android. Trong lớp học lập trình này, bạn sẽ tìm hiểu kiến thức cơ bản về các cách để tạo ứng dụng Android trước khi Compose ra đời — bằng XML, Khung hiển thị, Liên kết khung hiển thị (View Binding) và Mảnh (Fragment).

Điều kiện tiên quyết:

  • Hoàn thành tài liệu môn học trình bày Kiến thức cơ bản về tạo ứng dụng Android bằng Compose thông qua Bài 7.

Bạn cần có

  • Máy tính có kết nối Internet và Android Studio.
  • Một thiết bị hoặc trình mô phỏng
  • Mã khởi đầu cho ứng dụng Juice Tracker

Sản phẩm bạn sẽ tạo ra

Trong lớp học lập trình này, bạn sẽ hoàn thiện ứng dụng Juice Tracker. Ứng dụng này cho phép bạn theo dõi các loại nước ép nổi bật bằng cách tạo một danh sách chứa các mục chi tiết. Bạn sẽ thêm và sửa đổi các Mảnh cũng như XML để hoàn tất giao diện người dùng và mã khởi đầu. Cụ thể, bạn sẽ tạo biểu mẫu nhập thông tin để tạo ra một loại nước ép mới, bao gồm giao diện người dùng và mọi logic hoặc thành phần điều hướng có liên quan. Kết quả thu được là một ứng dụng có danh sách trống mà bạn có thể thêm những loại nước ép của riêng mình.

d6dc43171ae62047.png 87b2ca7b49e814cb.png 2d630489477e216e.png

2. Tải đoạn mã khởi đầu

  1. Trong Android Studio, hãy mở thư mục basic-android-kotlin-compose-training-juice-tracker.
  2. Mở mã ứng dụng Juice Tracker trong Android Studio.

3. Tạo bố cục

Khi tạo ứng dụng bằng Views, bạn sẽ tạo giao diện người dùng bên trong một Bố cục. Các bố cục thường được khai báo bằng XML. Các tệp bố cục XML này nằm trong thư mục tài nguyên trong phần res > layout (tài nguyên > bố cục). Bố cục chứa các thành phần tạo nên giao diện người dùng; các thành phần này được gọi là View. Cú pháp XML bao gồm các thẻ, phần tử và thuộc tính. Để biết thêm thông tin về cú pháp XML, hãy tham khảo lớp học lập trình Tạo bố cục XML cho Android.

Trong phần này, bạn sẽ xây dựng bố cục XML cho hộp thoại nhập thông tin về "Loại nước ép" như trong hình.

87b2ca7b49e814cb.png

  1. Tạo một Layout Resource File (Tệp tài nguyên bố cục) mới trong thư mục main > res > layout có tên là fragment_entry_dialog.

Ngăn bối cảnh của ngăn dự án Android Studio đang mở với lựa chọn tạo tệp tài nguyên bố cục.

6adb279d6e74ab13.png

Bố cục fragment_entry_dialog.xml chứa các thành phần giao diện người dùng mà ứng dụng hiển thị đến người dùng.

Lưu ý rằng Root element (Thành phần gốc) là một ConstraintLayout. Loại bố cục này là một ViewGroup cho phép bạn xác định vị trí và kích thước cho Thành phần hiển thị theo cách linh hoạt bằng cách dùng các quy tắc hạn chế. ViewGroup là một loại View chứa các View khác, được gọi là View con. Các bước sau đây sẽ trình bày chi tiết hơn về chủ đề này. Tuy nhiên, bạn có thể tìm hiểu thêm về ConstraintLayout trong bài viết Tạo giao diện người dùng thích ứng bằng ConstraintLayout.

  1. Sau khi tạo tệp, hãy xác định vùng chứa tên ứng dụng trong ConstraintLayout.

fragment_entry_dialog.xml

<androidx.constraintlayout.widget.ConstraintLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
  1. Thêm các nguyên tắc sau vào ConstraintLayout.

fragment_entry_dialog.xml

<androidx.constraintlayout.widget.Guideline
   android:id="@+id/guideline_left"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:orientation="vertical"
   app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
   android:id="@+id/guideline_middle"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:orientation="vertical"
   app:layout_constraintGuide_percent="0.5" />
<androidx.constraintlayout.widget.Guideline
   android:id="@+id/guideline_top"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:orientation="horizontal"
   app:layout_constraintGuide_begin="16dp" />

Những Guideline này là khoảng đệm cho các thành phần hiển thị khác. Các nguyên tắc này ràng buộc văn bản tiêu đề có tên "Type of juice" (Loại nước ép).

  1. Tạo một phần tử TextView. TextView này đại diện cho tiêu đề của mảnh chi tiết.

110cad4ae809e600.png

  1. Thiết lập cho TextView một id thuộc header_title.
  2. Thiết lập layout_width thành 0dp. Cuối cùng, các điều kiện ràng buộc về bố cục sẽ xác định chiều rộng của TextView này. Do đó, việc xác định chiều rộng chỉ thêm các phép tính không cần thiết trong quá trình vẽ giao diện người dùng; việc đặt chiều rộng là 0dp sẽ tránh được nhiều phép tính thừa.
  3. Đặt thuộc tính TextView text thành @string/juice_type.
  4. Đặt textAppearance thành @style/TextAppearance.MaterialComponents.Headline5.

fragment_entry_dialog.xml

<TextView
   android:id="@+id/header_title"
   android:layout_width="0dp"
   android:layout_height="wrap_content"
   android:text="@string/juice_type"
   android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" />

Cuối cùng, bạn cần xác định các điều kiện ràng buộc. Không giống như Guideline có sử dụng kích thước làm điều kiện ràng buộc, chính các nguyên tắc cũng ràng buộc TextView này. Để có được kết quả này, bạn có thể tham chiếu mã nhận dạng của Guideline mà bạn muốn ràng buộc khung hiển thị.

  1. Ràng buộc phần đầu của tiêu đề với phần cuối của guideline_top.

fragment_entry_dialog.xml

<TextView
   android:id="@+id/header_title"
   android:layout_width="0dp"
   android:layout_height="wrap_content"
   android:text="@string/juice_type"
   android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
   app:layout_constraintTop_toBottomOf="@+id/guideline_top" />
  1. Ràng buộc điểm kết thúc với điểm bắt đầu của guideline_middle và điểm bắt đầu với điểm bắt đầu của guideline_left để hoàn tất việc xác định vị trí của TextView. Xin lưu ý rằng cách bạn ràng buộc một khung hiển thị cụ thể phụ thuộc hoàn toàn vào giao diện người dùng mà bạn muốn.

fragment_entry_dialog.xml

<TextView
   android:id="@+id/header_title"
   android:layout_width="0dp"
   android:layout_height="wrap_content"
   android:text="@string/juice_type"
   android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
   app:layout_constraintTop_toBottomOf="@+id/guideline_top"
   app:layout_constraintEnd_toStartOf="@+id/guideline_middle"
   app:layout_constraintStart_toStartOf="@+id/guideline_left" />

Thử xây dựng phần còn lại trên giao diện người dùng dựa trên các ảnh chụp màn hình. Bạn có thể tìm thấy tệp fragment_entry_dialog.xml hoàn chỉnh trong phần giải pháp.

4. Tạo mảnh với Thành phần hiển thị

Trong Compose, bạn tạo các bố cục theo cách khai báo bằng Kotlin hoặc Java. Bạn có thể truy cập vào các "màn hình" khác nhau bằng cách di chuyển đến những Thành phần kết hợp khác nhau, thường là trong cùng một hoạt động. Khi tạo ứng dụng bằng Khung hiển thị, một Mảnh lưu trữ bố cục XML sẽ thay thế khái niệm của một "màn hình" Thành phần kết hợp.

Trong phần này, bạn sẽ tạo một Fragment để lưu trữ bố cục fragment_entry_dialog và cung cấp dữ liệu cho giao diện người dùng.

  1. Trong gói juicetracker, hãy tạo một lớp mới có tên là EntryDialogFragment.
  2. Làm cho EntryDialogFragment mở rộng BottomSheetDialogFragment.

EntryDialogFragment.kt

import com.google.android.material.bottomsheet.BottomSheetDialogFragment

class EntryDialogFragment : BottomSheetDialogFragment() {
}

DialogFragment là một Fragment hiển thị hộp thoại nổi. BottomSheetDialogFragment kế thừa từ lớp DialogFragment, nhưng hiển thị một trang tính có chiều rộng màn hình được ghim vào cuối màn hình. Phương thức này phù hợp với giao diện thiết kế được thể hiện trước đây.

  1. Nếu bạn tạo lại dự án, các tệp Liên kết khung hiển thị dựa trên bố cục fragment_entry_dialog sẽ tự động được tạo. Liên kết khung hiển thị cho phép bạn truy cập và tương tác với các View được khai báo XML, bạn có thể đọc thêm về các liên kết này trong tài liệu Liên kết khung hiển thị.
  2. Trong lớp EntryDialogFragment, hãy triển khai hàm onCreateView(). Đúng như tên gọi, hàm này tạo View cho Fragment này.

EntryDialogFragment.kt

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

override fun onCreateView(
   inflater: LayoutInflater,
   container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   return super.onCreateView(inflater, container, savedInstanceState)
}

Hàm onCreateView() trả về View, nhưng hiện không trả về một View hữu ích.

  1. Trả về View được tạo bằng cách mở rộng FragmentEntryDialogViewBinding thay vì trả về super.onCreateView().

EntryDialogFragment.kt

import com.example.juicetracker.databinding.FragmentEntryDialogBinding

override fun onCreateView(
   inflater: LayoutInflater,
   container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   return FragmentEntryDialogBinding.inflate(inflater, container, false).root
}
  1. Tạo một thực thể của EntryViewModel bên ngoài hàm onCreateView(), nhưng bên trong lớp EntryDialogFragment.
  2. Triển khai hàm onViewCreated().

Sau khi mở rộng Liên kết khung hiển thị, bạn có thể truy cập và sửa đổi View trong bố cục. Phương thức onViewCreated() được gọi sau onCreateView() trong vòng đời. Bạn nên sử dụng phương thức onViewCreated() để truy cập và sửa đổi các View trong bố cục.

  1. Tạo một thực thể của liên kết khung hiển thị bằng cách gọi phương thức bind() trên FragmentEntryDialogBinding.

Khi đó, đoạn mã của bạn sẽ trông giống như ví dụ sau:

EntryDialogFragment.kt

import androidx.fragment.app.viewModels
import com.example.juicetracker.ui.AppViewModelProvider
import com.example.juicetracker.ui.EntryViewModel

class EntryDialogFragment : BottomSheetDialogFragment() {

   private val entryViewModel by viewModels<EntryViewModel> { AppViewModelProvider.Factory }

   override fun onCreateView(
       inflater: LayoutInflater,
       container: ViewGroup?,
       savedInstanceState: Bundle?
   ): View {
       return FragmentEntryDialogBinding.inflate(inflater, container, false).root
   }

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val binding = FragmentEntryDialogBinding.bind(view)
    }
}

Bạn có thể truy cập và thiết lập các Khung hiển thị thông qua liên kết. Ví dụ: bạn có thể đặt TextView thông qua phương thức setText().

binding.name.setText("Apple juice")

Giao diện người dùng của hộp thoại nhập thông tin có vai trò là nơi người dùng tạo một mục mới. Tuy nhiên, bạn cũng có thể dùng giao diện này để sửa đổi một mục hiện có. Do đó, Mảnh này cần truy xuất một mục đã nhấp. Thành phần điều hướng hỗ trợ việc di chuyển đến EntryDialogFragment và truy xuất một mục đã nhấp.

EntryDialogFragment chưa hoàn tất, nhưng bạn đừng lo lắng! Bây giờ, hãy chuyển sang phần tiếp theo để tìm hiểu thêm về cách sử dụng Thành phần điều hướng trong một ứng dụng bằng View.

5. Sửa đổi Thành phần điều hướng

Trong phần này, bạn sẽ dùng thành phần điều hướng để mở hộp thoại nhập thông tin và truy xuất một mục, nếu có.

Compose tạo cơ hội để hiển thị nhiều thành phần kết hợp chỉ bằng cách gọi chúng. Tuy nhiên, các Mảnh hoạt động không giống nhau. Thành phần điều hướng điều phối "đích đến" của Mảnh. Nhờ đó, bạn có thể dễ dàng di chuyển giữa các Mảnh và Khung hiển thị có trong đó.

Sử dụng Thành phần điều hướng để điều hướng đến EntryDialogFragment.

  1. Mở tệp nav_graph.xml và nhớ chọn thẻ Design (Thiết kế). 783cb5d7ff0ba127.png
  2. Nhấp vào biểu tượng 93401bf098936c15.png để thêm một đích đến mới.

d5410c90e408b973.png

  1. Chọn đích EntryDialogFragment. Thao tác này sẽ khai báo entryDialogFragment trong biểu đồ điều hướng, giúp bạn có thể truy cập vào các thao tác điều hướng.

418feed425072ea4.png

Bạn cần khởi chạy EntryDialogFragment từ TrackerFragment. Do đó, một thao tác điều hướng cần hoàn thành tác vụ này.

  1. Kéo con trỏ qua trackerFragment. Chọn dấu chấm màu xám rồi kéo đường thẳng đó đến entryDialogFragment. 85decb6fcddec713.png
  2. Khung hiển thị bản thiết kế nav_graph cho phép bạn khai báo các đối số cho một đích đến bằng cách chọn đích đó và nhấp vào biểu tượng a0d73140a20e4348.png bên cạnh trình đơn thả xuống Arguments (Đối số). Dùng tính năng này để thêm một đối số itemId thuộc loại Long vào entryDialogFragment; giá trị mặc định phải là 0L.

555cf791f64f62b8.png

840105bd52f300f7.png

Xin lưu ý rằng TrackerFragment chứa danh sách các mục Juice. Nếu bạn nhấp vào một trong các mục này, EntryDialogFragment sẽ khởi chạy.

  1. Tạo lại dự án. Giờ đây, bạn có thể truy cập vào đối số itemId trong EntryDialogFragment.

6. Hoàn tất mảnh

Với dữ liệu từ các đối số điều hướng, hãy hoàn tất hộp thoại nhập thông tin.

  1. Truy xuất navArgs() trong phương thức onViewCreated() của EntryDialogFragment.
  2. Truy xuất itemId từ navArgs().
  3. Triển khai saveButton để lưu loại nước ép mới/đã sửa đổi bằng cách sử dụng ViewModel.

Hãy nhớ lại giao diện hộp thoại nhập thông tin, giá trị màu sắc mặc định sẽ là màu đỏ. Giờ hãy truyền thông tin này dưới dạng một phần giữ chỗ.

Truyền mã mục từ đối số khi gọi saveJuice().

EntryDialogFragment.kt

import androidx.navigation.fragment.navArgs
import com.example.juicetracker.data.JuiceColor

class EntryDialogFragment : BottomSheetDialogFragment() {

   //...
   var selectedColor: JuiceColor = JuiceColor.Red

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val binding = FragmentEntryDialogBinding.bind(view)
        val args: EntryDialogFragmentArgs by navArgs()
        val juiceId = args.itemId

        binding.saveButton.setOnClickListener {
           entryViewModel.saveJuice(
               juiceId,
               binding.name.text.toString(),
               binding.description.text.toString(),
               selectedColor.name,
               binding.ratingBar.rating.toInt()
           )
        }
    }
}
  1. Sau khi lưu dữ liệu, hãy loại bỏ hộp thoại bằng phương thức dismiss().

EntryDialogFragment.kt

class EntryDialogFragment : BottomSheetDialogFragment() {

    //...
    var selectedColor: JuiceColor = JuiceColor.Red
    //...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val binding = FragmentEntryDialogBinding.bind(view)
        val args: EntryDialogFragmentArgs by navArgs()
        binding.saveButton.setOnClickListener {
           entryViewModel.saveJuice(
               juiceId,
               binding.name.text.toString(),
               binding.description.text.toString(),
               selectedColor.name,
               binding.ratingBar.rating.toInt()
           )
           dismiss()
        }
    }
}

Xin lưu ý rằng mã ở trên không hoàn tất EntryDialogFragment. Bạn vẫn cần triển khai một số hoạt động, chẳng hạn như điền vào các trường có dữ liệu Juice hiện có (nếu thích hợp), chọn màu sắc từ colorSpinner, triển khai cancelButton, v.v. Tuy nhiên, mã này không phải là duy nhất đối với Fragment. Bạn có thể triển khai mã này theo cách của mình. Hãy cố gắng triển khai các chức năng còn lại. Khi không còn cách nào khác, bạn có thể tham khảo mã nguồn giải pháp của lớp học lập trình này.

7. Khởi chạy hộp thoại nhập thông tin

Nhiệm vụ cuối cùng là khởi chạy hộp thoại nhập thông tin bằng Thành phần điều hướng. Hộp thoại nhập thông tin cần khởi chạy khi người dùng nhấp vào nút hành động nổi (FAB). Hộp thoại này cũng cần khởi chạy và truyền mã nhận dạng tương ứng khi người dùng nhấp vào một mục.

  1. Trong onClickListener() dành cho FAB, hãy gọi navigate() trên trình điều khiển điều hướng.

TrackerFragment.kt

import androidx.navigation.findNavController

//...

binding.fab.setOnClickListener { fabView ->
   fabView.findNavController().navigate(
   )
}

//...
  1. Trong hàm điều hướng, hãy truyền thao tác điều hướng từ trình theo dõi đến hộp thoại nhập thông tin.

TrackerFragment.kt

//...

binding.fab.setOnClickListener { fabView ->
   fabView.findNavController().navigate(
TrackerFragmentDirections.actionTrackerFragmentToEntryDialogFragment()
   )
}

//...
  1. Lặp lại thao tác này trong nội dung hàm lambda cho phương thức onEdit() trong JuiceListAdapter, nhưng lần này, hãy truyền id của Juice.

TrackerFragment.kt

//...

onEdit = { drink ->
   findNavController().navigate(
       TrackerFragmentDirections.actionTrackerFragmentToEntryDialogFragment(drink.id)
   )
},

//...

8. Lấy mã nguồn giải pháp

Để tải mã này xuống khi lớp học lập trình đã kết thúc, bạn có thể sử dụng các lệnh git sau:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-juice-tracker.git
$ cd basic-android-kotlin-compose-training-juice-tracker
$ git checkout views

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp zip rồi giải nén và mở trong Android Studio.

Nếu bạn muốn xem mã giải pháp, hãy xem mã đó trên GitHub.