从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,这一期就此完结。

相关推荐
阿巴斯甜12 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker13 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952714 小时前
Andorid Google 登录接入文档
android
黄林晴15 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android