Activity 和 Fragment 的封装

2,659 阅读7分钟

通过对 Activity 和 Fragment 的封装, 更加理解其生命周期, 一个 Activity 和 Fragment 的通用基本操作进行封装, 方便对其使用. 同时封装了 ButterKnife 注解框架, 方便我们的使用.

封装来自于 Mooc 课程学习 慕课网

BaseFragment

package com.dcr.italker.common.app;

import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

import butterknife.ButterKnife;
import butterknife.Unbinder;

/**
 * @author Dcr
 */
public abstract class BaseFragment extends Fragment {

    protected View mRoot;
    protected Unbinder mRootUnBinder;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        // 初始化参数
        initArgs(getArguments());
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        int layoutId = getContentLayoutId();
        if (mRoot == null) {
            // 初始化当前的根布局, 但是不在创建时就添加到 container 里面
            mRoot = inflater.inflate(layoutId, container, false);
            // 如果第一次初始化, 创建 mRroot 并且初始化控件
            initWidget(mRoot);
        } else {
            if (mRoot.getParent() != null) {
                // 把当前 Root 从其父控件中移除
                ((ViewGroup) mRoot.getParent()).removeView(mRoot);
            }
        }
        return mRoot;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // 当 View 初始化完成后, 初始化数据
        initData();
    }


    /**
     * 得到当前界面的资源文件 id
     *
     * @return 资源文件 id
     */
    protected abstract int getContentLayoutId();

    /**
     * 初始化控件
     */
    protected void initWidget(View root) {
        // 完成 ButterKnife 的绑定
        mRootUnBinder = ButterKnife.bind(this, root);
    }

    /**
     * 初始化数据
     */
    protected void initData() {

    }

    /**
     * 初始化参数
     *
     * @param args 参数
     */
    protected void initArgs(Bundle args) {
    }

    /**
     * 返回按键触发时调用
     *
     * @return true 代表已处理返回逻辑, Activity 就不用关心了, 否则需要上层 Activity 处理; 默认情况下不拦截.
     */
    public boolean onBackPressed() {
        return false;
    }
}

BaseFragment 的基本封装如上所示, 下面进行每一部分的分析.

Fragment 生命周期

首先我们需要分析一下 Fragment 的生命周期, 详细的 Fragment 生命周期的讲解我会在另一篇文章中分析, 这里提供一篇 Fragment 生命周期的简略介绍, 非常简明但是使用, 可以作为手册查看 (当然, Fragment 的生命周期还是要熟记的)

其中我们这里封装用到的生命周期包括以下几个:

  • onAttach()
  • onCreateView()
  • onActivityCreated()

我们会在 onAttach() 中初始化参数, 在 onCreateView() 中 inflate View 并且初始化控件, 在 onViewCreated() 中初始化数据.

初始化参数

onAttach() 是 Fragment 经历的第一个生命周期, 我们在 onAttach() 中封装一个接收参数的操作.

@Override
public void onAttach(Context context) {
    super.onAttach(context);
    // 初始化参数
    initArgs(getArguments());
}

/**
 * 初始化参数
 *
 * @param args 参数
 */
protected void initArgs(Bundle args) {
}

我们通过 Fragment 所提供的 getArguments() 方法来获取参数, 所获取的参数是 Bundle 类型, 同时我们提供接口 initArgs() 来供子类来完成对参数的处理.

初始化控件

在 Fragment 的 onCreateView() 方法中, 我们给定 layout 资源文件, 来生成对应的实际 View 对象, 我们对其的封装如下所示.

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    int layoutId = getContentLayoutId();
    if (mRoot == null) {
        // 初始化当前的根布局, 但是不在创建时就添加到 container 里面
        mRoot = inflater.inflate(layoutId, container, false);
        // 如果第一次初始化, 创建 mRroot 并且初始化控件
        initWidget(mRoot);
    } else {
        if (mRoot.getParent() != null) {
            // 把当前 Root 从其父控件中移除
            ((ViewGroup) mRoot.getParent()).removeView(mRoot);
        }
    }
    return mRoot;
}

getContentLayoutId()

我们在创建 View 时, 需要布局资源文件, 因此我们提供一个 getContentLayoutId() 的接口, 用于子类提供资源文件 Id.

这个方法子类必须实现, 因此声明为了抽象方法:

/**
 * 得到当前界面的资源文件 id
 *
 * @return 资源文件 id
 */
protected abstract int getContentLayoutId();

onCreateView() 流程分析

onCreateView() 中, 我们首先通过 getContentLayoutId() 获取资源文件, 然后需要判断一下是否有 mRoot 缓存, 如果没有, 则通过 inflater 创建 view; 如果有, 则将当前的 root 从其父控件中移除 (这里其实不太懂, 还需要学习一个).

如果需要新建 view, 那么调用 initWidget() 方法完成初始化.

/**
 * 初始化控件
 */
protected void initWidget(View root) {
    // 完成 ButterKnife 的绑定
    mRootUnBinder = ButterKnife.bind(this, root);
}

initWidget() 中, 完成了 ButterKnife 的绑定.

数据初始化

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    // 当 View 初始化完成后, 初始化数据
    initData();
}

/**
 * 初始化数据
 */
protected void initData() {

}

onViewCreated() 方法中, 调用 initData() 接口来进行数据初始化.

对返回按键的处理

/**
 * 返回按键触发时调用
 *
 * @return true 代表已处理返回逻辑, Activity 就不用关心了, 否则需要上层 Activity 处理; 默认情况下不拦截.
 */
public boolean onBackPressed() {
    return false;
}

BaseFragment 中, 添加了对返回按键的处理, Fragment 本身没有对返回按键的处理, 这里需要从 Activity 传入这个事件, 进而在 Fragment 中完成处理, 可以参见 BaseActivity 的相关部分.

BaseActivity

对 Activity 的封装也很简单, 也是按照生命周期进行封装. 完整的封装结果如下:

package com.dcr.italker.common.app;

import android.os.Bundle;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;

import java.util.List;

import butterknife.ButterKnife;
import butterknife.Unbinder;

/**
 * @author Dcr
 */
public abstract class BaseActivity extends AppCompatActivity {

    protected Unbinder mRootUnBinder;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 在界面未初始化之前调用的初始化操作
        initWindows();
        if (initArgs(getIntent().getExtras())) {
            // 得到界面Id并设置到Activity界面中
            setContentView(getContentLayoutId());
            initWidget();
            initData();
        } else {
            // 初始化参数失败
            finish();
        }
    }

    /**
     * 初始化窗口, 在界面未初始化之前调用的初始化操作
     */
    protected void initWindows() {

    }

    /**
     * 初始化参数
     *
     * @param args 参数
     * @return 如果参数正确, 返回 True, 错误返回 False.
     */
    protected boolean initArgs(Bundle args) {
        return true;
    }

    /**
     * 得到当前界面的资源文件 id
     *
     * @return 资源文件 id
     */
    protected abstract int getContentLayoutId();

    /**
     * 初始化控件
     */
    protected void initWidget() {
        // 完成 ButterKnife 的绑定
        mRootUnBinder = ButterKnife.bind(this);
    }

    /**
     * 初始化数据
     */
    protected void initData() {

    }

    @Override
    public boolean onSupportNavigateUp() {
        // 当点击界面导航返回时, finish 当前界面
        finish();
        return super.onSupportNavigateUp();
    }

    @Override
    public void onBackPressed() {
        // 获取所有的 Fragments
        List<Fragment> fragments = getSupportFragmentManager().getFragments();
        // 判断 fragments 是否为空
        if (fragments.size() > 0) {
            // 遍历每个 Fragment
            for (Fragment fragment :
                    fragments) {
                // 是否是我们的 BaseFragment
                if (fragment instanceof BaseFragment) {
                    if (((BaseFragment) fragment).onBackPressed()) {
                        // 如果 Fragment 拦截了 back 按键, 直接返回
                        return;
                    }
                }
            }
        }
        super.onBackPressed();
        finish();
    }
}

onCreate()

initWidget()

initWidget() 是在界面初始化之前进行的操作.

/**
 * 初始化窗口, 在界面未初始化之前调用的初始化操作
 */
protected void initWindows() {

}

获取参数

传入 Activity 的参数, 我们需要进行封装处理, 传入参数可能会报错, 我们需要进行判断.

if (initArgs(getIntent().getExtras())) {
    // 获取参数成功
    // 得到界面Id并设置到Activity界面中
    setContentView(getContentLayoutId());
    initWidget();
    initData();
} else {
    // 初始化参数失败
    finish();
}

在传入参数解析完成后, 我们进行界面的初始化.

setContentView()

Activity 需要调用 setContentView() 来设置布局资源, 因此我们提供一个接口 getConentLayoutId() 来获取资源文件, 并设置到 Activity 中.

/**
 * 得到当前界面的资源文件 id
 *
 * @return 资源文件 id
 */
protected abstract int getContentLayoutId();

getContentLayoutId() 在子类种必须实现. 在 onCreate() 生命周期中, 我们会调用并且进行设置.

界面初始化和数据初始化

我们提供两个接口给子类使用, 分别进行界面的初始化和数据的初始化.

/**
 * 初始化控件
 */
protected void initWidget() {
    // 完成 ButterKnife 的绑定
    mRootUnBinder = ButterKnife.bind(this);
}

/**
 * 初始化数据
 */
protected void initData() {

}

返回按钮的处理

Activity 需要处理两个返回事件, 一个是点击了界面导航的返回按钮, 一个是点击了实体的返回按钮.

@Override
public boolean onSupportNavigateUp() {
    // 当点击界面导航返回时, finish 当前界面
    finish();
    return super.onSupportNavigateUp();
}

对界面导航的返回按钮, 我们只需要结束当前 Activity, 然后返回上一层即可.

对于实体的按钮, 因为 Activity 可能嵌套有 Fragment, 因此, 我们需要判断子 Fragment 中是否对返回事件进行了拦截, 如果进行了拦截, 我们就交由 Fragment 来处理, Activity 不做进一步处理, 否则也结束当前页面, 返回上一层.

@Override
public void onBackPressed() {
    // 获取所有的 Fragments
    List<Fragment> fragments = getSupportFragmentManager().getFragments();
    // 判断 fragments 是否为空
    if (fragments.size() > 0) {
        // 遍历每个 Fragment
        for (Fragment fragment :
                fragments) {
            // 是否是我们的 BaseFragment
            if (fragment instanceof BaseFragment) {
                if (((BaseFragment) fragment).onBackPressed()) {
                    // 如果 Fragment 拦截了 back 按键, 直接返回
                    return;
                }
            }
        }
    }
    super.onBackPressed();
    finish();
}

这里, 我们使用了 getSupportFragmentManager().getFragments() 接口的方式获取 Activity 的所有 Fragment, 然后对 Fragment 进行遍历, 判断是否对返回事件进行了拦截.

以上便是对 Activity 和 Fragment 的简单封装, 随着学习的进步, 还会对其封装进行修改完善, 会及时更新本文.