Timer doesn't cancel - Flutter Provider - flutter

The objective of my app is when I tap on start button, a timer should start which will run a function every 5 seconds, and when stopped the timer should cancel and stop that function from running. (Now I have different screens for the same purpose so I made a common screen and now I'm sending a Timer timer to that common stateful class constructor). So, the problem is, the timer doesn't cancel, when I tap start the function does executes every 5 seconds but doesn't stop.
Below I have provided some relevant code snippets to my query.
Main screen:
class StrategyOneScreen extends StatefulWidget {
const StrategyOneScreen({Key? key}) : super(key: key);
#override
State<StrategyOneScreen> createState() => _StrategyOneScreenState();
}
class _StrategyOneScreenState extends State<StrategyOneScreen> {
#override
Widget build(BuildContext context) {
Timer timer1 = Timer(Duration(), () {});
return StrategyScreen(
stratName: 'Free Strategy',
timer: timer1,
toggle: Provider.of<StrategyProvider>(
context,
listen: true,
).companyStratOneToggle,
toggleTimer: Provider.of<StrategyProvider>(
context,
listen: false,
).toggleCompanyTimer,
setToggle: Provider.of<StrategyProvider>(
context,
listen: false,
).setCompanyStratOneToggle,
chartLiveData: Provider.of<StrategyProvider>(
context,
listen: true,
).companyChartLiveData,
);
}
}
Inside the common StrategyScreen:
class StrategyScreen extends StatefulWidget {
const StrategyScreen({
Key? key,
required this.stratName,
required this.timer,
required this.toggle,
required this.toggleTimer,
required this.setToggle,
required this.chartLiveData,
}) : super(key: key);
final stratName;
final timer;
final toggle;
final toggleTimer;
final setToggle;
final chartLiveData;
#override
_StrategyScreenState createState() => _StrategyScreenState();
}
class _StrategyScreenState extends State<StrategyScreen> {
#override
Widget build(BuildContext context) {
print("Timer: ${widget.timer}"); // console logs:=> Timer: null
return Scaffold(
...
Row(
children: [
Expanded(
child: Center(
child: FloatingActionButton.extended(
heroTag: 'btn1',
onPressed: widget.toggle == false
? () => {
widget.toggleTimer(
ref,
showNotification('title', 'body'),
widget.timer,
widget.toggle,
),
widget.setToggle(),
}
: null,
label: Text('Start'),
icon: Icon(Icons.launch),
backgroundColor: Colors.greenAccent,
),
),
),
Expanded(
child: Center(
child: FloatingActionButton.extended(
heroTag: 'btn2',
onPressed: widget.toggle
? () => {
widget.toggleTimer(
ref,
showNotification('title', 'body'),
widget.timer,
widget.toggle,
),
widget.setToggle(),
}
: null,
label: Text('Stop'),
icon: Icon(Icons.stop),
backgroundColor: Colors.pink,
),
),
),
],
),
StrategyProvider.dart :
class StrategyProvider with ChangeNotifier {
// Toggles
bool _companyStratOneToggle = false;
bool get companyStratOneToggle => _companyStratOneToggle;
ChartLiveData _companyChartLiveData = ChartLiveData(
...
);
ChartLiveData get companyChartLiveData => _companyChartLiveData;
toggleCompanyTimer(ref, showNotification, timer, bool toggle) {
if (toggle == false) {
timer = Timer.periodic(
Duration(seconds: 5),
(Timer t) => {
fetchCompanyLiveStockData( // The function I want to run every 5 seconds
ref,
showNotification,
),
},
);
} else {
timer.cancel();
print("Timer Canceled!");
}
}
// Toggle setters for different strategies
setCompanyStratOneToggle() {
_companyStratOneToggle = !_companyStratOneToggle;
notifyListeners();
}
}
So as I told earlier that I am able to start the timer but cannot cancel it (as it is null) and it keeps on running every 5 seconds. Below is the console output:
Unhandled Exception: NoSuchMethodError: Class 'Future<dynamic>' has no instance method 'call'.
E/flutter ( 2746): Receiver: Instance of 'Future<dynamic>'
E/flutter ( 2746): <asynchronous suspension>
E/flutter ( 2746):
I/flutter ( 2746): Timer: null
And when I press cancel button:
The following NoSuchMethodError was thrown while handling a gesture:
The method 'cancel' was called on null.
Receiver: null
Tried calling: cancel()
When the exception was thrown, this was the stack
#0 Object.noSuchMethod (dart:core-patch/object_patch.dart:63:5)
#1 StrategyProvider.toggleSbinTimer
package:myapp/…/global/strategy_provider.dart:67
#2 _StrategyScreenState.build.<anonymous closure>
package:myapp/…/global/strategy_screen.dart:152

Ultimately your issue is that you're trying to construct a Timer object in toggleCompanyTimer and trying to assign a reference to that object in the caller. However, Dart is not pass-by-reference, so there is no direct way for toggleCompanyTimer to do that.
You'd be much better off if your StrategyProvider class owned and managed the Timer object entirely by itself. For example:
class StrategyProvider with ChangeNotifier {
Timer? timer;
// ... Other code...
void toggleCompanyTimer(ref, showNotification, bool stopTimer) {
// Cancel any existing Timer before creating a new one.
timer?.cancel();
timer = null;
if (!stopTimer) {
timer = Timer.periodic(
Duration(seconds: 5),
(Timer t) => {
fetchCompanyLiveStockData(
ref,
showNotification,
),
},
);
} else {
print("Timer Canceled!");
}
}
}

Related

Geolocator, setState, memory leak

I'm working on geolocation with geolocation 7.6.2 package. I have a problem with memory leak when closing the stream. Widget looks like this:
class _GeolocatorActiveState extends State<GeolocatorActive> {
StreamSubscription<Position>? _currentPosition;
double distanceToday = 0.0;
double distanceTotal = 0.0;
bool gpsActive = true;
#override
void initState() {
getCurrentPosition();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
// Some stuff
),
),
backgroundColor: Colors.deepOrange
),
body: Column(
children: [
// some stuff here
TextButton(onPressed: () {
setState(() {
gpsActive = false;
stopStream();
});
Navigator.pushNamed(context, '/');},
child: Text(
'Finish',
)
),
// Some stuff there
],
),
);
}
void getCurrentPosition () {
final positionStream = GeolocatorPlatform.instance.getPositionStream();
if(gpsActive == true){
_currentPosition = positionStream.listen((position) {
setState(() {
long = position.longitude.toString();
lat = position.latitude.toString();
});
});
}
}
void stopStream () {
_currentPosition = null;
_currentPosition?.cancel();
super.dispose();
}
}
Now, the thing is that when I push the button "Finish" I want to close and remove this Widget. I tried with Navigator.pushNamedAndRemoveUntil and thought it causes the memory leak but no. I had stopStream function inside getCurrentPosition as else statement but it didn't work either. How can I force app to close stream before it closes this widget? Or am I missing something?
Error looks like this:
E/flutter ( 4655): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: setState() called after dispose(): _GeolocatorActiveState#6b088(lifecycle state: defunct)
E/flutter ( 4655): This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.
E/flutter ( 4655): The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
E/flutter ( 4655): This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().
So there are two things wrong here:
You call super.dispose outside of dispose method, which is not something you should do.
You nullify _currentComposition stream first and then you try to cancel it, which is too late because you already lost access to that stream. You should switch the order.
By the way, I think you can easily put all stream disposal method inside dispose, rather than close them on button't onTap callback.
Here is your code example that I modified, notice overriden dispose method:
class _GeolocatorActiveState extends State<GeolocatorActive> {
StreamSubscription<Position>? _currentPosition;
double distanceToday = 0.0;
double distanceTotal = 0.0;
bool gpsActive = true;
#override
void initState() {
super.initState();
getCurrentPosition();
}
#override
void dispose() {
_currentPosition?.cancel();
_currentPosition = null;
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
// Some stuff
),
),
backgroundColor: Colors.deepOrange
),
body: Column(
children: [
// some stuff here
TextButton(onPressed: () {
setState(() {
gpsActive = false;
});
Navigator.pushNamed(context, '/');},
child: Text(
'Finish',
)
),
// Some stuff there
],
),
);
}
void getCurrentPosition () {
final positionStream = GeolocatorPlatform.instance.getPositionStream();
if(gpsActive == true){
_currentPosition = positionStream.listen((position) {
setState(() {
long = position.longitude.toString();
lat = position.latitude.toString();
});
});
}
}
}
You can try what it suggests:
Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
Like this:
if (mounted) {
setState(() {
long = position.longitude.toString();
lat = position.latitude.toString();
});
}

CallBacks error : The method 'call' was called on null

I am currently working on this tutorial in which transfers a variable to an external file to another (with a callback)
but i have this error :
======== Exception caught by gesture ===============================================================
The following NoSuchMethodError was thrown while handling a gesture:
The method 'call' was called on null.
Receiver: null
Tried calling: call(3)
When the exception was thrown, this was the stack:
#0 Object.noSuchMethod (dart:core-patch/object_patch.dart:54:5)
#1 Son.build.<anonymous closure> (package:montest/son.dart:24:21)
#2 GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:182:24)
#3 TapGestureRecognizer.handleTapUp (package:flutter/src/gestures/tap.dart:607:11)
#4 BaseTapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:296:5)
...
Handler: "onTap"
Recognizer: TapGestureRecognizer#cf4d9
debugOwner: GestureDetector
state: possible
won arena
finalPosition: Offset(178.3, 84.9)
button: 1
sent tap down
====================================================================================================
son.dart
///
/// Inside the Child Widget
///
import 'package:flutter/material.dart';
// Step 1: Define a Callback.
typedef void IntCallback(int id);
class Son extends StatelessWidget {
// Step 2: Configre the child to expect a callback in the constructor(next 2 lines):
final IntCallback onSonChanged;
Son({ #required this.onSonChanged });
int elementId = 3;
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// Step 3: On specific action(e.g onPressed/
// onTap/onLoad.. onWhatEver) trigger the callback
// with the data you want to pass to the parent.
// Data will be passed as parameter(see elementId):
onSonChanged(elementId);
// Done in the child.
},
child: Text('Click me to call the callback!'),
);
}
}
///
///////////////
parent.dart
import 'son.dart';
///
/// Inside the Parent Widget
///
import 'package:flutter/material.dart';
class Father extends StatefulWidget {
#override
_FatherState createState() => _FatherState();
}
class _FatherState extends State<Father> {
// Step 1 (optional): Define a Global variable
// to store the data comming back from the child.
int id;
// Step 2: Define a function with the same signature
// as the callback, so the callback will point to it,
// this new function will get the data from the child,
// set it to the global variable (from step 1)
// in the parent, and then update the UI by setState((){});
void updateId(int newId) {
setState(() {
id = newId;
});
}
#override
Widget build(BuildContext context) {
return Container(
// Step 3: Construct a child widget and pass the
child: Son(
// Many options to make onSonChanged points to
// an executable code(function) within memory
// called 'updateId':
//
// 1st option:
onSonChanged: (int newId) {
updateId(newId);
},
// 2nd option: onSonChanged: updateId,
// 3rd option: onSonChanged: (int newId) => updateId(newId)
// So each time the 'onSonChanged' called by the action
// we defined inside the child, a new data will be
// passed to this parent.
)
);
}
}
Thanks for your help
make id nullable and check it.
try this
import 'package:flutter/material.dart';
typedef void IntCallback(int id);
class Son extends StatelessWidget {
final IntCallback onSonChanged;
Son({required this.onSonChanged});
int elementId = 3;
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
onSonChanged(elementId);
},
child: Text('Click me to call the callback!'),
);
}
}
class Father extends StatefulWidget {
#override
_FatherState createState() => _FatherState();
}
class _FatherState extends State<Father> {
int? id;
void updateId(int newId) {
setState(() {
id = newId;
});
}
#override
Widget build(BuildContext context) {
return Column(
children: [
id == null ? Text(" son value: null") : Text("son value: $id"),
Container(
child: Son(
onSonChanged: (int newId) {
updateId(newId);
},
),
),
],
);
}
}

Unable to reliably trigger animation with longPress on GestureDetector

I'm trying to create a button with a progress indicator (CircularProgressIndicator)
Desired flow:
The user taps on the button it should fire a function
The user presses the button (and holds), it should trigger the animation and fire a function
When the user releases their hold, it should reset the animation and fire a function
At this point, my code works on the second time pressing (and holding) the element. The first time around, the animation controller's addListener prints 2-3 times and then stops, whereas the second time, it holds true and continues to print as the user holds the element. Ontap functionality works regardless.
It's happening while running locally on an android and an ios device
Stripped code block:
import 'package:flutter/material.dart';
import 'package:homi_frontend/constants/woopen_colors.dart';
class ProgressButton extends StatefulWidget {
ProgressButton({
#required this.onTap,
#required this.onLongPress,
#required this.onLongPressUp,
this.duration = const Duration(seconds: 60),
});
final Function onTap;
final Function onLongPress;
final Function onLongPressUp;
final Duration duration;
#override
ProgressButtonState createState() => ProgressButtonState();
}
class ProgressButtonState extends State<ProgressButton>
with SingleTickerProviderStateMixin {
AnimationController _animationController;
bool _beingPressed = false;
#override
void initState() {
_animationController = AnimationController(
vsync: this,
duration: widget.duration,
);
_animationController.addListener(_animationListener);
_animationController.addStatusListener(_animationStatusListener);
super.initState();
}
void _animationListener() {
print('Animation Controller Listener');
setState(() {});
}
void _animationStatusListener(AnimationStatus status) {
print('_animationStatusListener');
if (status == AnimationStatus.completed) {
print(
'Completed duration of ${widget.duration}, fire _handleOnLongPressUp');
_handleOnLongPressUp();
}
if (status == AnimationStatus.forward) {
this.setState(() {
_beingPressed = true;
});
}
}
void _handleOnLongPress() {
print('_handleOnLongPress');
try {
_animationController.forward();
} catch (e) {
print('_handleOnLongPress error: ${e.toString()}');
} finally {
if (_animationController.status == AnimationStatus.forward) {
print('Controller has been started, fire widget.onLongPress');
widget.onLongPress();
}
}
}
void _handleOnLongPressUp() {
print('_handleOnLongPressUp');
try {
this.setState(() {
_beingPressed = false;
});
_animationController.reset();
} catch (e) {
print('_handleOnLongPressUp error: ${e.toString()}');
} finally {
if (_animationController.status == AnimationStatus.dismissed) {
print('Controller has been dismissed, fire widget.onLongPressUp');
widget.onLongPressUp();
}
}
}
#override
dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
key: Key('progressButtonGestureDetector'),
behavior: HitTestBehavior.opaque,
onLongPress: _handleOnLongPress,
onLongPressUp: _handleOnLongPressUp,
onTap: widget.onTap,
child: Container(
width: 80,
height: 80,
child: Text(_animationController.value.toStringAsFixed(2)),
),
);
}
}
Output:
flutter: _handleOnLongPress
flutter: _animationStatusListener
flutter: Controller has been started, fire widget.onLongPress
(2) flutter: Animation Controller Listener
# here it just seems to loose its connection, but if I press (and hold) again, I get:
flutter: _handleOnLongPress
flutter: _animationStatusListener
flutter: Controller has been started, fire widget.onLongPress
(326) flutter: Animation Controller Listener
flutter: _handleOnLongPressUp
flutter: Animation Controller Listener
flutter: _animationStatusListener
flutter: Controller has been dismissed, fire widget.onLongPressUp
I've also looked briefly into RawGestureDetector but only my TapGestureRecognizer gestures seem to fire, the LongPressGestureRecognizer ones don't... even if TapGestureRecognizers are removed.
_customGestures = Map<Type, GestureRecognizerFactory>();
_customGestures[TapGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {
instance
..onTapDown = (TapDownDetails details) {
print('onTapDown');
}
..onTapUp = (TapUpDetails details) {
print('onTapUp');
}
..onTap = () {
print('onTap');
}
..onTapCancel = () {
print('onTapCancel');
};
},
);
_customGestures[LongPressGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(
duration: widget.duration, debugOwner: this),
(LongPressGestureRecognizer instance) {
instance
..onLongPress = () {
print('onLongPress');
}
..onLongPressStart = (LongPressStartDetails details) {
print('onLongPressStart');
_animationController.forward();
}
..onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) {
print('onLongPressMoveUpdate');
}
..onLongPressEnd = (LongPressEndDetails details) {
print('onLongPressEnd');
_animationController.reset();
}
..onLongPressUp = () {
print('onLongPressUp');
};
},
);
Please & thank you for your time!
You are using a Text widget to receive the hit within the
GestureDetector, which have a small hit box compare to the thumb. This might be the reason why you might misclick the hit box occasionally.
You can use the debugPaintPointersEnabled to see the behavior more clearly (need to do a Hot Restart if the app is running):
import 'package:flutter/rendering.dart';
void main() {
// Add the config here
debugPaintPointersEnabled = true;
runApp(App());
}
You can see that the hit box does not flash all the time, even when we think we hit the Text. To increase the accuracy, let's wrap a Container with size around the Text
GestureDetector(
// ... other lines
child: Container(
width: 100,
height: 50,
color: Colors.blue,
alignment: Alignment.center,
child:
Text('Value: ${_animationController.value.toStringAsFixed(2)}')),
);
You can see that the hit box flashes everytime now

Observe List with GetX outside of widget

I have isolate that makes some heavy calculations then on receive the list with the result run a for loop to add them to observable list with items var items = [].obs;
The thing is I'm trying to observe the items list from a splash controller and once the list != [] I'll navigate to another screen, so in onInit() I have this code:
class SplashController extends GetxController {
#override
void onInit() {
final ItemsController _itemsController = Get.put(ItemsController());
// TODO: implement onInit
super.onInit();
ever(_itemsController.items, (newItems) {
print('new items here $newItems');
});
}
}
Despite the itemsController.items is populated (after the for loop I print the itemsController.items and it's not empty) the worker on the splash controller doesn't trigger when the items are added.
What am I doing wrong here? Is this the correct way to observe variable outside of widget using Getx?
Can anyone help me with this, please?
Edit: In the items controller I’m adding the items this way
add(item) => items.add(item)
Continuing with the Isolate example, but without using a StatefulWidget i.e. no setState usage.
The ever worker in SplashX will receive items generated from the Isolate. The Stateless Widget page will display the latest item emitted from the Isolate.
SplashController + ever worker
class SplashX extends GetxController {
ItemsX itemsX;
SplashX({this.itemsX});
#override
void onInit() {
super.onInit();
ever(itemsX.items, (items) => print('Ever items: $items'));
}
}
Items Controller
class ItemsX extends GetxController {
RxList<String> items = RxList<String>();
bool running = false;
void add(String item) {
items.add(item);
}
void updateStatus(bool isRunning) {
running = isRunning;
update();
}
void reset() {
items.clear();
}
/// Only relevant for UnusedControllerPage
List<Widget> get texts => items.map((item) => Text('$item')).toList();
}
Isolate Controller
class IsolateX extends GetxController {
IsolateX({this.itemsX});
ItemsX itemsX;
Isolate _isolate;
static int _counter = 0;
ReceivePort _receivePort;
bool running = false;
static void _checkTimer(SendPort sendPort) async {
Timer.periodic(Duration(seconds: 1), (Timer t) {
_counter++;
String msg = 'notification ' + _counter.toString();
print('SEND: ' + msg);
sendPort.send(msg);
});
}
void _handleMessage(dynamic data) {
itemsX.add(data); // update observable
}
void updateStatus(bool isRunning) {
running = isRunning;
update();
}
void start() async {
itemsX.reset();
updateStatus(true);
_receivePort = ReceivePort();
_isolate = await Isolate.spawn(_checkTimer, _receivePort.sendPort);
_receivePort.listen(_handleMessage, onDone:() {
print("done!");
});
}
void stop() {
if (_isolate != null) {
updateStatus(false);
_receivePort.close();
_isolate.kill(priority: Isolate.immediate);
_isolate = null;
}
}
}
Stateless Page
class MyHomePageStateless extends StatelessWidget {
#override
Widget build(BuildContext context) {
ItemsX ix = Get.put(ItemsX()); // Instantiate ItemsController
IsolateX isox = Get.put(IsolateX(itemsX: ix));
SplashX sx = Get.put(SplashX(itemsX: ix));
return Scaffold(
appBar: AppBar(
title: Text('Isolate Stateless'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GetX<ItemsX>(
builder: (ix) => Text(ix.items.isNotEmpty ? ix.items.last : ''),
),
],
),
),
floatingActionButton: GetBuilder<IsolateX>(
builder: (_ix) => FloatingActionButton(
onPressed: _ix.running ? isox.stop : isox.start,
tooltip: _ix.running ? 'Timer stop' : 'Timer start',
child: _ix.running ? Icon(Icons.stop) : Icon(Icons.play_arrow),
),
),
);
}
}
Here's two controllers, with one ever worker listening for events of another controller, where that controller's events are coming from data generated in an Isolate.
I'm not aware of anything special about generating data in an Isolate as opposed to any other async data source, but I'm not overly familiar with Isolates.
Controllers
class SplashX extends GetxController {
ItemsX itemsX;
SplashX({this.itemsX});
#override
void onInit() {
super.onInit();
ever(itemsX.items, (items) => print('Received items: $items'));
}
}
class ItemsX extends GetxController {
RxList<String> items = RxList<String>();
void add(String item) {
items.add(item);
}
/// Only relevant for SimplePage at bottom
List<Widget> get texts => items.map((item) => Text('$item')).toList();
}
Page /w Isolate
And here's the edits to the Isolate snippet which you're using.
I've instantiated ItemsX controller as a field and SplashX in onInit.
(There shouldn't be a need to use Stateful Widgets since you can put all state into a Controller, but I didn't want to rewrite the Isolate example).
class _MyHomePageState extends State<MyHomePage> {
Isolate _isolate;
bool _running = false;
static int _counter = 0;
String notification = "";
ReceivePort _receivePort;
ItemsX ix = Get.put(ItemsX()); // Instantiate ItemsController
#override
void initState() {
super.initState();
SplashX sx = Get.put(SplashX(itemsX: ix));
// ↑ Instantiate SplashCont with ever worker
}
Change to the _handleMessage method:
void _handleMessage(dynamic data) {
//print('RECEIVED: ' + data);
ix.add(data); // update observable
setState(() {
notification = data;
});
}
And finally the debug output results showing ever worker handling observable events (Received items...) :
[GETX] "ItemsX" has been initialized
[GETX] "SplashX" has been initialized
I/flutter (19012): SEND: notification 1
I/flutter (19012): Received items: [notification 1]
I/flutter (19012): SEND: notification 2
I/flutter (19012): Received items: [notification 1, notification 2]
I/flutter (19012): SEND: notification 3
I/flutter (19012): Received items: [notification 1, notification 2, notification 3]
I/flutter (19012): done!
Controllers in Non-Isolate Page
Example of using the same controllers above, without the noise of a Stateful Widget page and all the Isolate stuff.
class SplashX extends GetxController {
ItemsX itemsX;
SplashX({this.itemsX});
#override
void onInit() {
super.onInit();
ever(itemsX.items, (items) => print('Received items: $items'));
}
}
class ItemsX extends GetxController {
RxList<String> items = RxList<String>();
void add(String item) {
items.add(item);
}
/// Only relevant for SimplePage
List<Widget> get texts => items.map((item) => Text('$item')).toList();
}
class SimplePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
ItemsX ix = Get.put(ItemsX());
SplashX sx = Get.put(SplashX(itemsX: ix));
return Scaffold(
body: SafeArea(
child: Column(
children: [
Expanded(
flex: 10,
child: Obx(
() => ListView(
children: ix.texts,
),
),
),
Expanded(
flex: 1,
child: RaisedButton(
child: Text('Add'),
onPressed: () => ix.add('more...'),
)
)
],
),
),
);
}
}

How to handle navigation using stream from inheritedWidget?

I'm using an inherited Widget to access a Bloc with some long running task (e.g. search).
I want to trigger the search on page 1 and continue to the next page when this is finished. Therefore I'm listening on a stream and wait for the result to happen and then navigate to the result page.
Now, due to using an inherited widget to access the Bloc I can't access the bloc with context.inheritFromWidgetOfExactType() during initState() and the exception as I read it, recommends doing this in didChangeDependencies().
Doing so this results in some weird behavior as the more often I go back and forth, the more often the stream I access fires which would lead to the second page beeing pushed multiple times. And this increases with each back and forth interaction. I don't understand why the stream why this is happening. Any insights here are welcome. As a workaround I keep a local variable _onSecondPage holding the state to avoid pushing several times to the second Page.
I found now How to call a method from InheritedWidget only once? which helps in my case and I could access the inherited widget through context.ancestorInheritedElementForWidgetOfExactType() and just listen to the stream and navigate to the second page directly from initState().
Then the stream behaves as I would expect, but the question is, does this have any other side effects, so I should rather get it working through listening on the stream in didChangeDependencides() ?
Code examples
My FirstPage widget listening in the didChangeDependencies() on the stream. Working, but I think I miss something. The more often i navigate from first to 2nd page, the second page would be pushed multiple times on the navigation stack if not keeping a local _onSecondPage variable.
#override
void didChangeDependencies() {
super.didChangeDependencies();
debugPrint("counter: $_counter -Did change dependencies called");
// This works the first time, after that going back and forth to the second screen is opened several times
BlocProvider.of(context).bloc.finished.stream.listen((bool isFinished) {
_handleRouting(isFinished);
});
}
void _handleRouting(bool isFinished) async {
if (isFinished && !_onSecondPage) {
_onSecondPage = true;
debugPrint("counter: $_counter - finished: $isFinished : ${DateTime.now().toIso8601String()} => NAVIGATE TO OTHER PAGE");
await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
_onSecondPage = false;
} else {
debugPrint("counter: $_counter - finished: $isFinished : ${DateTime.now().toIso8601String()} => not finished, nothing to do now");
}
}
#override
void dispose() {
debugPrint("counter: $_counter - disposing my homepage State");
subscription?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StreamBuilder(
stream: BlocProvider.of(context).bloc.counter.stream,
initialData: 0,
builder: (context, snapshot) {
_counter = snapshot.data;
return Text(
"${snapshot.data}",
style: Theme.of(context).textTheme.display1,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
A simple Bloc faking some long running work
///Long Work Bloc
class LongWorkBloc {
final BehaviorSubject<bool> startLongWork = BehaviorSubject<bool>();
final BehaviorSubject<bool> finished = BehaviorSubject<bool>();
int _counter = 0;
final BehaviorSubject<int> counter = BehaviorSubject<int>();
LongWorkBloc() {
startLongWork.stream.listen((bool start) {
if (start) {
debugPrint("Start long running work");
Future.delayed(Duration(seconds: 1), () => {}).then((Map<dynamic, dynamic> reslut) {
_counter++;
counter.sink.add(_counter);
finished.sink.add(true);
finished.sink.add(false);
});
}
});
}
dispose() {
startLongWork?.close();
finished?.close();
counter?.close();
}
}
Better working code
If I however remove the code to access the inherited widget from didChangeDependencies() and listen to the stream in the initState() it seems to be working properly.
Here I get hold of the inherited widget holding the stream through context.ancestorInheritedElementForWidgetOfExactType()
Is this ok to do so? Or what would be a flutter best practice in this case?
#override
void initState() {
super.initState();
//this works, but I don't know if this is good practice or has any side effects?
BlocProvider p = context.ancestorInheritedElementForWidgetOfExactType(BlocProvider)?.widget;
if (p != null) {
p.bloc.finished.stream.listen((bool isFinished) {
_handleRouting(isFinished);
});
}
}
Personally, I have not found any reason not to listen to BLoC state streams in initState. As long as you remember to cancel your subscription on dispose
If your BlocProvider is making proper use of InheritedWidget you should not have a problem getting your value inside of initState.
like So
void initState() {
super.initState();
_counterBloc = BlocProvider.of(context);
_subscription = _counterBloc.stateStream.listen((state) {
if (state.total > 20) {
Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context) {
return TestPush();
}));
}
});
}
Here is an example of a nice BlocProvider that should work in any case
import 'package:flutter/widgets.dart';
import 'bloc_base.dart';
class BlocProvider<T extends BlocBase> extends StatefulWidget {
final T bloc;
final Widget child;
BlocProvider({
Key key,
#required this.child,
#required this.bloc,
}) : super(key: key);
#override
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context) {
final type = _typeOf<_BlocProviderInherited<T>>();
_BlocProviderInherited<T> provider =
context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
return provider?.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T extends BlocBase> extends State<BlocProvider<BlocBase>> {
#override
Widget build(BuildContext context) {
return _BlocProviderInherited<T>(
bloc: widget.bloc,
child: widget.child,
);
}
#override
void dispose() {
widget.bloc?.dispose();
super.dispose();
}
}
class _BlocProviderInherited<T> extends InheritedWidget {
final T bloc;
_BlocProviderInherited({
Key key,
#required Widget child,
#required this.bloc,
}) : super(key: key, child: child);
#override
bool updateShouldNotify(InheritedWidget oldWidget) => false;
}
... and finally the BLoC
import 'dart:async';
import 'bloc_base.dart';
abstract class CounterEventBase {
final int amount;
CounterEventBase({this.amount = 1});
}
class CounterIncrementEvent extends CounterEventBase {
CounterIncrementEvent({amount = 1}) : super(amount: amount);
}
class CounterDecrementEvent extends CounterEventBase {
CounterDecrementEvent({amount = 1}) : super(amount: amount);
}
class CounterState {
final int total;
CounterState(this.total);
}
class CounterBloc extends BlocBase {
CounterState _state = CounterState(0);
// Input Streams/Sinks
final _eventInController = StreamController<CounterEventBase>();
Sink<CounterEventBase> get events => _eventInController;
Stream<CounterEventBase> get _eventStream => _eventInController.stream;
// Output Streams/Sinks
final _stateOutController = StreamController<CounterState>.broadcast();
Sink<CounterState> get _states => _stateOutController;
Stream<CounterState> get stateStream => _stateOutController.stream;
// Subscriptions
final List<StreamSubscription> _subscriptions = [];
CounterBloc() {
_subscriptions.add(_eventStream.listen(_handleEvent));
}
_handleEvent(CounterEventBase event) async {
if (event is CounterIncrementEvent) {
_state = (CounterState(_state.total + event.amount));
} else if (event is CounterDecrementEvent) {
_state = (CounterState(_state.total - event.amount));
}
_states.add(_state);
}
#override
void dispose() {
_eventInController.close();
_stateOutController.close();
_subscriptions.forEach((StreamSubscription sub) => sub.cancel());
}
}