[Android Jetpack] Navigation - Conditional Navigation

1 Introduction

Proposed to be completed in this subsection Conditional Navigation case. The specific logic is:

  • There are a total of three Fragment s, which are the home page that visitors can view, the details that can only be viewed after logging in, and the login page.
  • After the user enters the home page, if the user clicks to jump to the details page, it is determined whether the user is logged in. If not logged in to enter the login page, otherwise display the details page data.
  • The specific technologies used are Navigation, ViewModel, and LiveData.

Note: The user's login here is simulated, and the background is not requested, and the completion is judged directly in the ViewModel.

2. Implementation

2.1 Basic configuration

Specify the logical jump relationship between the home page and the details page in the nav_graph.xml navigation graph file, as shown below:

And specify the starting target as homeFragment in nav_graph.xml:

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    app:startDestination="@id/homeFragment">

Use ConstraintLayout in the layout file of MainActvity, and define a Fragment container, specified as NavHostFragment type, namely:

<fragment
    android:id="@+id/fragmentContainerView"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="true"
    app:navGraph="@navigation/nav_graph" />

And specify that the app:defaultNavHost property is true, which means that NavHostFragment will intercept the system return button; use the app:navGraph property to specify the navigation graph file. In order to be able to intercept the return key, the following two steps need to be completed in MainActivity:

  • Set the intercepted system return arrow display through NavigationUI;
  • Respond to user click events by overriding onSupportNavigationUp;

code show as below:

class MainActivity : AppCompatActivity() {

    // NavController object
    private lateinit var navController: NavController

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 1. Set the display return arrow through NavigationUI
        navController =
            Navigation.findNavController(this, R.id.fragmentContainerView)
        NavigationUI.setupActionBarWithNavController(this, navController)
    }

    // 2. Override onSupportNavigateUp
    override fun onSupportNavigateUp(): Boolean {
        return  navController.navigateUp()
    }
}

2.2 Page logic

Divided into three Fragment pages, the first is to jump to the monitoring of DetailFragment after completing the click in HomeFragment:

// HomeFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    val textView = view.findViewById<TextView>(R.id.homeFragment_textView)

    textView.setOnClickListener {
        this.findNavController().navigate(R.id.detailFragment)
    }
}

Complete the judgment of user login in DetailFragment. The logic here is:

  • Use ViewModel to store a user object of type LiveData, initialize the content as a visitor when entering, and register an observation for the LiveData type of this user data type. If the user stored in the current ViewModel is a visitor, jump to the login page;
  • Get the instance of the current return stack, and get the SavedStateHandle from it. This object is used to store data in the form of key-value pairs. It can be used to set an observable LiveData during get and set.

Complete the judgment of user login in LoginFragment. The logic is:

  • Use the same ViewModel as DetailFragment, and add a variable to store the login success or failure in this ViewModel. After the login is successful, the user User and the corresponding login success flag are updated.
  • Register the observer of the login success flag in LoginFragment. If the login is successful, store the login success flag as true in the SavedStateHandle of the previous instance in the stack. and back the stack.

2.3 Code

2.3.1 UserModel

Store two variables, the user and the login success flag:

class UserViewModel : ViewModel() {
    var user = MutableLiveData<User>()
    var loginResult = MutableLiveData<LoginResult>()

    fun login(username: String, password: String){
        val flag = (username == "Name" && password == "123")
        if(flag) { // equal in writing
            user.value = User(username, password)
        }
        loginResult.value = LoginResult(flag)
        Log.e("TAG", "login: $flag")
    }

    fun initUser(){
        user.value = User("tourists", "123")
    }
}

2.3.2 LoginFragment

The login judgment in the ViewModel is called, and the registration observer observes the login success or not. Then add flags and fall back according to the logic.

class LoginFragment : Fragment() {

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

    private lateinit var userViewModel: UserViewModel
    private var savedStateHandle: SavedStateHandle? = null

    companion object {
        const val LOGIN_SUCCESSFUL = "LOGIN_SUCCESSFUL"
    }


    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        userViewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory())
            .get(UserViewModel::class.java)
        // Retrieve the NavBackStackEntry of the previous destination
        savedStateHandle = Navigation.findNavController(view)
            .previousBackStackEntry
            ?.savedStateHandle
        // At the initial moment, set the login status to false
        savedStateHandle?.set(LOGIN_SUCCESSFUL, false)

        // Get the data entered by the user
        val userName = view.findViewById<EditText>(R.id.editTextTextPersonName)
        val password = view.findViewById<EditText>(R.id.editTextTextPassword)
        val submit = view.findViewById<Button>(R.id.submit)

        submit.setOnClickListener {
            login(userName.text.toString(), password.text.toString())
        }

        userViewModel.loginResult.observe(viewLifecycleOwner) {
            Log.e("TAG", "onViewCreated: Hahaha ${ it?.success }" )
            if (it?.success == true) {
                savedStateHandle?.set(LOGIN_SUCCESSFUL, true);
                NavHostFragment.findNavController(this@LoginFragment).popBackStack()
                Snackbar.make(view, "login successful", Snackbar.LENGTH_LONG)
                    .show()
            } else {
                Snackbar.make(view, "Login failed", Snackbar.LENGTH_LONG)
                    .show()
            }
        }
    }

    private fun login(userName: String, passWord: String){
        userViewModel.login(userName, passWord)
    }
}

2.3.3 DetailFragment

class DetailFragment : Fragment() {

    private lateinit var userViewModel: UserViewModel

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

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // It may be a fallback to this page after logging in
        Log.e("TAG", "onCreate: It may be a fallback to this page after logging in", )
        val navController = NavHostFragment.findNavController(this)
        // retrieve the current nav
        val currentBackStackEntry = navController.currentBackStackEntry
        val savedStateHandle = currentBackStackEntry?.savedStateHandle
        if (currentBackStackEntry != null) {
            savedStateHandle?.getLiveData<Boolean>(LoginFragment.LOGIN_SUCCESSFUL)
                ?.observe(currentBackStackEntry, object : Observer<Boolean>{
                    override fun onChanged(flag: Boolean?) {
                        if(flag == true) {
                            // login successful
                            val startDestinationId = navController.graph.startDestinationId
                            val navOptions = NavOptions.Builder()
                                .setPopUpTo(startDestinationId, true)
                                .build()
                            navController.navigate(startDestinationId, null, navOptions)
                        } else {
                            // Login failed, fall back to guest page
                            NavHostFragment.findNavController(this@DetailFragment).popBackStack()
                        }
                    }
                })
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // Determine if user information is empty
        userViewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory())
            .get(UserViewModel::class.java)
        val findNavController = this.findNavController()

        userViewModel.user.observe(viewLifecycleOwner) {
            if (it.name == "tourists") {
                findNavController.navigate(R.id.loginFragment)
            }
        }

        // Initialize a guest login
        if(userViewModel.user.value?.name == null) {
            userViewModel.initUser()
        }
    }

}

3. Code address

Code address: https://github.com/baiyazi/JetpackNotes/tree/main/project/navi

Tags: Android kotlin Navigation

Posted by paul_20k on Tue, 03 May 2022 04:32:59 +0300