This project was completely vibe-coded, 100%, with the help of Claude Code and Opus 4.5. There it is, I said it, the shame is now gone.
I am/was pretty skeptical on LLM-generated code and this was born as an experiment to learn how it actually was to vibe-code something from scratch.
Turns out it's pretty awesome and easy to follow the happy path for a stock, modern Android app which just displays data consumed from a JSON REST API. I'm a DevOps guy and I have zero mobile development skills, so achieving this can be considered pretty awesome in my book. But I'm pretty sure in the eyes of a skilled Kotlin Android developer, this code might induce different feelings.
At the moment, I completely depend on CC to maintain the app, but I would like to take this opportunity as an excuse to learn more about the Android development ecosystem, beside learning how to tame an LLM agent.
matedroid/
├── app/src/main/java/com/matedroid/
│ ├── data/ # Data layer (API, repository, local storage)
│ ├── domain/ # Domain layer (models, use cases)
│ ├── ui/ # UI layer (screens, components, theme)
│ └── di/ # Dependency injection modules
├── gradle/ # Gradle wrapper and version catalog
├── util/ # Utility scripts
├── ASSETS.md # Tesla car image asset documentation
└── PLAN.md # Detailed implementation plan
- Language: Kotlin
- UI: Jetpack Compose with Material Design 3
- Architecture: MVVM + Clean Architecture
- DI: Hilt
- Networking: Retrofit + OkHttp + Moshi
- Local Storage: DataStore
- Charts: Vico
- Maps: osmdroid (OpenStreetMap)
The app supports multiple languages using Android's standard resource-based localization system. Currently supported languages:
- English (default) -
res/values/strings.xml - Italian -
res/values-it/strings.xml - Spanish -
res/values-es/strings.xml - Catalan -
res/values-ca/strings.xml
- All user-visible strings must be in string resources - never hardcode text in Kotlin files
- String naming convention: Use
snake_case(e.g.,settings_title,drive_history) - Add context comments for translators above each string:
<!-- Dialog title when user has multiple vehicles --> <string name="select_vehicle">Select Vehicle</string>
-
Add the English string to
res/values/strings.xml:<!-- Description of what this string is for --> <string name="new_feature_label">Feature Name</string>
-
Add translations to all locale files:
res/values-it/strings.xmlres/values-es/strings.xmlres/values-ca/strings.xml
-
Use in Kotlin code:
import androidx.compose.ui.res.stringResource import com.matedroid.R Text(text = stringResource(R.string.new_feature_label))
For strings with dynamic values, use placeholders:
<!-- %d is the percentage -->
<string name="charge_limit_format">Limit: %d%%</string>
<!-- %1$s is the date, %2$d is the number of days -->
<string name="avg_year_message">Since %1$s (%2$d days ago)</string>Usage:
stringResource(R.string.charge_limit_format, chargeLimit)
stringResource(R.string.avg_year_message, formattedDate, dayCount)- Create a new folder:
res/values-{language_code}/ - Copy
res/values/strings.xmlto the new folder - Translate all strings, keeping the same
nameattributes - Android will automatically use the correct language based on device settings
Change your device/emulator language in Settings > System > Languages to test different locales.
Python script to download Tesla car 3D renders from Tesla's compositor service. Requires uv for dependency management.
# Download all car images (Model 3 & Y, various colors/wheels)
./util/fetch_tesla_assets.py
# Preview what would be downloaded
./util/fetch_tesla_assets.py --dry-run
# Custom output directory
./util/fetch_tesla_assets.py --output-dir /path/to/assetsSee ASSETS.md for detailed documentation on Tesla compositor APIs, color/wheel code mappings, and troubleshooting.
The mockserver/ directory contains a proxy server that lets you test the app with different Tesla car configurations without owning multiple vehicles. It forwards requests to a real Teslamate API instance while injecting mock car information (model, color, trim, wheels).
- Python 3.11+
- uv (for automatic dependency management)
# List available car profiles
./mockserver/server.py --list-cars
# Start the mock server (proxies to upstream and injects car overrides)
./mockserver/server.py --upstream http://your-teslamate-api:4000 --car modely_juniper_grey_19
# With custom port
./mockserver/server.py -u http://localhost:4000 -c model3_highland_white_18 -p 5000Then configure the app to connect to http://localhost:4001 (or your chosen port) instead of the real Teslamate API.
| Option | Description |
|---|---|
-u, --upstream |
Upstream Teslamate API URL (required) |
-c, --car |
Car profile name from cars.json (required) |
-p, --port |
Port to run mock server on (default: 4001) |
--host |
Host to bind to (default: 127.0.0.1) |
--cars-file |
Path to cars config JSON (default: cars.json) |
--list-cars |
List available car profiles and exit |
Car profiles are defined in mockserver/cars.json. Each profile specifies overrides that get deep-merged into the API response for /api/v1/cars/* endpoints:
{
"profile_name": {
"car_details": {
"model": "Y",
"trim_badging": "74"
},
"car_exterior": {
"exterior_color": "StealthGrey",
"spoiler_type": "None",
"wheel_type": "Crossflow19"
}
}
}The naming convention for profiles is: {model}_{generation}_{color}_{wheels}
Pre-configured profiles include:
- Model 3 Legacy:
model3_legacy_white_18,model3_legacy_black_18, etc. - Model 3 Highland:
model3_highland_white_18,model3_highland_grey,model3_highland_perf_red, etc. - Model Y Legacy:
modely_legacy_white_gemini,modely_legacy_blue_induction, etc. - Model Y Juniper:
modely_juniper_white_19,modely_juniper_grey_20,modely_juniper_perf_red, etc. - Model S/X:
models_plaid_white,modelx_plaid_blue,models_100d_silver, etc. - Cybertruck:
cybertruck_foundation
Models: 3, Y, S, X, Cybertruck
Trim badging:
- Model 3 Highland:
LRAWD,P(Performance) - Model 3 Legacy:
74D,P74D - Model Y Juniper:
50(RWD),74(AWD),P74D(Performance) - Model Y Legacy:
74D,P74D - Model S/X:
100D,Plaid
Exterior colors: PearlWhite, StealthGrey, DeepBlue, UltraRed, RedMulticoat, MidnightSilver, SolidBlack, Quicksilver, BlackDiamond, ObsidianBlack, StainlessSteel
Wheel types:
- Model 3 Highland:
Glider18,Pinwheel18CapKit,Photon18,Performance20 - Model 3 Legacy:
Pinwheel18,Sport19,Performance20 - Model Y Juniper:
Photon18,Crossflow19,Helix20,Uberturbine21 - Model Y Legacy:
Gemini19,Induction20,Uberturbine21 - Model S:
Tempest19 - Model X:
Turbine22,Cyberstream20 - Cybertruck:
Cybertruck20
- The server listens on the configured port (default 4001)
- All incoming requests are proxied to the upstream Teslamate API
- For
/api/v1/cars/*endpoints, the response JSON is modified to include the overrides from the selected car profile (deep-merged intocar_detailsandcar_exterior) - Other endpoints are passed through unchanged
# Unit tests
./gradlew test
# Instrumented tests (requires emulator/device)
./gradlew connectedAndroidTestReleases are automated via GitHub Actions. When a release is published, the workflow builds the APK and attaches it to the release, and deploys to Google Play.
The recommended way to create releases is using the /release skill in Claude Code, which automates:
- Version bumping in
app/build.gradle.kts(versionCode and versionName) - Updating
CHANGELOG.mdwith the release date - Creating Fastlane changelogs in all supported languages
- Committing, tagging, and pushing
- Creating the GitHub release
The Play Store listing is managed through Fastlane metadata in fastlane/metadata/android/:
fastlane/metadata/android/
├── en-US/ # English (default)
│ ├── title.txt
│ ├── short_description.txt
│ ├── full_description.txt
│ └── changelogs/
│ └── {versionCode}.txt
├── it-IT/ # Italian
├── es-ES/ # Spanish
└── ca-ES/ # Catalan
Each release requires a changelog file named {versionCode}.txt (e.g., 24.txt) in all locale directories. The /release skill automatically creates translated changelogs for all supported languages.
The versionCode is generated from Unix epoch / 10 via scripts/bump-version-code.sh. This produces a monotonically increasing integer with 10-second granularity, shared by both stable and nightly alpha releases. The scheme is safe until ~2650 (well within int32 range).
# Preview what the next versionCode would be
./scripts/bump-version-code.sh --dry-run
# Update build.gradle.kts with a new versionCode
./scripts/bump-version-code.shA GitHub Actions workflow runs daily at 04:00 UTC. If there are new commits on main since the last 24h, it builds and uploads an alpha AAB to the Google Play closed testing (alpha) track with version name MAJOR.MINOR-alpha-YYYYMMDD.
If releasing manually:
# 1. Bump versionCode
./scripts/bump-version-code.sh
# 2. Update versionName in app/build.gradle.kts
# 3. Update CHANGELOG.md with release notes
# 4. Create changelogs in fastlane/metadata/android/{locale}/changelogs/{versionCode}.txt
# 5. Commit and push
# 6. Create a release with GitHub CLI
gh release create v1.2.0 --generate-notes
# Or create a draft release to edit notes first
gh release create v1.2.0 --generate-notes --draftFor release signing with a custom keystore, set these repository secrets:
KEYSTORE_BASE64: Base64-encoded keystore file (base64 -w0 your.keystore)KEYSTORE_PASSWORD: Keystore passwordKEY_ALIAS: Key aliasKEY_PASSWORD: Key password
Without secrets, the APK is signed with a debug keystore (fine for sideloading, not for Play Store).
- Start your Android emulator or connect a device
- Build and install:
make install - View logs:
adb logcat | grep -i matedroid
| Target | Description |
|---|---|
make build |
Build debug APK |
make install |
Build and install debug APK on connected device |
make run |
Build, install, and launch the app |
make clean |
Clean build artifacts |
make test |
Run unit tests |
Or use Android Studio:
- Open the project folder
- Wait for Gradle sync
- Click Run (green play button)
On first launch, you'll be prompted to configure your TeslamateApi connection:
- Server URL: Your TeslamateApi instance URL (e.g.,
https://teslamate-api.example.com) - API Token: (Optional) If your instance requires authentication