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(...)
Related
I've got a List<Data> which is diplayed in a ListView that uses Riverpod to watch any changes to the list. When I add or remove an item from that list, the ListView rebuilds as intended, but it appears like every ListViewItem and its descending widgets are rebuild - even though they show the same content as before. Here's a simplified version of my code:
class MyApp extends ConsumerWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final listLength = ref.watch(dataLengthProvider);
return MaterialApp(
home: Scaffold(
body: Column(
children: [
ElevatedButton(
child: const Icon(Icons.add),
onPressed: () => ref.read(dataListProvider.notifier).add(),
),
Expanded(
child: ListView.builder(
itemCount: listLength,
itemBuilder: (context, index) {
return MyListItem(index);
},
),
),
],
),
),
);
}
}
class MyListItem extends ConsumerWidget {
final int index;
const MyListItem(this.index, {Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final countValue =
ref.watch(dataItemProvider(index).select((dataItem) => dataItem.value));
return Text('Value: ${countValue.toString()}');
}
}
// Providers -------------------------------------------------------------------
final dataListProvider = StateNotifierProvider<DataListNotifier, List<Data>>(
(ref) => DataListNotifier());
final dataLengthProvider =
Provider<int>((ref) => ref.watch(dataListProvider).length);
final dataItemProvider = Provider.family<Data, int>(
(ref, index) => ref.watch(dataListProvider)[index]);
// Notifier --------------------------------------------------------------------
class DataListNotifier extends StateNotifier<List<Data>> {
DataListNotifier() : super([const Data(), const Data()]);
void add() {
state = [...state, const Data(value: 0)];
}
}
// Data model ------------------------------------------------------------------
#immutable
class Data {
final int value;
const Data({this.value = 0});
Data copyWith({int? newValue}) => Data(value: newValue ?? value);
}
Now my question: Is Flutter smart enough to automatically re-use those unchanged widgets?
If not, what can I do to avoid unneccessary builds?
You can check something. To do this, remake your class MyListItem in to have access to dispose():
class MyListItem extends ConsumerStatefulWidget {
final int index;
const MyListItem(
this.index, {
Key? key,
}) : super(key: key);
#override
ConsumerState createState() => _MyListItemState();
}
class _MyListItemState extends ConsumerState<MyListItem> {
#override
Widget build(BuildContext context) {
print(widget.index);
final countValue = ref.watch(
dataItemProvider(widget.index).select((dataItem) => dataItem.value));
return Text('Value: ${countValue.toString()}');
}
#override
void dispose() {
print('dispose: ${widget.index}');
super.dispose();
}
}
and add method delete() near add():
void delete() {
state.removeLast();
state = List.of(state);
}
and add button in MyApp:
ElevatedButton(
child: const Icon(Icons.delete),
onPressed: () => ref.read(dataListProvider.notifier).delete(),
),
And check this code again. There, of course, the RangeError (index) error will be raised, but this is not the point. But on the other hand, you can see that the dispose() method is not called when the element is added, which means that the object is not removed from the tree. At the same time, when the last element is removed, we can see the call to the dispose() method, but only for the last element! So you are on the right track :)
You can use the select for getting the reference of the provider for stopping unnecessary rebuilds in the list item.
https://riverpod.dev/docs/concepts/reading/#using-select-to-filter-rebuilds
I need a DropdownButton with items depending on another DropdownButton. Sounds a bit confusing but it isnt. Here is my code with comments at the important parts in order to understand my intention.
Parent
class Parent extends StatefulWidget {
const Parent({ Key? key }) : super(key: key);
#override
State<Parent> createState() => _ParentState();
}
class _ParentState extends State<Parent> {
#override
Widget build(BuildContext context) {
return SafeArea(
child: SizedBox(
width: 500,
height: 500,
child: Column(
children: const [
// Main
DropDownWidget(collection: "MainCollection",),
// Depending
DropDownWidget(collection: ""), // Collection should equals value from Main DropDownWidget
],
),
),
);
}
}
Child
class DropDownWidget extends StatefulWidget {
final String collection;
const DropDownWidget({Key? key, required this.collection}) : super(key: key);
#override
State<DropDownWidget> createState() => _DropDownWidgetState();
}
class _DropDownWidgetState extends State<DropDownWidget> {
var selectedItem;
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection(widget.collection)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.hasError) {
return const CircularProgressIndicator();
} else {
var length = snapshot.data?.docs.length;
List<DropdownMenuItem<String>> items = [];
for (int i = 0; i < length!; i++) {
DocumentSnapshot snap = snapshot.data!.docs[i];
items.add(DropdownMenuItem(
child: Text(snap.id),
value: snap.id,
));
}
return DropdownButtonFormField<String>(
onChanged: (value) {
setState(() {
selectedItem = value;
// ********************
// PASS value TO PARENT
// ********************
});
},
value: selectedItem,
items: items);
}
});
}
}
When the Main DropdownButton changes its value, it should pass that to my parent in order to change the focused collection of my depending DropdownButton. I already solved that problem by throwing all the code in one class buts that not the way I want to go.
So maybe you can help me out :)
Thanks
Create an argument ValueChanged<String> onSelectItem in your child. Call the method when the value changes.
Then in your parent, you provide a function that needs to be called when the value changes in your child.
I have a list of objects, but I want to change the state of one object to "isLoading" where it will have a different title, etc.
I'm building my list view:
#override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey,
body: Obx(() => buildListView(context)));
}
Widget buildListView(BuildContext context) {
return ListView.builder(
itemCount: controller.saveGames.length,
itemBuilder: (context, index) {
final saveGame = controller.saveGames.elementAt(index);
return saveGame.isLoading
? buildListTileIsLoading(context, saveGame)
: buildListTile(context, saveGame);
});
}
ListTile buildListTile(BuildContext context, SaveGame saveGame) {
return ListTile(
onTap: () => controller.process(saveGame)
);
}
The controller:
class SaveGameController extends GetxController {
final RxList<SaveGame> saveGames = <SaveGame>[].obs;
void process(SaveGame saveGame) {
saveGame.working = true;
update();
}
}
Where have I gone wrong here?
edits: Added more code
So despite the fact, I'm only updating one object in the list and not modifying the content of the list (adding/removing objects) I still need to call saveGames.refresh();
An oversight on my end didn't think you'd need to refresh the entire list if you're just changing the property on one of the objects.
Good to know :)
update() is used with GetBuilder()
obs() is used with obx()
you need to make a change on list to update widgets
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_navigation/get_navigation.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return GetMaterialApp(
onInit: () {
Get.lazyPut(() => SaveGameController());
},
home: const HomePage(),
);
}
}
class HomePage extends GetView<SaveGameController> {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(appBar: AppBar(), body: Obx(() => buildListView(context)));
}
Widget buildListView(BuildContext context) {
return ListView.builder(
itemCount: controller.saveGames.length,
itemBuilder: (context, index) {
final saveGame = controller.saveGames.elementAt(index);
return buildListTile(context, saveGame);
});
}
ListTile buildListTile(BuildContext context, SaveGame saveGame) {
return ListTile(
tileColor: saveGame.working ? Colors.red : Colors.yellow,
title: Text(saveGame.name),
onTap: () => controller.process(saveGame));
}
}
class SaveGameController extends GetxController {
final RxList<SaveGame> saveGames = <SaveGame>[
SaveGame(id: 0, name: 'a', working: false),
SaveGame(id: 1, name: 'b', working: false),
SaveGame(id: 2, name: 'c', working: false)
].obs;
void process(SaveGame saveGame) {
final index = saveGames.indexWhere((element) => element.id == saveGame.id);
saveGames
.replaceRange(index, index + 1, [saveGame.copyWith(working: true)]);
}
}
class SaveGame {
final int id;
final String name;
final bool working;
SaveGame({required this.id, required this.name, required this.working});
SaveGame copyWith({int? id, String? name, bool? working}) {
return SaveGame(
id: id ?? this.id,
name: name ?? this.name,
working: working ?? this.working);
}
}
I am new to flutter, so please excuse my experience.
I have 2 classes, both stateful widgets.
One class contains the tiles for a listview.
Each tile class has a checkbox with a state bool for alternating true or false.
The other class (main) contains the body for creating the listview.
What I'd like to do is retrieve the value for the checkbox in the main class, and then update a counter for how many checkbboxes from the listview tiles have been checked, once a checkbox value is updated. I am wondering what the best practices are for doing this.
Tile class
class ListTile extends StatefulWidget {
#override
_ListTileState createState() => _ListTileState();
}
class _ListTileState extends State<ListTile> {
#override
Widget build(BuildContext context) {
bool selected = false;
return Container(
child: Row(
children: [Checkbox(value: selected, onChanged: (v) {
// Do something here
})],
),
);
}
}
Main Class
class OtherClass extends StatefulWidget {
#override
_OtherClassState createState() => _OtherClassState();
}
class _OtherClassState extends State<OtherClass> {
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Text("Checkbox selected count <count here>"),
ListView.builder(itemBuilder: (context, index) {
// Do something to get the selected checkbox count from the listview
return ListTile();
}),
],
),
);
}
}
Hope this is you are waiting for
class OtherClass extends StatefulWidget {
#override
_OtherClassState createState() => _OtherClassState();
}
class _OtherClassState extends State<OtherClass> {
bool selected = false;
#override
void initState() {
super.initState();
}
var items = [
Animal("1", "Buffalo", false),
Animal("2", "Cow", false),
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("title")),
body: Container(
child: ListView.builder(
itemCount: items.length,
shrinkWrap: true,
itemBuilder: (ctx, i) {
return Row(
children: [
Text(items[i].name),
ListTile(
id: items[i].id,
index: i,
)
],
);
}),
));
}
}
ListTileClass
class ListTile extends StatefulWidget {
final String? id;
final int? index;
final bool? isSelected;
const ListTile ({Key? key, this.id, this.index, this.isSelected})
: super(key: key);
#override
_ListTileState createState() => _ListTileState();
}
class _ListTileState extends State<ListTile> {
bool? selected = false;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
width: 20,
child: Checkbox(
value: selected,
onChanged: (bool? value) {
setState(() {
selected = value;
});
}));
}
}
I'd recommend using a design pattern such as BLoC or using the Provider package. I personally use the Provider Package. There are plenty of tutorials on youtube which can help get you started.
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
],
);
},
);
}
}