I have a view which switches between a ListView and a ReorderableListView.
Widget _buildList(
BuildContext context,
List<ExerciseTemplate> exerciseTemplates,
EditWorkoutModel dao,
) {
if (_isInEditingMode) {
return ReorderableListView(
key: ObjectKey('reordeableListView'),
onReorder: ((oldIndex, newIndex) {
dao.reorderIndexes(
oldIndex,
(oldIndex < newIndex) ? newIndex - 1 : newIndex,
);
}),
padding: const EdgeInsets.only(bottom: 120),
children: [
for (var exerciseTemplate in exerciseTemplates)
Provider(
key: ObjectKey('${exerciseTemplate.id}_compactExerciseTemplateRow_provider'),
create: (context) => EditExerciseModel(exerciseTemplate),
child: CompactExerciseTemplateRow(
key: ObjectKey('${exerciseTemplate.id}_compactExerciseTemplateRow'),
),
),
],
);
} else {
return ListView.builder(
key: ObjectKey('listView'),
itemCount: exerciseTemplates.length,
padding: const EdgeInsets.only(bottom: 120),
itemBuilder: (BuildContext context, int index) {
final exerciseTemplate = exerciseTemplates[index];
return Provider(
// Key is needed here to properly handle deleted rows in the ui.
// Without the key, deleted rows are being shown.
key: ObjectKey(
'${exerciseTemplate.id}_exerciseTemplateRow_provider'),
create: (context) => EditExerciseModel(exerciseTemplate),
child: ExerciseTemplateRow(
key: ObjectKey('${exerciseTemplate.id}_exerciseTemplateRow'),
onDelete: () async {
await dao.deleteExercise(exerciseTemplate);
return true;
},
),
);
},
);
}
}
Both lists show the same data, but tapping a button, switches to a ReorderableListView which shows the data with different widgets. Tapping the button again switches back to the ListView.
However, switching forth and back results that I am not able to interact with elements within the row of the ListView. This issue appeared after I added a globalKey for each element in the ListView. I need this key, to properly handle deleting rows, so I can not just remove the key again.
How can I make it work, that I can interact with widgets within the row after I switched to the ReorderableListView and back to the ListView?
Copied from Provider document:
https://pub.dev/packages/provider
DON'T create your object from variables that can change over time.
In such a situation, your object would never update when the value changes.
int count;
Provider(
create: (_) => MyModel(count),
child: ...
)
If you want to pass variables that can change over time to your object, consider using ProxyProvider:
int count;
ProxyProvider0(
update: (_, __) => MyModel(count),
child: ...
)
It's ok to use Global key and switch between ListView and ReorderableListView, see example below:
https://dartpad.dev/?id=fd39a89b67448d86e682dd2c5ec77453
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool reOrder = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(reOrder ? "ReoderableListView" : "ListView"),
),
floatingActionButton: FloatingActionButton(onPressed: () {
setState(() {
reOrder = !reOrder;
});
}),
body: MyListView(reOrder));
}
}
final data = List.generate(10, (index) => {"title": 'item $index', "value": false});
class MyListView extends StatefulWidget {
final bool reOrder;
const MyListView(this.reOrder, {super.key});
#override
State<MyListView> createState() => _MyListViewState();
}
class _MyListViewState extends State<MyListView> {
#override
Widget build(BuildContext context) {
if (widget.reOrder) {
return ReorderableListView(
key: const ObjectKey('reordeableListView'),
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final item = data.removeAt(oldIndex);
data.insert(newIndex, item);
});
},
children: [
for (var item in data)
ListTile(
key: ObjectKey('${item["title"]}_compactExerciseTemplateRow_provider'),
title: Text(item["title"] as String),
trailing: Text((item["value"] as bool).toString()),
),
],
);
} else {
return ListView.builder(
key: const ObjectKey('listView'),
itemCount: data.length,
padding: const EdgeInsets.only(bottom: 120),
itemBuilder: (BuildContext context, int index) {
return CheckboxListTile(
key: ObjectKey('${data[index]["title"]}_exerciseTemplateRow_provider'),
title: Text(data[index]["title"] as String),
value: (data[index]["value"] as bool),
onChanged: (bool? value) {
setState(() => data[index]["value"] = value!);
},
);
},
);
}
}
}
So the issue was that I was using ObjectKey instead of ValueKey.
The difference between those two is that ObjectKey checks if the identity (the instance) is the same. ValueKey checks the underlying value with the == operator.
My guess here is that by using ObjectKey in my case, flutter is not able to properly replace the old widget with the new one, since the new widget always have a different key. By using ValueKey flutter can distinguish between old and new widgets. Widgets will be in my case replaced after I switch between the lists, because the row widget won't be visible and therefor disposed.
Because the widgets were not properly replaced, somehow the old widgets are still being rendered, but all gesture listeners were already disposed. Therefor no interaction was possible anymore.
These are just my assumption, let me know if I am completely wrong here.
I have a valueNotifier that generates a list of events and takes a random string every 5 seconds and sends it to the screen. It lies in inheritedWidget. How can I display in the ListView the event that came with the valueNotifier? What is the correct way to print the answer?
My code:
class EventList extends StatelessWidget {
const EventList({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return EventInherited(
child: EventListScreen(),
);
}
}
class EventListScreen extends StatefulWidget {
const EventListScreen({Key? key}) : super(key: key);
#override
State<EventListScreen> createState() => _EventListScreenState();
}
class _EventListScreenState extends State<EventListScreen> {
#override
Widget build(BuildContext context) {
final eventNotifier = EventInherited.of(context).eventNotifier;
return Scaffold(
appBar: AppBar(
title: const Text('Event List'),
centerTitle: true,
),
body: Container(
padding: const EdgeInsets.all(30),
child: ValueListenableBuilder(
valueListenable: eventNotifier,
builder: (BuildContext context, List<String> value, Widget? child) {
return ListView(
children: [
],
);
},
),
),
);
}
}
class EventNotifier extends ValueNotifier<List<String>> {
EventNotifier(List<String> value) : super(value);
final List<String> events = ['add', 'delete', 'edit'];
final stream = Stream.periodic(const Duration(seconds: 5));
late final streamSub = stream.listen((event) {
value.add(
events[Random().nextInt(4)],
);
});
}
class EventInherited extends InheritedWidget {
final EventNotifier eventNotifier = EventNotifier([]);
EventInherited({required Widget child}) : super(child: child);
static EventInherited of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType()!;
}
#override
bool updateShouldNotify(EventInherited oldWidget) {
return oldWidget.eventNotifier.streamSub != eventNotifier.streamSub;
}
}
If you have correct value, you can return listview like this:
return ListView.builder(
itemCount: value.length,
itemBuilder: (context, index) {
return Text(value[index]);
},
);
After having a quick look at ValueNotifier,
It says the following:
When the value is replaced with something that is not equal to the old value as evaluated by the equality operator ==, this class notifies its listeners.
In your case, the value is an array. By adding items to the array, it wont recognise a change.
Also see other Stacko post.
Try something like:
value = [...value].add(...)
I want to display the JSON data inside ListView.builder received from the previous screen. Below is the sample code till now that I have tried.
FirstPage.dart
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) => MyOrderDetails(
storeItems: order.inDetail!.menuItems!
)));
This is the sample json i am passing to Next Screen
{
"item_name": "Test",
"quantity": 1,
"subtotal": "434.78"
}
MyOrderDetail.dart
class MyOrderDetails extends StatefulWidget {
final List storeItems;
const MyOrderDetails(
{Key? key,
required this.storeItems})
: super(key: key);
#override
State<MyOrderDetails> createState() => _MyOrderDetailsState();
}
class _MyOrderDetailsState extends State<MyOrderDetails> {
#override
Widget build(BuildContext context) {
var height = MediaQuery.of(context).size.height;
var width = MediaQuery.of(context).size.width;
var lang = translator.activeLanguageCode;
return Scaffold(
appBar: AppBar(
elevation: 0,
),
body: ListView(
children: [
ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemCount: widget.storeItems.length,
itemBuilder: (BuildContext context, int index) {
return Text(widget.storeItems[index]['item_name']); // Getting error here
}),
],
),
);
}
}
Your variable should be like that. List is not enough by itself you should declare which class is for that list.
final List<YourDataClass> storeItems;
const MyOrderDetails(
{Key? key,
required this.storeItems})
: super(key: key);
I have the following issue with my 'workout' App using multiple workoutlists with various workoutitems:
I select a workoutlist with 12 workoutitems.
The 'activity' screen with the AnimatedList is shown.
Afterwards, I select a different workoutlist with 80 workoutitems.
The AnimatedList is now showing the new workoutlist but only the first 12 workoutitems.
Why?
I thought that the AnimatedList inside the build Widget is rebuild every time (I am not using GlobalKey).
class WorkoutListView extends StatelessWidget {
const WorkoutListView({this.filename});
final String filename;
#override
Widget build(BuildContext context) {
return Selector<WorkoutListModel, List<Workout>>(
selector: (_, model) => model.filterWorkouts(filename),
builder: (context, workouts, _) {
return AnimatedWorkoutList(
list: workouts,
);
},
);
}
}
class AnimatedWorkoutList extends StatefulWidget {
const AnimatedWorkoutList({
Key key,
#required List<Workout> list,
}) : _list = list,
super(key: key);
final List<Workout> _list;
#override
_AnimatedWorkoutListState createState() => _AnimatedWorkoutListState();
}
class _AnimatedWorkoutListState extends State<AnimatedWorkoutList> {
#override
Widget build(BuildContext context) {
return AnimatedList(
initialItemCount: widget._list.length,
itemBuilder: (context, index, animation) {
final workout = widget._list[index];
return Column(
children: [
// Using AnimatedList.of(context).removeItem() for list manipulation
],
);
},
);
}
}
try this:
class AnimatedWorkoutList extends StatefulWidget {
const AnimatedWorkoutList({
#required List<Workout> list,
});
final List<Workout> list;
#override
_AnimatedWorkoutListState createState() => _AnimatedWorkoutListState();
}
class _AnimatedWorkoutListState extends State<AnimatedWorkoutList> {
#override
Widget build(BuildContext context) {
return AnimatedList(
initialItemCount: widget.list.length,
itemBuilder: (context, index, animation) {
final workout = widget.list[index];
return Column(
children: [
// Using AnimatedList.of(context).removeItem() for list manipulation
],
);
},
);
}
}
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,
);
}