Related
I am trying to add animation to a list of widgets inside of a stack. When ever I remove a Widget or add a Widget to the List I Want to have a scale up/down transition as if the widget pops up from no where and shrinks to nothing. Any idea on how I can achieve this?
You can use AnimatedList widget as solution.
Example video here
Flutter documentation example
import 'package:flutter/material.dart';
void main() {
runApp(const AnimatedListSample());
}
class AnimatedListSample extends StatefulWidget {
const AnimatedListSample({super.key});
#override
State<AnimatedListSample> createState() => _AnimatedListSampleState();
}
class _AnimatedListSampleState extends State<AnimatedListSample> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
late ListModel<int> _list;
int? _selectedItem;
late int
_nextItem; // The next item inserted when the user presses the '+' button.
#override
void initState() {
super.initState();
_list = ListModel<int>(
listKey: _listKey,
initialItems: <int>[0, 1, 2],
removedItemBuilder: _buildRemovedItem,
);
_nextItem = 3;
}
// Used to build list items that haven't been removed.
Widget _buildItem(
BuildContext context, int index, Animation<double> animation) {
return CardItem(
animation: animation,
item: _list[index],
selected: _selectedItem == _list[index],
onTap: () {
setState(() {
_selectedItem = _selectedItem == _list[index] ? null : _list[index];
});
},
);
}
// Used to build an item after it has been removed from the list. This
// method is needed because a removed item remains visible until its
// animation has completed (even though it's gone as far this ListModel is
// concerned). The widget will be used by the
// [AnimatedListState.removeItem] method's
// [AnimatedListRemovedItemBuilder] parameter.
Widget _buildRemovedItem(
int item, BuildContext context, Animation<double> animation) {
return CardItem(
animation: animation,
item: item,
// No gesture detector here: we don't want removed items to be interactive.
);
}
// Insert the "next item" into the list model.
void _insert() {
final int index =
_selectedItem == null ? _list.length : _list.indexOf(_selectedItem!);
_list.insert(index, _nextItem++);
}
// Remove the selected item from the list model.
void _remove() {
if (_selectedItem != null) {
_list.removeAt(_list.indexOf(_selectedItem!));
setState(() {
_selectedItem = null;
});
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('AnimatedList'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.add_circle),
onPressed: _insert,
tooltip: 'insert a new item',
),
IconButton(
icon: const Icon(Icons.remove_circle),
onPressed: _remove,
tooltip: 'remove the selected item',
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: AnimatedList(
key: _listKey,
initialItemCount: _list.length,
itemBuilder: _buildItem,
),
),
),
);
}
}
typedef RemovedItemBuilder<T> = Widget Function(
T item, BuildContext context, Animation<double> animation);
/// Keeps a Dart [List] in sync with an [AnimatedList].
///
/// The [insert] and [removeAt] methods apply to both the internal list and
/// the animated list that belongs to [listKey].
///
/// This class only exposes as much of the Dart List API as is needed by the
/// sample app. More list methods are easily added, however methods that
/// mutate the list must make the same changes to the animated list in terms
/// of [AnimatedListState.insertItem] and [AnimatedList.removeItem].
class ListModel<E> {
ListModel({
required this.listKey,
required this.removedItemBuilder,
Iterable<E>? initialItems,
}) : _items = List<E>.from(initialItems ?? <E>[]);
final GlobalKey<AnimatedListState> listKey;
final RemovedItemBuilder<E> removedItemBuilder;
final List<E> _items;
AnimatedListState? get _animatedList => listKey.currentState;
void insert(int index, E item) {
_items.insert(index, item);
_animatedList!.insertItem(index);
}
E removeAt(int index) {
final E removedItem = _items.removeAt(index);
if (removedItem != null) {
_animatedList!.removeItem(
index,
(BuildContext context, Animation<double> animation) {
return removedItemBuilder(removedItem, context, animation);
},
);
}
return removedItem;
}
int get length => _items.length;
E operator [](int index) => _items[index];
int indexOf(E item) => _items.indexOf(item);
}
/// Displays its integer item as 'item N' on a Card whose color is based on
/// the item's value.
///
/// The text is displayed in bright green if [selected] is
/// true. This widget's height is based on the [animation] parameter, it
/// varies from 0 to 128 as the animation varies from 0.0 to 1.0.
class CardItem extends StatelessWidget {
const CardItem({
super.key,
this.onTap,
this.selected = false,
required this.animation,
required this.item,
}) : assert(item >= 0);
final Animation<double> animation;
final VoidCallback? onTap;
final int item;
final bool selected;
#override
Widget build(BuildContext context) {
TextStyle textStyle = Theme.of(context).textTheme.headline4!;
if (selected) {
textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]);
}
return Padding(
padding: const EdgeInsets.all(2.0),
child: SizeTransition(
sizeFactor: animation,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: SizedBox(
height: 80.0,
child: Card(
color: Colors.primaries[item % Colors.primaries.length],
child: Center(
child: Text('Item $item', style: textStyle),
),
),
),
),
),
);
}
}
I am trying to build a view/route that will list items fetched from a REST source.
I want to show a notification item below the list while the data is being fetched.
But my ListView builder is constructed around the fetched data's structure, so I figured just have a ListTile fit some appropriate UX elements below the generated list inside a Column - which was kinda working great - or so I thought - until the list grows to fill the screen causing RenderFlex overflowed error. Wrapping the ListView builder in Expanded fixed that but moved the indicator to the bottom of the screen.
In trying to fix it I seem to have broken more of the plumbing and the boolean variable that should control the idicator widget; isLoading: stockSet.isBusyLoading doesn't seem to update.
At the moment if I hardcode it as `` it does sit in the appropraite position but I am back with the RenderFlex overflow.
Once all of this is working I'll be wanting to automatically load items untill the screen is full - not sure where I'll be triggering that from yet.
class MyStockSet extends StatefulWidget {
const MyStockSet({super.key});
static const indexStr = 'stocks';
static const labelStr = 'Properties';
#override
State<MyStockSet> createState() => _MyStockSetState();
}
class _MyStockSetState extends State<MyStockSet> {
#override
Widget build(BuildContext context) {
const String imagePath = 'assets/images/${MyStockSet.indexStr}.png';
var assetImage = const AssetImage(imagePath);
//var stockSet = context.watch<StockSet>(); <- didn't work either
var stockSet = Provider.of<StockSet>(context,listen: false);
return Scaffold(
appBar: AppBar(
title: Row(
children: [
AscHero(
assetImage: assetImage,
tag: MyStockSet.indexStr,
title: MyStockSet.labelStr,
radius: 32,
),
const SizedBox(width: 12),
const Text(MyStockSet.labelStr),
],
),
actions: [
IconButton(
onPressed: () {
var stockSet = context.read<StockSet>();
int newNr = stockSet.stocks.length + 1;
Stock tmpstock = Stock(
id: newNr,
title: 'test$newNr',
thumbUrl: 'url',
description: 'desc');
stockSet.add(tmpstock);
},
icon: const Icon(Icons.add),
),
IconButton(
onPressed: () {
developer.log('btn before isBusyLoading ${stockSet.isBusyLoading}');
stockSet.fetch();
developer.log('after btn isBusyLoading ${stockSet.isBusyLoading}');
},
icon: const Icon(Icons.handshake),
),
],
),
body: Column(
children: [
Row(
// these will be filters, order toggle etc.
children: [
ElevatedButton(
onPressed: () => developer.log('Btn pressed.'),
child: Text('Btn')),
],
),
Expanded(
child: Column(children: [
_StockListView(),
LoadingStockListItemNotif(
isLoading: true,
),
]),
),
],
),
);
}
}
class _StockListView extends StatefulWidget {
#override
State<_StockListView> createState() => _StockListViewState();
}
class _StockListViewState extends State<_StockListView> {
#override
void didChangeDependencies() {
super.didChangeDependencies();
developer.log('_StockListView didChangeDependencies()');
// developer.log('scroll pos ${_scrollController.position}');
}
#override
Widget build(BuildContext context) {
var stockSet = context.watch<StockSet>();
return ListView.builder(
// controller: _scrollController,
shrinkWrap: true,
itemCount: stockSet.stocks.length,
itemBuilder: (context, index) => InkWell(
child: StockListItem(
stock: stockSet.stocks[index],
),
onTap: () => Navigator.pushNamed(
context,
'/stocks/stock',
arguments: ScreenArguments(stockSet.stocks[index]),
),
),
);
}
void _scrollListener() {
developer.log('_scrollListener');
}
}
and
class StockSet extends ChangeNotifier {
final List<Stock> _stocks = [];
late bool isBusyLoading = false;
List<Stock> get stocks => _stocks;
void add(Stock stock) {
_stocks.add(stock);
developer.log('added stock :${stock.title}');
notifyListeners();
}
void remove(Stock stock) {
_stocks.remove(stock);
notifyListeners();
}
Future<void> fetch() async {
developer.log('fetch() iL T');
isBusyLoading = true;
notifyListeners();
Stock tmpStock = await _fetchAStock();
developer.log('fetch() iL F');
isBusyLoading = false;
notifyListeners();
add(tmpStock);
}
Future<Stock> _fetchAStock() async {
developer.log('fetch stock ');
final response = await http.get(
Uri.https(
//...
),
);
developer.log('response.statusCode:${response.statusCode}');
if (response.statusCode == 200) {
final Map<String, dynamic> map = json.decode(response.body);
return Stock(
id: map['id'] as int,
title: map['title'] as String,
description: map['description'] as String,
thumbUrl: map['thumbUrl'] as String,
);
}
throw Exception('error fetching stocks:');
}
}
Apologies for the convoluted question.
Add mainAxisSize : MainAxisSize.min for the column inside the expanded widget. The expanded doesn't have any bounds and that's why it throws an error. You can wrap the column with a SingleChildScrollView if you have long content to display
This worked for me!
Just set the shrinkWrap attribute to true
Main lesson:
Don't fight the framework.
Answer:
Instead of tying yourself into Möbius knots trying to put the ListView's functionality outside of itself; use the fact that the ListView.builder allows you to sculpt the logic of how it gets built and what it will contain - given that the provider can trigger its rebuild when the variable in the data set changes.
In other words; by increasing the loop of the builder, you can insert a kind of footer to the Listview. The appearance (or not) of that can depend on the provider, provided it fires the appropriate notifyListeners()s etc.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:equatable/equatable.dart';
import 'dart:async';
class ItemSetRoute extends StatefulWidget {
const ItemSetRoute({Key? key}) : super(key: key);
#override
State<ItemSetRoute> createState() => _ItemSetRouteState();
}
class _ItemSetRouteState extends State<ItemSetRoute> {
#override
Widget build(BuildContext context) {
var itemSet = Provider.of<ItemSet>(
context,
listen: true /* in order to rebuild */,
);
return Scaffold(
appBar: AppBar(title: const Text('Test'), actions: [
IconButton(
onPressed: () {
itemSet.fetch();
},
icon: const Icon(Icons.download),
)
]),
body: Column(
//screen
children: [
Row(
children: [
ElevatedButton(
onPressed: () {
itemSet.fetch();
},
child: const Text('Btn')),
],
),
Expanded(
child: ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: itemSet.items.length + 1,
itemBuilder: (context, index) {
/* logic here to create a kind of footer of the ListView */
if (index <= itemSet.items.length - 1) {
return InkWell(
child: ItemListItem(
item: itemSet.items[index],
),
onTap: () {
//('Item tapped, navigate etc.');
});
} else {
return LoadingItemNotifier(
isLoading: itemSet.isBusyLoading,
);
}
},
),
),
],
),
);
}
}
//Models
class ItemListItem extends StatelessWidget {
const ItemListItem({Key? key, required this.item}) : super(key: key);
final Item item;
#override
Widget build(BuildContext context) {
return Material(
child: ListTile(
title: Text(item.title),
subtitle: Text(item.description),
),
);
}
}
class LoadingItemNotifier extends StatefulWidget {
const LoadingItemNotifier({Key? key, this.isLoading = false})
: super(key: key);
final bool isLoading;
#override
State<LoadingItemNotifier> createState() => _LoadingItemNotifierState();
}
class _LoadingItemNotifierState extends State<LoadingItemNotifier> {
#override
Widget build(BuildContext context) {
if (widget.isLoading) {
return Material(
child: ListTile(
leading: SizedBox(
width: 48,
height: 48,
child: ClipOval(
child: Material(
color: Colors.lightBlue.withOpacity(0.25),
child: const Center(
child: Icon(Icons.download),
),
),
),
),
title: const Text('Loading'),
isThreeLine: true,
subtitle: const Text('One moment please...'),
dense: true,
),
);
} else {
return const SizedBox(height: 0);
}
}
}
class ItemSet extends ChangeNotifier {
final List<Item> _items = [];
late bool isBusyLoading = false;
List<Item> get items => _items;
void add(Item item) {
_items.add(item);
notifyListeners();
}
void remove(Item item) {
_items.remove(item);
notifyListeners();
}
Future<void> fetch() async {
isBusyLoading = true;
notifyListeners();
/* handling REST call here */
await Future.delayed(const Duration(milliseconds: 500));
Item newItem = const Item(id: 123, title: 'Title', description: 'Desc');
isBusyLoading = false;
add(newItem);
}
}
class Item extends Equatable {
const Item({
required this.id,
required this.title,
required this.description,
});
final int id;
final String title;
final String description;
#override
List<Object> get props => [id, title, description];
}
Caveats
I don't know if this is the most efficient way of doing this - perhaps there should be fewer states, etc. ...
I want to call this function addPresetToList() from a different parent widget. I have imported the AnimatedListWidget (Child widget) to the parent widget. But still the function is not visible and publicly available. What could be the issue? and what are the other ways to call the functions which is inside an another class? I can't separate the function into a different class and call it as it requires to aqqire data eg: _selectedItem etc.
void addPresetToList() {
final int index =
_selectedItem == null ? _list.length : _list.indexOf(_selectedItem!);
_list.insert(index, _nextItem++);
}
This is the full code of the list widget which contains the above code
import 'package:flutter/material.dart';
// void main() {
// runApp(const AnimatedListSample());
// }
class AnimatedListWidget extends StatefulWidget {
const AnimatedListWidget({Key? key}) : super(key: key);
#override
State<AnimatedListWidget> createState() => _AnimatedListWidgetState();
}
class _AnimatedListWidgetState extends State<AnimatedListWidget> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
late ListModel<int> _list;
int? _selectedItem;
late int
_nextItem; // The next item inserted when the user presses the '+' button.
#override
void initState() {
super.initState();
_list = ListModel<int>(
listKey: _listKey,
initialItems: <int>[0, 1, 2],
removedItemBuilder: _buildRemovedItem,
);
_nextItem = 3;
}
// Used to build list items that haven't been removed.
Widget _buildItem(
BuildContext context, int index, Animation<double> animation) {
return CardItem(
animation: animation,
item: _list[index],
selected: _selectedItem == _list[index],
onTap: () {
setState(() {
_selectedItem = _selectedItem == _list[index] ? null : _list[index];
});
},
);
}
Widget _buildRemovedItem(
int item, BuildContext context, Animation<double> animation) {
return CardItem(
animation: animation,
item: item,
// No gesture detector here: we don't want removed items to be interactive.
);
}
// class AddRemoveListItems {
// Insert the "next item" into the list model.
void addPresetToList() {
final int index =
_selectedItem == null ? _list.length : _list.indexOf(_selectedItem!);
_list.insert(index, _nextItem++);
}
// Remove the selected item from the list model.
void remove() {
if (_selectedItem != null) {
_list.removeAt(_list.indexOf(_selectedItem!));
setState(() {
_selectedItem = null;
});
}
}
// }
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
//child: Expanded(
child: SingleChildScrollView(
child: Expanded(
child: AnimatedList(
shrinkWrap: true,
key: _listKey,
initialItemCount: _list.length,
itemBuilder: _buildItem,
),
))));
}
}
typedef RemovedItemBuilder<T> = Widget Function(
T item, BuildContext context, Animation<double> animation);
class ListModel<E> {
ListModel({
required this.listKey,
required this.removedItemBuilder,
Iterable<E>? initialItems,
}) : _items = List<E>.from(initialItems ?? <E>[]);
final GlobalKey<AnimatedListState> listKey;
final RemovedItemBuilder<E> removedItemBuilder;
final List<E> _items;
AnimatedListState? get _animatedList => listKey.currentState;
void insert(int index, E item) {
_items.insert(index, item);
_animatedList!.insertItem(index);
}
E removeAt(int index) {
final E removedItem = _items.removeAt(index);
if (removedItem != null) {
_animatedList!.removeItem(
index,
(BuildContext context, Animation<double> animation) {
return removedItemBuilder(removedItem, context, animation);
},
);
}
return removedItem;
}
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,
this.onTap,
this.selected = false,
required this.animation,
required this.item,
}) : assert(item >= 0),
super(key: key);
final Animation<double> animation;
final VoidCallback? onTap;
final int item;
final bool selected;
#override
Widget build(BuildContext context) {
TextStyle textStyle = Theme.of(context).textTheme.headline4!;
if (selected) {
textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]);
}
return Padding(
padding: const EdgeInsets.all(2.0),
child: SizeTransition(
sizeFactor: animation,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: SizedBox(
height: 80.0,
child: Card(
color: Colors.primaries[item % Colors.primaries.length],
child: Center(
child: Text('Item $item', style: textStyle),
),
),
),
),
),
);
}
}
So it is usually impossible (or not good practice) to call a function from a parent widget, usually, all communication between parent and child looks like this:
+--------+ +-------+
| parent | ---data---> | child |
| | <-callback- | |
+--------+ +-------+
In your case, that probably means that both _list and _selectedItem must be part of your parent widget and sent over as data for the child widget to use.
class AnimatedListWidget extends StatelessWidget
{
AnimatedListWidget({this.selectedItem, required this.list, required this.listKey, required this.changeSelected});
final int? selectedItem;
final ListModel<int> list;
final GlobalKey<AnimatedListState> listKey;
final ValueChanged<int>? changeSelected;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Expanded(
child: AnimatedList(
shrinkWrap: true,
key: _listKey,
initialItemCount: list.length,
itemBuilder: _buildItem,
),
))));
}
Widget _buildItem(
BuildContext context, int index, Animation<double> animation) {
return CardItem(
animation: animation,
item: list[index],
selected: selectedItem == list[index],
onTap: changeSelected==null?null : () => changeSelected(index);
},
);
}
}
Then on the parent widget:
class ParentWidget extends StatefulWidget {
...
}
class _ParentWidgetState extends State<ParentWidget> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
late ListModel<int> _list;
int? _selectedItem;
late int
_nextItem; // The next item inserted when the user presses the '+' button.
#override
void initState() {
super.initState();
_list = ListModel<int>(
listKey: _listKey,
initialItems: <int>[0, 1, 2],
removedItemBuilder: _buildRemovedItem,
);
_nextItem = 3;
}
#override Widget build(BuildContext context) {
return AnimatedListWidget(
selectedItem = _selectedItem,
list: _list,
listKey: _listKey,
changeSelected: (i) => setState(() =>_selectedItem = _selectedItem == _list[i] ? null : _list[i]),
);
}
void addPresetToList() {
final int index =
_selectedItem == null ? _list.length : _list.indexOf(_selectedItem!);
_list.insert(index, _nextItem++);
setState(() {});
}
}
This way the parent can access the function because it is part of the parent. If you want the child to access it, you have to pass it as a callback like you would on an onPressed method on an elevated button
I have multiple widget and lists within CustomScrollView and I would like to stop CustomScrollView to scroll while scrolling on some pixels bound condition.
I can use NeverScrollPhysics() to stop it but I don't want to use setState() function here because the CustomScrollview content with lists is big enough to make the screen laggy while reloading on scroll.
Also tried with Provider but the builder is providing only a child widget which is not working with sliver list.
Here is the code using setState() :
NotificationListener(
onNotification: (ScrollNotification notif) {
if(notif is ScrollUpdateNotification) {
if (canScroll && notif.metrics.pixels > 100) {
canScroll = false;
setState(() {});
}
}
if(notif is ScrollEndNotification) {
if(!canScroll) {
canScroll = true;
setState(() {});
}
}
return true;
},
child: CustomScrollView(
shrinkWrap: true,
physics: canScroll ? BouncingScrollPhysics() : NeverScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(),
List(),
List(),
],
),
),
Is there a way to reload only the CustomScrollView without its child ? Otherwise any workaround to prevent scrolling in this case ?
Thanks for help
When the build method is called, all widgets in that build method will be rebuild except for const widgets, but const widget cannot accept dynamic arguments (only a constant values).
Riverpod provides a very good solution in this case,
With ProviderScope you can pass arguments by inherited widget instead of widget constructor (as when passing arguments using navigation) so the contractor can be const.
Example :
Data module
TLDR you need to use Freezed package or override the == operator and the hashCode almost always because of dart issue.
class DataClass {
final int age;
final String name;
const DataClass(this.age, this.name);
#override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is DataClass && other.age == age && other.name == name;
}
#override
int get hashCode => age.hashCode ^ name.hashCode;
}
setting our ScopedProvider as a global variable
final dataClassScope = ScopedProvider<DataClass>(null);
The widget we use in our list
class MyChildWidget extends ConsumerWidget {
const MyChildWidget();
#override
Widget build(BuildContext context, ScopedReader watch) {
final data = watch(dataClassScope);
// Note for better optimaization
// in case you are sure the data you are passing to this widget wouldn't change
// you can just use StatelessWidget and set the data as:
// final data = context.read(dataClassScope);
// use ConsumerWidget (or Consumer down in this child widget tree) if the data has to change
print('widget with name\n${data.name} rebuild');
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20),
child: Text(
'Name : ${data.name}\nAge ${data.age}',
textAlign: TextAlign.center,
),
),
);
}
}
finally the main CustomScrollView widget
class MyMainWidget extends StatefulWidget {
const MyMainWidget();
#override
State<MyMainWidget> createState() => _MyMainWidgetState();
}
class _MyMainWidgetState extends State<MyMainWidget> {
bool canScroll = true;
void changeCanScrollState() {
setState(() => canScroll = !canScroll);
print('canScroll $canScroll');
}
final dataList = List.generate(
20,
(index) => DataClass(10 * index, '$index'),
);
#override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
changeCanScrollState();
},
child: CustomScrollView(
shrinkWrap: true,
physics: canScroll
? BouncingScrollPhysics()
: NeverScrollableScrollPhysics(),
slivers: [
for (int i = 0; i < dataList.length; i++)
ProviderScope(
overrides: [
dataClassScope.overrideWithValue(dataList[i]),
],
child: const MyChildWidget(),
),
],
),
),
);
}
}
Don't forget to wrap the MaterialApp with ProviderScope.
runApp(
ProviderScope(
child: MyApp(),
),
);
Try this solution use const constructor for child widget so it won't rebuild unless widget changed
class MyHomePage extends StatelessWidget {
ValueNotifier<ScrollPhysics> canScroll =
ValueNotifier(const BouncingScrollPhysics());
MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: NotificationListener(
onNotification: (ScrollNotification notif) {
if (notif is ScrollUpdateNotification) {
if (canScroll.value.runtimeType == BouncingScrollPhysics &&
notif.metrics.pixels > 100) {
canScroll.value = const NeverScrollableScrollPhysics();
debugPrint("End false");
}
}
if (notif is ScrollEndNotification) {
if (canScroll.value.runtimeType == NeverScrollableScrollPhysics) {
debugPrint("End");
Future.delayed(const Duration(milliseconds: 300),
() => canScroll.value = const BouncingScrollPhysics());
debugPrint("End1");
}
}
return true;
},
child: ValueListenableBuilder(
valueListenable: canScroll,
builder:
(BuildContext context, ScrollPhysics scrollType, Widget? child) =>
CustomScrollView(
physics: scrollType,
slivers: [
SliverToBoxAdapter(
child: Container(
height: 200,
color: Colors.black,
),
),
SliverToBoxAdapter(
child: Column(
children: [
Container(
height: 100,
color: Colors.blue,
),
Container(
height: 200,
color: Colors.grey,
),
Container(
height: 200,
color: Colors.blue,
),
Container(
height: 200,
color: Colors.grey,
),
Container(
height: 200,
color: Colors.blue,
),
],
),
),
],
),
),
),
);
}
}
Are you just need to stop the user from scrolling it? I think you can try to controller the list to a fixed position by using jumoTo.
...
final _controller = ScrollController();
#override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (ScrollNotification notif) {
if (notif is ScrollUpdateNotification) {
if (notif.metrics.pixels > 100) {
_controller.jumpTo(100)
}
}
return true;
},
child: CustomScrollView(
controller: _controller,
...
I am trying to learn checkboxes in Flutter.
The problem is, when I want to use checkboxes in Scaffold(body:) it is working. But I want to use it in different places like an item in ListView.
return Center(
child: Checkbox(
value: testValue,
onChanged: (bool value) {
setState() {
testValue = value;
}
},
));
But it is not working, updating and changing anything.
Edit: I solved my problem with putting checkbox in a StatefulBuilder. Thanks to #cristianbregant
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Center(
child: CheckboxListTile(
title: const Text('Animate Slowly'),
value: _valueCheck,
onChanged: (bool value) {
setState(() {
_valueCheck = value;
});
},
secondary: const Icon(Icons.hourglass_empty),
),
);
});
Try these maybe:
return Center(
child: CheckboxListTile(
title: const Text('Animate Slowly'),
value: _valueCheck,
onChanged: (bool value) {
setState(() {
_valueCheck = value;
});
},
secondary: const Icon(Icons.hourglass_empty),
),
);
and remember that if you are using it in a dialog or bottomsheet you need to wrap the Checkbox Widget in a Stateful builder because the state does not update.
Checkboxes require you have a Scaffold or Material as their parent. Without either of these, you get this helpful error message:
The following assertion was thrown building Checkbox(dirty, state: _CheckboxState#1163b):
No Material widget found.
Checkbox widgets require a Material widget ancestor.
In material design, most widgets are conceptually "printed" on a sheet of material.
In Flutter's material library, that material is represented by the Material widget. It is the Material widget that renders ink splashes, for instance. Because of this, many material library widgets require that there be a Material widget in the tree above them.
Once you have a material ancestor, you can place the ListView as it's child and it should show fine:
class SettingsPage extends StatefulWidget {
#override
_SettingsPageState createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
var _foo = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Toggle Foo'),
Checkbox(
value: _foo,
onChanged: (bool value) {
setState(() => _foo = value);
},
),
],
),
],
),
);
}
}
Seems like you will have to use both initState and dispose.
See my code example below:
class SettingsOrder extends StatefulWidget {
#override
_SettingsOrderState createState() => _SettingsOrderState();
}
class _SettingsOrderState extends State<SettingsOrder> {
List options = [];
List<bool> newoptions = [];
int selectedoption;
bool checkedstatus;
bool initialcall;
Future getproductlist(selectedoption, checkedstatus, initialcall) async{
List updatedlist = [];
final arguments = ModalRoute.of(context).settings.arguments as Map;
int itempos = 0;
options.clear();
if(initialcall == false){
for(var item in arguments['options']){
updatedlist.add({
'checkbox' : newoptions[itempos]
});
itempos++;
}
} else {
for(var item in arguments['options']){
updatedlist.add({
'checkbox' : checkedstatus
});
newoptions.add(false);
itempos++;
}
}
setState(() {
options = updatedlist;
});
}
#override
void initState(){
super.initState();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
getproductlist(0, false, true);
return Scaffold(
body: SingleChildScrollView(
child: Container(
width: double.infinity,
child: ListView.builder(
primary: false,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index){
return Container(
child: Theme(
data: ThemeData(
unselectedWidgetColor: Colors.grey
),
child: CheckboxListTile(
controlAffinity: ListTileControlAffinity.trailing,
title: Text(options[index]['name']),
value: options[index]['checkbox'],
onChanged: (newvalue){
int indexposition = index;
newoptions.removeAt(indexposition);
newoptions.insert(indexposition, newvalue);
getproductlist(indexposition, newvalue, false);
},
activeColor: Color.fromRGBO(0, 130, 214, 1),
checkColor: Colors.white,
),
),
);
}
),
),
),
);
}