Dart variable not final warning - flutter

So I have written this code below to make an Icon Stateful inside a Stateless widget.
class IconState extends StatefulWidget {
final bool isSelected;
IconState({
this.isSelected,
});
_IconState state; // this is not final because I need to assign it below
void toggle() {
state.change();
}
#override
_IconState createState() => state = new _IconState(
isSelected: this.isSelected,
);
}
class _IconState extends State<IconState> {
_IconState({
this.isSelected,
});
bool isSelected = false;
Widget _unSelected = Icon(
null,
);
Widget _selected = Icon(
Icons.check_outlined,
color: Colors.red,
);
void change() {
setState(() {
this.isSelected = this.isSelected == true ? false : true;
});
}
Icon evaluate() {
if (isSelected) {
return _selected;
}
return _unSelected;
}
#override
Widget build(BuildContext context) {
return evaluate();
}
}
To update the state of the Icon, I call the toggle() method from my Stateless widget.
Dart is giving me a non-final instance warning inside an #immutable class, but I am unable to find a workaround for this.
I have tried following:
final _IconState state = new _IconState(
isSelected: this.isSelected, // throws an error => Invalid reference to 'this' expression.
);
also this, but doesn't work either
final _IconState state;
IconState({this.isSelected}) {
this.state = new _IconState(
isSelected: this.isSelected,
);
};
Is there a workaround?

I would put the isSelected boolean inside an external state management class, then you can return 2 separate widgets in response to the change. Otherwise you would have to change the state inside of the widget where the icon will be displayed. Something like this:
class IconState extends ChangeNotifier{
bool _isSelected;
//any other needed state
bool get isSelected => _isSelected;
void changeIsSelected(bool selected) {
_isSelected = selected;
notifyListeners();
}
}
Then use ChangeNotifierProvider to to inject the state and call the change method.
final iconStateProvider = ChangeNotifierProvider((ref) => IconState());
Now, you can use iconStateProvider to access the state and methods. You will need a Builder or Consumer widget to listen for changes to the state.
Consumer( builder: (context, watch, child) {
final iconState = watch(iconStateProvider);
if (iconState.isSelected) {
return Icon();
} else {
return null;
}
This is using the Riverpod library, which is only 1 of many external state management libraries. I recommend watching tutorials on YouTube on different libraries and pick one that best suits you.

Related

How do I set my state when the widget builds with riverpod?

To preface, I am completely new to riverpod so my apologies if I get some terminology wrong.
I have an edit feature that I'm implementing with riverpod that I'm migrating over to from flutter_blocs. Basically, with the bloc implementation, I am fetching my data from my server and when the widget builds, the BlocConsumer will emit an event to set my data from the server into my bloc state so that I can display and edit it in a TextInput.
Bloc implementation:
BlocConsumer<JournalBloc, JournalState>(
bloc: BlocProvider.of<JournalBloc>(context)
..add(
SetModelState(
title: journalEntry.title,
rating: journalEntry.rating.toDouble(),
),
),
builder: (context, state) {
return Column(
children: [
TextInput(
labelText: 'label',
value: state.editEntryTitle.value,
onChanged: (title) => context.read<JournalBloc>().add(EditJournalEntryTitleChanged(title: title))
)
],
);
}
Now with Riverpod, where I'm stuck on is I don't know how to set my values from my server into my state when the widget renders. I have my controller, and I have my providers.
My controller:
class EditJournalEntryController extends StateNotifier<EditJournalEntryState> {
final JournalService journalService;
EditJournalEntryController({required this.journalService})
: super(EditJournalEntryState());
void setSelectedJournalType(JournalType journalType) {
state = state.copyWith(selectedJournalType: journalType);
}
void setRating(int rating) {
state = state.copyWith(rating: rating);
}
}
final editJournalEntryController =
StateNotifierProvider<EditJournalEntryController, EditJournalEntryState>((ref) {
final journalService = ref.watch(journalServiceProvider);
return EditJournalEntryController(journalService: journalService);
});
My state:
class EditJournalEntryState implements Equatable {
final AsyncValue<void> value;
final JournalType? selectedJournalType;
final int? rating;
bool get isLoading => value.isLoading;
EditJournalEntryState({
this.selectedJournalType,
this.rating,
this.value = const AsyncValue.data(null),
});
EditJournalEntryState copyWith({
MealType? selectedJournalType,
int? rating,
AsyncValue? value,
}) {
return EditJournalEntryState(
selectedJournalType: selectedJournalType ?? this.selectedJournalType,
rating: rating ?? this.rating,
value: value ?? this.value,
);
}
#override
List<Object?> get props => [selectedJournalType, rating, value];
#override
bool? get stringify => true;
}
What I have tried
In my UI, I am referencing my controller and then using my notifier to set my state:
class EditJournal extends StatelessWidget {
const EditFoodJournal({
Key? key,
required this.journalEntry, // coming from server in a few parents above
}) : super(key: key);
final JournalEntry journalEntry;
#override
Widget build(BuildContext context) {
return Consumer(
builder: ((context, ref, child) {
final state = ref.watch(editJournalEntryController);
final notifier = ref.read(editJournalEntryController.notifier);
notifier.setSelectedJournalType(journalEntry.type)
notifier.setRating(journalEntry.rating)
But for obvious reasons I get this error:
At least listener of the StateNotifier Instance of 'EditFoodJournalEntryController' threw an exception
when the notifier tried to update its state.
The exceptions thrown are:
Tried to modify a provider while the widget tree was building.
If you are encountering this error, chances are you tried to modify a provider
in a widget life-cycle, such as but not limited to:
- build
- initState
- dispose
- didUpdateWidget
- didChangeDepedencies
Modifying a provider inside those life-cycles is not allowed, as it could
lead to an inconsistent UI state. For example, two widgets could listen to the
same provider, but incorrectly receive different states.
To fix this problem, you have one of two solutions:
- (preferred) Move the logic for modifying your provider outside of a widget
life-cycle. For example, maybe you could update your provider inside a button's
onPressed instead.
- Delay your modification, such as by encasuplating the modification
in a `Future(() {...})`.
This will perform your upddate after the widget tree is done building.
Ideally I want to render that value from state and not from the variable I have.
I feel like I've ran into a wall. Is there an ideal way of handling with this?
you are trying to modify a provider while the widget tree is building.
final notifier = ref.read(editJournalEntryController.notifier);
notifier.setSelectedJournalType(journalEntry.type)//here
notifier.setRating(journalEntry.rating)//here
this will cause the issue, you need to initialize the editJournalEntryController with an initial state you can do it by .family provider modifier
Edit:
Something like this
final editJournalEntryController = StateNotifierProvider.family<
EditJournalEntryController,
EditJournalEntryState,
JournalEntry>((ref, journalEntry) {
final journalService = ref.watch(journalServiceProvider);
return EditJournalEntryController(journalService: journalService,journalentry: journalEntry);
});
EditJournalEntryController will be
class EditJournalEntryController extends StateNotifier<EditJournalEntryState> {
final JournalService journalService;
final JournalEntry journalentry;
EditJournalEntryController({required this.journalService,required this.journalentry})
: super(EditJournalEntryState(selectedJournalType: journalentry.type,rating: journalentry.rating));
void setSelectedJournalType(String journalType) {
state = state.copyWith(selectedJournalType: journalType);
}
void setRating(int rating) {
state = state.copyWith(rating: rating);
}
}
will be called like
class EditFoodJournal extends StatelessWidget {
const EditFoodJournal({
Key? key,
required this.journalEntry, // coming from server in a few parents above
}) : super(key: key);
final JournalEntry journalEntry;
#override
Widget build(BuildContext context) {
return Consumer(builder: (context, ref, child) {
final state = ref.watch(editJournalEntryController(journalEntry));
return Container();
});
}
}

Flutter Custom State Management

What I am trying to achieve is a small custom state management solution that I believe is powerful enough to run small and large apps. The core is based on the ValueNotifier and ValueListenable concepts in flutter. The data can be accessed anywhere in the app with out context since I am storing the data like this:
class UserData {
static ValueNotifier<DataLoader<User>> userData =
ValueNotifier(DataLoader<User>());
static Future<User> loadUserData() async {
await Future.delayed(const Duration(seconds: 3));
User user = User();
user.age = 23;
user.family = 'Naoushy';
user.name = 'Anass';
return user;
}
}
So by using UserData.userData you can use the data of the user whenever you want. Everything works fine until I encountered a problem of providing a child to my custom data consumer that rebuilds the widget when there is a new event fired. The DataLoader class looks like this:
enum Status { none, hasError, loading, loaded }
class DataLoader<T> {
Status status = Status.none;
T? data;
Object? error;
bool get hasError => error != null;
bool get hasData => data != null;
}
which is very simple. Now the class for consuming the data and rebuilding looks like this:
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:testing/utils/dataLoader/data_loader.dart';
class DataLoaderUI<T> extends StatefulWidget {
final ValueNotifier<DataLoader<T>> valueNotifier;
final Widget noneStatusUI;
final Widget hasErrorUI;
final Widget loadingUI;
final Widget child;
final Future<T> future;
const DataLoaderUI(
{Key? key,
required this.valueNotifier,
this.noneStatusUI = const Text('Data initialization has not started'),
this.hasErrorUI = const Center(child: Text('Unable to fetch data')),
this.loadingUI = const Center(
child: CircularProgressIndicator(),
),
required this.child,
required this.future})
: super(key: key);
#override
State<DataLoaderUI> createState() => _DataLoaderUIState();
}
class _DataLoaderUIState extends State<DataLoaderUI> {
Future startLoading() async {
widget.valueNotifier.value.status = Status.loading;
widget.valueNotifier.notifyListeners();
try {
var data = await widget.future;
widget.valueNotifier.value.data = data;
widget.valueNotifier.value.status = Status.loaded;
widget.valueNotifier.notifyListeners();
} catch (e) {
log('future error', error: e.toString());
widget.valueNotifier.value.error = e;
widget.valueNotifier.value.status = Status.hasError;
widget.valueNotifier.notifyListeners();
}
}
#override
void initState() {
super.initState();
log('init state launched');
if (!widget.valueNotifier.value.hasData) {
log('reloading or first loading');
startLoading();
}
}
//AsyncSnapshot asyncSnapshot;
#override
Widget build(BuildContext context) {
return ValueListenableBuilder<DataLoader>(
valueListenable: widget.valueNotifier,
builder: (context, dataLoader, ui) {
if (dataLoader.status == Status.none) {
return widget.noneStatusUI;
} else if (dataLoader.status == Status.hasError) {
return widget.hasErrorUI;
} else if (dataLoader.status == Status.loading) {
return widget.loadingUI;
} else {
return widget.child;
}
});
}
}
which is also simple yet very effective. since even if the initState function is relaunched if the data is already fetched the Future will not relaunch.
I am using the class like this:
class TabOne extends StatefulWidget {
static Tab tab = const Tab(
icon: Icon(Icons.upload),
);
const TabOne({Key? key}) : super(key: key);
#override
State<TabOne> createState() => _TabOneState();
}
class _TabOneState extends State<TabOne> {
#override
Widget build(BuildContext context) {
return DataLoaderUI<User>(
valueNotifier: UserData.userData,
future: UserData.loadUserData(),
child: Text(UserData.userData.value.data!.name??'No name'));
}
}
The error is in this line:
Text(UserData.userData.value.data!.name??'No name'));
Null check operator used on a null value
Since I am passing the Text widget as an argument with the data inside it. Flutter is trying to pass it but not able to since there is no data yet so its accessing null values. I tried with a normal string and it works perfectly. I looked at the FutureBuilder widget and they use a kind of builder and also the ValueLisnableBuilder has a builder as an arguement. The problem is that I am not capable of creating something like it for my custom solution. How can I just pass the child that I want without having such an error and without moving the ValueLisnable widget into my direct UI widget?
I have found the solution.
Modify the DataLoaderUI class to this:
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:testing/utils/dataLoader/data_loader.dart';
class DataLoaderUI<T> extends StatefulWidget {
final ValueNotifier<DataLoader<T>> valueNotifier;
final Widget noneStatusUI;
final Widget hasErrorUI;
final Widget loadingUI;
final Widget Function(T? snapshotData) child;
final Future<T> future;
const DataLoaderUI(
{Key? key,
required this.valueNotifier,
this.noneStatusUI = const Text('Data initialization has not started'),
this.hasErrorUI = const Center(child: Text('Unable to fetch data')),
this.loadingUI = const Center(
child: CircularProgressIndicator(),
),
required this.child,
required this.future})
: super(key: key);
#override
State<DataLoaderUI<T>> createState() => _DataLoaderUIState<T>();
}
class _DataLoaderUIState<T> extends State<DataLoaderUI<T>> {
Future startLoading() async {
widget.valueNotifier.value.status = Status.loading;
widget.valueNotifier.notifyListeners();
try {
var data = await widget.future;
widget.valueNotifier.value.data = data;
widget.valueNotifier.value.status = Status.loaded;
widget.valueNotifier.notifyListeners();
} catch (e) {
log('future error', error: e.toString());
widget.valueNotifier.value.error = e;
widget.valueNotifier.value.status = Status.hasError;
widget.valueNotifier.notifyListeners();
}
}
#override
void initState() {
super.initState();
log('init state launched');
if (!widget.valueNotifier.value.hasData) {
log('reloading or first loading');
startLoading();
}
}
//AsyncSnapshot asyncSnapshot;
#override
Widget build(BuildContext context) {
return ValueListenableBuilder<DataLoader<T>>(
valueListenable: widget.valueNotifier,
builder: (context, dataLoader, ui) {
if (dataLoader.status == Status.none) {
return widget.noneStatusUI;
} else if (dataLoader.status == Status.hasError) {
return widget.hasErrorUI;
} else if (dataLoader.status == Status.loading) {
return widget.loadingUI;
} else {
return widget.child(dataLoader.data);
}
});
}
}
and use it like this:
DataLoaderUI<User>(
valueNotifier: UserData.userData,
future: UserData.loadUserData(),
child: (user) {
return Text(user!.name ?? 'kk');
});
Take a look at my version of the same sort of state management approach here: https://github.com/lukehutch/flutter_reactive_widget

I want to use data from a Future inside a ChangeNotifier Provider and a ListView

I can't figure out how to get the data from the myProvider before I call the getWalletItems(). Should I do 2 seperate providers??
My goal here is just to get all these items from a Future<List<Wallet'>> and return them into a listview that is able to have each item be selectable with a checkbox which will then pass on all the selected items to a different page. They will not be rebuilt there so I don't think I need another model but if I do just let me know. Here is my code for the ChangeNotifier:
class WalletModel extends ChangeNotifier {
List<Wallet> _wallet = [];
List<Wallet> get wallet => _wallet;
set wallet(List<Wallet> newValue) {
_wallet = newValue;
notifyListeners();
}
myProvider() {
loadValue();
}
Future<void> loadValue() async {
wallet = await WalletApi.getWalletItems();
}
UnmodifiableListView<Wallet> get allWalletItems =>
UnmodifiableListView(_wallet);
UnmodifiableListView<Wallet> get incompleteTasks =>
UnmodifiableListView(_wallet.where((_wallet) => !_wallet.isSelected));
UnmodifiableListView<Wallet> get completedTasks =>
UnmodifiableListView(_wallet.where((_wallet) => _wallet.isSelected));
void toggleWallet(Wallet wallet) {
final walletIndex = _wallet.indexOf(wallet);
_wallet[walletIndex].toggleSelected();
notifyListeners();
}
}
Here is the checkbox to select
Checkbox(
value: wallet.isSelected,
onChanged: (bool? checked) {
Provider.of<WalletModel>(context, listen: false)
.toggleWallet(wallet);
},
),
Here is the listview and if I need to post anyother code just let me know because I'm quite lost on what to do.
class WalletList extends StatelessWidget {
final List<Wallet> wallets;
WalletList({required this.wallets});
#override
Widget build(BuildContext context) {
return ListView(
children: getWalletListItems(),
);
}
List<Widget> getWalletListItems() {
return wallets
.map((walletItem) => WalletListItem(wallet: walletItem))
.toList();
}
}
make myProvider() a future and then use below code for WalletList Widget
before build runs for WalletList we want to get the items from the provider so we have used didChangedDependencies() as it runs before build and can be converted to future.
when the list is got we use the list that was set by above the make the UI
Note : Consumer changes its state whenever notifyListener() is called in Provider.
import 'package:flutter/material.dart';
class WalletList extends StatefulWidget {
#override
_WalletListState createState() => _WalletListState();
}
class _WalletListState extends State<WalletList> {
bool _isInit = true;
#override
void didChangeDependencies() async {
//boolean used to run the set list fucntion only once
if (_isInit) {
//this will save the incoming data to list before build runs
await Provider.of<WalletModel>(context, listen: false).myProvider();
_isInit = false;
}
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
return Consumer<WalletModel>(builder: (context, providerInstance, _) {
return ListView(
children: providerInstance
.wallet
.map<Widget>((walletItem) => WalletListItem(wallet: walletItem))
.toList(),
);
});
}
// List<Widget> getWalletListItems() {
// return Provider.of<WalletModel>(context, listen: false)
// .wallet
// .map((walletItem) => WalletListItem(wallet: walletItem))
// .toList();
// }
}

Accessing Flutter context when creating StatefulWidget

I'm having trouble accessing a services object when initializing a stateful widget. The problem comes from the context object not being available in initState.
I'm using InheritedWidget to inject a services object in my main.dart file like so
void main() async {
final sqflite.Database database = await _openDatabase('db.sqlite3');
runApp(
Services(
database: database,
child: MyApp(),
),
);
}
The Services object is quite straightforward. It will have more than just the database as a member. The idea is that the widgets don't need to know if a local database, local cache, or remote server is being accessed.
class Services extends InheritedWidget {
final Database database;
const Services({
Key key,
#required Widget child,
#required this.database,
}) : assert(child != null),
assert(database != null),
super(key: key, child: child);
Future<List<models.Animal>> readAnimals() async {
return db.readAnimals(database: this.database);
}
#override
bool updateShouldNotify(InheritedWidget oldWidget) {
return false;
}
static Services of(BuildContext context) {
return context.inheritFromWidgetOfExactType(Services) as Services;
}
}
The trouble comes in my _HomePageState state when I want to load all the animals from the database. I need to access the Services object. I cannot access the Services object in initState so I am using didChangeDependencies. A problem comes when the home page is removed from the stack. It seems didChangeDependences is called and the access to the context object is illegal. So I created an _initialized flag that I can use in didChangeDependencies to ensure I only load the animals the first time. This seems very inelegant. Is there a better way?
class _HomePageState extends State<HomePage> {
bool _initialized = false;
bool _loading = false;
List<Animal> _animals;
#override
Widget build(final BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(Strings.of(this.context).appName),
),
body: _HomeBody(
loading: this._loading,
animals: this._animals,
),
);
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
if (!this._initialized) {
this._initialized = true;
this._loadAnimals();
}
}
void _loadAnimals() async {
this.setState(() {
this._loading = true;
this._animals = null;
});
final List<Animal> animals = await Services.of(this.context).readAnimals();
this.setState(() {
this._loading = false;
this._animals = animals;
});
}
}
For that case you could use addPostFrameCallback of your WidgetsBinding instance to execute some code after your widget was built.
_onLayoutDone(_) {
this._loadAnimals();
}
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_onLayoutDone);
super.initState();
}

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