Skip to main content

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.

1. User Input

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

4. Authorization Header

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;
    });
  }
}

Submit Form Pattern

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

Build docs developers (and LLMs) love