StreamProvider
Handling real-time data in Riverpod — working with streams, WebSockets, and continuous data updates.
StreamProvider
is a type of provider in Riverpod that holds a
Stream
.
It's designed for real-time, continuous data flows like:
- Firebase Firestore real-time updates
- WebSocket connections
- Sensor data (GPS, accelerometer)
- Chat messages
- Live stock prices
- User presence status
✅ When to Use StreamProvider
Use
StreamProvider
when you need to
listen to a continuous stream of data
that updates over time. Unlike
FutureProvider
which returns a single value,
StreamProvider
emits multiple values over time.
A Stream is a sequence of asynchronous events. It's like a pipe that delivers data over time. There are two types of streams in Dart:
📡 Single-Subscription Stream
Can only be listened to once. The listener receives all events from the stream. Most common type.
final
stream = Stream.fromIterable([1, 2, 3]);
📢 Broadcast Stream
Can be listened to by multiple listeners. Each listener receives the same events. Used for events that multiple parts of the app need to hear.
final
stream = StreamController.broadcast().stream;
💡 Stream vs Future
- Future – Emits a single value or error once
- Stream – Emits multiple values over time
- StreamProvider handles streams with loading and error states
Defining a
StreamProvider
is similar to other providers, but the creation function
returns a
Stream
.
import
'package:flutter_riverpod/flutter_riverpod.dart'
;
// A StreamProvider that emits messages from a chat service
final
messagesProvider = StreamProvider<Message>((ref) {
// Return a stream of messages
return
chatService.messagesStream;
});
// A StreamProvider with a broadcast stream
final
notificationsProvider = StreamProvider<Notification>((ref) {
final
controller = StreamController<Notification>.broadcast();
// ... setup notification listener ...
return
controller.stream;
});
// A StreamProvider that uses a timer
final
timerProvider = StreamProvider<int>((ref) {
return
Stream.periodic(Duration(seconds: 1), (i) => i + 1);
});
When you
watch
a
StreamProvider
, you get an
AsyncValue
that
updates every time the stream emits a new value.
import
'package:flutter/material.dart'
;
import
'package:flutter_riverpod/flutter_riverpod.dart'
;
import
'providers.dart'
;
class
MessagesScreen
extends
ConsumerWidget {
@override
Widget
build
(BuildContext context, WidgetRef ref) {
// 👀 Watch the messages provider
final
messagesAsync = ref.watch(messagesProvider);
return
Scaffold(
appBar: AppBar(title: Text(
'Chat Messages'
)),
body: messagesAsync.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text(
'Error: $error'
)),
data: (messages) {
// messages is the latest value from the stream
return
ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) => ListTile(
title: Text(messages[index].text),
subtitle: Text(messages[index].sender),
),
);
},
),
);
}
}
messagesProvider
is a
StreamProvider
that returns a stream of messages from a chat service.
ref.watch(messagesProvider)
returns an
AsyncValue
that updates with each new value from the stream.
.when()
method handles loading, error, and data states.
The data state receives the latest value from the stream.
Sometimes you need to control the stream manually, like sending data to a WebSocket or
adding events to a stream. Here's how to use a
StreamController
with
StreamProvider
.
import
'package:flutter_riverpod/flutter_riverpod.dart'
;
// Define a class to hold the controller
class
ChatNotifier
{
final
StreamController<String> _controller = StreamController<String>.broadcast();
// Expose the stream
Stream<String>
get
messages => _controller.stream;
// Method to send a message
void
sendMessage
(String message) {
_controller.add(message);
}
// Clean up
void
dispose
() {
_controller.close();
}
}
// Create the provider
final
chatProvider = Provider<ChatNotifier>((ref) {
return
ChatNotifier();
});
// StreamProvider that uses the chat notifier
final
messagesStreamProvider = StreamProvider<String>((ref) {
final
chat = ref.watch(chatProvider);
return
chat.messages;
});
class
ChatScreen
extends
ConsumerWidget {
final
TextEditingController _textController = TextEditingController();
@override
Widget
build
(BuildContext context, WidgetRef ref) {
final
messagesAsync = ref.watch(messagesStreamProvider);
final
chat = ref.watch(chatProvider);
return
Scaffold(
appBar: AppBar(title: Text(
'Chat'
)),
body: Column(
children: [
Expanded(
child: messagesAsync.when(
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text(
'Error: $error'
)),
data: (messages) => ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) => ListTile(
title: Text(messages[index]),
),
),
),
),
Padding(
padding: EdgeInsets.all(
8
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _textController,
decoration: InputDecoration(
hintText:
'Type a message...'
,
border: OutlineInputBorder(),
),
),
),
IconButton(
icon: Icon(Icons.send),
onPressed: () {
if
(_textController.text.isNotEmpty) {
chat.sendMessage(_textController.text);
_textController.clear();
}
},
),
],
),
),
],
),
);
}
}
✅ Key Pattern
-
Provider
holds the
ChatNotifierinstance - StreamProvider exposes the stream from the notifier
- ref.watch(chatProvider) gives access to the notifier to send messages
- ref.watch(messagesStreamProvider) listens for new messages
Here's a complete example of a real-time timer app that uses
StreamProvider
with a periodic stream.
import
'package:flutter_riverpod/flutter_riverpod.dart'
;
// A StreamProvider that emits the current time every second
final
timerProvider = StreamProvider<DateTime>((ref) {
return
Stream.periodic(Duration(seconds: 1), (_) => DateTime.now());
});
// A Provider that holds the timer state
class
TimerNotifier
extends
StateNotifier<bool> {
TimerNotifier() :
super
(
false
);
// false = stopped, true = running
void
toggle
() => state = !state;
}
final
timerStateProvider = StateNotifierProvider<TimerNotifier, bool>((ref) {
return
TimerNotifier();
});
// A Provider that controls the timer stream based on the state
final
controlledTimerProvider = StreamProvider<DateTime>((ref) {
final
isRunning = ref.watch(timerStateProvider);
// If not running, return an empty stream
if
(!isRunning) {
return
Stream.empty();
}
// If running, emit the time every second
return
Stream.periodic(Duration(seconds: 1), (_) => DateTime.now());
});
class
TimerScreen
extends
ConsumerWidget {
@override
Widget
build
(BuildContext context, WidgetRef ref) {
final
timerAsync = ref.watch(controlledTimerProvider);
final
isRunning = ref.watch(timerStateProvider);
return
Scaffold(
appBar: AppBar(title: Text(
'Real-Time Timer'
)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
timerAsync.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text(
'Error: $error'
),
data: (time) => Text(
'${time.hour.toString().padLeft(2, '0')}:'
'${time.minute.toString().padLeft(2, '0')}:'
'${time.second.toString().padLeft(2, '0')}'
,
style: TextStyle(fontSize:
48
, fontWeight: FontWeight.bold),
),
),
SizedBox(height:
32
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: isRunning
? null
: () => ref.read(timerStateProvider.notifier).toggle(),
child: Text(
'Start'
),
),
SizedBox(width:
16
),
ElevatedButton(
onPressed: isRunning
? () => ref.read(timerStateProvider.notifier).toggle()
: null,
child: Text(
'Stop'
),
),
],
),
],
),
),
);
}
}
StreamProvider
- Multiple values over time
- Real-time data updates
- Continuous connection
- Perfect for chat, sensors, notifications
- Can be controlled with StreamController
FutureProvider
- Single value once
- One-time data fetch
- Disconnected after completion
- Perfect for API calls, database reads
- Can be refreshed with ref.refresh()
If you create a
StreamController
, you must close it when it's no longer needed
to prevent memory leaks.
Use
ref.onDispose
or call
dispose()
in a
StateNotifier
to clean up controllers.
Single-subscription streams can only be listened to once. If multiple widgets try to listen, you'll get an error.
Use
StreamController.broadcast()
when multiple widgets need to listen to the same stream.
Like
FutureProvider
,
StreamProvider
has a loading state before the
first value is emitted. Always handle it.
Use
.when()
or
.maybeWhen()
to handle all three states explicitly.
🎯 Key Takeaway
StreamProvider
is the go-to solution for real-time data in Riverpod.
It handles streams with loading and error states, automatically updates the UI when new data arrives,
and works seamlessly with
StreamController
for manual control.
Use it for any continuous data flow in your app.