Kotlin进阶之——Lazy对于activity、fragment成员变量的妙用

我们在activity或fragment写成员变量时,是不是经常苦恼怎么写都似乎不怎么理想,总感觉缺少一点什么呢?

初入kotlin

当我们刚学习kotlin是,基本上都处于模仿Java写成员变量的过程中:

private var testDialog: Dialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    testDialog = Dialog(this)
    //…一些设置
}
fun ttt() {
//    一些操作
    testDialog?.show()
//    testDialog?.xxx
//    testDialog?.yyy
//    testDialog?.zzz
}

很明显,这个“?”着实有点多,所以当我们入门kotlin后又进化了一下代码:

fun ttt() {
    testDialog?.let { 
        it.show()
//        it.xxx
//        it.yyy
//        it.zzz
    }
}

但是当我们使用很多成员变量的时候又出现了新的头疼事:

private var testDialog: Dialog? = null
private var test2: D2? = null
private var test3: D3? = null
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    testDialog = Dialog(this)
    //…一些设置
    test2 = D2()
    //…一些设置
    test3 = D3()
    //…一些设置
}
fun ttt() {
    testDialog?.let { d ->
        test2?.let { t2 ->
            test3.let { t3 ->
//                …
            }
        }
    }
}

很明显,我们进入了非空判断地狱。当然这这是表象之一,有更大的潜在危害:随着更多的成员变量加入,我们根本无从知道哪些是真的需要非空判断哪些不需要非空判断,都使用“?.let”将出现更头疼的理解地狱。

初级进阶

当然Kotlin也早就想到了这种情况,所以“lateinit”应运而生,于是我们又进化了一次代码:

private lateinit var testDialog: Dialog
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    testDialog = Dialog(this)
}
fun ttt() {
    testDialog.show()
//    …
}

这样似乎回到了干净又清爽的新时代。但正当自我沉浸时,突然有一次爆出了“kotlin.UninitializedPropertyAccessException: lateinit property testDialog has not been initialized”。

根据堆栈信息很明显成员变量没初始化却先被调用了,这时才发现使用“lateinit”的成员变量对于空安全形同虚设,已经没有任何鸟用了。更细心的你发现,就连set都可以多次调用,想怎么改就怎么改——我们似乎又回到了Java时代。(在此处特别建议在任何地方都慎用lateinit!慎用lateinit!慎用lateinit!最好不用。)

高阶功法

经过一番搜寻“lazy”终于给了答案。首先它是val类型,这样就不怕被无故修改,并且它在调用时才初始化,在正确的代码调用下完全不怕activity相关生命周期问题。于是代码又进化了:

private val testDialog by lazyNone {//自己写的lazy无锁拓展
    Dialog(this).apply {
//        …一些设置
    }
}
fun ttt() {
    testDialog.show()
//    …
}

这似乎解决了所有问题,但……

当感觉完美解决问题的正嗨皮奔放时,测试突然发来个bug:几个tab页切换,很多有些操作或数据还是旧的……

这时突然想起来:在Fragment切换时,本身实例不一定会被销毁,而仅仅调用的onDestroyView而已。

此时想改却发现lazy只能是val类型……

终极目标

点开lazy代码,发现lazy其实是一个接口,仔细观察lazy实际上就是调用接口的value成员变量,这是似乎有一个好的想法:在Fragment destroy view时将内部成员变量重置,当再次调用的时候完全可以自行重新初始化了。于是有个大胆的想法:

private object UNINITIALIZED_VALUE

/**
 * 跟着ui的生命周期走,当destroy时会销毁,当再次create时会创建新的
 */
class UILazyDelegate<out T>(
    private val ui: IUIContext,//这里相当于activity或fragment,可自行拆成activity和fragment
    private val initializer: () -> T
) : Lazy<T>, Serializable {
    private var _value: Any? = UNINITIALIZED_VALUE

    override val value: T
        get() {
            if (!isInitialized()) {
                _value = initializer.invoke()
                ui.doOnDestroyed {//相当于监听destroy后移除监听
                    _value = UNINITIALIZED_VALUE
                }
                if (!isInitialized()) {
                    throw IllegalStateException("不允许在super.onDestroy后调用")
                }
            }
            return _value as T
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String =
        if (isInitialized()) value.toString() else "${initializer}:当前lazy尚未初始化"
}

这真的是终极解决方案?

当然不是,因为有了新的崩溃:“Method addObserver must be called on the main thread”。

我们的activity、fragment基本都在主线程操作,所以初始化时肯定都是主线程了。但lazy是延时加载,哪个线程第一次调用就会在哪个线程初始化,所以如果一个子线程先调用了,那崩溃就必不可少了。

很显然,解决方案就是:调之前回到主线程。当然这也不乏是个相对合适的解决方案,但因为我们写的是工具,对于外包来说整块代码都是隐藏的,使用者又不仅仅是你自己,想让所有人以后都不再出现这种情况,对于这种解决方案就有点草率了(博主认为,能用代码解决的就不要再多口舌了,人总有出错但代码却不曾失误)。

终极无修改版

反过来想:让lazy主动回到主线程,这样调用者就无需考虑自己是否处于哪个线程了。

那么问题来了:子线程必须要同步结果,这让我想起了“notify”、“wait”、“sleep”、“try”,想想代码都痛苦。

在kotlin里有没有一种功能可以无缝切换线程呢?很明显“协程”就是来拯救线程间切换的。只顾着看“launch”怎么用的了,似乎还没注意到有一个叫“runBlocking”的方法。

带线程切换的最终版应运而生:

private object UNINITIALIZED_VALUE

/**
 * 跟着ui的生命周期走,当destroy时会销毁,当再次create时会创建新的
 */
class UILazyDelegate<out T>(
    private val ui: IUIContext,
    private val initializer: () -> T
) : Lazy<T>, Serializable {
    private var _value: Any? = UNINITIALIZED_VALUE

    override val value: T
        get() {
            if (!isInitialized()) {
                if (isMainThread()) createForMain() else runBlocking(Dispatchers.Main) { createForMain() }
                if (!isInitialized()) {
                    throw IllegalStateException("不允许在super.onDestroy后调用")
                }
            }
            return _value as T
        }

    private fun createForMain() {
        _value = initializer.invoke()
        ui.doOnDestroyed {
            _value = UNINITIALIZED_VALUE
        }
    }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String =
        if (isInitialized()) value.toString() else "${initializer}:当前lazy尚未初始化"
}

当然也少不了加个拓展以方便使用:

/**
 * 根据生命周期自动管理初始化
 * 注意:请在super.onCreate后和super.onDestroy前使用
 * 注意:可以在子线程调用,调用期间会强制阻塞子线程
 */
@Suppress("NOTHING_TO_INLINE")
inline fun <T> IUIContext.lazyWithUI(noinline initializer: () -> T) = UILazyDelegate(this, initializer)
//IUIContext:activity或fragment或其他,可自行拆成两个方法

使用示例:

private val testDialog by lazyWithUI {
        Dialog(this)
    }

相关推荐

  1. kotlin by lazy 使用

    2024-05-14 11:14:07       57 阅读
  2. MySQL存储过程(变量

    2024-05-14 11:14:07       49 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-05-14 11:14:07       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-05-14 11:14:07       106 阅读
  3. 在Django里面运行非项目文件

    2024-05-14 11:14:07       87 阅读
  4. Python语言-面向对象

    2024-05-14 11:14:07       96 阅读

热门阅读

  1. 蓝桥杯-移动距离(最简单的写法)

    2024-05-14 11:14:07       30 阅读
  2. 你眼中的IT行业现状与未来趋势

    2024-05-14 11:14:07       27 阅读
  3. 谈谈std::map的lower_bound

    2024-05-14 11:14:07       30 阅读
  4. Linux-vi/vim

    2024-05-14 11:14:07       30 阅读
  5. CentOS常见的命令

    2024-05-14 11:14:07       32 阅读
  6. 水利行业工程设计资质如何去申请

    2024-05-14 11:14:07       32 阅读
  7. vue预览PDF文件的方法

    2024-05-14 11:14:07       28 阅读
  8. Flink Stream API实践

    2024-05-14 11:14:07       28 阅读
  9. 13.复习1笔记

    2024-05-14 11:14:07       28 阅读
  10. Gitee使用教程

    2024-05-14 11:14:07       32 阅读