awaneetdecoder/civicnexus-app
CivicNexus के लिए Flutter फ्रंटएंड। Gemini 2.5 Flash पर आधारित AI-सशक्त नागरिक मुद्दों की रिपोर्टिंग, स्वचालित जीपीएस कैप्चर, JWT प्रमाणीकरण, और खेल-आधारित नागरिक पुरस्कार।
- Stars
- 1
- Forks
- 0
- अंतिम पुश (UTC)
- 5 जुल॰ 2026
- प्रोजेक्ट स्थिति
- सक्रिय
तकनीकी विषय
उपयोग की गई निर्भरताएँ
निर्भरता सूची 11 आइटम
- flutter
{"sdk":"flutter"} - cupertino_icons
^1.0.6 - page_transition
^2.1.0 - provider
^6.1.2 - geolocator
^12.0.0 - geocoding
^3.0.0 - image_picker
^1.1.2 - http
^1.2.1 - flutter_secure_storage
^9.2.4 - flutter_testडेवलपमेंट
{"sdk":"flutter"} - flutter_lintsडेवलपमेंट
^3.0.0
मूल README
यह अंग्रेज़ी मूल स्नैपशॉट है। नवीनतम सामग्री GitHub पर देखें।
README खोलें / बंद करें
CivicNexus — Flutter App
Flutter Dart Gemini Status License
Mobile frontend for CivicNexus — the AI-powered civic issue reporting platform. Citizens photograph a problem; the app handles GPS capture, AI categorization, and real-time status, with no manual form-filling.
⚙️ Companion repo — Spring Boot backend: civicnexus-backend
Architecture note. This app talks to the full Spring Boot backend above. For the hackathon's mandatory live demo, the same Gemini-driven flow — photo analysis, severity scoring, map, community upvoting, gamified rewards — also ships as a Firebase-hosted web client, built to deploy reliably inside a hard deadline. Live demo: civicnexus-94d0b.web.app
Table of Contents
- What This Does
- Tech Stack
- Project Structure
- Key Features
- Running Locally
- API Integration Pattern
- How Frontend and Backend Connect
- What's Built
- Roadmap
- Known Limitations
- What This Project Demonstrates
- Author
What This Does
Citizens open the app, photograph a civic problem, tap submit. The app captures GPS coordinates automatically, sends the photo to the backend, and displays the Gemini AI analysis result — issue type, severity, responsible department, and coins earned — within 3–5 seconds. No manual categorization, no form-filling beyond an optional description.
Tech Stack
| Layer | Technology |
|---|---|
| Framework | Flutter 3.32, Dart 3.8 |
| State Management | Provider |
| HTTP | dart:http (multipart for image upload) |
| Storage | flutter_secure_storage (JWT token) |
| Location | geolocator + geocoding |
| Image | image_picker |
| Navigation | page_transition |
| Theme | Dynamic light/dark mode via ThemeNotifier |
Project Structure
lib/
├── main.dart # App entry, theme setup, auth check on startup
├── auth_screen.dart # Login with JWT
├── signup_screen.dart # Registration
├── main_shell.dart # Bottom nav shell
├── api_config.dart # Single source of truth for all API endpoints
├── theme_notifier.dart # Light/dark mode state
├── models/
│ ├── issue_model.dart # Typed model for Issue API response
│ └── report_model.dart # Legacy report model
├── screens/
│ ├── home_screen.dart # Landing/dashboard
│ ├── reporter_screen.dart # Photo + GPS + submit flow
│ ├── activity_screen.dart # User's own reports
│ └── profile_screen.dart # Coins, settings, dark mode toggle
├── services/
│ ├── gemini_service.dart # Direct Gemini API call from Flutter
│ └── secure_storage_service.dart # JWT save/read/delete
└── widget/
├── activity_list_item.dart # Report card widget
└── app_logo.dart # Shared logo widget
Key Features
Automatic GPS capture. Location fetches on button tap using geolocator with high accuracy. Coordinates are stored in state and sent with every report; geocoding reverse-resolves them to a readable address for display.
Secure JWT storage. After login, the token is stored using flutter_secure_storage — backed by Android Keystore on Android and Keychain on iOS. Every authenticated request reads this token and sends it in the Authorization: Bearer header.
Multipart image upload. Report submission uses http.MultipartRequest to send the image file alongside text fields (latitude, longitude, address, description) in a single HTTP request. The backend's @RequestPart and @RequestParam annotations receive them separately.
Gemini analysis display. On successful submission, a dialog shows the AI result — issue type, severity, responsible department, citizen advisory, and coins awarded — and resets the form on dismissal.
Dynamic theme. Light and dark themes are defined in main.dart via ThemeData. A ThemeNotifier (ChangeNotifier) stores the active mode and exposes a toggle; the profile screen's switch calls toggleTheme(), and the whole app re-renders through Consumer<ThemeNotifier>.
Auto login check. On startup, main.dart reads the stored JWT. If present and non-empty, the user lands directly on MainShell, skipping the login screen; otherwise they see AuthScreen. This runs inside a FutureBuilder with a loading spinner while the check completes.
Running Locally
Prerequisites: Flutter 3.32+, Android Studio or VS Code with the Flutter extension, an Android emulator or physical device.
git clone https://github.com/awaneetdecoder/civicnexus-app
cd civicnexus-app
flutter pub get
Update lib/api_config.dart:
// For Android emulator
static const String _baseUrl = 'http://10.0.2.2:8080';
// For physical device (use your PC's local IP)
static const String _baseUrl = 'http://192.168.x.x:8080';
// For a deployed backend
static const String _baseUrl = 'https://civicnexus-backend.onrender.com';
flutter run
Note: Android 9+ blocks plain HTTP by default. For local development, android/app/src/main/res/xml/network_security_config.xml is configured to allow cleartext traffic to 10.0.2.2 and localhost.
API Integration Pattern
Every screen that fetches data follows this exact pattern:
// 1. Declare future in state
Future<List<IssueModel>>? _issuesFuture;
// 2. Start fetch in initState
@override
void initState() {
super.initState();
_issuesFuture = _fetchIssues();
}
// 3. Use FutureBuilder in build
FutureBuilder<List<IssueModel>>(
future: _issuesFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting)
return CircularProgressIndicator();
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
return ListView(children: snapshot.data!.map(...).toList());
},
)
This pattern handles loading, error, and data states explicitly — no unhandled nulls.
How Frontend and Backend Connect
Flutter Spring Boot
─────── ───────────
1. User taps Login
│
├── http.post(/api/auth/login)
│ Body: {email, password} JSON
│ JwtAuthFilter skips (public endpoint)
│ AuthController.login()
│ AuthService validates credentials
│ JwtService.generateToken(user)
│ Returns {token, name, email, coins}
│
├── SecureStorageService.saveToken(token)
└── Navigate to MainShell
2. User submits report
│
├── http.MultipartRequest POST /api/issues
│ Header: Authorization: Bearer <token>
│ Fields: latitude, longitude, address
│ File: image
│ JwtAuthFilter intercepts
│ Extracts token from header
│ JwtService.extractEmail(token)
│ UserRepository.findByEmail()
│ Sets SecurityContext
│ IssueController.submitIssue()
│ @AuthenticationPrincipal User → current user
│ IssueService.submitIssue()
│ → saveImageLocally()
│ → GeminiService.analyzeIssue()
│ → issueRepository.save()
│ → update user coins
│ Returns IssueResponse JSON
│
└── Parse response → show success dialog with AI analysis
What's Built
- [x] JWT auth — login, signup, auto-login check on startup
- [x] GPS auto-capture and reverse geocoding on the report flow
- [x] Multipart photo + GPS submission to the backend
- [x] Client-side Gemini pre-submission preview for instant feedback
- [x] Activity screen — user's own report history via the
FutureBuilderpattern - [x] Profile screen — coins, settings, dark mode toggle
- [x] Secure token storage via platform Keystore/Keychain
Roadmap
- [ ] Issue map screen —
GET /api/issues/allandIssueModelalready exist on the backend side; the interactive map UI consuming them is the remaining piece - [ ] Resolution flow UI for municipal workers, gated behind the backend's planned role-based access control
- [ ] Move the client-side Gemini call behind a backend proxy endpoint (see Known Limitations)
- [ ] Push notifications on issue status change (currently pull-based, refreshed on screen load)
- [ ] Widget and integration test suite
Known Limitations
Gemini API key is embedded in the client. gemini_service.dart calls Gemini directly for a fast pre-submission preview, which means the key ships inside the compiled app and can be extracted by anyone who decompiles it. The backend's own server-side key — used for the authoritative analysis — is not exposed this way. The fix is straightforward: route the preview call through a lightweight backend proxy endpoint instead of calling Gemini from the client at all.
No offline support. Report submission requires an active connection; there's no local queueing for spotty connectivity, which matters for the field conditions this app is actually designed for.
What This Project Demonstrates
- Building a complete mobile client against a real, self-built REST API — multipart uploads, JWT-secured requests, structured AI responses
- Secure credential handling using platform-native secure storage rather than shared preferences
- Disciplined async data loading — every fetch goes through an explicit loading/error/data state machine, not optimistic rendering
- Understanding of Android's network security model, not just working around an error message
- The ability to identify a project's own client-side security gap and describe the correct fix, rather than leaving it unexamined
Author
Awaneet Mishra @awaneetdecoder · awaneet03991@gmail.com
License
MIT — free to use for educational purposes.