DEV Community

Cover image for Streaming Real-Time Data in Flutter: A Step-by-Step Guide for Developers
Mayank Variya
Mayank Variya

Posted on

Streaming Real-Time Data in Flutter: A Step-by-Step Guide for Developers

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:

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');
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)),
);
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Loading State: The loading state is set to true while the data is being fetched and updated to false 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;
        },
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Image description

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!

Top comments (0)