I am working on app that displays products. The app has 4 blocks in a listview. Each block showcases the product image with its information, and can be scrolled horizontally to get more data.
I have following sample code
class Home extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return HomeState();
}
}
class HomeState extends State<Home> {
//------- other codes
#override
void initState() {
bloc.fetchNewProperties(0);
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
drawer: CustomDrawer(),
backgroundColor: Constants.scaffoldColor,
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
expandedHeight: 216,
floating: false,
titleSpacing: 0.0,
pinned: true,
flexibleSpace: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Constants.gradientStart, Constants.gradientEnd]),
),
child: FlexibleSpaceBar(
centerTitle: true,
background: Swiper(
itemBuilder: (BuildContext context, int index) {
return new Image.asset(
tempBannerImages[index],
fit: BoxFit.fill,
);
},
itemCount: 2,
pagination: new SwiperPagination(),
autoplay: true,
autoplayDelay: 8000,
autoplayDisableOnInteraction: true,
),
),
),
),
];
},
body: ListView(
padding: EdgeInsets.only(
left: size.getSizePx(10), right: size.getSizePx(10)),
children: <Widget>[
newProductsBlock(),
newProductsBlock(),
newProductsBlock(),
newProductsBlock(),
newProductsBlock(), //have added widget multiple times to make stuff scrollable
],
),
),
);
}
Widget newProductsBlock() {
print("---This gets called just once---");
return Column(
children: <Widget>[
Text("New Products"),
Container(
height: 230,
child: StreamBuilder(
stream: bloc.allNewProps,
builder: (context, AsyncSnapshot<ProductsModel> snapshot) {
print("--this gets called multiple times.---");
if (snapshot.hasData) {
return buildNewPropListSwipe(snapshot);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return showCirularLoadingIndicator();
},
),
)
],
);
}
}
Problem
The products are loaded successfully in these blocks. But when I scrolled all the way down and back to top. The circularloading indicator gets visible, in the first block, instead of already loaded items. When I checked i found that Streambuilder is called multiple times as I scroll down and back again up.
Can anybody help me on this, i am quite new to flutter.
Thanks
For anybody who stumble upon same issue,
i changed Parent Listview to ScrollSingleChildView and wrapped widgets into Column and it seems to get fixed now
Related
can i implement my own card swip animation ?
what i want is something similar to this package here. but the slides should shown from the top like so: (and without a loop)
my code:
final controller = SwiperController();
List<Widget> _pages = [
Container(color: Colors.blue),
Container(color: Colors.black),
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: new Swiper(
loop: false,
controller: SwiperController(),
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: _pages[index],
);
},
itemCount: _pages.length,
itemWidth: 400.0,
itemHeight: 700.0,
layout: SwiperLayout.TINDER,
),
);
}
}
can you suggest me a way to reach that and thanks.
My editor warns me about this:
The library 'package:flutter_swiper/flutter_swiper.dart' is legacy, and shouldn't be imported into a null safe library.
So I guess you should search for a up-to-date, null-safe package which does the same.
And for the vertical layout that you want to achieve... Can't you just rotate the whole swiper widget by 90 degrees?
Like so:
#override
Widget build(BuildContext context) {
return Scaffold(
body: RotatedBox(
quarterTurns: 1,
child: Swiper(
loop: false,
controller: SwiperController(),
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: _pages[index],
);
},
itemCount: _pages.length,
itemWidth: 400.0,
itemHeight: 700.0,
layout: SwiperLayout.TINDER,
),
),
);
}
When i select TextField()(means keyboard open) my widget tree rebuilt.
I know the reason for this is because when the keyboard is opened, the screen sizes change and the whole class is rebuilt (both StateFull and Stateless widgets). And this is normal.
And so the AnimatedList() in my class is rebuilt. I do not want it to be rebuilt (because list indexes and list items in it change). How can I prevent this AnimatedList() Widget rebuilt? Or is it possible to prevent the class from being rebuilt when the keyboard is opened in general?
Please Help Me
Here my code:
class CommentPage extends StatelessWidget {
final String activityname;
final String postid;
final String usernameuid;
const CommentPage({
Key key,
#required this.activityname,
#required this.postid,
#required this.usernameuid,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return SafeArea(
child: Container(
height: 400,
child: Scaffold(
resizeToAvoidBottomInset: false,
resizeToAvoidBottomPadding: false,
appBar: AppBar(
title: Text('Coments'),
centerTitle: true,
),
body: Container(
height: 350,
width: 300,
child: ChangeNotifierProvider(
create: (context) => CommentLikeController(),
builder: (context, child) => FutureBuilder<List<Comment>>(
future:
Provider.of<CommentLikeController>(context, listen: false)
.initial(),
builder: (context, snapshot) {
if (snapshot.hasData)
return Stack(children: [
Positioned(
bottom: 50,
child: Container(
height: 350,
width: 300,
child: AnimatedList(
shrinkWrap: true,
reverse: true,
key: Provider.of<CommentLikeController>(context)
.listkey,
initialItemCount: snapshot.data.length,
itemBuilder: (context, index, animation) {
return ListItem(
index: index,
postid: postid,
activityname: activityname,
usernameuid: usernameuid,
);
},
),
),
),
Positioned(
bottom: 0,
left: 0,
child: Container(
height: 50,
width: 350,
child: TextField(),
),
),
]);
else
return LinearProgressIndicator();
}),
),
),
),
),
);
}
}
Create another StatefulWidget and place your TextField widget inside it. Create the text field controller in your CommentPage and pass it as parameter to your new created StatefulWidget and use it as the the TextField's controller. The keyboard effect will only impact the new created StatefulWidget
I have a fairly simple flutter app. It has a chat feature.
However, I have a problem with the chat feature.
It's made up of a widget does Scaffold and in it SingleChildScrollView - which has a message-list (container) and input-area (container). Code is attached.
Problem is: if I click on the input box, the keyboard opens and it pushes the message-list.
Pushing the message-list is an acceptable thing if you are already at the bottom of the message-list.
However, if the user scrolled up and saw some old messages, I don't want the message-list widget to be pushed up.
Also, I don't want the message-list to be pushed up if I have only a handful of messages (because that just makes the messages disappear when keyboard opens, and then I need to go and scroll to the messages that have been pushed [user is left with 0 visible messages until they scroll]).
I tried different approaches - like
resizeToAvoidBottomInset: false
But nothing seems to work for me, and this seems like it should be a straightforward behavior (for example, whatsapp act like the desired behavior).
My only option I fear is to listen to keyboard opening event, but I was hoping for a more elegant solution.
Here's my code:
#override
Widget build(BuildContext context) {
height = MediaQuery.of(context).size.height;
width = MediaQuery.of(context).size.width;
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: <Widget>[
SizedBox(height: height * 0.1),
buildMessageList(), // container
buildInputArea(context), // container
],
),
),
);
Widget buildInputArea(BuildContext context) {
return Container(
height: height * 0.1,
width: width,
child: Row(
children: <Widget>[
buildChatInput(),
buildSendButton(context),
],
),
);
}
Widget buildMessageList() {
return Container(
height: height * 0.8,
width: width,
child: ListView.builder(
controller: scrollController,
itemCount: messages.length,
itemBuilder: (BuildContext context, int index) {
return buildSingleMessage(index);
},
),
);
}
This seems to work for me:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ScrollController _controller = ScrollController();
#override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
body: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
buildMessageList(),
buildInputArea(context),
],
),
),
);
}
Widget buildInputArea(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: TextField(),
),
RaisedButton(
onPressed: null,
child: Icon(Icons.send),
),
],
);
}
Widget buildMessageList() {
return Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: 50,
controller: _controller,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: EdgeInsets.all(10),
child: Container(
color: Colors.red,
height: 20,
child: Text(index.toString()),
),
);
},
),
);
}
}
I think the problem is that you are using fixed sizes for all widgets. In this case it is better to use Expanded for the ListView and removing the SingleChildScrollView. That way the whole Column won't scroll, but only the ListView.
Try to use Stack:
#override
Widget build(BuildContext context) {
height = MediaQuery.of(context).size.height;
width = MediaQuery.of(context).size.width;
return Scaffold(
body: Stack(
children: <Widget>[
SingleChildScrollView(
child: Column(
children: <Widget>[
SizedBox(height: height * 0.1),
buildMessageList(),
],
),
),
Positioned(
bottom: 0.0,
child: buildInputArea(context),
),
],
),
);
}
Setting resizeToAvoidBottomInset property to false in your Scaffold should work.
You can use NotificationListener to listen to scroll notifications to detect that user is at the bottom of the message-list. If you are at the bottom you can then set resizeToAvoidBottomInset to true.
Something like this should work
final resizeToAvoidBottomInset = true;
_onScrollNotification (BuildContext context, ScrollNotification scrollNotification) {
if (scrollNotification is ScrollUpdateNotification) {
// detect scroll position here
// and set resizeToAvoidBottomInset to false if needed
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: this.resizeToAvoidBottomInset,
body: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
return _onScrollNotification(context, scrollNotification);
},
child: SingleChildScrollView(
child: Column(
children: <Widget>[
buildMessageList(), // container
buildInputArea(context), // container
],
),
),
),
);
}
this is technically already answered, and the answer is almost correct. However, I have found a better solution to this. Previously the author mentions that he wants to have a similar experience to WhatsApp. By using the previous solution, the listview would not be able to scrolldown to maxExtent when the sent button is pressed. To fix this I implemented Flex instead of Expanded, and use a singlechildscrollview for the input area
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ScrollController _controller = ScrollController();
TextEditingController _textcontroller=TextEditingController();
List<String> messages=[];
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
body: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: messages.length,
controller: _controller,
itemBuilder: (BuildContext context, int index) {
print("From listviewbuilder: ${messages[index]}");
return Padding(
padding: EdgeInsets.all(10),
child: Container(
color: Colors.red,
height: 20,
child: Text(messages[index])
),
);
},
),
),
SingleChildScrollView(
child: Row(
children: <Widget>[
Expanded(
child: TextField(controller: _textcontroller),
),
RaisedButton(
onPressed: (){
Timer(Duration(milliseconds: 100), () {
_controller.animateTo(
_controller.position.maxScrollExtent,
//scroll the listview to the very bottom everytime the user inputs a message
curve: Curves.easeOut,
duration: const Duration(milliseconds: 300),
);
});
setState(() {
messages.add(_textcontroller.text);
});
print(messages);
},
child: Icon(Icons.send),
),
],
),
),
],
),
),
);
}
}
It's better to use flex because expanded as the documentation says, expands over available space, whereas flex would resize to the appropriate proportion. This way if you are going for the "WhatsApp experience" in which the listview scrolls down once you sent a message. The listview would resize when the keyboard pops up and you will get to the bottom, instead of it not going fully to the bottom.
I have implemented a screen with the CustomScrollView, SliverAppBar and FlexibleSpaceBar like the following:
Now, I'm stuck trying to further expand the functionality by trying to replicate the following effect:
Expand image to fullscreen on scroll
Can something like this be done by using the slivers in Flutter?
Basically, I want the image in it's initial size when screen opens, but depending on scroll direction, it should animate -> contract/fade (keeping the list scrolling functionality) or expand to fullscreen (maybe to new route?).
Please help as I'm not sure in which direction I should go.
Here's the code for the above screen:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
static const double bottomNavigationBarHeight = 48;
#override
Widget build(BuildContext context) => MaterialApp(
debugShowCheckedModeBanner: false,
home: SliverPage(),
);
}
class SliverPage extends StatefulWidget {
#override
_SliverPageState createState() => _SliverPageState();
}
class _SliverPageState extends State<SliverPage> {
double appBarHeight = 0.0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
physics: AlwaysScrollableScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
centerTitle: true,
expandedHeight: MediaQuery.of(context).size.height * 0.4,
pinned: true,
flexibleSpace: LayoutBuilder(builder: (context, boxConstraints) {
appBarHeight = boxConstraints.biggest.height;
return FlexibleSpaceBar(
centerTitle: true,
title: AnimatedOpacity(
duration: Duration(milliseconds: 200),
opacity: appBarHeight < 80 + MediaQuery.of(context).padding.top ? 1 : 0,
child: Padding(padding: EdgeInsets.only(bottom: 2), child: Text("TEXT"))),
background: Image.network(
'https://images.pexels.com/photos/443356/pexels-photo-443356.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940',
fit: BoxFit.cover,
),
);
}),
),
SliverList(delegate: SliverChildListDelegate(_buildList(40))),
],
),
);
}
List _buildList(int count) {
List<Widget> listItems = List();
for (int i = 0; i < count; i++) {
listItems.add(
new Padding(padding: new EdgeInsets.all(20.0), child: new Text('Item ${i.toString()}', style: new TextStyle(fontSize: 25.0))));
}
return listItems;
}
}
use CustomScrollView with SliverPersistentHeader
child: LayoutBuilder(
builder: (context, constraints) {
return CustomScrollView(
controller: ScrollController(initialScrollOffset: constraints.maxHeight * 0.6),
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
delegate: Delegate(constraints.maxHeight),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(ctx, i) => Container(height: 100, color: i.isOdd? Colors.green : Colors.green[700]),
childCount: 12,
),
),
],
);
},
),
the Delegate class used by SliverPersistentHeader looks like:
class Delegate extends SliverPersistentHeaderDelegate {
final double _maxExtent;
Delegate(this._maxExtent);
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
var t = shrinkOffset / maxExtent;
return Material(
elevation: 4,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
Image.asset('images/bg.jpg', fit: BoxFit.cover,),
Opacity(
opacity: t,
child: Container(
color: Colors.deepPurple,
alignment: Alignment.bottomCenter,
child: Transform.scale(
scale: ui.lerpDouble(16, 1, t),
child: Text('scroll me down',
style: Theme.of(context).textTheme.headline5.copyWith(color: Colors.white)),
),
),
),
],
),
);
}
#override double get maxExtent => _maxExtent;
#override double get minExtent => 64;
#override bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => true;
}
I'm new guy using the flutter and when I code my app I met a problem that how could I set SliverAppBar collapsed status by default. To solve my confuse, I check the flutter's doc, unfortunately I couldn't find the solution, so I'm here.
Below is my code, if you know how to solve it, help me plz. Thanks guys.
class CollapsingBarLayoutState extends State<CollapsingBarLayout>
with TickerProviderStateMixin {
final List<ListItem> listData = [];
#override
Widget build(BuildContext context) {
for (int i = 0; i < 20; i++) {
listData.add(new ListItem("$i", Icons.cake));
}
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
expandedHeight: 200.0,
pinned: true,
centerTitle: false,
snap: true,
floating: true,
titleSpacing: 0,
backgroundColor: Colors.green,
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
title: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _buildBriefCalendarChildren(),
),
),
background: GridPage(
children: _buildCalendarPageChildren(),
column: 7,
row: 5,
)),
),
];
},
body: Center(
child: new ListView.builder(
itemBuilder: (BuildContext context, int index) {
return new ListItemWidget(listData[index]);
},
itemCount: listData.length,
),
),
),
);
}
}
There is a good plugin https://pub.dev/packages/after_layout I used in a situation like this.
Just add a mixin and then the only method you would need to implement would look something like this:
void afterFirstLayout(BuildContext context) {
_scrollController //scroll controller of your list view under sliver
.animateTo((expandedHeight) - collapsedHeight,
duration: new Duration(milliseconds: 1),//0 dont work as duration
curve: Curves.linear)
.then((_) {
setState(() {
expandedPercentage = 0; //just my custom tracking of expansion percentage
});
});
}
Edit - better solution:
Initialise a new ScrollController(initialScrollOffset: expandedHeight - collapsedHeight); and pass it as a controller to your NestedScrollView.