In today’s fast-paced world, providing real-time data updates is crucial for any application. Users expect instant access to live information, whether it’s scores, stats, or any other dynamic data. This guide will demonstrate how to manage real-time data updates in a Flutter application using Firebase, illustrated through an example of streaming cricket match data in our Khelo app—a cricket team management platform.
Why Choose Streams?
Streams are an excellent way to handle asynchronous data in Flutter. They allow your app to listen for updates in real time, making them perfect for applications that require live data. With Firebase Firestore, you can easily implement this functionality for any type of data.
Setting Up Your Flutter Project
Before diving into the code, ensure you have the following set up:
- Flutter SDK: Install and configure the Flutter SDK.
- Firebase Project: Create and configure your Firebase project.
- Firestore Database: Set up your Firestore database to store your data.
- Riverpod Package: Add the Riverpod package to manage state efficiently.
- Freezed Package: Add the Freezed package to simplify data class creation and immutable state management.
Step 1: Create Firestore Collection
Start by creating a Firestore collection named matches
, where each document represents a cricket match. Here’s how you might define your Firestore setup in your app:
final _dataCollection = FirebaseFirestore.instance.collection('matches');
Step 2: Stream Data by ID
Next, implement a function to stream data by its unique ID. This function will listen for changes in the Firestore document and return updated data in real time.
Stream<MatchModel> streamDataById(String id) {
return _dataCollection.doc(id).snapshots().asyncMap((snapshot) async {
if (snapshot.exists) {
return MatchModel.fromMap(snapshot.data()!);
}
throw Exception('Data not found');
}).handleError((error, stack) => throw AppError.fromError(error, stack));
}
Key Points:
- snapshots(): Subscribes to real-time updates from Firestore.
- asyncMap: Performs asynchronous transformations on the data.
-
Error Handling: Utilizes
handleError
to manage any errors gracefully.
Step 3: Implement State Management with Riverpod
Create a state class to manage the data state within your application:
@freezed
class MatchDetailTabState with _$MatchDetailTabState {
const factory MatchDetailTabState({
Object? error,
MatchModel? match,
@Default(false) bool loading,
}) = _MatchDetailTabState;
}
Setting Up the Provider
Use a StateNotifierProvider
to expose your data state throughout the app:
final matchDetailTabStateProvider = StateNotifierProvider.autoDispose<
MatchDetailTabViewNotifier, MatchDetailTabState>(
(ref) => MatchDetailTabViewNotifier(ref.read(matchServiceProvider)),
);
Step 4: Load Data in the Notifier
Implement the logic to load data using the stream within your notifier:
class MatchDetailTabViewNotifier extends StateNotifier<MatchDetailTabState> {
MatchDetailTabViewNotifier(this._matchService) : super(const MatchDetailTabState());
final MatchService _matchService;
StreamSubscription? _matchStreamSubscription;
late String _matchId; // Declare _matchId here
void setData(String matchId) {
loadMatch(matchId);
}
void loadMatch(String matchId) {
state = state.copyWith(loading: true); // Set loading state
_matchStreamSubscription = _matchService.streamMatchById(matchId).listen(
(match) {
state = state.copyWith(match: match, loading: false);
},
onError: (error) {
state = state.copyWith(error: AppError.fromError(error), loading: false);
},
);
}
@override
void dispose() {
_matchStreamSubscription?.cancel();
super.dispose();
}
}
Explanation:
-
Loading State: The loading state is set to
true
while the data is being fetched and updated tofalse
once the data is received or if an error occurs. - State Management: Updates the state whenever new match data is received.
- Error Handling: Captures any errors and updates the state accordingly.
- Resource Management: Cancels the stream subscription when the notifier is disposed to avoid memory leaks.
Step 5: Building the UI
Create a simple UI to display the data. Show a loading indicator while data is being fetched, and then display the information once it is available.
class MatchDetailTabScreen extends ConsumerStatefulWidget {
final String matchId;
const MatchDetailTabScreen({super.key, required this.matchId});
@override
ConsumerState createState() => _MatchDetailTabScreenState();
}
class _MatchDetailTabScreenState extends ConsumerState<MatchDetailTabScreen> {
late MatchDetailTabViewNotifier notifier;
@override
void initState() {
super.initState();
notifier = ref.read(matchDetailTabStateProvider.notifier);
runPostFrame(() => notifier.loadMatch(widget.matchId));
}
@override
Widget build(BuildContext context) {
final state = ref.watch(matchDetailTabStateProvider);
return AppPage(
title: state.match?.name ?? 'Match Details', // Handle null case
body: Builder(
builder: (context) {
if (state.error != null) {
return ErrorScreen(state.error); // Ensure error is not null
}
if (state.loading) {
return Center(child: AppProgressIndicator());
}
return //Show UI;
},
),
);
}
}
Explanation:
-
Loading Indicator: Displays a
CircularProgressIndicator
while the data is being fetched. - Error Handling: Shows an error message if something goes wrong.
- Display Data: Displays the match details once they are loaded.
Example Implementation Screenshots
Conclusion
With these steps, you can successfully manage real-time data updates in your Flutter app using Firebase Firestore. By implementing streams, you ensure that users receive timely updates, enhancing their overall experience in your applications.
For more detailed code examples and additional information, feel free to check out our Khelo GitHub repository. If you find this guide helpful, please give it a star on GitHub!