Navigasi bersyarat

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

Login pengguna

Dalam contoh ini, pengguna mencoba membuka 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:

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

Untuk melakukan autentikasi, aplikasi harus membuka 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 kredensialnya tidak valid menggunakan Snackbar. Jika pengguna beranjak kembali ke layar profil tanpa login, dia akan diarahkan ke layar main_fragment.

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 untuk melihat profilnya. Jika ingin melihat layar profil, pengguna harus melakukan autentikasi terlebih dahulu. Interaksi ini dibuat menggunakan dua fragmen terpisah, tetapi bergantung pada status pengguna bersama. Informasi status ini tidak menjadi tanggung jawab kedua fragmen tersebut dan lebih sesuai disimpan dalam UserViewModel bersama. ViewModel ini dibagikan di antara fragmen dengan menggabungkannya ke aktivitas, yang menerapkan ViewModelStoreOwner. Dalam contoh berikut, requireActivity() me-resolve ke MainActivity karena MainActivity menghosting ProfileFragment:

Kotlin

class ProfileFragment : Fragment() {
    private val userViewModel: UserViewModel by activityViewModels()
    ...
}

Java

public class ProfileFragment extends Fragment {
    private UserViewModel userViewModel;
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        userViewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class);
        ...
    }
    ...
}

Data pengguna di UserViewModel ditampilkan melalui LiveData, sehingga untuk memutuskan tujuan navigasi, Anda harus mengamati data ini. Setelah membuka ProfileFragment, aplikasi menampilkan pesan selamat datang jika data pengguna ada. Jika data pengguna adalah null, Anda harus membuka LoginFragment, karena pengguna perlu melakukan autentikasi sebelum melihat profilnya. Tentukan logika penentu di ProfileFragment Anda, seperti yang ditunjukkan pada contoh berikut:

Kotlin

class ProfileFragment : Fragment() {
    private val userViewModel: UserViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val navController = findNavController()
        userViewModel.user.observe(viewLifecycleOwner, Observer { user ->
            if (user != null) {
                showWelcomeMessage()
            } else {
                navController.navigate(R.id.login_fragment)
            }
        })
    }

    private fun showWelcomeMessage() {
        ...
    }
}

Java

public class ProfileFragment extends Fragment {
    private UserViewModel userViewModel;

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        userViewModel = new ViewModelProvider(requireActivity()).get(UserViewModel.class);
        final NavController navController = Navigation.findNavController(view);
        userViewModel.user.observe(getViewLifecycleOwner(), (Observer<User>) user -> {
            if (user != null) {
                showWelcomeMessage();
            } else {
                navController.navigate(R.id.login_fragment);
            }
        });
    }

    private void showWelcomeMessage() {
        ...
    }
}

Jika data pengguna adalah null saat sampai di ProfileFragment, pengguna akan dialihkan ke LoginFragment.

Anda dapat menggunakan NavController.getPreviousBackStackEntry() untuk mengambil NavBackStackEntry pada tujuan sebelumnya, yang mengenkapsulasi status khusus NavController untuk tujuan tersebut. LoginFragment menggunakan SavedStateHandle dari NavBackStackEntry sebelumnya untuk menetapkan nilai awal yang menunjukkan apakah pengguna berhasil login. Ini adalah status yang ingin kita tampilkan jika pengguna segera menekan tombol kembali sistem. Menetapkan status ini menggunakan SavedStateHandle akan memastikan bahwa status tetap ada selama penghentian proses.

Kotlin

class LoginFragment : Fragment() {
    companion object {
        const val LOGIN_SUCCESSFUL: String = "LOGIN_SUCCESSFUL"
    }

    private val userViewModel: UserViewModel by activityViewModels()
    private lateinit var savedStateHandle: SavedStateHandle

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        savedStateHandle = findNavController().previousBackStackEntry!!.savedStateHandle
        savedStateHandle.set(LOGIN_SUCCESSFUL, false)
    }
}

Java

public class LoginFragment extends Fragment {
    public static String LOGIN_SUCCESSFUL = "LOGIN_SUCCESSFUL"

    private UserViewModel userViewModel;
    private SavedStateHandle savedStateHandle;

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

        savedStateHandle = Navigation.findNavController(view)
                .getPreviousBackStackEntry()
                .getSavedStateHandle();
        savedStateHandle.set(LOGIN_SUCCESSFUL, false);
    }
}

Setelah pengguna memasukkan nama pengguna dan sandi, dia akan diteruskan ke UserViewModel untuk autentikasi. Jika autentikasi berhasil, UserViewModel akan menyimpan data pengguna. Kemudian, LoginFragment akan mengubah nilai LOGIN_SUCCESSFUL pada SavedStateHandle dan memunculkannya dari data sebelumnya.

Kotlin

class LoginFragment : Fragment() {
    companion object {
        const val LOGIN_SUCCESSFUL: String = "LOGIN_SUCCESSFUL"
    }

    private val userViewModel: UserViewModel by activityViewModels()
    private lateinit var savedStateHandle: SavedStateHandle

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        savedStateHandle = findNavController().previousBackStackEntry!!.savedStateHandle
        savedStateHandle.set(LOGIN_SUCCESSFUL, false)

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

        loginButton.setOnClickListener {
            val username = usernameEditText.text.toString()
            val password = passwordEditText.text.toString()
            login(username, password)
        }
    }

    fun login(username: String, password: String) {
        userViewModel.login(username, password).observe(viewLifecycleOwner, Observer { result ->
            if (result.success) {
                savedStateHandle.set(LOGIN_SUCCESSFUL, true)
                findNavController().popBackStack()
            } else {
                showErrorMessage()
            }
        })
    }

    fun showErrorMessage() {
        // Display a snackbar error message
    }
}

Java

public class LoginFragment extends Fragment {
    public static String LOGIN_SUCCESSFUL = "LOGIN_SUCCESSFUL"

    private UserViewModel userViewModel;
    private SavedStateHandle savedStateHandle;

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

        savedStateHandle = Navigation.findNavController(view)
                .getPreviousBackStackEntry()
                .getSavedStateHandle();
        savedStateHandle.set(LOGIN_SUCCESSFUL, false);

        EditText usernameEditText = view.findViewById(R.id.username_edit_text);
        EditText passwordEditText = view.findViewById(R.id.password_edit_text);
        Button loginButton = view.findViewById(R.id.login_button);

        loginButton.setOnClickListener(v -> {
            String username = usernameEditText.getText().toString();
            String password = passwordEditText.getText().toString();
            login(username, password);
        });
    }

    private void login(String username, String password) {
        userViewModel.login(username, password).observe(viewLifecycleOwner, (Observer<LoginResult>) result -> {
            if (result.success) {
                savedStateHandle.set(LOGIN_SUCCESSFUL, true);
                NavHostFragment.findNavController(this).popBackStack();
            } else {
                showErrorMessage();
            }
        });
    }

    private void showErrorMessage() {
        // Display a snackbar error message
    }
}

Perlu diingat bahwa semua logika yang berkaitan dengan autentikasi akan disimpan dalam UserViewModel. Hal ini penting, karena LoginFragment atau ProfileFragment tidak bertanggung jawab untuk menentukan cara pengguna diautentikasi. Enkapsulasi 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.

Kembali ke ProfileFragment, nilai LOGIN_SUCCESSFUL yang disimpan di SavedStateHandle dapat dilihat di metode onCreate(). Saat pengguna kembali ke ProfileFragment, nilai LOGIN_SUCCESSFUL akan diperiksa. Jika nilainya adalah false, pengguna dapat dialihkan kembali ke MainFragment.

Kotlin

class ProfileFragment : Fragment() {
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val navController = findNavController()

        val currentBackStackEntry = navController.currentBackStackEntry!!
        val savedStateHandle = currentBackStackEntry.savedStateHandle
        savedStateHandle.getLiveData<Boolean>(LoginFragment.LOGIN_SUCCESSFUL)
                .observe(currentBackStackEntry, Observer { success ->
                    if (!success) {
                        val startDestination = navController.graph.startDestination
                        val navOptions = NavOptions.Builder()
                                .setPopUpTo(startDestination, true)
                                .build()
                        navController.navigate(startDestination, null, navOptions)
                    }
                })
    }

    ...
}

Java

public class ProfileFragment extends Fragment {
    ...

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        NavController navController = NavHostFragment.findNavController(this);

        NavBackStackEntry navBackStackEntry = navController.getCurrentBackStackEntry();
        SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle();
        savedStateHandle.getLiveData(LoginFragment.LOGIN_SUCCESSFUL)
                .observe(navBackStackEntry, (Observer<Boolean>) success -> {
                    if (!success) {
                        int startDestination = navController.getGraph().getStartDestination();
                        NavOptions navOptions = new NavOptions.Builder()
                                .setPopUpTo(startDestination, true)
                                .build();
                        navController.navigate(startDestination, null, navOptions);
                    }
                });
    }

    ...
}

Jika pengguna berhasil login, ProfileFragment akan menampilkan pesan selamat datang.

Teknik yang digunakan di sini untuk memeriksa hasilnya memungkinkan Anda membedakan antara dua kasus yang berbeda:

  • Kasus awal, saat pengguna tidak login dan harus diminta untuk login.
  • Pengguna tidak login karena dia memilih untuk tidak login (hasil dari false).

Dengan membedakan kasus penggunaan ini, Anda dapat menghindari berulang kali meminta pengguna untuk login. Logika bisnis untuk menangani kasus kegagalan diserahkan kepada Anda dan mungkin termasuk menampilkan overlay yang menjelaskan alasan pengguna perlu login, menyelesaikan seluruh aktivitas, atau mengalihkan pengguna ke tujuan yang tidak mengharuskan login, seperti yang terjadi pada contoh kode sebelumnya.