androidkotlinlibgdxdatastorekotlin-flow

How do I wait until data from Preferences DataStore is loaded?


I'm trying to make a LibGDX Live Wallpaper. But I think that is unrelated to the problem I'm having at the moment. Basically, I'm storing the wallpaper's settings inside a Preferences DataStore. Now, if you need to retrieve data from the DataStore, you need to retrieve a Kotlin Flow and use it to get the preferences. Inside LibGDX's ApplicationListener inside the onCreate() method, I need to create a KtxScreen object. I decided to pass all the preferences inside a constructor of my Screen. However, since a Flow's data can only be obtained inside a coroutine, I cannot construct my KtxScreen object with the user preferences as a constructor parameter because I need to wait until my Flow emits the values. I solved this with a very very dirty while (userPreferences == null) Unit piece of code which basically blocks the whole app until the preferences are emitted so I can construct my KtxScreen. What would be a good way to solve this?


Solution

  • It is acceptable in libGDX to block the rendering thread. This is a separate thread from the Android main thread, so it won't freeze the Android UI or put you at risk of an ANR (application not responding error). It will freeze any game/wallpaper rendering while blocking, but that's OK when you're still loading the scene.

    So, it would be acceptable to use runBlocking in create() or render() to wait for the first batch of settings. Since KtxScreen doesn't use create(), you could block directly at the property initialization. You can call first() on a Flow to get just one item from it, which would be fine for initial setup of your KtxScreen class.

    If you're using the ktx-async library from libKtx, you can collect your flow on the rendering thread, so then if the user updates the settings while your wallpaper is running, it will safely update your settings.

    Here's one example of how you might achieve this (I didn't test it), but there are many acceptable different ways it could be handled. Note: this is assuming you instantiate this screen on the rendering thread, not the Android main thread. The rendering thread is what is used to call create() on your game class, which is the typical place to instantiate your first screen.

    class MyScreen(
        val settingsFlow: Flow<UserPreferences>
    ): KtxScreen {
    
        var prefs: UserPreferences = runBlocking { settingsFlow.first() }
    
        init {
            settingsFlow
                .drop(1) // the first one should be the value we already got above
                         // unless the user changed them impossibly fast
                .onEach { prefs = it }
                .launchIn(KtxAsync)
        }
    
        //...
    }
    

    If you want to show some sort of initial loading image in your wallpaper, you could use if/else in your render function like this. But I don't think it should be necessary because preferences will take probably less than half a second to load.

    class MyScreen(
        val settingsFlow: Flow<UserPreferences>
    ): KtxScreen {
    
        var _prefs: UserPreferences? = null
    
        init {
            settingsFlow
                .onEach { _prefs = it }
                .launchIn(KtxAsync)
        }
    
        override fun render() {
            val prefs = _prefs // for the sake of smart-casting
            if (prefs == null) {
                renderLoadingImage()
            } else {
                renderScene(prefs)
            }
        }
    
        //...
    
        private fun renderScene(prefs: UserPreferences) {
            //...
        }
    }