How to update StatefulWidget from other StatefulWidget? - flutter

I'm having screen with 3 tabs. All has different views. When user come to the screen, I've to hit the API and update the views for all 3 tabs. I'm not able to update view inside tabs.
Not able to update the states of different tabs after getting API response.
Error
Error-> NoSuchMethodError: The method 'updateInfo' was called on null.
I/flutter ( 7515): Receiver: null
I/flutter ( 7515): Tried calling: updateInfo(Instance of 'OwnerInfo')
View of Screen is like:
DetailItem
class DetailItem extends StatefulWidget {
PropertyItem _propertyObj;
OwnerInfo _ownerInfo;
Review _reviewInfo;
#override
_DetailItemState createState() => _DetailItemState();
}
class _DetailItemState extends State<DetailItem> with TickerProviderStateMixin {
ScrollController _scrollController = new ScrollController();
double _appBarHeight = 0.0;
TabController _tabController;
final GlobalKey<AddressViewTabState> key =
new GlobalKey<AddressViewTabState>();
//Tabs used in Detail Screen
DetailViewTab _detailViewTab;
AddressViewTab _addressViewTab;
ReviewTab _reviewTab;
#override
void initState() {
_tabController = TabController(length: 3, vsync: this, initialIndex: 0); // initialize tab controller
//Initialize Tabs
_detailViewTab = DetailViewTab(widget._propertyObj);
_addressViewTab = AddressViewTab(key: key, propertyDetailModel: widget._ownerInfo);
_reviewTab = ReviewTab(widget._propertyObj);
_hitDetailAPI(); //init state Hit API
super.initState();
}
//Detail API
void _hitDetailAPI() async {
Future<PropertyDetailModel> obj = APIHandler()
.getPropertyDetail(widget._propertyObj.getRoomId.toString());
obj.then((response) {
if (!mounted) {
return;
}
//update data in set state
setState(() {
if (response != null && response.getPropertyItem != null) {
widget._propertyObj = response.getPropertyItem; // update data to property object the 1st tab(Detail)
widget._ownerInfo = response.getOwnerInfo; // update data to owner info object the second tab (Address)
key.currentState.updateInfo(widget._ownerInfo); // Tried to update the address tab view by calling through key current state.
}
});
}).catchError((error) {
print("Error-> " + error.toString());
});
}
AddressViewTab
class AddressViewTab extends StatefulWidget {
OwnerInfo ownerInfo = OwnerInfo();
AddressViewTab({ Key key , #required this.ownerInfo}) : super(key: key);
#override
AddressViewTabState createState() => AddressViewTabState();
}
class AddressViewTabState extends State<AddressViewTab>{
updateInfo(OwnerInfo ownerInfo){
print("Update method called");
if (!mounted) {
return;
}
setState(() {
widget.ownerInfo=ownerInfo; // update state
});
}
#override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: ListView(
children: <Widget>[
getOwnerInfoCard(widget.ownerInfo),
],
),
);
}
}
Any help would be appreciated. Thanks in advance.

You could use the flutter implementation of Redux for things like this https://pub.dartlang.org/packages/flutter_redux for this.
In short, this can make 'any' widget update itself when some data changes.

I already answered this question here https://stackoverflow.com/a/63832366/13439617
You can use callbacks to do this job

From pushing stateFul widget:
GestureDetector(
onTap: () async {
dynamic result = await Navigator.of(context).push(MaterialPageRoute(
builder: (context) => CreateUser(
usersObj: usersObj,
)));
if (result != null) {
setState(() {
usersObj = result;
});
}
},
From pop stateFul widget:
Navigator.pop(context,usersObj);

Related

Flutter BlocBuilder displays previous state in TextFormField value

I am trying to create a TextFormField with increment and decrement buttons and TextFormField is editable "by hand" as well. But there is a small problem if I use BLoC with this - state "falls" one behind, meaning that when I tap "+" first time nothing changes, but when I tap it the second time it changes its value to 21 (and so on..).
I tried the same implementation with just a regular Text and it works as expected and updating properly.
I'm just wondering if my logic of how I am setting TextFormField is flawed:
Instantiating TextEditingController with default value amount (20);
On "+" tap:
Adding PlusEvent to increment current value
Getting amount value from state
Widget class:
class MyCalculation extends StatefulWidget {
const MyCalculation({Key? key}) : super(key: key);
#override
State<MyCalculation> createState() => _MyCalculationState();
}
class _MyCalculationState extends State<MyCalculation> {
late TextEditingController _controller;
late MyCalcBloc _bloc;
#override
void initState() {
super.initState();
_bloc = context.read();
_controller.text = _bloc.state.amount.toString();
}
#override
void dispose() {
super.dispose();
_controller.dispose();
}
#override
Widget build(BuildContext context) {
return BlocBuilder<MyCalcBloc, MyCalcState>(builder: (context, state) {
return MyCustomTextFormField(
controller: _controller,
onChanged: (value) {},
onPlusTap: () {
_bloc.add(PlusEvent());
_bloc.text = '${state.amount}';
},
onMinusTap: () {});
});
}
}
BLoC class:
class MyCalcBloc extends Bloc<MyCalcEvent, MyCalcState> {
MyCalcBloc() : super(const MyCalcState(amount: 20)) {
on<IncrementFromEvent>(_onPlusEvent);
}
void _onPlusEvent(PlusEvent event, Emitter<MyCalcState> emit) {
final newValue = state.amount + 1;
emit(MyCalcState(amount: newValue));
}
}
You should instantiate TextEditingController within BlocProvider, that way you'll get "current" state value displayed in TextFormField.
#override
Widget build(BuildContext context) {
return BlocBuilder<MyCalcBloc, MyCalcState>(builder: (context, state) {
_controller = TextEditingController(text: state.amount.toString());
return MyCustomTextFormField(
controller: _controller,
onChanged: (value) {},
onPlusTap: () {
_bloc.add(PlusEvent());
_bloc.text = '${state.amount}';
},
onMinusTap: () {});
});
}

State and Scroll position restore Flutter

I have an app which fetches posts from a site using a API and then displays it. There are three navigation options, which are basically filters.
The problem is, whenever I switch to another navigation tab (I'm using bottom navigation bar), it ends up rebuilding the whole page, meaning it will fetch all that data again and it might potentially contain new data.
What I want to do is to keep restore this data in a way that is fast and my initState() doesn't get called(because that is what fetches the data). I did try using all the different kind of keys but I cant get it to work.
Main page:
class AppHomePage extends StatefulWidget {
AppHomePage({Key? key}) : super(key: key);
#override
_AppHomePageState createState() => _AppHomePageState();
}
List<Widget> _navs = [
BestPostsRoute(key: PageStorageKey("bestP")),
HotPostsRoute(key: PageStorageKey("hotP")),
NewPostsRoute(key: PageStorageKey("newP"))
];
class _AppHomePageState extends State<AppHomePage> {
int _currentIndex = 0;
onTap(index) => {
setState(() => {_currentIndex = index})
};
#override
Widget build(BuildContext context) {
return Scaffold(
/* appbar ... */
body: _navs.elementAt(_currentIndex),
bottomNavigationBar: BottomNavigationBar(
items: [
/* nav items */
],
currentIndex: _currentIndex,
onTap: onTap,
),
);
}
}
One of the three pages(the code is similar in all three):
/* imports... */
class HotPostsRoute extends StatefulWidget {
HotPostsRoute({Key? key}) : super(key: key);
#override
_HotPostsRouteState createState() => _HotPostsRouteState();
}
class _HotPostsRouteState extends State<HotPostsRoute> {
late PostInstance postInstance;
List<Post> _posts = [];
bool _loaded = false;
fetchPosts(String? after) async {
var stream = postInstance.front.hot(limit: 10, after: after);
await for (UserContent post in stream) {
Submission submission = post as Submission;
Post pPost = Post(submission);
pPost.parse().then((value) => setState(() {
_posts.add(pPost);
}));
}
setState(() {
_loaded = true;
});
}
#override
void initState() {
super.initState();
if (mounted) {
setState(() {
redditInstance =
Provider.of<PostInstanceState>(context, listen: false)
.getInstance;
});
fetchPosts("");
}
}
// Fetches and generates posts
Widget _buildPosts() {
return ListView.builder(
itemCount: _posts.length + 1,
itemBuilder: (ctx, index) {
if (index < _posts.length) {
return _buildPost(_posts.elementAt(index));
} else {
fetchPosts(_posts.last.fullname);
return SpinKitDualRing(color: Colors.white);
}
},
);
}
// A singular post
Widget _buildPost(Post post) {
print(post.object);
return PostCard(post, key: ObjectKey(post.object)); // .object just creates a map of all fields
}
#override
Widget build(BuildContext context) {
setState(() {});
return Container(
child: _loaded ? _buildPosts() : SpinKitDualRing(color: Colors.white),
);
}
}
So I kept searching and eventually a post on Medium led me to the IndexedStack Widget.
Its a widget that is made from the Stack widget and basically loads and stores the state of all its childrens. Unlike Stack, it shows its children one at a time and thus is perfect to use with BottomNavigationBar.
Here's the Blog post for anyone looking out.

How to force stateful widget redraw using keys?

I'm making an app that pulls data from an API and displays it in a view (MVC style).
I need to figure out how to force my view widget to redraw itself. Right now I tried with ValueKeys and ObjectKeys but to no avail.
There's lots and lots of code so I am going to use snippets as much as possible to keep it clear. (If you need to see more code feel free to ask)
Here's my view widget:
class view extends StatefulWidget{
view({
Key key,
this.count = 0,
}) : super(key: key);
int count;
String _action='';
var _actionParams='';
var _data;
Function(String) callback;
void setAction(String newAction){
_action = newAction;
}
void setActionParams(String params){
_actionParams = jsonDecode(params);
}
void setData(String data){
_data = jsonDecode(data);
}
void incrementCounter(){
count++;
}
#override
_viewState createState() => _viewState();
}
class _viewState extends State<view>{
Object redrawObject = Object();
#override
Widget build(BuildContext context) {
/*
switch(widget._action){
case '':
break;
default:
return null;
}
*/
return Text("Counter: "+widget.count.toString());
}
#override
void initState(){
this.redrawObject = widget.key;
super.initState();
}
}
You can see in the commented code that I am planning to change the way the view builds itself in function of the data that gets passed to it.
What I have tried so far is to pass a ValueKey/ObjectKey to the view from main.dart in a constructor and then changing the object at runtime. Unfortunately that did not work.
At the top of my main.dart(accessible from anywhere within main) I have this:
Object redraw = Object();
final dataView = new view(key: ObjectKey(redraw));
Then in the body of the homepage I have the view and a floating button right under.
If I press the button it should increment the counter inside the view and force it to redraw. Here's the code I have tried so far:
body: Center(
child: dataView
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.badge),
onPressed: (){
dataView.incrementCounter();
redraw = new Object();
},
),
From what I understand, if the object that was used as a key gets changed, then flutter should rebuild the state for the widget. So I'm setting my object to a new object but it's not working.
I also tried something like this:
onPressed: (){
setState((){
dataView.incrementCounter();
redraw = new Object();
});
},
Eventually I'd like to use a navigator in conjunction with my view widget (so that we have a back button) but I don't know if this is possible.
It feels a bit like I'm fighting with the framework. Is there a different paradigm I should use (like pages?) or is it possible for me to do it this way?
How do I force my view widget to get redrawn?
Using Göktuğ Vatandaş' answer and GlobalKeys I was able to figure it out.
I made a reDraw() function inside the state and then I called it from my main using a GlobalKey.
Note: Wrapping in a container and using a key for the container is not necessary. Calling setState() is enough to force a redraw.
Here's the new view widget:
import 'dart:convert';
import 'package:flutter/cupertino.dart';
GlobalKey<_viewState> viewKey = GlobalKey();
class view extends StatefulWidget{
view({
Key key,
this.count = 0,
}) : super(key: key);
int count;
String _action='';
var _actionParams='';
var _data;
Function(String) callback;
void setAction(String newAction){
_action = newAction;
}
void setActionParams(String params){
_actionParams = jsonDecode(params);
}
void setData(String data){
_data = jsonDecode(data);
}
void incrementCounter(){
count++;
}
#override
_viewState createState() => _viewState();
}
class _viewState extends State<view>{
#override
Widget build(BuildContext context) {
/*
switch(widget._action){
case '':
break;
default:
return null;
}
*/
return Text("Counter: "+widget.count.toString());
}
#override
void initState(){
super.initState();
}
void reDraw(){
setState((){});
}
}
Here's where I declare the view widget in my main:
final dataView = new view(key: viewKey);
Here's where I call the reDraw() function:
floatingActionButton: FloatingActionButton(
child: Icon(Icons.badge),
onPressed: (){
dataView.incrementCounter();
viewKey.currentState.reDraw();
},
),
Thanks Göktuğ Vatandaş!
You can check flutter_phoenix's logic for redraw effect. I think its very useful or you can just use package itself. Basically it does what you trying to achive.
It creates a unique key in state.
Key _key = UniqueKey();
Injects it to a container.
#override
Widget build(BuildContext context) {
return Container(
key: _key,
child: widget.child,
);
}
And when you call rebirth it just refresh key and that causes view to rebuild.
void restartApp() {
setState(() {
_key = UniqueKey();
});
}

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