PageView, inside ListView, inside Column: `Null check operator used on a null value` - flutter

I have a PageView, inside a ListView, inside a Column. It seems necessary because of the layout (top green widget should be scrollable, but bottom widget shouldn't be).
class MyWidget extends StatefulWidget {
Widget createPage(int page, Color color) {
return Container(
color: color,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 200.0),
child: Text("Page: $page. Swipe right to go to next page"),
),
);
}
late final List<Widget> pages = [
createPage(1, Colors.purple),
createPage(2, Colors.red)
];
#override
createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late final PageController _pageController;
int page = 0;
#override
initState() {
super.initState();
_pageController = PageController();
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: ListView(
children: [
Container(
color: Colors.green,
child: const Text("Top of screen, scrollable.")),
// This works
Column(
children: widget.pages,
),
// PageView tries to take the full height. Without Flexible, PageView is unbounded.
// Flexible(
// child: PageView.builder(
// itemCount: widget.pages.length,
// controller: _pageController,
// onPageChanged: (int index) => setState(() => page = index),
// itemBuilder: (context, index) => widget.pages[index],
// ),
// )
],
),
),
Container(
color: Colors.green, child: const Text("Bottom of screen, always")),
],
);
}
}
However, I get various error based on what I try. This is an unhelpful error, since ListView doesn't have a problem when it's children is a Column:
The following _CastError was thrown during performLayout():
Null check operator used on a null value
The relevant error-causing widget was:
ListView ListView:file:///path/to/project/lib/main.dart:63:18
Code
I've created a Dartpad pad to reproduce this. I've commented out the PageView and replaced it with a column so you can see the app. The issue is replacing Column with 2 pages to PageView.
I've tried various things:
Wrapping ListView in things:
SizedBox.expand(child: ListView(...))
Expanded(child: ListView(...)) following this answer
Flexible(child: ListView(...))
ListView.builder(shrinkWrap: true,...),
Changing mainAxisSize of Column

Related

Flutter TabBar-like Indicator

I need help identifying a Flutter widget that could do things as demonstrated by this GIF:
I am interested in the circular indicator that allows me to browse through the resort locations like above. So far the closest thing that I have found was TabBar, but that isn't what I am exactly looking for. It seems far too complex to modify it to do the things that the GIF does, and I am very certain that there is a simpler solution, but after hours of exhaustive search through the list of widgets, I am not sure what to look for.
Could someone please help me?
Based on your UI, you can use dots_indicator or better smooth_page_indicator
class TestSnippet extends StatefulWidget {
const TestSnippet({super.key});
#override
State<TestSnippet> createState() => _TestSnippetState();
}
class _TestSnippetState extends State<TestSnippet> {
final PageController controller = PageController();
int itemCount = 44;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(
child: PageView.builder(
itemCount: itemCount ~/ 3,
controller: controller,
itemBuilder: (context, index) => Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
index.toString(),
),
Text("${index + 1}"),
Text("${index + 2}"),
],
),
),
),
SmoothPageIndicator(
count: itemCount ~/ 3,
controller: controller,
)
],
),
);
}
}

How to update a single item of a SliverList in Flutter?

Do you guys know how can I update a single item of a sliver list without having to invoke a setState() ?
In my case I have a SliverList and i want to click into an item and change it's color, the problem using setState() is that it rebuilds the whole UI in a not smooth way and also mess up with Custom Scroll position.
The funny thing is that this SliverList behaviour does not occur when using normal ListView, when use setState() on a ListView the load is smooth and it doesn't break the scroll state. Looks like the ListView can implicitely handle state better than SliverList.
But since I have a Custom Scroll I can't use ListVew it has to be SliverList
Any options ? Providers ?Notifiers ? Stream ? Bloc ?
Ok, after all, I could solve my need using a simple ChangeNotifier combined with an AnimatedBuilder inside each item of the SliverList, I'll post some high-level code of the solution - it works just fine for my need!
class ChangeColorSliverListItemNotifier extends ChangeNotifier {
int index;
Color current_label_color;
ChangeColorSliverListItemNotifier()
{
this.current_label_color = Colors.white;
}
void onTap(int selected_index)
{
index = selected_index;
this.current_label_color = Colors.yellow;
notifyListeners();
}
}
// code block inside SliverList items binding- changing only the color of the selected Widget
SliverList(
delegate: SliverChildBuilderDelegate((context, index)
{
return GestureDetector(
onTap:() {
changeColorSliverListItemNotifier.onTap(index);
},
child:AnimatedBuilder(
animation: changeColorSliverListItemNotifier,
builder: (_, __) =>
Row(children:[
Container(
margin: EdgeInsets.only(right:8),
child: Icon(Icons.edit,color:changeColorSliverListItemNotifier.index==index?changeColorSliverListItemNotifier.current_label_color:default_color,size: 20,)
),
])
)
)
....
Just in case you want to use the Riverpod state management library, I made the following.
class SliverScreen extends StatelessWidget {
const SliverScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
_appBar(),
_list(),
],
),
);
}
SliverFixedExtentList _list() {
return SliverFixedExtentList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListItem(index: index),
),
itemExtent: 100,
);
}
SliverAppBar _appBar() {
return SliverAppBar(
title: Text("Slivering..."),
backgroundColor: Colors.teal[900],
expandedHeight: 200,
pinned: true,
stretch: true,
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.teal[100]!,
Colors.teal[600]!,
],
),
),
),
),
);
}
}
final colorStateProvider = StateProvider.family<Color, int>((ref, key) {
return Colors.blue[100]!;
});
class ListItem extends HookConsumerWidget {
final int index;
const ListItem({Key? key, required this.index}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final colorState = ref.watch(colorStateProvider(index));
return InkWell(
onTap: () => colorState.state = Colors.blue,
child: Container(
color: colorState.state,
child: Center(
child: Text("Item $index"),
),
),
);
}
}
If you click an item, it updates the color without rebuilding the whole list, only the item itself.

Flutter AlwaysScrollableScrollPhysics() not working

Question
Hi, I was searching a solution to allow user scroll on a list even when there is insufficient content.
Looking throght Flutter documentation i found this page https://api.flutter.dev/flutter/widgets/ScrollView/physics.html
As the documentation said
To force the scroll view to always be scrollable even if there is insufficient content, as if primary was true but without necessarily setting it to true, provide an AlwaysScrollableScrollPhysics physics object, as in:
physics: const AlwaysScrollableScrollPhysics(),
so I tried to run a simple code an detect user scroll even when there isn't enough content
code
class Page extends StatefulWidget {
#override
_PageState createState() => _PageState();
}
class _PageState extends State<Page> {
#override
Widget build(BuildContext context) {
final ScrollController scrollController = ScrollController();
#override
void initState(){
scrollController.addListener((){
print('listener called');
});
super.initState();
}
return Scaffold(
body: ListView.builder(
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: 5,
itemBuilder: (context, index){
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Container(
color: Colors.black,
height: 50,
),
);
},
),
);
}
}
Why this isn't working?
edit
Here is the design i'm looking forward
I have a list that is dynamically created. I want to be able to detect user vertical swipes on that list even if there is no scroll because there aren't enough element to overflow the screen height.
On a scrollable list I can simply add a scroll Listener and then every time a scroll is detected I can do my logic with scrollController.position info's.
I want scroll listener to be called even when user swipes on list of this type
I do see the effect of scroll with the addition of AlwaysScrollableScrollPhysics so that part seems to be working. Maybe wrapping the scaffold on a NotificationListener can do what you're trying to do:
class _PageState extends State<Page> {
#override
Widget build(BuildContext context) {
final ScrollController scrollController = ScrollController();
return NotificationListener(
child: Scaffold(
body: ListView.builder(
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: 5,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Container(
color: Colors.black,
height: 50,
),
);
},
),
),
onNotification: (scrollNotification) {
if (scrollNotification is ScrollStartNotification) {
print('Widget has started scrolling');
}
return true;
},
);
}
}
NotificationListener has a property called onNotification that allows you to check for different kinds of scrollNotifications, you can check more here: NotificationListener Class and ScrollNotification class

Flutter - How to flip the previous card back using FlipCard

After days of search I'm getting help.
I work on a flutter application.
Context:
A grid view feeded with Json
-childs : GridTile with Flipcard in (https://pub.dev/packages/flip_card)
-On tap on GridTile there is a callback to get the selected Item and an animation because of the flipcard onTap
What I would:
When an item is aleready selected (flipcard flipped so we show the back of the card),
And I selected another item of the grid te(so flipcard of this itme also flipped)
I would like to flip back the old selected item Flipcard without rebuild the tree because I would lost the state of the new selected item.
I tried many thing. For example I tried to use GlobalKey on GridTiles to interract with after build but currentState is always null when I want to interact with.
I wonder what is the good practice in this case ?
I hope I was clear :) (I'm french)
Thank you the community!
.
Something to know...
It is possible to interract with the flipcard (child of gridtile) like this
(GlobalKey)
GlobalKey<FlipCardState> cardKey = GlobalKey<FlipCardState>();
#override
Widget build(BuildContext context) {
return FlipCard(
key: cardKey,
flipOnTouch: false,
front: Container(
child: RaisedButton(
onPressed: () => cardKey.currentState.toggleCard(),
child: Text('Toggle'),
),
),
back: Container(
child: Text('Back'),
),
);
}
I'm not sure if I understood your question, but here is an example of how you could use a GridView with FlipCards:
var cardKeys = Map<int, GlobalKey<FlipCardState>>();
GlobalKey<FlipCardState> lastFlipped;
Widget _buildFlipCard(String text, Color color, int index) {
return SizedBox(
height: 120.0,
child: Card(
color: color,
child: Center(
child:
Text(text, style: TextStyle(color: Colors.white, fontSize: 20.0)),
),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("FlipCards")),
body: GridView.builder(
itemCount: 20,
itemBuilder: (context, index) {
cardKeys.putIfAbsent(index, () => GlobalKey<FlipCardState>());
GlobalKey<FlipCardState> thisCard = cardKeys[index];
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlipCardWithKeepAlive(
child: FlipCard(
flipOnTouch: false,
key: thisCard,
front: _buildFlipCard("$index", Colors.blue, index),
back: _buildFlipCard("$index", Colors.green, index),
onFlip: () {
if (lastFlipped != thisCard) {
lastFlipped?.currentState?.toggleCard();
lastFlipped = thisCard;
}
},
),
),
RaisedButton(
child: Text("Flip Card"),
onPressed: () => cardKeys[index].currentState.toggleCard(),
)
],
);
},
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
),
);
}
class FlipCardWithKeepAlive extends StatefulWidget {
final FlipCard child;
FlipCardWithKeepAlive({Key key, this.child}) : super(key: key);
#override
State<StatefulWidget> createState() => FlipCardWithKeepAliveState();
}
class FlipCardWithKeepAliveState extends State<FlipCardWithKeepAlive>
with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
#override
bool get wantKeepAlive => true;
}
You need to use a different key for each element of the list, I used a Map in this case.
I also wrapped the FlipCard with a custom FlipCardWithKeepAlive stateful widget that uses AutomaticKeepAliveClientMixin to keep alive the FlipCard while scrolling.
Edit: I updated the code so when you flip one card, the previous card flipped gets flipped back. Basically you need to save the last flipped card and when a new one is flipped, flip the last one and put the new one as last flipped.
The code will make both cards flip at the same time, if you want one card to wait the other use onFlipDone() instead of onFlip(), like this:
onFlipDone: (isFront) {
bool isFlipped = !isFront;
if (isFlipped && lastFlipped != thisCard) {
lastFlipped?.currentState?.toggleCard();
lastFlipped = thisCard;
}
}

Flutter How to remove overscroll effect from ListView [duplicate]

By default, flutter adds a glowing effect on ListView/GridView/... to overscrolls on android phones
I would like to remove this effect entirely or on one specific scrollable.
I know that I can change ScrollPhysics to change between Bounce/Clamp. But this doesn't actually remove the glow effect.
What can I do ?
The glow effect comes from GlowingOverscrollIndicator added by ScrollBehavior
To remove this effect, you need to specify a custom ScrollBehavior. For that, simply wrap any given part of your application into a ScrollConfiguration with the desired ScrollBehavior.
The following ScrollBehavior will remove the glow effect entirely :
class MyBehavior extends ScrollBehavior {
#override
Widget buildOverscrollIndicator(
BuildContext context, Widget child, ScrollableDetails details) {
return child;
}
}
To remove the glow on the whole application, you can add it right under MaterialApp :
MaterialApp(
builder: (context, child) {
return ScrollConfiguration(
behavior: MyBehavior(),
child: child,
);
},
home: new MyHomePage(),
);
To remove it on a specific ListView, instead wrap only the desired ListView :
ScrollConfiguration(
behavior: MyBehavior(),
child: ListView(
...
),
)
This is also valid if you want to change the effect. Like adding a fade when reaching borders of the scroll view.
The glow will disappear by changing the ListView's physics property to BouncingScrollPhysics to imitate the List behavior on iOS.
ListView.builder(
physics: BouncingScrollPhysics(),
}
The above solution did not work for me. I did this from another solution.
Wrap it with this widget to remove the shadow completely:
NotificationListener<OverscrollIndicatorNotification>(
onNotification: (overscroll) {
overscroll.disallowGlow();
},
child: new ListView.builder(
//Your stuff here.
),
),
You can try BouncingScrollPhysics with all list or grid or scrollview:
//ScrollView:
SingleChildScrollView(
physics: BouncingScrollPhysics(),
)
//For ListView:
ListView.builder(
physics: BouncingScrollPhysics(),
}
//GridView
GridView.Builder(
physics: BouncingScrollPhysics(),
)
You can wrap your SingleChildScrollView or ListView.
NotificationListener<OverscrollIndicatorNotification>(
onNotification: (OverscrollIndicatorNotification overscroll) {
overscroll.disallowGlow();
return;
},
child: SingleChildScrollView()
)
Update on 2021
as buildViewportChrome is deprecated on March `21, we may have new way to implement this
A. Working Solution
class MyCustomScrollBehavior extends MaterialScrollBehavior {
#override
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
return child;
}
}
class MainApp extends StatelessWidget {
const MainApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
scrollBehavior: MyCustomScrollBehavior(),
title: 'App Title',
home: HomeUI(),
);
}
}
B. Explanation
By default, Flutter wraps any child widget into GlowingOverscrollIndicator as below code.
#override
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
switch (getPlatform(context)) {
case TargetPlatform.iOS:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
return child;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return GlowingOverscrollIndicator(
axisDirection: details.direction,
color: Theme.of(context).colorScheme.secondary,
child: child, // < ---------- our Child Widget is wrapped by Glowing Indicator
);
}
}
So we can easily override it, by directly return child without wrapping it to GlowingOverscrollIndicator
class MyCustomScrollBehavior extends MaterialScrollBehavior {
#override
Widget buildOverscrollIndicator(
BuildContext context, Widget child, ScrollableDetails details) {
return child;
}
}
You don't need to build your own custom ScrollBehavior class. Instead, just wrap your scrollable widget in a ScrollConfiguration widget and set the behavior property to:
const ScrollBehavior().copyWith(overscroll: false).
Full code example:
ScrollConfiguration(
behavior: const ScrollBehavior().copyWith(overscroll: false),
child: PageView(
physics: const PageScrollPhysics(),
controller: model.pageController,
children: [
PageOne(),
PageTwo(),
PageThree(),
PageFour(),
],
),
),
try this work for me mybe work for you to
ScrollConfiguration(
behavior: new ScrollBehavior()..buildViewportChrome(context, null, AxisDirection.down),
child: SingleChildScrollView()
);
You can also try
SingleChildScrollView(
physics: ClampingScrollPhysics(),
)
If you migrated to null safety, you might get issues with the behavior. You can use this method that works with null safety:
NotificationListener<OverscrollIndicatorNotification>(
onNotification: (OverscrollIndicatorNotification? overscroll) {
overscroll!.disallowGlow();
return true;
},
child: child,
),
The currently accepted answer is outdated in the current version of Flutter.
Scroll behavior's ScrollBehavior.copyWith() method has an overscroll flag which can be set to false to avoid having to create your own ScrollBehavior class.
For example:
ScrollConfiguration(
behavior: MaterialScrollBehavior().copyWith(overscroll: false),
child : someScrollableWidget
)
`
It isn't good practice to just change the scroll behavior, as you may lose the native scrolling feel when running your app on different devices.
I have used below one for Scroll body without Scroll glow effect
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ScrollConfiguration(
behavior: new ScrollBehavior()
..buildViewportChrome(context, null, AxisDirection.down),
child: SingleChildScrollView(
After Flutter 2.10 update Previous NotificationListener parameter code has been removed/deprecated.
New Code
NotificationListener<OverscrollIndicatorNotification>(
onNotification: (overscroll) {
overscroll.disallowIndicator(); //previous code overscroll.disallowGlow();
return true;
},
child: ListView(
padding: const EdgeInsets.symmetric(
horizontal: 15, vertical: 15),
scrollDirection: Axis.horizontal,
children: List.generate(
items.length,
(index) => Padding(
padding: const EdgeInsets.only(right: 15),
child: AspectRatio(
aspectRatio: 13 / 9,
child:
LayoutBuilder(builder: (context, boxcon) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
boxShadow: const [
BoxShadow(
color: Colors.black12,
spreadRadius: 5,
blurRadius: 12)
],
image: DecorationImage(
fit: BoxFit.cover,
image: NetworkImage(items[index])),
color: greengradientcolor,
),
);
}),
))),
),
),