How to observe Flow in Broadcast Receiver? - android

I have an app widget implemented using shared preferences.
Now I am working on its migration to Data Store.
The problem here is how can I observe/collect the Flow data in AppWidgetProvider (a subclass of BroadCastReceiver)?
Minimum Code to reproduce the issue.
MyAppWidgetProvider:
class MyAppWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
}
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
RemoteViews(context.packageName, R.layout.widget_layout).also { views ->
val data = loadDataFromPreferences(context, appWidgetId)
views.setTextViewText(
R.id.textview_title,
data.title
)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
DataStoreUtil:
internal fun loadDataFromPreferences(context: Context, appWidgetId: Int): Flow<Data> {
val dataStore: DataStore<Preferences> = context.createDataStore(
name = PREFS_NAME,
migrations = listOf(SharedPreferencesMigration(context, PREFS_NAME))
)
val PREF_TITLE = stringPreferencesKey(PREF_PREFIX_KEY + appWidgetId + PREF_SUFFIX_TITLE)
return dataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}
.map { preferences ->
// No type safety.
val title = preferences[PREF_TITLE] ?: ""
Data(title)
}
}
Note:
Data - A custom model class
loadDataFromPreferences() return type was Data when using Shared Preferences. Changed it to Flow<Data> for DataStore which causes error in updateAppWidget() in the line :
val data = loadDataFromPreferences(context, appWidgetId) - as the data type has changed to Flow.

You can use collect to get the data from Flow
val result = loadDataFromPreferences(context, appWidgetId)
CoroutineScope(Dispatchers.Main).launch{
result.collect{ data ->
views.setTextViewText(
R.id.textview_title,
data.title
}
}

Thanks to rajan kt's answer.
Started with the CoroutineScope(Dispatchers.Main).launch which was the main aspect of the solution.
But, collect() didn't work as expected. Tried with first() instead and it solved the issue.
Posting the working code below:
MyAppWidgetProvider:
class MyAppWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
CoroutineScope(Dispatchers.Main).launch {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
}
}
internal suspend fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
RemoteViews(context.packageName, R.layout.widget_layout).also { views ->
loadDataFromPreferences(context, appWidgetId)
.first {
views.setTextViewText(
R.id.textview_title,
data.title
)
appWidgetManager.updateAppWidget(appWidgetId, views)
true
}
}
}
DataStoreUtil:
internal suspend fun loadDataFromPreferences(context: Context, appWidgetId: Int): Flow<Data> {
val dataStore: DataStore<Preferences> = context.createDataStore(
name = PREFS_NAME,
migrations = listOf(SharedPreferencesMigration(context, PREFS_NAME))
)
val PREF_TITLE = stringPreferencesKey(PREF_PREFIX_KEY + appWidgetId + PREF_SUFFIX_TITLE)
return dataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}
.map { preferences ->
// No type safety.
val title = preferences[PREF_TITLE] ?: ""
Data(title)
}
}

Related

Getting an error even after callling suspend function from coroutine scope

I have an async process in my android app and I am using a co-routine scope to call a suspend function. But I am getting this error.
Suspend function 'getByTimeType' should be called only from a
coroutine or another suspend function
This is the code giving me error:
class WidgetProvider : HomeWidgetProvider() {
private var job: Job = Job()
private val scope = CoroutineScope(job + Dispatchers.Main)
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
widgetData: SharedPreferences
) {
Log.d("debugging", "widget loaded")
appWidgetIds.forEach { widgetId ->
val views = RemoteViews(context.packageName, R.layout.widget_layout).apply {
val todosRemoteView = RemoteViews.RemoteCollectionItems.Builder()
scope.launch {
val db = Room.databaseBuilder(
context,
AppDatabase::class.java, "db"
).build()
val todosDAO = db.TodoDAO()
val todos: List<Todo> = todosDAO.getByTimeType("day")
}
Log.d("debugging", "update is triggered");
setRemoteAdapter(
R.id.todos_list,
todosRemoteView
.build()
)
}
appWidgetManager.updateAppWidget(widgetId, views)
}
}
}

Android widget OnReceive() being called multiple times when running WorkManager request

I have an app which has a home screen widget via GlanceAppWidget().
I would like to run a worker inside the Content() function of GlanceAppWidget(). I have used enqueue(work) from WorkManager api to successfully execute my worker.
The problem is that onReceive is getting called multiple (infinite) number of times. How can I run the worker once without having onReceive called multiple times?
class MyWidget : GlanceAppWidget() {
#Composable
override fun Content() {
val work = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
WorkManager.getInstance().enqueue(work)
//some composable/ui code that consumes worker output
}
}
class MyWorker(
private val context: Context,
private val workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters) {
override suspend fun doWork(): Result {
return try {
startForegroundService()
//some task here
Result.Success.success(
workDataOf(
"MyKey" to "worker completed successfully"
)
)
} catch (throwable: Throwable) {
Result.failure()
}
}
private suspend fun startForegroundService() {
setForeground(
ForegroundInfo(
Random.nextInt(),
NotificationCompat.Builder(context, "download_channel")
.setSmallIcon(R.drawable.ic_launcher_background)
.setContentText("Downloading...")
.setContentTitle("Download in progress")
.build()
)
)
}
}
class GlanceReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget
get() = MyWidget()
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
}
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
}
}

How to update more than one Remote View in Android Studio Widget

That's my code for the widget
class MovieOfTheDayWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
override fun onEnabled(context: Context) {
// Enter relevant functionality for when the first widget is created
}
override fun onDisabled(context: Context) {
// Enter relevant functionality for when the last widget is disabled
}
}
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
) {
val randomMovieList = Constants.getAllMovies()
val movie: AllMovies
val randomMovieListSize = randomMovieList.size
var randomPosition = Random().nextInt(randomMovieListSize)
movie = randomMovieList[randomPosition]
val idArray = arrayListOf<RemoteViews>()
// Construct the RemoteViews object
val movieName = RemoteViews(context.packageName, R.layout.movie_of_the_day_widget)
movieName.setTextViewText(R.id.seriesMovieNameAndYearWidget, "${movie.movieName} (${movie.date}")
val movieImg = RemoteViews(context.packageName, R.layout.movie_of_the_day_widget)
movieImg.setImageViewResource(R.id.seriesMovieImgWidget, movie.moviePic)
val movieRating = RemoteViews(context.packageName, R.layout.movie_of_the_day_widget)
movieRating.setTextViewText(R.id.seriesMovieRatingWidget, movie.rating)
idArray.add(movieName)
idArray.add(movieRating)
idArray.add(movieImg)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, idArray[0])
appWidgetManager.updateAppWidget(appWidgetId, idArray[1])
appWidgetManager.updateAppWidget(appWidgetId, idArray[2])
}
But the last 3 lines don't work properly, they only the second line works, how to update all of the RemoteViews in the idArray.
I've tried putting it in a for loop and updating i, but it also didn't work.
Is it even the wright thing to call the updateAppWidget function more than once? I've tried passing more than one parameter to the function but it returned an error.
You are correct, appWidgetManager.updateAppWidget() should be called just once.
You need to apply all changes to the same RemoteViews instance, so it should be:
// Construct the RemoteViews object
val remoteViews = RemoteViews(context.packageName, R.layout.movie_of_the_day_widget)
remoteViews.setTextViewText(R.id.seriesMovieNameAndYearWidget, "${movie.movieName} (${movie.date}")
remoteViews.setImageViewResource(R.id.seriesMovieImgWidget, movie.moviePic)
remoteViews.setTextViewText(R.id.seriesMovieRatingWidget, movie.rating)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, remoteViews)

How to create an instance of Room Dao or Repository or Viewmodel in GlanceAppWidget class using Jetpack Compose

I am trying to load list of data in App Widget using jetpack compose and i have stored in Room Local database, how i can retrive the data in GlanceAppWidget class.
You need to work with GlanceAppWidgetReceiver class. You can create coroutine and access your domain layer. After that, you can find your GlanceAppWidget class and send your data your widget class. Please have look this article to full example : https://medium.com/better-programming/android-jetpack-glance-for-app-widgets-bd7a704624ba
#AndroidEntryPoint
class MarketWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = MarketWidget()
private val coroutineScope = MainScope()
#Inject
lateinit var marketInformationUseCase: MarketInformationUseCase
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
observeData(context)
}
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
if (intent.action == MarketRefreshCallback.UPDATE_ACTION) {
observeData(context)
}
}
private fun observeData(context: Context) {
coroutineScope.launch {
val marketInformation =
marketInformationUseCase.getMarketInformation(MarketInformationTimespan.TIMESPAN_1DAYS)
val glanceId =
GlanceAppWidgetManager(context).getGlanceIds(MarketWidget::class.java).firstOrNull()
glanceId?.let {
updateAppWidgetState(context, PreferencesGlanceStateDefinition, it) { pref ->
pref.toMutablePreferences().apply {
this[currentPrice] =
marketInformation.currentPrice
this[changeRate] =
marketInformation.changeRate
this[isChangeRatePositive] =
marketInformation.changeStatus == MarketInformationChangeStatus.POSITIVE
}
}
glanceAppWidget.update(context, it)
}
}
}
companion object {
val currentPrice = stringPreferencesKey("currentPrice")
val changeRate = stringPreferencesKey("changeRate")
val isChangeRatePositive = booleanPreferencesKey("isChangeRatePositive")
}
}

How to update widget Android Studio Kotlin

I am new to Android Studio and my problem is my widget is not updating. Let's say I changed my data JSON. I have read some threads and says that a widget has a time interval of 30mins to refresh. But I have waited for my simulator 30mins and nothing changed. Can someone help me out on this ? Because I want my widget to update atleast 30mins default..
class TestWidget : AppWidgetProvider() {
private val httpClient = AsyncHttpClient()
private var title = String.toString()
private var imageUrl = String.toString()
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
// There may be multiple widgets active, so update all of them
httpClient.get(jsonUrl, object : JsonHttpResponseHandler() {
override fun onSuccess(statusCode: Int, headers: Array<out Header>, response: JSONArray) {
val json = response.getJSONObject(0)
title = json.getString("title")
imageUrl = json.getString("imageUrl")
val views = RemoteViews(context.packageName, R.layout.test_widget)
views.setTextViewText(R.id.text, title)
Picasso.with(context)
.load(imageUrl)
.into(views, R.id.image, appWidgetIds)
}
override fun onFailure(statusCode: Int, headers: Array<out Header>?, throwable: Throwable?, errorResponse: JSONObject?) {
println(throwable?.localizedMessage)
}
})
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
override fun onEnabled(context: Context) {
// Enter relevant functionality for when the first widget is created
}
override fun onDisabled(context: Context) {
// Enter relevant functionality for when the last widget is disabled
}
companion object {
internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager,
appWidgetId: Int) {
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.test_widget)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
OH, I think i have found a solution.
There is a android:updatePeriodMillis on my Widget Info XML. That should do the trick.

Categories

Resources