Skip to content

feat: add native Android app with Jetpack Compose + Material 3#58

Open
RickyNotaro wants to merge 5 commits intomasterfrom
claude/native-android-conversion-zfvMG
Open

feat: add native Android app with Jetpack Compose + Material 3#58
RickyNotaro wants to merge 5 commits intomasterfrom
claude/native-android-conversion-zfvMG

Conversation

@RickyNotaro
Copy link
Copy Markdown
Owner

Replace the Capacitor WebView wrapper with a fully native Android app:

  • Kotlin + Jetpack Compose + Material 3 (Material You dynamic colors)
  • Bottom navigation: Véhicules list + Radar map
  • WebView login flow preserving the cookie trick (shared CookieManager
    between WebView and OkHttp for seamless session auth)
  • Vehicle list with search, sort (distance/number/battery/brand), fuel
    filter chips, pull-to-refresh, and detail bottom sheet
  • Radar screen with Google Maps, radius slider, vehicle markers,
    auto-refresh countdown, and auto-book feature
  • Retrofit + OkHttp networking with WebViewCookieJar bridge
  • MVVM architecture with ViewModels and Kotlin Flows
  • DataStore for auth persistence
  • Accompanist for runtime permissions (location)

https://claude.ai/code/session_01QnYF5hKnktCPP2m5mSgwLp

claude added 2 commits March 30, 2026 05:41
Replace the Capacitor WebView wrapper with a fully native Android app:

- Kotlin + Jetpack Compose + Material 3 (Material You dynamic colors)
- Bottom navigation: Véhicules list + Radar map
- WebView login flow preserving the cookie trick (shared CookieManager
  between WebView and OkHttp for seamless session auth)
- Vehicle list with search, sort (distance/number/battery/brand), fuel
  filter chips, pull-to-refresh, and detail bottom sheet
- Radar screen with Google Maps, radius slider, vehicle markers,
  auto-refresh countdown, and auto-book feature
- Retrofit + OkHttp networking with WebViewCookieJar bridge
- MVVM architecture with ViewModels and Kotlin Flows
- DataStore for auth persistence
- Accompanist for runtime permissions (location)

https://claude.ai/code/session_01QnYF5hKnktCPP2m5mSgwLp
- Triggers on push to native-app and native-android branches, and PRs
- Uses setup-java (17), setup-android, and gradle/actions/setup-gradle
- Generates wrapper on-the-fly (no wrapper jar committed)
- Uploads debug APK as artifact
- Supports MAPS_API_KEY from repo secret or env var

https://claude.ai/code/session_01QnYF5hKnktCPP2m5mSgwLp
Copilot AI review requested due to automatic review settings March 30, 2026 05:46
Copy link
Copy Markdown

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

Adds a new fully native Android application (Kotlin + Jetpack Compose + Material 3) under android-native/, replacing the previous Capacitor WebView-wrapper approach while preserving the WebView→CookieManager→OkHttp cookie-bridging authentication flow.

Changes:

  • Introduces a standalone Android Gradle project (android-native/) with Compose UI, MVVM ViewModels, and Retrofit/OkHttp networking.
  • Implements core screens: login WebView flow, vehicle list (search/sort/filter), and radar map (Google Maps + auto-refresh/auto-book).
  • Adds a GitHub Actions workflow to build and upload a debug APK for the native Android app.

Reviewed changes

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

Show a summary per file
File Description
android-native/settings.gradle.kts Defines standalone Gradle project + :app module.
android-native/gradle/wrapper/gradle-wrapper.properties Adds Gradle wrapper configuration for the native project.
android-native/gradle.properties Adds Gradle properties including MAPS_API_KEY placeholder.
android-native/build.gradle.kts Declares plugin versions (AGP/Kotlin/Compose).
android-native/app/src/main/res/values/themes.xml Adds Material 3 base theme for the app.
android-native/app/src/main/res/values/strings.xml Adds app name string.
android-native/app/src/main/java/com/communauto/tools/viewmodel/VehicleViewModel.kt Vehicle list state, filtering, sorting, and fetching.
android-native/app/src/main/java/com/communauto/tools/viewmodel/RadarViewModel.kt Radar state, radius filtering, refresh loop, and auto-book logic.
android-native/app/src/main/java/com/communauto/tools/viewmodel/AuthViewModel.kt Auth state orchestration around WebView cookie acquisition.
android-native/app/src/main/java/com/communauto/tools/util/LocationUtils.kt Adds default coordinates and Haversine distance helper.
android-native/app/src/main/java/com/communauto/tools/ui/theme/Type.kt Defines custom typography.
android-native/app/src/main/java/com/communauto/tools/ui/theme/Theme.kt Sets up MaterialTheme + dynamic color support.
android-native/app/src/main/java/com/communauto/tools/ui/theme/Color.kt Defines brand color palette.
android-native/app/src/main/java/com/communauto/tools/ui/screen/VehicleListScreen.kt Compose UI for vehicle list with search/sort/filter + detail sheet.
android-native/app/src/main/java/com/communauto/tools/ui/screen/RadarScreen.kt Compose UI for radar map + bottom sheet controls.
android-native/app/src/main/java/com/communauto/tools/ui/screen/LoginScreen.kt Compose login screen + embedded WebView login flow.
android-native/app/src/main/java/com/communauto/tools/ui/navigation/AppNavigation.kt Compose navigation + location permission handling + initial fetching.
android-native/app/src/main/java/com/communauto/tools/ui/component/VehicleDetailSheet.kt Bottom sheet for vehicle details + booking action.
android-native/app/src/main/java/com/communauto/tools/ui/component/VehicleCard.kt Vehicle list item UI.
android-native/app/src/main/java/com/communauto/tools/data/repository/VehicleRepository.kt Repository wrapper around API calls + distance enrichment.
android-native/app/src/main/java/com/communauto/tools/data/model/Vehicle.kt Network models, response wrappers, and UI-friendly helpers.
android-native/app/src/main/java/com/communauto/tools/data/auth/AuthManager.kt DataStore-backed auth flag + CookieManager cookie parsing/UID extraction.
android-native/app/src/main/java/com/communauto/tools/data/api/NetworkModule.kt Retrofit/OkHttp setup + CookieManager-backed CookieJar.
android-native/app/src/main/java/com/communauto/tools/data/api/CommunautoApi.kt Retrofit interface for WCF endpoints.
android-native/app/src/main/java/com/communauto/tools/MainActivity.kt Compose entry activity and theme setup.
android-native/app/src/main/java/com/communauto/tools/CommunautoApp.kt Application singleton wiring for auth + repository.
android-native/app/src/main/AndroidManifest.xml Declares permissions, app class, activity, and Maps API key metadata.
android-native/app/proguard-rules.pro ProGuard rules for Retrofit/Gson/OkHttp.
android-native/app/build.gradle.kts Android app configuration + dependencies (Compose, Maps, Retrofit, DataStore).
android-native/.gitignore Ignores Android/Gradle build artifacts for the native project.
.github/workflows/build-native-apk.yml CI workflow to build and upload a debug APK artifact.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +117 to +126
refreshJob = viewModelScope.launch {
while (true) {
delay(1000)
_countdown.value--
if (_countdown.value <= 0) {
_countdown.value = seconds
fetchVehicles()
// Try auto-book after refresh
if (_autoBookActive.value) tryAutoBook()
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

setRefreshInterval calls fetchVehicles() and then tryAutoBook() in the timer loop, but fetchVehicles() launches a new coroutine and returns immediately. This means auto-book can run against stale vehiclesInRadius data (before the refresh completes). Consider refactoring fetchVehicles into a suspend function (or returning/awaiting the Job) and only calling tryAutoBook() after the vehicles update succeeds.

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +95
}
vehicleViewModel.fetchVehicles()
radarViewModel.fetchVehicles()
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

This LaunchedEffect always calls fetchVehicles() for both view models, even when location permission is not granted yet (so it fetches using the default lat/lng). When permission becomes granted, requestLocation calls vehicleViewModel.updateLocation(...), which itself triggers another fetchVehicles(), leading to duplicate network calls. Consider restructuring so fetching happens exactly once per location update (e.g., fetch only after location is set, or remove the fetch inside updateLocation).

Suggested change
}
vehicleViewModel.fetchVehicles()
radarViewModel.fetchVehicles()
} else {
vehicleViewModel.fetchVehicles()
radarViewModel.fetchVehicles()
}

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +52
suspend fun logout() {
_isAuthenticated.value = false
context.dataStore.edit { it[KEY_AUTHENTICATED] = false }
CookieManager.getInstance().removeAllCookies(null)
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

logout() clears cookies via CookieManager.removeAllCookies(null), but this API is asynchronous and you are not waiting for completion or flushing. That can leave cookies temporarily present for subsequent requests. Consider using the callback form and calling CookieManager.getInstance().flush() (and/or awaiting completion) before treating the user as fully logged out.

Copilot uses AI. Check for mistakes.
Comment on lines +229 to +233
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?,
): Boolean = false
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

shouldOverrideUrlLoading always returns false, so the WebView will navigate to any URL the page redirects to. For a login WebView, it's safer to restrict navigation to expected https hosts (e.g., *.reservauto.net, identity provider) and block/hand off unexpected schemes/hosts to an external browser to reduce phishing / intent-scheme risks.

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +120
expanded = false,
onExpandedChange = {},
placeholder = { Text("Rechercher...") },
leadingIcon = { Icon(Icons.Default.Search, "Rechercher") },
)
},
expanded = false,
onExpandedChange = {},
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The SearchBar is hard-coded with expanded = false and onExpandedChange = {} (no-op), so it can never expand and the onSearch callback just sets local state. If you intend a non-expandable search field, consider using a TextField instead; otherwise wire expanded/onExpandedChange to state.

Suggested change
expanded = false,
onExpandedChange = {},
placeholder = { Text("Rechercher...") },
leadingIcon = { Icon(Icons.Default.Search, "Rechercher") },
)
},
expanded = false,
onExpandedChange = {},
expanded = searchActive,
onExpandedChange = { searchActive = it },
placeholder = { Text("Rechercher...") },
leadingIcon = { Icon(Icons.Default.Search, "Rechercher") },
)
},
expanded = searchActive,
onExpandedChange = { searchActive = it },

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +55
.addInterceptor(
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BASIC
}
)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

HttpLoggingInterceptor is enabled unconditionally. Even at BASIC, this can leak sensitive URLs/metadata in production logs and adds overhead. Consider enabling logging only for debug builds (e.g., via BuildConfig.DEBUG) and disabling it in release.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +67
val q = query.lowercase()
v.vehicle.carNo.toString().contains(q) ||
v.vehicle.carBrand.lowercase().contains(q) ||
v.vehicle.carModel.lowercase().contains(q) ||
v.vehicle.carColor.lowercase().contains(q)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

query.lowercase() uses the device default locale, which can cause unexpected matching behavior for certain locales (e.g., Turkish). For stable search behavior, consider normalizing with lowercase(Locale.ROOT) (and similarly for the vehicle fields) and trimming the query once.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +46
fun onWebViewPageFinished(url: String) {
// After the user navigates through login, check for cookies
if (authManager.hasCookies()) {
_cookieStatus.value = authManager.getCookieStatus()
viewModelScope.launch {
authManager.onLoginSuccess()
}
_showWebView.value = false
}
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

onWebViewPageFinished(url: String) doesn't use its url parameter. If the URL isn't needed, remove the parameter (and adjust the caller) to avoid an unused-parameter warning, or use it to validate that the user reached an expected post-login page before accepting cookies.

Copilot uses AI. Check for mistakes.
val fuelFilter by viewModel.fuelFilter.collectAsState()
val selectedVehicle by viewModel.selectedVehicle.collectAsState()

var searchActive by remember { mutableStateOf(false) }
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

searchActive is set but never read anywhere in this composable, which will trigger an unused-state warning. Consider removing it or using it to drive the SearchBar expanded state.

Suggested change
var searchActive by remember { mutableStateOf(false) }

Copilot uses AI. Check for mistakes.
build:
runs-on: ubuntu-latest
permissions:
contents: write
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

This workflow grants contents: write permissions, but the job only builds and uploads an artifact. Consider reducing to contents: read (or removing the explicit permissions block) to follow least-privilege.

Suggested change
contents: write
contents: read

Copilot uses AI. Check for mistakes.
claude added 2 commits March 30, 2026 05:54
The Material3 XML theme parent (Theme.Material3.DayNight.NoActionBar)
requires the MDC-Android library in XML resources. Since all Material 3
theming is handled by Jetpack Compose at runtime, use the platform
android:Theme.Material.Light.NoActionBar instead.

https://claude.ai/code/session_01QnYF5hKnktCPP2m5mSgwLp
Adds adaptive icon (car silhouette on Communauto green) and required
mipmap/color resources to fix AAPT resource linking error.

https://claude.ai/code/session_01QnYF5hKnktCPP2m5mSgwLp
Copilot AI review requested due to automatic review settings March 30, 2026 06:02
- Replace java.lang.Math.toRadians() with pure Kotlin helper
- Add Gradle cache to CI workflow for faster rebuilds
- Add separate compileDebugKotlin step for fast failure feedback

https://claude.ai/code/session_01QnYF5hKnktCPP2m5mSgwLp
Copy link
Copy Markdown

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

Copilot reviewed 35 out of 35 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +83 to +85
// Pull-to-refresh
implementation("androidx.compose.material3:material3-android")

Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

androidx.compose.material3:material3 is already included above, and PullToRefreshBox lives in that artifact. The extra androidx.compose.material3:material3-android dependency looks redundant and can be removed unless there’s a specific reason to keep both.

Suggested change
// Pull-to-refresh
implementation("androidx.compose.material3:material3-android")

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +58
import com.communauto.tools.ui.theme.VehicleGray
import com.communauto.tools.viewmodel.FuelFilter
import com.communauto.tools.viewmodel.RadarViewModel
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.CircleOptions
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

There are unused imports in this file (e.g., CircleOptions, VehicleGray) that should be removed to avoid warnings and keep the file tidy (and to satisfy stricter lint setups if enabled).

Suggested change
import com.communauto.tools.ui.theme.VehicleGray
import com.communauto.tools.viewmodel.FuelFilter
import com.communauto.tools.viewmodel.RadarViewModel
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.CircleOptions
import com.communauto.tools.viewmodel.FuelFilter
import com.communauto.tools.viewmodel.RadarViewModel
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.android.gms.maps.model.CameraPosition

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +62
if (isAuthenticated) {
onLoginSuccess()
return
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

onLoginSuccess() is invoked directly during composition when isAuthenticated becomes true. This is a side effect and can be triggered multiple times across recompositions (and can also cause “state update during composition” issues depending on what the callback does). Move this into a LaunchedEffect(isAuthenticated) (or similar one-shot effect) and keep the composable branch pure UI.

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +122
while (true) {
delay(1000)
_countdown.value--
if (_countdown.value <= 0) {
_countdown.value = seconds
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The refresh job loops forever and triggers fetchVehicles(), but fetchVehicles() launches a new coroutine. If requests are slow, ticks can queue overlapping fetches (and overlapping auto-book attempts). Prefer a single refresh coroutine that calls a suspend refresh function directly and loops with while (isActive) / guards with _loading or a mutex.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +7
# Replace with your Google Maps API key
MAPS_API_KEY=YOUR_GOOGLE_MAPS_API_KEY_HERE
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

Committing an API-key placeholder in gradle.properties encourages putting real secrets into a tracked file (and it will also propagate into the manifest placeholder if someone forgets to override it). Prefer removing MAPS_API_KEY from the committed gradle.properties and sourcing it from local.properties (ignored) or environment variables / CI secrets only.

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +66
var searchActive by remember { mutableStateOf(false) }
var sortMenuExpanded by remember { mutableStateOf(false) }

Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

Unused state is introduced (searchActive) and several imports appear unused (e.g., CircularProgressIndicator). Please remove unused imports/state to avoid warnings and keep the new screen code minimal (and compatible with stricter lint configurations).

Copilot uses AI. Check for mistakes.
viewModel = vehicleViewModel,
isAuthenticated = isAuthenticated,
onBookVehicle = { carId ->
// Booking handled via ViewModel
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The “Réserver ce véhicule” action is wired to a no-op here: onBookVehicle is passed as an empty lambda, so the booking button in VehicleDetailSheet won’t actually create a booking. Please either implement booking (e.g., ViewModel/repository call using AuthViewModel.getUid()) or remove the booking UI until it’s supported.

Suggested change
// Booking handled via ViewModel
val userId = authViewModel.getUid()
if (userId != null) {
vehicleViewModel.bookVehicle(userId, carId)
}

Copilot uses AI. Check for mistakes.
Comment on lines +213 to +217
factory = { context ->
WebView(context).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
CookieManager.getInstance().setAcceptThirdPartyCookies(this, true)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The WebView created in AndroidView is never explicitly destroyed. WebView instances are heavyweight and can leak resources if not cleaned up when this screen leaves composition. Add a DisposableEffect/onRelease hook to call stopLoading()/destroy() (and clear the reference) when the composable is disposed.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +48
- name: Generate Gradle wrapper
run: gradle wrapper --gradle-version 8.11.1

Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The workflow generates the Gradle wrapper at runtime and then runs ./gradlew. This is fragile (it hides missing wrapper scripts/JARs in the repo and adds work each run). Prefer committing the wrapper scripts + gradle-wrapper.jar under android-native/ and removing the wrapper-generation step so builds are reproducible locally and in CI.

Suggested change
- name: Generate Gradle wrapper
run: gradle wrapper --gradle-version 8.11.1

Copilot uses AI. Check for mistakes.
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.

3 participants