How to update streamProvider stream based on other providers value - flutter

I want to update StreamProvider stream, based on the value of the Provider. I arrived at this solution.
return Provider<SelectedDay>.value(
value: selectedDay,
child: ProviderToStream<SelectedDay>(
builder: (_, dayStream, Widget child) {
return StreamProvider<DailyHomeData>(
initialData: DailyHomeData.defaultValues(DateTime.now()),
create: (_) async* {
await for (final selectedDay in dayStream) {
yield await db
.dailyHomeDataStream(
dateTime: selectedDay.selectedDateTime)
.first;
}
},
child: MainPage(),
);
},
),
);
This is the providerToStream class, which i copied from here Trouble rebuilding a StreamProvider to update its current data
class ProviderToStream<T> extends StatefulWidget {
const ProviderToStream({Key key, this.builder, this.child}) : super(key: key);
final ValueWidgetBuilder<Stream<T>> builder;
final Widget child;
#override
_ProviderToStreamState<T> createState() => _ProviderToStreamState<T>();
}
class _ProviderToStreamState<T> extends State<ProviderToStream> {
final StreamController<T> controller = StreamController<T>();
#override
void dispose() {
controller.close();
super.dispose();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
controller.add(Provider.of<T>(context));
}
#override
Widget build(BuildContext context) {
return widget.builder(context, controller.stream, widget.child);
}
}
And this is the error i get when i try to use it
type '(BuildContext, Stream<SelectedDay>, Widget) => StreamProvider<DailyHomeData>' is not a subtype of type '(BuildContext, Stream<dynamic>, Widget) => Widget'
Note: The code doesnt even work when i place a dummy widget inside the ProviderToStream widget.
child: ProviderToStream<SelectedDay>(
builder: (_, ___, Widget child) {
return child;
},
),
I also tried to force somehow the type in the builder, so that it is not dynamic, with no luck
child: ProviderToStream<SelectedDay>(
builder: (_, Stream<SelectedDay> dayStream, Widget child) {
return child;
},
),

Related

ChangeNotifierProvider not re-rendering the UI (Flutter,Dart,Provider)

I have a BaseView that contains ChangeNotifieProvider and Consumer which will be common to use anywhere. This Widget also receives Generic types of ViewModel. It has onModelReady that to be called inside init state.
Also using get_it for Dependency injection.
Issue: Whenever the user inserts a new entry and calls fetch data, data gets loaded but UI still remains as it is.
If I remove the ChangeNotifierProvider and use only Consumer then it's re-rendering UI in a proper way. But I cannot pass the onModelReady function that is to be called in initState()
:::::::::::CODE:::::::::::::::::::::::::
base_view.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:businesshub/injections/injection_container.dart';
import 'package:businesshub/features/views/viewmodels/base_model.dart';
class BaseView<T extends BaseModel> extends StatefulWidget {
const BaseView({
Key? key,
this.onModelReady,
required this.builder,
}) : super(key: key);
final Function(T)? onModelReady;
final Widget Function(BuildContext context, T model, Widget? child) builder;
#override
_BaseViewState<T> createState() => _BaseViewState();
}
class _BaseViewState<T extends BaseModel> extends State<BaseView<T>> {
T model = locator<T>();
#override
void initState() {
super.initState();
if (widget.onModelReady != null) {
widget.onModelReady!(model);
}
}
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<T>(
create: (context) => model,
child: Consumer<T>(
builder: widget.builder,
),
);
}
}
USING::::::::::::::::HERE::::::::::::::::::::::
class RecentBillBuilder extends StatelessWidget {
const RecentBillBuilder({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return BaseView<SalesBillViewModel>(
onModelReady: (model) {
model.fetchAndSetSalesBills(currentUser!.uid);
model.searchController.clear();
},
builder: (ctx, model, _) {
if (model.state == ViewState.busy) {
return Center(
child: CircularProgressIndicator.adaptive(),
);
}
return model.bills.fold(
(l) => ResourceNotFound(title: l.message!),
(r) => (r.isEmpty)
? ResourceNotFound(title: "Sales Bills not created yet!")
: ListView.builder(
itemCount: min(r.length, 7),
shrinkWrap: true,
reverse: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (ctx, index) {
return RecentBillsCard(bill: r[index]);
},
),
);
},
);
}
}

How to add controller to streamBuilder?

Flutter
Dart
I am a beginner in flutter and i am trying to add controller to streamBuilderWidget so i can dispose it but i have no idea where should i put the controller.. i tried this
the stream below as a widget not function
StreamController<QuerySnapshot> controller;
void dispose() {
super.dispose();
controller.close();
}
void initState() {
super.initState();
controller = StreamController<QuerySnapshot>();
}
StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection("users").doc(widget.documentUid).snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) {
return Center(
child: circulearProgress(),
);
}
in this code it never disposed or closed the stream:(
Anyone who edits my code in the right way will be very grateful to him , thanks friends
StreamController is like a pipeline. In your case, that pipeline went from water supplier to your house, there is no need to worried about what goes in there.
But if you want to set up a pipeline from your washing machine to the draining hole, that is where you need to use StreamController.
Example:
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final NumberController controller = NumberController();
#override
void dispose() {
controller.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
InputWidget(controller: controller,),
OutputWidget(controller: controller,)
],
),
),
),
);
}
}
class NumberController {
//This is the pipeline of "number"
final StreamController<int> controller = StreamController<int>.broadcast();
//This is where your "number" go in
Sink<int> get inputNumber => controller.sink;
//This is where your "number" go out
Stream<int> get outputNumber => controller.stream;
//Dispose
void dispose() {
controller.close();
}
}
class InputWidget extends StatelessWidget {
final NumberController controller;
const InputWidget({Key key, this.controller}) : super(key: key);
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
controller.inputNumber.add(Random().nextInt(10));
},
child: Text(
'Random Number'
),);
}
}
class OutputWidget extends StatelessWidget {
final NumberController controller;
const OutputWidget({Key key, this.controller}) : super(key: key);
#override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: controller.outputNumber,
builder: (context, snapshot) {
return Text(snapshot.hasData ? snapshot.data.toString() : 'No data');
},
);
}
}
You don't have to use StreamController. StreamBuilder you are using closes the stream internally.
From your comments, you seem to want to close the listener in the method below:
void handleDelete() {
FirebaseFirestore.instance.collection("handleCountM").doc(currentUser.uid + widget.documentUid).collection("handleCountM2").limit(1).snapshots()
.listen((value) {
value.docs.forEach((element) {
element.reference.delete();
});
});
}
You can do that by getting a reference to the stream subscription and calling .cancel on the subscription.
Calling .listen on a stream returns a stream subscription object like this:
StreamSubscription handleDeleteStreamSubscription = FirebaseFirestore.instance.collection("handleCountM").doc(currentUser.uid + widget.documentUid).collection("handleCountM2").limit(1).snapshots()
.listen((value) {
value.docs.forEach((element) {
element.reference.delete();
});
});
Cancelling the subscription is done like this:
handleDeleteStreamSubscription.cancel();

Is there GlobalKey counterpart of HookWidget?

I want to call the onPopPage in the following code in the parent widget.
In traditional way I may use GlobalKey<SomeState>().currentState.someMethod.
But what's the desirable way with HookWidget?
class SomeWidget extends HookWidget {
final innerNavigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
final isPasswordReset = useState(false);
final onPopPage = useMemoized(() => (_, __) {
if (isInInnerRoute.value &&
innerNavigatorKey.currentState?.onWillPop() == true) {
return true;
}
return false;
});
return Navigator(
pages: [
NoTransitionPage(child: _buildLogin(() {
isInInnerRoute.value = true;
})),
if (isInInnerRoute.value)
NoTransitionPage(
child: SomePage(navigatorKey: innerNavigatorKey)),
],
onPopPage: onPopPage,
);
}
}
class NoTransitionPage extends Page {
final Widget child;
NoTransitionPage({Key key, this.child}) : super(key: key);
Route createRoute(BuildContext context) {
return PageRouteBuilder(
maintainState: false,
settings: this,
pageBuilder: (_, __, ___) => child,
);
}
}
I asked to library author and got an answer. The gist is key usage should be avoided.

How to show errors from ChangeNotifier using Provider in Flutter

I'm trying to find the best way to show errors from a Change Notifier Model with Provider through a Snackbar.
Is there any built-in way or any advice you could help me with?
I found this way that is working but I don't know if it's correct.
Suppose I have a simple Page where I want to display a list of objects and a Model where I retrieve those objects from api. In case of error I notify an error String and I would like to display that error with a SnackBar.
page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Page extends StatefulWidget {
Page({Key key}) : super(key: key);
#override
_PageState createState() => _PageState();
}
class _PageState extends State< Page > {
#override
void initState(){
super.initState();
Provider.of<Model>(context, listen: false).load();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
Provider.of< Model >(context, listen: false).addListener(_listenForErrors);
}
#override
Widget build(BuildContext context){
super.build(context);
return Scaffold(
appBar: AppBar(),
body: Consumer<Model>(
builder: (context, model, child){
if(model.elements != null){
...list
}
else return LoadingWidget();
}
)
)
);
}
void _listenForErrors(){
final error = Provider.of<Model>(context, listen: false).error;
if (error != null) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
backgroundColor: Colors.red[600],
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.error),
Expanded(child: Padding( padding:EdgeInsets.only(left:16), child:Text(error) )),
],
),
),
);
}
}
#override
void dispose() {
Provider.of<PushNotificationModel>(context, listen: false).removeListener(_listenForErrors);
super.dispose();
}
}
page_model.dart
import 'package:flutter/foundation.dart';
class BrickModel extends ChangeNotifier {
List<String> _elements;
List<String> get elements => _elements;
String _error;
String get error => _error;
Future<void> load() async {
try{
final elements = await someApiCall();
_elements = [..._elements, ...elements];
}
catch(e) {
_error = e.toString();
}
finally {
notifyListeners();
}
}
}
Thank you
Edit 2022
I ported (and reworked) this package also for river pod if anyone is interested
https://pub.dev/packages/riverpod_messages/versions/1.0.0
EDIT 2020-06-05
I developed a slightly better approach to afford this kink of situations.
It can be found at This repo on github so you can see the implementation there, or use this package putting in your pubspec.yaml
provider_utilities:
git:
url: https://github.com/quantosapplications/flutter_provider_utilities.git
So when you need to present messages to the view you can:
extend your ChangeNotifier with MessageNotifierMixin that gives your ChangeNotifier two properties, error and info, and two methods, notifyError() and notifyInfo().
Wrap your Scaffold with a MessageListener that will present a Snackbar when it gets called notifyError() or NotifyInfo()
I'll give you an example:
ChangeNotifier
import 'package:flutter/material.dart';
import 'package:provider_utilities/provider_utilities.dart';
class MyNotifier extends ChangeNotifier with MessageNotifierMixin {
List<String> _properties = [];
List<String> get properties => _properties;
Future<void> load() async {
try {
/// Do some network calls or something else
await Future.delayed(Duration(seconds: 1), (){
_properties = ["Item 1", "Item 2", "Item 3"];
notifyInfo('Successfully called load() method');
});
}
catch(e) {
notifyError('Error calling load() method');
}
}
}
View
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_utilities/provider_utilities.dart';
import 'notifier.dart';
class View extends StatefulWidget {
View({Key key}) : super(key: key);
#override
_ViewState createState() => _ViewState();
}
class _ViewState extends State<View> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: MessageListener<MyNotifier>(
child: Selector<MyNotifier, List<String>>(
selector: (ctx, model) => model.properties,
builder: (ctx, properties, child) => ListView.builder(
itemCount: properties.length,
itemBuilder: (ctx, index) => ListTile(
title: Text(properties[index])
),
),
)
)
);
}
}
OLD ANSWER
thank you.
Maybe I found a simpler way to handle this, using the powerful property "child" of Consumer.
With a custom stateless widget (I called it ErrorListener but it can be changed :))
class ErrorListener<T extends ErrorNotifierMixin> extends StatelessWidget {
final Widget child;
const ErrorListener({Key key, #required this.child}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<T>(
builder: (context, model, child){
//here we listen for errors
if (model.error != null) {
WidgetsBinding.instance.addPostFrameCallback((_){
_handleError(context, model); });
}
// here we return child!
return child;
},
child: child
);
}
// this method will be called anytime an error occurs
// it shows a snackbar but it could do anything you want
void _handleError(BuildContext context, T model) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
backgroundColor: Colors.red[600],
content: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.error),
Expanded(child: Padding( padding:EdgeInsets.only(left:16), child:Text(model.error) )),
],
),
),
);
// this will clear the error on model because it has been handled
model.clearError();
}
}
This widget must be put under a scaffold if you want to use a snackbar.
I use a mixin here to be sure that model has a error property and a clarError() method.
mixin ErrorNotifierMixin on ChangeNotifier {
String _error;
String get error => _error;
void notifyError(dynamic error) {
_error = error.toString();
notifyListeners();
}
void clearError() {
_error = null;
}
}
So for example we can use this way
class _PageState extends State<Page> {
// ...
#override
Widget build(BuildContext context) =>
ChangeNotifierProvider(
builder: (context) => MyModel(),
child: Scaffold(
body: ErrorListener<MyModel>(
child: MyBody()
)
)
);
}
You can create a custom StatelessWidget to launch the snackbar when the view model changes. For example:
class SnackBarLauncher extends StatelessWidget {
final String error;
const SnackBarLauncher(
{Key key, #required this.error})
: super(key: key);
#override
Widget build(BuildContext context) {
if (error != null) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => _displaySnackBar(context, error: error));
}
// Placeholder container widget
return Container();
}
void _displaySnackBar(BuildContext context, {#required String error}) {
final snackBar = SnackBar(content: Text(error));
Scaffold.of(context).hideCurrentSnackBar();
Scaffold.of(context).showSnackBar(snackBar);
}
}
We can only display the snackbar once all widgets are built, that's why we have the WidgetsBinding.instance.addPostFrameCallback() call above.
Now we can add SnackBarLauncher to our screen:
class SomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Title',
),
),
body: Stack(
children: [
// Other widgets here...
Consumer<EmailLoginScreenModel>(
builder: (context, model, child) =>
SnackBarLauncher(error: model.error),
),
],
),
);
}
}

ValueListenableBuilder: listen to more than one value

I am using ValueListenableBuilder to watch some values in several animated custom widgets. Those are using several values, based on their children sizes, to animate depending actions.
My problem is listening to more than one value. I have to nest them.
The following is a reduced example to explain:
class MyWidget extends StatelessWidget {
final ValueNotifier<double> _height1 = ValueNotifier<double>(40);
final ValueNotifier<double> _height2 = ValueNotifier<double>(120);
final ValueNotifier<double> _height3 = ValueNotifier<double>(200);
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
builder: (BuildContext context, double h1, Widget child) {
return ValueListenableBuilder(
builder: (BuildContext context, double h2, Widget child) {
return ValueListenableBuilder(
builder: (BuildContext context, double h3, Widget child) {
return GestureDetector(
// i am using h1, h2 and h3 here ...
child: Stack(
children: <Widget>[
Positioned(
left: 0,
right: 0,
bottom: value,
child: Column(
children: <Widget>[
Container(height: _height1.value),
Container(height: _height2.value),
Container(height: _height3.value),
],
),
),
],
),
);
},
valueListenable: _height3,
child: null);
},
valueListenable: _height2,
child: null,
);
},
valueListenable: _height1,
child: null,
);
}
}
https://github.com/flutter/flutter/issues/40906#issuecomment-533383128 give a short hint to Listenable.merge, but I have no idea how to use this.
There's nothing built-in.
You could use Listenable.merge. But it has flaws:
it's not "safe" (you can forget to listen to a value)
it's not ideal performance-wise (since we're recreating a Listenable on every build)
Instead, we can use composition: we can write a stateless widget that combines 2+ ValueListenableBuilders into one, and use that.
It'd be used this way:
ValueListenable<SomeValue> someValueListenable;
ValueListenable<AnotherValue> anotherValueListenable;
ValueListenableBuilder2<SomeValue, AnotherValue>(
someValueListenable,
anotherValueListenable,
builder: (context, someValue, anotherValue, child) {
return Text('$someValue $anotherValue');
},
child: ...,
);
Where the code of such ValueListenableBuilder2 is:
class ValueListenableBuilder2<A, B> extends StatelessWidget {
ValueListenableBuilder2(
this.first,
this.second, {
Key key,
this.builder,
this.child,
}) : super(key: key);
final ValueListenable<A> first;
final ValueListenable<B> second;
final Widget child;
final Widget Function(BuildContext context, A a, B b, Widget child) builder;
#override
Widget build(BuildContext context) {
return ValueListenableBuilder<A>(
valueListenable: first,
builder: (_, a, __) {
return ValueListenableBuilder<B>(
valueListenable: second,
builder: (context, b, __) {
return builder(context, a, b, child);
},
);
},
);
}
}
UPDATE: null safety version:
class ValueListenableBuilder2<A, B> extends StatelessWidget {
const ValueListenableBuilder2({
required this.first,
required this.second,
Key? key,
required this.builder,
this.child,
}) : super(key: key);
final ValueListenable<A> first;
final ValueListenable<B> second;
final Widget? child;
final Widget Function(BuildContext context, A a, B b, Widget? child) builder;
#override
Widget build(BuildContext context) => ValueListenableBuilder<A>(
valueListenable: first,
builder: (_, a, __) {
return ValueListenableBuilder<B>(
valueListenable: second,
builder: (context, b, __) {
return builder(context, a, b, child);
},
);
},
);
}
You cannot use ValueListenableBuilder with Listenable.merge because merge returns only a Listenable and no ValueListenable and ValueListenableBuilder expects a Value Changenotifier.
You can use AnimatedBuilder instead which despite its name is just a ListenableBuilder.
class MyWidget extends StatelessWidget {
final ValueNotifier<double> _height1 = ValueNotifier<double>(40);
final ValueNotifier<double> _height2 = ValueNotifier<double>(120);
final ValueNotifier<double> _height3 = ValueNotifier<double>(200);
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: Listenable.merge([_height1, _height2, _height3]),
builder: (BuildContext context, _) {
return GestureDetector(
// i am using h1, h2 and h3 here ...
child: Stack(
children: <Widget>[
Positioned(
left: 0,
right: 0,
bottom: value,
child: Column(
children: <Widget>[
Container(height: _height1.value),
Container(height: _height2.value),
Container(height: _height3.value),
],
),
),
],
),
);
},
);
}
}
I too ran into same problem and had to use nested ValueListenableBuilder widgets. But then I wrapped whole logic inside a custom MultiValueListenableBuilder widget and published it as a Flutter package.
Here is link to my package: https://pub.dev/packages/multi_value_listenable_builder
Using this package you can provide a list of ValueListenable and their latest values will be available in same order in builder function. Here is an example.
MultiValueListenableBuider(
// Add all ValueListenables here.
valueListenables: [
listenable0,
listenable1,
listenable2,
.
.
listenableN
],
builder: (context, values, child) {
// Get the updated value of each listenable
// in values list.
return YourWidget(
values.elementAt(0),
values.elementAt(1),
values.elementAt(2),
.
.
values.elementAt(N),
child: child // Optional child.
);
},
child: YourOptionalChildWidget(),
)
Instead of messing with the builder simply wrap the listenables using a dummy Valuelistenable and the Listenable.merge
This ends up with minor overhead but far simpler code.
Please keep in mind that since it doesn't check the value of each listenable it might be slightly less inefficient if the listeners are being called with same value. Normally the builder would have ignored the notification because the value would be the same.
You could easily keep track of each individual value and compare before toggling the val, but i haven't seen any real performance issue so i didn't bother for now.
Using this code the 2nd argument of the builder will toggle between true/false to tell the ValueListenableBuilder that there has been a change. So simply ignore it.
You can always perform your functionality from your passed listenables
import 'package:flutter/foundation.dart';
class ValuesNotifier implements ValueListenable<bool> {
final List<ValueListenable> valueListenables;
late final Listenable listenable;
bool val = false;
ValuesNotifier(this.valueListenables) {
listenable = Listenable.merge(valueListenables);
listenable.addListener(onNotified);
}
#override
void addListener(VoidCallback listener) {
listenable.addListener(listener);
}
#override
void removeListener(VoidCallback listener) {
listenable.removeListener(listener);
}
#override
bool get value => val;
void onNotified() {
val = !val;
}
}
Example usage
ValueListenableBuilder(
valueListenable: ValuesNotifier([
valueNotifier1,
valueNotifier2,
]),
builder: (_, __, ___) => Visibility(
visible: valueNotifier1.value,
child: Text(valueNotifier2.value),
))
Multiple values can be listened, if those are encapsulated as a class, So you could listen to the data type of the class and access the members inside it, so any changes made to it are reflected to the listener,
A "Flutter-like" solution is to copy the existing ValueListenableBuilder code and add support for another listenable param. Do this for as many ValueListenableBuilders as needed (i.e. ValueListenableBuilder3, ValueListenableBuilder4...).
For more than 3 ValueListenableBuilders, it might be easier to use a ChangeNotifier instead.
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
typedef ValueWidgetBuilder2<T, T2> = Widget Function(
BuildContext context, T value1, T2 value2, Widget? child);
class ValueListenableBuilder2<T, T2> extends StatefulWidget {
const ValueListenableBuilder2({
Key? key,
required this.valueListenable1,
required this.valueListenable2,
required this.builder,
this.child,
}) : super(key: key);
final ValueListenable<T> valueListenable1;
final ValueListenable<T2> valueListenable2;
final ValueWidgetBuilder2<T, T2> builder;
final Widget? child;
#override
State<StatefulWidget> createState() => _ValueListenableBuilder2State<T, T2>();
}
class _ValueListenableBuilder2State<T, T2>
extends State<ValueListenableBuilder2<T, T2>> {
late T value1;
late T2 value2;
#override
void initState() {
super.initState();
value1 = widget.valueListenable1.value;
value2 = widget.valueListenable2.value;
widget.valueListenable1.addListener(_valueChanged1);
widget.valueListenable2.addListener(_valueChanged2);
}
#override
void didUpdateWidget(ValueListenableBuilder2<T, T2> oldWidget) {
if (oldWidget.valueListenable1 != widget.valueListenable1 &&
oldWidget.valueListenable2 != widget.valueListenable2) {
oldWidget.valueListenable1.removeListener(_valueChanged1);
value1 = widget.valueListenable1.value;
widget.valueListenable1.addListener(_valueChanged1);
oldWidget.valueListenable2.removeListener(_valueChanged2);
value2 = widget.valueListenable2.value;
widget.valueListenable2.addListener(_valueChanged2);
}
super.didUpdateWidget(oldWidget);
}
#override
void dispose() {
widget.valueListenable1.removeListener(_valueChanged1);
widget.valueListenable2.removeListener(_valueChanged2);
super.dispose();
}
void _valueChanged1() {
setState(() {
value1 = widget.valueListenable1.value;
});
}
void _valueChanged2() {
setState(() {
value2 = widget.valueListenable2.value;
});
}
#override
Widget build(BuildContext context) {
return widget.builder(context, value1, value2, widget.child);
}
}
Ive done recursive function to make behaviour you want, I am not sure its the best answer but you will probably like it.
class _MultiValueListenableBuilder extends StatelessWidget {
final List<ValueNotifier> valueListenables;
final Widget Function(BuildContext, List values) builder;
const _MultiValueListenableBuilder({Key? key,
required this.valueListenables,
required this.builder,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return _create(valueListenables, context);
}
Widget _create(List<ValueNotifier> notifiers, BuildContext context,) {
if(notifiers.isEmpty) {
return builder(context, valueListenables.map((e) => e.value).toList());
}
final copy = [...notifiers];
final notifier = copy.removeAt(0);
return ValueListenableBuilder(
valueListenable: notifier,
builder: (_, __, ___) => _create(copy, _),
child: _create(copy, context),
);
}
}
I also created test widget to check if everything works as it is supposed
class _MultiTest extends StatefulWidget {
const _MultiTest({Key? key}) : super(key: key);
#override
_MultiTestState createState() => _MultiTestState();
}
class _MultiTestState extends State<_MultiTest> {
final notifier1 = ValueNotifier('otherwise');
final notifier2 = ValueNotifier('look');
#override
Widget build(BuildContext context) {
return Column(
children: [
_MultiValueListenableBuilder(
valueListenables: [notifier1, notifier2],
builder: (_, values) => Text(values.fold<String>('', (pv, e) => pv + ' ' + e), style: const TextStyle(color: Colors.black, fontSize: 20),),
),
MyTextButton(
title: 'do',
onPressed: () {
notifier1.value = notifier1.value + 'otherwise';
notifier2.value = notifier2.value + 'look';
},
)
],
);
}
}
Feel free to ask any question I probably missed something up.
I was trying to achieve similar goals as in this answer i.e. without nesting BuildContext
I have left the class as abstract in my case, but, it can receive a callback function as argument as an alternative.
abstract class AllMultiValueListeners extends StatelessWidget {
/// List of [ValueListenable]s to listen to.
final List<ValueListenable> valueListenables;
const AllMultiValueListeners({Key key, #required this.valueListenables})
: super(key: key);
ValueListenableBuilder getListenableBuilder(int index) {
List<dynamic> values = [];
return ValueListenableBuilder(
valueListenable: valueListenables.elementAt(index),
builder: (BuildContext context, value, Widget child) {
for (int i = 0; i < valueListenables.length; i++) {
if (values.length <= i) {
values.add(valueListenables.elementAt(i).value);
} else {
values[i] = valueListenables.elementAt(i).value;
}
}
if (index == valueListenables.length - 1) {
return valuesChangedBuilder(context, values, child);
} else {
return getListenableBuilder(index + 1);
}
});
}
#override
Widget build(BuildContext context) {
return getListenableBuilder(0);
}
/// override this method to rebuild something when there the one or more values have changed
Widget valuesChangedBuilder(
BuildContext ctx,
List<dynamic> values,
Widget child,
);
}