Kotlin属性委托的巧妙使用-埋点上报封装

⏰ : 全文字数:3150+

🥅 : 内容关键字:kotlin, 属性委托,埋点设计

🤙 : 公_众_号:七郎的小院

🥅 : 更多文章:blog.softgeek.cn

概述

在实际的项目开发中,一定会有埋点上报的需求。可能每个项目上报的方式不一样,有些是通过自动化埋点,不需要开发手动上报,有些是需要开发自己上报。在我们业务的一个项目中,因为采用的是业务推动迭代的方式,有大量的跟业务相关的埋点,且变动比较大,所以我们基本上都是开发自己上报。

在之前的 Java 作为主力语言开发的时候,埋点参数上报的封装基本上是利用 Builder 模式,或者使用 Map 的方式来进行上报,而到了 Kotlin 作为主力开发语言后,可以利用 Kotlin 提供的一些语法,更好的封装实现埋点的上报,今天就来谈谈我在实际项目中封装的一种思路。

旧方式

原来的埋点上报,对于上报参数的组织,有些是通过 Builder,有些是利用 Map 来组织。我这里取神策埋点 SDK 的上报方式为例:

利用 JSONObject 构建 Json 数据的方式上报

java 复制代码
try {
    JSONObject newProperties = new JSONObject();
    newProperties.put("AdSource", "Email");
    properties.put("AdSource", "XXX Store");

    // 再次设定用户渠道,设定无效,"developer@sensorsdata.cn" 的 "AdSource" 属性值仍然是 "XXX Store"
    SensorsDataAPI.sharedInstance().profileSetOnce(newProperties);
} catch (JSONException e) {
    e.printStackTrace();
}

利用 Map 集合的方式上报:

java 复制代码
// map集合
Map<String, Number> properties = new HashMap<String, Number>();
properties.put("UserPaid", 1);
properties.put("PointEarned", 12.5);

SensorsDataAPI.sharedInstance().profileIncrement(properties);

个人在使用的时候,觉得这种上报参数的组织方式,比较零散,不易于管理,复用性不强。我们之前也是使用了差不多的上报方式,但是使用多了之后,发现不是很方便,因此基于 Kotlin 的委托的语法特性,重新进行了封装。

新思路

基础抽象类 BaseEventTrack

首先对于每个埋点,我们采用了类来进行管理,即每个埋点抽象成一个埋点类。下每个埋点类基础一个抽象类,这个抽象类,来定义一些基础的信息,大概如下:

kotlin 复制代码
abstract class BaseEventTrack {
    private val tag = "BaseEventTrack"
    // 所有上报参数得收集,这一块可以自己定义和修改
    private val paramsMap = hashMapOf<String, Any?>()
    // 埋点名字
    abstract val eventId: String
    // 埋点的归类
    abstract val eventTrackCategory: String

    /**
     * 触发上报当前的埋点
     */
    fun upload() {
        // 
        EventTrack.stat(eventTrackCategory, eventId, paramsMap)
    }
    /**
     * 定义每种类型的委托方法,控制它的get和set方法,如果有新增,可以在这里新增
     */
    protected fun int(): ReadWriteProperty<BaseEventTrack, Int?> = InnerReadWriteProperty<BaseEventTrack, Int?>(0)

    protected fun string(): ReadWriteProperty<BaseEventTrack, String?> =
        InnerReadWriteProperty<BaseEventTrack, String?>("")

    protected fun long(): ReadWriteProperty<BaseEventTrack, Long?> = InnerReadWriteProperty<BaseEventTrack, Long?>(0)
    protected fun boolean(): ReadWriteProperty<BaseEventTrack, Boolean?> =
        InnerReadWriteProperty<BaseEventTrack, Boolean?>(false)

    protected fun float(): ReadWriteProperty<BaseEventTrack, Float?> =
        InnerReadWriteProperty<BaseEventTrack, Float?>(0F)

    protected fun double(): ReadWriteProperty<BaseEventTrack, Double?> =
        InnerReadWriteProperty<BaseEventTrack, Double?>(0.0)

    // 代理类属性的处理
    inner class InnerReadWriteProperty<T, V>(defaultValue: V) : ReadWriteProperty<T, V> {
        private var currentValue: V = defaultValue
        override fun getValue(thisRef: T, property: KProperty<*>): V {
            Log.i(tag, "thisRef=${thisRef},property=${property}")
            return currentValue
        }

        override fun setValue(thisRef: T, property: KProperty<*>, value: V) {
            Log.i(tag, "thisRef=${thisRef},property=${property},value=${value}")
            currentValue = value
            paramsMap[property.name] = value
        }
    }

}

这个埋点抽象类主要的作用在:

  • 定义了埋点一些基础属性,如 eventId
  • 利用 Kotlin 的属性委托,定义每种常用的基础数据类型的委托代理方法
  • 定义上报埋点的方法

这里新的思路就是:利用 Kotlin 中的属性委托类 ReadWriteProperty 来处理每个属性的 get 和 set,通过这个委托代理,收集和处理每个字段的值。其实就是起到一个拦截属性赋值和获取值的作用。

当有新的埋点时,我们可以这样定义埋点数据。比如我们常见的 Show 埋点:

kotlin 复制代码
class ShowEvent : BaseEventTrack() {
    override val eventId: String
        get() = "show"
    override val eventTrackCategory: String
        get() = "10001"

    /**
     * 页面停留时间
     */
    var pageTime by long()

    /**
     * 场景
     */
    var sceneName by string()

}

触发上报

埋点数据类封装好了之后,那么怎么给数据赋值和触发上报呢?这里使用inline函数,定义一个全局的静态方法,具体内容如下:

kotlin 复制代码
/**
 * @param block 初始化函数
 */
inline fun <reified T : BaseEventTrack> uploadEvent(crossinline block: T.() -> Unit) {
    kotlin.runCatching {
        // 实例化泛型T
        val clz = T::class.java
        val instance = clz.getDeclaredConstructor()
        instance.isAccessible = true
        val tInstance = instance.newInstance()
        // 触发对象初始化
        block.invoke(tInstance)
        // 触发上报
        tInstance.upload()
    }.onFailure {
        // 抛出异常,或者使用兜底方案
        Log.w("EventTrack", "upload event error,message=${it.message}")
    }
}

这个方法的参数是一个类型 T 的扩展函数类型,这个函数内部主要做的是初始化埋点数据字段的值。而方法内部的逻辑主要有:

  • 对泛型 T 进行实例化,构造对象
  • 触发对象的初始化操做,即对埋点字段赋值
  • 触发埋点的上报

注意:这里的方法中的reified必须加上,否则我们无法实例话一个泛型类型,这也是和 Java 不同的地方

比如上面的 Show 埋点,我们在使用的时候,可以这么使用:

kotlin 复制代码
fun uploadShowStat(){
    uploadEvent<ShowEvent> {
        sceneName = "七郎"
        pageTime = 100
    }
}

这样用起来是不是很方便呢~~~~

总结

这里的新思路,利用了 kotlin 的属性委托和reified的特性,实现了一种埋点参数封装的方式,这种方式优点是:使用管理起来非常的方便,每个埋点都对应一个类,代码可读性较好 。缺点是:过程中可能会参数一些额外的类

当然上面只是提供了一种思路,一些细节没有提到,比如一些异常处理,兜底逻辑,线程安全等,有兴趣的可以在上面的基础上实现。

相关推荐
SRC_BLUE_171 小时前
SQLI LABS | Less-39 GET-Stacked Query Injection-Intiger Based
android·网络安全·adb·less
无尽的大道4 小时前
Android打包流程图
android
镭封6 小时前
android studio 配置过程
android·ide·android studio
夜雨星辰4876 小时前
Android Studio 学习——整体框架和概念
android·学习·android studio
邹阿涛涛涛涛涛涛6 小时前
月之暗面招 Android 开发,大家快来投简历呀
android·人工智能·aigc
IAM四十二6 小时前
Jetpack Compose State 你用对了吗?
android·android jetpack·composer
mit6.8247 小时前
[Docker#4] 镜像仓库 | 部分常用命令
linux·运维·docker·容器·架构
奶茶喵喵叫7 小时前
Android开发中的隐藏控件技巧
android
Winston Wood9 小时前
Android中Activity启动的模式
android
众乐认证9 小时前
Android Auto 不再用于旧手机
android·google·智能手机·android auto