Drop yourself into Türkiye's most famous places with AI, then share it.
Lokaly is a social photo app. You upload a photo of yourself, pick a city and a landmark, and AI realistically places you at that spot — keeping your face and appearance intact. The generated image lands in a social feed where it can be liked, commented on, and shared.
| Login | Google Sign-In |
|---|---|
| 1 · Select City | 2 · Select Place | 3 · Select Photo |
|---|---|---|
| Generating | Result | Native Share |
|---|---|---|
| Feed | Comments | Profile |
|---|---|---|
| Edit Profile | Settings (EN) | Settings (TR) |
|---|---|---|
- 📸 AI placement — upload your photo and get realistically placed at a famous Turkish landmark
- 🏙️ 81 cities — landmark suggestions are fetched live per city via Gemini
- 🤖 Person-preserving generation — your face and appearance stay identical to the reference
- 🧭 Guided 4-step create flow — City → Place → Photo → Result with a progress bar
- ❤️ Social feed — like, comment, and follow other users
- 📤 Share — share generated photos through the native share sheet
- 👤 Profiles — stats, post grid, editable username / bio / avatar
- 🌗 Theme — Light / Dark / System
- 🌍 Localization — Turkish & English (
easy_localization) - 🔐 Auth — email/password and Google Sign-In via Firebase
| Layer | Technology |
|---|---|
| Framework | Flutter (Dart SDK ^3.12) |
| Page state | flutter_bloc (Cubit) |
| Global state | provider (AppState) |
| Navigation | go_router |
| Extensions | kartal |
| Auth | firebase_auth + google_sign_in |
| Database | cloud_firestore |
| Storage | firebase_storage |
| AI (landmarks) | firebase_ai — Gemini 2.5 Flash |
| AI (image) | firebase_ai — Gemini 2.5 Flash Image |
| Images | image_picker, cached_network_image, flutter_image_compress |
| Sharing | share_plus |
| Localization | easy_localization |
| Fonts | google_fonts (Manrope) |
| Env | flutter_dotenv |
| Models | freezed + json_serializable |
| Unit tests | flutter_test, mockito, bloc_test |
| Integration tests | patrol |
- Flutter SDK (latest stable, Dart
^3.12) - A Firebase project with Authentication (Email + Google), Firestore, and Storage enabled
- Firebase AI (Gemini) enabled on the project
flutterfire_cliinstalled
git clone https://github.com/burakJs/Lokaly.git
cd lokaly
flutter pub getConfigure Firebase (generates firebase_options.dart and platform config):
dart pub global activate flutterfire_cli
flutterfire configureCreate a .env file in the project root (it is referenced as an asset in pubspec.yaml):
# Optional — only needed for Google Sign-In on Android.
# The Google "Web client" OAuth ID (serverClientId). Leave empty to rely on
# platform config (google-services.json / Info.plist).
GOOGLE_SERVER_CLIENT_ID=
# Gemini access is provided through the Firebase project (firebase_ai),
# so no separate Gemini API key is required at runtime.The app loads
.envon startup, so the file must exist (it can be empty).
Run code generation (freezed / json_serializable / mockito):
dart run build_runner build --delete-conflicting-outputsRun the app:
flutter run# Unit & widget tests
flutter test
# With coverage
flutter test --coverage
# Patrol integration / E2E tests (Android)
patrol test
# Patrol integration / E2E tests (iOS)
patrol test --iosThe codebase follows a core / feature / product layering convention.
lib/
├── main.dart # App bootstrap: init + global providers + runApp
├── app/ # LokalyApp (MaterialApp) + go_router config
├── core/ # Infrastructure: theme, init, constants (UI-agnostic)
│ ├── theme/ # AppColors, text styles, ThemeData (light/dark)
│ ├── constants/ # App constants + Turkey cities
│ └── init/ # dotenv + Firebase initialization
├── feature/ # One folder per screen: cubit/ (state) + view/ (UI)
│ ├── splash/ # auth-state redirect
│ ├── auth/ # login + register
│ ├── feed/ # home feed
│ ├── create/ # city → landmark → photo → generating → result
│ ├── comments/ # realtime comments sheet
│ ├── profile/ # profile + edit profile
│ ├── post_detail/ # single post view
│ └── settings/ # theme + language + account
└── product/ # Cross-feature shared code
├── state/ # AppState (global ChangeNotifier)
├── models/ # user / post / comment models
├── services/ # auth, firestore, gemini, image gen, share, etc.
├── widgets/ # shared widgets (logo, post card, badges...)
└── navigation/ # route paths
test/ # Unit + widget + cubit tests (flutter_test)
integration_test/ # Patrol E2E tests
screenshots/ # Images used in this README
Two-layer state management:
- Page state → each screen owns a Cubit (
flutter_bloc) underfeature/<x>/cubit/. Holds screen-local concerns: form, loading, error, lists, selection. - Global state →
AppState(provider/ChangeNotifier) underproduct/state/. Holds app-wide concerns: theme mode, current user, locale.
View (feature/<x>/view)
↓ (BlocBuilder / BlocProvider)
Cubit (feature/<x>/cubit) ←→ AppState (global, via provider)
↓ (constructor injection)
Services (product/services: Auth, Firestore, Gemini, Image, Share)
↓
Firebase · Gemini (firebase_ai)
Rule: service calls happen inside Cubits, never in the view. Cubits receive their services via constructor injection so they can be mocked in tests.
users/{uid} → username, email, photoURL, bio,
followersCount, followingCount, createdAt
posts/{postId} → userId, userUsername, userPhotoURL,
imageURL, city, landmark,
likesCount, commentsCount, createdAt
posts/{postId}/likes/{uid} → createdAt
posts/{postId}/comments/{commentId} → userId, userUsername, text, createdAt
following/{uid}/userFollowing/{targetUid} → createdAt
followers/{uid}/userFollowers/{targetUid} → createdAt
Likes and follows are written via Firestore transactions so the denormalized counters stay consistent.
Built with Flutter · Firebase · Gemini.