Skip to content

feat(android): migrate XML layouts to Jetpack Compose#943

Open
AchoArnold wants to merge 12 commits into
mainfrom
feature/migrate-xml-to-compose
Open

feat(android): migrate XML layouts to Jetpack Compose#943
AchoArnold wants to merge 12 commits into
mainfrom
feature/migrate-xml-to-compose

Conversation

@AchoArnold

@AchoArnold AchoArnold commented Jul 7, 2026

Copy link
Copy Markdown
Member

This PR migrates the httpSMS Android application's UI from legacy XML layouts to Jetpack Compose.

Key Changes

  • Build System: Enabled Compose and added Material3 dependencies.
  • Theming: Created a new Material3 theme (\HttpSmsTheme) and typography.
  • Architecture: Introduced ViewModels (\MainViewModel, \LoginViewModel, \SettingsViewModel) for reactive state management.
  • UI Migration: Re-implemented Login, Main, and Settings screens as Composables.
  • Cleanup: Removed legacy XML layouts and refactored Activities to use \setContent.

Verification

  • Project builds successfully with ./gradlew assembleDebug.
  • All features (Login, Main dashboard, Settings) are functional in the new Compose UI.

@codacy-production

codacy-production Bot commented Jul 7, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 3 high · 8 medium · 33 minor

Alerts:
⚠ 44 issues (≤ 0 issues of at least minor severity)

Results:
44 new issues

Category Results
Documentation 33 minor
ErrorProne 3 high
Complexity 8 medium

View in Codacy

🟢 Metrics 78 complexity · -1 duplication

Metric Results
Complexity 78
Duplication -1

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@AchoArnold AchoArnold changed the title \feat: migrate XML layouts to Jetpack Compose" --body-file \C:/Users/achoa/AppData/Local/Google/AndroidStudio2026.1.1/projects/android.6b880862/.artifacts/20260625-115653-3d264f9e-e99a-4263-a680-2bde6150421d/pr_body.txt\ --base main --head feature/migrate-xml-to-compose feat(android): migrate XML layouts to Jetpack Compose Jul 7, 2026
@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR migrates all three Android activity UIs (Login, Main, Settings) from XML layouts to Jetpack Compose, introducing ViewModels backed by StateFlow for each screen and wiring the Activities to Compose content via setContent {}.

  • Login, Main, and Settings screens are each replaced with a new *ActivityContent.kt composable and a matching *ViewModel.kt; the Activities are reduced to Android-specific duties (permissions, intents, toasts).
  • Build configuration adds the Compose BOM (2024.10.01), Material3, Activity-Compose, and the kotlin.plugin.compose Gradle plugin, while the XML layout files are kept but are now effectively dead code.
  • One regression introduced: the URL validation in LoginViewModel was weakened from URLUtil.isValidUrl + URLUtil.isHttpsUrl to simple string-prefix checks, creating a path where URI(serverUrl) can throw an uncaught URISyntaxException and leave isLoading = true indefinitely.

Confidence Score: 3/5

The overall migration is well-structured, but the login flow has a regression that can leave the UI permanently unresponsive when a user enters a malformed server URL.

The weakened URL validation in LoginViewModel removed the structural URL check that previously guarded the URI() constructor call. Any URL that passes the new prefix check but is not a syntactically valid URI (e.g., contains spaces or illegal characters) will cause an uncaught URISyntaxException inside the coroutine, silently killing it while isLoading stays true — the user cannot retry without force-closing the app.

android/app/src/main/java/com/httpsms/ui/login/LoginViewModel.kt — the URL validation and exception-handling around the URI() / network call block need attention before shipping.

Important Files Changed

Filename Overview
android/app/src/main/java/com/httpsms/ui/login/LoginViewModel.kt New ViewModel for login flow; weakened URL validation (prefix check vs. full URLUtil) can leave isLoading stuck at true when URI() throws on a malformed URL
android/app/build.gradle.kts Adds Compose BOM and dependencies; composeOptions.kotlinCompilerExtensionVersion is redundant with the Kotlin 2.0 Compose plugin, and navigation-compose is added but unused
android/app/src/main/java/com/httpsms/ui/settings/SettingsActivityContent.kt New Compose UI for settings screen; contains a duplicate/deprecated Icons.Filled.ArrowBack import alongside the correct Icons.AutoMirrored.Filled.ArrowBack
android/app/src/main/java/com/httpsms/LoginActivity.kt Migrated to Compose; business logic correctly moved to LoginViewModel; redirectToMain() check on loginSuccess handled via LaunchedEffect
android/app/src/main/java/com/httpsms/MainActivity.kt Migrated to Compose; onResume now calls viewModel.updateState() to refresh UI state; clean transition from XML-based approach
android/app/src/main/java/com/httpsms/ui/login/LoginActivityContent.kt New Compose UI for login screen; duplicate wildcard import of androidx.compose.foundation.layout.* at bottom alongside specific imports
android/app/src/main/java/com/httpsms/ui/main/MainViewModel.kt New ViewModel for main screen; correctly uses StateFlow, handles heartbeat sending with loading state, and refreshes state after heartbeat
android/app/src/main/java/com/httpsms/ui/settings/SettingsViewModel.kt New ViewModel for settings; correctly persists each setting change to SharedPreferences and updates StateFlow; logout clears all settings

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant A as LoginActivity
    participant VM as LoginViewModel
    participant API as HttpSmsApiService

    A->>VM: initialize(context, defaultServerUrl)
    VM-->>A: uiState (StateFlow)

    A->>A: "setContent { LoginScreen(viewModel) }"
    note over A: LaunchedEffect(uiState.loginSuccess)

    A->>VM: login(context, countryCode, callbacks)
    VM->>VM: validate phone numbers
    VM->>VM: validate serverUrl (prefix check only ⚠️)
    VM->>+API: updateFcmToken(phone1, SIM1, fcmToken)
    API-->>-VM: Pair(apiError?, urlError?)
    VM->>VM: save Settings (apiKey, serverUrl, phones)
    VM->>VM: "uiState.loginSuccess = true"
    note over A: LaunchedEffect fires → redirectToMain()
    A->>A: finish() + startActivity(MainActivity)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant A as LoginActivity
    participant VM as LoginViewModel
    participant API as HttpSmsApiService

    A->>VM: initialize(context, defaultServerUrl)
    VM-->>A: uiState (StateFlow)

    A->>A: "setContent { LoginScreen(viewModel) }"
    note over A: LaunchedEffect(uiState.loginSuccess)

    A->>VM: login(context, countryCode, callbacks)
    VM->>VM: validate phone numbers
    VM->>VM: validate serverUrl (prefix check only ⚠️)
    VM->>+API: updateFcmToken(phone1, SIM1, fcmToken)
    API-->>-VM: Pair(apiError?, urlError?)
    VM->>VM: save Settings (apiKey, serverUrl, phones)
    VM->>VM: "uiState.loginSuccess = true"
    note over A: LaunchedEffect fires → redirectToMain()
    A->>A: finish() + startActivity(MainActivity)
Loading

Reviews (1): Last reviewed commit: "feat: migrate XML layouts to Jetpack Com..." | Re-trigger Greptile

Comment on lines +95 to +128
// Simple URL validation (can be improved)
if (!serverUrl.startsWith("http")) {
_uiState.value = _uiState.value.copy(
isLoading = false,
serverUrlError = "Server URL [$serverUrl] is invalid"
)
return@launch
}

if (!serverUrl.startsWith("https")) {
_uiState.value = _uiState.value.copy(
isLoading = false,
serverUrlError = "Server URL [$serverUrl] must be HTTPS"
)
return@launch
}

val authResult = withContext(Dispatchers.IO) {
val service = HttpSmsApiService(apiKey, URI(serverUrl))
val e164Phone1 = PhoneNumberValidator.formatE164(phone1, countryCode)
val response1 = service.updateFcmToken(e164Phone1, Constants.SIM1, Settings.getFcmToken(context) ?: "")

if (response1.second != null || response1.third != null) {
return@withContext Pair(response1.second, response1.third)
}

if (currentState.isDualSim) {
val e164Phone2 = PhoneNumberValidator.formatE164(phone2, countryCode)
val response2 = service.updateFcmToken(e164Phone2, Constants.SIM2, Settings.getFcmToken(context) ?: "")
return@withContext Pair(response2.second, response2.third)
}

Pair(null, null)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Weak URL validation can cause permanently frozen UI

The original validation used URLUtil.isValidUrl() + URLUtil.isHttpsUrl(), which verify full URL structure. The replacement (startsWith("http") / startsWith("https")) only checks the scheme prefix, so a URL like "https://not valid" (containing a space) passes both checks. When URI(serverUrl) is then constructed inside withContext(Dispatchers.IO), it throws URISyntaxException. That exception is not caught anywhere in the coroutine, so it propagates silently — but isLoading was already set to true before the launch and is never reset, leaving the progress indicator spinning indefinitely.

To fix, wrap the network block in a try/catch (at minimum) and reset isLoading = false in the catch clause, or restore full URL structural validation before reaching the URI(...) call.

Comment on lines +7 to +8
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowBack

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The non-automirrored ArrowBack import (deprecated on Android) is unused — only Icons.AutoMirrored.Filled.ArrowBack is referenced in the code below. Removing it avoids a compiler/IDE warning.

Suggested change
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowBack

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +45 to 47
composeOptions {
kotlinCompilerExtensionVersion = "1.5.15"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 When using the org.jetbrains.kotlin.plugin.compose Gradle plugin (Kotlin 2.0+), the compose compiler is bundled with the Kotlin plugin and the composeOptions { kotlinCompilerExtensionVersion } block is no longer needed. Including version 1.5.15 here alongside the 2.0.21 plugin can produce a build warning and is at minimum confusing.

Suggested change
composeOptions {
kotlinCompilerExtensionVersion = "1.5.15"
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

implementation("androidx.compose.material3:material3")
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation("androidx.navigation:navigation-compose:2.8.3")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 navigation-compose is added as a dependency but is never used in this PR — all three activities still navigate via explicit Intents. This unused artifact increases build time and APK size needlessly. Remove it if Compose Navigation is not yet planned, or leave a comment explaining its intent.

Suggested change
implementation("androidx.navigation:navigation-compose:2.8.3")

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +40 to +42
import com.httpsms.ui.theme.Blue500
import com.httpsms.ui.theme.Pink500
import androidx.compose.foundation.layout.*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 This wildcard layout.* import duplicates the specific layout imports already declared above it (lines 6–11). The redundant wildcard can cause IDE lint warnings and makes the dependency footprint of this file harder to read at a glance.

Suggested change
import com.httpsms.ui.theme.Blue500
import com.httpsms.ui.theme.Pink500
import androidx.compose.foundation.layout.*
import com.httpsms.ui.theme.Blue500
import com.httpsms.ui.theme.Pink500

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

1 participant