শর্তসাপেক্ষ নেভিগেশন

আপনার অ্যাপের জন্য নেভিগেশন ডিজাইন করার সময়, আপনি হয়ত শর্তসাপেক্ষ লজিকের ভিত্তিতে একটি গন্তব্যের পরিবর্তে অন্য একটি গন্তব্যে নেভিগেট করতে চাইবেন। যেমন, কোনও ব্যবহারকারী এমন কোনও গন্তব্যের ডিপ লিঙ্ক ফলো করতে পারেন যেখানে ব্যবহারকারীকে লগ-ইন করতে হবে অথবা কোনও গেমে প্লেয়ার জিতলে বা হারলে আপনার কাছে আলাদা আলাদা গন্তব্য থাকতে পারে।

ব্যবহারকারীর লগ-ইন

এই উদাহরণে, একজন ব্যবহারকারী প্রোফাইল স্ক্রিনে নেভিগেট করার চেষ্টা করেন, যার জন্য যাচাইকরণ প্রয়োজন। এই অ্যাকশনের জন্য যাচাইকরণ প্রয়োজন, তাই ব্যবহারকারী আগে থেকে যাচাইকৃত না হলে, তাকে লগ-ইন স্ক্রিনে রিডাইরেক্ট করা উচিত।

এই উদাহরণের জন্য নেভিগেশন গ্রাফটি দেখতে কিছুটা এইরকম হতে পারে:

অ্যাপের মূল নেভিগেশন ফ্লো থেকে স্বাধীনভাবে লগ-ইন ফ্লো
            ম্যানেজ করা হয়।
ছবি ১. অ্যাপের মূল নেভিগেশন ফ্লো থেকে স্বাধীনভাবে লগ-ইন ফ্লো ম্যানেজ করা হয়।

যাচাইকরণ সম্পূর্ণ করতে, অ্যাপটিকে অবশ্যই login_fragment-এ নেভিগেট করতে হবে, যেখানে ব্যবহারকারী যাচাইকরণ সম্পূর্ণ করতে একটি ইউজারনেম এবং পাসওয়ার্ড লিখতে পারবেন। অনুমোদিত হলে, ব্যবহারকারীকে profile_fragment স্ক্রিনে ফিরিয়ে নিয়ে যাওয়া হয়। গ্রহণ না করা হলে, ব্যবহারকারীকে জানানো হয় যে তার ক্রেডেনশিয়াল সঠিক নয় এবং এর জন্য Snackbar ব্যবহার করা হয়। ব্যবহারকারী লগ-ইন না করে প্রোফাইল স্ক্রিনে ফিরে গেলে, তাকে main_fragment স্ক্রিনে পাঠানো হয়।

এই অ্যাপের নেভিগেশন গ্রাফ এখানে দেওয়া হল:

<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-এ একটি বোতাম আছে, ব্যবহারকারী তার প্রোফাইল দেখতে সেখানে ক্লিক করতে পারেন। ব্যবহারকারী প্রোফাইল স্ক্রিন দেখতে চাইলে, তাকে প্রথমে যাচাইকরণ করতে হবে। এই ইন্টার‍্যাকশন দুটি আলাদা ফ্র্যাগমেন্ট ব্যবহার করে মডেল করা হয়েছে, তবে এটি শেয়ার করা ব্যবহারকারীর অবস্থার উপর নির্ভর করে। এই স্টেট সংক্রান্ত তথ্য এই দুটি ফ্র্যাগমেন্টের কোনওটিরই দায়িত্ব নয় এবং এটি আরও উপযুক্তভাবে একটি শেয়ার করা UserViewModel-এ রাখা হয়। এই ViewModel অ্যাক্টিভিটিতে স্কোপ করার মাধ্যমে ফ্র্যাগমেন্টের মধ্যে শেয়ার করা হয়, যা ViewModelStoreOwner প্রয়োগ করে। নিচের উদাহরণে, requireActivity()-এর সমাধান MainActivity-এ হয়, কারণ MainActivity হোস্ট করে ProfileFragment:

Kotlin

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

জাভা

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);
        ...
    }
    ...
}

UserViewModel-এ ব্যবহারকারীর ডেটা LiveData-এর মাধ্যমে প্রকাশ করা হয়, তাই কোথায় নেভিগেট করতে হবে তা সিদ্ধান্ত নিতে, আপনার এই ডেটা পর্যবেক্ষণ করা উচিত। ProfileFragment-এ নেভিগেট করার পরে, ব্যবহারকারীর ডেটা থাকলে, অ্যাপটি একটি স্বাগত মেসেজ দেখায়। ব্যবহারকারীর ডেটা null হলে, আপনি LoginFragment-এ নেভিগেট করেন, কারণ ব্যবহারকারীকে তার প্রোফাইল দেখার আগে যাচাই করতে হবে। নিচের উদাহরণে যেভাবে দেখানো হয়েছে সেই হিসেবে আপনার ProfileFragment-এ সিদ্ধান্ত নেওয়ার লজিক নির্ধারণ করুন:

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() {
        ...
    }
}

জাভা

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() {
        ...
    }
}

ProfileFragment-এ পৌঁছানোর সময় ব্যবহারকারীর ডেটা null হলে, তাকে LoginFragment-এ রিডাইরেক্ট করা হয়।

আপনি NavController.getPreviousBackStackEntry() ব্যবহার করে আগের গন্তব্যের জন্য NavBackStackEntry পুনরুদ্ধার করতে পারবেন, যা গন্তব্যের জন্য NavController-নির্দিষ্ট স্টেট এনক্যাপসুলেট করে। LoginFragment আগের NavBackStackEntry-এর SavedStateHandle ব্যবহার করে একটি প্রাথমিক ভ্যালু সেট করে যা থেকে বোঝা যায় যে ব্যবহারকারী সফলভাবে লগ-ইন করেছেন কিনা। ব্যবহারকারী সিস্টেমের 'ফিরে যান' বোতামে সাথে সাথে প্রেস করলে, আমরা এই অবস্থায় ফিরে যেতে চাই। SavedStateHandle ব্যবহার করে এই স্টেট সেট করলে, প্রসেস বন্ধ করার পরেও স্টেটটি থেকে যায়।

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

জাভা

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);
    }
}

ব্যবহারকারী ইউজারনেম ও পাসওয়ার্ড লিখলে, তা যাচাইকরণের জন্য UserViewModel-এ পাঠানো হয়। যাচাইকরণ সফল হলে, UserViewModel ব্যবহারকারীর ডেটা স্টোর করে। LoginFragment তারপর SavedStateHandle-এ LOGIN_SUCCESSFUL ভ্যালু আপডেট করে এবং ব্যাক স্ট্যাক থেকে নিজেকে পপ অফ করে।

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
    }
}

জাভা

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
    }
}

মনে রাখবেন, যাচাইকরণ সংক্রান্ত সব লজিক UserViewModel-এর মধ্যে থাকে। এটি গুরুত্বপূর্ণ, কারণ ব্যবহারকারীদের কীভাবে যাচাই করা হবে তা নির্ধারণ করার দায়িত্ব LoginFragment বা ProfileFragment-এর নয়। আপনার লজিককে ViewModel-এ এনক্যাপসুলেট করলে, সেটি শুধু শেয়ার করাই সহজ হয় না, টেস্ট করাও সহজ হয়। আপনার নেভিগেশন লজিক জটিল হলে, আপনাকে বিশেষ করে পরীক্ষার মাধ্যমে এই লজিক যাচাই করতে হবে। পরীক্ষা করা যায় এমন কম্পোনেন্টের ভিত্তিতে আপনার অ্যাপের আর্কিটেকচার স্ট্রাকচার করার ব্যাপারে আরও তথ্যের জন্য অ্যাপ আর্কিটেকচার সংক্রান্ত গাইড দেখুন।

ProfileFragment-এ ফিরে গিয়ে, SavedStateHandle-এ সেভ করা LOGIN_SUCCESSFUL ভ্যালু onCreate() পদ্ধতিতে দেখা যেতে পারে। ব্যবহারকারী ProfileFragment-এ ফিরে এলে, LOGIN_SUCCESSFUL ভ্যালু চেক করা হবে। মান false হলে, ব্যবহারকারীকে 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)
                    }
                })
    }

    ...
}

জাভা

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);
                    }
                });
    }

    ...
}

ব্যবহারকারী সফলভাবে লগ-ইন করলে, ProfileFragment একটি স্বাগতম মেসেজ দেখায়।

এখানে ফলাফল চেক করার জন্য যে কৌশল ব্যবহার করা হয়েছে, তা আপনাকে দুটি আলাদা আলাদা কেসের মধ্যে পার্থক্য করতে সাহায্য করে:

  • প্রাথমিক কেস, যেখানে ব্যবহারকারী লগ-ইন করেননি এবং তাকে লগ-ইন করতে বলা উচিত।
  • ব্যবহারকারী লগ-ইন করেননি কারণ তিনি লগ-ইন না করার সিদ্ধান্ত নিয়েছেন (false-এর ফলাফল)।

এইসব ব্যবহারের কেস আলাদা করে, আপনি ব্যবহারকারীকে বারবার লগ-ইন করতে বলার সমস্যা এড়াতে পারবেন। ব্যর্থতার কেস ম্যানেজ করার জন্য বিজনেস লজিক আপনার উপর ছেড়ে দেওয়া হয়েছে এবং এতে এমন একটি ওভারলে দেখানো অন্তর্ভুক্ত থাকতে পারে যা ব্যাখ্যা করে যে কেন ব্যবহারকারীকে লগ-ইন করতে হবে, সম্পূর্ণ অ্যাক্টিভিটি শেষ করতে হবে বা ব্যবহারকারীকে এমন একটি গন্তব্যে রিডাইরেক্ট করতে হবে যেখানে লগ-ইন করার প্রয়োজন নেই, যেমনটি আগের কোডের উদাহরণে ছিল।