Chào mừng các bạn đến với chủ đề về Android Jetpack.
Mình xin phép điểm lại các phần nhỏ liên quan đến Navigation trong chủ đề Jetpack để bạn dễ tham khảo.
Sang tập tiếp theo này chúng ta sẽ dùng thành phần quan trọng còn lại của Navigation, đó chính là NavController để tạo sự di chuyển giữa các destination bên trong NavHost mà chúng ta đã xây dựng. Ngoài ra chúng ta còn xem xét việc xây dựng hiệu ứng chuyển đổi các màn hình sao cho ứng dụng sinh động và mượt mà hơn. Ở cuối cùng của bài viết là các cách mà chúng ta có thể truyền dữ liệu qua lại giữa các destination.
Nào bài viết sẽ dài, chúng ta bắt đầu thôi.
Sử Dụng NavController
NavController là gì và tại sao phải dùng chúng? Câu trả lời cho câu hỏi là gì thì mình cũng đã nói ở Tập 1 của Navigation rồi. Nhưng mình đoán có nhiều bạn cũng đang thắc mắc công dụng của thành phần này.
Bạn nhớ lại xem, nếu bạn xây dựng một ứng dụng với nhiều Activity, một khi bạn muốn từ một Activity này chuyển sang hiển thị Activity khác, thì từ Activity gốc bạn gọi startActivity() đúng không nào. Hoặc trong một Activity của bạn có hiển thị Fragment một cách thủ công (không phải bằng Navigation), bạn muốn chuyển đổi hiển thị từ một Fragment này sang một Fragment khác bạn phải “triệu hồi” ra một FragmentManager thông qua lời gọi getSupportFragmentManager() ở Activity hay getChildFragmentManager() ở Fragment, để rồi từ FragmentManager này chúng ta mới thay thế hay thêm mới một Fragment vào được.
Và bây giờ, chúng ta bước sang sử dụng Navigation, bạn có một cách mới gọn gàng và đa năng hơn, đó là NavController. Công dụng của NavController cũng bao gồm cả hai công dụng chính mình nêu ra trên đây, di chuyển từ một destination này đến một destination khác (là một Activity hay một Fragment khác).
Gọi Ra Một NavController
Mình dùng “triệu hồi” nhé, cho nó vui và dễ nhớ, vì NavController có sẵn bên trong NavHostFragment. Các câu lệnh triệu hồi như sau.
Fragment.findNavController()
View.findNavController()
Activity.findNavController(viewId: Int)
Trên đây là 3 cách triệu hồi ở 3 ngữ cảnh khác nhau, lần lượt ở Fragment, View hay Activity. Google cũng có cảnh báo chúng ta rằng nên gọi findNavController() cho đúng chỗ, vì NavController luôn đi kèm với NavHosFragment, nên đảm bảo nơi gọi đến hàm này phải là một nơi nằm trong hay có liên quan đến một NavController.
Dùng NavController Để Di Chuyển Đến Destination
Lúc nãy mình có nói cách dùng với NavController là đa năng, vâng đa năng chỗ này đây, có nhiều cách để bạn di chuyển từ một destination này đến destination khác bên trong NavHost sử dụng NavController.
Chúng ta hãy bắt đầu xây dựng việc click vào button View Profile ở HomeFragment sẽ mở ra ProfileFragment qua từng cách dưới đây.
À trước hết chúng ta nên xây dựng sẵn sự kiện click trên button View Profile đã nhé.
class HomeFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_home, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) view.findViewById<Button>(R.id.btnViewProfile)?.setOnClickListener { // Will use NavController here } } }
Cách 1 – Di Chuyển Trực Tiếp Trên Navigation Graph
NavController có một phương thức navigate() cho phép chúng ta truyền vào một id của destination để di chuyển. Dùng như sau.
view.findViewById<Button>(R.id.btnViewProfile)?.setOnClickListener { findNavController().navigate(R.id.profileFragment) }
Với việc thêm 1 dòng trên vào sự kiện click của button thôi là bạn đã có thể mở ProfileFragment từ HomeFragment rồi, bạn thử thực thi ứng dụng rồi trải nghiệm nhé.
Cách 2 – Di Chuyển Bằng Action
Để làm được cách này, chúng ta hãy xây dựng action. Ở Tập 1 các bạn đã được giới thiệu action giúp cho biết trực quan đường đi từ một destination này đến destination khác. Thì giờ đây công dụng của action đã được làm rõ hơn thông qua việc thực thi việc di chuyển.
Để tạo một action, trước hết bạn hãy mở lại login_nav_graph lên, đảm bảo đang xem ở tab Design. Click chọn vào HomeFragment, bạn sẽ thấy một khung xanh bao quanh destination này kèm với một hình tròn ở bên phải khung xanh này.
Nhấn giữ chuột và kéo từ chấm tròn trên homeFragment này vào profileFragment. Khi thả chuột ra bạn sẽ tạo được một action hình mũi tên như thế này.
Nếu vẫn nhấn chọn vào action vừa mới tạo (action sẽ chuyển sang màu xanh khi được chọn). Nhìn vào khung Attributes bạn sẽ thấy một số thuộc tính quan trọng của nó.
- id: chính là id của action. Bạn có thể thấy là nó khá dài, bạn hoàn toàn có thể sửa id này cho ngắn lại, nhưng mình thấy để như vậy cũng không sao, vì nó khá là rõ nghĩa.
- destination: chính là id của destination mà action này sẽ dẫn đến.
Giờ chúng ta hãy chuyển sang tab Code để xem diễn biến bên này ra sao nhé.
<?xml version="1.0" encoding="utf-8"?> <navigation 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/login_nav_graph" app:startDestination="@id/homeFragment"> <fragment android:id="@+id/profileFragment" android:name="com.yellowcode.navigationsample.ProfileFragment" android:label="fragment_profile" tools:layout="@layout/fragment_profile" /> <fragment android:id="@+id/homeFragment" android:name="com.yellowcode.navigationsample.HomeFragment" android:label="fragment_home" tools:layout="@layout/fragment_home" > <action android:id="@+id/action_homeFragment_to_profileFragment" app:destination="@id/profileFragment" /> </fragment> <fragment ... /> </navigation>
Bạn xem, nếu như ở bài trước sau khi thêm các destination vào file giao diện này, chúng ta chỉ mới có các tag fragment. Nay trong fragment mà id là homeFragment có thêm một tag con action với 2 thuộc tính id và destination mà chúng ta vừa mới làm quen.
Trên đây là cách tổ chức về phía giao diện và code XML của file login_nav_graph.
Quay trở lại với cách dùng NavController thứ 2. Bạn hãy thay thế id của destination ở cách 1 bằng id của action, và chạy lại chương trình để xem kết quả nhé.
view.findViewById<Button>(R.id.btnViewProfile)?.setOnClickListener { findNavController().navigate(R.id.action_homeFragment_to_profileFragment) }
Do action tự nó biết đích đến là ở đâu, nên việc gọi navigate() và truyền vào một id của action sẽ giúp cho hệ thống biết destination nào cần mở ra lúc này.
Bạn đã nắm rõ hai cách di chuyển chưa nào. Chúng ta cùng đến phần tiếp theo để thấy với hai cách trên, cách nào sẽ hay hơn nhé.
Tạo Hiệu Ứng Di Chuyển Qua Lại Giữa Các Destination
Bạn có thấy dù dùng cách nào để di chuyển qua lại giữa các destination trên đây, thì việc chuyển màn hình xảy ra rất đột ngột đúng không. Triết lý Material Design không thích điều này. Một giao diện đẹp là một giao diện có tính chuyển động uyển chuyển. Vậy chúng ta hãy thêm vào chút animation cho nó.
Trước hết bạn hãy tạo một resource /anim mới trong project bằng cách click chuội phải vào thư mục /res rồi chọn New > Android Resource Directory, chọn Resource type là anim ở cửa sổ sau.
Sau đó bạn hãy vào link này để xem và xây dựng lại từng anim vào trong thư mục /anim vừa mới tạo nhé. Tên file resource đã nói lên loại chuyển động hoặc hiệu ứng anim rồi nên mình không nói gì thêm.
Đến đây thì tùy xem bạn đã áp dụng việc di chuyển destination theo cách nào trên đây mà sẽ có các cách áp dụng animation khác nhau.
Nếu bạn dùng cách Di Chuyển Trực Tiếp Trên Navigation Graph, thì bạn phải xây dựng một NavOptions. Chúng ta sẽ tận dụng kiến trúc Builder của lớp này để override lại một số anim mà chúng ta cần thay đổi. Và cũng cảm ơn Kotlin KTX dành cho Navigation đã giúp chúng ta khai báo NavOptions đẹp đẽ hơn, rồi truyền NavOptions này vào trong navigate() như sau.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val options = navOptions { anim { enter = R.anim.slide_in_right exit = R.anim.slide_out_left popEnter = R.anim.slide_in_left popExit = R.anim.slide_out_right } } view.findViewById<Button>(R.id.btnViewProfile)?.setOnClickListener { findNavController().navigate(R.id.profileFragment, null, options) } }
Còn nếu dùng cách Di Chuyển Bằng Action, thì còn tuyệt vời hơn, chúng ta không cần phải viết code Kotlin dài dòng, chỉ cần bạn mở login_nav_graph lên, click chọn vào action (mũi tên bạn vừa tạo khi nãy), rồi gõ tên anim vào khung Attributes ở các cột tương ứng. Như sau.
Bạn cũng có thể qua tab Code để xem cách mà hệ thống khai báo các anim này vào action ở XML như thế nào nhé.
Lưu ý là code ở HomeFragment, sự kiện click lên nút View Profile cũng vẫn như trên, chỉ có vỏn vẹn thế này thôi.
view.findViewById<Button>(R.id.btnViewProfile)?.setOnClickListener { findNavController().navigate(R.id.action_homeFragment_to_profileFragment) }
Dù code bằng cách nào thì giờ đây bạn đã có thể trải nghiệm bằng cách thực thi ứng dụng và thử nhấn vào nút View Profile được rồi.
Truyền Data Qua Lại Giữa Các Destination
Giờ thì chúng ta sẽ nói đến cách truyền dữ liệu qua destination. Tuy nhiên mình cũng nói trước rằng dữ liệu bạn cần phải truyền lúc này thường là các dữ liệu nhỏ, như id của user, hay user name, hay một tham số Boolean gì đó để làm cờ bật tắt cho tính năng nào đó,… Tóm lại là các kiểu dữ liệu nhỏ xíu thôi, thì dùng cách dưới đây. Còn nếu bạn muốn truyền cả một object hay các dữ liệu lớn hơn, thì dùng ViewModel cho nó khỏe bạn nhé.
Để bắt đầu việc truyền dữ liệu “nhỏ”. Cụ thể ở ví dụ ứng dụng của chúng ta thì khi bạn đi từ màn hình HomeFragment sang ProfileFragment, bạn sẽ thấy một câu chào hỏi rất là lạ.
Sở dĩ “Welcome” chưa biết ai để chào hỏi là vì chúng ta chưa truyền gì từ HomeFragment qua đây. Kịch bản mong muốn là sau khi login thành công, HomeFragment sẽ biết được tên người đăng nhập và truyền tên này qua cho ProfileFragment hiển thị. Mặc dù chúng ta chưa xây dựng thành công luồng đăng nhập, nhưng có thể tạm xây dựng đầy đủ việc truyền một cái tên qua đây để kiểm thử.
Đến đây thì cũng có 2 cách để chúng ta truyền dữ liệu này.
Cách 1 – Truyền Dữ Liệu Theo Bundle
Đây có thể nói là cách “truyền thống”, khi mà nếu bạn xây dựng Activity hay Fragment theo cách cũ đều đã có dùng từ rất lâu. Nay thì phương thức navigation() cũng hỗ trợ chúng ta điều này.
Và cũng cảm ơn Android KTX đã cho chúng ta một đoạn code khá dễ hiểu sau khi tạo một Bundle như sau. Do chúng ta muốn truyền giá trị tên qua destination, nên trường hợp này chúng ta đặt tên cho key là “name”. Sau khi tạo xong Bundle chúng ta sẽ truyền nó thông qua phương thức navigation().
view.findViewById<Button>(R.id.btnViewProfile)?.setOnClickListener { val bundle = bundleOf( "name" to "My Name", ) findNavController().navigate(R.id.action_homeFragment_to_profileFragment, bundle) }
Ở ProfileFragment, chúng ta chỉ cần gọi getArguments() để lấy ra giá trị bên trong Bundle này.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val name = arguments?.getString("name") view.findViewById<TextView>(R.id.tvWelcome)?.text = getString(R.string.welcome, "$name") }
Xong bước này bạn có thể chạy lại ứng dụng và xem ProfileFragment đã hiển thị đúng tên được truyền qua từ HomeFragment chưa nhé.
Cách 2 – Sử Dụng Safe Args
Safe Args là một cách thức truyền dữ liệu mới dùng trong Navigaton này. Sở dĩ gọi là Safe Args là vì đây là một kiểu truyền dữ liệu được đảm bảo type-safety, tức là an toàn về kiểu dữ liệu.
Nếu như kiểu truyền thống Bundle trên kia buộc chúng ta phải nhớ sao cho khớp giữa việc đưa dữ liệu vào Bundle và lấy dữ liệu ra khỏi Bundle. Như ví dụ với key là “name” trên kia, thì chúng ta phải nhớ nò là kiểu String. Trong trường hợp chúng ta có ít giá trị truyền qua thì không sao, nhưng nếu một ứng dụng có quá nhiều giá trị, việc vô tình tạo ra sự không đồng nhất về giá trị giữa truyền và nhận Bundle là chuyện có thể xảy ra. Vậy hãy xem Safe Args giúp chúng ta thế nào nhé.
Định Nghĩa Argument
Safe Args là viết tắt của từ Safe Arguments. Arguments sẽ thay thế Bundle (về mặt tên gọi thôi chứ thực ra chúng là một). Việc thay thế như thế nào thì trước hết chúng ta hãy cùng tạo một argument cho giá trị name nào.
Trước hết bạn phải đảm bảo login_nav_graph đang được mở với tab Design. Nhấn chọn vào profileFragment. Nhìn sang khung Attributes, tìm đến phần Arguments, xổ thành phần này ra, sau đó nhấn vào dấu + để bắt đầu thêm một argument.
Một popup tiếp theo sẽ xuất hiện cho chúng ta khai báo thông tin của argument này. Vì chúng ta muốn chuyển qua profileFragment một giá trị tên, nên chúng ta đặt là nameArg, kiểu String. Như sau.
Lần này chúng ta đặt tên là nameArg để cho khác với name của Bundle trên kia, để dễ phân biệt và so sánh.
Nếu bạn muốn hiểu rõ hơn về argument này, như các kiểu dữ liệu mà argument hỗ trợ thì đọc thêm ở đây, hay cách override lại một argument cụ thể với từng action thì đọc thêm ở đây.
Bạn nên nhớ là dữ liệu cần truyền vào cho destination nào thì định nghĩa argument ở destination đó nhé. Không cần biết destination đó nhận dữ liệu từ đâu, hễ destination cần dữ liệu nào, argument sẽ khai báo ở destination đó.
Khai Báo Thư Viện
Cơ mà, để dùng Safe Args thì việc cần thiết tiếp theo đó là chúng ta cần khai báo các thư viện. Bạn hãy thêm classpath sau vào file build.gradle của project.
buildscript { ext.kotlin_version = "1.5.10" ext.nav_version = "2.3.5" repositories { google() mavenCentral() } dependencies { classpath "com.android.tools.build:gradle:4.2.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" } }
Trong file build.gradle của module thì chúng ta phải khai báo thêm plugin như mình đã tô sáng như sau.
plugins { id 'com.android.application' id 'kotlin-android' id 'androidx.navigation.safeargs.kotlin' }
Sau khi khai báo các bước trên đây, hệ thống sẽ căn cứ vào project của chúng ta mà sẽ tạo ra một số lớp tương ứng. Mình xin giới thiệu trước các lớp này, một lát sau chúng ta sẽ dùng tới nó.
- Cứ mỗi một destination mà có một action “dính” tới nó. Tức là một mũi tên từ nó trỏ ra. Sẽ có thêm một lớp mới tạo ra, với tên lớp là sự kết hợp giữa tên lớp của destination cộng với từ “Directions”. Như vậy trong project của chúng ta giờ đây sẽ có thêm một lớp: HomeFragmentDirections, vì chúng ta chỉ mới khai báo một action đi ra từ destination homeFragment thôi. Lớp này sẽ chứa đựng các phương thức chính là các action được định nghĩa bên trong destination này (xem giải nghĩa phương thức này ngay dưới đây).
- Mỗi một phương thức action trên đây có tham số đầu vào chính là các argument cần truyền qua destination khác. Tên của phương thức này cũng chính là tên action. Như vậy với việc khai báo một action với tên (id) là action_homeFragment_to_profileFragment, chúng ta sẽ có phương thức kèm theo với tên actionHomeFragmentToProfileFragment(), tham số truyền vào chính là một kiểu String có tên nameArg.
- Cứ mỗi một destination mà có một argument “dính” tới nó. Tức là đây là destination nhận dữ liệu. Sẽ có thêm một lớp mới tạo ra, với tên lớp là sự kết hợp giữa tên lớp của destination cộng với từ “Args”. Như vậy trong project của chúng ta giờ đây sẽ có thêm một lớp: ProfileFragmentArgs.
Với “bộ ba” thông tin mình liệt kê trên đây bạn đã phần nào mường tượng ta Safe Args sử dụng như thế nào chưa. Bạn hãy nhìn vào code khi truyền tham số từ homeFragment như dưới đây sẽ hiểu.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) view.findViewById<Button>(R.id.btnViewProfile)?.setOnClickListener { val action = HomeFragmentDirections.actionHomeFragmentToProfileFragment(nameArg = "My Name") findNavController().navigate(action) } }
Còn ở ProfileFragment, chúng ta dùng như sau (có sự hỗ trợ Android KTX ở dòng khai báo args đầu tiên).
val args: ProfileFragmentArgs by navArgs() // ... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val name = args.nameArg view.findViewById<TextView>(R.id.tvWelcome)?.text = getString(R.string.welcome, "$name") }
Uhm tuy sử dụng Safe Args có phần “rườm rà”. Nhưng bù lại nó khá an toàn và trơn tru, vì khi này chúng ta truyền hay nhận argument thông qua việc sử dụng biến, chứ không gọi ra từ key trong Bundle nữa, an toàn là chỗ này.
Bạn hãy thử chạy lại ứng dụng để xem kết quả có mỹ mãn không nhé.
Kết Luận
Tuy bạn thấy cách chúng ta dùng Navigation để dựng nên một ứng dụng cho tới bài hôm nay khá là rối rắm và nhiều bước. Nhưng bạn cũng nên biết, chúng ta đang xây dựng một ứng dụng hoàn chỉnh với khá nhiều màn hình, và Navigation đang giúp chúng ta quản lý tốt từ các màn hình cho đến đường đi giữa chúng, và cả animation lẫn truyền dữ liệu nữa. Nếu không phải là Navigation, chắc chắn chúng ta sẽ đau đầu hơn trong việc giải quyết từng chuyện một một cách thủ công với những thể loại ứng dụng có nhiều màn hình như ví dụ này đây. Tuy nhiên ứng dụng vẫn chưa xong, chúng ta cần một bài viết nữa để có thể sử dụng Navigation ở mức thuần thục hơn.
Download Source Code Mẫu
Bạn có thể download source code mẫu của bài này ở đây.
Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy ủng hộ blog bằng cách:
– Đánh giá 5 sao ở mỗi bài viết nếu thấy thích.
– Comment bên dưới mỗi bài viết nếu có thắc mắc.
– Để lại địa chỉ email của bạn ở thanh bên phải để nhận được thông báo sớm nhất khi có bài viết mới.
– Chia sẻ các bài viết của Yellow Code Books đến nhiều người khác.
– Ủng hộ blog theo hướng dẫn ở thanh bên phải để blog ngày càng phát triển hơn.