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

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);
},
),
),
],
);
}
}

Related

Timer doesn't cancel - Flutter Provider

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!");
}
}
}

SetState called during build()

I wrote logic with edit mode which allows user to make changes in input field, but when edit mode button is clicked again then input need back to value before editing. And there is a problem with that, because everything works fine but console is showing me this error:
════════ Exception caught by foundation library ════════════════════════════════
The following assertion was thrown while dispatching notifications for TextEditingController:
setState() or markNeedsBuild() called during build.
This Form widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was: Form-[LabeledGlobalKey<FormState>#bcaba]
state: FormState#65267
The widget which was currently being built when the offending call was made was: ProfileInput
dirty
state: _ProfileInputState#32ea5
I know what this error means, but I can't find a place responsible for this. Could someone explain it to me?
class Profile extends StatefulWidget {
#override
_ProfileState createState() => _ProfileState();
}
class _ProfileState extends State<Profile> {
GlobalKey<FormState> _formKey = GlobalKey<FormState>();
User _user = User(
username: "name",
);
String? _tmpUsername;
bool _editMode = false;
void _createTemporaryData() {
_tmpUsername = _user.username;
}
void _restoreData() {
_user.username = _tmpUsername!;
}
void _changeMode() {
if (_editMode)
_restoreData();
else
_createTemporaryData();
setState(() {
_editMode = !_editMode;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
ElevatedButton(
onPressed: () => _changeMode(), child: Text("change mode")),
Form(
key: _formKey,
child: ProfileInput(
editMode: _editMode,
user: _user,
onChangeName: (value) {
_user.username = value;
},
),
),
],
),
);
}
}
class ProfileInput extends StatefulWidget {
final bool editMode;
final User user;
final void Function(String value)? onChangeName;
ProfileInput({
required this.editMode,
required this.user,
required this.onChangeName,
});
#override
_ProfileInputState createState() => _ProfileInputState();
}
class _ProfileInputState extends State<ProfileInput> {
TextEditingController _nameController = TextEditingController();
#override
void dispose() {
_nameController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
_nameController.text = widget.user.username;
return TextFormField(
onChanged: widget.onChangeName,
controller: _nameController,
enabled: widget.editMode,
);
}
}
Put the following line in the initState or use addPostFrameCallback.
_nameController.text = widget.user.username; // goes into initState
initState
#override
void initState() {
super.initState();
_nameController.text = widget.user.username;
}
addPostFrameCallback
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_nameController.text = widget.user.username;
}); // 1
SchedulerBinding.instance.addPostFrameCallback((_) {
_nameController.text = widget.user.username;
}); // 2
// use either 1 or 2.
// rest of the code, return statement.
}
Calling text setter on _nameController would notify all the listener and it's called inside the build method during an ongoing build that causes setState() or markNeedsBuild() called during build.
From Documentation:
Setting this will notify all the listeners of this TextEditingController that they need to update (it calls notifyListeners). For this reason, this value should only be set between frames, e.g. in response to user actions, not during the build, layout, or paint phases.

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...'),
)
)
],
),
),
);
}
}

'call' was called on null for parent callback function from stateful widget

I want to have a multi-section form which has several subforms which when completed and validated will callback to the parent widget in order to go to the next subform.
my issue is I keep getting an error saying the function is null and I'm not sure why.
iv tried having a function within the StatefulWidget of the child but I always get an error saying the function is null when calling the parents passed through 'complete' function.
I/flutter (23821): ══╡ EXCEPTION CAUGHT BY GESTURE ╞═══════════════════════════════════════════════════════════════════
I/flutter (23821): The following NoSuchMethodError was thrown while handling a gesture:
I/flutter (23821): The method 'call' was called on null.
I/flutter (23821): Receiver: null
I/flutter (23821): Tried calling: call()
I thought I was doing it correctly but am a bit lost on what to try next, here is my code :
parent widget
class CreateForm extends StatefulWidget {
CreateForm({
Key key
}): super(key: key);
CreateFormState createState() => CreateFormState();
}
class CreateFormState extends State < CreateForm > {
bool userDetailsCompleted = false;
final formKey = GlobalKey < FormState > ();
#override
Widget build(BuildContext context) {
return !userDetailsCompleted ? UserDetailsForm(
completed: () {
print("testing");
// setState(() {
// userDetailsCompleted = true;
// });
},
) : nextSection(
);
}
}
child widget
class UserDetailsForm extends StatefulWidget {
final Function completed;
UserDetailsForm({
#required this.completed,
Key key
}): super(key: key);
UserDetailsFormState createState() => UserDetailsFormState();
}
class UserDetailsFormState extends State < UserDetailsForm > {
#override
Widget build(BuildContext context) {
return Form(
child: CustomButton(
size: 15,
text: 'Submit',
onPressed: () {
print('UserDetails account form submit');
widget.completed();
},
)
);
}
}
I tried your code locally, and it works. I think you just need to restart the Flutter, as maybe you just replaced Stateles to Stateful widget a moment before
Fully-working repo
You can build it locally also in this Github repo
demo

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());
}
}