How do I know for sure that a stream is disposed when terminating Flutter app? - riverpod

I am using RiverPod in the following basic app which listens to intStreamProvider. How do I know for sure that the stream is disposed when I terminate the app. Currently, when the app starts, this message is printed to the console "===> created stream provider". However, when I terminate the app, this message is NOT printed to the console '===> disposed stream provider'. Why is that? Please follow the comments in the code.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// THIS IS THE STREAM THAT I WOULD LIKE DISPOSED WHEN TERMINATING THE APP
// How do I know for sure that this stream was disposed when app terminates?
final intStreamProvider = StreamProvider.autoDispose<int>((ref) {
// MESSAGE PRINTED ON CREATION
debugPrint('===> created stream provider');
// MESSAGE DOES NOT PRINT ON TERMINATION OF APP
ref.onDispose(() => debugPrint('===> disposed stream provider'));
return Stream.value(0);
});
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(home: MyHomePage());
}
}
class MyHomePage extends ConsumerWidget {
const MyHomePage({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
// WATCHING THE STREAM OVER HERE
ref.watch(intStreamProvider);
return Scaffold(body: Container());
}
}

When apps are stopped, the process is killed.
This doesn't "dispose" providers or widgets; it simply stops the program.
Streams will be killed too though, as the program is killed, so they are killed along with it.
But you cannot perform an action before the program is about to get killed. At least not using dispose. There are platform-specific solutions. But that's a different question

Related

How do you make local storage persist after the app has been closed in Flutter?

I'm using the package localstorage - https://pub.dartlang.org/packages/localstorage#-installing-tab-
This is my code:
import 'package:flutter/material.dart';
import 'package:localstorage/localstorage.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
_MyHomePageState createState() => _MyHomePageState();
}
final LocalStorage storage = new LocalStorage("level");
class _MyHomePageState extends State<MyHomePage> {
void initState() {
super.initState();
//storage.setItem("level", 0);
printStorage();
}
void printStorage() {
print("level stored: " + storage.getItem("level").toString());
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("LocalStorage Example"),
),
body: Center(
),
);
}
}
When storage.setItem("level", 0) is not commented out the app works fine and prints out "level stored: 0". However after closing the app, commenting out storage.setItem("level", 0), saving the app and rerunning the app the app prints out "level stored: null".
How do you make the storage persist from the last time the app was run?
I am using the Xcode iPhone simulator.
Any help is greatly appreciated!
By looking at the package’s source code it looks like you’re not giving it enough time to load the data from the underlying async call. That’s why it exposes a flag property for you to check if it’s ready to perform read/write operations.
Just use it before printing as so:
storage.ready.then((_) => printStorage());
I had similar questions, but after hours of testing and digging around, I can confirm that the localstorage package does persist data even after the app has been closed. If you want to see the actual database file for testing purposes (you can see the contents change live during iOS simulation) look here on macOS:
/Users/YOURUSERNAME/Library/Developer/CoreSimulator/Devices/SOMEDEVICENUMBER/data/Containers/Data/Application/SOMEAPPLICATIONNUMBER/Documents/level.json
So this means it just becomes a matter of waiting the few nanoseconds for the localstorage connection to be ready before trying to read and use the values. Being new to Dart/Flutter, for me this means experimenting with the correct combination and location of async/await, or Future/then.
For example, this change will work correctly in your sample app:
void printStorage() async {
//this will still print null:
print("before ready: " + testDb.getItem("level").toString());
//wait until ready
await storage.ready;
//this will now print 0
print("after ready: " + testDb.getItem("level").toString());
}
In my actual app, if I forget to correctly place an "await" or "then", I get a lot of null errors before the actual value gets retrieved/assigned.
BTW in your file (level.json) you have only one element, {“level”, “0”}. For this scenario with simple data needs, using the shared_preferences package is an alternative consideration. But I like localstorage because it is clean and simple.

Connecting a sink of BLoC with another BLoC

I am using the BLoC pattern as described at the Google IO talk.
I have a simple BLoC which is used to display a alert in the UI whenever a string is added to messageSink:
class AlertBloc {
final _message = BehaviorSubject<String>();
AlertBloc() {}
Stream<String> get message => _message.stream;
Sink<String> get messageSink => _message.sink;
void dispose() {
_message.close(); }
}
Elsewhere in the app, I have another BLoC which needs to add a string to messageSink, when a certain condition is met.
I noticed it is not a good idea to provide the whole BLoC from the Google I/O repo for the talk, and they provide advice for connecting a stream from a BLoC to another BLoC sink:
Note that we are not providing [CartBloc] to the
[ProductSquareBloc] directly, although it would be easier to
implement. BLoCs should not depend on other BLoCs (separation of
concerns). They can only communicate with each other using
streams. In this case, the [CartBloc.items] output plugs into the
[ProductSquareBloc.cartItems] input.
My question is how to connect a sink from a BLoC to another BLoC stream?
Here is a simple example for you. Imagine the following two BLoCs:
The first one exposes a Stream and populates it with some values:
class ProducerBLoC {
//Controller is private - you do not want to expose it
final StreamController<int> _productionController = StreamController<int>();
//Instead, you expose a stream
Stream<int> get production => _productionController.stream;
//This method generates some values and puts them to stream
void produceValue() {
_productionController.sink.add(1);
_productionController.sink.add(2);
_productionController.sink.add(3);
}
//Don't forget to close your controllers
void dispose() {
_productionController.close();
}
}
The other one exposes a Sink and processes values that are put into it.
class ConsumerBLoC {
//Controller is private - you do not want to expose it
final StreamController<int> _consumptionController = StreamController<int>();
//Instead, you expose a sink
StreamSink<int> get consumption => _consumptionController.sink;
//In class constructor we start listening to the stream of values
ConsumerBLoC() {
_consumptionController.listen((value) {_consumeValue(value);} );
//or simply: _consumptionController.listen(_consumeValue); //theese are the same
}
//This method generates some values and puts them to stream
void consumeValue(int value) {
//Do something with the value
print('Value processed: $value');
}
//Don't forget to close your controllers
void dispose() {
_consumptionController.close();
}
}
Now, the task is to connect production stream to consumption sink. As you have correctly noticed, you do not want for any of two BLoCs to know anything about existence of the other one. So none of the two should hold references to the other one or even create instances of another one. Instead, you connect them using your Widget class:
//Define some widget to represent main screen of your application
class MainScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() => _MainScreenState();
}
//And define a state for this widget (state does not need to be public)
class _MainScreenState extends State<MainScreen> {
//You define both blocks here
ProducerBLoC _producer = new ProducerBLoC();
ConsumerBLoC _consumer = new ConsumerBLoC();
//Now, either do it in _MainScreenState constructor, or in the initState() method
#override
void initState() {
super.initState();
//Connect production stream with consumption sink
_producer.production.listen((value) => _consumer.consumption.add(value));
//Or, beautifully: _producer.production.pipe(_consumer.consumption);
}
#override
Widget build(BuildContext context) {
//The exact implementation does not matter in current context
}
//And don't forget to close your controllers
#override
dispose() {
super.dispose();
_producer.dispose();
_consumer.dispose();
}
}
This way, any value generated by ProducerBLoC will immediately be consumed by ConsumerBLoC. And, what's the most important, - both BLoCs are completely independent from one another!
The exact same way as you'd do with streams: Passing it as parameter
class Bloc {
final Sink<int> _external;
Bloc(this._external);
}

How to add event listeners in Flutter?

I'm pretty new to Flutter and experimenting with the SDK. I'm working on a simple app that has a countdown in the background and want to trigger an event at certain intervals. For example, when clock reaches one minute remaining, send a push notification. In general, I'm trying to get a feel for how to monitor certain activities such as time and usage of the app and once certain conditions are met, trigger other things. Is it as simple as an if-else statement placed in the right place?
What kind of thing am I looking for to implement this?
Thanks in advance.
I prefer to use streams for such tasks
Stream<int> timer = Stream.periodic(Duration(seconds: 1), (int count) => count);
...
_MyTextWidget(timer)
and my widget
class _MyTextWidget extends StatefulWidget {
_MyTextWidget(this.stream);
final Stream<int> stream;
#override
State<StatefulWidget> createState() => _MyTextWidgetState();
}
class _MyTextWidgetState extends State<_MyTextWidget> {
int secondsToDisplay = 0;
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: widget.stream,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
return snapshot.hasData ? Text(snapshot.data.toString()) : Text('nodata');
});
}
}
The following is a better example of an event listener based on Streams that I have found to work.
In the widget you want to listen to..
class CmVideoPlayer extends StatefulWidget {
String titlePlaying;
StreamController changeController = StreamController<VideoPlayerEvent>();
CmVideoPlayer({Key key, #required this.titlePlaying})
: assert(titlePlaying != null), super(key: key);
#override
_CmVideoPlayerState createState() => _CmVideoPlayerState();
}
See the line "StreamController changeController = StreamController();" that uses a small class VideoPlayerEvent to carry the message.
class VideoPlayerEvent {
var eventType;
var eventMessage;
VideoPlayerEvent(this.eventType, this.eventMessage);
}
Then in the STATEFULLWIDGET...
Refer the the stream as
class _CmVideoPlayerState extends State<CmVideoPlayer> {
void Member() {
widget.changeController.add(new VideoPlayerEvent('state', 'finished'));
}
}
As it is inside the _CmVideoPlayerState class, and using the ability to reach into the parent class via the widget variable.
Then in the area of the code using the widget, and to listen for the messages..
To listen for the messages
CmVideoPlayer myPlayer = CmVideoPlayer();
myPlayer.changeController.stream.listen((e) {
print('Reciever event from CmVideoPlayer: ' + e.eventMessage.toString());
}
That should do it.
HOWEVER, this only allows ONE listener at a time. After I got this going, I moved on. But plan to implement a multi listener down the track.
Maybe some one can expand on this. I am keeping it as simple as possible. If some one has a multi listener example. Please post here.
You can use ValueNotifier for this.
When value is replaced with something that is not equal to the old value as evaluated by the equality operator ==, this class notifies its listeners.
Helpful Medium link

A build function returned null The offending widget is: StreamBuilder<Response>

I'm new to Flutter and I'm trying to accomplish a simple thing:
I want to create a signup functionality using BLoC pattern and streams.
For the UI part I have a stepper, that on the very last step should fire a request to the server with the collected data.
I believe I have everything working until the StreamBuilder part. StreamBuilders are meant to return Widgets, however, in my case I don't need any widgets returned, if it's a success I want to navigate to the next screen, otherwise an error will be displayed in ModalBottomSheet.
StreamBuilder is complaining that no widget is returned.
Is there anything else that could be used on the View side to act on the events from the stream?
Or is there a better approach to the problem?
If you don't need to render anything, don't use StreamBuilder to begin with.
StreamBuilder is a helper widget used to display the content of a Stream.
What you want is different. Therefore you can simply listen to the Stream manually.
The following will do:
class Foo<T> extends StatefulWidget {
Stream<T> stream;
Foo({this.stream});
#override
_FooState createState() => _FooState<T>();
}
class _FooState<T> extends State<Foo<T>> {
StreamSubscription streamSubscription;
#override
void initState() {
streamSubscription = widget.stream.listen(onNewValue);
super.initState();
}
void onNewValue(T event) {
Navigator.of(context).pushNamed("my/new/route");
}
#override
void dispose() {
streamSubscription.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}

Flutter equivalent to Swift's NotificationCenter?

I'm using FirebaseMessaging to send push notifications to my Flutter app. Those notifications contain chat details.
If the app is currently active and the user is at the chat page, the page should be updated to show the new message (this I can handle).
If the app is on any other page, a local notification/toast should be shown.
My problem, how do I forward the notification to the chat page?
I have FirebaseMessaging listening on the root page. And I can use ModalRoute.of(context).isCurrent to determine if the root page is the current page when the notification comes in. How can I broadcast the notification to the chat page when it is the active page?
In Swift, I'd use NotificationCenter to send data from the app delegate and the chat page would listen for it. I'm hoping something similar is available for Flutter.
Try this dart-event-bus.
An Event Bus using Dart Streams for decoupling applications
I've found a solution and I'm hoping it can help someone else in a similar situation.
I found the package Dart Message Bus
It does everything I need and make handling Streams much easier.
I did have to add one additional method (see the end).
Since the instructions were a bit cryptic, here's how I got it working.
//main.dart
import 'package:dart_message_bus/dart_message_bus.dart';
//global variable
final globalBus = new MessageBus();
class _MyHomePageState extends State<MyHomePage> {
// set up firebase messaging here
#override
void initState() {
super.initState();
_setupPushNotifs;
}
_setupPushNotifs() {
_firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) {
_processChatPush(message);
},
);
}
_processChatPush(Map<String, dynamic> message) async {
String messageID = message['messageID'];
globalBus.publish(new Message('newChat', data: "$messageID"));
}
}
//chat.dart
import 'package:dart_message_bus/dart_message_bus.dart';
import 'main.dart';
class _ChatPageState extends State<ChatPage> {
StreamSubscription streamListener;
#override
void initState() {
super.initState();
_listen();
}
#override
void dispose() {
streamListener.cancel();
streamListener = null;
}
_listen() async {
streamListener = globals.globalBus.subscribe('newChat', (Message m) async {
Map<String, dynamic> message = m.data;
String messageID = message['messedID'];
}
}
The dispose method is very important or the listener will keep listening and cause problems.
If you need verification that a subscriber is actually listening, modify the calling and listen methods:
// main.dart
_processChatPush(Map<String, dynamic> message) async {
String messageID = message['messageID'];
var callbackMessage = await globalBus.publish(
new Message('newChat', data: "$messageID"),
waitForKey: 'ackChat',
timeout: const Duration(seconds: 2)
);
if(callbackMessage.isEmpty) {
// means the another service's message was not received
// and timeout occured.
// process the push notification here
} else {
// the callback from another service received
// and callbackMessage.data contains callback-data.
// the push notification has been handled by chat.dart
}
}
// chat.dart
_listen() async {
streamListener = globals.globalBus.subscribe('newChat', (Message m) async {
Map<String, dynamic> message = m.data;
String messageID = message['messageID'];
var data = "ack";
var ack = new Message('ackChat', data: data);
globalBus.publish(ack);
}
}
I had to add one additional method in order to close the publishing stream when it's no longer needed.
Add to the end of Class MessageBus in message_bus.dart in the package source:
close() {
_streamController.close();
_streamController = null;
}
and then you can dispose the stream:
void dispose() {
globalBus.close();
super.dispose();
}
I ended up putting the globalBus variable in a library file. Then import that library in main.dart and chat.dart and remove the import main.dart from chat.dart.

Resources