Skip to content

feat: add newsfeed widget (#384)#1018

Open
MatthewTighe wants to merge 4 commits intospacecowboy:masterfrom
MatthewTighe:add-widget
Open

feat: add newsfeed widget (#384)#1018
MatthewTighe wants to merge 4 commits intospacecowboy:masterfrom
MatthewTighe:add-widget

Conversation

@MatthewTighe
Copy link

@MatthewTighe MatthewTighe commented Jan 7, 2026

This PR adds a newsfeed widget using Jetpack Glance for #384. It:

  • establishes a separate "current feed and tag" for the widget
  • allows customization of the display feed and tag through a settings activity
  • allows refreshing the widget through a "refresh button"

There are a couple areas that could potentially use more polish, but I wanted to get this up to at least start a conversation as to whether it is worth waiting for them.

Areas of potential improvement:

  1. the widget preview never renders, just loads forever
  2. lack of tests - Glance is allegedly unit testable, but because this access LocalContext, it would need to provided by something like Robolectric, which I did not think was worth importing for that alone. there's otherwise very little logic here, but happy to eventually add tests for the Repository etc if preferred
  3. I can't get the "Refreshing" text to actually display for some reason - though it does if I make that the only content. I'll probably keep messing with this at some point
  4. widget isn't resizable

LMK your thoughts on whether any of these are worth baking longer - happy to keep trucking on this but it's obviously been slow going. Assume we will just squash these commits as well?

Screen_recording_20260106_222913.webm

Copy link
Owner

@spacecowboy spacecowboy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for doing this. Apologies for the delay in review, I've been busy IRL.

When trying this I got the following error messages spamming my log and the widget wouldn't show anything:

Error in Glance App Widget
java.lang.IllegalArgumentException: RemoteViews for widget update exceeds maximum bitmap memory usage (used: 22240000, max: 15552000)

Once I commented out the bitmap images of the items it displayed.

So I've added some suggestions to make bitmaps optional to start with. Obviously it wont' fix the crash I saw but at least items without images will be rendered now.

I think overall it's a great start.

Before merging I think it's important to ensure all items are displayed. If it's easier, just disable images to start with maybe? Up to you.

And it needs to be resizable, because I think that is an overall guideline requirement by Google these days?

Comment on lines 395 to 454
@OptIn(ExperimentalCoroutinesApi::class)
fun getCurrentWidgetFeedListItems(): Flow<PagingData<FeedListItem>> =
combine(
currentWidgetFeedAndTag,
feedListFilter,
search,
) { feedAndTag, feedListFilter, search ->
val (feedId, tag) = feedAndTag
FeedListArgs(
feedId = feedId,
tag = tag,
minReadTime = Instant.EPOCH,
newestFirst = true,
filter = feedListFilter,
search = search,
)
}.flatMapLatest {
feedItemStore.getPagedFeedItemsRaw(
feedId = it.feedId,
tag = it.tag,
minReadTime = it.minReadTime,
newestFirst = it.newestFirst,
filter = it.filter,
search = it.search,
)
}

@OptIn(ExperimentalCoroutinesApi::class)
fun getWidgetFeedListItems(): Flow<PagingData<FeedListItem>> =
combine(
currentFeedAndTag,
minReadTime,
currentSorting,
feedListFilter,
search,
) { feedAndTag, minReadTime, currentSorting, feedListFilter, search ->
val (feedId, tag) = feedAndTag
FeedListArgs(
feedId = feedId,
tag = tag,
minReadTime =
when (feedId) {
ID_SAVED_ARTICLES -> Instant.EPOCH
else -> minReadTime
},
newestFirst = currentSorting == SortingOptions.NEWEST_FIRST,
filter = feedListFilter,
search = search,
)
}.flatMapLatest {
feedItemStore.getPagedFeedItemsRaw(
feedId = it.feedId,
tag = it.tag,
minReadTime = it.minReadTime,
newestFirst = it.newestFirst,
filter = it.filter,
search = it.search,
)
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, but there is no search functionality for the widget right?

So no need to include the search query?

Suggested change
@OptIn(ExperimentalCoroutinesApi::class)
fun getCurrentWidgetFeedListItems(): Flow<PagingData<FeedListItem>> =
combine(
currentWidgetFeedAndTag,
feedListFilter,
search,
) { feedAndTag, feedListFilter, search ->
val (feedId, tag) = feedAndTag
FeedListArgs(
feedId = feedId,
tag = tag,
minReadTime = Instant.EPOCH,
newestFirst = true,
filter = feedListFilter,
search = search,
)
}.flatMapLatest {
feedItemStore.getPagedFeedItemsRaw(
feedId = it.feedId,
tag = it.tag,
minReadTime = it.minReadTime,
newestFirst = it.newestFirst,
filter = it.filter,
search = it.search,
)
}
@OptIn(ExperimentalCoroutinesApi::class)
fun getWidgetFeedListItems(): Flow<PagingData<FeedListItem>> =
combine(
currentFeedAndTag,
minReadTime,
currentSorting,
feedListFilter,
search,
) { feedAndTag, minReadTime, currentSorting, feedListFilter, search ->
val (feedId, tag) = feedAndTag
FeedListArgs(
feedId = feedId,
tag = tag,
minReadTime =
when (feedId) {
ID_SAVED_ARTICLES -> Instant.EPOCH
else -> minReadTime
},
newestFirst = currentSorting == SortingOptions.NEWEST_FIRST,
filter = feedListFilter,
search = search,
)
}.flatMapLatest {
feedItemStore.getPagedFeedItemsRaw(
feedId = it.feedId,
tag = it.tag,
minReadTime = it.minReadTime,
newestFirst = it.newestFirst,
filter = it.filter,
search = it.search,
)
}
@OptIn(ExperimentalCoroutinesApi::class)
fun getCurrentWidgetFeedListItems(): Flow<PagingData<FeedListItem>> =
combine(
currentWidgetFeedAndTag,
feedListFilter,
) { feedAndTag, feedListFilter ->
val (feedId, tag) = feedAndTag
FeedListArgs(
feedId = feedId,
tag = tag,
minReadTime = Instant.EPOCH,
newestFirst = true,
filter = feedListFilter,
search = "",
)
}.flatMapLatest {
feedItemStore.getPagedFeedItemsRaw(
feedId = it.feedId,
tag = it.tag,
minReadTime = it.minReadTime,
newestFirst = it.newestFirst,
filter = it.filter,
search = it.search,
)
}
@OptIn(ExperimentalCoroutinesApi::class)
fun getWidgetFeedListItems(): Flow<PagingData<FeedListItem>> =
combine(
currentFeedAndTag,
minReadTime,
currentSorting,
feedListFilter,
) { feedAndTag, minReadTime, currentSorting, feedListFilter ->
val (feedId, tag) = feedAndTag
FeedListArgs(
feedId = feedId,
tag = tag,
minReadTime =
when (feedId) {
ID_SAVED_ARTICLES -> Instant.EPOCH
else -> minReadTime
},
newestFirst = currentSorting == SortingOptions.NEWEST_FIRST,
filter = feedListFilter,
search = "",
)
}.flatMapLatest {
feedItemStore.getPagedFeedItemsRaw(
feedId = it.feedId,
tag = it.tag,
minReadTime = it.minReadTime,
newestFirst = it.newestFirst,
filter = it.filter,
search = it.search,
)
}

Comment on lines 71 to 301
data class FeedWidgetItem(
val id: Long,
val title: String,
val snippet: String,
val feedTitle: String,
val unread: Boolean,
val pubDate: String,
val image: Bitmap,
val link: String?,
val bookmarked: Boolean,
val feedImageUrl: URL?,
val primarySortTime: Instant,
val rawPubDate: ZonedDateTime?,
val wordCount: Int,
)

private fun FeedListItem.toWidgetItem(bitmap: Bitmap) =
FeedWidgetItem(
id,
title,
snippet,
feedTitle,
unread,
pubDate,
bitmap,
link,
bookmarked,
feedImageUrl,
primarySortTime,
rawPubDate,
wordCount,
)

sealed class WidgetState {
data object Syncing : WidgetState()

data class Ready(
val items: PagingData<FeedWidgetItem>,
) : WidgetState()
}

object FeederWidgetGlanceColorScheme {
val colors =
ColorProviders(
light = lightColorScheme(),
dark = darkColorScheme(),
)
}

private class WidgetStateDataStore(
private val context: Context,
) : DataStore<WidgetState> {
private val app = (context.applicationContext as FeederApplication)
private val imageLoader = app.imageLoader
private val loadImageBitmap: suspend (String) -> Bitmap? = { url ->
val request =
ImageRequest
.Builder(context)
.data(url)
.scale(Scale.FIT)
.precision(Precision.INEXACT)
.allowHardware(false)
.build()

(imageLoader.execute(request) as? SuccessResult)?.image?.toBitmap(200, 200)
}

private val repository: Repository by app.di.instance()

override val data: Flow<WidgetState>
get() =
combine(
repository.syncWorkerRunning,
repository
.getCurrentWidgetFeedListItems()
.map { pagingData ->
pagingData
.map { listItem ->
val bitmap = loadImageBitmap(listItem.feedImageUrl?.toString() ?: "")
Pair(listItem, bitmap)
}.filter { it.second != null }
.map {
it.first.toWidgetItem(it.second!!)
}
},
) { syncWorkerRunning, feedWidgetItems ->
if (syncWorkerRunning) {
WidgetState.Syncing
} else {
WidgetState.Ready(feedWidgetItems)
}
}

override suspend fun updateData(transform: suspend (t: WidgetState) -> WidgetState): WidgetState = throw NotImplementedError("Widget does not need to update its own data")
}

class FeedWidget : GlanceAppWidget() {
override val stateDefinition: GlanceStateDefinition<WidgetState>
get() =
object : GlanceStateDefinition<WidgetState> {
override suspend fun getDataStore(
context: Context,
fileKey: String,
): DataStore<WidgetState> = WidgetStateDataStore(context)

override fun getLocation(
context: Context,
fileKey: String,
): File = throw NotImplementedError("Widget does not provide a concrete state file location")
}

override suspend fun provideGlance(
context: Context,
id: GlanceId,
) {
provideContent {
val app = (context.applicationContext as FeederApplication)
val coroutineScope = rememberCoroutineScope()
val runSync = {
val rssLocalSync by app.di.instance<RssLocalSync>()
coroutineScope.launch { rssLocalSync.syncFeeds() }
Unit
}
GlanceTheme(colors = FeederWidgetGlanceColorScheme.colors) {
WidgetContent(
widgetState = currentState(),
runSync = runSync,
)
}
}
}

@Composable
private fun WidgetContent(
widgetState: WidgetState,
runSync: () -> Unit,
) {
Column(
modifier =
GlanceModifier
.fillMaxHeight()
.background(GlanceTheme.colors.background)
.padding(4.dp),
) {
FeederTitleBar(runSync)
when (widgetState) {
is WidgetState.Syncing -> WidgetSyncingContent()
is WidgetState.Ready -> WidgetReadyContent(widgetState.items)
}
}
}

@Composable
private fun FeederTitleBar(runSync: () -> Unit) {
TitleBar(
startIcon = ImageProvider(R.drawable.ic_stat_f),
title = LocalContext.current.getString(R.string.widget_title),
modifier = GlanceModifier.clickable(onClick = actionStartActivity(MainActivity::class.java)),
actions = {
Image(
provider = ImageProvider(R.drawable.ic_stat_sync),
contentDescription = null,
colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface),
modifier = GlanceModifier.clickable(runSync),
)

Spacer(modifier = GlanceModifier.width(8.dp))

Image(
provider = ImageProvider(R.drawable.ic_settings),
contentDescription = "",
colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface),
modifier = GlanceModifier.clickable(actionStartActivity(FeedWidgetSettingsActivity::class.java)),
)
},
)
}

@Composable
private fun WidgetSyncingContent() {
Box(contentAlignment = Alignment.Center) {
Text(
text = LocalContext.current.getString(R.string.widget_refreshing),
style = TextStyle(color = GlanceTheme.colors.onSurface),
)
}
}

@Composable
private fun WidgetReadyContent(data: PagingData<FeedWidgetItem>) {
val items = flowOf(data).collectAsLazyPagingItems()
LazyColumn(modifier = GlanceModifier.padding(start = 12.dp)) {
items(
count = items.itemCount,
itemId = { items[it]?.id ?: 0 },
) { index ->
items[index]?.let {
WidgetCard(it)
}
}
}
}

@Composable
private fun WidgetCard(item: FeedWidgetItem) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(ImageProvider(item.image), contentDescription = null)
Column {
Text(
text = item.title,
maxLines = 1,
style =
TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = GlanceTheme.colors.onSurface,
),
)
Text(
text = item.snippet,
maxLines = 1,
style =
TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
color = GlanceTheme.colors.secondary,
),
)
}
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
data class FeedWidgetItem(
val id: Long,
val title: String,
val snippet: String,
val feedTitle: String,
val unread: Boolean,
val pubDate: String,
val image: Bitmap,
val link: String?,
val bookmarked: Boolean,
val feedImageUrl: URL?,
val primarySortTime: Instant,
val rawPubDate: ZonedDateTime?,
val wordCount: Int,
)
private fun FeedListItem.toWidgetItem(bitmap: Bitmap) =
FeedWidgetItem(
id,
title,
snippet,
feedTitle,
unread,
pubDate,
bitmap,
link,
bookmarked,
feedImageUrl,
primarySortTime,
rawPubDate,
wordCount,
)
sealed class WidgetState {
data object Syncing : WidgetState()
data class Ready(
val items: PagingData<FeedWidgetItem>,
) : WidgetState()
}
object FeederWidgetGlanceColorScheme {
val colors =
ColorProviders(
light = lightColorScheme(),
dark = darkColorScheme(),
)
}
private class WidgetStateDataStore(
private val context: Context,
) : DataStore<WidgetState> {
private val app = (context.applicationContext as FeederApplication)
private val imageLoader = app.imageLoader
private val loadImageBitmap: suspend (String) -> Bitmap? = { url ->
val request =
ImageRequest
.Builder(context)
.data(url)
.scale(Scale.FIT)
.precision(Precision.INEXACT)
.allowHardware(false)
.build()
(imageLoader.execute(request) as? SuccessResult)?.image?.toBitmap(200, 200)
}
private val repository: Repository by app.di.instance()
override val data: Flow<WidgetState>
get() =
combine(
repository.syncWorkerRunning,
repository
.getCurrentWidgetFeedListItems()
.map { pagingData ->
pagingData
.map { listItem ->
val bitmap = loadImageBitmap(listItem.feedImageUrl?.toString() ?: "")
Pair(listItem, bitmap)
}.filter { it.second != null }
.map {
it.first.toWidgetItem(it.second!!)
}
},
) { syncWorkerRunning, feedWidgetItems ->
if (syncWorkerRunning) {
WidgetState.Syncing
} else {
WidgetState.Ready(feedWidgetItems)
}
}
override suspend fun updateData(transform: suspend (t: WidgetState) -> WidgetState): WidgetState = throw NotImplementedError("Widget does not need to update its own data")
}
class FeedWidget : GlanceAppWidget() {
override val stateDefinition: GlanceStateDefinition<WidgetState>
get() =
object : GlanceStateDefinition<WidgetState> {
override suspend fun getDataStore(
context: Context,
fileKey: String,
): DataStore<WidgetState> = WidgetStateDataStore(context)
override fun getLocation(
context: Context,
fileKey: String,
): File = throw NotImplementedError("Widget does not provide a concrete state file location")
}
override suspend fun provideGlance(
context: Context,
id: GlanceId,
) {
provideContent {
val app = (context.applicationContext as FeederApplication)
val coroutineScope = rememberCoroutineScope()
val runSync = {
val rssLocalSync by app.di.instance<RssLocalSync>()
coroutineScope.launch { rssLocalSync.syncFeeds() }
Unit
}
GlanceTheme(colors = FeederWidgetGlanceColorScheme.colors) {
WidgetContent(
widgetState = currentState(),
runSync = runSync,
)
}
}
}
@Composable
private fun WidgetContent(
widgetState: WidgetState,
runSync: () -> Unit,
) {
Column(
modifier =
GlanceModifier
.fillMaxHeight()
.background(GlanceTheme.colors.background)
.padding(4.dp),
) {
FeederTitleBar(runSync)
when (widgetState) {
is WidgetState.Syncing -> WidgetSyncingContent()
is WidgetState.Ready -> WidgetReadyContent(widgetState.items)
}
}
}
@Composable
private fun FeederTitleBar(runSync: () -> Unit) {
TitleBar(
startIcon = ImageProvider(R.drawable.ic_stat_f),
title = LocalContext.current.getString(R.string.widget_title),
modifier = GlanceModifier.clickable(onClick = actionStartActivity(MainActivity::class.java)),
actions = {
Image(
provider = ImageProvider(R.drawable.ic_stat_sync),
contentDescription = null,
colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface),
modifier = GlanceModifier.clickable(runSync),
)
Spacer(modifier = GlanceModifier.width(8.dp))
Image(
provider = ImageProvider(R.drawable.ic_settings),
contentDescription = "",
colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface),
modifier = GlanceModifier.clickable(actionStartActivity(FeedWidgetSettingsActivity::class.java)),
)
},
)
}
@Composable
private fun WidgetSyncingContent() {
Box(contentAlignment = Alignment.Center) {
Text(
text = LocalContext.current.getString(R.string.widget_refreshing),
style = TextStyle(color = GlanceTheme.colors.onSurface),
)
}
}
@Composable
private fun WidgetReadyContent(data: PagingData<FeedWidgetItem>) {
val items = flowOf(data).collectAsLazyPagingItems()
LazyColumn(modifier = GlanceModifier.padding(start = 12.dp)) {
items(
count = items.itemCount,
itemId = { items[it]?.id ?: 0 },
) { index ->
items[index]?.let {
WidgetCard(it)
}
}
}
}
@Composable
private fun WidgetCard(item: FeedWidgetItem) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(ImageProvider(item.image), contentDescription = null)
Column {
Text(
text = item.title,
maxLines = 1,
style =
TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = GlanceTheme.colors.onSurface,
),
)
Text(
text = item.snippet,
maxLines = 1,
style =
TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
color = GlanceTheme.colors.secondary,
),
)
}
}
}
data class FeedWidgetItem(
val id: Long,
val title: String,
val snippet: String,
val feedTitle: String,
val unread: Boolean,
val pubDate: String,
val image: Bitmap?,
val link: String?,
val bookmarked: Boolean,
val feedImageUrl: URL?,
val primarySortTime: Instant,
val rawPubDate: ZonedDateTime?,
val wordCount: Int,
)
private fun FeedListItem.toWidgetItem(bitmap: Bitmap?) =
FeedWidgetItem(
id,
title,
snippet,
feedTitle,
unread,
pubDate,
bitmap,
link,
bookmarked,
feedImageUrl,
primarySortTime,
rawPubDate,
wordCount,
)
sealed class WidgetState {
data object Syncing : WidgetState()
data class Ready(
val items: PagingData<FeedWidgetItem>,
) : WidgetState()
}
object FeederWidgetGlanceColorScheme {
val colors =
ColorProviders(
light = lightColorScheme(),
dark = darkColorScheme(),
)
}
private class WidgetStateDataStore(
private val context: Context,
) : DataStore<WidgetState> {
private val app = (context.applicationContext as FeederApplication)
private val imageLoader = app.imageLoader
private val loadImageBitmap: suspend (String) -> Bitmap? = { url ->
val request =
ImageRequest
.Builder(context)
.data(url)
.scale(Scale.FIT)
.precision(Precision.INEXACT)
.allowHardware(false)
.build()
(imageLoader.execute(request) as? SuccessResult)?.image?.toBitmap(200, 200)
}
private val repository: Repository by app.di.instance()
override val data: Flow<WidgetState>
get() =
combine(
repository.syncWorkerRunning,
repository
.getCurrentWidgetFeedListItems()
.map { pagingData ->
pagingData
.map { listItem ->
val bitmap = loadImageBitmap(listItem.feedImageUrl?.toString() ?: "")
Pair(listItem, bitmap)
}
.map {
it.first.toWidgetItem(it.second)
}
},
) { syncWorkerRunning, feedWidgetItems ->
if (syncWorkerRunning) {
WidgetState.Syncing
} else {
WidgetState.Ready(feedWidgetItems)
}
}
override suspend fun updateData(transform: suspend (t: WidgetState) -> WidgetState): WidgetState = throw NotImplementedError("Widget does not need to update its own data")
}
class FeedWidget : GlanceAppWidget() {
override val stateDefinition: GlanceStateDefinition<WidgetState>
get() =
object : GlanceStateDefinition<WidgetState> {
override suspend fun getDataStore(
context: Context,
fileKey: String,
): DataStore<WidgetState> = WidgetStateDataStore(context)
override fun getLocation(
context: Context,
fileKey: String,
): File = throw NotImplementedError("Widget does not provide a concrete state file location")
}
override suspend fun provideGlance(
context: Context,
id: GlanceId,
) {
provideContent {
val app = (context.applicationContext as FeederApplication)
val coroutineScope = rememberCoroutineScope()
val runSync = {
val rssLocalSync by app.di.instance<RssLocalSync>()
coroutineScope.launch { rssLocalSync.syncFeeds() }
Unit
}
GlanceTheme(colors = FeederWidgetGlanceColorScheme.colors) {
WidgetContent(
widgetState = currentState(),
runSync = runSync,
)
}
}
}
@Composable
private fun WidgetContent(
widgetState: WidgetState,
runSync: () -> Unit,
) {
Column(
modifier =
GlanceModifier
.fillMaxHeight()
.background(GlanceTheme.colors.background)
.padding(4.dp),
) {
FeederTitleBar(runSync)
when (widgetState) {
is WidgetState.Syncing -> WidgetSyncingContent()
is WidgetState.Ready -> WidgetReadyContent(widgetState.items)
}
}
}
@Composable
private fun FeederTitleBar(runSync: () -> Unit) {
TitleBar(
startIcon = ImageProvider(R.drawable.ic_stat_f),
title = LocalContext.current.getString(R.string.widget_title),
modifier = GlanceModifier.clickable(onClick = actionStartActivity(MainActivity::class.java)),
actions = {
Image(
provider = ImageProvider(R.drawable.ic_stat_sync),
contentDescription = null,
colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface),
modifier = GlanceModifier.clickable(runSync),
)
Spacer(modifier = GlanceModifier.width(8.dp))
Image(
provider = ImageProvider(R.drawable.ic_settings),
contentDescription = "",
colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface),
modifier = GlanceModifier.clickable(actionStartActivity(FeedWidgetSettingsActivity::class.java)),
)
},
)
}
@Composable
private fun WidgetSyncingContent() {
Box(contentAlignment = Alignment.Center) {
Text(
text = LocalContext.current.getString(R.string.widget_refreshing),
style = TextStyle(color = GlanceTheme.colors.onSurface),
)
}
}
@Composable
private fun WidgetReadyContent(data: PagingData<FeedWidgetItem>) {
val items = flowOf(data).collectAsLazyPagingItems()
LazyColumn(modifier = GlanceModifier.padding(start = 12.dp)) {
items(
count = items.itemCount,
itemId = { items[it]?.id ?: 0 },
) { index ->
items[index]?.let {
WidgetCard(it)
}
}
}
}
@Composable
private fun WidgetCard(item: FeedWidgetItem) {
Row(verticalAlignment = Alignment.CenterVertically) {
item.image?.let {
Image(ImageProvider(item.image), contentDescription = null)
}
Column {
Text(
text = item.title,
maxLines = 1,
style =
TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = GlanceTheme.colors.onSurface,
),
)
Text(
text = item.snippet,
maxLines = 1,
style =
TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
color = GlanceTheme.colors.secondary,
),
)
}
}
}

It must be possible to display items without images

@MatthewTighe
Copy link
Author

MatthewTighe commented Feb 12, 2026

@spacecowboy thanks so much for taking a look! I've applied your patch feedback.

For the memory crash, I've tried a couple things:

  1. setting a target size to the coil ImageRequest. I doubt this did much, but I'm wondering if perhaps oversized bitmaps were being loaded and then just visually scaled down.
  2. I changed the format used for loading the image to the lowest density one.

That said, I haven't run into the crash in my local testing. Could you either see if you can still reproduce, or perhaps pass along a repro OPML export?

I've kept the patch applying your feedback separate for now, assuming we will squash and merge eventually

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants