ู…ูƒูˆู† ุงู„ุชู†ู‚ู„-ุฌูˆุชุณูˆ ุŒ ุงู„ู…ุฌู„ุฏ. 3 - ุฑูƒู† ุงู„ุญุงู„ุงุช



ููŠ ู‡ุฐุง ุงู„ุฌุฒุก ู…ู† ุงู„ุซู„ุงุซูŠุฉ ุญูˆู„ ู…ูƒูˆู† ุงู„ุชู†ู‚ู„ ุŒ ุณู†ุญู„ู„ ูƒูŠููŠุฉ ุชู†ุธูŠู… ุงู„ุชู†ู‚ู„ ููŠ ุงู„ุชุทุจูŠู‚ุงุช ู…ุชุนุฏุฏุฉ ุงู„ูˆุญุฏุงุช ุŒ ูˆูƒูŠููŠุฉ ุงู„ุนู…ู„ ู…ุน ุงู„ุฑูˆุงุจุท ุงู„ุนู…ูŠู‚ุฉ ุŒ ูˆูƒุฐู„ูƒ ุงู„ู†ุธุฑ ููŠ ุงู„ุญุงู„ุงุช ุงู„ุชูŠ ุชุญุชูˆูŠ ุนู„ู‰ ุฃุฌุฒุงุก ูˆู…ุฑุจุนุงุช ุญูˆุงุฑ ู…ุถู…ู†ุฉ.



ู‡ุฐู‡ ู‡ูŠ ุงู„ู…ู‚ุงู„ุฉ ุงู„ุซุงู„ุซุฉ ูˆุงู„ุฃุฎูŠุฑุฉ ููŠ ุณู„ุณู„ุฉ ุญูˆู„ ุญุงู„ุงุช ุงู„ุชู†ู‚ู„ ุงู„ู…ุฎุชู„ูุฉ ุจุงุณุชุฎุฏุงู… ู…ูƒูˆู† ุงู„ุชู†ู‚ู„. ูŠู…ูƒู†ูƒ ุฃูŠุถุง ุฃู† ุชุฑู‰ ุงู„ุฃูˆู„ู‰ ูˆ ุงู„ุซุงู†ูŠุฉ ุฃุฌุฒุงุก





, , . , . , UI, ( API presentation-) . โ€“ , .



?



, : :vacancy :company flow. :vacancy :company, .



, .



App- +



โ€“ application- feature- .





: app-, feature-, feature-, . feature- Navigation Component, :



// ::vacancy module
interface VacancyRouterSource {

    fun openNextVacancy(vacancyId: String)

    // For navigation to another module
    fun openCompanyFlow()

}


app- , action- :



fun initVacancyDI(navController: NavController) {
  VacancyDI.vacancyRouterSource = object : VacancyRouterSource {
      override fun openNextVacancy(vacancyId: String) {
          navController.navigate(
              VacancyFragmentDirections
                .actionVacancyFragmentToVacancyFragment(vacancyId = vacancyId)
          )
      }

      override fun openCompanyFlow() {
          initCompanyDI(navController)
          navController.navigate(R.id.action__VacancyFragment__to__CompanyFlow)
      }
  }
}


โ€“ , . :



  • , , DI feature-;
  • Safe Args , navArgs, Directions, Navigation Component- feature-, .


, , .



feature- +



โ€“ feature- ( โ€“ URI, Navigation Component 2.1).





, : app-, feature-, feature-, .



app- , . feature-.



<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/company_flow__nav_graph"
    app:startDestination="@id/CompanyFragment">
    <fragment
        android:id="@+id/CompanyFragment"
        android:name="company.CompanyFragment">

        <deepLink app:uri="companyflow://company" />

        <!-- Or with arguments -->
        <argument android:name="company_id" app:argType="long" />
        <deepLink app:uri="companyflow://company" />

        <action
            android:id="@+id/action__CompanyFragment__to__CompanyDetailsFragment"
            app:destination="@id/CompanyDetailsFragment" />
    </fragment>

    <fragment
        android:id="@+id/CompanyDetailsFragment"
        android:name="company.CompanyDetailsFragment" />
</navigation>


Feature- , . , . deepLink, CompanyFragment .



CompanyFragment :vacancy :



// ::vacancy module

fragment_vacancy__button__open_company_flow.setOnClickListener {
  // Navigation through deep link
  val companyFlowUri = "companyflow://company".toUri()
  findNavController().navigate(companyFlowUri)
}


, . โ€“ Safe Args, ยซยปโ€Ž (Enum, Serializable, Parcelable) .



P.S. , , JSON String- , -โ€ฆ .





โ€“ , feature-.





- app-, โ€“ feature-; . , feature-. feature- common navigation.



? , common- destination- (, , activity), XML-! , Android Studio : XML- , , , , Safe Args . feature- common-, action- .



โ€“ - Navigation Component- feature-. :



  • critical path feature-, ;
  • : - destination-, , common-.




  • .
  • , , , , .




. Android- ยซโ€Ž ยปโ€Ž. , . , Navigation Component โ€“ , .



.



?



, Navigation Component.



  • โ€“ , Splash-


, Favorites Splash-:





  • ViewPager-


ViewPager- Responses:





  • , โ€“ . Splash-, , ,


Profile . Splash-, , โ€“ Profile.





, . Navigation Component .





, . , ( , ):



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

    <fragment
        android:id="@+id/SplashFragment"
        android:name="ui.splash.SplashFragment" />

    <fragment
        android:id="@+id/MainFragment"
        android:name="ui.main.MainFragment">

        <deepLink app:uri="www.example.com/main" />

    </fragment>

</navigation>


, , Android Manifest:



<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.aaglobal.jnc_playground">

    <application android:name=".App">
        <activity android:name=".ui.root.RootActivity">

            <nav-graph android:value="@navigation/app_nav_graph"/>

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

        </activity>
    </application>

</manifest>


, , adb-:



adb shell am start \
  -a android.intent.action.VIEW \
  -d "https://www.example.com/main" com.aaglobal.jnc_playground


--โ€ฆ . . โ€“ IllegalStateException: FragmentManager is already executing transactions. , , Handler.post:



// MainFragment.kt โ€” fragment with BottomNavigationView

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    if (savedInstanceState == null) {
        safeSetupBottomNavigationBar()
    }
}

private fun safeSetupBottomNavigationBar() {
    Handler().post {
        setupBottomNavigationBar()
    }
}


, : , Splash-, . , , .



, : , Activity. activity . , URI, adb- โ€“ , , startDestination.



โ€“ .



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

    <fragment
        android:id="@+id/SearchContainerFragment"
        android:name="tabs.search.SearchContainerFragment">

        <deepLink app:uri="www.example.com/main" />

        <action
            android:id="@+id/action__SearchContainerFragment__to__CompanyFlow"
            app:destination="@id/company_flow__nav_graph" />
        <action
            android:id="@+id/action__SearchContainerFragment__to__VacancyFragment"
            app:destination="@id/vacancy_nav_graph" />
    </fragment>
</navigation>


, , :





, , Splash-. , ! Splash-, .



โ€“ , .



Navigation Component, :



When a user opens your app via an explicit deep link, the task back stack is cleared and replaced with the deep link destination.

back stack , Navigation Component- . , - , - .



. โ€“ handleDeepLink NavController-:



handleDeepLink
public void handleDeepLink(@Nullable Intent intent) {
    // ...
    if ((flags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
        // Start with a cleared task starting at our root when we're on our own task
        if (!mBackStack.isEmpty()) {
            popBackStackInternal(mGraph.getId(), true);
        }
        int index = 0;
        while (index < deepLink.length) {
            int destinationId = deepLink[index++];
            NavDestination node = findDestination(destinationId);
            if (node == null) {
                final String dest = NavDestination.getDisplayName(mContext, destinationId);
                throw new IllegalStateException("Deep Linking failed:"
                        + " destination " + dest
                        + " cannot be found from the current destination "
                        + getCurrentDestination());
            }
            navigate(node, bundle,
                    new NavOptions.Builder().setEnterAnim(0).setExitAnim(0).build(), null);
        }
        return true;
    }
}


, :



  • Navigation Component;
  • NavController ( , NavController- ) โ€“ FixedNavController;
  • NavController- FixedNavController.


, ? , . , . , . .



, : auth-.





, Profile, . Back . - , Profile.



, . , , .





NavController, , .



NavController- โ€“ isDeepLinkHandled, โ€“ , NavController . , , ViewPager, , :



if (findMyNavController().isDeepLinkHandled && requireActivity().intent.data != null) {
    val uriString = requireActivity().intent.data?.toString()
    val selectedPosition = when {
        uriString == null -> 0
        uriString.endsWith("favorites") -> 0
        uriString.endsWith("subscribes") -> 1
        else -> 2
    }
    fragment_favorites_container__view_pager.setCurrentItem(selectedPosition, true)
}


, , , NavController-, isDeepLinkHandled private-. , reflection-, .





Navigation Component . , Google :



  • , ;
  • , , โ€“ ;
  • auth flow, .., ..


Navigation Component- .



Navigation Component



  • , .
  • โ€“ , AndroidManifest- .


- โ€“



, , . . .





, , .



?



, โ€“ :



<fragment
  android:id="@+id/VacancyFragment"
  android:name="com.aaglobal.jnc_playground.ui.vacancy.VacancyFragment"
  android:label="Fragment vacancy"
  tools:layout="@layout/fragment_vacancy">

  <argument
      android:name="vacancyId"
      app:argType="string"
      app:nullable="false" />

  <action
      android:id="@+id/action__VacancyFragment__to__VacancyFragment"
      app:destination="@id/VacancyFragment" />

</fragment>


โ€“ , Back . , , popUpTo action-.





hh . , , . , .



?



:



<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/fragment_favorites_container__text__title"
        style="@style/LargeTitle"
        android:text="Favorites container" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragment_favorites_container__container__recommend_vacancies"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>


runtime- :



class FavoritesContainerFragment : Fragment(R.layout.fragment_favorites_container) {

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

        childFragmentManager.attachFragmentInto(
          containerId = R.id.fragment_container_view,
          fragment = createVacancyListFragment()
        )

    }
}


attachFragmentInfo childFragmentManager โ€“ extension-, , .



:



class FavoritesContainerFragment : Fragment(R.layout.fragment_favorites_container) {

    // ...
    private fun createVacancyListFragment(): Fragment {
        return VacancyListFragment.newInstance(
          vacancyType = "favorites_container",
          vacancyListRouterSource = object : VacancyListRouterSource {
              override fun navigateToVacancyScreen(item: VacancyItem) {
                  findNavController().navigate(
                      R.id.action__FavoritesContainerFragment__to__VacancyFragment,
                      VacancyFragmentArgs(vacancyId = "${item.name}|${item.id}").toBundle()
                  )
              }
        }
     }

}


โ€“ , .





BottomSheetDialog-, Navigation Component.



?



- , . - dialog destination- , action .



<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/menu__favorites"
    app:startDestination="@id/FavoritesContainerFragment">
   <dialog
        android:id="@+id/ABottomSheet"
        android:name="ui.dialogs.dialog_a.ABottomSheetDialog">
        <action
            android:id="@+id/action__ABottomSheet__to__BBottomSheet"
            app:destination="@id/BBottomSheet"
            app:popUpTo="@id/ABottomSheet"
            app:popUpToInclusive="true" />
    </dialog>

    <dialog
        android:id="@+id/BBottomSheet"
        android:name="ui.dialogs.dialog_b.BBottomSheetDialog">
        <action
            android:id="@+id/action__BBottomSheet__to__ABottomSheet"
            app:destination="@id/ABottomSheet"
            app:popUpTo="@id/BBottomSheet"
            app:popUpToInclusive="true" />
    </dialog>
</navigation>


, , Back .



-



โ€“ .





Navigation Component. , , - . , Navigation Component-, - .



, , . , โ€“ , -: , Cicerone. , , Navigation Component-.



Github- .








All Articles