Android-封装基类Activity\Fragment,从0到1记录

封装基类Activity\Fragment,从0到1记录

思考1:基础能力

  • 生命周期与钩子方法
  • ViewBinding
  • 沉浸式
  • 权限(本文忽略,权限封装可查看我另一篇文章点击此处,值得一看~)
    思考2:UI能力

我们所看的UI,除去导航栏之类的,单说UI界面,暂时分为三种情况:

  • 1.加载中
  • 2.显示数据
  • 3.缺省页(空数据,异常错误等等)

并且基本上每个UI都需要,那是不是可以把它在抽离一下,这样就不用每个界面都写一遍,这样界面层次就更清楚。

直接上菜:

根据思考1,构建顶层核心基类BaseActivity相关

ViewBinding

ini 复制代码
android {
    ...
    buildFeatures {
        ...
        viewBinding = true
    }
}
复制代码

BaseActivity

kotlin 复制代码
/**
 * 通用 Activity 基类
 * 涵盖:
 * 1. ViewBinding 自动解析 (反射方案)
 * 2. 沉浸式状态栏控制 (Immersive Mode)
 * 3. 通用生命周期与钩子方法 (Hook methods)
 */
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {

    private var _binding: VB? = null
    protected val mViewBinding: VB
        get() = _binding ?: throw IllegalStateException("Binding is only valid after onCreateView")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 1. 初始化进入前的钩子方法
        onPreCreate()
        
        // 2. 初始化 ViewBinding
        _binding = inflateBinding()
        setContentView(mViewBinding.root)
        
        // 3. 沉浸式与系统栏配置
        setupImmersiveMode()
        
        // 4. 初始化数据与视图的钩子方法
        initView(savedInstanceState)
        initData()
        initListener()
    }

    /**
     * 通过反射获取 ViewBinding 实例
     */
    @Suppress("UNCHECKED_CAST")
    private fun inflateBinding(): VB {
        var type = javaClass.genericSuperclass
        // 处理如果是多层继承的情况
        while (type !is ParameterizedType) {
            type = (type as Class<*>).genericSuperclass
        }
        val clazz = type.actualTypeArguments[0] as Class<VB>
        val method = clazz.getMethod("inflate", LayoutInflater::class.java)
        return method.invoke(null, layoutInflater) as VB
    }

    /**
     * 初始化视图 (Hook)
     */
    abstract fun initView(savedInstanceState: Bundle?)

    /**
     * 初始化数据 (Hook)
     */
    open fun initData() {}

    /**
     * 初始化监听器 (Hook)
     */
    open fun initListener() {}

    /**
     * 在 onCreate 的 setContentView 之前执行的钩子方法
     * 可用于转场动画、主题切换等
     */
    open fun onPreCreate() {}

    /**
     * 是否开启沉浸式(内容延伸到状态栏和导航栏下方),默认 true
     */
    open fun isImmersiveModeEnabled(): Boolean = true

    /**
     * 状态栏背景颜色,默认透明
     */
    open fun getStatusBarColor(): Int = android.graphics.Color.TRANSPARENT

    /**
     * 导航栏背景颜色,默认透明
     */
    open fun getNavigationBarColor(): Int = android.graphics.Color.TRANSPARENT

    /**
     * 状态栏文字/图标是否为浅色外观 
     * true: 状态栏内容为深色(适合浅色背景的主题)
     * false: 状态栏内容为浅色(适合暗色背景的主题)
     */
    open fun isLightStatusBarAppearance(): Boolean = false

    /**
     * 导航栏文字/图标是否为浅色外观
     * true: 导航栏内容为深色(适合浅色背景的主题)
     * false: 导航栏内容为浅色(适合暗色背景的主题)
     */
    open fun isLightNavigationBarAppearance(): Boolean = true

    /**
     * 统一设置沉浸式状态及系统栏背景和内容颜色
     */
    protected open fun setupImmersiveMode() {
        // 根据 isImmersiveModeEnabled() 决定内容是否延伸到系统栏区域
        WindowCompat.setDecorFitsSystemWindows(window, !isImmersiveModeEnabled())
        
        // 设置状态栏和导航栏颜色
        window.statusBarColor = getStatusBarColor()
        window.navigationBarColor = getNavigationBarColor()
        
        // 设置系统栏内容(图标与文字)的外观颜色
        val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
        windowInsetsController.isAppearanceLightStatusBars = isLightStatusBarAppearance()
        windowInsetsController.isAppearanceLightNavigationBars = isLightNavigationBarAppearance()
    }

    override fun onDestroy() {
        super.onDestroy()
        _binding = null
    }
}
复制代码

二、根据思考2,构建核心基类BaseUiActivity相关

状态枚举

c 复制代码
/**
 * 页面显示状态枚举
 */
enum class PageState {
    /**正常内容*/
    CONTENT,
    /**加载中*/
    LOADING,
    /**空数据状态*/
    EMPTY,
    /**异常错误状态*/
    ERROR
}
复制代码

layout_state_empty.xml 空数据布局

ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    android:background="?android:attr/colorBackground"
    android:padding="24dp">

    <ImageView
        android:id="@+id/iv_empty_icon"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@android:drawable/ic_menu_info_details" />

    <TextView
        android:id="@+id/tv_empty_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="暂无数据"
        android:textColor="@color/black"
        android:textSize="16sp" />

</LinearLayout>

复制代码

layout_state_error.xml 异常状态布局

ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    android:background="?android:attr/colorBackground"
    android:padding="24dp">

    <ImageView
        android:id="@+id/iv_error_icon"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@android:drawable/ic_dialog_alert" />

    <TextView
        android:id="@+id/tv_error_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="网络请求失败,请稍后重试"
        android:textColor="@color/black"
        android:textSize="16sp" />

    <Button
        android:id="@+id/btn_retry"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="重新加载" />

</LinearLayout>

复制代码

layout_state_loading.xml 加载中布局

ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    android:background="?android:attr/colorBackground">

    <ProgressBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminateTint="@color/purple_500" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="加载中..."
        android:textColor="@color/black"
        android:textSize="14sp" />

</LinearLayout>

复制代码

BaseUiActivity

kotlin 复制代码
/**
 * 带有状态管理的 Activity 基类
 * 自动拦截 BaseActivity 的 ContentView,包裹一层 FrameLayout 用来切换不同的状态视图:
 * 1. 内容状态 (正常显示子类传入的 ViewBinding 视图)
 * 2. 加载中状态 (Loading)
 * 3. 空数据 (Empty)
 * 4. 异常状态 (Error)
 */
abstract class BaseUiActivity<VB : ViewBinding> : BaseActivity<VB>() {

    private lateinit var containerLayout: FrameLayout
    
    // 状态视图缓存
    private var loadingView: View? = null
    private var emptyView: View? = null
    private var errorView: View? = null

    /**
     * 重写 onCreate,在设置子类 Content 之前包裹一层 Container
     */
    override fun onCreate(savedInstanceState: Bundle?) {
        // 创建底层容器,该容器会填满 BaseActivity 提供的根视图空间
        containerLayout = FrameLayout(this).apply {
            layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, 
                ViewGroup.LayoutParams.MATCH_PARENT
            )
        }

        super.onCreate(savedInstanceState)
    }

    /**
     * 我们需要拦截 BaseActivity 中的 setContentView,但是 BaseActivity 内部使用的是 binding.root
     * 为了不影响反射和类型推断,我们在 setContentView(binding.root) 之后,把界面偷换。
     */
    override fun setContentView(view: View) {
        // view 实际上就是通过反射创建出来的 binding.root
        // 我们把它添加到 containerLayout 中
        containerLayout.addView(view, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
        // 真正显示的是我们构建的 containerLayout
        super.setContentView(containerLayout)
        
        // 默认显示内容区域
        showContent()
    }

    /**
     * 切换页面状态
     */
    fun setPageState(state: PageState, msg: String? = null, iconRes: Int? = null) {
        hideAllStates()
        when (state) {
            PageState.CONTENT -> mViewBinding.root.visibility = View.VISIBLE
            PageState.LOADING -> showLoading()
            PageState.EMPTY -> showEmpty(msg, iconRes)
            PageState.ERROR -> showError(msg, iconRes)
        }
    }

    /**
     * 显示加载中视图
     */
    private fun showLoading() {
        if (loadingView == null) {
            loadingView = LayoutInflater.from(this).inflate(R.layout.layout_state_loading, containerLayout, false)
            containerLayout.addView(loadingView)
        }
        loadingView?.visibility = View.VISIBLE
    }

    /**
     * 显示空数据缺省视图
     * @param msg 缺省提示文字
     * @param iconRes 缺省提示图标
     */
    private fun showEmpty(msg: String? = null, iconRes: Int? = null) {
        if (emptyView == null) {
            emptyView = LayoutInflater.from(this).inflate(R.layout.layout_state_empty, containerLayout, false)
            containerLayout.addView(emptyView)
        }
        emptyView?.apply {
            visibility = View.VISIBLE
            msg?.let { findViewById<TextView>(R.id.tv_empty_text)?.text = it }
             iconRes?.let { findViewById<ImageView>(R.id.iv_empty_icon)?.setImageResource(it) }
        }
    }

    /**
     * 显示异常错误缺省视图
     * @param msg 缺省提示文字
     * @param iconRes 缺省提示图标
     */
    private fun showError(msg: String? = null, iconRes: Int? = null) {
        if (errorView == null) {
            errorView = LayoutInflater.from(this).inflate(R.layout.layout_state_error, containerLayout, false)
            errorView?.findViewById<Button>(R.id.btn_retry)?.setOnClickListener {
                onRetryClick()
            }
            containerLayout.addView(errorView)
        }
        errorView?.apply {
            visibility = View.VISIBLE
            msg?.let { findViewById<TextView>(R.id.tv_error_text)?.text = it }
             iconRes?.let { findViewById<ImageView>(R.id.iv_error_icon)?.setImageResource(it) }
        }
    }

    /**
     * 恢复并显示正常的业务内容视图
     */
    fun showContent() {
        setPageState(PageState.CONTENT)
    }

    /**
     * 点击缺省页面的"重新加载/刷新"按钮回调
     */
    protected open fun onRetryClick() {
        setPageState(PageState.LOADING)
        initData() // 默认尝试重新调用取数据的方法
    }

    /**
     * 隐藏所有附加的状态层和业务视图层
     */
    private fun hideAllStates() {
        mViewBinding.root.visibility = View.GONE
        loadingView?.visibility = View.GONE
        emptyView?.visibility = View.GONE
        errorView?.visibility = View.GONE
    }
}
复制代码
举个栗子:
kotlin 复制代码
class MainActivity : BaseUiActivity<ActivityMainBinding>() {

    override fun isImmersiveModeEnabled(): Boolean = false

    override fun onPreCreate() {
    }

    override fun initView(savedInstanceState: Bundle?) {
    }

    override fun initData() {
        // 数据请求或初始化
        setPageState(PageState.LOADING)

        mViewBinding.root.postDelayed({
            // 模拟失败,进入包含刷新按钮的异常页面
            setPageState(PageState.ERROR, "网络好像抛锚了...")
//            setPageState(PageState.CONTENT)
        }, 2000)
    }
}
复制代码

tips: 如果你的沉浸不生效,快去检查一下清单文件中application下的theme配置吧

到这里就差不多了,该基类Activity微调空间比较大,快去试试吧。不足之处 欢迎交流、指正~

--------------------------------- 差点忘了Fragment ---------------------------------------

与Activity同理,直接贴BaseFragment和BaseUiFragment代码:

kotlin 复制代码
/**
 * 通用 Fragment 基类 (BaseFragment)
 * 涵盖:
 * 1. ViewBinding 自动解析与生命周期安全管理 (反射方案)
 * 2. 通用生命周期与钩子方法 (Hook methods)
 */
abstract class BaseFragment<VB : ViewBinding> : Fragment() {

    private var _binding: VB? = null
    // 只能在 onCreateView 到 onDestroyView 之间访问
    protected val mViewBinding: VB
        get() = _binding ?: throw IllegalStateException("Binding is only valid between onCreateView and onDestroyView")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        onPreCreate(savedInstanceState)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = inflateBinding(inflater, container)
        // 给子类一个机会在返回根 View 之前进行包装干预
        return onWrapperView(mViewBinding.root)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initView(savedInstanceState)
        initData()
        initListener()
    }

    /**
     * 通过反射获取 ViewBinding 实例
     */
    @Suppress("UNCHECKED_CAST")
    private fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?): VB {
        var type = javaClass.genericSuperclass
        // 处理如果是多层继承的情况
        while (type !is ParameterizedType) {
            type = (type as Class<*>).genericSuperclass
        }
        val clazz = type.actualTypeArguments[0] as Class<VB>
        val method = clazz.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
        // Fragment 的 ViewBinding inflate 参数通常是 (inflater, parent, attachToParent)
        return method.invoke(null, inflater, container, false) as VB
    }

    /**
     * 在返回 onCreateView 结果时进行包裹,默认直接返回 binding.root
     * 如果子类需要状态管理(如 BaseUiFragment),可重写该方法将 root 包裹在 Container 中
     */
    protected open fun onWrapperView(root: View): View {
        return root
    }

    /**
     * Fragment onCreate 早期的初始化机会
     */
    open fun onPreCreate(savedInstanceState: Bundle?) {}

    /**
     * 初始化视图 (Hook)
     */
    abstract fun initView(savedInstanceState: Bundle?)

    /**
     * 初始化数据 (Hook)
     */
    open fun initData() {}

    /**
     * 初始化事件监听 (Hook)
     */
    open fun initListener() {}

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
复制代码
kotlin 复制代码
/**
 * 带有状态管理的 Fragment 基类
 * 在 BaseFragment.onWrapperView 中,将真实的视图包裹在 FrameLayout 容器中,
 * 用于动态切换显示:内容 (CONTENT)、加载中 (LOADING)、空数据 (Empty)、异常状态 (Error)。
 */
abstract class BaseUiFragment<VB : ViewBinding> : BaseFragment<VB>() {

    private lateinit var containerLayout: FrameLayout

    // 状态视图缓存
    private var loadingView: View? = null
    private var emptyView: View? = null
    private var errorView: View? = null

    /**
     * 重写包装逻辑:将 ViewBinding 解析出来的业务视图放进底层的 FrameLayout 中,
     * 以便实现多种状态层的自由切换和覆盖。
     */
    override fun onWrapperView(root: View): View {
        // 创建底层容器
        containerLayout = FrameLayout(requireContext()).apply {
            layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
        }
        
        // 将原业务视图加入容器
        containerLayout.addView(root, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
        
        // 初始默认显示内容
        showContent()

        // 告诉并返回给系统,这个 Fragment 的真实根视图变成了 containerLayout
        return containerLayout
    }

    /**
     * 切换页面状态
     */
    fun setPageState(state: PageState, msg: String? = null, iconRes: Int? = null) {
        hideAllStates()
        when (state) {
            PageState.CONTENT -> mViewBinding.root.visibility = View.VISIBLE
            PageState.LOADING -> showLoading()
            PageState.EMPTY -> showEmpty(msg, iconRes)
            PageState.ERROR -> showError(msg, iconRes)
        }
    }

    /**
     * 显示加载中视图
     */
    private fun showLoading() {
        if (loadingView == null) {
            loadingView = LayoutInflater.from(requireContext()).inflate(R.layout.layout_state_loading, containerLayout, false)
            containerLayout.addView(loadingView)
        }
        loadingView?.visibility = View.VISIBLE
    }

    /**
     * 显示空数据缺省视图
     * @param msg 缺省提示文字
     * @param iconRes 缺省提示图标
     */
    private fun showEmpty(msg: String? = null, iconRes: Int? = null) {
        if (emptyView == null) {
            emptyView = LayoutInflater.from(requireContext()).inflate(R.layout.layout_state_empty, containerLayout, false)
            containerLayout.addView(emptyView)
        }
        emptyView?.apply {
            visibility = View.VISIBLE
            msg?.let { findViewById<TextView>(R.id.tv_empty_text)?.text = it }
            iconRes?.let { findViewById<ImageView>(R.id.iv_empty_icon)?.setImageResource(it) }
        }
    }

    /**
     * 显示异常错误缺省视图
     * @param msg 缺省提示文字
     * @param iconRes 缺省提示图标
     */
    private fun showError(msg: String? = null, iconRes: Int? = null) {
        if (errorView == null) {
            errorView = LayoutInflater.from(requireContext()).inflate(R.layout.layout_state_error, containerLayout, false)
            errorView?.findViewById<Button>(R.id.btn_retry)?.setOnClickListener {
                onRetryClick()
            }
            containerLayout.addView(errorView)
        }
        errorView?.apply {
            visibility = View.VISIBLE
            msg?.let { findViewById<TextView>(R.id.tv_error_text)?.text = it }
            iconRes?.let { findViewById<ImageView>(R.id.iv_error_icon)?.setImageResource(it) }
        }
    }

    /**
     * 恢复并显示正常的业务内容视图
     */
    fun showContent() {
        setPageState(PageState.CONTENT)
    }

    /**
     * 点击缺省页面的"重新加载"按钮回调
     */
    protected open fun onRetryClick() {
        setPageState(PageState.LOADING)
        initData() // 默认尝试重新调用取数据的方法
    }

    /**
     * 隐藏所有层
     */
    private fun hideAllStates() {
        mViewBinding.root.visibility = View.GONE
        loadingView?.visibility = View.GONE
        emptyView?.visibility = View.GONE
        errorView?.visibility = View.GONE
    }
}
相关推荐
奥陌陌7 小时前
android 打印函数调用堆栈
android
用户985120035837 小时前
Compose Navigation 3 深度解析(二):基础用法
android·android jetpack
恋猫de小郭7 小时前
Android 官方正式官宣 AI 支持 AppFunctions ,Android 官方 MCP 和系统级 OpenClaw 雏形
android·前端·flutter
黄林晴8 小时前
Android 17 Beta 2,隐私这把锁又拧紧了
android
Kapaseker8 小时前
研究表明,开发者对Kotlin集合的了解不到 20%
android·kotlin
bqliang9 小时前
Compose 媒体查询 (Media Query API) 🖱️👇🕹️
android·android jetpack
程序员陆业聪17 小时前
Android 平台 AI Agent 技术架构深度解析
android·人工智能
BD_Marathon1 天前
工厂方法模式
android·java·工厂方法模式
王码码20351 天前
Flutter for OpenHarmony:socket_io_client 实时通信的事实标准(Node.js 后端的最佳拍档) 深度解析与鸿蒙适配指南
android·flutter·ui·华为·node.js·harmonyos