Android Jetpack Glance 1.0.0:更新小部件时出现问题

Iri*_*ios 7 android android-widget glance glance-appwidget

我正在为我正在开发的一个简单的待办事项列表应用程序实现我的第一个 Jetpack Glance 支持的应用程序小部件。我按照https://developer.android.com/jetpack/compose/glance上的指南进行操作,一切都很好,直到我需要更新我的小部件以匹配应用程序内发生的数据更新。

根据我对管理和更新 GlanceAppWidget的理解,可以通过从应用程序代码本身调用小部件实例上的updateupdateIf或方法来触发小部件重组。updateAll具体来说,对这些函数的调用应该触发该GlanceAppWidget.provideGlance(context: Context, id: GlanceId)方法,该方法负责获取任何所需的数据并提供小部件内容,如以下代码片段中所述:

class MyAppWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: Context, id: GlanceId) {

        // In this method, load data needed to render the AppWidget.
        // Use `withContext` to switch to another thread for long running
        // operations.

        provideContent {
            // create your AppWidget here
            Text("Hello World")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

但就我而言,它并不总是有效。以下是我在几次尝试后观察到的结果:

  • 第一次将小部件添加到我的仪表板时它总是有效。那里的数据总是新鲜的。
  • 第一次从应用程序本身调用更新方法时它总是有效(我正在使用updateAll)。小部件已更新并显示最新数据,
  • 那么如果我在上次调用后太快调用更新方法,它就不起作用。在这种情况下,该provideGlance方法根本不会被触发。

然后我查看了GlanceAppWidget源代码,发现它依赖于一个AppWidgetSession类:

    /**
     * Internal version of [update], to be used by the broadcast receiver directly.
     */
    internal suspend fun update(
        context: Context,
        appWidgetId: Int,
        options: Bundle? = null,
    ) {
        Tracing.beginGlanceAppWidgetUpdate()
        val glanceId = AppWidgetId(appWidgetId)
        if (!sessionManager.isSessionRunning(context, glanceId.toSessionKey())) {
            sessionManager.startSession(context, AppWidgetSession(this, glanceId, options))
        } else {
            val session = sessionManager.getSession(glanceId.toSessionKey()) as AppWidgetSession
            session.updateGlance()
        }
    }
Run Code Online (Sandbox Code Playgroud)

如果会话正在运行,它将用于触发概览小部件更新。否则它将被启动并用于相同的目的。

我注意到当且仅当只有一个会话正在运行时才会出现我的问题,这可以解释为什么如果我在更新调用之间给它足够的时间就不会发生它:没有更多正在运行的会话(无论它到底意味着什么)和一个新的会话需要创建一个。

我尝试进一步挖掘 Glance 内部结构,以了解为什么它在使用正在运行的会话时不起作用,但到目前为止还没有成功。我注意到并认为奇怪的唯一一件事是,在某些时候,AppWidgetSession内部使用了一个名为 的类GlanceStateDefinition,我在官方 Android Glance 指南中没有看到提及,但网络上的其他一些指南使用它来实现 Glance 小部件(尽管使用 Jetpack Glance 库的 alpha 或 beta 版本)。

有谁知道它为什么会这样?这里有更多信息,如果您需要其他信息,请告诉我。多谢 !

  • 我使用androidx.glance:glance-appwidget几天前发布的 1.0.0 版本的库,
  • 我没有忘记<receiver>在我的 中添加标签AndroidManifest.xml,以及android.appwidget.provider在我的 res/xml 文件夹中添加所需的 xml 文件。我认为我已经正确完成了Glance 设置页面上提到的所有操作,因为我首先在主屏幕上添加小部件没有问题,
  • SQLiteOpenHelper在底层使用我创建的一些辅助类来访问我的数据,而不是使用Room或任何其他 ORM 库(我现在想让我的应用程序保持简单)。
  • 我的方法如下provideGlance
    override suspend fun provideGlance(context: Context, id: GlanceId) {

        val todoDbHelper = TodoDbHelper(context)
        val dbHelper = DbHelper(todoDbHelper.readableDatabase)
        val todoDao = TodoDao(dbHelper)
        val todos = todoDao.findAll()

        provideContent {
            TodoAppWidgetContent(todos)
        }
    }
Run Code Online (Sandbox Code Playgroud)

返回todoDao.findAll()一个简单的列表(它依赖于运行的辅助函数,Dispatchers.IO以便主线程不会被阻塞)

  • 我也不使用Hilt或任何其他 DI 库。

Iri*_*ios 12

我又花了几个小时搜索并找到了答案:

事实证明,我错误地认为在调用任何上述方法时应该再次触发该provideGlance方法,甚至应该再次触发。您可以在其中获取一些初始化数据,但不能依赖它来保持小部件更新,仅当当前没有运行Glance 会话时(首次添加小部件时/添加后经过一段时间时)才会调用它。相反,您可以(/应该)依赖Glance Widget 的状态。provideContentupdate

至少可以说,我认为指南中对“浏览状态”的概念解释得很少,所以我会尝试一下,希望能帮助那些与我有同样问题的人:

  • Glance 会话运行时,每次GlanceAppWidget.update从应用程序内调用任何方法时,Glance 都会使用Glance 状态的新副本重新组合您的小部件内容
  • 必须通过向扩展GlanceAppWidget 的类提供一个实例来预先定义此Glance 状态。该实例负责提供状态的类型(类),以及 Glance将在内部使用来获取Glance 状态的更新版本GlanceStateDefinitionDataStore

A提供DataStoreinterface两种用于获取和更新数据的抽象方法(更多信息参见:DataStore)。提供了 2 个实现:Preferences DataStoreProto DataStore。第一个旨在替换SharedPreferences为存储键值对的方法,第二个可用于存储类型化对象。

我在网上找到的大多数 Glance 教程都在其示例中使用了Preferences DataStore,但出于我的目的,我选择实现我自己的 a 版本DataStore作为我的 Dao 对象的只读代理,如下所示:

class TodoDataStore(private val context: Context): DataStore<List<TodoListData>> {
    override val data: Flow<List<TodoListData>>
        get() {
            val todoDbHelper = TodoDbHelper(context)
            val dbHelper = DbHelper(todoDbHelper.readableDatabase)
            val todoDao = TodoDao(dbHelper)
            return flow { emit(todoDao.findAll()) }
        }

    override suspend fun updateData(transform: suspend (t: List<TodoListData>) -> List<TodoListData>): List<TodoListData> {
        throw NotImplementedError("Not implemented in Todo Data Store")
    }
}
Run Code Online (Sandbox Code Playgroud)

我的类扩展中的状态定义GlanceAppWidget如下所示:

    override val stateDefinition: GlanceStateDefinition<List<TodoListData>>
        get() = object: GlanceStateDefinition<List<TodoListData>> {
            override suspend fun getDataStore(
                context: Context,
                fileKey: String
            ): DataStore<List<TodoListData>> {
                return TodoDataStore(context)
            }

            override fun getLocation(context: Context, fileKey: String): File {
                throw NotImplementedError("Not implemented for Todo App Widget State Definition")
            }
        }
Run Code Online (Sandbox Code Playgroud)

这意味着我现在可以通过使用以下方法来依赖Glance Widget 的状态currentState(),而不是直接使用 Dao 类:

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            TodoAppWidgetContent(currentState())
        }
    }
Run Code Online (Sandbox Code Playgroud)

现在它就像一个魅力!

我打算解决一个问题,即Glance 指南中缺乏有关Glance 状态及其与数据存储概念的关系的文档。