Skip to main content
Trackmart implements state management using Flutter’s built-in StatefulWidget pattern combined with SharedPreferences for persistence and Firebase real-time listeners for reactive updates. This approach provides a balance between simplicity and functionality for the app’s requirements.

State Management Architecture

Local State

Managed by StatefulWidget for UI-specific state like form inputs, tab selection, and loading states.

Persisted State

Stored in SharedPreferences for user preferences, authentication tokens, and cached user data.

Remote State

Synchronized via Firebase listeners for real-time updates of orders, messages, and driver locations.

StatefulWidget Pattern

Trackmart uses StatefulWidget extensively for managing component-level state.

Authentication State (RootPage)

The RootPage manages the global authentication state:
enum AuthStatus { notSignedIn, signedIn, loading }

class RootPageState extends State<RootPage> {
  AuthStatus authStatus = AuthStatus.loading;
  String currentUserId;
  bool login = false;
  Firestore firestore;
  
  @override
  void initState() {
    firestore = Firestore.instance;
    super.initState();
    
    FirebaseAuth.instance.currentUser().then((user) {
      setState(() {
        authStatus = user == null 
          ? AuthStatus.notSignedIn 
          : AuthStatus.signedIn;
        
        if (user != null) {
          currentUserId = user.uid;
          if (user.email[0] != '+' && !user.isEmailVerified) {
            login = true;
            authStatus = AuthStatus.notSignedIn;
          }
        }
      });
    });
  }
}
The authentication state determines the entire app flow. All other screens are only accessible after reaching the signedIn state.

Home Page State

The home page manages complex state including orders, drivers, and UI interactions:
home_page.dart
class _TabbedGuyState extends State<TabbedGuy> 
    with SingleTickerProviderStateMixin {
  
  // UI State
  bool isLoading = false;
  bool _stillLoading = true;
  bool _isTransacting = false;
  bool _forexError = false;
  bool _isConverting = false;
  bool requested = true;
  bool transit = true;
  bool delivered = true;
  
  // Form State
  double quantity = 1;
  String _searchText = "";
  String _nameText = "";
  String _unit = 'Tonne';
  String _paymnt = 'Mobile money';
  final String _product = 'Sand';
  
  // Data State
  List<Driver> filteredDrivers = [];
  List<HistoryItem> _history = [];
  List<HistoryItem> _filteredHistory = [];
  double rate;
  static int _selected = -1;
  
  // User State
  final String currentUserId;
  String currentUserName;
  String currentUserPhoto;
  String currentUserPhone;
  static double currentLat;
  static double currentLong;
  
  // Firebase References
  DatabaseReference databaseReference;
  FirebaseDatabase database;
  cf.Firestore firestore;
  SharedPreferences prefs;
  
  // Controllers
  TabController _tabController;
  final TextEditingController _filter = TextEditingController();
  final TextEditingController _namefilter = TextEditingController();
  final TextEditingController _moneyController = TextEditingController();
  final TextEditingController _moneyController2 = TextEditingController();
  FocusNode myFocusNode;
  FocusNode myFocusNode2;
  FocusNode _focus;
  FocusNode _focus2;
}
State Categories:
CategoryVariablesPurpose
UI StateisLoading, _stillLoading, _isTransactingControl loading indicators and button states
Form Statequantity, _unit, _paymnt, _searchTextTrack user input across form fields
Data StatefilteredDrivers, _history, rateCache loaded data and filter results
User StatecurrentUserId, currentLat, currentLongStore current user information
Selection State_selected, requested, transit, deliveredTrack UI selections and section visibility

State Initialization

The home page initializes state asynchronously:
1

Firebase Initialization

Configure Firebase services with persistence
database = FirebaseDatabase.instance;
database.setPersistenceEnabled(true);
database.setPersistenceCacheSizeBytes(10000000);

await cf.Firestore.instance.settings(persistenceEnabled: true);
firestore = cf.Firestore.instance;
2

Load Persisted Data

Retrieve cached user data from SharedPreferences
prefs = await SharedPreferences.getInstance();
currentUserName = prefs.getString('displayName');
currentUserPhone = prefs.getString('phoneNo');
currentUserPhoto = prefs.getString('photoUrl');
3

Location Services

Get current location and start tracking
await geolocator
    .getCurrentPosition(desiredAccuracy: LocationAccuracy.best)
    .then((value) {
      _updateLocation(value);
      geolocator.getPositionStream(locationOptions)
          .listen(_updateLocation);
    });
4

Setup Controllers

Initialize text controllers and add listeners
_moneyController.addListener(() {
  if (_focus.hasFocus) {
    _moneyController2.text = 
      (double.parse(_moneyController.text) * (rate ?? 0))
        .toStringAsFixed(0);
  }
});

_filter.addListener(() {
  setState(() {
    _searchText = _filter.text;
    _filteredHistory = _history.where((item) =>
      item.driver.toLowerCase().contains(_searchText.toLowerCase())
    ).toList();
  });
});
5

Load History

Fetch order history from local database
_getHistory();
6

Update Loading State

Signal that initialization is complete
setState(() {
  _stillLoading = false;
});
The _stillLoading state prevents the UI from rendering until all async initialization is complete, avoiding null reference errors.

Map State Management

The map pages manage real-time location state:
class _MapState extends State<MapPage> {
  String distance;
  String duration;
  double dlat;  // Driver latitude
  double dlong; // Driver longitude
  List<LatLng> points = [];
  LatLng _center;
  MapController _mapController = MapController();
  var route;
  String summary = 'Steps';
  
  @override
  void initState() {
    // Listen to driver location updates
    FirebaseDatabase.instance
        .reference()
        .child('Drivers')
        .child(widget.driverId)
        .onValue
        .listen((e) {
          if (mounted) {
            Map<String, dynamic> map = 
              e.snapshot.value.cast<String, dynamic>();
            
            _updateLocation(Position(
              longitude: map['long'].toDouble(),
              latitude: map['lat'].toDouble()
            ));
          }
        });
    
    super.initState();
  }
  
  _updateLocation(Position position) {
    setState(() {
      dlat = position.latitude;
      dlong = position.longitude;
    });
    
    // Fetch new route from Mapbox
    fetchRoute();
  }
}
Always check if (mounted) before calling setState() in async callbacks to prevent updating state on disposed widgets.

Chat State Management

chat.dart
class ChatScreenState extends State<ChatScreen> {
  String peerId;
  String peerName;
  String peerAvatar;
  String id;
  var listMessage;
  String groupChatId;
  SharedPreferences prefs;
  
  File imageFile;
  bool isLoading;
  bool isShowSticker;
  String imageUrl;
  
  final TextEditingController textEditingController = 
    TextEditingController();
  final ScrollController listScrollController = 
    ScrollController();
  final FocusNode focusNode = FocusNode();
  
  @override
  void initState() {
    super.initState();
    
    focusNode.addListener(onFocusChange);
    
    isLoading = false;
    isShowSticker = false;
    imageUrl = '';
    
    readLocal();
  }
  
  void onFocusChange() {
    if (focusNode.hasFocus) {
      // Hide sticker panel when keyboard appears
      setState(() {
        isShowSticker = false;
      });
    }
  }
  
  void onSendMessage(String content, int type) {
    if (content.trim() != '') {
      textEditingController.clear();
      
      // Transaction ensures atomic write
      firestore.runTransaction((transaction) async {
        await transaction.set(
          documentReference,
          {
            'idFrom': id,
            'idTo': peerId,
            'timestamp': DateTime.now().millisecondsSinceEpoch.toString(),
            'content': content,
            'type': type
          },
        );
      });
      
      // Scroll to bottom
      listScrollController.animateTo(
        0.0,
        duration: Duration(milliseconds: 300),
        curve: Curves.easeOut
      );
    }
  }
}
State Variables:
  • isLoading: Shows loading indicator during image upload
  • isShowSticker: Toggles sticker picker visibility
  • imageFile: Holds selected image before upload
  • listMessage: Cached message list from StreamBuilder
  • groupChatId: Computed chat room identifier

SharedPreferences for Persistence

Trackmart uses SharedPreferences to persist user data across app sessions.

Stored Data

prefs.setString('id', userId);
prefs.setString('displayName', name);
prefs.setString('phoneNo', phone);
prefs.setString('photoUrl', photoUrl);
prefs.setString('aboutMe', bio);
Retrieved in:
  • home_page.dart:189 - HomePage initialization
  • chat.dart:124 - Chat screen initialization
  • settings.dart:63 - Settings page

Usage Pattern

Future<void> saveUserProfile(User user) async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  
  await prefs.setString('id', user.uid);
  await prefs.setString('displayName', user.displayName);
  await prefs.setString('phoneNo', user.phoneNumber);
  await prefs.setString('photoUrl', user.photoUrl);
}
SharedPreferences provides instant access to cached data, reducing Firebase queries and improving app startup time.

Firebase Real-time Listeners

Trackmart uses Firebase listeners for reactive state updates.

StreamBuilder Pattern

The app extensively uses StreamBuilder to rebuild UI when data changes:
StreamBuilder(
  stream: databaseReference
      .child('buyers')
      .child(currentUserId)
      .child('requests')
      .onValue,
  builder: (context, snap) {
    if (snap.hasData && !snap.hasError && 
        snap.data.snapshot.value != null) {
      
      List<HistoryItem> items = [];
      Map<String, dynamic> map = 
        snap.data.snapshot.value.cast<String, dynamic>();
      
      map.forEach((key, values) {
        if (values != null) {
          items.add(Order(
            key: key,
            userId: values['userId'],
            driverId: values['driverId'],
            quantity: values['quantity'].toDouble(),
            // ... other fields
          ).toHistoryItem());
        }
      });
      
      return Column(children: items);
    } else {
      return Container();
    }
  }
)

Direct Listeners

Some components use direct .listen() for more control:
StreamSubscription<Event> orderSubscription;

@override
void initState() {
  super.initState();
  
  orderSubscription = FirebaseDatabase.instance
      .reference()
      .child('Drivers')
      .child(driverId)
      .onValue
      .listen((event) {
        if (mounted) {
          Map<String, dynamic> data = 
            event.snapshot.value.cast<String, dynamic>();
          
          setState(() {
            driverLat = data['lat'];
            driverLong = data['long'];
          });
          
          updateRoute();
        }
      });
}

@override
void dispose() {
  orderSubscription?.cancel();
  super.dispose();
}
Always cancel stream subscriptions in dispose() to prevent memory leaks.

State Update Patterns

Reactive Form Updates

Text controllers automatically update related fields:
_moneyController.addListener(() {
  if (_moneyController.text.isEmpty) {
    setState(() {
      _moneyController2.text = "";
    });
  } else if (_focus.hasFocus) {
    setState(() {
      _moneyController2.text = 
        (double.parse(_moneyController.text) * (rate ?? 0))
          .toStringAsFixed(0);
    });
  }
});

_moneyController2.addListener(() {
  if (_moneyController2.text.isEmpty) {
    setState(() {
      _moneyController.text = "";
    });
  } else if (_focus2.hasFocus) {
    if (rate != null) {
      setState(() {
        _moneyController.text = 
          (double.parse(_moneyController2.text) / rate)
            .toStringAsFixed(2);
      });
    }
  }
});

Search Filtering

Search state updates filter displayed lists:
_filter.addListener(() {
  if (_filter.text.isEmpty) {
    setState(() {
      _searchText = "";
      _filteredHistory = _history;
    });
  } else {
    setState(() {
      _searchText = _filter.text;
      _filteredHistory = _history.where((item) =>
        item.driver.toLowerCase().contains(_searchText.toLowerCase()) ||
        item.date.toLowerCase().contains(_searchText.toLowerCase())
      ).toList();
    });
  }
});

Selection State

Track selected items across the app:
static int _selected = -1;

select(index) {
  filteredDrivers[index].selected = true;
  filteredDrivers[index].deselect = deselect;
  
  setState(() {
    _selected = index;
  });
}

deselect() {
  setState(() {
    _selected = -1;
  });
}

Loading States

Manage multiple loading indicators:
// Global loading
if (_stillLoading) {
  return LoadingScreen();
}

// Operation-specific loading
if (_isTransacting) {
  return CircularProgressIndicator();
}

// Async operation loading
await showDialog(
  barrierDismissible: false,
  builder: (context) => AlertDialog(
    content: LinearProgressIndicator(),
  )
);

State Lifecycle

Component Lifecycle

1

initState()

Initialize state variables, set up listeners, load persisted data
@override
void initState() {
  super.initState();
  _tabController = TabController(length: 3, vsync: this);
  _startup();
}
2

didChangeDependencies()

Called when widget dependencies change (rarely used in Trackmart)
3

build()

Render UI based on current state
@override
Widget build(BuildContext context) {
  return _stillLoading ? LoadingScreen() : MainScreen();
}
4

setState()

Trigger rebuild when state changes
setState(() {
  _selected = index;
  filteredDrivers = newDrivers;
});
5

dispose()

Clean up resources before widget removal
@override
void dispose() {
  _tabController.dispose();
  myFocusNode.dispose();
  _filter.dispose();
  subscription?.cancel();
  super.dispose();
}

State Persistence on App Lifecycle

class _TabbedGuyState extends State<TabbedGuy> 
    with WidgetsBindingObserver {
  
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }
  
  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
  
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
      case AppLifecycleState.resumed:
        // App came to foreground
        updateRate();
        break;
      
      case AppLifecycleState.paused:
        // App went to background
        saveState();
        break;
      
      case AppLifecycleState.inactive:
      case AppLifecycleState.detached:
        break;
    }
  }
}

Best Practices

Always Check mounted

Before calling setState() in async callbacks:
if (mounted) {
  setState(() {
    // Update state
  });
}

Dispose Resources

Cancel subscriptions and dispose controllers:
@override
void dispose() {
  controller.dispose();
  subscription.cancel();
  super.dispose();
}

Use Keys for Lists

Provide keys for stateful list items:
ListView.builder(
  itemBuilder: (context, index) =>
    DriverCard(
      key: Key(driver.id),
      driver: driver,
    ),
)

Minimize setState Scope

Only update necessary parts:
// Good
setState(() {
  _selected = index;
});

// Bad - includes unrelated logic
setState(() {
  _selected = index;
  var data = processData();
  updateDatabase(data);
});

Common State Patterns

Loading State Pattern

class _MyWidgetState extends State<MyWidget> {
  bool _isLoading = false;
  String _error;
  Data _data;
  
  Future<void> loadData() async {
    setState(() {
      _isLoading = true;
      _error = null;
    });
    
    try {
      final data = await fetchData();
      
      if (mounted) {
        setState(() {
          _data = data;
          _isLoading = false;
        });
      }
    } catch (e) {
      if (mounted) {
        setState(() {
          _error = e.toString();
          _isLoading = false;
        });
      }
    }
  }
  
  @override
  Widget build(BuildContext context) {
    if (_isLoading) return LoadingIndicator();
    if (_error != null) return ErrorView(error: _error);
    if (_data == null) return EmptyView();
    return DataView(data: _data);
  }
}

Form State Pattern

class _FormState extends State<FormWidget> {
  final _formKey = GlobalKey<FormState>();
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  bool _isValid = false;
  
  @override
  void initState() {
    super.initState();
    
    _nameController.addListener(_validateForm);
    _emailController.addListener(_validateForm);
  }
  
  void _validateForm() {
    setState(() {
      _isValid = _formKey.currentState?.validate() ?? false;
    });
  }
  
  @override
  void dispose() {
    _nameController.dispose();
    _emailController.dispose();
    super.dispose();
  }
}

Performance Considerations

  1. Avoid unnecessary rebuilds
    // Use const constructors when possible
    const Text('Static text')
    
    // Extract static widgets
    static final Widget staticHeader = Container(...);
    
  2. Limit StreamBuilder scope
    // Good - only rebuilds ListView
    StreamBuilder(
      stream: ordersStream,
      builder: (context, snapshot) => ListView(...),
    )
    
    // Bad - rebuilds entire screen
    StreamBuilder(
      stream: ordersStream,
      builder: (context, snapshot) => Scaffold(...),
    )
    
  3. Debounce rapid updates
    Timer _debounce;
    
    void onSearchChanged(String query) {
      if (_debounce?.isActive ?? false) _debounce.cancel();
      
      _debounce = Timer(Duration(milliseconds: 500), () {
        setState(() {
          _searchQuery = query;
        });
      });
    }
    
  4. Cache computed values
    List<Driver> _allDrivers;
    List<Driver> _filteredDrivers;
    String _lastQuery;
    
    void filterDrivers(String query) {
      if (query == _lastQuery) return;
      
      _lastQuery = query;
      _filteredDrivers = _allDrivers
        .where((d) => d.name.contains(query))
        .toList();
    }
    

Testing State

testWidgets('Updates state on button tap', (tester) async {
  await tester.pumpWidget(MyWidget());
  
  expect(find.text('Not selected'), findsOneWidget);
  
  await tester.tap(find.byType(ElevatedButton));
  await tester.pump();
  
  expect(find.text('Selected'), findsOneWidget);
});

Architecture Overview

Learn about the overall app structure

Database Schema

Understand data storage patterns

Testing Guide

Learn how to test stateful components

Build docs developers (and LLMs) love