封装基类Activity\Fragment,从0到1记录
思考1:基础能力
- 生命周期与钩子方法
- ViewBinding
- 沉浸式
- 权限(本文忽略,权限封装可查看我另一篇文章点击此处,值得一看~)
思考2:UI能力
我们所看的UI,除去导航栏之类的,单说UI界面,暂时分为三种情况:
- 1.加载中
- 2.显示数据
- 3.缺省页(空数据,异常错误等等)
并且基本上每个UI都需要,那是不是可以把它在抽离一下,这样就不用每个界面都写一遍,这样界面层次就更清楚。
直接上菜:
一、根据思考1,构建顶层核心基类BaseActivity相关
ViewBinding
android {
...
buildFeatures {
...
viewBinding = true
}
}
复制代码
BaseActivity
/**
* 通用 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")
/**
* 是否开启沉浸式(内容延伸到状态栏和导航栏下方),默认 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
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. 初始化前的钩子方法
onPreCreate()
// 2. 初始化 ViewBinding
_binding = inflateBinding()
setContentView(mViewBinding.root)
// 3. 沉浸式与系统栏配置
setupImmersiveMode()
// 4. 初始化数据与视图的钩子方法
initView(savedInstanceState)
initListener()
initData()
}
/**
* 通过反射获取 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)
*/
abstract fun initListener()
/**
* 初始化数据 (Hook)
*/
abstract fun initData()
/**
* 在 onCreate 的 setContentView 之前执行的钩子方法
* 可用于转场动画、主题切换等
*/
open fun onPreCreate() {}
/**
* 统一设置沉浸式状态及系统栏背景和内容颜色
*/
protected open fun setupImmersiveMode() {
// 根据 isImmersiveModeEnabled() 决定内容是否延伸到系统栏区域
WindowCompat.setDecorFitsSystemWindows(window, !isImmersiveModeEnabled())
// 设置状态栏和导航栏颜色
window.statusBarColor = getStatusBarColor()
window.navigationBarColor = getNavigationBarColor()
// 设置系统栏内容(图标与文字)的外观颜色
val controller = WindowInsetsControllerCompat(window, window.decorView)
controller.isAppearanceLightStatusBars = isLightStatusBarAppearance()
controller.isAppearanceLightNavigationBars = isLightNavigationBarAppearance()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
复制代码
栗子省略
二、根据思考2,构建核心基类BaseUiActivity相关
状态枚举
/**
* 页面显示状态枚举
*/
enum class PageState {
/**正常内容*/
CONTENT,
/**加载中*/
LOADING,
/**空数据状态*/
EMPTY,
/**异常错误状态*/
ERROR
}
复制代码
layout_state_empty.xml 空数据布局
<?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 异常状态布局
<?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 加载中布局
<?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
/**
* 带有状态管理的 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
}
}
复制代码
举个栗子:
class MainActivity : BaseUiActivity<ActivityMainBinding>() {
override fun isImmersiveModeEnabled(): Boolean = true
override fun onPreCreate() {
}
override fun initView(savedInstanceState: Bundle?) {
}
override fun initListener() {
}
override fun initData() {
// 数据请求或初始化
setPageState(PageState.LOADING)
mViewBinding.root.postDelayed({
// 模拟失败,进入包含刷新按钮的异常页面
setPageState(PageState.ERROR, "网络好像抛锚了...")
// setPageState(PageState.CONTENT)
}, 2000)
}
}
复制代码
tips: 如果你的沉浸不生效,快去检查一下清单文件中application下的theme配置吧
三、Fragment基类
与Activity同理,直接贴BaseFragment和BaseUiFragment代码:
/**
* 通用 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)
initListener()
initData()
}
/**
* 通过反射获取 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 早期的初始化机会 (Hook)
*/
open fun onPreCreate(savedInstanceState: Bundle?) {}
/**
* 初始化视图 (Hook)
*/
abstract fun initView(savedInstanceState: Bundle?)
/**
* 初始化事件监听 (Hook)
*/
abstract fun initListener()
/**
* 初始化数据 (Hook)
*/
abstract fun initData()
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
复制代码
/**
* 带有状态管理的 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
}
}
栗子省略
到这里就差不多了,该基类Activity微调空间比较大,快去试试吧。不足之处 欢迎交流、指正~