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
    }
}
相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work3 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work3 天前
ch21 签名、校验与发版
kotlin
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui