一般来说,我们创建一个页面大概是以下步骤:
- 1 初始化UI,通过
findViewById()
或者ViewBinding
。 - 2 请求服务器数据。
- 3 数据到了之后渲染到UI上。
说白了就是:创建UI;请求数据;渲染UI。
如此简单的逻辑,却要写很多重复代码,那,有没有啥方法能简化一下呢?
那必须有。
不但有,而且可以让创建UI和请求数据一起发生,两步都搞定了再去渲染UI,写着简单,速度还快,最重要的是:能装逼。
首先,我们需要定义一个页面枚举,用它来区分不同的页面类型。
// 定义页面类型
enum class PageType(val tag: String) {
PAGE_OTHER("OTHER"),
PAGE_ACTIVITY("Activity"),
PAGE_FRAGMENT("Fragment"),
PAGE_DIALOG("Dialog");
}
// 定义页面接口,作为页面(Activity,Fragment,Dialog等)的上级
interface IPage {
/**
* 页面类型
*/
fun pageType(): PageType
}
然后,我们需要定义一个数据标记,用于统一所有数据的类型。
// 定义数据标记,作为所有请求结果的统一点
data class RspResult<T>(val code: Int, val msg: String?, val result: T?) {
fun success(): Boolean {
return code == 200
}
}
接着,我们来定义页面的顶层渲染逻辑。
// 制定UI渲染协议
interface IUIRender<VB : ViewBinding, T> {
/**
* 创建UI
*/
fun createVB(inflater: LayoutInflater? = null, container: ViewGroup? = null): VB?
/**
* 请求数据
*/
suspend fun getData(): RspResult<T>
/**
* 渲染数据
*/
fun renderUI(vb: VB?, data: RspResult<T>)
/**
* 由于View创建速度快于数据,在View添加完毕后会回调这个,用来执行一些UI的初始化工作,比如点击事件。
*/
fun onContentViewAdded(vb: VB) {
}
/**
* 有的页面需要刷新行为,提供给页面调用,这会重新触发[renderUI]
*/
fun refresh() {
}
}
只看这个定义我们就知道页面需要做什么了。创建UI得到一个VB
,请求数据得到一个RspResult
,然后用这俩去渲染数据就完事了。这就是核心逻辑,也是我们一开始要解决的问题。
好,顶层逻辑定义完了,我们现在可以来实现具体的Activity
的逻辑了。
abstract class AsyncVBActivity<VB : ViewBinding, T> : BaseActivity(), IUIRender<VB, T>, IPage {
@Volatile
protected var vb: VB? = null
@Volatile
protected var data: RspResult<T>? = null
private val ioDispatcher = Dispatchers.IO
private val uiDispatcher = Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 在IO线程中创建出VB 和 Data,同时到达后切换到UI线程去处理
lifecycleScope.launch(ioDispatcher) {
val uiJob = async { createVBInternal() }
val dataJob = async { getDataInternal() }
renderUIInternal(uiJob.await(), dataJob.await())
}
}
private fun createVBInternal(inflater: LayoutInflater? = null, container: ViewGroup? = null): VB? {
val realVB = createVB(inflater, container)
log("createVBInternal: $realVB")
if (realVB == null) return null
this@AsyncVBActivity.vb = realVB
lifecycleScope.launch(uiDispatcher) {
setContentView(realVB.root)
onContentViewAdded(realVB)
}
return realVB
}
private suspend fun getDataInternal(): RspResult<T> {
val realData = getData()
log("getDataInternal: $realData")
this.data = realData
return realData
}
/**
* 创建默认VB,子类可以自行创建从而避免反射带来的开销
*/
override fun createVB(inflater: LayoutInflater?, container: ViewGroup?): VB? {
log("createVB: by $this")
// 这里默认是通过反射来创建ViewBinding
return createViewBinding(this)
}
/**
* 这里可以做一些校验检测
* 1 vb是否添加空校验
* 2 数据校验/拦截处理
*/
private suspend fun renderUIInternal(vb: VB?, data: RspResult<T>) {
withContext(uiDispatcher) {
log("renderUIInternal: $vb -> $data")
renderUI(vb, data)
}
}
override fun onContentViewAdded(vb: VB) {
log("onContentViewAdded: $vb")
}
override fun refresh() {
log("refresh")
lifecycleScope.launch(ioDispatcher) {
renderUIInternal(this@AsyncVBActivity.vb, getDataInternal())
}
}
override fun pageType(): PageType {
return PageType.PAGE_ACTIVITY
}
private fun log(msg: String) {
Ln.debug("[AsyncVBActivity]: {$msg}")
}
}
这里的逻辑也很简单,我们在IO线程创建UI以及请求数据。这两个都拿到后,就放在UI线程去进行渲染。到这里问题其实已经解决了。
这里附上反射创建ViewBinding
的代码。
// 通过反射来创建ViewBinding
fun <T : ViewBinding?> createViewBinding(activity: Activity): T? {
var viewBinding: T? = null
var cls: Class<*>? = null
var type: Type? = activity.javaClass.genericSuperclass ?: return viewBinding
while (type !is ParameterizedType && type is Class<*>) {
type = type.genericSuperclass
}
if (type is ParameterizedType) {
if (type.actualTypeArguments[0] is Class<*>) {
cls = type.actualTypeArguments[0] as Class<*>
}
} else {
return viewBinding // 如果所有的父类都不是ViewBinding 的泛型类型,直接返回null
}
try {
if (cls == null) return viewBinding
val inflate = cls.getDeclaredMethod("inflate", LayoutInflater::class.java)
viewBinding = inflate.invoke(null, activity.layoutInflater) as T
} catch (e: NoSuchMethodException) {
Ln.e(e)
} catch (e: IllegalAccessException) {
Ln.e(e)
} catch (e: InvocationTargetException) {
Ln.e(e)
}
return viewBinding
}
逻辑已经写完了,来个例子:
class TestActivity : AsyncVBActivity<ActivityTestBinding, User>() {
override suspend fun getData(): RspResult<User> {
// 这里是一个挂起函数,返回用户信息,使用suspendCancellableCoroutine实现。
return ApiUserService.getUserInfo(uid=10086)
}
override fun renderUI(vb: ActivityTestAbBinding?, data: RspResult<User>) {
vb ?: return
if (!data.success()) return
// 这里就可以直接拿到user,来渲染UI了。
vb.tvName.text = data.result.name
vb.tvAge.text = "${data.result.age}"
// 可以直接刷新
vb.refresh.setSafeClickListener {
refresh()
}
}
}
只需要重载两个函数,一个请求数据的,一个渲染UI的。
Fragment
也是一样的,只不过Fragment
的onCreateView()
需要返回一个View
,而我们的View
又是异步创建的,怎么返回呢?这里可以先创建一个空的View(FrameLayout)
来返回,同时去异步创建真实的View
,等到真实的View
创建完了,再添加到这个FrameLayout
上即可。
abstract class AsyncVBFragment<VB : ViewBinding, T> : Fragment(), IUIRender<VB, T>, IPage {
@Volatile
private var vb: VB? = null
@Volatile
private var data: RspResult<T>? = null
private val ioDispatcher = Dispatchers.IO
private val uiDispatcher = Dispatchers.Main
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// 使用一个空的ViewGroup占位,真实的View创建完成再来替换
val emptyView = inflater.inflate(R.layout.layout_empty_view, container, true) as ViewGroup
// 在IO线程中创建出VB 和 Data,同时到达后切换到UI线程去处理
viewLifecycleOwner.lifecycleScope.launch(ioDispatcher) {
val uiJob = async { createVBInternal(emptyView) }
val dataJob = async { getDataInternal() }
renderUIInternal(uiJob.await(), dataJob.await())
}
return emptyView
}
private fun createVBInternal(
holderView: ViewGroup,
inflater: LayoutInflater? = null,
container: ViewGroup? = null
): VB? {
val realVB = createVB(inflater, container)
log("createVBInternal: $realVB")
if (realVB == null) return null
this@AsyncVBFragment.vb = realVB
viewLifecycleOwner.lifecycleScope.launch(uiDispatcher) {
holderView.addView(realVB.root)
onContentViewAdded(realVB)
}
return realVB
}
private suspend fun getDataInternal(): RspResult<T> {
val realData = getData()
log("getDataInternal: $realData")
this.data = realData
return realData
}
override fun createVB(inflater: LayoutInflater?, container: ViewGroup?): VB? {
log("createVB: by $this")
if(inflater == null) return null
return ViewBindingUtils.createViewBinding(this, inflater, container)
}
/**
* 这里会等到数据过来后,才把真实的View添加到Fragment上。
* 如果需要提前添加,则可以在uiJob完成后就添加
*/
private suspend fun renderUIInternal(vb: VB?, data: RspResult<T>) {
withContext(uiDispatcher) {
log("renderUIInternal: $vb -> $data")
renderUI(vb, data)
}
}
override fun onContentViewAdded(vb: VB) {
log("onContentViewAdded: $vb")
}
override fun refresh() {
log("refresh")
lifecycleScope.launch(ioDispatcher) {
renderUIInternal(this@AsyncVBFragment.vb, getDataInternal())
}
}
override fun pageType(): PageType {
return PageType.PAGE_FRAGMENT
}
private fun log(msg: String) {
Ln.debug("[AsyncVBFragment]: {$msg}")
}
}
写到这里,相信你都懂了,我就不BB了,具体自己总结吧。