Skip to main content
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