” An app only needs an Activity, you can use Fragments, just don’t use the backstack with fragments ”

— Jake Wharton @Droidcon NYC 2017

In recent years, SPA, namely single Activity architecture, has gradually become popular, and many excellent tripartite libraries have emerged, most of which are implemented based on fragments. The most representative one is Fragmentation. Later, the birth of Jetpack Navigation also marked Google’s affirmation of SPA architecture from an official standpoint.

The advent of Navigation does not accelerate the Fragment to replace the Activity completely, one important reason is that it relies too much on configuration (NavGraph), and loses the flexibility of Activity. Fragmentation is well done and has a similar experience to Activity. Unfortunately, it does not support Kotlin and its maintenance has been stopped long ago, so it cannot use various new features introduced in AndroidX in recent years.

Is there a tool that is as flexible as Fragmentation and as compatible with the new features in AndroidX as Navigation? Fragivity was born in this context: github.com/vitaviva/fr…

Fragivity : Use Fragment like Activity


As the name suggests, Fragivity wants to make the Fragment feel like an Activity, so that it can truly replace it in all sorts of scenarios:

  • The lifecycle is consistent with the Activity behavior
  • Support for multiple launchmodes
  • Support OnBackPressed event handling, support for SwipeBack
  • Supports Transition and SharedElement
  • Supports Dialog style display
  • Support Deep Links

Fragivity is implemented based on Navigation and has Fragmentation Fragmentation flexibility. It can realize screen jump without NavGraph configuration. Here’s a quick comparison of the differences:

Fragmentation Navigation Fragivity
Free to jump yes No (depends on NavGraph) yes
Launch Mode Three kinds of Two kinds of Three kinds of
Support Deep Links no Yes (depending on NavGraph) Yes (with annotations)
Kotlin friendly no yes yes
The life cycle Inconsistent with Activity (add mode) Inconsistent with Activity (replace mode) In line with the Activity
Communication between the fragments startFragmentForResult viewmodel Viewmodel, callback, ResultAPI, etc
Cinematic sequences View Animation Transition Animation Transition Animation
Swipe Back Yes (Dependent base class) no Yes (no base class required)
Support Dialog display no yes yes
OnBackPressed intercept Yes (Dependent base class) Yes (no base class required) Yes (no base class required)

By comparison, it can be found that Fragivity is more consistent with Activity behavior in multiple dimensions than the previous two.


1. Basic use


Fragivity is cheap to access.

1.1 gradle rely on

implementation 'com.github.fragivity:core:$latest_version'
Copy the code

1.2 the statement NavHostFragment

Like Navigation, Fragivity requires NavHostFragment as Parent, and then page jumps between ChildFragments.

We declare NavHostFragment in XML

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:fitsSystemWindows="true">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true" />
</FrameLayout>
Copy the code

1.3 Loading the Home Page

Normally we need to define a MainActivity as the entry point. Again, here we load an initial Fragment via loadRoot:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host) as NavHostFragment

        navHostFragment.loadRoot(HomeFragment::class)}}Copy the code

1.4 Page Hopping

Now you can jump between fragments

// Jump to the target Fragment
navigator.push(DestinationFragment::class)

// Jump with parameters
val bundle = bundleOf(KEY_ARGUMENT to "some args")
navigator.push(DestinationFragment::class.bundle)

// You can set parameters via the trailing lambda, with the advantage that you can do other configurations as well
navigator.push(DestinationFragment::class) {
	arguments = bundle // Set parameters
    launchMade = ... // More Settings
    
}
Copy the code

1.5 Page Return

You can return to the previous page using the POP method

// return to the previous page
navigator.pop()

// Return to the specified page
navigator.popTo(HomeFramgent::class)
Copy the code

1.6 Transition animation

Based on Navigation capability, the Transition animation can be set when the screen jumps

navigator.push(UserProfile::class.bundle) { //this:NavOptions
    // Configure the animation
    enterAnim = R.anim.enter_anim
    exitAnim = R.anim.exit_anim
    popEnterAnim = R.anim.enter_anim
    popExitAnim = R.anim.exit_anim
}
Copy the code

With FragmentNavigatorExtras, you can also set SharedElement for more elegant animations

// Set SharedElement to imageView when jumping
navigator.push(UserProfile::class.bundle) { //this:NavOptions
                
    enterAnim = R.anim.enter_anim
    exitAnim = R.anim.exit_anim
    popEnterAnim = R.anim.enter_anim
    popExitAnim = R.anim.exit_anim
    
    // Configure shared elements
    sharedElements = sharedElementsOf(imageView to "iv_id")}Copy the code
class UserProfile : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        // Set the shared element animation in the target Fragment
        sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
    }

}
Copy the code




2. No configuration is required to redirect the page


Navigation requires NavGraph configuration to jump between pages, for example:

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

    <fragment
        android:id="@+id/fragment_first"
        android:name=".FirstFagment"
        android:label="@string/tag_first">
        <action
            android:id="@+id/action_to_second"
            app:destination="@id/fragment_second"/>
    </fragment>
    <fragment
        android:id="@+id/fragment_second"
        android:name=".SecondFragment"
        android:label="@string/tag_second"/>
</navigation>
Copy the code

Each
corresponds to a NavGraph object,
corresponds to each Destination in the NavGraph, and the NavController holds the NavGraph by controlling the jump between the destinations.

Page hops that depend on configuration cannot be as flexible as activities. Fragivity dynamically builds NavGraph to jump without configuration:

2.1 Dynamically creating Graph

When the home page is loaded, the Graph is dynamically created

fun NavHostFragment.loadRoot(root: KClass<out Fragment>) {

    navController.apply {
        / / add the Navigator
        navigatorProvider.addNavigator(
            FragivityNavigator(
                context,
                childFragmentManager,
                id
            )
        )
        
        / / create a Graph
        graph = createGraph(startDestination = startDestId) {
            val startDestId = root.hashCode()
            / / add startDestination
            destination(
                FragmentNavigatorDestinationBuilder(
                    provider[FragivityNavigator::class].startDestId.root))}}}Copy the code

The FragivityNavigator handles the logic for page jumps, which I’ll cover separately later. After Graph is created, startDestination is added to load the front page.

2.2 Dynamically Adding a Destination

In addition to startDestination, you need to dynamically add this Destination to Graph whenever you jump to a new page:

fun NavHost.push(
    clazz: KClass<out Fragment>,
    args: Bundle? = null,
    extras: Navigator.Extras? = null,
    optionsBuilder: NavOptions. () - >Unit= {}) = with(navController) {
    // Create Destination dynamically
    val node = putFragment(clazz)
    // Call the Navigate method of NavController to jump
    navigate(
        node.id, args,
        convertNavOptions(clazz, NavOptions().apply(optionsBuilder)),
        extras
    )
}

// Create and add Destination
private fun NavController.putFragment(clazz: KClass<out Fragment>): FragmentNavigator.Destination {
    val destId = clazz.hashCode()
    lateinit var destination: FragmentNavigator.Destination
    if (graph.findNode(destId) == null) {
        destination = (FragmentNavigatorDestinationBuilder(
            navigatorProvider[FragivityNavigator::class].destId.clazz
        )).build()
        graph.plusAssign(destination)// Add to Graph
    } else {
        destination = graph.findNode(destId) as FragmentNavigator.Destination
    }
    return destination
}
Copy the code

After a Destination is created, navigate to it through the Navigate method of the NavController.


3. BackStack and its life cycle


As God J said, one of the reasons fragments are not a good replacement for activities is the difference in the management of BackStack, which can affect different life cycles.

Imagine the following scenario: PAGE A >(Startup)> Page B >(Back)> Page A

We know that there are two ways to add a Fragment: Add and replace. Either way, the life cycle of an Activity is different when the screen jumps:

The startup mode of page B The lifetime of the return from B
Activity ActivityB: onPasue -> onStop -> onDestroy

ActivityA: onStart -> onResume
Fragments (add) FragmentB : onPause -> onStop -> onDestroy

FragmentA : no change
Fragments (replace) FragmentB: onPause -> onStop -> onDestroy

FragmentA: onCreateView -> onStart -> onResume

If you want the Fragment’s life cycle to be consistent with the Activity’s behavior when the screen jumps, you need to achieve at least three goals:

  • Target 1: FragmentB does not re-onCreateView during rollback (Add meets)
  • Objective 2: On a fallback, FragmentB triggers onStart -> onResume (replace satisfies)
  • Objective 3: The background Fragment does not change with the parent lifecycle (replace satisfies)

Neither Navigation nor Fragmentation can satisfy the above three criteria simultaneously.

3.1 rewrite FragmentNavigator

NavController realize specific jump through FragmentNavigator logic, is the Navigator FragmentNavigator derived class, responsible for FragmentNavigator. Destination type of jump.

Navigate () implements the specific logic for the Fragment jump, and its core code is as follows

@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> {

    @Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {

        String className = destination.getClassName();
        
        // instantiate Fragment
        final Fragment frag = instantiateFragment(mContext, mFragmentManager,
        className, args);
        frag.setArguments(args);
        
        final FragmentTransaction ft = mFragmentManager.beginTransaction();
        ft.replace(mContainerId, frag); // Add Fragment in replace mode
        ft.setPrimaryNavigationFragment(frag);
  
        // transaction pressing
        ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
            
        ft.setReorderingAllowed(true); ft.commit(); }}Copy the code

The FragmentNavigator creates a Fragment jump from replace, which will re-onCreateView when it falls back, not as expected. We subclass FragivityNavigator, override navigate() method, change replace to add, and avoid re-onCreateView to achieve goal 1.

public class FragivityNavigator extends FragmentNavigator {
    @Override
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {

        final Fragment frag = instantiateFragment(mContext, mFragmentManager,
        className, args);
        //ft.replace(mContainerId, frag); // replace to addft.add(mContainerId, frag, generateBackStackName(mBackStack.size(), destination.getId())); }}Copy the code

3.2 add OnBackStackChangedListener

At the right time to add OnBackStackChangedListener FragmentManger, when listening to the backstack change manually trigger the lifecycle callback, achieve the target for 2″

private final FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener =
        new FragmentManager.OnBackStackChangedListener() {

            @Override
            public void onBackStackChanged(a) {
                if(mIsPendingAddToBackStackOperation) { mIsPendingAddToBackStackOperation = ! isBackStackEqual();if (mFragmentManager.getFragments().size() > 1) {
                        // The life cycle when cutting to the background
                        Fragment fragment = mFragmentManager.getFragments().get(mFragmentManager.getFragments().size() - 2);
                        if (fragment instanceof ReportFragment) {
                            fragment.performPause();
                            fragment.performStop();
                            ((ReportFragment) fragment).setShow(false); }}}else if(mIsPendingPopBackStackOperation) { mIsPendingPopBackStackOperation = ! isBackStackEqual();// The life cycle when you return to the foreground
                    Fragment fragment = mFragmentManager.getPrimaryNavigationFragment();
                    if (fragment instanceof ReportFragment) {
                        ((ReportFragment) fragment).setShow(true); fragment.performStart(); fragment.performResume(); }}}};Copy the code

3.3 ReportFragment agent

To achieve goal 3, create for the Fragment when it is instantiatedReportFragmentActing as an agent. The so-called agency is actually throughParentFragmentInternal distribution and control of the lifecycle:

//ReportFragment
internal class ReportFragment : Fragment() {

    internal lateinit var className: String
    private val _real: Class<out Fragment> by lazy {
        Class.forName(className) as Class<out Fragment>
    }
    private val _realFragment by lazy {  _real.newInstance() }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        // Manage target Framgent as a child
        mChildFragmentManager.beginTransaction().apply {
            _realFragment.arguments = arguments
            add(R.id.container, _realFragment)
            commitNow()
        }
    }

}
Copy the code
//ReportFragmentManager
internal class ReportFragmentManager : FragmentManager() {
    //isShow: in the background, does not respond to lifecycle distribution
    internal var isShow = true
    public override fun dispatchResume(a) {
        if (isShow) super.dispatchResume()
    }

    / /...
}
Copy the code


4. Support Launch Modes


Fragivity supports three Launch Modes: Standard, SingleTop and SingleTask.

The startup method is very simple:

navigator.push(LaunchModeFragment::class.bundle) { //this: NavOptions
    launchMode = LaunchMode.STANDARD // Can be omitted by default
    //or LaunchMode.SINGLE_TOP, LaunchMode.SINGLE_TASK
}
Copy the code

Here is the implementation of SingleTop. Navigation also supports SingleTop, but it is done in Navigator. Since we have rewritten Navigator (replace to add), the implementation of SingleTop needs to be adjusted accordingly:

@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
    
    final Fragment preFrag = mFragmentManager.getPrimaryNavigationFragment();
    
    // Start with singleTop
    if (isSingleTopReplacement) {
            if (mBackStack.size() > 1) {
                ft.remove(preFrag);// Delete the old instance
                
                // Update instance information in FragmentTransaction
                frag.mTag = generateBackStackName(mBackStack.size() - 1, destination.getId());
                if (mFragmentManager.mBackStack.size() > 0) {
                    List<FragmentTransaction.Op> ops =
                            mFragmentManager.mBackStack.get(mFragmentManager.mBackStack.size() - 1).mOps;
                    for (FragmentTransaction.Op op : ops) {
                        if (op.mCmd == OP_ADD && op.mFragment == preFrag) {
                            op.mFragment = frag;
                        }
                    }
                }
            }
        } 
}
Copy the code

SingleTop requires that only one instance can exist when the top type and the target type are the same, so old instances need to be removed to avoid repeated addition. In addition, to ensure the normal transaction behavior when BackStack is rolled back, you need to update the information about the transaction with the old instance to the new instance.


5. Communication between fragments


Fragivity supports all forms of Androidx. fragment communication, such as using ViewModel or using ResultApi (Fragment version higher than 1.3.0-beta02). In addition, Fragivity provides a simpler way to communicate based on Callback:

//SourceFragment
val cb = { it : Boolean -> 
    / /...
}
navigator.push {
    DestinationFragment(cb)
}

//Destination
class DestinationFragment(val cb:(Boolean) - >Unit) {... }Copy the code

Previously, if the Fragment had to use a constructor with no arguments, it would fail to package otherwise. Thanks to the advances made by AndroidX, this restriction has now been removed, allowing custom constructors with arguments. So we can create the Fragment dynamically with lambda and pass the callback as a construction parameter.

inline fun <reified T : Fragment> NavHost.push(
    noinline optionsBuilder: NavOptions. () - >Unit = {},
    noinline block: () -> T
) {
    / /...
    push​(T::class.optionsBuilder)
}
Copy the code

As mentioned above, the Fragment’s Class is still used internally as a parameter to jump, but kotlin’s Reified feature is used to retrieve the Class information of the generic type.


6. Support Deep Links


Activities can be started implicitly by URI, and Deep Links support for fragments is needed to cover such usage scenarios. Navigation configures URI information for Destination in NavGraph. Fragivity does not have NavGraph, but you can configure urIs with annotations. The basic idea is similar to ARouter’s routing principle:

  1. Annotations are parsed through Kapt at compile time to get URI information and associate with the Fragment
  2. Intercepting the Intent at the entrance to the Activity, parsing the URI and jumping to the associated Fragment

6.1 Adding a Kapt Dependency

kapt 'com.github.fragivity:processor:$latest_version'
Copy the code

6.2 configure the URI

When defining a Fragment, use @deeplink to configure the URI

const val URI = "myapp://fragitiy.github.com/"

@DeepLink(uri = URI)
class DeepLinkFragment : AbsBaseFragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup? , savedInstanceState:Bundle?).: View? {
        return inflater.inflate(R.layout.fragment_deep_link, container, false)}}Copy the code

6.3 with Intent

At the entrance to the MainActivity, the URI in the Intent is handled

//MainActivity#onCreate
override fun onCreate(savedInstanceState: Bundle?). {
     super.onCreate(savedInstanceState)
     setContentView(R.layout.activity_main)

     val navHostFragment = supportFragmentManager
            .findFragmentById(R.id.nav_host) as NavHostFragment

     navHostFragment.handleDeepLink(intent)

}
Copy the code

HandleDeepLink will eventually call the NavController method to resolve the URI:

//NavController
public void navigate(@NonNull Uri deepLink) {
    navigate(new NavDeepLinkRequest(deepLink, null.null));
}
Copy the code

After that, we can jump to the target Fragment from outside the APP using a URI:

val intent = Intent(Intent.ACTION_VIEW, Uri.parse("myapp://fragitiy.github.com/"))
startActivity(intent)
Copy the code


7. OnBackPressed Event interception


Fragment does not have an Activity OnBackPressed method. Fragmentation inherits the onBackPressedSupport method. However, this method introduces a new base class and is highly intrusive to business code.

Fragivity blocks back key events in a more invulnerable manner based on androidx.activity’s OnBackPressedDispatcher. OnBackPressedDispatcher ensures that back events are consumed in order through a chain of responsibility mode, while at the same time Lifecycle is sensed and automatically cancelled at the appropriate time to avoid leaks. Reference: developer.android.com/guide/navig…

override fun onCreate(savedInstanceState: Bundle?). {
    super.onCreate(savedInstanceState)
    requireActivity().onBackPressedDispatcher.addCallback( this.object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed(a) {
                // Intercept the back key event}})}Copy the code

The back key returns with pop() returns

Fragivity provides the pop method, which is returned by code that eventually calls Navigator#popBackStack inside. To ensure the consistency of the rollback logic, we want the back key’s rollback to be handled by popBackStack as well. Navigation is implemented via NavHostFragment:

//NavHostFragment#onCreate
public void onCreate(@Nullable Bundle savedInstanceState) {
        /​/...
     mNavController = new NavHostController(context);
        mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
        / /...
        
}
Copy the code
//NavController#setOnBackPressedDispatcher
void setOnBackPressedDispatcher(@NonNull OnBackPressedDispatcher dispatcher) {
    if (mLifecycleOwner == null) {
        throw new IllegalStateException("You must call setLifecycleOwner() before calling "
                + "setOnBackPressedDispatcher()");
    }
    // Remove the callback from any previous dispatcher
    mOnBackPressedCallback.remove();
    // Then add it to the new dispatcher
    dispatcher.addCallback(mLifecycleOwner, mOnBackPressedCallback);
}
Copy the code
//NavController#mOnBackPressedCallback
private final OnBackPressedCallback mOnBackPressedCallback =
        new OnBackPressedCallback(false) {
    @Override
    public void handleOnBackPressed(a) {
        popBackStack(); // Finally callback Navigator#popBackStack}};Copy the code


8. SwipeBack


Navigation does not provide the ability to slide back, we found a solution from Fragmentation: onCreateView with SwipeLayout as Container

It’s very simple to use:

class SwipeBackFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup? , savedInstanceState:Bundle?).: View? {
        return inflater.inflate(R.layout.fragment_swipe_back, container, false)}override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
        super.onViewCreated(view, savedInstanceState)
        swipeBackLayout.setEnableGesture(true) // Start SwipeBack in one sentence}}Copy the code

The introduction of additional base classes is avoided with the ReportFragment agent. SwipeBackLayout is an extended property that actually obtains an instance of parentFragment (ReportFragment)

val Fragment.swipeBackLayout
    get() = (parentFragment as ReportFragment).swipeBackLayout
Copy the code

Handling in ReportFragment is very simple, using SwipeLayout as a Container

internal class ReportFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup? , savedInstanceState:Bundle?).: View? {
        swipeBackLayout =
            SwipeBackLayout(requireContext()).apply {
                attachToFragment(
                    this@ReportFragment,
                    inflater.inflate(R.layout.report_layout, container, false)
                        .apply { appendBackground() } // add a default background color to make it opaque

                )
                setEnableGesture(false) //default false
            }
        return swipeBackLayout
    }
Copy the code

To avoid background penetration during sliding, call applyBackgroud() to add the same default background color to the Fragment as the current theme

private fun View.appendBackground(a) {
    val a: TypedArray =
        requireActivity().theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
    val background = a.getResourceId(0.0)
    a.recycle()
    setBackgroundResource(background)
}
Copy the code




9. Show Dialog


The Activity can be launched as a Dialog style by setting a Theme. You can also use DialogFragment to create a Dialog-style Fragment. Navigation already supports DialogFragment, and Fragivity calls the relevant method:

9.1 define DialogFragment

class DialogFragment : DialogFragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup? , savedInstanceState:Bundle?).: View? {
        return inflater.inflate(R.layout.fragment_dialog, container, false)}}Copy the code

9.2 according to the Dialog

navigator.showDialog(DialogFragment::class)
Copy the code

DialogFramgent also needs to dynamically add a Destination to the Graph, which is different from the normal Fragment. DialogFragmentNavigator is of the same type:

/ / create a Destination
val destination = DialogFragmentNavigatorDestinationBuilder(
       navigatorProvider[DialogFragmentNavigator::class].destId.clazz ).apply {
            label = clazz.qualifiedName
       }.build()

// Add to Graph
graph.plusAssign(destination)
Copy the code


The last


Fragivity’s core logic is to maximize the ability to reuse Navigation and keep it in sync with the latest releases, which helps keep the framework advanced and stable. Fragivity also aims to create an Activity-like experience to help developers move to a single-activity architecture at a lower cost.

Project source code in this article introduces a variety of API demo, welcome to download experience, mention the issue, feel good don’t forget to start github.com/vitaviva/fr…