تجربة التنقّل

من المهم اختبار منطق التنقل في تطبيقك قبل الشحن للتأكّد من عمل تطبيقك على النحو المتوقَّع.

يتولى مكوِّن التنقل جميع أعمال إدارة التنقل بين الوجهات، وتمرير الوسيطات، والعمل باستخدام FragmentManager. سبق أن تم اختبار هذه الإمكانات بدقة عالية، لذلك لا داعي لاختبارها مرة أخرى في تطبيقك، ولكن ما يهمّ اختبارها هو التفاعلات بين الرمز البرمجي المحدّد للتطبيق في أجزائه وNavController الخاصة بها. يشرح هذا الدليل بعض سيناريوهات التنقل الشائعة وكيفية اختبارها.

اختبار التنقل بين الأجزاء

لاختبار تفاعلات التجزئة مع NavController بشكل منفصل، يوفّر الانتقال 2.3 والإصدارات الأحدث TestNavHostController الذي يوفّر واجهات برمجة تطبيقات لإعداد الوجهة الحالية والتحقّق من تسلسل استدعاء الدوال البرمجية بعد عمليات NavController.navigate().

يمكنك إضافة أداة اختبار التنقل إلى مشروعك عن طريق إضافة التبعية التالية في ملف build.gradle لوحدة التطبيق:

رائع

dependencies {
  def nav_version = "2.7.7"

  androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
}

Kotlin

dependencies {
  val nav_version = "2.7.7"

  androidTestImplementation("androidx.navigation:navigation-testing:$nav_version")
}

لنفترض أنك تصمم لعبة معلومات عامة. تبدأ اللعبة بعبارة title_screen وتنتقل إلى شاشة in_game عندما ينقر المستخدم على زر التشغيل.

قد يبدو الجزء الذي يمثّل title_screen على النحو التالي:

Kotlin

class TitleScreen : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ) = inflater.inflate(R.layout.fragment_title_screen, container, false)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        view.findViewById<Button>(R.id.play_btn).setOnClickListener {
            view.findNavController().navigate(R.id.action_title_screen_to_in_game)
        }
    }
}

Java

public class TitleScreen extends Fragment {

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_title_screen, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        view.findViewById(R.id.play_btn).setOnClickListener(v -> {
            Navigation.findNavController(view).navigate(R.id.action_title_screen_to_in_game);
        });
    }
}

لاختبار ما إذا كان التطبيق ينقل المستخدم بشكل صحيح إلى شاشة in_game عندما ينقر المستخدم على تشغيل، يحتاج الاختبار إلى التحقّق من أنّ هذا الجزء ينقل NavController إلى شاشة R.id.in_game بطريقة صحيحة.

باستخدام مزيج من FragmentScenario والإسبريسو وTestNavHostController، يمكنك إعادة إنشاء الشروط اللازمة لاختبار هذا السيناريو، كما هو موضّح في المثال التالي:

Kotlin

@RunWith(AndroidJUnit4::class)
class TitleScreenTest {

    @Test
    fun testNavigationToInGameScreen() {
        // Create a TestNavHostController
        val navController = TestNavHostController(
            ApplicationProvider.getApplicationContext())

        // Create a graphical FragmentScenario for the TitleScreen
        val titleScenario = launchFragmentInContainer<TitleScreen>()

        titleScenario.onFragment { fragment ->
            // Set the graph on the TestNavHostController
            navController.setGraph(R.navigation.trivia)

            // Make the NavController available via the findNavController() APIs
            Navigation.setViewNavController(fragment.requireView(), navController)
        }

        // Verify that performing a click changes the NavController’s state
        onView(ViewMatchers.withId(R.id.play_btn)).perform(ViewActions.click())
        assertThat(navController.currentDestination?.id).isEqualTo(R.id.in_game)
    }
}

Java

@RunWith(AndroidJUnit4.class)
public class TitleScreenTestJava {

    @Test
    public void testNavigationToInGameScreen() {

        // Create a TestNavHostController
        TestNavHostController navController = new TestNavHostController(
            ApplicationProvider.getApplicationContext());

        // Create a graphical FragmentScenario for the TitleScreen
        FragmentScenario<TitleScreen> titleScenario = FragmentScenario.launchInContainer(TitleScreen.class);

        titleScenario.onFragment(fragment ->
                // Set the graph on the TestNavHostController
                navController.setGraph(R.navigation.trivia);

                // Make the NavController available via the findNavController() APIs
                Navigation.setViewNavController(fragment.requireView(), navController)
        );

        // Verify that performing a click changes the NavController’s state
        onView(ViewMatchers.withId(R.id.play_btn)).perform(ViewActions.click());
        assertThat(navController.currentDestination.id).isEqualTo(R.id.in_game);
    }
}

ينشئ المثال أعلاه مثيل TestNavHostController ويخصّصه للجزء. ثم يستخدم Espresso لإدارة واجهة المستخدم والتحقق من اتخاذ إجراء التنقّل المناسب.

تمامًا مثل NavController الحقيقي، يجب طلب setGraph لإعداد TestNavHostController. في هذا المثال، كان الجزء الذي يجري اختباره وجهة البداية للرسم البياني لدينا. TestNavHostController توفر طريقة setCurrentDestination تسمح لك بتعيين الوجهة الحالية (ووسيطات تلك الوجهة اختياريًا) بحيث تكون NavController في الحالة الصحيحة قبل بدء الاختبار.

على عكس NavHostController الذي يستخدمه NavHostFragment، TestNavHostController لا يشغّل سلوك navigate() الأساسي (مثل FragmentTransaction الذي يفعله FragmentNavigator) عند طلب navigate()، بل يعدّل حالة TestNavHostController فقط.

اختبار واجهة مستخدم التنقل باستخدام سيناريو التجزئة

في المثال السابق، يتم استدعاء استدعاء الدالة الذي تم توفيره إلى titleScenario.onFragment() بعد انتقال الجزء خلال مراحل نشاطه إلى حالة RESUMED. بحلول هذا الوقت، يكون عرض الجزء قد تم إنشاؤه وإرفاقه، لذلك قد يكون الأوان قد فات في دورة حياة الاختبار بشكل صحيح. على سبيل المثال، عند استخدام NavigationUI مع طرق العرض في الجزء الخاص بك، كما هو الحال مع عنصر Toolbar الذي يتحكّم فيه الجزء، يمكنك استدعاء طُرق الإعداد باستخدام NavController قبل أن يصل الجزء إلى حالة RESUMED. وبالتالي، تحتاج إلى طريقة لضبط TestNavHostController في مرحلة مبكرة من دورة حياة المنتج.

يمكن كتابة الجزء الذي يملك Toolbar الخاص به على النحو التالي:

Kotlin

class TitleScreen : Fragment(R.layout.fragment_title_screen) {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val navController = view.findNavController()
        view.findViewById<Toolbar>(R.id.toolbar).setupWithNavController(navController)
    }
}

Java

public class TitleScreen extends Fragment {
    public TitleScreen() {
        super(R.layout.fragment_title_screen);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        NavController navController = Navigation.findNavController(view);
        view.findViewById(R.id.toolbar).setupWithNavController(navController);
    }
}

هنا نحتاج إلى NavController الذي تم إنشاؤه عند استدعاء onViewCreated(). سيؤدي استخدام النهج السابق لـ onFragment() إلى تأخير TestNavHostController كثيرًا في دورة الحياة، ما يتسبب في تعذُّر طلب findNavController().

توفّر FragmentScenario واجهة FragmentFactory يمكن استخدامها لتسجيل عمليات معاودة الاتصال لأحداث مراحل النشاط. يمكن دمج هذا الإجراء مع Fragment.getViewLifecycleOwnerLiveData() لتلقّي رد اتصال بعد onCreateView() مباشرةً، كما هو موضّح في المثال التالي:

Kotlin

val scenario = launchFragmentInContainer {
    TitleScreen().also { fragment ->

        // In addition to returning a new instance of our Fragment,
        // get a callback whenever the fragment’s view is created
        // or destroyed so that we can set the NavController
        fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
            if (viewLifecycleOwner != null) {
                // The fragment’s view has just been created
                navController.setGraph(R.navigation.trivia)
                Navigation.setViewNavController(fragment.requireView(), navController)
            }
        }
    }
}

Java

FragmentScenario<TitleScreen> scenario =
FragmentScenario.launchInContainer(
       TitleScreen.class, null, new FragmentFactory() {
    @NonNull
    @Override
    public Fragment instantiate(@NonNull ClassLoader classLoader,
            @NonNull String className,
            @Nullable Bundle args) {
        TitleScreen titleScreen = new TitleScreen();

        // In addition to returning a new instance of our fragment,
        // get a callback whenever the fragment’s view is created
        // or destroyed so that we can set the NavController
        titleScreen.getViewLifecycleOwnerLiveData().observeForever(new Observer<LifecycleOwner>() {
            @Override
            public void onChanged(LifecycleOwner viewLifecycleOwner) {

                // The fragment’s view has just been created
                if (viewLifecycleOwner != null) {
                    navController.setGraph(R.navigation.trivia);
                    Navigation.setViewNavController(titleScreen.requireView(), navController);
                }

            }
        });
        return titleScreen;
    }
});

وباستخدام هذه التقنية، تكون السمة NavController متاحة قبل استدعاء السمة onViewCreated()، ما يسمح للجزء باستخدام طرق NavigationUI بدون تعطُّل.

اختبار التفاعلات مع إدخالات الحزمة الخلفية

عند التفاعل مع إدخالات تسلسل استدعاء الدوال البرمجية في الخلفية، تسمح لك السمة TestNavHostController بتوصيل وحدة التحكّم باختبارك الخاص بكل من LifecycleOwner وViewModelStore وOnBackPressedDispatcher، وذلك باستخدام واجهات برمجة التطبيقات التي تكتسبها من NavHostController.

على سبيل المثال، عند اختبار جزء يستخدم ViewModel على نطاق التنقّل، يجب استدعاء setViewModelStore على TestNavHostController:

Kotlin

val navController = TestNavHostController(ApplicationProvider.getApplicationContext())

// This allows fragments to use by navGraphViewModels()
navController.setViewModelStore(ViewModelStore())

Java

TestNavHostController navController = new TestNavHostController(ApplicationProvider.getApplicationContext());

// This allows fragments to use new ViewModelProvider() with a NavBackStackEntry
navController.setViewModelStore(new ViewModelStore())