Splitties 学习笔记:BundleSpec

411 阅读2分钟

Activity 通过 Intent 传参的一般写法:

class ListActivity : Activity() {
    fun toDetail(id: Int, name: String) {
        Intent(this, DetailActivity::class.java).apply {
            putExtra(DetailActivity.EXTRA_ID, id)
            putExtra(DetailActivity.EXTRA_NAME, name)
        }.apply { startActivity(this) }
    }
}

class DetailActivity : Activity() {
    companion object {
        const val EXTRA_ID = "EXTRA_ID"
        const val EXTRA_NAME = "EXTRA_NAME"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val (id: Int, name: String) = intent.run {
            getIntExtra(EXTRA_ID, -1) to getStringExtra(EXTRA_NAME)!!
        }
    }
}

在目标 Activity 中构建 Intent 工具方法的写法:

class ListActivity : Activity() {
    fun toDetail(id: Int, name: String) {
        DetailActivity.createIntent(this, id, name).apply {
            startActivity(this)
        }
    }
}

class DetailActivity : Activity() {
    companion object {
        private const val EXTRA_ID = "EXTRA_ID"
        private const val EXTRA_NAME = "EXTRA_NAME"

        private fun Intent.id() = getIntExtra(EXTRA_ID, -1)

        private fun Intent.name() = getStringExtra(EXTRA_NAME)!!

        fun createIntent(context: Context, id: Int, name: String): Intent {
            return Intent(context, DetailActivity::class.java).apply {
                putExtra(EXTRA_ID, id)
                putExtra(EXTRA_NAME, name)
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val id = intent.id()
        val name = intent.name()
    }
}

Splitties 写法:

class ListActivity : Activity() {
    fun toDetail(id: Int, name: String) {
        start<DetailActivity> {
            putExtras(DetailActivity.ExtraSpec) {
                this.id = id
                this.name = name
            }
        }

//        start(DetailActivity) { _, extra ->
//            extra.id = id
//            extra.name = name
//        } // call with ActivityIntentSpec
    }
}

class DetailActivity : Activity() {
//    companion object : ActivityIntentSpec<DetailActivity, ExtraSpec> by activitySpec(ExtraSpec)

    object ExtraSpec : BundleSpec() {
        var id: Int by bundle()
        var name: String by bundle()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val (id, name) = withExtras(ExtraSpec) {
            id to name
        }
    }
}

BundleSpec 的优点有:1. 提供类型约束 2. 省略 EXTRA_KEY

接下来看看 BundleSpec 的实现原理

先从神奇的 by bundle() 代理方法看起

fun <T : Any> BundleSpec.bundle() = BundleDelegate as ReadWriteProperty<BundleSpec, T>

private object BundleDelegate : ReadWriteProperty<BundleSpec, Any> {

    override operator fun getValue(thisRef: BundleSpec, property: KProperty<*>): Any {
        val key = property.name
        return checkNotNull(thisRef.bundle[key]) { "Property $key could not be read" }
    }

    override operator fun setValue(thisRef: BundleSpec, property: KProperty<*>, value: Any) {
        thisRef.put(property.name, value)
    }
}

可以看出 BundleSpec 内定义的参数字段,最终会以 property.name 为键,与 BundleSpec 包裹的 bundle 相关联

那么 BundleSpec 又是如何包裹 bundle 对象呢?就得从入口方法 putExtras(BundleSpec) 和出口方法 withExtras(BundleSpec) 找答案了

inline fun <Spec : BundleSpec> Intent.putExtras(
    spec: Spec,
    crossinline block: Spec.() -> Unit
) {
    replaceExtras((extras ?: Bundle()).apply { with(spec, block) })
}

inline fun <Spec : BundleSpec, R> Bundle.with(
    spec: Spec,
    crossinline block: Spec.() -> R
): R {
    return try {
        spec.currentBundle = this
        spec.block()
    } finally {
        spec.currentBundle = null
    }
}

inline fun <Spec : BundleSpec, R> Intent.withExtras(
    spec: Spec,
    crossinline block: Spec.() -> R
): R {
    return try {
        spec.isReadOnly = true
        spec.currentBundle = extras ?: Bundle()
        spec.block()
    } finally {
        spec.currentBundle = null
        spec.isReadOnly = false
    }
}

open class BundleSpec {

    internal var currentBundle: Bundle?
    // ...

    internal var isReadOnly: Boolean
    // ...
    
}

可以看出 BundleSpec 中留有 currentBundle 字段,被 internal 约束,仅可由库代码访问,在赋值和取值的出入口方法中,先与上下文中 Intent.bundle 相关联(hook),提供了代理底层支持,然后配置访问,最后再解除关联,保证不泄露地安全地单次使用