Wanderly is a personal travel assistant for Android. It uses your live location to surface highly-rated places nearby, builds a balanced day-by-day itinerary tailored to your interests and budget, and gets you there with on-screen routes and a one-tap handoff to Google Maps. Trips, accounts, and preferences are all stored on-device.
- First-launch onboarding — illustrated 3-page intro that introduces the app's main features
- Local accounts — sign up and log in with username + password; sessions persist across launches
- GPS & location via FusedLocationProvider, with permission handling and graceful "Locating…" fallbacks
- Functional Google Map with stylized user marker, in-app route polylines, place markers, search-on-map, and an "Open in Google Maps" handoff for turn-by-turn nav
- Home tab — interesting places near you with photos, ratings, weather, and Wikipedia summaries
- Itinerary planner — pick a city, days, budget, transport, and from 10 interest chips (Food, Culture, History, Nightlife, Nature, Adventure, Shopping, Wellness, Family, Coffee). The planner generates a full day-by-day plan with meals and activities, costs, and timings
- Saved trips — persisted in Room, scoped per user so different accounts on the same device see only their own trips
- Profile — real account info, member-since date, current location, saved-trip count, and sign-out flow
Wanderly needs two API keys to run with full functionality:
- Google Maps key — with Maps SDK for Android, Directions API, Places API (New), and the legacy Places API all enabled in the Cloud project. Same key for all four.
- OpenWeatherMap key — newly created keys take up to 2 hours to activate.
Both are read from local.properties at the project root. That file is gitignored, so each developer keeps their own keys locally.
To set up:
- Copy
local.properties.exampletolocal.properties(or, if Android Studio already generatedlocal.propertieswith the SDK path, just append the two API key lines from the example). - Paste your keys into the empty values.
- File → Sync Project with Gradle Files, then Run.
If a key is missing, the app degrades gracefully — the Map tab won't render, weather will show "Unavailable", and the itinerary will fall back to a curated NYC pool with a banner.
Wanderly follows MVVM with a clean repository layer:
- UI is composed of stateless Compose screens that read state via
StateFlow - ViewModels own all business logic and state. They never touch Retrofit or Room directly
- Repositories abstract data sources — same call site whether data comes from cache (Room) or network (Retrofit)
- Coroutines are scoped to
viewModelScope; failures bubble up as sealed result types ornullso the UI can render fallbacks instead of crashing
A representative slice:
WeatherCard ── collects ─► WeatherViewModel ── reads ─► WeatherRepository
│
├── Retrofit (OpenWeather)
└── Room (PlaceEntity cache)
The auth flow follows the same pattern: LoginScreen ⇆ AuthViewModel ⇆ AuthRepository ⇆ UserDao.
Room is the single source of truth for on-device data. Three tables, schema version 5:
key— composite of LatLng + query type (PRIMARY KEY)json— serialized Place response from Google's APItimestamp— used to expire cached entries older than 24 h
id— auto-increment primary keyuserId— owning account; indexed, used as a filter in every read/writecity,days,stopCount,totalCost,transportMode,savedAt— display summarypayloadJson— full serialized itinerary (setup + day-by-day plan)
id— auto-increment primary keyusername— unique index, lowercased on writedisplayNamepasswordHash+passwordSalt— base64-encoded; see Authentication & PrivacycreatedAt
Schema migrations are real (not destructive): MIGRATION_3_4 adds the users table; MIGRATION_4_5 adds userId to saved_trips and back-fills any pre-auth rows with the first registered user, so legacy data isn't lost on upgrade.
| API | Purpose |
|---|---|
| OpenWeather | Current weather at the user's location |
| Google Maps SDK for Android | Embedded map, markers, polylines |
| Google Places API (legacy) | Home recommendations + photos (the photo endpoint is legacy-only) |
| Google Places API (New) | Itinerary generation — uses fine-grained includedTypes filters that aren't available on the legacy API |
| Google Directions API | Encoded polyline that we decode and draw on the map |
| Wikipedia (Geosearch + Summary) | Background information for tapped places |
- GPS / Location services via
FusedLocationProviderClient. Updates are coalesced to ~2-5 second intervals and cached so background tab switches don't burn battery. BothACCESS_FINE_LOCATIONandACCESS_COARSE_LOCATIONare requested at runtime, with a degraded-state path if the user denies permission.
Accounts live entirely on-device. There is no backend.
- Hashing. Passwords are hashed with
PBKDF2WithHmacSHA256, 120 000 iterations, 256-bit derived key, with a fresh 16-byteSecureRandomsalt per user. Verification usesMessageDigest.isEqualfor constant-time comparison so login does not leak timing information. - Storage. Only the Base64-encoded hash and salt are persisted; raw passwords are wiped from the
PBEKeySpecimmediately after hashing. - Sessions. A small
SharedPreferencesfile (wanderly_session) stores the currentuserIdand an onboarding-seen flag. Nothing sensitive is stored there. - Per-user data isolation. Every read and write to
saved_tripsis filtered byuserId.get(id, userId)anddelete(id, userId)both require a user ID match, so even a buggy load-by-id can't reach across accounts.
Work was divided by feature area, with explicit handoff seams (e.g. the Map tab takes three independent focus states, one per feature owner).
- Home screen + map screen architecture
- Wikipedia, Places (legacy), Directions, OpenWeather wiring
- Room scaffolding, MVVM patterns, and
secrets-gradle-pluginfor API key injection - Map tab integration: in-app route polylines, search-on-map, place detail sheet, stylized user marker
- Geocoding helpers and home-screen recommendations
- Itinerary planner (TripSetup → ItineraryScreen, day-by-day rendering, multi-stop deeplinks) and User Profile
- Places API (New) integration with granular type filters
SavedTripRoom entity + repository + ViewModel- Local authentication: PBKDF2 password hashing,
AuthRepositorywith sealedAuthResult,AuthViewModel, Login/Signup screens - First-launch onboarding pager with Material 3 illustrated panels
- Per-user data isolation (Room v4 → v5 migration with orphan-claim) and Profile redesign
The bulk of testing was on the Android Emulator with LogCat as the primary signal. We exercised:
- Permission flows (deny → re-prompt → grant)
- Configuration changes (rotation, dark/light theme, font scale) to verify state survives via
rememberSaveableand StateFlow - Network failure modes (key missing, API quota, no network) to confirm the graceful-fallback paths actually render
- DB upgrade paths (each migration was tested by installing the prior schema, then upgrading, and asserting no data loss)
LLM tools (Gemini in the IDE, Claude, and ChatGPT) helped trace LogCat output, propose hypotheses, and rewrite specific composables. Every AI suggestion was reviewed against the existing patterns before merging.
Across the semester we used Gemini (in the Android Studio companion), Claude, and ChatGPT to accelerate development. Every AI suggestion was reviewed before being accepted, and several were explicitly rejected (see below). The work and architectural decisions are ours; AI was a productivity multiplier, not a designer.
- Architecture. AI helped articulate the MVVM + Repository + StateFlow split early, especially the rule that ViewModels don't import Retrofit or Room directly. That rule held up across the whole codebase.
- Code. Boilerplate-heavy work — Compose layouts, Retrofit interfaces, Room DAO methods, ViewModelScope coroutine launches — was AI-assisted. Specific examples:
- The
AuthScaffoldshared composable was iterated with AI to keep Login and Signup visually identical without copy-paste - The
flatMapLatestpattern inSavedTripsViewModel(for re-subscribing to Room on user change) was AI-suggested when we ran into a race condition between auth state and trip queries - The PBKDF2
PBEKeySpec+SecretKeyFactorysnippet came from AI; we audited it against Android docs before merging
- The
- Testing. AI helped reproduce edge cases (e.g. "what does the app do if the user denies location permission and immediately rotates?") and write the migration test cases.
- UX. Material 3 specifics — gradient brushes, surface alpha layering, animated indicator widths — were largely AI-driven first drafts that we then trimmed for taste.
- Material 3 UI scaffolding (cards, lists, chips, dialogs) — easily a 3-4× speedup on screens like Profile and the auth/onboarding flows
- API result data classes with Gson deserialization — tedious by hand, near-instant via AI
- Migration SQL boilerplate —
ALTER TABLE, index naming conventions, and theCOALESCEtrick for orphan claiming - Cross-referencing deprecation warnings (e.g.
Icons.Outlined.Login→Icons.AutoMirrored.Outlined.Login) and applying fixes in bulk
- FourSquare Places API. ChatGPT recommended FourSquare for richer POI data. Rejected — restrictive free tier, more involved auth, and no real advantage over Google Places (New) for our use cases.
- SHA-256 for password hashing. Initially proposed by AI as a simple option. Rejected — SHA-256 is too fast and GPU-friendly for password storage. We switched to PBKDF2 with 120k iterations to make brute-force prohibitively slow without adding a third-party dependency.
- Firebase Authentication. Suggested for "real" login. Rejected — adds a backend dependency, OAuth setup, and extra build configuration that didn't match our scope (a personal travel assistant with on-device data). Local Room-backed auth fit the existing architecture.
- Hardcoded demo seed credentials. Suggested as a way to skip the signup screen during the demo. Rejected — embedding credentials in source is a code-quality smell, and the signup flow itself is a strong demo beat.
- Mock the database in unit tests. Rejected on the same principle as our integration tests on real Room: mock divergence has burned us before, and Room's in-memory builder is fast enough to use for free.
- Excessive comments. AI tends to over-comment generated code. We removed almost all explanatory comments — well-named identifiers and small functions document themselves; comments should only justify non-obvious decisions (see e.g. the
MIGRATION_4_5comment).
Prompt: "i would like the places in the PlacesCards composable to be sorted in order of distance from the user. please implement this"
Response: "I have updated PlacesCards.kt to sort the list of places by distance from the user before rendering…"
Follow-up: "we are now calculating distance twice for this, once when sorting and once for rendering cards, right? is this not inefficient?"
Response: "You're absolutely right. Calculating the distance twice (once for sorting and once inside each card) is redundant…"
The follow-up is the lesson: AI's first answer is usually fine functionally and often careless about performance. Reviewing as a critical reader (not a typist) is what makes the collaboration useful.