Skip to content

Migrate TemplateWidgetConfigureActivity to Compose#6444

Draft
amlwin wants to merge 7 commits intohome-assistant:mainfrom
amlwin:feat/template-widget-configure-activity-compose
Draft

Migrate TemplateWidgetConfigureActivity to Compose#6444
amlwin wants to merge 7 commits intohome-assistant:mainfrom
amlwin:feat/template-widget-configure-activity-compose

Conversation

@amlwin
Copy link
Copy Markdown

@amlwin amlwin commented Feb 16, 2026

Summary

Migrates the Template Widget configuration screen from XML and Views to Jetpack Compose. This change replaces TemplateWidgetConfigureActivity's XML layout with a new Composable screen, TemplateWidgetConfigureView.

A new TemplateWidgetConfigureViewModel is introduced to manage the screen's state and business logic, including template rendering and widget creation/updating. This also adds unit tests for the new ViewModel to ensure its correctness.

Checklist

  • New or updated tests have been added to cover the changes following the testing guidelines.
  • The code follows the project's code style and best_practices.
  • The changes have been thoroughly tested, and edge cases have been considered.
  • Changes are backward compatible whenever feasible. Any breaking changes are documented in the changelog for users and/or in the code for developers depending on the relevance.

Screenshots

No Ui changes
Screenshot 2026-02-16 at 11 28 19 AM

Link to pull request in documentation repositories

User Documentation: home-assistant/companion.home-assistant#

Migrates the Template Widget configuration screen from XML and Views to Jetpack Compose. This change replaces `TemplateWidgetConfigureActivity`'s XML layout with a new Composable screen, `TemplateWidgetConfigureView`.

A new `TemplateWidgetConfigureViewModel` is introduced to manage the screen's state and business logic, including template rendering and widget creation/updating. This also adds unit tests for the new ViewModel to ensure its correctness.
Copy link
Copy Markdown

@home-assistant home-assistant bot left a comment

Choose a reason for hiding this comment

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

Hi @amlwin

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@home-assistant home-assistant bot marked this pull request as draft February 16, 2026 03:30
@home-assistant
Copy link
Copy Markdown

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

Copy link
Copy Markdown
Member

@TimoPtr TimoPtr left a comment

Choose a reason for hiding this comment

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

Thanks for your work 🎖️, some comments but overall it looks ok.
Would you mind adding a screenshot test too to your compose screen, it should be rather easy?

Comment thread .idea/kotlinc.xml
Comment on lines -9 to -12
<component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="2.3" />
<option name="languageVersion" value="2.3" />
</component>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should have not be committed but I'm fine about keeping it since I think it became obsolete.

* Loads existing widget configuration if editing an existing widget.
*/
fun onSetup(widgetId: Int, supportedTextColors: List<String>) {
if (this.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID) return
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In which scenario this could happen?

Copy link
Copy Markdown
Author

@amlwin amlwin Feb 18, 2026

Choose a reason for hiding this comment

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

i'm going to remove that line coz view (activity) already have these check.

if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID && !requestLauncherSetup) {
            finish()
            return
        }

@TimoPtr TimoPtr marked this pull request as draft February 16, 2026 11:45
Comment on lines -229 to -234
templateText = getString(
if (e.cause is SerializationException) {
commonR.string.template_error
} else {
commonR.string.template_render_error
},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it is a good idea to keep this logic that was giving a bit more details to the user making the template on what is the current error.

… move logic to the ViewModel (home-assistant#6454)

* Extract `TemplateWidgetConfigureScreen` and `TemplateWidgetConfigureView` to a separate file
* Migrate configuration state (server, template text, text size, background type) to a single `TemplateWidgetConfigureUiState` managed by `MutableStateFlow`
* Move widget pinning and update logic from the Activity to the ViewModel
* Improve template rendering error handling with a new `TemplateRenderError` enum and better exception catching
* Update tests to reflect state management and logic changes
* Remove obsolete SDK checks and `Action` sealed class

---------

Co-authored-by: Joris Pelgröm <jpelgrom@users.noreply.github.com>
@amlwin
Copy link
Copy Markdown
Author

amlwin commented Feb 18, 2026

Changes Summary

1. TemplateWidgetConfigureViewModel.kt - Major refactor:

  • MVI with StateFlow: Replaced all mutableStateOf properties with a single TemplateWidgetConfigureUiState data class exposed via StateFlow (no more Compose-specific state in ViewModel)
  • Constants at top: Moved RENDER_DEBOUNCE_MS and DEFAULT_TEXT_SIZE to the top of the file
  • Job reference for template rendering: startTemplateRendering() now stores a Job reference and cancels any previous one before restarting
  • Context passed to widget operations: updateWidgetConfiguration(context) and requestWidgetCreation(context) now take a Context parameter and handle AppWidgetManager/broadcast directly - eliminating the Action sealed class and the race condition with finish()
  • KDoc fixed: Updated doc for updateWidgetConfiguration to accurately describe what it does
  • SerializationException distinction preserved: Added TemplateRenderError enum to differentiate between template syntax errors and render errors, displayed with appropriate string resources
  • onSetup guard documented: Added comment explaining the guard prevents re-initialization on configuration changes

2. TemplateWidgetConfigureScreen.kt - New file:

  • Extracted all Composable functions from the Activity into a dedicated file
  • Updated to consume uiState StateFlow from ViewModel
  • Added template error display using TemplateRenderError to show specific error messages (template_error vs template_render_error)

3. TemplateWidgetConfigureActivity.kt - Simplified:

  • Removed enableEdgeToEdgeCompat() (already in BaseActivity)
  • Removed all Compose UI code (moved to new file)
  • Removed Action observation/handling
  • Removed @RequiresApi(Build.VERSION_CODES.O) annotations (SDK_INT always >= 26)
  • Passes context directly to ViewModel methods, eliminating the race condition

4. TemplateWidgetConfigureViewModelTest.kt - Updated:

  • All tests access state via viewModel.uiState.value instead of direct properties
  • State mutations use ViewModel methods (onTemplateTextChanged, onTextSizeChanged, etc.) instead of direct property assignment
  • updateWidgetConfiguration tests pass a mock Context
  • Added assertion for TemplateRenderError.RENDER_ERROR in error test

cc: @TimoPtr

* Implement `errorMessage` flow in `TemplateWidgetConfigureViewModel`
* Add `SnackbarHost` to `TemplateWidgetConfigureScreen` to display one-shot error events
* Update `TemplateWidgetConfigureActivity` to use the ViewModel for error reporting instead of manual Toast calls
…te files

* Extract TemplateWidgetConfigureUiState and TemplateRenderError to a new file
* Handle CancellationException during template rendering in TemplateWidgetConfigureViewModel
@TimoPtr
Copy link
Copy Markdown
Member

TimoPtr commented Feb 18, 2026

@amlwin reopen the PR when you think it is ready for review.

* Do not catch `CancellationException` in `TemplateWidgetConfigureActivity` to ensure coroutines are cancelled properly
* Update `TemplateWidgetConfigureViewModel` to use `Duration` for debounce and switch template rendering to `Dispatchers.Default`
* Ensure widget configuration is updated even if a `widgetId` was already set in `onSetup`
@amlwin
Copy link
Copy Markdown
Author

amlwin commented Feb 18, 2026

Thanks for your work 🎖️, some comments but overall it looks ok. Would you mind adding a screenshot test too to your compose screen, it should be rather easy?

look like screenshot is not working properly with current gradle version
The comment in libs.versions.toml even hints at this: On the next update, replace the hack in AndroidComposeConvertionPlugin with the real fix

Screenshot 2026-02-18 at 10 59 29 PM

* Refactor `TemplateWidgetConfigureScreen` into smaller, more maintainable composables: `ServerSelectionSection`, `TemplateInputSection`, `TemplateRenderResult`, and `WidgetAppearanceSection`.
* Replace standard `Button` with `HAAccentButton`.
* Improve error handling in `TemplateWidgetConfigureActivity` when updating widget configurations.
@amlwin amlwin marked this pull request as ready for review February 18, 2026 15:03
@amlwin amlwin requested review from TimoPtr and Copilot February 18, 2026 15:03
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Migrates the Template widget configuration flow from the legacy XML/view-based TemplateWidgetConfigureActivity to a Jetpack Compose screen backed by a new TemplateWidgetConfigureViewModel, with unit tests added for the ViewModel.

Changes:

  • Replaces the XML-based configuration UI with a Compose-based TemplateWidgetConfigureScreen
  • Introduces TemplateWidgetConfigureViewModel + TemplateWidgetConfigureUiState to drive state, template rendering, and widget persistence
  • Adds unit tests covering ViewModel setup, template rendering, and saving behavior

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
app/src/main/kotlin/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureActivity.kt Switches configuration activity to Compose + ViewModel-driven behavior
app/src/main/kotlin/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureScreen.kt New Compose UI implementation for the configuration screen
app/src/main/kotlin/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureUiState.kt New state model + error enum for the screen
app/src/main/kotlin/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureViewModel.kt New ViewModel handling setup, rendering, saving, and pinning widget creation
app/src/test/kotlin/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureViewModelTest.kt New unit tests for the ViewModel
app/src/main/res/layout/widget_template_configure.xml Removes the legacy XML layout
.idea/kotlinc.xml Updates IDE Kotlin compiler settings
Files not reviewed (1)
  • .idea/kotlinc.xml: Language not supported

return
}
if (!serverManager.isRegistered() || serverManager.getServer(serverId) == null) {
Timber.w("Not rendering template because server is not set")
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

When the server is not registered or getServer(serverId) is null, renderTemplate returns early without updating renderedTemplate/isTemplateValid. If the user previously had a valid render, the UI can keep showing stale data and keep the action button enabled while the selected server is now invalid. Consider explicitly setting isTemplateValid = false (and clearing renderedTemplate/setting an error) before returning.

Suggested change
Timber.w("Not rendering template because server is not set")
Timber.w("Not rendering template because server is not set")
_uiState.update {
it.copy(
renderedTemplate = null,
isTemplateValid = false,
templateRenderError = TemplateRenderError.RENDER_ERROR,
)
}

Copilot uses AI. Check for mistakes.
Comment on lines +195 to +205
if (e is SerializationException) {
Timber.e(e, "Template syntax error")
} else {
Timber.e(e, "Error rendering template")
}
val errorRendered = if (e.cause is SerializationException) {
TemplateRenderError.TEMPLATE_ERROR
} else {
TemplateRenderError.RENDER_ERROR
}
_uiState.update {
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The template error classification is inconsistent: the log branch checks e is SerializationException, but the error mapping uses e.cause is SerializationException. This can misclassify template syntax errors depending on where the SerializationException is wrapped. Align both checks to the same condition (matching the previous behavior that looked at the cause).

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +157
@OptIn(FlowPreview::class)
private fun startTemplateRendering() {
templateRenderingJob?.cancel()
templateRenderingJob = viewModelScope.launch {
combine(
_uiState.mapField { it.templateText },
_uiState.mapField { it.selectedServerId },
) { template, serverId -> template to serverId }
.debounce(RENDER_DEBOUNCE_MS.milliseconds)
.collect { (template, serverId) ->
renderTemplate(template, serverId)
}
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

startTemplateRendering() uses .collect { renderTemplate(...) }, so if rendering is slow (network) and the user changes input again, older renders will still complete and can overwrite newer results. Using collectLatest (or cancelling in-flight renders) avoids stale/out-of-order UI updates.

Copilot uses AI. Check for mistakes.
serverId = state.selectedServerId,
template = state.templateText,
textSize = state.textSize.toFloatOrNull() ?: DEFAULT_TEXT_SIZE,
lastUpdate = "Loading",
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

getPendingDaoEntity() always sets lastUpdate = "Loading". Previously, updating an existing widget preserved the prior lastUpdate from the DAO; resetting it here will temporarily replace the displayed widget content with a loading placeholder until the next widget update/render.

Suggested change
lastUpdate = "Loading",
lastUpdate = state.renderedTemplate ?: "Loading",

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This comment is kinda weird because the previous code was dao.get(appWidgetId)?.lastUpdate ?: "Loading", please check

Comment on lines +8 to +25
import androidx.annotation.VisibleForTesting
import androidx.core.text.HtmlCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.database.widget.TemplateWidgetDao
import io.homeassistant.companion.android.database.widget.TemplateWidgetEntity
import io.homeassistant.companion.android.database.widget.WidgetBackgroundType
import io.homeassistant.companion.android.widgets.ACTION_APPWIDGET_CREATED
import io.homeassistant.companion.android.widgets.BaseWidgetProvider.Companion.UPDATE_WIDGETS
import io.homeassistant.companion.android.widgets.EXTRA_WIDGET_ENTITY
import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

There are several unused imports in this file (e.g., VisibleForTesting, Duration, CoroutineDispatcher). These typically get flagged by ktlint/IDE inspections; please remove unused imports to keep the file clean and avoid CI failures if unused-import rules are enforced.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +88
private suspend fun onUpdateWidget() {
try {
viewModel.updateWidgetConfiguration(this@TemplateWidgetConfigureActivity)
setResult(RESULT_OK)
finish()
} catch (e: IllegalStateException) {
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

When finishing widget configuration (non-launcher flow), the result should include AppWidgetManager.EXTRA_APPWIDGET_ID so the launcher/host knows which widget was configured. Previously this was handled by BaseWidgetConfigureActivity.updateWidget(); now only setResult(RESULT_OK) is returned, which can cause the widget host to treat configuration as failed.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good catch 👍🏻, I'm not sure it is needed in the TodoWidgetConfigure because we use Glance and we manually call the update method from the manager.

Comment on lines +22 to +26
private val supportedTextColors: List<String>
get() = listOf(
application.getHexForColor(commonR.color.colorWidgetButtonLabelBlack),
application.getHexForColor(android.R.color.white),
)
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

TemplateWidgetConfigureView shows text color choices in the order Black then White, and the Activity passes supportedTextColors in the same order. In the previous XML, the White option was checked by default for transparent widgets; with the new setup the default becomes Black (index 0). If this is unintended, reorder the options or default textColorIndex to preserve the prior default behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +76
private fun onActionClick(requestLauncherSetup: Boolean) {
lifecycleScope.launch {
if (requestLauncherSetup) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
lifecycleScope.launch {
requestWidgetCreation()
}
} else {
showAddWidgetError() // this shouldn't be possible
}
requestPinWidget()
} else {
lifecycleScope.launch {
updateWidget()
}
onUpdateWidget()
}
}
}

override fun onServerSelected(serverId: Int) = renderTemplateText()

override suspend fun getPendingDaoEntity(): TemplateWidgetEntity {
val serverId = checkNotNull(selectedServerId) { "Selected server ID is null" }
val template = checkNotNull(binding.templateText.text?.toString()) { "Template text is null" }

return TemplateWidgetEntity(
id = appWidgetId,
serverId = serverId,
template = template,
textSize = binding.textSize.text.toString().toFloat(),
backgroundType = when (binding.backgroundType.selectedItem as String?) {
getString(commonR.string.widget_background_type_dynamiccolor) -> WidgetBackgroundType.DYNAMICCOLOR
getString(commonR.string.widget_background_type_transparent) -> WidgetBackgroundType.TRANSPARENT
else -> WidgetBackgroundType.DAYNIGHT
},
textColor = if (binding.backgroundType.selectedItem as String? ==
getString(commonR.string.widget_background_type_transparent)
) {
getHexForColor(
if (binding.textColorWhite.isChecked) {
android.R.color.white
} else {
commonR.color.colorWidgetButtonLabelBlack
},
)
} else {
null
},
lastUpdate = dao.get(appWidgetId)?.lastUpdate ?: "Loading",
)
}

override val widgetClass: Class<*> = TemplateWidget::class.java

private fun renderTemplateText() {
val editableText = binding.templateText.text ?: return
if (editableText.isNotEmpty()) {
renderTemplateText(editableText.toString())
} else {
binding.renderedTemplate.text = getString(commonR.string.empty_template)
binding.addButton.isEnabled = false
private suspend fun requestPinWidget() {
try {
viewModel.requestWidgetCreation(this@TemplateWidgetConfigureActivity)
finish()
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

requestPinWidget() is called without an API 26+ guard, but AppWidgetManager.requestPinAppWidget only exists on O+. On pre-O devices this path can crash with NoSuchMethodError. Add a Build.VERSION.SDK_INT >= Build.VERSION_CODES.O check (as other widget configure activities do) or mark the call site @RequiresApi(26) and ensure it’s never invoked below 26.

Copilot uses AI. Check for mistakes.
Comment on lines +73 to 91
private suspend fun requestPinWidget() {
try {
viewModel.requestWidgetCreation(this@TemplateWidgetConfigureActivity)
finish()
} catch (e: IllegalStateException) {
if (e is CancellationException) throw e
showAddWidgetError()
}
}

private fun renderTemplateText(template: String) {
val serverId = selectedServerId
if (serverId == null) {
Timber.w("Not rendering template because server is not set")
return
private suspend fun onUpdateWidget() {
try {
viewModel.updateWidgetConfiguration(this@TemplateWidgetConfigureActivity)
setResult(RESULT_OK)
finish()
} catch (e: IllegalStateException) {
if (e is CancellationException) throw e
showAddWidgetError()
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

Both requestPinWidget() and onUpdateWidget() catch IllegalStateException and then check if (e is CancellationException), but e can never be a CancellationException due to the catch type. Either catch Exception (and rethrow CancellationException) or remove the unreachable cancellation check.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Remove the if no need if you only catch IllegalStateException. The cancelation is already thrown

@TimoPtr
Copy link
Copy Markdown
Member

TimoPtr commented Feb 19, 2026

Thanks for your work 🎖️, some comments but overall it looks ok. Would you mind adding a screenshot test too to your compose screen, it should be rather easy?

look like screenshot is not working properly with current gradle version The comment in libs.versions.toml even hints at this: On the next update, replace the hack in AndroidComposeConvertionPlugin with the real fix
Screenshot 2026-02-18 at 10 59 29 PM

It never really worked in the IDE but it works when you execute the update screenshot task.

Comment on lines +269 to +281
val flags = PendingIntent.FLAG_MUTABLE
appWidgetManager?.requestPinAppWidget(
ComponentName(context, TemplateWidget::class.java),
null,
PendingIntent.getBroadcast(
context,
System.currentTimeMillis().toInt(),
Intent(context, TemplateWidget::class.java).apply {
action = ACTION_APPWIDGET_CREATED
putExtra(EXTRA_WIDGET_ENTITY, pendingEntity)
},
flags,
),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
val flags = PendingIntent.FLAG_MUTABLE
appWidgetManager?.requestPinAppWidget(
ComponentName(context, TemplateWidget::class.java),
null,
PendingIntent.getBroadcast(
context,
System.currentTimeMillis().toInt(),
Intent(context, TemplateWidget::class.java).apply {
action = ACTION_APPWIDGET_CREATED
putExtra(EXTRA_WIDGET_ENTITY, pendingEntity)
},
flags,
),
appWidgetManager?.requestPinAppWidget(
ComponentName(context, TemplateWidget::class.java),
null,
PendingIntent.getBroadcast(
context,
System.currentTimeMillis().toInt(),
Intent(context, TemplateWidget::class.java).apply {
action = ACTION_APPWIDGET_CREATED
putExtra(EXTRA_WIDGET_ENTITY, pendingEntity)
},
PendingIntent.FLAG_MUTABLE,
),

Copy link
Copy Markdown
Member

@TimoPtr TimoPtr left a comment

Choose a reason for hiding this comment

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

If you are using AI I think that for this final step you should do it yourself to make sure to fully get my comments.

Comment on lines +77 to +78
} catch (e: IllegalStateException) {
if (e is CancellationException) throw e
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
} catch (e: IllegalStateException) {
if (e is CancellationException) throw e
} catch (e: CancellationException) {
throw e
} catch (e: IllegalStateException) {

This is way we use in the repo, please adjust in the activity and the viewModel

} else {
showAddWidgetError() // this shouldn't be possible
}
requestPinWidget()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why not checking the version anymore? If you do so it would remove the warning in the ViewModel by properly annotating the function requestPinWidget like it is done in TodoWidgetConfigure.

Comment on lines +83 to +88
private suspend fun onUpdateWidget() {
try {
viewModel.updateWidgetConfiguration(this@TemplateWidgetConfigureActivity)
setResult(RESULT_OK)
finish()
} catch (e: IllegalStateException) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good catch 👍🏻, I'm not sure it is needed in the TodoWidgetConfigure because we use Glance and we manually call the update method from the manager.

binding.addButton.isEnabled = enabled && isValidServerId()
}
private fun showAddWidgetError() {
viewModel.showError(commonR.string.widget_creation_error)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The error is always the same and always trigger from the activity I propose you to not use the viewModel and directly send this to the screen, it would simplify things.

}

@Composable
private fun TemplateWidgetConfigureView(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
private fun TemplateWidgetConfigureView(
@VisibleForTesting
internal fun TemplateWidgetConfigureScreen(

Ideally we should write a compose screen test https://github.com/home-assistant/android/blob/457de6f8896328e6ebebeb5f3db8a01a39e7168f/app/src/test/kotlin/io/homeassistant/companion/android/onboarding/sethomenetwork/SetHomeNetworkScreenTest.kt for inspiration

return
}
if (!serverManager.isRegistered() || serverManager.getServer(serverId) == null) {
Timber.w("Not rendering template because server is not set")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe you could set an Error here in the ViewState otherwise the user wouldn't see anything.

Comment on lines +184 to +187
val rendered = HtmlCompat.fromHtml(result, HtmlCompat.FROM_HTML_MODE_LEGACY)
?.toString()
?.trimEnd()
?: result
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
val rendered = HtmlCompat.fromHtml(result, HtmlCompat.FROM_HTML_MODE_LEGACY)
?.toString()
?.trimEnd()
?: result
val rendered = HtmlCompat.fromHtml(result, HtmlCompat.FROM_HTML_MODE_LEGACY).toString().trimEnd()

If you check the doc from fromHtml it won't ever return null. Any reason to do otherwise?

Comment on lines +192 to +195
if (e is CancellationException) {
throw e
}
if (e is SerializationException) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Put this in a dedicated catch block above this catch.

serverId = state.selectedServerId,
template = state.templateText,
textSize = state.textSize.toFloatOrNull() ?: DEFAULT_TEXT_SIZE,
lastUpdate = "Loading",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This comment is kinda weird because the previous code was dao.get(appWidgetId)?.lastUpdate ?: "Loading", please check

Comment on lines +286 to +293
/**
* Emit a one-shot error event to be displayed as a Snackbar.
*
* @param messageResId the string resource ID for the error message
*/
fun showError(messageResId: Int) {
viewModelScope.launch { _errorMessage.emit(messageResId) }
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
/**
* Emit a one-shot error event to be displayed as a Snackbar.
*
* @param messageResId the string resource ID for the error message
*/
fun showError(messageResId: Int) {
viewModelScope.launch { _errorMessage.emit(messageResId) }
}

@home-assistant home-assistant bot marked this pull request as draft February 19, 2026 09:34
@amlwin
Copy link
Copy Markdown
Author

amlwin commented Feb 21, 2026

If you are using AI I think that for this final step you should do it yourself to make sure to fully get my comments.

I'll check in this weekend

@TimoPtr
Copy link
Copy Markdown
Member

TimoPtr commented Feb 27, 2026

Thanks for your work 🎖️, some comments but overall it looks ok. Would you mind adding a screenshot test too to your compose screen, it should be rather easy?

look like screenshot is not working properly with current gradle version The comment in libs.versions.toml even hints at this: On the next update, replace the hack in AndroidComposeConvertionPlugin with the real fix
Screenshot 2026-02-18 at 10 59 29 PM

It never really worked in the IDE but it works when you execute the update screenshot task.

@amlwin If you merge main in your branch and update your Android Studio to the latest version then it is going to work. It has been fixed this week 🎉

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants