Thông Thạo Jetpack – Phần 6 – Navigation (Tập Cuối)

Posted by
No votes yet.
Please wait...

Chào mừng các bạn đến với chủ đề về Android Jetpack.

Như các bạn đã biết, chủ đề về Navigation của Jetpack mà chúng ta cùng nhau đi qua đã đến phần hoàn chỉnh cuối cùng cho ứng dụng có chức năng Đăng nhập/Đăng xuất. Trước khi đi vào xây dựng hoàn thành ứng dụng, mình xin phép điểm lại các phần liên quan đến kiến thức Navigation và quá trình phát triển ứng dụng này như sau.

  • Kiến thức sơ bộ về Navigation được nói đến ở Tập 1.
  • Xây dựng Navigation Graph và NavHost cho ứng dụng ở Tập 2.
  • Dùng NavController để thực hiện di chuyển qua lại giữa các destination, tạo hiệu ứng chuyển đổi và truyền dữ liệu qua lại giữa các destinationTập 3.

Và bài viết hôm nay, tập cuối cùng của Navigation, chúng ta sẽ tìm hiểu khả năng hỗ trợ cao hơn nữa của thành phần này, đó là việc chuyển đổi một cách có điều kiện giữa các destination. Đồng thời xem xét việc kết hợp Navigation với các thành phần khác của Android, như ViewModel, LiveData, và Toolbar xem như thế nào nhé.

Thêm Các Action Còn Thiếu Vào Project

Mục này chúng ta sẽ thêm một số thành phần liên quan đến Natigation còn thiếu để project có đủ các yếu tố cho việc sắp xếp ở các bước hoàn thiện kế tiếp.

Thêm Các Action

Ở bài thực hành trước bạn đã dựng xong một action, action này nối giữa homeFragmentprofileFragment. Mình có nói các bạn rằng chúng ta không cần đổi tên action, nên khi đó sau khi tạo xong, action của chúng ta có cái tên action_homeFragment_to_profileFragment, hơi dài nhưng rõ nghĩa.

Bước tiếp theo hôm nay chúng ta sẽ xây dựng 2 action còn lại, chúng nối giữa homeFragment – signInFragment, và signInFragment – signUpFragment.

Bạn hãy mở lại login_nav_graph.xml lên, đảm bảo tab Design đang được chọn, sau đó hãy theo hướng dẫn của bài hôm trước kéo 2 mũi tên còn lại sao cho chúng trông như sau. Một lần nữa, chỉ xây dựng action và giữ tên mặc định là được.

Các action hoàn chỉnh cho ứng dụng login của chúng ta
Các action hoàn chỉnh cho ứng dụng login của chúng ta

Thêm Animation Cho Action

Bạn hãy nhớ khai báo hiệu ứng di chuyển qua lại giữa các destination cho từng action mà bạn mới tạo ra nhé. Để nhanh chóng thì mình sẽ mở tab Code khi login_nav_graph.xml vẫn đang mở, copy và paste các dòng tô sáng sau vào từng action là xong.

<?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" >
        <argument
            android:name="nameArg"
            app:argType="string" />
    </fragment>
    <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"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />
        <action
            android:id="@+id/action_homeFragment_to_signInFragment"
            app:destination="@id/signInFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"/>
    </fragment>
    <fragment
        android:id="@+id/signInFragment"
        android:name="com.yellowcode.navigationsample.SignInFragment"
        android:label="fragment_sign_in"
        tools:layout="@layout/fragment_sign_in" >
        <action
            android:id="@+id/action_signInFragment_to_signUpFragment"
            app:destination="@id/signUpFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right"/>
    </fragment>
    <fragment
        android:id="@+id/signUpFragment"
        android:name="com.yellowcode.navigationsample.SignUpFragment"
        android:label="fragment_sign_up"
        tools:layout="@layout/fragment_sign_up" />
</navigation>

Xây Dựng Các Lớp Logic

Ở các phần trước chúng ta đã xây dựng Activity và các Fragment cho từng màn hình, đây chỉ mới là các UI. Để một ứng dụng được hoàn chỉnh, chúng ta cần phải có các lớp xử lý logic cho nó nữa.

Lớp UserPreferencesManagerment.kt

Lớp đầu tiên sẽ là lớp quản lý việc lưu trữ thông tin người dùng đã đăng ký và đăng nhập. Để không quá tập trung rườm rà vào tầng quản lý dữ liệu này, mình chỉ đơn thuần xây dựng một lớp nơi đó chứa các top level function, là các hàm hữu ích thực hiện thuần các chức năng lưu trữ thông tin đăng nhập của người dùng vào SharedPreferences. Lớp này có tên là UserPreferencesManagerment.kt. Bạn có thể xem source code của lớp này ngay dưới đây, hoặc ở link đến Github mình để ở cuối bài viết nhé.

private const val PREF_NAME = "com.yellowcode.navigationsample.shared_preferences"
private const val USERNAME_KEY = "USERNAME_KEY"
private const val PASSWORD_KEY = "PASSWORD_KEY"
private const val LOGGED_IN_KEY = "LOGGED_IN_KEY"

fun saveUsernamePassword(context: Context, userName: String, password: String) {
    val sharedPref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
    sharedPref.edit(commit = false) {
        putString(USERNAME_KEY, userName)
        putString(PASSWORD_KEY, password)
    }
}

fun saveLoginStatus(context: Context, loggedIn: Boolean) {
    val sharedPref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
    sharedPref.edit(commit = false) {
        putBoolean(LOGGED_IN_KEY, loggedIn)
    }
}

fun getUserName(context: Context): String? {
    val sharedPref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
    return sharedPref.getString(USERNAME_KEY, "")
}

fun getPassword(context: Context): String? {
    val sharedPref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
    return sharedPref.getString(PASSWORD_KEY, "")
}

fun getLoginStatus(context: Context): Boolean {
    val sharedPref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
    return sharedPref.getBoolean(LOGGED_IN_KEY, false)
}

Các phương thức trên không quá khó hiểu đúng không nào. Tiếp theo dĩ nhiên sẽ là ViewModel giúp kết nối giữa các view, chính là các Activity và Fragment của chúng ta, với các phương thức tương tác với dữ liệu lưu trữ được xây dựng trên đây.

Lớp UserViewModel

ViewModel của chúng ta có tên là UserViewModel.

class UserViewModel(application: Application) : AndroidViewModel(application) {

    private var _logedIn = MutableLiveData<Boolean>()
    val loggedIn: LiveData<Boolean> = _logedIn

    init {
        // Init status of logged in by call this function
        _logedIn.value = getLoginStatus(getApplication())
    }

    fun getUserName(): String? {
        return com.yellowcode.loginnavigationdemo.getUserName(getApplication())
    }

    fun login(userName: String, password: String): LiveData<Boolean> {
        val liveData = MutableLiveData<Boolean>()
        if (userName.isNullOrEmpty().not() &&
            userName == com.yellowcode.loginnavigationdemo.getUserName(getApplication()) &&
            password.isNullOrEmpty().not() &&
            password == getPassword(getApplication())
        ) {
            // Login successful
            saveLoginStatus(getApplication(), true)
            _logedIn.value = true
            liveData.value = true
        } else {
            // Login error
            _logedIn.value = false
            liveData.value = false
        }
        return liveData
    }

    fun signUp(userName: String, password: String): LiveData<Boolean> {
        val liveData = MutableLiveData<Boolean>()
        if (userName.isNullOrEmpty() || password.isNullOrEmpty()) {
            // Sign up error
            liveData.value = false
        } else {
            // Sign up successful
            saveUsernamePassword(getApplication(), userName, password)
            liveData.value = true
        }
        return liveData
    }

    fun logout(): LiveData<Boolean> {
        saveLoginStatus(getApplication(), false)
        _logedIn.value = false
        return MutableLiveData(true)
    }
}

Trong UserViewModel này có một biến loggedIn được khai báo là kiểu LiveData. View sẽ lắng nghe dạng observer trên biến này để biết được trạng thái của người dùng đã login hay chưa. Các phương thức còn lại trong ViewModel này giúp View lấy được thông tin người dùng, và các hoạt động đăng nhập, đăng ký và đăng xuất khác.

Di Chuyển Giữa Các Destination Theo Điều Kiện

Phần này cũng không phải là một kỹ thuật gì cao siêu cả, chúng ta chỉ đang vận dụng tính linh hoạt của Navigation để chọn lựa destination nào mà chúng ta cần di chuyển khi ở HomeFragment. Đồng thời phần này cũng cho các bạn thấy khả năng kết hợp giữa Navigation với một số thành phần khác của Jetpack như ViewModel hay LiveData.

Vậy chúng ta hãy mở HomeFragment.kt lên và thêm vào một số code được tô sáng như sau.

class HomeFragment : Fragment() {

    private val userViewModel: UserViewModel by activityViewModels()
    
    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)
        val navController = findNavController()

        userViewModel.loggedIn.observe(viewLifecycleOwner, { hasLoggedIn ->
            if (hasLoggedIn.not()) {
                navController.navigate(R.id.action_homeFragment_to_signInFragment)
            }
        })

        view.findViewById<Button>(R.id.btnViewProfile)?.setOnClickListener {
            val action = HomeFragmentDirections.actionHomeFragmentToProfileFragment(nameArg = userViewModel.getUserName() ?: "")
            navController.navigate(action)
        }
    }
}

Đoạn code trên đây cũng đơn giản đúng không nào. Trước hết chúng ta cần khai báo một UserViewModel có tên userViewModel, dòng khai báo này có nhờ đến sự hỗ trợ của Android KTX.

private val userViewModel: UserViewModel by activityViewModels()

Sau đó, ngay khi Fragment này được tạo ra và hiển thị, nó lắng nghe sự thay đổi của userViewModel.loggedIn. Nếu biến này cho kết quả là false, tức là người dùng chưa đăng nhập, nó sẽ lập tức dẫn đến SignInFragment.

userViewModel.loggedIn.observe(viewLifecycleOwner, { hasLoggedIn ->
    if (hasLoggedIn.not()) {
        navController.navigate(R.id.action_homeFragment_to_signInFragment)
    }
})

Thay đổi cuối cùng là ở sự kiện click vào nút View Profile, chúng ta vẫn dùng code cũ, chỉ khác là thay thế việc truyền dòng chữ cứng “My Name” ở bài trước bằng đúng tên của người dùng đã đăng nhập thông qua lời gọi đến userViewModel.getUserName().

Đến đây khi thực thi chương trình, bạn sẽ nhìn thấy ngay màn hình đăng nhập, đó là do điều kiện khi lắng nghe observer từ LiveData khi HomeFragment được mở lên. Chúng ta hãy hoàn thiện các màn hình còn lại để hoàn chỉnh ứng dụng ở các bước tiếp theo nhé.

Hoàn Thiện Các Màn hình

Màn Hình Đăng Nhập

Ngay khi chạy ứng dụng thử nghiệm ở bước trên, chúng ta thấy ngay giao diện của màn hình đăng nhập. Tuy nhiên giao diện này chưa có bất kỳ sự kiện hay logic để người dùng tương tác gì cả, chúng ta sẽ hoàn thiện bằng các code được tô sáng sau.

class SignInFragment : Fragment() {

    private val userViewModel: UserViewModel by activityViewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_sign_in, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val navController = findNavController()
        view.findViewById<Button>(R.id.btnSignIn).setOnClickListener {
            userViewModel.login(
                view.findViewById<EditText>(R.id.edtUsername).text.toString(),
                view.findViewById<EditText>(R.id.edtPassword).text.toString()
            ).observe(viewLifecycleOwner, Observer { successful ->
                if (successful) {
                    navController.popBackStack()
                } else {
                    Toast.makeText(
                        requireContext(),
                        "Username or Password not correct",
                        Toast.LENGTH_SHORT
                    ).show()
                }
            })
        }

        view.findViewById<Button>(R.id.btnSignUp).setOnClickListener {
            navController.navigate(R.id.action_signInFragment_to_signUpFragment)
        }
    }
}

Đầu tiên vẫn là khai báo cho việc sử dụng userViewModel như ở HomeFragment trên kia.

Sau đó nếu người dùng nhấn vào nút Sign In, chúng ta sẽ gọi đến phương thức login()userViewModel. Nếu login thành công thì sẽ gọi đến phương thức navController.popBackStack(), câu lệnh này sẽ quay về Fragment trước đó trong Back Stack, đó chính là HomeFragment. Nếu login thất bại thì hiển thị thông báo dạng Toast. Nếu người dùng nhấn vào nút Sign Up sẽ đơn giản là dùng NavController để chuyển sang màn hình SignUpFragment thông qua action đã xây dựng trên kia.

Màn Hình Đăng Ký

Code hoàn thiện cho màn hình SignUpFragment cũng không quá khác so với SignInFragment ở trên.

class SignUpFragment : Fragment() {

    private val userViewModel: UserViewModel by activityViewModels()
    
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_sign_up, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        view.findViewById<Button>(R.id.btnCreateAccount).setOnClickListener {
            val userNameText = view.findViewById<EditText>(R.id.edtCreateUsername).text.toString()
            val passwordText = view.findViewById<EditText>(R.id.edtCreatePassword).text.toString()
            userViewModel.signUp(userNameText, passwordText).observe(viewLifecycleOwner, Observer { successful ->
                if (successful) {
                    findNavController().popBackStack()
                } else {
                    Toast.makeText(requireContext(), "Username or Password is not empty", Toast.LENGTH_SHORT).show()
                }
            })
        }
    }
}

Dĩ nhiên vẫn cần khai báo sử dụng userViewModel.

Rồi đến việc hiện thực sự kiện nhấn nút Sign Up, sự kiện này sẽ gọi đến phương thức userViewModel.signUp(). Nếu đăng ký thành công, chúng ta lại gọi findNavController.popBackStack(), câu lệnh này như bạn biết sẽ quay về Fragment trước đó trong Back Stack, đó chính là SignInFragment, quá đúng với mong muốn của chúng ta. Nếu đăng ký thất bại do dữ liệu nhập vào chưa chuẩn thì cũng hiển thị thông báo dạng Toast ra cho người dùng.

Màn Hình Xem Profile

Đến với ProfileFragment, nơi đây có một nút Logout. Đơn giản chúng ta chỉ cần gắn userViewModel và gọi đến phương thức userViewModel.logout().

class ProfileFragment : Fragment() {

    private val userViewModel: UserViewModel by activityViewModels()
    val args: ProfileFragmentArgs by navArgs()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_profile, container, false)
    }

    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")

        view.findViewById<Button>(R.id.btnLogout).setOnClickListener {
            userViewModel.logout().observe(viewLifecycleOwner, {
                findNavController().popBackStack()
            })
        }
    }
}

Trong phương thức logout() mình vẫn gọi đến findNavController().popBackStack(). Fragment trước ProfileFragment khi này là HomeFragment, việc popBackStack() khi này dĩ nhiên là đưa người dùng về lại HomeFragment rồi. Nhưng ngay khi về HomeFragment, sẽ gặp ngay điều kiện kiểm tra cờ loggedIn ở màn hình này, ngay lập tức người dùng được dẫn đến màn hình đăng nhập. Vậy thực tế thì các màn hình diễn ra sau khi nhấn vào nút Logout là: ProfileFragment -> HomeFragment -> SignInFragment, nhưng quá trình này diễn ra quá nhanh nên người dùng cảm giác như chuyển động sẽ từ ProfileFragment -> SignInFragment.

Đến lúc này thì bạn có thể thực thi ứng dụng để trải nghiệm một ứng dụng hoàn chỉnh rồi nhé.

Tuy nhiên, ứng dụng chỉ mới hoàn chỉnh chứ chưa hoàn hảo. Bạn có nhận thấy rằng tiêu đề của ứng dụng luôn luôn là NavigationSample không.

Tiêu đề của ứng dụng luôn là NavigatioonSample
Tiêu đề của ứng dụng luôn là NavigatioonSample

Chúng ta nên đặt tiêu đề cho từng màn hình cụ thể, để người dùng có một khái niệm rõ ràng rằng họ đang ở màn hình nào với mục tiếp theo đây.

Xây Dựng Toolbar

Cái việc hiển thị thông tin ở phía trên cùng của ứng dụng, bao gồm icon bên trái, tiêu đề ứng dụng ở giữa, rồi các icon bên phải,… là một chủ đề dài hơi, nó được gọi chung lại thành một cái tên là Top App Bar. Mình có từng viết bài hướng dẫn về cách sử dụng ActionBar để xây dựng các thành phần cho Top App Bar này, tuy nhiên có một công cụ mới hơn ActionBar để xây dựng nên một Top App Bar, nó có tên là Toolbar.

Dù bạn có đang xây dựng ứng dụng mới có dùng Navigation kết hợp với Toolbar, hay đang chỉnh sửa ứng dụng cũ có ActionBar hoạt động chung với Navigation của bài hôm nay, thì Navigation đều hỗ trợ sự kết nối này hết nhé. Chi tiết cho toàn bộ sự hỗ trợ này bạn có thể tìm hiểu đầy đủ ở hướng dẫn về NavigationUIlink này. Mục này mình chỉ ứng dụng kết hợp NavigationUI với Toolbar để xem tính hiệu quả của nó như thế nào nhé.

Khai Báo Các Tiêu Đề

Trước hết dĩ nhiên là phải khai báo resource các string tiêu đề cho từng màn hình rồi.

<resources>
    <string name="app_name">NavigationSample</string>
    <string name="welcome">Welcome %s</string>
    <string name="username">Username</string>
    <string name="password">Password</string>
    <string name="sign_in">Sign In</string>
    <string name="sign_up">Sign Up</string>
    <string name="don_t_have_account">Don\'t have account?</string>
    <string name="create_account">Create Account</string>
    <string name="logout">Logout</string>
    <string name="view_profile">View Profile</string>

    <!-- App titles -->
    <string name="home_label">Navigation Sample</string>
    <string name="sign_in_label">Sign In</string>
    <string name="sign_up_label">Sign Up</string>
    <string name="view_profile_label">My Profile</string>
    
</resources>

Sau đó, với các resource string được khai báo mới trên đây, bạn hãy mở lại login_nav_graph.xml. Nếu ở tab Design thì bạn hãy khai báo label cho từng màn hình tương ứng như hình dưới đây của mình. Các label này chính là các tiêu đề mà chúng ta mong muốn hiển thị lên Top App Bar ở từng loại màn hình.

Khai báo label, chính là tiêu đề cho từng màn hình
Khai báo label, chính là tiêu đề cho từng màn hình

Hoặc nhanh nhất là chuyển sang tab Code hay tab Split rồi sửa lại giá trị cho thuộc tính android:label ở mỗi thẻ fragment cũng được 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="@string/view_profile_label"
        tools:layout="@layout/fragment_profile" >
        <... />
    </fragment>
    <fragment
        android:id="@+id/homeFragment"
        android:name="com.yellowcode.navigationsample.HomeFragment"
        android:label="@string/home_label"
        tools:layout="@layout/fragment_home">
        <... />
    </fragment>
    <fragment
        android:id="@+id/signInFragment"
        android:name="com.yellowcode.navigationsample.SignInFragment"
        android:label="@string/sign_in_label"
        tools:layout="@layout/fragment_sign_in" >
        <.../>
    </fragment>
    <fragment
        android:id="@+id/signUpFragment"
        android:name="com.yellowcode.navigationsample.SignUpFragment"
        android:label="@string/sign_up_label"
        tools:layout="@layout/fragment_sign_up" />
</navigation>

Thêm Toolbar Vào Layout

Như đã nói ở trên, các label này sẽ hiển thị trên Top App Bar thông qua Toolbar. Để làm vậy thì chúng ta phải xây dựng Toolbar này trong giao diện của ứng dụng. Nếu bạn nào còn chưa rõ, thì Toolbar (hay Top App Bar) là một thành phần UI có thể nói là “xương sống” của ứng dụng. Nó sẽ luôn là một thanh ở trên cùng của màn hình, thanh UI này không bị thay thế trong quá trình hiển thị các màn hình khác nhau của ứng dụng, chúng chỉ thay đổi các trạng thái hay các nội dung thôi. Toolbar, ActionBar, Navigation Drawer hay Bottom Navigation đều là cùng một dạng UI kiểu xương sống này. Chính vì vậy mà để mang Toolbar vào ứng dụng, chúng ta nên thiết kế nó ở Activity, điều này cho phép chúng không bị khởi tạo lại trong quá trình thay đổi các Fragment con trong Activity đó.

Ứng dụng của chúng ta cũng vậy, để xây dựng Toolbar, bạn hãy mở activity_main.xml lên. Ở tab Design, bạn hãy tìm đến thành phần Toolbar như trong hình dưới đây.

Xây dựng Toolbar cho ứng dụng
Xây dựng Toolbar cho ứng dụng

Sau đó hãy kéo Toolbar này vào màn hình thiết kế, canh chỉnh sao cho Toolbar bạn vừa tạo ra nằm ở trên cùng màn hình, chiều dài hết cỡ chiều ngang của màn hình. Còn NavHostFragment mà chúng ta đã xây dựng từ các bài đầu tiên thì dĩ nhiên là nằm dưới Toolbar rồi. Điều này giúp cho như mình nói trên kia, rằng Toolbar sẽ được gắn vào theo Activity, còn các destination hiển thị bên trong NavHostFragment sẽ độc lập và không làm cho Toolbar phải bị gỡ ra thêm vào gì cả.

Dưới đây là hình ảnh sau khi đã canh chỉnh Toolbar so với NavHostFragment.

Canh chỉnh Toolbar nằm trên so với NavHostFragment
Canh chỉnh Toolbar nằm trên so với NavHostFragment

Thay Đổi Theme

Nếu bạn chạy ứng dụng ngay lúc này, bạn sẽ thấy dường như có 2 Top App Bar. Nguyên nhân là vì ứng dụng mặc định khi bạn tạo mới project có khai báo sẵn một ActionBar. Để dùng Toolbar thay thế thì chúng ta phải bỏ khai báo ActionBar mặc định này đi. Bằng cách bạn hãy tìm các file themes.xml (sở dĩ mình nói các file là vì các project được tạo mới khi này sẽ tự động thêm một themes.xml nữa cho dark mode bên cạnh themes.xml mặc định). Bạn hãy đảm bảo chuyển sao cho chúng dùng tới theme NoActionBar như sau.

<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.NavigationSample" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorOnPrimary">@color/white</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_700</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
        <!-- Customize your theme here. -->
    </style>
</resources>

Đến đây nếu bạn thử thực thi lại ứng dụng… thì… wow, có Top App Bar rồi. Cơ mà sao lại chẳng có tiêu đề gì cho từng màn hình cả? Nguyên nhân là vì ToolBarNavigation của chúng ta khi này chưa có sự kết nối với nhau. Chúng ta đi đến bước tiếp theo đây để xây dựng mối quan hệ này.

Dùng NavigationUI Để Kết Nối Navigation Với Toolbar

NavigationUI là một thành phần đi kèm với Navigation mà chúng ta đang làm quen đây. Tuy nhiên, chúng ta không mang ra nói chi tiết thành phần NavigationUI này vì dường như chúng ta không làm việc trực tiếp và nhiều đến nó. NavigationUI cung cấp các cách thức hữu dụng để giúp Navigation làm việc tốt với Top App Bar, hay với Navigation DrawerBottom Navigation nữa nếu ứng dụng có sử dụng các thành phần này.

Để thực hiện việc kết nối NavigationToolbar thì bạn hãy mở MainActivity.kt lên và thêm vào các dòng code sau.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val host: NavHostFragment = supportFragmentManager
            .findFragmentById(R.id.fragmentContainerView) as NavHostFragment? ?: return
        val navController = host.navController
        val appBarConfiguration = AppBarConfiguration(navController.graph)
        findViewById<Toolbar>(R.id.toolbar)
            .setupWithNavController(navController, appBarConfiguration)
    }
}

Đầu tiên chúng ta cần “triệu hồi” một NavController từ MainActivity. Khai báo một AppBarConfiguration và kết nối vào Toolbar. Sau các dòng code trên, ngoài việc tiêu đề của các màn hình đã được hiển thị lên Toolbar ra, thì một icon cũng được hiển thị ở bên trái của Toolbar.

Việc của chúng ta chẳng làm gì cả. Tất cả các thành phần trên đây tự chúng sẽ kết nối và xây dựng các phương thức tương tác với nhau thay cho chúng ta. Nếu người dùng đang ở start destination, trong trường hợp này là HomeFragment, sẽ không có icon nào ở bên trái màn hình, hoặc nó sẽ là drawer icon nếu có gắn với Navigation Drawer. Còn nếu người dùng đang ở các destination khác, icon lúc này tự động là Up Button giúp người dùng có thể quay về màn hình trước đó.

Bạn hãy thử khởi chạy ứng dụng để xem thành quả nhé.

Kết Luận

Với việc dừng ở bài hôm trước thì bạn cũng đã có cái nhìn đầy đủ về các kiến thức cơ bản của Navigation rồi. Do đó bài hôm nay chỉ mang tính chất mở rộng hơn cách thức sử dụng Navigation và mối liên quan của nó với các thành phần khác mà chúng ta đã biết, như ViewModel, LiveData hay Toolbar.

Tuy mình cố gắng làm sao trình bày cách sử dụng công cụ này ở một mức rõ ràng nhất vá xúc tích nhất có thể, mình vẫn thấy nó khá dài dòng và rườm ra, chính vì vậy mà thật khó để có thể tiếp tục nói thêm cho đủ về thành phần thú vị này. Còn khá nhiều thông tin còn khuyết về Navigation mà mình không thể nói đến, xin phép liệt kê ra đây để các bạn có thể tiếp tục tự tìm hiểu, hoặc nếu có duyên mình cũng sẽ viết về nó ở các bài viết khác trong thời gian sắp tới.

  • Chúng ta đã biết tạo các destination là các Fragment, vậy nếu destination là một Activity thì sao, hãy xem thêm ở đây.
  • Cách thức để lồng các NavGraph vào nhau (gọi là Nested Graph), hay sử dụng include để dùng đến một NavGraph khác bên trong một NavGraph sẵn có, thì xem thêm ở đây.
  • Global action, là action mà nhiều destination có thể dùng chung, xem ở đây.
  • Xây dựng deep link đến một destination xem ở đây.

Và còn nhiều kiến thức về Navigation khác nữa được tổng hợp ở trang chính của Android Developer cũng như ở các trang blog khác, nếu có hứng thú và quan tâm, các bạn có thể tự tìm đọc ở các tài liệu này nhé.

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.

Leave a Reply