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.
Trackmart uses StatefulWidget extensively for managing component-level state.
Authentication State (RootPage)
The RootPage manages the global authentication state:
State Definition
State Transitions
State Rendering
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 State Variables
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: Category Variables Purpose UI State isLoading, _stillLoading, _isTransactingControl loading indicators and button states Form State quantity, _unit, _paymnt, _searchTextTrack user input across form fields Data State filteredDrivers, _history, rateCache loaded data and filter results User State currentUserId, currentLat, currentLongStore current user information Selection State _selected, requested, transit, deliveredTrack UI selections and section visibility
State Initialization
The home page initializes state asynchronously:
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;
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' );
Location Services
Get current location and start tracking await geolocator
. getCurrentPosition (desiredAccuracy : LocationAccuracy .best)
. then ((value) {
_updateLocation (value);
geolocator. getPositionStream (locationOptions)
. listen (_updateLocation);
});
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 ();
});
});
Load History
Fetch order history from local database
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:
MapPage State
MapPage2 State (Driver List)
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 State Implementation
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
User Profile
App Preferences
Authentication
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
prefs. setString ( 'unit' , 'Tonne' );
prefs. setString ( 'quality' , 'Fine' );
Retrieved in:
settings.dart:66 - User preferences
Used to pre-fill order form fields
// Stored during login
prefs. setString ( 'id' , user.uid);
// Retrieved to check auth status
String userId = prefs. getString ( 'id' );
if (userId != null ) {
// User is logged in
}
Usage Pattern
Save to Preferences
Load from Preferences
Clear Preferences (Logout)
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:
Order Requests Stream
Chat Messages Stream
Driver List Stream
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
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
initState()
Initialize state variables, set up listeners, load persisted data @override
void initState () {
super . initState ();
_tabController = TabController (length : 3 , vsync : this );
_startup ();
}
didChangeDependencies()
Called when widget dependencies change (rarely used in Trackmart)
build()
Render UI based on current state @override
Widget build ( BuildContext context) {
return _stillLoading ? LoadingScreen () : MainScreen ();
}
setState()
Trigger rebuild when state changes setState (() {
_selected = index;
filteredDrivers = newDrivers;
});
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);
}
}
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 ();
}
}
Avoid unnecessary rebuilds
// Use const constructors when possible
const Text ( 'Static text' )
// Extract static widgets
static final Widget staticHeader = Container (...);
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 (...),
)
Debounce rapid updates
Timer _debounce;
void onSearchChanged ( String query) {
if (_debounce ? .isActive ?? false ) _debounce. cancel ();
_debounce = Timer ( Duration (milliseconds : 500 ), () {
setState (() {
_searchQuery = query;
});
});
}
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