https://kotlinlang.org logo
#language-proposals
Title
# language-proposals
s

Sanjeeviraj M

03/13/2023, 3:10 PM
Lazy properties with an option to recompute, reset - ResettableLazy Use-case - we used to have a nullable variable and nullify user session specific values like user data, API client, etc.. When ResettableLazy is introduced, we can reduce few lines of code and avoid overhead of synchronization Please see the below code and share suggestions
Copy code
class Example {
    val loggedInUser: ResettableLazy<User> = resettableLazy {
        getUserFromDB()
    }

    fun logout() {
        loggedInUser.reset()
    }

    fun showUserInfo() {
        userNameText.text = loggedInUser.value.name
    }
}

object UninitializedValue

fun <T> resettableLazy(initializer: () -> T): ResettableLazy<T> = ResettableLazyImpl(initializer)
fun <T> resettableLazy(lock: Any? = null, initializer: () -> T): ResettableLazy<T> = ResettableLazyImpl(initializer)

fun <T> resettableLazyUnSynchronized(initializer: () -> T): ResettableLazy<T> = ResettableLazyUnSynchronizedImpl(initializer)

interface ResettableLazy<T> {
    val value: T

    fun isInitialized(): Boolean
    fun reset()
}

private class ResettableLazyImpl<T>(private val initializer: () -> T, lock: Any? = null) :
    ResettableLazy<T> {
    /**
     * This is an extended version of Kotlin Lazy property [kotlin.SynchronizedLazyImpl]
     * calling reset() will set UninitializedValue
     * if the values are used after reset() call, the value will be initialised again
     */
    @Volatile private var _value: Any? = UninitializedValue
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            var tempValue = _value
            if (tempValue !== UninitializedValue) {
                @Suppress("UNCHECKED_CAST")
                return tempValue as T
            }

            return synchronized(lock) {
                tempValue = _value
                if (tempValue !== UninitializedValue) {
                    @Suppress("UNCHECKED_CAST") (tempValue as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    typedValue
                }
            }
        }

    override fun reset() {
        synchronized(lock) {
            _value = UninitializedValue
        }
    }

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

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
}

private class ResettableLazyUnSynchronizedImpl<T>(private val initializer: () -> T) :
    ResettableLazy<T> {
    /**
     * This is a downgraded version of Kotlin Lazy property [ResettableLazyImpl], use it if everything happens in main thread
     */
    private var _value: Any? = UninitializedValue

    override val value: T
        get() {
            if (_value === UninitializedValue) {
                _value = initializer()
            }
            @Suppress("UNCHECKED_CAST")
            return _value as T
        }

    override fun reset() {
        _value = UninitializedValue
    }

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

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
}
a

Andreas Scheja

03/13/2023, 7:40 PM
Just to get a better idea: What is the life-cycle of your
Example
class? From where does
getUserFromDB
know the user to load from db? From the looks of it right now it would have to be created with some outer context that would supply this function, but this would also mean that it is inherently bound to exactly one user - so why keep the object around when the user is logged out? One would think that if the user logged out the session will be invalid, but calling
showUserInfo
would re-initialize the
ResettableLazy
and as such "log-in" your user again?
s

Sanjeeviraj M

03/13/2023, 10:11 PM
Well, this is not taken from a real project. It doesn't have answers for those questions. Basically it is a simplified version of the below example
Copy code
class Example {
    private var loggedInUserNullable: User? = null
    val loggedInUser: User
    get() {
        if (loggedInUserNullable != null) {
            return loggedInUserNullable!!
        }
        synchronized(this) {
            if (loggedInUserNullable == null) {
                loggedInUserNullable = getUserFromDB()
            }
            return loggedInUserNullable!!
        }
    }

    fun logout() {
        loggedInUserNullable = null
    }

    fun showUserInfo() {
        userNameText.text = loggedInUser.value.name
    }
}
---
Sharing few examples used in real Android project -
Copy code
// retrofitClient will be used in lots of places
// the client should be reinstantiated when the user logs out
// it should be synchronized

object ApiClient {
    private val retrofitClient: ResettableLazy<Retrofit> = resettableLazy {
        Retrofit.Builder()
            .baseUrl(IAMUtil.getBaseUrl())
            .client(getOkHttpClient())
            .addConverterFactory(
                MoshiConverterFactory.create(
                    Moshi.Builder().add(JSONObjectAdapter).build()
                )
            )
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build()
    }

    fun getClient(): Retrofit {
        return retrofitClient.value
    }

    fun refresh() {
        retrofitClient.reset()
    }
}
---
Copy code
object Logger {
    private val logFile: ResettableLazy<File> = resettableLazy {
        getOrCreateFile("APILogs.txt")
    }

    fun logV(content: String) {
        // append is an extension function of File
        logFile.value.append("V: $content")
    }

    fun logD(content: String) {
        logFile.value.append("D: $content")
    }

    fun refresh() {
        // This method will be called when logging out.
        // Before calling this function, the parent directory would have been deleted
        // resetting the logFile value will create new file on next logFile access
        apiLogFile.reset()
    }
}
---
Copy code
// Need to observer push count and change UI
// observer need to be removed on lifecyle end
// here the view can be attached and detached multiple times
// removing old scope, cancelling it and creating new scope everytime when needed
// everything happens in main thread. so using UnSynchronized ResettableLazy version
class MenuView : FrameLayout {
    private val coroutineScope: ResettableLazy<CoroutineScope> = resettableLazyUnSynchronized {
        CoroutineScope(Dispatchers.Main)
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        // launchAndCollect is an extension function
        Util.pushCountFlow<Int>.launchAndCollect(
            scope = coroutineScope.value,
            context = Dispatchers.Main
        ) { count ->
            pushCountView.text = count.toString()
        }

        Util.searchPermissionFlow<Boolean>.launchAndCollect(
            scope = coroutineScope.value,
            context = Dispatchers.Main
        ) { isSearchEnabled ->
            searchIconView.isVisible = isSearchEnabled
        }
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        coroutineScope.value.cancel()
        coroutineScope.reset()
    }
}
---
Copy code
object RemoteConfig {
     val isFullScreenFeatureEnabled: ResettableLazy<Boolean> = resettableLazyUnSynchronized {
        // reads value from remote config preferences file
        // return respective value
        return@resettableLazyUnSynchronized isUnSupported
    }

    // will be called whenever remote config preferences is updated
    fun refreshCondition() {
        isFullScreenFeatureEnabled.reset()
    }
}
a

Andreas Scheja

03/15/2023, 7:43 PM
To me most of these examples look like you could just move the ability to reset to a different place, i.e. instead of having an object like RemoteConfig you should rather have a class RemoteConfig and whatever would decide to reset the RemoteConfig would just overwrite that instance with a new instance? Also for the android case there is https://developer.android.com/topic/libraries/architecture/coroutines#lifecyclescope?
s

Sanjeeviraj M

03/22/2023, 10:12 PM
There are lots of ways to solve this use-case. We had these cases in our project and created this class to solve it. It works quite well with less no of lines. No need to add synchronisation everywhere. It does the synchronisation internally, just like lazy variable. I understand a language shouldn't be bloated with features. But there are lots of stack overflow answers and github snippets available to achieve the same behaviour. Thought this class might be helpful to lots of people if added to Kotlin itself. As an extended version of Lazy. By the way, answers for your queries - 1. The use-case is, compute only if necessary, Rewriting the instance does the computing even if it is not needed. 2. AndroidX SDK provides reliable lifeCycleScope for Fragment and Activity. When it comes to View, the solution they provide will work differently and it won't be suitable for all cases. This is the method provided in AndroidX SDK
Copy code
findViewTreeLifecycleOwner().lifecycleScope
It either returns the Activity scope or Fragment root view scope which won't be suitable for some cases. In the above MenuView example, using findViewTreeLifecycleOwner().lifecycleScope will leak memory. The scope of menu view is smaller than Activity. Even after MenuView is removed from the view hierarchy, the MenuView won't be Garbage collected because of parent lifecycle scope
j

Joffrey

03/29/2023, 11:49 AM
Note that this doesn't seem to be a request for a language feature, but rather a request for a #stdlib class/function
👍 1
211 Views