In my Application, I have an AnimatedList in one of my pages. Items are being added to the list by pressing a button. I animate the scrollView when an Item is being inserted to the list. Sometimes the list grows How can I find out than an Item is still visible on the screen at a moment when the list gets too long?
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:async';
class AnimatedListSample extends StatefulWidget {
#override
_AnimatedListSampleState createState() => _AnimatedListSampleState();
}
class _AnimatedListSampleState extends State<AnimatedListSample> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
ListModel<int> _list;
final ScrollController _controller = ScrollController();
int _nextItem;
#override
void initState() {
super.initState();
_list = ListModel<int>(
listKey: _listKey,
initialItems: <int>[0, 1, 2],
);
_nextItem = 3;
}
Widget _buildItem(
BuildContext context, int index, Animation<double> animation) {
return CardItem(
animation: animation,
item: _list[index],
);
}
void _insert() {
_list.insert(_list.length, _list.length + 1);
Timer(
Duration(milliseconds: 300),
() => _controller.animateTo(
_controller.position.maxScrollExtent,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 300),
));
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('AnimatedList'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.add_circle),
onPressed: _insert,
)
],
),
body: AnimatedList(
controller: _controller,
key: _listKey,
initialItemCount: _list.length,
itemBuilder: _buildItem,
),
),
);
}
}
class ListModel<E> {
ListModel({
#required this.listKey,
Iterable<E> initialItems,
}) : assert(listKey != null),
_items = List<E>.from(initialItems ?? <E>[]);
final GlobalKey<AnimatedListState> listKey;
final List<E> _items;
AnimatedListState get _animatedList => listKey.currentState;
void insert(int index, E item) {
_items.insert(index, item);
_animatedList.insertItem(index);
}
int get length => _items.length;
E operator [](int index) => _items[index];
int indexOf(E item) => _items.indexOf(item);
}
class CardItem extends StatelessWidget {
const CardItem({Key key, #required this.animation, #required this.item})
: assert(animation != null),
assert(item != null && item >= 0),
super(key: key);
final Animation<double> animation;
final int item;
#override
Widget build(BuildContext context) {
TextStyle textStyle = Theme.of(context).textTheme.headline4;
return SizeTransition(
axis: Axis.vertical,
sizeFactor: animation,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
child: SizedBox(
height: 80,
child: Card(
color: Colors.primaries[item % Colors.primaries.length],
child: Center(
child: Text('Item $item', style: textStyle),
),
),
),
),
);
}
}
You can use the visibility_detector package, which fires a callback whenever the visibility of a widget changes. So you can wrap each of the widgets in your list with the VisibilityDetector widget and have the callback change the state as the visibility changes. You can then handle visibility changes however you need based on your application.
Related
I'm animating the removal and addition of items to my list using an AnimatedList whose data is from a firestore stream.
I use riverpod to manage my state and noticed my List state rebuilds whenever I navigator to another screen. It also rebuilds on hot reload.
StateNotifier class for Nav bar
class NavigationState extends StateNotifier<Widget> {
NavigationState() : super(_homeScreen);
static const HomeScreen _homeScreen = HomeScreen();
static const SettingScreen _settingScreen = SettingScreen();
static Screens _page = Screens.home;
Screens get page => _page;
void selectPage(Screens page) {
switch (page) {
case Screens.home:
_page = Screens.home;
state = _homeScreen;
break;
case Screens.settings:
_page = Screens.settings;
state = _settingScreen;
}
}
}
Nav bar class
class CustomNav extends ConsumerWidget {
const CustomNav({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final navIndex = ref.watch(navigationStateProvider.notifier);
return AnimatedContainer(
key: UniqueKey(),
curve: Curves.ease,
duration: const Duration(milliseconds: 1000),
padding: const EdgeInsets.only(top: 5),
height: 58,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NavigationIcon(
icon: "assets/svg/home.svg",
color: navIndex.page == Screens.home
? Colors.green
: const Color.fromRGBO(133, 144, 132, 1),
label: "Home",
onPressed: () {
ref.read(navigationStateProvider.notifier).selectPage(
Screens.home);
},
),
NavigationIcon(
key: UniqueKey(),
icon: "assets/svg/setting.svg",
color: navIndex.page == Screens.settings
? Colors.green
: const Color.fromRGBO(133, 144, 132, 1),
label: "Settings",
onPressed: () {
ref
.read(navigationStateProvider.notifier)
.selectPage(Screens.settings);
}),
],
),
);
}
}
Home Screen
class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
ConsumerState<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends ConsumerState<HomeScreen>
with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
late final TabController _tabController;
final List<UserProperties> _firebaseList = [];
final pref = Preferences();
final GlobalKey<AnimatedListState> _key = GlobalKey();
#override
void initState() {
_tabController = TabController(length: 2, vsync: this);
super.initState();
updateKeepAlive();
}
void _addItem(List<QueryDocumentSnapshot> data) {
for (var element in data) {
final userProperties =
UserProperites.fromJson(element.data() as Map<String, dynamic>);
if (!_firebaseList.contains(userProperties)) {
_firebaseList.add(userProperties);
final itemIndex = data.indexOf(element);
_key.currentState
?.insertItem(itemIndex, duration: const Duration(seconds: 5));
}
}
}
#override
void dispose() {
super.dispose();
_tabController.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
final selectedIndex = ref.watch(tabIndexProvider.notifier);
final checkBoxState = ref.watch(mainCheckBoxProvider.state);
_tabController.addListener(() {
checkBoxState.update((state) => false);
selectedIndex.state = _tabController.index;
});
final userId = pref.pref.getString("userId");
return Scaffold(
key: UniqueKey(),
body: Column(
key: UniqueKey(),
children: [
const SizedBox(
height: 15,
),
Expanded(
child: StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
key: UniqueKey(),
stream: FirebaseFirestore.instance
.collection("users")
.doc(userId!)
.collection("properties")
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Container();
}
var data = snapshot.data!.docs;
_addItem(data);
return AnimatedList(
padding: const EdgeInsets.only(bottom: 100),
key: _key,
initialItemCount: _firebaseList.length,
itemBuilder: (context, index, animation) {
final userPro = _firebaseList.elementAt(index);
return Padding(
padding: const EdgeInsets.all(2.0),
child: GestureDetector(
onTap: () => Navigator.pushNamed(
context, "details"),
child: PropertyTile(
key: ObjectKey(index),
image: user.image,
name: user.name),
),
),
);
});
}),
),
],
),
);
}
#override
bool get wantKeepAlive => true;
}
I've been on this issue for a while, that's part of the reason UniqueKey() is littered everywhere.
I thought of using Provider.select, but I don't really know what to select since I'm using an enum.
Prior to AnimatedList, everything works fine, and no unnecessary rebuilds.
I want to make the title fade and the title should disappear when the previous screen reaches the mid Screen. The next title should fade till midway and the and a new title for the next screen should fade in. How can I add this animation I am using AnimatedOpacity and AnimatedSwitcher but it doesn't work. The code files are:
The Data Class
import 'package:flutter/material.dart';
class DataModel {
List<String> titles = ['Red1', 'Blue2', 'Green3'];
List<String> content = ['Red1', 'Blue2', 'Green3'];
}
The Main Screen
import 'package:flutter/material.dart';
import 'package:ui_design/model/data.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 100));
animation = CurvedAnimation(parent: parent, curve: curve)
}
//
DataModel dataModel = DataModel();
int currentPageIndex = 0;
bool isPageChanged = true;
//
#override
Widget build(BuildContext context) {
var appBarTitle = dataModel.titles[currentPageIndex];
return Scaffold(
appBar: AppBar(
leading: const Icon(Icons.menu),
title: Text(
appBarTitle,
),
),
body: PageView.builder(
scrollDirection: Axis.horizontal,
itemCount: dataModel.content.length,
onPageChanged: (value) => setState(() {
appBarTitle = dataModel.titles[currentPageIndex];
}),
itemBuilder: (BuildContext context, int index) {
currentPageIndex = index;
print('Current page index $currentPageIndex');
return Container(
alignment: Alignment.center,
child: Text(
dataModel.content[index],
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
);
},
),
);
}
}
// AnimatedSwitcher(
// duration: const Duration(milliseconds: 500),
// transitionBuilder: (Widget child, Animation<double> animation) {
// return FadeTransition(child: child, opacity: animation);
// },
// child: Image.asset(
// imageList[currentIndex.toInt()],
// key: ValueKey<int>(currentIndex),
// ),
You can try widget, I am using PageController and Opcaity on title.
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
DataModel dataModel = DataModel();
int currentPageIndex = 0;
double opacity = 1;
late final PageController pageController = PageController(
initialPage: 0,
)..addListener(() {
debugPrint(pageController.page.toString());
opacity =
double.tryParse(pageController.page.toString().substring(1)) ?? 1;
if (opacity < .5) {
opacity = 1 - opacity * 2;
}
setState(() {});
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: const Icon(Icons.menu),
title: Opacity(
opacity: opacity,
child: Text(
dataModel.titles[currentPageIndex],
),
),
),
body: PageView.builder(
controller: pageController,
scrollDirection: Axis.horizontal,
itemCount: dataModel.content.length,
onPageChanged: (value) {
debugPrint("on page changed: $value");
currentPageIndex = value;
setState(() {});
},
itemBuilder: (BuildContext context, int index) {
return Container(
alignment: Alignment.center,
child: Text(
dataModel.content[index],
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
);
},
),
);
}
}
i have a Widget that has an AppBar with a progress bar, and a PageView with 4 pages, when moving between pages i am increasing / decreasing the progress bar.
I'm trying to do all the logic in my ViewModel.
This is my ViewModel (omitted non relevant stuff):
class RegisterViewModel extends BaseViewModel with RegisterViewModelInputs, RegisterViewModelOutputs {
final StreamController _sexStreamController = StreamController<int>.broadcast();
final StreamController _progressBarController = StreamController<double>.broadcast();
final StreamController _currentIndexController = StreamController<int>.broadcast();
final StreamController _isBackEnabled = StreamController<bool>.broadcast();
double _progress = 0.25;
int _index = 0;
#override
setCurrentIndex(int index) {
currentIndex.add(index);
}
#override
increaseProgress() {
if (_progress <= 1.0) {
_progress += 0.25;
progress.add(_progress);
}
}
#override
decreaseProgress() {
if (_progress > 0) {
_progress -= 0.25;
progress.add(_progress);
}
}
#override
setIsBackEnabled(int index) {
_isBackEnabled.add(index > 0 ? true : false);
}
#override
nextPage() {
if (_index < 4) {
_index++;
increaseProgress();
setCurrentIndex(_index);
}
}
#override
previousPage() {
if (_index > 0) {
_index--;
decreaseProgress();
setCurrentIndex(_index);
}
}
#override
Sink get currentIndex => _currentIndexController.sink;
#override
Sink get progress => _progressBarController.sink;
#override
Sink get isBackEnabled => _isBackEnabled.sink;
#override
Stream<int> get outputCurrentIndex => _currentIndexController.stream.map((currentIndex) => currentIndex);
#override
Stream<double> get outputProgress => _progressBarController.stream.map((progress) => progress);
#override
Stream<bool> get outputIsBackEnabled => outputIsBackEnabled.map((isEnabled) => isEnabled);
}
And here is my View:
class RegisterView extends StatefulWidget {
const RegisterView({Key? key}) : super(key: key);
#override
_RegisterViewState createState() => _RegisterViewState();
}
class _RegisterViewState extends State<RegisterView> {
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
final PageController _pageController = PageController(initialPage: 0);
final FixedExtentScrollController _weightScrollController = FixedExtentScrollController(initialItem: 80);
final FixedExtentScrollController _ageScrollController = FixedExtentScrollController(initialItem: 13);
final FixedExtentScrollController _heightScrollController = FixedExtentScrollController(initialItem: 13);
#override
void initState() {
_bind();
super.initState();
}
#override
void dispose() {
_viewModel.dispose();
super.dispose();
}
_bind() {
_viewModel.start();
}
#override
Widget build(BuildContext context) {
_viewModel.outputCurrentIndex.listen((index) {
_pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
List<Widget> pagesList = [
SexPage(
onConfirm: (sex) {
_viewModel.setSex(sex);
_viewModel.nextPage();
},
),
AgePage(
scrollController: _ageScrollController,
),
WeightPage(scrollController: _weightScrollController),
HeightPage(scrollController: _heightScrollController),
];
return Scaffold(
backgroundColor: ColorManager.backgroundColor,
appBar: AppBar(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: ColorManager.backgroundColor,
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
),
centerTitle: true,
title: AppBarWidget(_pageController),
elevation: AppSize.s0,
),
body: PageView(
reverse: true,
controller: _pageController,
physics: NeverScrollableScrollPhysics(),
children: [...pagesList],
),
);
}
}
class AppBarWidget extends StatelessWidget {
final PageController pageController;
final RegisterViewModel _viewModel = getIt<RegisterViewModel>();
AppBarWidget(
this.pageController, {
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
_viewModel.outputCurrentIndex.listen((index) {
pageController.animateToPage(index, duration: const Duration(milliseconds: 1000), curve: Curves.ease);
});
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
flex: 1,
child: InkWell(
child: Text(
AppStrings.skip,
style: Theme.of(context).textTheme.labelMedium,
),
onTap: () => _viewModel.nextPage(),
),
),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppPadding.p60),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(pi),
child: StreamBuilder<double>(
stream: _viewModel.outputProgress,
builder: (context, snapshot) {
return Progresso(
progress: snapshot.data ?? 0,
progressStrokeCap: StrokeCap.round,
backgroundStrokeCap: StrokeCap.round,
progressColor: ColorManager.primary,
backgroundColor: ColorManager.progressBarBackgroundGrey,
progressStrokeWidth: 10.0,
backgroundStrokeWidth: 10.0,
);
}),
),
),
),
StreamBuilder<int>(
stream: _viewModel.outputCurrentIndex,
builder: (context, snapshot) {
return Expanded(
flex: 1,
child: (snapshot.data ?? 0) > 0
? InkWell(
child: Row(
children: [
Text(
AppStrings.back,
style: Theme.of(context).textTheme.labelMedium,
),
Icon(
Icons.arrow_forward_ios,
color: ColorManager.subtitleGrey,
),
],
),
onTap: () => _viewModel.previousPage(),
)
: Container(),
);
}),
],
);
}
}
When i'm calling _viewModel.previousPage() & _viewModel.previousPage()` from the AppBarWidget, the progress bar view is updated, and there is a scroll animation to the next page.
But for some reason if the onConfirm callback:
onConfirm: (sex) {
_viewModel.setSex(sex);
_viewModel.nextPage();
}
is called from within SexPage, the scroll animation is working, but the progress bar view and the isBackEnabled is not updating.
I have checked and a new value is being added to the _progressBarController sink, but for some reason the StreamBuilder does not receive it? same for the isBackEnabled stream..
What am i doing wrong?
And another question i have is where should I listen to the outputCurrentIndex stream, and call _pageController.animateToPage()?
Apparently i had an issue with my Dependency Injection.
I'm using get_it and i used registerFactory, instead of registerLazySingleton.
Which probably made me have 2 separate ViewModels in each widget.
I have model which implements ChangeNotifier
class DataModel with ChangeNotifier{
List<Data> data = List<Data>();
void addData(Data data){
data.add(data);
notifyListeners();
}
}
and a ListView which listens to those changes:
class DataListView extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<DataModel>(
builder: (context, model, child) {
return ListView.builder(
itemCount: model.data.length,
itemBuilder: (context, index) {
return Text(model.data[index].value);
},
);
},
);
}
}
so far so good, when an item is added to the list in the model, the change notification triggers a rebuild of the Listview and I see the new data. But I cant wrap my head around using this with a AnimatedList instead of a ListView. Preferably id like to keep my model as it is, seeing as the animation is a concern of the ui and not of my logic.
The changenotifier always gives me a uptodate version of my data, but what i really need is a "item added" or "item removed" notification.
Is there a best practice way of doing this?
This is the result of my trial.
It's a riverpod version, but I think it's the same for providers.
There are two points.
Initialize the state in the parent widget of the widget that uses
AnimatedList.
Add / delete AnimatedList and add / delete states asynchronously by using async.
main.dart
import 'package:animatedlist_riverpod_sample/provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:hooks_riverpod/all.dart';
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Home(),
);
}
}
class Home extends HookWidget {
const Home({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final todoList = useProvider(todoListProvider.state);
return Scaffold(appBar: AppBar(title: Text('Todo[${todoList.length}]')), body: TodoListView());
}
}
class TodoListView extends HookWidget {
TodoListView({Key key}) : super(key: key);
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
final todoList = useProvider(todoListProvider.state);
#override
Widget build(BuildContext context) {
return AnimatedList(
key: _listKey,
initialItemCount: todoList.length,
itemBuilder: (context, index, animation) =>
_buildItem(todoList[index], animation, index, context),
);
}
Slidable _buildItem(Todo todo, Animation<double> animation, int index, BuildContext context) {
return Slidable(
actionPane: SlidableDrawerActionPane(),
child: SizeTransition(
sizeFactor: animation,
axis: Axis.vertical,
child: ListTile(title: Text(todo.description), subtitle: Text(todo.id), onTap: () => {})),
secondaryActions: <Widget>[
IconSlideAction(
caption: 'Delete',
color: Colors.red,
icon: Icons.delete,
onTap: () {
_listKey.currentState.removeItem(
index, (context, animation) => _buildItem(todo, animation, index, context),
duration: Duration(milliseconds: 200));
_removeItem(context, todo);
},
),
],
);
}
void _removeItem(BuildContext context, Todo todo) async {
await Future.delayed(
Duration(milliseconds: 200), () => context.read(todoListProvider).remove(todo));
}
}
provider.dart
import 'package:hooks_riverpod/all.dart';
final todoListProvider = StateNotifierProvider<TodoList>((ref) {
return TodoList([
Todo(id: '0', description: 'Todo1'),
Todo(id: '1', description: 'Todo2'),
Todo(id: '2', description: 'Todo3'),
]);
});
class Todo {
Todo({
this.id,
this.description,
});
final String id;
final String description;
}
class TodoList extends StateNotifier<List<Todo>> {
TodoList([List<Todo> initialTodos]) : super(initialTodos ?? []);
void add(String description) {
state = [
...state,
Todo(description: description),
];
}
void remove(Todo target) {
state = state.where((todo) => todo.id != target.id).toList();
}
}
sample repository is here.
I recently started to learn Flutter and was surprised to find that this topic isn't covered properly anywhere. I came up with two approaches which I called Basic and Advanced. Let's start from Basic. It's named like that because Provider is called within the same widget where AnimatedList is built.
class Users extends ChangeNotifier {
final _list = ['0', '1', '2', '3', '4'];
int get length => _list.length;
operator [](index) => _list[index];
int add() {
final int index = length;
_list.add('$index');
notifyListeners();
return index;
}
String removeAt(int index) {
String user = _list.removeAt(index);
notifyListeners();
return user;
}
}
class BasicApp extends StatelessWidget {
const BasicApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ChangeNotifierProvider(create: (_) => Users(), child: AnimatedListDemo()));
}
}
class AnimatedListDemo extends StatelessWidget {
final GlobalKey<AnimatedListState> _listKey = GlobalKey();
AnimatedListDemo({Key? key}) : super(key: key);
void addUser(Users users) {
final int index = users.add();
_listKey.currentState!.insertItem(index, duration: const Duration(seconds: 1));
}
void deleteUser(Users users, int index) {
String user = users.removeAt(index);
_listKey.currentState!.removeItem(
index,
(context, animation) {
return SizeTransition(sizeFactor: animation, child: _buildItem(users, user));
},
duration: const Duration(seconds: 1),
);
}
Widget _buildItem(Users users, String user, [int? removeIndex]) {
return ListTile(
key: ValueKey<String>(user),
title: Text(user),
leading: const CircleAvatar(
child: Icon(Icons.person),
),
trailing: (removeIndex != null)
? IconButton(
icon: const Icon(Icons.delete),
onPressed: () => deleteUser(users, removeIndex),
)
: null,
);
}
#override
Widget build(BuildContext context) {
Users users = Provider.of<Users>(context, listen: false);
return Scaffold(
appBar: AppBar(
title: const Text('Basic AnimatedList Provider Demo'),
),
body: AnimatedList(
key: _listKey,
initialItemCount: users.length,
itemBuilder: (context, index, animation) {
return FadeTransition(
opacity: animation,
child: _buildItem(users, users[index], index),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => addUser(users),
tooltip: 'Add an item',
child: const Icon(Icons.add),
),
);
}
}
Advanced approach differs in that it encapsulates AnimatedListState. I took this idea from Flutter's AnimatedList docs.
typedef RemovedItemBuilder = Widget Function(
String user, BuildContext context, Animation<double> animation);
class Users extends ChangeNotifier {
final _list = ['0', '1', '2', '3', '4'];
final GlobalKey<AnimatedListState> _listKey = GlobalKey();
final RemovedItemBuilder _removedItemBuilder;
Users(this._removedItemBuilder);
int get length => _list.length;
operator [](index) => _list[index];
GlobalKey<AnimatedListState> get listKey => _listKey;
int add() {
final int index = length;
_list.add('$index');
_listKey.currentState!.insertItem(index, duration: const Duration(seconds: 1));
notifyListeners();
return index;
}
String removeAt(int index) {
String user = _list.removeAt(index);
_listKey.currentState!.removeItem(
index,
(BuildContext context, Animation<double> animation) {
return _removedItemBuilder(user, context, animation);
},
duration: const Duration(seconds: 1),
);
notifyListeners();
return user;
}
}
class AdvancedApp extends StatelessWidget {
const AdvancedApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: AnimatedListDemo());
}
}
class AnimatedListDemo extends StatelessWidget {
const AnimatedListDemo({Key? key}) : super(key: key);
Widget _buildItem(BuildContext context, String user, [int? removeIndex]) {
Users users = Provider.of<Users>(context, listen: false);
return ListTile(
key: ValueKey<String>(user),
title: Text(user),
leading: const CircleAvatar(
child: Icon(Icons.person),
),
trailing: (removeIndex != null)
? IconButton(
icon: const Icon(Icons.delete),
onPressed: () => users.removeAt(removeIndex),
)
: null,
);
}
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(create: (_) => Users((user, context, animation) {
return SizeTransition(sizeFactor: animation, child: _buildItem(context, user));
}), child: Scaffold(
appBar: AppBar(
title: const Text('Advanced AnimatedList Provider Demo'),
),
body: Consumer<Users>(builder: (BuildContext context, Users users, _){
return AnimatedList(
key: users.listKey,
shrinkWrap: true,
initialItemCount: users.length,
itemBuilder: (context, index, animation) {
return FadeTransition(
opacity: animation,
child: _buildItem(context, users[index], index),
);
},
);
}),
floatingActionButton: const AddButtonSeparateWidget(),
));
}
}
class AddButtonSeparateWidget extends StatelessWidget {
const AddButtonSeparateWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
Users users = Provider.of<Users>(context, listen: false);
return FloatingActionButton(
onPressed: users.add,
tooltip: 'Add an item',
child: const Icon(Icons.add),
);
}
}
All code is published on Github. Now I want to elaborate a bit on your proposition of having "item added" or "item removed" notifications. From what I understand it goes against Flutter's philosophy where widget is a UI config. When a widget's state changes, Flutter diffs against its previous state and magically applies the diff to UI. That's why I didn't use "item added", "item removed" notifications in my implementations. However I think it should be possible to do because I saw a similar approach in Firestore subscription to document changes although for now I can't figure how to implement the same with Provider. Provider's documentation is kind of poor. After a careful reading I can't say how to implement partial updates with Provider. May be ProxyProvider with its update could help or may be ListenableProvider. Let me know if you could find the solution to your proposition.
I'm trying to create a simple lisview with animated items. Everything is working fine, BUT when I tried to add the items at the top the behavior of the animated item is wrong
Here is my state class
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
List<ItemAnimado> lista = [];
void _incrementCounter() {
setState(() {
_counter++;
lista.add(ItemAnimado(_counter.toString()));
//lista.insert(0, ItemAnimado(_counter.toString()));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView.builder( itemCount: lista.length, itemBuilder: ( context, index,) {
return lista[index];
},),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Here is my animatedItem
class _ItemAnimadoState extends State<ItemAnimado> with TickerProviderStateMixin {
AnimationController _controller;
Animation<Offset> animation;
#override
void initState() {
super.initState();
_controller = AnimationController(duration: Duration(seconds: 1), vsync: this);
animation = Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(CurvedAnimation(parent: _controller, curve: Curves.bounceOut));
_controller.forward();
}
#override
Widget build(BuildContext context) {
return SlideTransition(
position: animation,
//duration: Duration(seconds: 1),
child: Card(
elevation: 9,
color: Colors.white,
child: ListTile(
title: Text(widget.texto),
),
),
);
}
}
https://giphy.com/gifs/d5Yd3FZFNNKuTr5ku5
I found out how to make the code works as I wanted.
The State class is almost the same. BUT, now I have a globalKey for the AnimatedList.
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
void _incrementCounter() {
setState(() {
Provider.of<ItemProvider>(context).addItem(
ItemAnimado(texto: 'New item'), 0);
});
}
#override
Widget build(BuildContext context) {
final _itemProvider = Provider.of<ItemProvider>(context);
_itemProvider.providerKey(_listKey);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: AnimatedList(
key: _listKey,
initialItemCount: _itemProvider.listaItems.length,
itemBuilder: (context, index, animation) {
return SlideTransition(
//key: _myListKey,
position: animation.drive(Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
)),
child: Card(
elevation: 9,
color: Colors.white,
child: ListTile(
title: Text(_itemProvider.listaItems[index].texto),
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
But the most important part is in how I manage the data. I'm using a Provider
I'm sending the Key to the Provider and in here I manage the insertItem inside the addItem function
class ItemProvider with ChangeNotifier {
List<ItemAnimado> _listaItems = [
ItemAnimado(texto: '1'),
ItemAnimado(texto: '2'),
ItemAnimado(texto: '3'),
];
GlobalKey<AnimatedListState> _listKey;
void providerKey(GlobalKey<AnimatedListState> key ){
_listKey = key;
}
List<ItemAnimado> get listaItems {
return [..._listaItems];
}
addItem(ItemAnimado nuevo, int index){
_listaItems.insert(0,nuevo);
_listKey.currentState.insertItem(index);
notifyListeners();
}
}
Here is a Gif with the final result
http://www.giphy.com/gifs/QybBE5E8kUUtxIbsnw
This is a case where you need to use Keys in your widgets. Here's a complete example:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
List<ItemAnimado> lista = [];
void _incrementCounter() {
setState(() {
_counter++;
lista.add(ItemAnimado(_counter.toString(), key: ValueKey(_counter)));
//lista.insert(0, ItemAnimado(_counter.toString()));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView.builder( itemCount: lista.length, itemBuilder: ( context, index,) {
return lista[index];
},),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class ItemAnimado extends StatefulWidget {
final String texto;
ItemAnimado(this.texto, {Key key}) : super(key: key);
_ItemAnimadoState createState() => _ItemAnimadoState();
}
class _ItemAnimadoState extends State<ItemAnimado> with TickerProviderStateMixin {
AnimationController _controller;
Animation<Offset> animation;
#override
void initState() {
super.initState();
_controller = AnimationController(duration: Duration(seconds: 1), vsync: this);
animation = Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(CurvedAnimation(parent: _controller, curve: Curves.bounceOut));
_controller.forward();
}
#override
Widget build(BuildContext context) {
return SlideTransition(
position: animation,
//duration: Duration(seconds: 1),
child: Card(
elevation: 9,
color: Colors.white,
child: ListTile(
title: Text(widget.texto),
),
),
);
}
}
Notice where we added key: ValueKey(_counter) in the creation of your ItemAnimado. The key tells Flutter which widgets have already been created and which ones have been newly added.
For more on Keys, see this video.