1

I have made a home screen widget with compose glance.

It works perfectly on API 32.

But for all API < 32, the widget is not updating when new preferences/state are updated.

What I have tried :

Understanding deeper the way state preferences interact with GlanceWidget to interact with the data, but it seems I made my best...

Here is my GlanceAppWidget()

class CrTWidget : GlanceAppWidget() {

    override val stateDefinition: GlanceStateDefinition<*> = PreferencesGlanceStateDefinition
    //override val stateDefinition = TickerInfoStateDefinition

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        //Log.v("GlanceAppWidget", "provideGlance")
        provideContent {
            //we're in composable context
            val prefs = currentState<Preferences>()
            var prefsMap =  prefs.asMap()
            //val prefs = currentState<TickerInfo>()
            // create your AppWidget here
            Log.v("GlanceAppWidget", "provideContent")
            MyContent(prefsMap)
        }
    }
    @Composable
    private fun MyContent(mapTickerz : Map<Preferences.Key<*>, Any>) {
        //https://developer.android.com/jetpack/compose/glance/glance-app-widget
        val listTickerz = mutableListOf<String>()
        mapTickerz.forEach{
            listTickerz.add(it.key.toString())
            //Log.v("GlanceAppWidget", "KEY FOUND IS IN PREFS MAP IS ${it.key}")
        }
        Log.v("GlanceAppWidget","PREFS MAP IS $listTickerz")
    }
    SomeCompositionWithmapTickerz(mapTickerz)
}

& GlanceAppWidgetReceiver()

class CrTWidgetReceiver : GlanceAppWidgetReceiver() {
    override val glanceAppWidget: GlanceAppWidget = CrTWidget()

    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)
        Log.v("CrTWidgetReceiver", "onUpdate GlanceAppWidgetReceiver")
        observeData(context)
    }

    override fun onReceive(context: Context, intent: Intent) {
        super.onReceive(context, intent)
        Log.v("CrTWidgetReceiver", "onReceive GlanceAppWidgetReceiver + ${intent.action}")
        if(intent.action == context.packageName + ".UpdateWIDGET"){
            observeData(context)
        }
        /*if(intent.action == "android.appwidget.action.APPWIDGET_UPDATE"){
            observeData(context)
        }*/

    }

    private fun observeData(context: Context) {
        try{
            Log.v("CrTWidgetReceiver", "observeData STARTED")
            val applicationScope = CoroutineScope(SupervisorJob())
            // Using by lazy so the database and the repository are only created when they're needed
            // rather than when the application starts
            val database by lazy { DatabasePT.getDatabase(context, applicationScope) }
            val repository by lazy { RepositoryPT(database.CrTickerDAO(), database.CrPairsDAO()) }
            var mapTickers: MutableMap<String, CrTickerModel> = mutableMapOf()

            //fetch data from repo
            var firstFlowReached = false
            CoroutineScope(SupervisorJob()).launch {
                repository.getAllDataSymbol.takeWhile { !firstFlowReached }.collect {syms ->
                    //Log.v("CrTWidgetReceiver", "collect OCCURENCE")
                    syms.forEach {symbol ->
                        mapTickers[symbol.cryptoTickName] = symbol
                    }
                    GlanceAppWidgetManager(context).getGlanceIds(CrTWidget::class.java).forEach { glanceId ->
                        updateAppWidgetState(context, glanceId) { pref ->
                            pref.clear()
                            mapTickers.forEach { symbol ->
                                Log.v("CrTWidgetReceiver", "LAST PRICE OF ${symbol.value.cryptoTickSymbol} IS ${symbol.value.cryptoTickQuote}")
                                val tick = symbol.value.cryptoTickSymbol
                                val tickQuoter = symbol.value.cryptoTickQuoter
                                val price = symbol.value.cryptoTickQuote
                                val source = symbol.value.cryptoTickSource
                                pref[stringPreferencesKey(symbol.value.cryptoTickName)] = "$tick¤$tickQuoter¤$price¤$source"
                            }
                        }
                        CrTWidget().update(context, glanceId)
                    }
                    firstFlowReached = true
                    Log.v("CrTWidgetReceiver", "EXITING COLLECT")
                }
            }
        }catch (e : Exception){
            var stringWriter = StringWriter()
            e.printStackTrace(PrintWriter(stringWriter))
            Log.e("CrTWidgetReceiver", "observeData ERROR $stringWriter")
        }
    }

}

After reading

Google Tutorial

Google Developper Infos

This amazing tutorial

I still don't find the way to make it work :( If anyone can give me an hint ;)

EDIT

After trying @summers-pittman proposition and @marcel explainations, I ended up using this :

class CrTWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        val applicationScope = CoroutineScope(SupervisorJob())
        // Using by lazy so the database and the repository are only created when they're needed
        // rather than when the application starts
        val database by lazy { DatabasePT.getDatabase(context, applicationScope) }
        val repository by lazy { RepositoryPT(database.CrTickerDAO(), database.CrPairsDAO()) }

        provideContent {
            var watchTickerState = repository.getAllDataSymbol.collectAsState(initial = listOf(
                CrTickerModel(cryptoTickTimestamp = "0", cryptoTickName = "EMPTY", cryptoTickNameAPI = "EMPTY", cryptoTickQuote = "EMPTY", cryptoTickQuoter = "EMPTY", cryptoTickSource = "BINANCE", cryptoTickSymbol = "EMPTY")
            ))
            //we're in composable context
            val prefs = currentState<Preferences>()
            var prefsMap =  prefs.asMap()
            MyContent(watchTickerState)
        }
    }
    @Composable
    private fun MyContent(watchTickerState: State<List<CrTickerModel>>) {



        //https://developer.android.com/jetpack/compose/glance/glance-app-widget
        watchTickerState.value.let { modelList ->
            Log.v("recomposed", modelList.toString())
            SomeCompositionWithList(modelList)
        }
    }
}

With this example, I even don't need to call CrTWidget().updateAll(context) to update the state of the widget as it does automatically with var watchTickerState = repository.getAllDataSymbol.collectAsState()

But the problem is the same, it works perfectly on API>32 but on others it stop updating after the first update...

HSMKU
  • 81
  • 10

1 Answers1

1

I would recommend getting rid of GlanceStateDefinition. Inside of your provideGlance method you can reference the repository and database and load the values from there like you would in a View-based composable. For clarity you will probably want to move the initializers for the repository and database into your Application class and reference them from your widget's provideGlance method. Once you've done this your can remove your onReceive and onUpdate methods from your receiver class.

The Android docs describe how this works in more detail. Also, take a look at the JetNews Glance widget to see an example of this pattern.

  • No difference from using it in `GlanceAppWidgetReceiver()` except some concurrency that fail data load sometimes. Also this is not the suited design for GlanceWIdget as explained here https://stackoverflow.com/a/72282212/11173029. Seems that the UI < api 32 don't respond to `CrTWidget().update(context, glanceId)` method.... – HSMKU Aug 02 '23 at 09:55
  • This is not the case as of beta 01. As a very high level explanation, when the widget is updated, Glance creates a worker instance and you can subscribe to flows while the widget is being rendered. Once the widget is rendered and the worker ends the flows end. Please check the JetNews example. – Summers Pittman Aug 02 '23 at 14:06
  • @SummersPittman is correct. That answer was before Glance Beta01. Currently is better to use your own persistent mechanism and consum flows/livedata or use side effects inside your glance-composables. Similar to the way you would do in compose. Check the updated sample https://github.com/android/platform-samples/tree/main/samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/weather – Marcel Aug 02 '23 at 14:24
  • I will try right now. What I understand from this sample is that : `val weatherInfo by WeatherRepo.currentWeather.collectAsState()` in `class WeatherGlanceWidget : GlanceAppWidget()` trigger recomposition when the state is changing. Means If I update that state from `Loading` to `Available` from somewhere in my APP, the widget recomposition is triggered ? – HSMKU Aug 04 '23 at 07:03
  • Please have a look on my ***EDIT***, I really don't know what to do... – HSMKU Aug 04 '23 at 08:08
  • What is weird is that for the first recomposition, the widget is updating perfectly. But once the first recomposition is done, the others don't work... And it's the same behavior from API 23 to API 31, then API >31 it works for every recomposition. – HSMKU Aug 04 '23 at 08:28
  • 1
    Are you using Glance RC1? This sounds like a [bug](https://issuetracker.google.com/issues/240300611) that was supposed to be fixed in that release. – Summers Pittman Aug 04 '23 at 20:04
  • Men Incredible !!! It was the BUG !!! Moving LazyColumn from the top root to inner Box() made the job !!! Now it works !!! Congrats. – HSMKU Aug 07 '23 at 19:34