从Dart到Kotlin扩展函数的基本常规操作及应用场景

Kotlin的扩展属性与扩展方法及其实战应用

前言

扩展函数真的是现代开发语言的神器,不管是之前用的 Kotlin 还是 Dart 都可以使用扩展函数,在不改变原本类的前提下添加一些方法和变量,特别的方便。

前段时间在写 Flutter 项目,定义了很多扩展函数,有各种定义:

当时还不觉得,最近又开始写 Kotlin 项目突然觉得,好像自己一直都是用 Kotlin 的扩展方法,不带括号的写法自己还真没怎么用,一时间反应不过来赶紧理清一下头绪,属实是被 Dart 给教育了一波。

关于 KT 的扩展大家可能都知道可以扩展对象的方法,也能扩展对象的属性,如果从一个简单的外观来区分,我们简单的区分为带括号的和不带括号的(扩展方法和扩展属性)。

下面一起复习一下 KT 的扩展函数的扩展方法和扩展属性吧。

一、跟着Dart学Kotlin扩展函数

在 Dart 中我们可以定义扩展方法 dp() 并且可以传递参数。同时我们也可以定义 ap 扩展属性,给 int 加入一个属性,并且我们指定了这个属性是只读的,代码如下:

csharp 复制代码
extension IntExtension on int {
  double get ap {
    return ScreenUtil.getInstance().getAdapterSize(toDouble());
  }

  double dp() {
    return ScreenUtil.getInstance().getAdapterSize(this);
  }
}

他们的功能是一样的,但是使用起来就不同了,走的逻辑分支也不同,一个是调用方法,一个是调用属性。

其实我们扩展属性也是可以分别设置读写设置的:

extension IdNameEntityExtension on IdNameEntity { String get propertyId { return id ?? ""; }

set propertyId(String id) { this.id = id; }

String get propertyName { return name!; }

set propertyName(String value) { name = value; } }

比如我们对一个 IDName 的实体做一个扩展属性,那么可以设置它的可读可写,虽然是一个简单的例子看起来很傻,但是这个简单的示例确实最基本的属性扩展可读写的权限控制演示。

同样的代码我们也可以用 Kotlin 实现:

kotlin 复制代码
val Int.ap: Int
    get() = CommUtils.dip2px(this)

fun Int.sp(): Int {
    return CommUtils.dip2px(this)
}

同样的设置 sp() 为扩展方法,ap 为可读的扩展属性。只是与 Dart 的扩展不同的是标识不同,扩展函数用 fun 标识函数,扩展属性用 val 或 var。

对应自定义对象的扩展, KT 也是类似的做法:

kotlin 复制代码
var IDNameEntity.propertyId: String
    get() = this.id
    set(value:String) {
        this.id = value
    }

val IDNameEntity.propertyName :String
       get() = this.name

如果只读的,我们用下面的方法 val 标识函数,使用 get 返回对应的值,如果是可读可写的就需要用 var 标识并且设置 get 和 set 标识并处理对应的逻辑。

测试代码:

ini 复制代码
  val idNameEntity = IDNameEntity("25", "关羽")
  val propertyName = idNameEntity.propertyName
  idNameEntity.propertyId = "30"
  val propertyId = idNameEntity.propertyId

  YYLogUtils.w("propertyName:$propertyName propertyId:$propertyId")

打印:

二、实践一:空间大小格式化

熟悉了扩展方法和扩展属性,我们就可以先来一个简单的空间大小格式化的处理。

一般我们获取到当前 App 占用的空间之后,我们就会把内置卡占用空间和外置卡占用空间加起来就是 App 占用空间。

如果单位的格式不同我们就需要转换为最基本的 B 单位相加,然后再处理格式化到指定的空间展示,我们可以把这一过程用扩展方法和扩展属性来集中管理。

我们参考大佬的文章【传送门】实现。

我们先定义一个普通的类,用于传入基本的 B 单位:

kotlin 复制代码
class DataUnitSize constructor(val rawBytes: Long) {

    fun toDouble(unit: DataUnit): Double = convertDataUnit(rawBytes.toDouble(), DataUnit.BYTES, unit)

    fun toLong(unit: DataUnit): Long = convertDataUnit(rawBytes, DataUnit.BYTES, unit)

    //默认的 toString 输出最基本的 B 单位
    override fun toString(): String = String.format("%dB", rawBytes)

    fun toString(unit: DataUnit, decimals: Int = 2): String {
        require(decimals >= 0) { "小数点必须大于0, 现在的小数点是$decimals" }
        val number = toDouble(unit)
        if (number.isInfinite()) {
            return number.toString()
        }

        val newDecimals = decimals.coerceAtMost(12)
        return DecimalFormat("0").run {
            if (newDecimals > 0) minimumFractionDigits = newDecimals
            roundingMode = RoundingMode.HALF_UP
            format(number) + unit.shortName
        }
    }
}

我们这里简单的定义一个构造方法,用于传递最基本的 B 单位,重写和提供 toString 方法,分别打印最基本的 B 单位大小,和指定格式的大小。

内部涉及到 DataUnit 单位枚举 和 convertDataUnit 单位转换的方法:

kotlin 复制代码
enum class DataUnit(val shortName: String) {
    BYTES("B"),
    KILOBYTES("KB"),
    MEGABYTES("MB"),
    GIGABYTES("GB"),
    TERABYTES("TB"),
}

private const val BYTES_PER_KB: Long = 1024  //KB
private const val BYTES_PER_MB = BYTES_PER_KB * 1024  //MB
private const val BYTES_PER_GB = BYTES_PER_MB * 1024  //GB
private const val BYTES_PER_TB = BYTES_PER_GB * 1024  //TB

//主要的单位转换方法之一
fun convertDataUnit(value: Long, sourceUnit: DataUnit, targetUnit: DataUnit): Long {
    val valueInBytes = when (sourceUnit) {
        DataUnit.BYTES -> value
        DataUnit.KILOBYTES -> Math.multiplyExact(value, BYTES_PER_KB)
        DataUnit.MEGABYTES -> Math.multiplyExact(value, BYTES_PER_MB)
        DataUnit.GIGABYTES -> Math.multiplyExact(value, BYTES_PER_GB)
        DataUnit.TERABYTES -> Math.multiplyExact(value, BYTES_PER_TB)
    }
    return when (targetUnit) {
        DataUnit.BYTES -> valueInBytes
        DataUnit.KILOBYTES -> valueInBytes / BYTES_PER_KB
        DataUnit.MEGABYTES -> valueInBytes / BYTES_PER_MB
        DataUnit.GIGABYTES -> valueInBytes / BYTES_PER_GB
        DataUnit.TERABYTES -> valueInBytes / BYTES_PER_TB
    }
}

//主要的单位转换方法之一
fun convertDataUnit(value: Double, sourceUnit: DataUnit, targetUnit: DataUnit): Double {
    val valueInBytes = when (sourceUnit) {
        DataUnit.BYTES -> value
        DataUnit.KILOBYTES -> value * BYTES_PER_KB
        DataUnit.MEGABYTES -> value * BYTES_PER_MB
        DataUnit.GIGABYTES -> value * BYTES_PER_GB
        DataUnit.TERABYTES -> value * BYTES_PER_TB
    }
    require(!valueInBytes.isNaN()) { "DataUnit value cannot be NaN." }
    return when (targetUnit) {
        DataUnit.BYTES -> valueInBytes
        DataUnit.KILOBYTES -> valueInBytes / BYTES_PER_KB
        DataUnit.MEGABYTES -> valueInBytes / BYTES_PER_MB
        DataUnit.GIGABYTES -> valueInBytes / BYTES_PER_GB
        DataUnit.TERABYTES -> valueInBytes / BYTES_PER_TB
    }
}

有了基本的类处理,和基本的单位转换方法,我们就能定义相对于的扩展方法和扩展属性了:

kotlin 复制代码
//定义常用的一系列扩展属性(只读)
val Long.b get() = this.toDataSize(DataUnit.BYTES)
val Long.kb get() = this.toDataSize(DataUnit.KILOBYTES)
val Long.mb get() = this.toDataSize(DataUnit.MEGABYTES)
val Long.gb get() = this.toDataSize(DataUnit.GIGABYTES)
val Long.tb get() = this.toDataSize(DataUnit.TERABYTES)

val Int.b get() = this.toLong().toDataSize(DataUnit.BYTES)
val Int.kb get() = this.toLong().toDataSize(DataUnit.KILOBYTES)
val Int.mb get() = this.toLong().toDataSize(DataUnit.MEGABYTES)
val Int.gb get() = this.toLong().toDataSize(DataUnit.GIGABYTES)
val Int.tb get() = this.toLong().toDataSize(DataUnit.TERABYTES)

val Double.b get() = this.toDataSize(DataUnit.BYTES)
val Double.kb get() = this.toDataSize(DataUnit.KILOBYTES)
val Double.mb get() = this.toDataSize(DataUnit.MEGABYTES)
val Double.gb get() = this.toDataSize(DataUnit.GIGABYTES)
val Double.tb get() = this.toDataSize(DataUnit.TERABYTES)

//定义的扩展方法之一,转换为 new 出来的 DataSize 对象
fun Long.toDataSize(unit: DataUnit): DataUnitSize {
    return DataUnitSize(convertDataUnit(this, unit, DataUnit.BYTES))
}

//定义的扩展方法之一,转换为 new 出来的 DataSize 对象
fun Double.toDataSize(unit: DataUnit): DataUnitSize {
    return DataUnitSize(convertDataUnit(this, unit, DataUnit.BYTES).roundToLong())
}

// 给自定义的 DataSize 定义扩展方法,可以自动格式化到指定的单位
fun DataUnitSize.autoFormatDataSize(): String {
    val dataSize = this
    return when {
        dataSize.inWholeTerabytes >= 1 -> dataSize.toString(DataUnit.TERABYTES)
        dataSize.inWholeGigabytes >= 1 -> dataSize.toString(DataUnit.GIGABYTES)
        dataSize.inWholeMegabytes >= 1 -> dataSize.toString(DataUnit.MEGABYTES)
        else -> dataSize.toString(DataUnit.KILOBYTES)
    }
}

// 给自定义的 DataSize 定义扩展方法,是否满足当前单位
val DataUnitSize.inWholeBytes: Long get() = toLong(DataUnit.BYTES)
val DataUnitSize.inWholeKilobytes: Long get() = toLong(DataUnit.KILOBYTES)
val DataUnitSize.inWholeMegabytes: Long get() = toLong(DataUnit.MEGABYTES)
val DataUnitSize.inWholeGigabytes: Long get() = toLong(DataUnit.GIGABYTES)
val DataUnitSize.inWholeTerabytes: Long get() = toLong(DataUnit.TERABYTES)

使用的时候,可以手动指定格式化单位:

kotlin 复制代码
    val dataSize1 = 888.mb

    YYLogUtils.w("格式化 B:${dataSize1.toString()}")
    YYLogUtils.w("格式化 KB:${dataSize1.toString(DataUnit.KILOBYTES)}")
    YYLogUtils.w("格式化 GB:${dataSize1.toString(DataUnit.GIGABYTES)}")

也可以自动转换格式化单位:

ini 复制代码
    val sizeInBytes = 1500000.b 
    val formattedSize = sizeInBytes.autoFormatDataSize()
    YYLogUtils.w(formattedSize) // 输出:1.43MB

如果想要想加也可以自己实现想加,当然也可以自己实现 Comparable 接口加上 operator 操作符,有兴趣可以往下看。

ini 复制代码
    val dataSize1 = 888.mb

    val dataSize2 = 1.gb

    val plusSize = DataUnitSize(dataSize1.rawBytes + dataSize2.rawBytes).autoFormatDataSize()
    YYLogUtils.w(plusSize)

打印 Log 如下:

二、实践二:汇率格式化

如果我们的需求是,做完这个任务获得100美元,做完另一个任务获得80港币,最终我需要计算一共获得多少人民币。

我们可以通过类似的方法来实现,由于上面给出了具体的实现步骤这里就直接上代码了:

kotlin 复制代码
enum class CurrencyUnit(val prefix: String, val fullName: String, val exchangeRate: Double) {
    CNY("¥", "CNY", 1.0),
    USD("$", "USD", 8.0),
    SGD("S$", "SGD", 5.0),
    HKD("HK$", "HKD", 0.8),
}

//简单的转换方法
fun convertCurrency(value: Double, sourceUnit: CurrencyUnit, targetUnit: CurrencyUnit): Double {
    val valueInRMB = value * sourceUnit.exchangeRate
    return valueInRMB / targetUnit.exchangeRate
}

/**
使用 @JvmInline value class 优化旨在提高内存和性能效率
使用 @JvmInline value class 必须有且仅有一个属性,主构造函数的参数只能是 val,
并且类不能被继承,生成的字节码会在使用处直接嵌入,而不会引入额外的对象

如果不想使用 @JvmInline value class 用普通的class也能行
 */
@JvmInline
value class CurrencyAmount constructor(private val rawAmount: Double) : Comparable<CurrencyAmount> {

    private fun toDouble(unit: CurrencyUnit): Double = convertCurrency(rawAmount, CurrencyUnit.CNY, unit)

    operator fun unaryMinus(): CurrencyAmount {
        return CurrencyAmount(-this.rawAmount)
    }

    operator fun plus(other: CurrencyAmount): CurrencyAmount {
        return CurrencyAmount(this.rawAmount + other.rawAmount)
    }

    operator fun minus(other: CurrencyAmount): CurrencyAmount {
        return this + (-other) // a - b = a + (-b)
    }

    operator fun times(scale: Int): CurrencyAmount {
        return CurrencyAmount(this.rawAmount * scale)
    }

    operator fun div(scale: Int): CurrencyAmount {
        return CurrencyAmount(this.rawAmount / scale)
    }

    operator fun times(scale: Double): CurrencyAmount {
        return CurrencyAmount(this.rawAmount * scale)
    }

    operator fun div(scale: Double): CurrencyAmount {
        return CurrencyAmount(this.rawAmount / scale)
    }

    override fun compareTo(other: CurrencyAmount): Int {
        return this.rawAmount.compareTo(other.rawAmount)
    }

    override fun toString(): String = String.format("¥ %.2f", rawAmount)

    fun toString(unit: CurrencyUnit, decimals: Int = 2): String {
        require(decimals >= 0) { "小数点必须大于0, 现在的小数点是$decimals" }
        val number = toDouble(unit)
        if (number.isInfinite()) {
            return number.toString()
        }
        val newDecimals = decimals.coerceAtMost(12)
        return DecimalFormat("0").run {
            if (newDecimals > 0) minimumFractionDigits = newDecimals
            roundingMode = RoundingMode.HALF_UP
            unit.prefix + " " + format(number)
        }
    }
}

//定义常用的一系列扩展属性(只读)
val Double.cny get() = CurrencyAmount(this)
val Long.cny get() = CurrencyAmount(this.toDouble())
val Int.cny get() = CurrencyAmount(this.toDouble())

val Double.usd get() = CurrencyAmount(this * CurrencyUnit.USD.exchangeRate)
val Long.usd get() = CurrencyAmount(this.toDouble() * CurrencyUnit.USD.exchangeRate)
val Int.usd get() = CurrencyAmount(this.toDouble() * CurrencyUnit.USD.exchangeRate)

val Double.sgd get() = CurrencyAmount(this * CurrencyUnit.SGD.exchangeRate)
val Long.sgd get() = CurrencyAmount(this.toDouble() * CurrencyUnit.SGD.exchangeRate)
val Int.sgd get() = CurrencyAmount(this.toDouble() * CurrencyUnit.SGD.exchangeRate)

val Double.hkd get() = CurrencyAmount(this * CurrencyUnit.HKD.exchangeRate)
val Long.hkd get() = CurrencyAmount(this.toDouble() * CurrencyUnit.HKD.exchangeRate)
val Int.hkd get() = CurrencyAmount(this.toDouble() * CurrencyUnit.HKD.exchangeRate)

相对而言更简单了,我们不需要想空间转换那样叠加使用,也不需要判断是否超过某一个单位,也无需自动格式化单位。

我们只需要简单的定义单位转换,并且实现简单语义化的加减乘除等操作,方便金额的计算。

如何使用?

kotlin 复制代码
    val money1 = 100.usd
    YYLogUtils.w("格式化金额人民币:${money1.toString()}")
    YYLogUtils.w("格式化金额新加坡:${money1.toString(CurrencyUnit.SGD)}")
    YYLogUtils.w("格式化金额港币:${money1.toString(CurrencyUnit.HKD)}")

    val money2 = 100.50.hkd

    YYLogUtils.w("100美元加100.5港币等于:${(money1+money2).toString()}")

打印如下:

当然这是试验性质,实际的汇率是根据后端返回实时变动的,切不可直接生搬硬套。

后记

除此之外常规的扩展方法更是应用广泛,比如RV的扩展,FindView的扩展,SP的扩展,DataStore的扩展,Intent的扩展,Dialog扩展等等层出不穷。

扩展属性对于一些简单的逻辑也更方便,更简洁。一般我们需要一个值可以直接用扩展属性,如果要进行逻辑运算那么可以用扩展方法,两者结合使用可以实现很多骚功能。

本文的几个例子只是使用了基本的扩展属性和扩展方法,大家有兴趣可以自行练习一下。本文的代码仅用于学习交流,如果要用于实战我推荐你最好自己参照着实现对应的逻辑。

除此之外,我们还能实现扩展的高阶函数,或实现DSL的方式,都是一些相对"高级"的用法,有兴趣的可以看我这篇文章【扩展高阶函数与DSL】

关于本文的内容如果想查看源码可以点击这里 【传送门】。你也可以关注我的这个Kotlin项目(虽然是两年前的老项目了)我有时间都会持续更新。

如果本文有错漏的地方或者有其他更好的方案,希望同学们可以在评论区指出或交流。

好了,如果感觉本文对你有一点点的启发与帮助,还望你能点赞支持一下,你的支持是我最大的动力啦。

Ok,这一期就此完结。

相关推荐
Estar.Lee4 分钟前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood16 分钟前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-3 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen5 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年13 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿15 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神16 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛17 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法17 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter18 小时前
Android吸顶效果,并有着ViewPager左右切换
android