I'm able to successfully animate an AnimatedList's contents in Flutter when the list data is stored in the same component that owns the list widget (i.e., there's no rebuild happening when there's changes to the list data). I run into issues when I try to get the items for the list from a ChangeNotifier using Provider and Consumer.
The component that owns the AnimatedList, let's call it ListPage, is built with a Consumer<ListItemService>. My understanding is that ListPage is then rebuilt whenever the service updates the list data and calls notifyListeners(). When that happens, I'm not sure where within ListPage I could call AnimatedListState.insertItem to animate the list, since during the build the list state is still null. The result is a list that doesn't animate its contents.
I think my question boils down to "how do I manage state for this list if the contents are fetched and updated in real time?", and ideally I'd like to understand what's going on but I'm open to suggestions on how I should change this if this isn't the best way to approach the task.
Here's some code that illustrates the problem:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<AuthService>(
create: (_) => AuthService(),
),
ChangeNotifierProxyProvider<AuthService, ListItemService>(
create: (_) => ListItemService(),
update: (_, authService, listItemService) =>
listItemService!..update(authService),
),
],
child: MaterialApp(
home: HomePage(),
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<ListItemService>(
builder: (context, listItemService, _) =>
ListPage(items: listItemService.items),
);
}
}
// Implementation details aren't really relevant, but
// this only updates if the user logs in or out.
class AuthService extends ChangeNotifier {}
class ListItemService extends ChangeNotifier {
List<Item> _items = [];
List<Item> get items => _items;
Future<void> update(AuthService authService) async {
// Method that subscribes to a Firestore snapshot
// and calls notifyListeners() after updating _items.
}
}
class Item {
Item({required this.needsUpdate, required this.content});
final String content;
bool needsUpdate;
}
class ListPage extends StatefulWidget {
const ListPage({Key? key, required this.items}) : super(key: key);
final List<Item> items;
#override
_ListPageState createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey();
late int _initialItemCount;
#override
void initState() {
_initialItemCount = widget.items.length;
super.initState();
}
void _updateList() {
for (int i = 0; i < widget.items.length; i++) {
final item = widget.items[i];
if (item.needsUpdate) {
// _listKey.currentState is null here if called
// from the build method.
_listKey.currentState?.insertItem(i);
item.needsUpdate = false;
}
}
}
#override
Widget build(BuildContext context) {
_updateList();
return AnimatedList(
key: _listKey,
initialItemCount: _initialItemCount,
itemBuilder: (context, index, animation) => SizeTransition(
sizeFactor: animation,
child: Text(widget.items[index].content),
),
);
}
}
You can use didUpdateWidget and check the difference between the old and new list. "Checking the difference" means looking at what has been removed vs added. In you case the Item widget should have something to be identified. You can use Equatable for example so that an equality between Items is an equality between their properties.
One other important aspect is that you are dealing with a list, which is mutable, but Widgets should be immutable. Therefore it is crucial that whenever you modify the list, you actually create a new one.
Here are the implementations details, the most interesting part being the comment of course (though the rendering is fun as well ;)):
import 'dart:async';
import 'dart:math';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<AuthService>(
create: (_) => AuthService(),
),
ChangeNotifierProxyProvider<AuthService, ListItemService>(
create: (_) => ListItemService(),
update: (_, authService, listItemService) => listItemService!..update(authService),
),
],
child: MaterialApp(
home: HomePage(),
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Material(
child: SafeArea(
child: Consumer<ListItemService>(
builder: (context, listItemService, _) => ListPage(
// List.from is very important because it creates a new list instead of
// giving the old one mutated
items: List.from(listItemService.items),
),
),
),
);
}
}
// Implementation details aren't really relevant, but
// this only updates if the user logs in or out.
class AuthService extends ChangeNotifier {}
class ListItemService extends ChangeNotifier {
List<Item> _items = [];
List<Item> get items => _items;
Future<void> update(AuthService authService) async {
// Every 5 seconds
Timer.periodic(Duration(seconds: 5), (timer) {
// Either create or delete an item randomly
if (Random().nextDouble() > 0.5 && _items.isNotEmpty) {
_items.removeAt(Random().nextInt(_items.length));
} else {
_items.add(
Item(
needsUpdate: true,
content: 'This is item with random number ${Random().nextInt(10000)}',
),
);
}
notifyListeners();
});
}
}
class Item extends Equatable {
Item({required this.needsUpdate, required this.content});
final String content;
bool needsUpdate;
#override
List<Object?> get props => [content]; // Not sure you want to include needsUpdate?
}
class ListPage extends StatefulWidget {
const ListPage({Key? key, required this.items}) : super(key: key);
final List<Item> items;
#override
_ListPageState createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
final _listKey = GlobalKey<AnimatedListState>();
// You can use widget if you use late
late int _initialItemCount = widget.items.length;
/// Handles any removal of [Item]
_handleRemovedItems({
required List<Item> oldItems,
required List<Item> newItems,
}) {
// If an [Item] was in the old but is not in the new, it has
// been removed
for (var i = 0; i < oldItems.length; i++) {
final _oldItem = oldItems[i];
// Here the equality checks use [content] thanks to Equatable
if (!newItems.contains(_oldItem)) {
_listKey.currentState?.removeItem(
i,
(context, animation) => SizeTransition(
sizeFactor: animation,
child: Text(oldItems[i].content),
),
);
}
}
}
/// Handles any added [Item]
_handleAddedItems({
required List<Item> oldItems,
required List<Item> newItems,
}) {
// If an [Item] is in the new but was not in the old, it has
// been added
for (var i = 0; i < newItems.length; i++) {
// Here the equality checks use [content] thanks to Equatable
if (!oldItems.contains(newItems[i])) {
_listKey.currentState?.insertItem(i);
}
}
}
// Here you can check any update
#override
void didUpdateWidget(covariant ListPage oldWidget) {
super.didUpdateWidget(oldWidget);
_handleAddedItems(oldItems: oldWidget.items, newItems: widget.items);
_handleRemovedItems(oldItems: oldWidget.items, newItems: widget.items);
}
#override
Widget build(BuildContext context) {
return AnimatedList(
key: _listKey,
initialItemCount: _initialItemCount,
itemBuilder: (context, index, animation) => SizeTransition(
sizeFactor: animation,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(widget.items[index].content),
),
),
);
}
}
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
The problem is that I would like to show a loading indicator when the user tries to fetch some data from an api. But when the user presses the button, loading indicator shows once. But I would like to show the loading indicator every time when the user tries to fetch. It works but as I say It works once. Could anyone have any idea what can cause this problem? Here's the minimal code to reproduce the issue:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => HomeCubit()),
],
child: const MaterialApp(
title: 'Flutter Bloc Demo',
home: HomeView(),
),
);
}
}
class HomeView extends BaseView<HomeCubit, HomeState> {
const HomeView({Key? key}) : super(key: key);
#override
Widget builder(HomeCubit cubit, HomeState state) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(state.count.toString()),
ElevatedButton(
onPressed: cubit.increment,
child: const Text('Increase'),
),
],
),
);
}
}
class HomeState extends BaseState {
final int count;
HomeState({required this.count});
HomeState copyWith({
int? count,
}) {
return HomeState(
count: count ?? this.count,
);
}
}
class HomeCubit extends BaseCubit<HomeState> {
HomeCubit() : super(HomeState(count: 0));
void increment() {
flow(() async {
await Future.delayed(const Duration(seconds: 1));
emit(state.copyWith(count: state.count + 1));
});
}
}
#immutable
abstract class BaseView<C extends StateStreamable<S>, S extends BaseState>
extends StatelessWidget {
const BaseView({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return BaseCubit(context.read<S>());
},
child: Scaffold(
body: BlocBuilder<C, S>(
builder: (context, state) {
final cubit = context.read<C>();
if (state.loadingState == LoadingState.loading) {
return loadingWidget;
}
return builder.call(cubit, state);
},
),
),
);
}
Widget builder(C cubit, S state);
Widget get loadingWidget => const Center(
child: CircularProgressIndicator(),
);
}
enum LoadingState { initial, loading, loaded }
class BaseState {
LoadingState loadingState;
BaseState({
this.loadingState = LoadingState.initial,
});
}
class BaseCubit<S extends BaseState> extends Cubit<S> {
BaseCubit(S state) : super(state);
Future<void> flow(Future<void> Function() function) async {
state.loadingState = LoadingState.loading;
emit(state);
await function();
state.loadingState = LoadingState.loaded;
emit(state);
}
}
Is it overengineering? I don't think you are duplicating much code if you just use BlocBuilder instead of some base class.
If bloc already exist you should provide it by BlocProvider.value instead of BlocProvider(create: read())
You should use context.watch instead of context.read to get a new value every time the state changes. context.read receives state only once.
It's overengineering, please take a look at https://bloclibrary.dev/#/coreconcepts. There are enough tutorials to catch the basic idea.
Then try to use bloc + freezed. Here is an example https://dev.to/ptrbrynt/why-bloc-freezed-is-a-match-made-in-heaven-29ai
i am clearing a hive box and updating it in the very next line.
The update can be seen in the same file code (through debugging).
But somehow for the other stateless widget/class/file box is empty.
boxT.clear() ;
setState(() {
boxT.addAll({
[count2, totalEntries]
});});
Reason for clearing the box on everytime a specific button is pressed: I am adding a map. addAll() simply creates another entry. i dont want it. i have also tried put and putall but they are only showing the value and not the key.
That's because Hive is NOT a state management tool, if you add/remove something to/from your box, you need to let your state know. You should have a global state that listens to changes from that Box.
Example with a ValueNotifier.
class ItemsNotifier extends ValueNotifier<List<Item>> {
ItemsNotifier() : super(getItemsFromBox());
List<Item> _items = getItemsFromBox();
#override
List<Item> get value => _items;
void addItem(Item item) {
addItemToBox(item);
_items = getItemsFromBox();
notifyListeners();
}
Future<void> deleteAll() async {
await clearBox();
_items = getItemsFromBox();
notifyListeners();
}
}
List<Item> getItemsFromBox() {
return Hive.box<Item>('items').values.toList();
}
Future<void> addItemToBox(Item item) async {
await Hive.box<Item>('items').add(item);
}
Future<void> clearBox() async {
await Hive.box<Item>('items').clear();
}
class MainPage extends StatefulWidget {
const MainPage({Key? key}) : super(key: key);
#override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
late final ItemsNotifier _notifier;
#override
void initState() {
_notifier = ItemsNotifier();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ExampleWidget(notifier: _notifier),
floatingActionButton: FloatingActionButton(
onPressed: _notifier.deleteAll,
child: const Icon(Icons.delete),
),
);
}
#override
void dispose() {
_notifier.dispose();
super.dispose();
}
}
class ExampleWidget extends StatelessWidget {
const ExampleWidget({required this.notifier, Key? key}) : super(key: key);
final ItemsNotifier notifier;
#override
Widget build(BuildContext context) {
return ValueListenableBuilder<List<Item>>(
valueListenable: notifier,
builder: (context, items, _) {
return ListView.builder(
itemBuilder: (context, index) => ListTile(
title: Text(items[index].title),
),
itemCount: items.length,
);
},
);
}
}
Note that if you have multiple pages using the data from this box, I would recommend using something like Provider and having a ChangeNotifier instead of the ValueNotifier or using flutter_bloc and having a Repository that handles communication with Hive.
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 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
],
);
},
);
}
}