Google berkomitmen untuk mendorong terwujudnya keadilan ras bagi komunitas Kulit Hitam. Lihat caranya.

Navigasi bersyarat

Saat mendesain navigasi untuk aplikasi Anda, sebaiknya lakukan navigasi ke satu tujuan dan tujuan lainnya berdasarkan logika bersyarat. Misalnya, Anda mungkin memiliki beberapa tujuan yang mengharuskan pengguna untuk login, atau mungkin Anda memiliki berbagai tujuan dalam game untuk saat pemain menang atau kalah.

Login pengguna

Dalam contoh ini, pengguna mencoba menavigasi ke layar profil yang memerlukan autentikasi. Karena tindakan ini memerlukan autentikasi, pengguna harus dialihkan ke layar login jika mereka belum diautentikasi.

Grafik navigasi untuk contoh ini akan terlihat seperti ini:

Gambar 1: Alur login ditangani secara terpisah dari alur navigasi utama aplikasi.

Untuk melakukan autentikasi, aplikasi harus menavigasi ke login_fragment, tempat pengguna dapat memasukkan nama pengguna dan sandi untuk autentikasi. Jika disetujui, pengguna akan dikirim kembali ke layar profile_fragment. Jika ditolak, pengguna akan diberi tahu bahwa kredensial mereka tidak valid menggunakan Snackbar.

Tujuan dalam aplikasi ini ditunjukkan menggunakan fragmen yang dihosting oleh aktivitas tunggal.

Berikut adalah grafik navigasi untuk aplikasi ini:

<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/nav_graph"
        app:startDestination="@id/main_fragment">

    <fragment
            android:id="@+id/main_fragment"
            android:name="com.google.android.conditionalnav.MainFragment"
            android:label="fragment_main"
            tools:layout="@layout/fragment_main">

        <action
                android:id="@+id/navigate_to_profile_fragment"
                app:destination="@id/profile_fragment"/>
    </fragment>

    <fragment
            android:id="@+id/login_fragment"
            android:name="com.google.android.conditionalnav.LoginFragment"
            android:label="login_fragment"
            tools:layout="@layout/login_fragment"/>

    <fragment
            android:id="@+id/profile_fragment"
            android:name="com.google.android.conditionalnav.ProfileFragment"
            android:label="fragment_profile"
            tools:layout="@layout/fragment_profile"/>

</navigation>

MainFragment berisi tombol yang dapat diklik pengguna jika ingin melihat profilnya. Jika ingin melihat layar profil, pengguna harus melakukan autentikasi. Model interaksi ini dibuat menggunakan dua fragmen terpisah, tetapi bergantung pada status bersama, apakah pengguna telah diautentikasi, dan jika sudah, nama pengguna yang diautentikasi. Perlu diingat bahwa informasi status bukan merupakan tanggung jawab kedua fragmen ini dan lebih sesuai disimpan dalam ViewModel bersama, seperti yang ditunjukkan dalam contoh berikut:

Kotlin

class LoginViewModel : ViewModel() {
    enum class AuthenticationState {
        UNAUTHENTICATED,        // Initial state, the user needs to authenticate
        AUTHENTICATED  ,        // The user has authenticated successfully
        INVALID_AUTHENTICATION  // Authentication failed
    }

    val authenticationState = MutableLiveData<AuthenticationState>()
    var username: String

    init {
        // In this example, the user is always unauthenticated when MainActivity is launched
        authenticationState.value = AuthenticationState.UNAUTHENTICATED
        username = ""
    }

    fun refuseAuthentication() {
        authenticationState.value = AuthenticationState.UNAUTHENTICATED
    }

    fun authenticate(username: String, password: String) {
        if (passwordIsValidForUsername(username, password)) {
            this.username = username
            authenticationState.value = AuthenticationState.AUTHENTICATED
        } else {
            authenticationState.value = AuthenticationState.INVALID_AUTHENTICATION
        }
    }

    private fun passwordIsValidForUsername(username: String, password: String): Boolean {
        ...
    }
}

Java

public class LoginViewModel extends ViewModel {

    public enum AuthenticationState {
        UNAUTHENTICATED,        // Initial state, the user needs to authenticate
        AUTHENTICATED,          // The user has authenticated successfully
        INVALID_AUTHENTICATION  // Authentication failed
    }

    final MutableLiveData<AuthenticationState> authenticationState =
            new MutableLiveData<>();
    String username;

    public LoginViewModel() {
        // In this example, the user is always unauthenticated when MainActivity is launched
        authenticationState.setValue(AuthenticationState.UNAUTHENTICATED);
        username = "";
    }

    public void authenticate(String username, String password) {
        if (passwordIsValidForUsername(username, password)) {
            this.username = username;
            authenticationState.setValue(AuthenticationState.AUTHENTICATED);
        } else {
            authenticationState.setValue(AuthenticationState.INVALID_AUTHENTICATION);
        }
    }

    public void refuseAuthentication() {
       authenticationState.setValue(AuthenticationState.UNAUTHENTICATED);
    }

    private boolean passwordIsValidForUsername(String username, String password) {
        ...
    }
}

ViewModel dicakupkan ke ViewModelStoreOwner. Anda dapat berbagi data antara fragmen dengan membuat ViewModel masuk ke cakupan aktivitas, yang menerapkan ViewModelStoreOwner. Dalam contoh berikut, requireActivity() dicocokkan ke MainActivity karena MainActivity menghosting ProfileFragment:

Kotlin

class LoginFragment : Fragment() {

    private val viewModel: LoginViewModel by activityViewModels()
    ...
}

Java

public class LoginFragment extends Fragment {

    private LoginViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class);
        ...
    }
    ...
}

Status autentikasi pengguna ditunjukkan sebagai class enum dalam LoginViewModel dan diekspos melalui LiveData, sehingga untuk memutuskan tujuan navigasi, Anda harus mengamati status tersebut. Saat menavigasi ke ProfileFragment, aplikasi akan menunjukkan pesan selamat datang jika pengguna telah diautentikasi. Jika tidak diautentikasi, Anda perlu menavigasi ke LoginFragment, karena pengguna harus melakukan autentikasi agar dapat melihat profilnya. Anda harus menentukan logika penentu dalam ViewModel, seperti yang ditunjukkan dalam contoh berikut:

Kotlin

class ProfileFragment : Fragment() {

    private val viewModel: LoginViewModel by activityViewModels()

    ...

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

        welcomeTextView = view.findViewById(R.id.welcome_text_view)

        val navController = findNavController()
        viewModel.authenticationState.observe(viewLifecycleOwner, Observer { authenticationState ->
            when (authenticationState) {
                AUTHENTICATED -> showWelcomeMessage()
                UNAUTHENTICATED -> navController.navigate(R.id.login_fragment)
            }
        })
    }

    private fun showWelcomeMessage() {
        ...
    }
}
...

Java

public class ProfileFragment extends Fragment {

    private LoginViewModel viewModel;

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class);

        welcomeTextView = view.findViewById(R.id.welcome_text_view);

        final NavController navController = Navigation.findNavController(view);
        viewModel.authenticationState.observe(getViewLifecycleOwner(),
                new Observer<LoginViewModel.AuthenticationState>() {
            @Override
            public void onChanged(LoginViewModel.AuthenticationState authenticationState) {
                switch (authenticationState) {
                    case AUTHENTICATED:
                        showWelcomeMessage();
                        break;
                    case UNAUTHENTICATED:
                        navController.navigate(R.id.loginFragment);
                        break;
                }
            }
        });
    }

    private void showWelcomeMessage() {
        ...
    }
    ...
}

Jika belum terautentikasi saat sampai di ProfileFragment, pengguna akan menavigasi ke LoginFragment. Setelah itu, mereka dapat memasukkan nama pengguna dan sandi, yang kemudian diteruskan ke LoginViewModel.

Jika autentikasi berhasil, ViewModel akan menetapkan status autentikasi ke AUTHENTICATED. Hal ini akan memunculkan LoginFragment pada data sebelumnya, sehingga mengalihkan pengguna kembali ke ProfileFragment. Jika autentikasi tidak berhasil karena kredensial tidak valid, status ditetapkan ke INVALID_AUTHENTICATION, dan pengguna diberi Snackbar dalam LoginFragment. Terakhir, jika pengguna menekan tombol Kembali, status akan ditetapkan ke UNAUTHENTICATED dan stack akan dikembalikan ke MainFragment.

Kotlin

class LoginFragment : Fragment() {

    private val viewModel: LoginViewModel by activityViewModels()

    private lateinit var usernameEditText: EditText
    private lateinit var passwordEditText: EditText
    private lateinit var loginButton: Button
    ...

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

        usernameEditText = view.findViewById(R.id.username_edit_text)
        passwordEditText = view.findViewById(R.id.password_edit_text)

        loginButton = view.findViewById(R.id.login_button)
        loginButton.setOnClickListener {
            viewModel.authenticate(usernameEditText.text.toString(),
                    passwordEditText.text.toString())
        }

        requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
            viewModel.refuseAuthentication()
            navController.popBackStack(R.id.main_fragment, false)
        })

        val navController = findNavController()
        viewModel.authenticationState.observe(viewLifecycleOwner, Observer { authenticationState ->
            when (authenticationState) {
                AUTHENTICATED -> navController.popBackStack()
                INVALID_AUTHENTICATION -> showErrorMessage()
            }
        })
    }

    private void showErrorMessage() {
        ...
    }
}

Java

public class LoginFragment extends Fragment {

    private LoginViewModel viewModel;

    private EditText usernameEditText;
    private EditText passwordEditText;
    private Button loginButton;

    ...

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class);

        usernameEditText = view.findViewById(R.id.username_edit_text);
        passwordEditText = view.findViewById(R.id.password_edit_text);

        loginButton = view.findViewById(R.id.login_button);
        loginButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                viewModel.authenticate(usernameEditText.getText().toString(),
                        passwordEditText.getText().toString());
            }
        });

        requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(),
            new OnBackPressedCallback(true) {
                @Override
                public void handleOnBackPressed() {
                    viewModel.refuseAuthentication();
                    navController.popBackStack(R.id.main_fragment, false);
                }
            });

        final NavController navController = Navigation.findNavController(view);
        final View root = view;
        viewModel.authenticationState.observe(getViewLifecycleOwner(),
                new Observer<LoginViewModel.AuthenticationState>() {
            @Override
            public void onChanged(LoginViewModel.AuthenticationState authenticationState) {
                switch (authenticationState) {
                    case AUTHENTICATED:
                        navController.popBackStack();
                        break;
                    case INVALID_AUTHENTICATION:
                        Snackbar.make(root,
                                R.string.invalid_credentials,
                                Snackbar.LENGTH_SHORT
                                ).show();
                        break;
                }
            }
        });
    }
}

Perlu diingat bahwa semua logika yang berkaitan dengan autentikasi akan disimpan dalam LoginViewModel. Hal ini penting, karena LoginFragment atau ProfileFragment tidak bertanggung jawab untuk menentukan cara pengguna diautentikasi. Enakapsulasi logika dalam ViewModel tidak hanya membuatnya lebih mudah dibagikan, tetapi juga lebih mudah untuk diuji. Jika logika navigasi Anda rumit, Anda harus secara khusus memverifikasi logika ini melalui pengujian. Baca Panduan arsitektur aplikasi untuk informasi selengkapnya tentang cara menyusun arsitektur aplikasi Anda menggunakan komponen yang dapat diuji.

Saat pengguna kembali ke ProfileFragment, status autentikasi mereka akan diperiksa kembali. Jika telah diautentikasi, aplikasi akan menampilkan pesan selamat datang menggunakan nama pengguna yang terautentikasi, seperti yang ditunjukkan dalam contoh berikut:

Kotlin

class ProfileFragment : Fragment() {

    private val viewModel: LoginViewModel by activityViewModels()

    private lateinit var welcomeTextView: TextView

    ...

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

        welcomeTextView = view.findViewById(R.id.welcome_text_view)

        val navController = findNavController()
        viewModel.authenticationState.observe(viewLifeycleOwner, Observer { authenticationState ->
            when (authenticationState) {
                AUTHENTICATED -> showWelcomeMessage()
                UNAUTHENTICATED -> navController.navigate(R.id.loginFragment)
        })
    }

    private fun showWelcomeMessage() {
        welcomeTextView.text = getString(R.string.welcome, viewModel.username)
    }

    ...
}

Java

public class ProfileFragment extends Fragment {

    private LoginViewModel viewModel;

    private TextView welcomeTextView;

    ...

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class);

        welcomeTextView = view.findViewById(R.id.welcome_text_view);

        final NavController navController = Navigation.findNavController(view);
        viewModel.authenticationState.observe(getViewLifecycleOwner(),
                new Observer<LoginViewModel.AuthenticationState>() {
            @Override
            public void onChanged(LoginViewModel.AuthenticationState authenticationState) {
                switch (authenticationState) {
                    case AUTHENTICATED:
                        showWelcomeMessage();
                        break;
                    case UNAUTHENTICATED:
                        navController.navigate(R.id.loginFragment);
                        break;
                }
            }
        });
    }

    private void showWelcomeMessage() {
        welcomeTextView.setText(getString(R.string.welcome, viewModel.username));
    }
    ...
}

Tidak semua tindakan navigasi didasarkan pada kondisi, tetapi pola ini dapat cukup berguna untuk yang didasarkan pada kondisi. Anda dapat menentukan cara pengguna melakukan navigasi dalam aplikasi Anda dengan menentukan kondisi saat mereka melakukan navigasi dan menyediakan sumber kepercayaan bersama dalam ViewModel untuk komunikasi antarfragmen.

Pengalaman pengguna pemula

Pengalaman pengguna pemula (FTUE) adalah alur spesifik yang hanya dilihat oleh pengguna saat meluncurkan aplikasi Anda untuk pertama kali. Daripada membuat alur ini menjadi bagian dari grafik navigasi utama aplikasi, Anda harus menyimpan alur ini sebagai grafik navigasi bertingkat terpisah.

Berdasarkan contoh login di bagian sebelumnya, Anda mungkin menjumpai skenario ketika pengguna memiliki kesempatan untuk mendaftar jika tidak memiliki info login, seperti yang ditunjukkan dengan tombol REGISTER pada gambar:

Gambar 2: Layar login sekarang memiliki tombol REGISTER.

Saat mengklik tombol REGISTER, pengguna akan dialihkan ke alur subnavigasi tertentu untuk pendaftaran. Setelah mendaftar, data sebelumnya akan dimunculkan, dan pengguna dialihkan langsung ke layar profil.

Grafik Navigasi pada contoh di bawah ini telah diupdate untuk menyertakan grafik navigasi bertingkat. Tindakan juga telah ditambahkan ke login_fragment dan dapat dipicu sebagai respons dari mengetuk REGISTER:

<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/nav_graph"
        app:startDestination="@id/main_fragment">

    ...

    <fragment
            android:id="@+id/login_fragment"
            android:name="com.google.android.conditionalnav.LoginFragment"
            android:label="login_fragment"
            tools:layout="@layout/fragment_login">

        <action android:id="@+id/action_login_fragment_to_register_fragment"
                app:destination="@id/registration_graph" />

    </fragment>

    <navigation android:id="@+id/registration_graph"
            app:startDestination="@id/enter_user_profile_fragment">

        <fragment android:id="@+id/enter_user_profile_fragment"
                android:name="com.google.android.conditionalnav.registration.EnterProfileDataFragment"
                android:label="Enter Profile Data"
                tools:layout="@layout/fragment_enter_profile_info">

            <action android:id="@+id/move_to_choose_user_password"
                    app:destination="@id/choose_user_password_fragment" />

        </fragment>

        <fragment android:id="@+id/choose_user_password_fragment"
                android:name="com.google.android.conditionalnav.registration.ChooseUserPasswordFragment"
                android:label="Choose User + Password"
                tools:layout="@layout/fragment_choose_user_password" />

    </navigation>
</navigation>

Ditunjukkan secara visual dalam editor Navigasi, grafik bertingkat ini muncul sebagai Nested Graph yang lebih kecil dengan ID registration_graph di bagian atas, seperti yang ditunjukkan pada gambar 3:

Gambar 3: Grafik navigasi sekarang menampilkan registration_graph bertingkat.

Klik dua kali pada Nested Graph di editor untuk menampilkan detail grafik pendaftaran. Pada gambar 4, Anda dapat melihat alur pendaftaran dua layar. Layar pertama mengumpulkan nama lengkap pengguna dan informasi biografi. Layar kedua mencatat nama pengguna dan sandi yang diinginkan. Untuk kembali ke grafik navigasi utama, klik ← Root di panel Destinations.

Gambar 4: Grafik bertingkat menunjukkan alur pendaftaran.

Tujuan awal grafik navigasi bertingkat ini adalah layar Enter Profile Data. Setelah pengguna memasukkan data profil dan mengklik tombol NEXT, aplikasi akan beralih ke layar Create Login Credentials. Setelah selesai membuat nama pengguna dan sandi, pengguna dapat mengklik REGISTER + LOGIN untuk langsung dialihkan ke layar profil.

Seperti contoh sebelumnya, ViewModel digunakan untuk berbagi informasi di antara fragmen pendaftaran:

Kotlin

class RegistrationViewModel : ViewModel() {

    enum class RegistrationState {
        COLLECT_PROFILE_DATA,
        COLLECT_USER_PASSWORD,
        REGISTRATION_COMPLETED
    }

    val registrationState =
            MutableLiveData<RegistrationState>(RegistrationState.COLLECT_PROFILE_DATA)

    // Simulation of real-world scenario, where an auth token may be provided as
    // an alternate authentication mechanism instead of passing the password
    // around. This is set at the end of the registration process.
    var authToken = ""
        private set

    fun collectProfileData(name: String, bio: String) {
        // ... validate and store data

        // Change State to collecting username and password
        registrationState.value = RegistrationState.COLLECT_USER_PASSWORD
    }

    fun createAccountAndLogin(username: String, password: String) {
        // ... create account
        // ... authenticate
        this.authToken = // token

        // Change State to registration completed
        registrationState.value = RegistrationState.REGISTRATION_COMPLETED
    }

    fun userCancelledRegistration() : Boolean {
        // Clear existing registration data
        registrationState.value = RegistrationState.COLLECT_PROFILE_DATA
        authToken = ""
        return true
    }

}

Java

public class RegistrationViewModel extends ViewModel {

    enum RegistrationState {
        COLLECT_PROFILE_DATA,
        COLLECT_USER_PASSWORD,
        REGISTRATION_COMPLETED
    }

    private MutableLiveData<RegistrationState> registrationState =
            new MutableLiveData<>(RegistrationState.COLLECT_PROFILE_DATA);

    public MutableLiveData<RegistrationState> getRegistrationState() {
        return registrationState;
    }

    // Simulation of real-world scenario, where an auth token may be provided as
    // an alternate authentication mechanism instead of passing the password
    // around. This is set at the end of the registration process.
    private String authToken;

    public String getAuthToken() {
        return authToken;
    }

    public void collectProfileData(String name, String bio) {
        // ... validate and store data

        // Change State to collecting username and password
        registrationState.setValue( RegistrationState.COLLECT_USER_PASSWORD);
    }

    public void createAccountAndLogin(String username, String password) {
        // ... create account
        // ... authenticate
        this.authToken = // token

        // Change State to registration completed
        registrationState.setValue(RegistrationState.REGISTRATION_COMPLETED);
    }

    public boolean userCancelledRegistration() {
        // Clear existing registration data
        registrationState.setValue(RegistrationState.COLLECT_PROFILE_DATA);
        authToken = "";
        return true;
    }

}

Status pendaftaran ViewModel ini teramati dari fragmen setiap layar pendaftaran. Status ini mendorong perpindahan ke layar berikutnya dan diupdate oleh RegistrationViewModel berdasarkan interaksi pengguna. Menekan kembali setiap saat akan membatalkan proses pendaftaran dan mengembalikan pengguna ke layar login:

Kotlin

class EnterProfileDataFragment : Fragment() {

    val registrationViewModel by activityViewModels<RegistrationViewModel>()

    ...

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

        val navController = findNavController()

        ...

        // When the next button is clicked, collect the current values from the
        // two edit texts and pass to the ViewModel to store.
        view.findViewById<Button>(R.id.next_button).setOnClickListener {
            val name = fullnameEditText.text.toString()
            val bio = bioEditText.text.toString()
            registrationViewModel.collectProfileData(name, bio)
        }

        // RegistrationViewModel will update the registrationState to
        // COLLECT_USER_PASSWORD when ready to move to the choose username and
        // password screen.
        registrationViewModel.registrationState.observe(
                viewLifecycleOwner, Observer { state ->
                    if (state == COLLECT_USER_PASSWORD) {
                        navController.navigate(R.id.move_to_choose_user_password)
                    }
                })

        // If the user presses back, cancel the user registration and pop back
        // to the login fragment. Since this ViewModel is shared at the activity
        // scope, its state must be reset so that it will be in the initial
        // state if the user comes back to register later.
        requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
            registrationViewModel.userCancelledRegistration()
            navController.popBackStack(R.id.login_fragment, false)
        })
    }
}

class ChooseUserPasswordFragment : Fragment() {

    private val loginViewModel: LoginViewModel by activityViewModels()
    private val registrationViewModel: RegistrationViewModel by activityViewModels()

    ...

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

        val navController = findNavController()

        ...

        // When the register button is clicked, collect the current values from
        // the two edit texts and pass to the ViewModel to complete registration.
        view.findViewById<Button>(R.id.register_button).setOnClickListener {
            registrationViewModel.createAccountAndLogin(
                    usernameEditText.text.toString(),
                    passwordEditText.text.toString()
            )
        }

        // RegistrationViewModel updates the registrationState to
        // REGISTRATION_COMPLETED when ready, and for this example, the username
        // is accessed as a read-only property from RegistrationViewModel and is
        // used to directly authenticate with loginViewModel.
        registrationViewModel.registrationState.observe(
                viewLifecycleOwner, Observer { state ->
                    if (state == REGISTRATION_COMPLETED) {

                        // Here we authenticate with the token provided by the ViewModel
                        // then pop back to the profie_fragment, where the user authentication
                        // status will be tested and should be authenticated.
                        val authToken = registrationViewModel.token
                        loginViewModel.authenticate(authToken)
                        navController.popBackStack(R.id.profile_fragment, false)
                    }
                }
        )

        // If the user presses back, cancel the user registration and pop back
        // to the login fragment. Since this ViewModel is shared at the activity
        // scope, its state must be reset so that it is in the initial state if
        // the user comes back to register later.
        requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
            registrationViewModel.userCancelledRegistration()
            navController.popBackStack(R.id.login_fragment, false)
        })

    }
}

Java

public class EnterProfileDataFragment extends Fragment {

    private RegistrationViewModel registrationViewModel;
    ...

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {

        registrationViewModel = new ViewModelProvider(requireActivity())
                .get(RegistrationViewModel.class);

        final NavController navController = findNavController(view);
        ...

        // When the next button is clicked, collect the current values from the two edit texts
        // and pass to the ViewModel to store.
        view.findViewById(R.id.next_button).setOnClickListener(v -> {
            String name = fullnameEditText.getText().toString();
            String bio = bioEditText.getText().toString();
            registrationViewModel.collectProfileData(name, bio);
        });

        // RegistrationViewModel updates the registrationState to
        // COLLECT_USER_PASSWORD when ready to move to the choose username and
        // password screen.
        registrationViewModel.getRegistrationState().observe(getViewLifecycleOwner(), state -> {
            if (state == COLLECT_USER_PASSWORD) {
                navController.navigate(R.id.move_to_choose_user_password);
            }
        });

        // If the user presses back, cancel the user registration and pop back
        // to the login fragment. Since this ViewModel is shared at the activity
        // scope, its state must be reset so that it is in the initial state if
        // the user comes back to register later.
        requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(),
            new OnBackPressedCallback(true) {
                @Override
                public void handleOnBackPressed() {
                    registrationViewModel.userCancelledRegistration();
                    navController.popBackStack(R.id.login_fragment, false);
                }
            });
    }
}

class ChooseUserPasswordFragment extends Fragment {

    private LoginViewModel loginViewModel;
    private RegistrationViewModel registrationViewModel;
    ...

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {

        ViewModelProvider provider = new ViewModelProvider(requireActivity());
        registrationViewModel = provider.get(RegistrationViewModel.class);
        loginViewModel = provider.get(LoginViewModel.class);

        final NavController navController = findNavController(view);

        ...

        // When the register button is clicked, collect the current values from
        // the two edit texts and pass to the ViewModel to complete registration.
        view.findViewById(R.id.register_button).setOnClickListener(v ->
                registrationViewModel.createAccountAndLogin(
                        usernameEditText.getText().toString(),
                        passwordEditText.getText().toString()
                )
        );

        // RegistrationViewModel updates the registrationState to
        // REGISTRATION_COMPLETED when ready, and for this example, the username
        // is accessed as a read-only property from RegistrationViewModel and is
        // used to directly authenticate with loginViewModel.
        registrationViewModel.getRegistrationState().observe(
                getViewLifecycleOwner(), state -> {

                    if (state == REGISTRATION_COMPLETED) {
                        // Here we authenticate with the token provided by the ViewModel
                        // then pop back to the profie_fragment, where the user authentication
                        // status will be tested and should be authenticated.
                        String authToken = registrationViewModel.getAuthToken();
                        loginViewModel.authenticate(authToken);
                        navController.popBackStack(R.id.profile_fragment, false);
                    }
                }
        );

        // If the user presses back, cancel the user registration and pop back
        // to the login fragment. Since this ViewModel is shared at the activity
        // scope, its state must be reset so that it will be in the initial
        // state if the user comes back to register later.
        requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(),
            new OnBackPressedCallback(true) {
                @Override
                public void handleOnBackPressed() {
                    registrationViewModel.userCancelledRegistration();
                    navController.popBackStack(R.id.login_fragment, false);
                }
            });
    }
}

Dengan tetap memisahkan alur FTUE ini ke dalam grafiknya sendiri, Anda dapat dengan mudah mengubah sub-aliran tanpa memengaruhi alur navigasi utama. Jika ingin merangkum lebih jauh grafik FTUE bertingkat, Anda juga dapat menyimpannya dalam file resource navigasi terpisah dan menyertakannya melalui elemen <include> dalam grafik navigasi utama Anda.