Documentation Index
Fetch the complete documentation index at: https://mintlify.com/ihfaz297/MND/llms.txt
Use this file to discover all available pages before exploring further.
This guide explains how data flows through the MND mobile application, from user interactions to API calls to UI updates.
Application Architecture
The app follows a layered architecture with clear separation of concerns:
User Interface (Screens/Widgets)
↓
State Management (Providers)
↓
Business Logic (Services)
↓
API Client (ApiService)
↓
Backend API
Complete Data Flow Example
Let’s trace a route search from user input to UI display.
User fills out the search form in HomeScreen:
class HomeScreen extends StatefulWidget {
// State variables
String? _fromNode;
String? _toNode;
String _time = '08:30';
// User selects "From" location
DropdownButtonFormField<String>(
value: _fromNode,
onChanged: (value) => setState(() => _fromNode = value),
)
// User taps "Find Routes" button
ElevatedButton(
onPressed: _searchRoutes,
child: Text('Find Routes'),
)
}
Source: lib/screens/home/home_screen.dart:165-230
2. Service Call
The screen calls RouteService to fetch routes:
Future<void> _searchRoutes() async {
setState(() {
_loading = true;
_routes = [];
});
try {
// Call RouteService
final routes = await _routeService.planRoute(
from: _fromNode!,
to: _toNode!,
time: _time,
);
setState(() {
_routes = routes;
_loading = false;
});
} catch (e) {
setState(() => _loading = false);
// Show error
}
}
Source: lib/screens/home/home_screen.dart:55-83
3. API Request
RouteService uses ApiService to make HTTP request:
class RouteService {
final ApiService _api = ApiService();
Future<List<RouteOption>> planRoute({
required String from,
required String to,
required String time,
}) async {
// Make GET request with query parameters
final data = await _api.get('/routes', params: {
'from': from,
'to': to,
'time': time,
});
// Parse response and return models
return (data['options'] as List)
.map((option) => RouteOption.fromJson(option))
.toList();
}
}
Source: lib/services/route_service.dart:13-27
4. HTTP Client
ApiService executes the HTTP request:
Future<Map<String, dynamic>> get(
String endpoint, {
Map<String, String>? params,
bool requireAuth = false,
}) async {
// Build URL with query parameters
final uri = Uri.parse('${ApiConfig.baseUrl}$endpoint')
.replace(queryParameters: params);
// Build headers (with auth token if available)
final headers = await _buildHeaders(requireAuth: requireAuth);
// Execute request
final response = await _client.get(uri, headers: headers)
.timeout(ApiConfig.timeout);
// Handle response
if (response.statusCode == 200) {
return json.decode(response.body);
} else {
throw Exception('Request failed: ${response.statusCode}');
}
}
Source: lib/services/api_service.dart:31-55
5. Backend API
Request is sent to backend:
GET http://192.168.0.114:3000/api/routes?from=CAMPUS&to=AMBARKHANA&time=08:30
6. Response Processing
Backend responds with JSON:
{
"options": [
{
"label": "Campus Express via Ambarkhana",
"category": "fastest",
"totalTimeMin": 45,
"totalCost": 30,
"transfers": 0,
"localTimeMin": 10,
"localDistanceMeters": 500,
"legs": [
{
"mode": "bus",
"from": "CAMPUS",
"to": "AMBARKHANA",
"departure": "08:35",
"arrival": "09:20",
"cost": 30
}
]
}
]
}
7. Model Parsing
JSON is parsed into Dart models:
class RouteOption {
final String label;
final String category;
final int totalTimeMin;
final List<RouteLeg> legs;
factory RouteOption.fromJson(Map<String, dynamic> json) {
return RouteOption(
label: json['label'],
category: json['category'],
totalTimeMin: json['totalTimeMin'],
totalCost: json['totalCost'],
transfers: json['transfers'],
localTimeMin: json['localTimeMin'],
localDistanceMeters: json['localDistanceMeters'],
legs: (json['legs'] as List)
.map((leg) => RouteLeg.fromJson(leg))
.toList(),
);
}
}
Source: lib/models/route_option.dart:24-35
8. State Update
Screen updates state with parsed data:
setState(() {
_routes = routes; // List<RouteOption>
_loading = false;
});
9. UI Rendering
Widget tree rebuilds with new data:
ListView.builder(
itemCount: _routes.length,
itemBuilder: (context, index) {
final route = _routes[index];
return RouteCard(route: route);
},
)
Source: lib/screens/home/home_screen.dart:246-256
10. Display Component
RouteCard displays the route:
RouteCard(
route: route,
onFavorite: auth.isLoggedIn ? () => _addFavorite(route) : null,
)
Source: lib/screens/home/home_screen.dart:251-254
Request/Response Flow Diagram
User Interaction
↓
UI Component (StatefulWidget)
↓
setState()
↓
Service Method
↓
ApiService
↓
HTTP Client
↓
Backend API
↓
JSON Response
↓
Model.fromJson()
↓
Service returns List<Model>
↓
setState() with data
↓
Widget rebuild
↓
UI Update
Authentication Flow
Authenticated requests include additional steps.
1. User Login
final authProvider = Provider.of<AuthProvider>(context);
await authProvider.sendMagicLink('[email protected]');
// User clicks link in email
await authProvider.verifyToken(token);
2. Token Storage
// AuthService stores token
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', token);
Source: lib/services/auth_service.dart:60-62
3. Token Retrieval
// ApiService retrieves token for each request
Future<String?> _getAuthToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('auth_token');
}
Source: lib/services/api_service.dart:10-13
final token = await _getAuthToken();
if (token != null) {
headers['Authorization'] = 'Bearer $token';
}
Source: lib/services/api_service.dart:21-23
5. Authenticated Request
GET http://192.168.0.114:3000/api/favorites
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Error Handling Flow
Errors propagate up the call stack.
1. Network Error
try {
final response = await _client.get(uri);
} catch (e) {
throw Exception('Network error: $e');
}
Source: lib/services/api_service.dart:52-54
2. HTTP Error
if (response.statusCode == 401) {
throw Exception('Session expired. Please login again.');
} else {
throw Exception('Request failed: ${response.statusCode}');
}
Source: lib/services/api_service.dart:46-50
3. Service Catches Error
Service passes error to caller (or handles it).
4. UI Handles Error
try {
await _routeService.planRoute(...);
} catch (e) {
setState(() => _loading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to find routes: $e')),
);
}
Source: lib/screens/home/home_screen.dart:76-82
State Management Flow (with Provider)
Using Provider centralizes state management.
1. User Action
void _searchRoutes() {
final routeProvider = Provider.of<RouteProvider>(context, listen: false);
routeProvider.planRoute(
from: _fromNode!,
to: _toNode!,
time: _time,
);
}
2. Provider Updates State
class RouteProvider extends ChangeNotifier {
Future<void> planRoute(...) async {
_isLoading = true;
notifyListeners(); // UI shows loading
try {
_routes = await _routeService.planRoute(...);
_isLoading = false;
notifyListeners(); // UI shows routes
} catch (e) {
_error = e.toString();
_isLoading = false;
notifyListeners(); // UI shows error
}
}
}
3. UI Reacts
Consumer<RouteProvider>(
builder: (context, routeProvider, child) {
if (routeProvider.isLoading) {
return CircularProgressIndicator();
}
return ListView.builder(
itemCount: routeProvider.routes.length,
itemBuilder: (context, index) {
return RouteCard(route: routeProvider.routes[index]);
},
);
},
)
Real-time Data Flow
For features like bus schedules that update frequently:
1. Initial Load
@override
void initState() {
super.initState();
_loadBuses();
}
2. Pull to Refresh
RefreshIndicator(
onRefresh: _loadBuses,
child: ListView(...),
)
Source: lib/screens/buses/upcoming_buses_screen.dart:196-198
3. Periodic Updates (Optional)
Timer.periodic(Duration(seconds: 30), (timer) {
if (mounted) {
_loadBuses();
}
});
Local Data Persistence
Some data is cached locally.
1. Save to Storage
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', token);
await prefs.setString('user_data', json.encode(userData));
Source: lib/services/auth_service.dart:60-62
2. Load from Storage
Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
_authToken = prefs.getString('auth_token');
final userData = prefs.getString('user_data');
if (userData != null) {
_currentUser = User.fromJson(json.decode(userData));
}
}
Source: lib/services/auth_service.dart:19-27
3. Clear Storage
Future<void> logout() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('auth_token');
await prefs.remove('user_data');
}
Source: lib/services/auth_service.dart:109-111
Best Practices
1. Separation of Concerns
- UI: Only rendering and user interactions
- Services: Business logic and API calls
- Models: Data structures
- Providers: State management
2. Error Propagation
- Services throw exceptions
- UI catches and displays errors
- Provide user-friendly error messages
3. Loading States
- Always show loading indicators
- Disable buttons during async operations
- Prevent duplicate requests
4. Async/Await
- Use
async/await for cleaner code
- Handle errors with try/catch
- Always check
mounted before setState
5. State Updates
- Call
setState() after state changes
- Call
notifyListeners() in providers
- Avoid unnecessary rebuilds
Common Patterns
Fetch Data Pattern
List<Item> _items = [];
bool _loading = false;
String? _error;
Future<void> _loadData() async {
setState(() {
_loading = true;
_error = null;
});
try {
final items = await _service.fetchItems();
setState(() {
_items = items;
_loading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_loading = false;
});
}
}
Future<void> _submitForm() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() => _loading = true);
try {
await _service.submitData(_formData);
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Success!')),
);
// Navigate away or refresh
Navigator.pop(context);
} catch (e) {
setState(() => _loading = false);
// Show error
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
}
Next Steps