单Activity+多Fragment框架的几种实现方式

3,694 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第25天,点击查看活动详情

Fragment添加的方式与单Activity+多Fragment的实现方式

谷歌一直再喊,一个App只需要一个Activity就够了,Activity少了之后,清单文件解析更快,Fragment更加轻量了,看起来很不错。但是也是有很多坑,恢复重建问题,生命周期问题,返回问题等。

而随着组件化开发的流行,我们没有那么极端的只有一个Activity,我们可以把每一个组件创建一个Activity,UI也实现组件化了,除了一些公用的Activity和多进程的Activity,我们一个组件只需要一个Activity就够了。

其实这个开发方式早在2014年左右就已经有类似的方式了。我们看看此框架的演进

一、addToBackStack的方式

首先来看看Fragment的几种添加方式

一般的添加

    FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
    ft.add(R.id.fl_fragment, mHistoryFragment).commitAllowingStateLoss();

几个Fragment切换的添加方式

    FragmentTransaction beginTransaction = getSupportFragmentManager().beginTransaction();
    beginTransaction.add(R.id.fl_fragment,fragmentModuleOneShow);
    beginTransaction.add(R.id.fl_fragment,fragmentMmoduleTwoShow);
    beginTransaction.show(fragmentModuleOneShow).hide(fragmentMmoduleTwoShow).commit();

切换的时候:就是show一个hide一个 然后提交

 public void onClick(View view){
    FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
    if(view.getId() == R.id.tv_moduleone_fragment){
            fragmentTransaction.show(fragmentModuleOneShow).hide(fragmentMmoduleTwoShow).commit();
    }else if(view.getId() == R.id.tv_moduletwo_fragment){
            fragmentTransaction.show(fragmentMmoduleTwoShow).hide(fragmentModuleOneShow).commit();
    }
}

进出栈的添加方式: 调用addToBackState()方法,将这些变化加入到activity管理的back stack中去,这样用户调用返回键就可以回退这些变化了

 SignUpOptionsFragment signUpOptionsFragment = new SignUpOptionsFragment();
            getSupportFragmentManager()
            .beginTransaction().
             add(R.id.signup_fragment_container, signUpOptionsFragment)
            .addToBackStack(SignUpOptionsFragment.class.getSimpleName())
            .commit();

第二次替换

         SignUpInputFragment signUpInputFragment = new SignUpInputFragment();
         signUpInputFragment.setGoogleSignInResult(result);
                getSupportFragmentManager()
                    .beginTransaction()
                    .replace(R.id.signup_fragment_container, signUpInputFragment)
                    .addToBackStack(SignUpInputFragment.class.getSimpleName())
                    .commit();

带动画效果 xml资源 动画设置一定要在前面

            getFragmentManager().beginTransaction()
            //设置自定义动画      
            .setCustomAnimations(R.animator.animator_enter,R.animator. animator_exit,R.animator.animator_enter,R.animator.animator_exit)
            .addToBackStack("OtherFragment")  //设置fragment加入回退栈
            .replace(R.id.fragment, new OtherFragment())
            .commit();

比较蛮荒的用法了,缺点太多了,无法控制生命周期,无法恢复,无法设置启动模式,无法精准回退,还会出现无法返回的Bug。当时Fragment的Bug不少。后面出现了框架对Bug进行修复。

二、Fragmentation框架

它是一个对 fragment 进行管理的三方库,可以方便地对 fragment 进行 进栈、出栈、显示隐藏等操作,只需要调用简单的 api;对 fragment 的生命周期进行了拓展;方便的实现单 Activity + 多 Fragment 的 app 架构。帮你简化使用过程,轻松解决各种复杂嵌套等问题,修复了官方Fragment库存在的一些BUG。源码在此。(已停止维护)

它大致原理是把我们的 Activity 和 Fragment 包装一层。当我们在 Activity 或 Fragment 中直接调用它的 API 的时候,它通过对应的代理类 SupportActivityDelegate 和 SupportFragmentDelegate,把代码逻辑转换到代理类中;而这两个代理类中都持有 TransactionDelegate 的实例,对 fragment 的操作逻辑又都转移到 TransactionDelegate 中去完成

TransactionDelegate 为核心类 ,加载根Fragment、加载多个根Fragment、替换根Fragment、子Fragment之间的替换、隐藏显示Fragment、启动目标Fragment、得到位于栈顶Fragment、获取栈内的Fragment、Fragment出栈、出栈到目标fragment、获取栈顶的子Fragment、获取当前Fragment的前一个Fragment、子栈内Fragment出栈 等等操作

使用起来也是很简单,封装的很好

        Fragmentation.builder()  
                .stackViewMode(Fragmentation.BUBBLE)
                .debug(BuildConfig.DEBUG)
                .handleException(new ExceptionHandler() {
                    @Override
                    public void onException(@NonNull Exception e) {
                       e.printStackTrace();
                    }
                })
                .install();

//使用的时候我们需要指定 SupportActivity 和 SupportFragment 即可

public class YYJobsAuthActivity extends SupportActivity {

       if (findFragment(EmployeeLoginFragment.class) == null) {
            loadRootFragment(R.id.fl_content, EmployeeLoginFragment.newInstance());
        }

}

public class EmployeeLoginFragment extends SupportFragment{
//常用方法
// 启动目标Fragment,并关闭当前Fragment
startWithPop(SupportFragment fragment)
// 以某种启动模式,启动新的Fragment
start(SupportFragment fragment, int launchMode)
// 出栈targetFragment之上的所有Fragments
popTo(Class targetFragment, boolean includeTargetFragment);
// 隐藏软键盘 一般用在hide时
hideSoftInput();
// 显示软键盘,调用该方法后,会在onPause时自动隐藏软键盘
showSoftInput(View view);
// show一个Fragment,hide一个Fragment; 主要用于类似微信主页那种 切换tab的情况
showHideFragment(SupportFragment showFragment, SupportFragment hideFragment);

}

缺点:

  1. 已经没有维护了,不支持AndroidX 在 androidx.activity.1.1.0 版本是可以用的,但是升级到 androidx.activity.1.2.0 以上就会报错。
  2. 需要包装 Fragment 和 Activity 而普通的 Fragment 和 SupportActivity不能通用,不兼容。

三、AndroidX Navigation框架

Jectpack工具开发出了 Navigation,现在已经逐步成为主流的单 Activity 应用了,毕竟谷歌自家的 App 都是用这个实现单Activity+多Fragment。

//androidx
implementation "androidx.fragment:fragment-ktx:1.2.0"
implementation "androidx.navigation:navigation-fragment-ktx:2.3.0"
implementation "androidx.navigation:navigation-ui-ktx:2.3.0"

使用的时候需要 Activity 定义 xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".simple.SimpleActivity">
    <fragment
        android:id="@+id/frag_nav_simple"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:navGraph="@navigation/nav_simple"
        app:defaultNavHost="true" />
</android.support.constraint.ConstraintLayout>

navGraph 核心是这个导航图,需要我们自己定义XML 大致如下所示:

<?xml version="1.0" encoding="utf-8"?>
<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/page1Fragment">    //这里是默认显示的fragmentid

    <fragment
        android:id="@+id/page1Fragment"           //用于跳转的fragmentid
        android:name="com.guadou.testnavigation.Fragment1"    //全路径
        android:label="fragment_page1"                      //提示信息没什么大用可以在toolbar上展示
        tools:layout="@layout/fragment_page1">               //fragment的布局文件
        <action                                              //是fragment的事件
            android:id="@+id/action_page2"                   //fragment的事件id,用于View的点击事件跳转
            app:destination="@id/page2Fragment" />           //destination是目的地的意思,跳转到哪个fragmentid
    </fragment>


    <fragment
        android:id="@+id/page2Fragment"
        android:name="com.guadou.testnavigation.Fragment2"
        android:label="fragment_page2"
        tools:layout="@layout/fragment_page2">
        <action
            android:id="@+id/action_page1"
            app:popUpTo="@id/page1Fragment" />            //popuTo 返回到哪一个fragmentid
        <action
            android:id="@+id/action_page3"
            app:destination="@id/nav_graph_page3" />
    </fragment>

    <navigation
        android:id="@+id/nav_graph_page3"
        app:startDestination="@id/page3Fragment">
        <fragment
            android:id="@+id/page3Fragment"
            android:name="com.guadou.testnavigation.Fragment3"
            android:label="fragment_page3"
            tools:layout="@layout/fragment_page3" />
    </navigation>


</navigation>

在我们跳转的时候就需要拿到我们定义好的这些action来进行操作:

public class Fragment2 extends Fragment {

    private Button mBtn_jump_to3;
    private Button mBtn_jump_to1;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View inflate = inflater.inflate(R.layout.fragment_page2, container, false);
        mBtn_jump_to1 = inflate.findViewById(R.id.btn_jump_to1);
        mBtn_jump_to3 = inflate.findViewById(R.id.btn_jump_to3);
        return inflate;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        mBtn_jump_to1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //返回,返回键也是这样的功能
                Navigation.findNavController(view).navigateUp();
            }
        });
        mBtn_jump_to3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //跳转,可以写道Bulde数据跳转
                Navigation.findNavController(view).navigate(R.id.action_page3);
            }
        });
    }
}

当然我们还能给这个action指定动画。

<?xml version="1.0" encoding="utf-8"?>
<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/page1Fragment">

    <fragment
        android:id="@+id/page1Fragment"
        android:name="com.guadou.testnavigation.Fragment1"
        android:label="fragment_page1"
        tools:layout="@layout/fragment_page1">
        <action
            android:id="@+id/action_page2"
            app:destination="@id/page2Fragment"
            app:enterAnim="@anim/open_enter"
            app:exitAnim="@anim/pre_close_exit"
            app:popEnterAnim="@anim/pre_open_enter"   
            app:popExitAnim="@anim/close_exit"/>
    </fragment>


    <fragment
        android:id="@+id/page2Fragment"
        android:name="com.guadou.testnavigation.Fragment2"
        android:label="fragment_page2"
        tools:layout="@layout/fragment_page2">

        <action
            android:id="@+id/action_page3"
            app:destination="@id/page3Fragment"
            app:enterAnim="@anim/open_enter"
            app:exitAnim="@anim/pre_close_exit"
            app:popEnterAnim="@anim/pre_open_enter"
            app:popExitAnim="@anim/close_exit"/>
    </fragment>


    <fragment
        android:id="@+id/page3Fragment"
        android:name="com.guadou.testnavigation.Fragment3"
        android:label="fragment_page3"
        tools:layout="@layout/fragment_page3" >

    </fragment>


</navigation>

我自己都不想再写了xml,太复杂了,如果一个项目中有100个页面,相信我你会崩溃的。

Demo的示例如下,能实现跳转,但是Fragment实例没有被保存,输入框里面的东西都没了。

好了,总结下Navigation的优缺点

优点

  1. Activity和Fragment完全兼容,无需包装类。
  2. 谷歌出品支持最新AndroidX。
  3. 基于导航图可以实现随意的跳转。

缺点

  1. 默认replace实现,无法恢复上一个页面状态。
  2. xml定义的导航图太过复杂,大型项目简直痛苦。
  3. 生命周期回调有问题,因为replace的原因,无法真正回调 onStart onResume 等生命周期。
  4. 返回事件无法拦截需要自定义

篇幅原因,后面一期会出Navigation的封装版本,解决 Navigation 上的一些缺点。让 Navigation 在实战中真正好用起来。

更新:Navigation的封装