SliverAppbar remains visible when CustomScrollView with ScrollController is scrolled - dart

Adding a ScrollController to function timelineList() causes the SliverAppBar to remain visible on scroll (when counter list is scrolled, SliverAppBar should hide). The issue goes away if the _scrollController is removed from the list (see timelineList function) but this gives rise to a new problem, I need to listen for when the scrollbar reaches bottom (to get more content).
See sample app below, copy/paste and run.
void main() => runApp(TestApp());
class TestApp extends StatelessWidget {
final _scrollController = new ScrollController();
TestApp(){
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
print('Get more data');
}
});
}
#override
Widget build(BuildContext context) {
TestBloc bloc = TestBloc();
bloc.fetchTestTimeline();
bloc.fetchTestAppBarTxt1();
bloc.fetchTestAppBarTxt2();
return MaterialApp(
home: new Scaffold(
backgroundColor: Colors.grey[200],
appBar: AppBar(
backgroundColor: Colors.blueGrey,
elevation: 0.0,
),
body: NestedScrollView(
headerSliverBuilder:
(BuildContext contrxt, bool innerBoxIsScrolled) {
return <Widget>[
buildSliverAppBar(context, bloc),
];
},
body: Column(
children: <Widget>[
timelineList(bloc),
],
)
)),
);
}
buildSliverAppBar(context, TestBloc bloc){
return SliverAppBar(
automaticallyImplyLeading: false,
backgroundColor: Colors.grey[400],
expandedHeight: 200.0,
floating: true,
snap: true,
flexibleSpace: FlexibleSpaceBar(
background: Column(
children: <Widget>[
Container(
padding: EdgeInsets.only(left: 2.0),
height: 200,
child: Column(
children: <Widget>[
StreamBuilder(
stream: bloc.testAppBarTxt1,
initialData: null,
builder: (BuildContext context,
AsyncSnapshot<String> snapshot) {
if (snapshot.data == null)
return buildProgressIndicator(true);
return Expanded(
child: Text('${snapshot.data}'));
}),
StreamBuilder(
stream: bloc.testAppBarTxt2,
initialData: null,
builder: (BuildContext context,
AsyncSnapshot<String> snapshot) {
if (snapshot.data == null)
return buildProgressIndicator(true);
return Expanded(
child: Text('${snapshot.data}'));
}),
],
),
)
],
),
));
}
timelineList(TestBloc bloc) {
return StreamBuilder(
stream: bloc.getTestTimeline,
initialData: null,
builder: (BuildContext context, AsyncSnapshot<List<int>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Expanded(child: buildProgressIndicator(true));
}
List<int> val = snapshot.data;
if (val.isNotEmpty) {
addToTimelineList(val, bloc);
return Expanded(
child: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate(new List<Widget>.generate(bloc.listTest.length, (int index) {
if (index == bloc.listTest.length) {
return buildProgressIndicator(bloc.isPerformingRequest);
} else {
return bloc.listTest[index];
}
})
))
],
),
);
}
});
}
void addToTimelineList(List<int> list, TestBloc bloc) {
for (var val in list) {
bloc.listTest.add(Text('$val'));
}
}
}
Widget buildProgressIndicator(showIndicator) {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: showIndicator ? 1.0 : 0.0,
child: Container(
width: 10.0,
height: 10.0,
child: new CircularProgressIndicator(
)),
),
),
);
}
class TestBloc {
String appbar1Val;
String appbar2Val;
List<Text> listTest = new List<Text>();
bool isPerformingRequest = false;
final _testAppBarText1 = BehaviorSubject<String>();
Observable<String> get testAppBarTxt1 => _testAppBarText1.stream;
final _testAppBarText2 = BehaviorSubject<String>();
Observable<String> get testAppBarTxt2 => _testAppBarText2.stream;
final _testTimeline = PublishSubject<List<int>>();
Observable<List<int>> get getTestTimeline => _testTimeline.stream;
fetchTestTimeline() async {
List item = await Future.delayed(
Duration(seconds: 2), () => List<int>.generate(100, (i) => i));
_testTimeline.sink.add(item);
}
fetchTestAppBarTxt1() async {
appbar1Val = await Future.delayed(Duration(seconds: 2), () => "Text One");
_testAppBarText1.sink.add(appbar1Val);
}
fetchTestAppBarTxt2() async {
appbar2Val = await Future.delayed(Duration(seconds: 2), () => "Text Two");
_testAppBarText2.sink.add(appbar2Val);
}
dispose() {
_testAppBarText1.close();
_testAppBarText2.close();
_testTimeline.close();
}
}

It's possible to achive the same result with wrapping your list with a Notification Listener.
NotificationListener<ScrollNotification>(
onNotification: (sn) {
if (sn.metrics.pixels ==
sn.metrics.maxScrollExtent) {
print('Get more data');
}
},
child: CustomScrollView(...
Edit: Since my initial answer didn't cover the animateTo use case, I got it working by removing the outer NestedScrollView. Here is the modified example.
void main() => runApp(TestApp());
class TestApp extends StatelessWidget {
final _scrollController = new ScrollController();
TestApp() {
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
print('Get more data');
}
});
}
#override
Widget build(BuildContext context) {
TestBloc bloc = TestBloc();
bloc.fetchTestTimeline();
bloc.fetchTestAppBarTxt1();
bloc.fetchTestAppBarTxt2();
return MaterialApp(
home: new Scaffold(
backgroundColor: Colors.grey[200],
appBar: AppBar(
backgroundColor: Colors.blueGrey,
elevation: 0.0,
),
body: Column(
children: <Widget>[
timelineList(bloc),
],
),
),
);
}
buildSliverAppBar(context, TestBloc bloc) {
return SliverAppBar(
automaticallyImplyLeading: false,
backgroundColor: Colors.grey[400],
expandedHeight: 200.0,
floating: true,
snap: true,
flexibleSpace: FlexibleSpaceBar(
background: Column(
children: <Widget>[
Container(
padding: EdgeInsets.only(left: 2.0),
height: 200,
child: Column(
children: <Widget>[
StreamBuilder(
stream: bloc.testAppBarTxt1,
initialData: null,
builder: (BuildContext context,
AsyncSnapshot<String> snapshot) {
if (snapshot.data == null)
return buildProgressIndicator(true);
return Expanded(child: Text('${snapshot.data}'));
}),
StreamBuilder(
stream: bloc.testAppBarTxt2,
initialData: null,
builder: (BuildContext context,
AsyncSnapshot<String> snapshot) {
if (snapshot.data == null)
return buildProgressIndicator(true);
return Expanded(child: Text('${snapshot.data}'));
}),
],
),
)
],
),
));
}
timelineList(TestBloc bloc) {
return StreamBuilder(
stream: bloc.getTestTimeline,
initialData: null,
builder: (BuildContext context, AsyncSnapshot<List<int>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Expanded(child: buildProgressIndicator(true));
}
List<int> val = snapshot.data;
if (val.isNotEmpty) {
addToTimelineList(val, bloc);
return Expanded(
child: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
buildSliverAppBar(context, bloc),
SliverList(
delegate: SliverChildListDelegate(
new List<Widget>.generate(bloc.listTest.length,
(int index) {
if (index == bloc.listTest.length) {
return buildProgressIndicator(bloc.isPerformingRequest);
} else {
return bloc.listTest[index];
}
})))
],
),
);
}
});
}
void addToTimelineList(List<int> list, TestBloc bloc) {
for (var val in list) {
bloc.listTest.add(Text('$val'));
}
}
}
Widget buildProgressIndicator(showIndicator) {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: showIndicator ? 1.0 : 0.0,
child: Container(
width: 10.0, height: 10.0, child: new CircularProgressIndicator()),
),
),
);
}
class TestBloc {
String appbar1Val;
String appbar2Val;
List<Text> listTest = new List<Text>();
bool isPerformingRequest = false;
final _testAppBarText1 = BehaviorSubject<String>();
Observable<String> get testAppBarTxt1 => _testAppBarText1.stream;
final _testAppBarText2 = BehaviorSubject<String>();
Observable<String> get testAppBarTxt2 => _testAppBarText2.stream;
final _testTimeline = PublishSubject<List<int>>();
Observable<List<int>> get getTestTimeline => _testTimeline.stream;
fetchTestTimeline() async {
List item = await Future.delayed(
Duration(seconds: 2), () => List<int>.generate(100, (i) => i));
_testTimeline.sink.add(item);
}
fetchTestAppBarTxt1() async {
appbar1Val = await Future.delayed(Duration(seconds: 2), () => "Text One");
_testAppBarText1.sink.add(appbar1Val);
}
fetchTestAppBarTxt2() async {
appbar2Val = await Future.delayed(Duration(seconds: 2), () => "Text Two");
_testAppBarText2.sink.add(appbar2Val);
}
dispose() {
_testAppBarText1.close();
_testAppBarText2.close();
_testTimeline.close();
}
}

Related

Flutter - setState not updating a List inside a ListView.separated

I am saving a list of favorites in my game app, I can add or remove games from the list, but on the removeFavorite method, when I use setState in "games.remove(index)" the listview.separeted doesn't update. If close and open favorite screen, the list is updated but while I am at the favorite screen it doesn't update.
class _FavoriteScreenState extends State<FavoriteScreen> {
List<dynamic> favoriteList = [];
List<Game> games = [];
#override
void initState() {
loadFavorites();
super.initState();
}
#override
Widget build(BuildContext context) {
Widget _buildListView(Game game, int index){
return InkWell(
child: Container(
height: 80,
child: Row(
children: <Widget>[
Expanded(
flex: 1,
child: Container(
margin: EdgeInsets.only(left: 5),
child: Image.network(game.cover),
),
),
Expanded(
flex: 2,
child: Container(
child: Row(
children: <Widget>[
SizedBox(width: 15,),
Text(
game.title,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w300,
fontSize: 14
),
),
],
),
),
)
],
),
),
onLongPress: (){
_showDialog(index);
},
);
}
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
appBar: AppBar(
title: Text("Favorites"),
),
body: Container(
margin: EdgeInsets.fromLTRB(16, 16, 16, screenHeight > 720 ? 90 : 62),
child: ListView.separated(
separatorBuilder: (BuildContext context, int index) => Divider(color: Colors.black,),
itemCount: games.length,
itemBuilder: (context, index){
return _buildListView(games[index], index);
},
)
),
);
}
Future<File> getFile() async{
final directory = await getApplicationDocumentsDirectory();
return File("${directory.path}/favorites.json");
}
Future<String> readFavorite() async{
try{
// Le e retorna o arquivo como String
final file = await getFile();
return file.readAsString();
} catch (e){
return null;
}
}
void loadFavorites() {
readFavorite().then((data){
// Transforma o arquivo JSON numa List
favoriteList = json.decode(data);
if(favoriteList.length > 0 && favoriteList != null){
favoriteList.forEach((map){
Game game = Game(map["cover"], map["title"], map["description"], map["url"]);
setState(() {
games.add(game);
});
});
} else {
}
print(games.length);
});
}
Future<File> saveFile() async{
String data = json.encode(favoriteList);
final file = await getFile();
return file.writeAsString(data);
}
void _showDialog(int index){
showDialog(
context: context,
builder: (BuildContext context){
return AlertDialog(
content: Text("?",
style: TextStyle(fontSize: 18) ,),
actions: <Widget>[
FlatButton(
child: Text("YES"),
onPressed: (){
Navigator.of(context).pop();
removeFavorite(index);
}
),
FlatButton(
child: Text("NO"),
onPressed: (){
Navigator.of(context).pop();
}
),
],
);
}
);
}
void removeFavorite(int index){
favoriteList.forEach((m) {
Map<String, dynamic> map = m;
if (map.containsValue(games[index].title)) {
favoriteList.remove(m);
saveFile();
setState(() {
games.remove(index);
});
}
});
}
}
My bad, I replaced the remove with removeAt and it worked.
setState(() {
games.removeAt(index);
});

Maps showing blank screen when build in release mode

I am building an application where I need to use Flutter maps, everything work as expected, however if I make a build through Jankins for release mode then for some reason the maps on iOS displays blank. I have another page where the same widget is opened in a full page and there it works. My thoughts are that placing google maps in SingleChildScrolView causes the issue. Below I am attaching the source code.I have replaced the SingleChildScrolView with with list view where I pass all widgets as children but the effect is the same, the map is displayed in debug mode but when building through Jankins the map is blank , but on another page the map widget is working as expected.
Any help will be greatly appreciated as I am banging my head for hours.
Regards
class DashboardPage extends StatefulWidget {
final Model _model;
DashboardPage(this._model);
#override
_DashboardPageState createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage> {
List<Dossier> _dossiers = List();
_DashboardPageState();
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(shrinkWrap: true, children: <Widget>[
_header(StringResources.dashboardTitle),
_mapWidget(),
_header(StringResources.myFiles),
_listWidgets()
],),
);
}
Widget _mapWidget() {
return Container(
height: 300,
child: DossierMap(
compassEnabled: false,
model: widget._model,
onDossierMapViewCreated: _onMapWidgetCreated,
onDossierInfoWindowTap: _selectedMarker,
),
);
}
Widget _listWidgets() {
return StreamBuilder<List<Dossier>>(
initialData: [],
stream: widget._model.dossierService
.loadDossiers(widget._model.loginService.token)
.catchError((e) {
if (widget._model.loggedIn) {
widget._model.forcedServerLogout();
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) {
return LoginPage(
model: widget._model,
errorText: "",
onLoggedInCallback: (context) => Navigator.of(context)
.pushReplacement(MaterialPageRoute(
builder: (context) => DossiersPage(widget._model))),
);
},
));
}
}).asStream(),
builder: (_context, snapshot) {
if (snapshot.hasData) {
_dossiers = snapshot.data;
return snapshot.data.length != 0
? _buildList(snapshot.data)
: _noDossierContentWidget();
} else if (snapshot.hasError) {
return _progressIndicator();
} else {
return _progressIndicator();
}
});
}
Widget _header(String text) {
return new Container(
decoration: new BoxDecoration(color: Color(ColorResources.darkGray)),
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 0, 12),
child: Align(
alignment: Alignment.centerLeft,
child: Text(text,
overflow: TextOverflow.ellipsis,
style: new TextStyle(
fontWeight: FontWeight.w500,
fontSize: 17,
color: Colors.white))),
),
);
}
void _onMapWidgetCreated(dynamic controller) {
controller.setMarkers(_dossiers);
}
Widget _buildList(List<Dossier> dossierList) {
return Container(
color: Color(ColorResources.dividerColor),
height: 400,
child: ListView.separated(
padding: EdgeInsets.all(0.0),
separatorBuilder: (context, index) =>
Divider(color: Colors.grey.shade300, height: 1.5),
itemCount: dossierList != null ? dossierList.length : 0,
shrinkWrap: true,
physics: ScrollPhysics(),
itemBuilder: (BuildContext context, int index) => DossierListItem(
dossier: dossierList[index],
fromSearch: true,
widget: Icon(
Icons.keyboard_arrow_right,
size: 30,
color: Color(ColorResources.gray),
),
onListItemClickListener: () =>
onListItemClicked(dossierList[index]),
)));
}
void onListItemClicked(Dossier dossier) {
_selectedDossier(dossier);
}
Widget _progressIndicator() {
return Center(child: CircularProgressIndicator());
}
Widget _noDossierContentWidget() {
return Container(
color: Theme.of(context).cardColor,
child: ListTile(
title: Text(
StringResources.noOwnedFiles,
softWrap: true,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).primaryTextTheme.caption,
),
));
}
void _selectedMarker(DossierCluster cluster) async {
if (cluster.isCluster) {
Dossier selected = await showDialog(
context: context,
builder: (context) {
return AlertDialog(
contentPadding: EdgeInsets.fromLTRB(0.0, 20.0, 0.0, 24.0),
title: Center(child: Text(StringResources.selectFile)),
content: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children:
dossiersFromCluster(cluster, onDossierListMarkerClick),
),
),
actions: <Widget>[
FlatButton(
child: Text(StringResources.cancel),
onPressed: () => Navigator.pop(context),
)
],
);
});
if (selected != null) _selectedDossier(selected);
} else {
_selectedDossier(cluster.getFirst);
}
}
List<Widget> dossiersFromCluster(
DossierCluster cluster, Function onDossierListMarkerClick) {
List<Widget> widgets = List();
for (int i = 0; i < cluster.size; i++) {
Dossier dossier = cluster.get(i);
widgets.add(DossierListItem(
dossier: dossier,
fromSearch: false,
widget: Container(),
onListItemClickListener: () => onDossierListMarkerClick(dossier),
));
}
return widgets;
}
void _selectedDossier(Dossier dossier) {
widget._model.setDossier(dossier);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DossierDetailsPage(widget._model)),
);
}
void _openFullSize() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => FullScreenMapPage(widget._model, _dossiers),
),
);
}
void onDossierListMarkerClick(Dossier dossier) {
widget._model.setDossier(dossier);
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => DossierDetailsPage(widget._model)));
}
}

flutter , I Want Change Qty List From StreamController?

flutter , I Want Change Qty List From StreamController ?
I want action ontap
IconButton Change data
Text(poduct[index].qty.toString()),
from StreamController
I don't want to use setState(() {});
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(new MaterialApp(title: "Simple Material App", home: new MyHome()));
}
class MyHome extends StatefulWidget {
#override
MyHomeState createState() => new MyHomeState();
}
class Product {
String productName;
int qty;
Product({this.productName, this.qty});
}
class MyHomeState extends State<MyHome> {
List<Product> poduct = [Product(productName: "Nike",qty: 20),Product(productName: "Vans",qty: 30),];
var listPoduct = StreamController<List<Product>>();
#override
void initState() {
listPoduct.sink.add(poduct);
super.initState();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("test stream"),
),
body: Container(
padding: EdgeInsets.all(8.0),
child: StreamBuilder(
stream: listPoduct.stream,
builder: (context, snapshot) {
return ListView.builder(
itemCount: poduct.length,
padding: EdgeInsets.all(10),
itemBuilder: (BuildContext context, int index){
return Padding(
padding: const EdgeInsets.only(top: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(poduct[index].productName,style: TextStyle(fontSize: 24.0),),
new IconButton(icon: new Icon(Icons.remove), onPressed: (){
// How to Add ? listPoduct.sink ?
}),
Text(poduct[index].qty.toString()), /// <<< I Want Change Qty List Form StreamController
new IconButton(icon: new Icon(Icons.add), onPressed: (){
// How to Add ? listPoduct.sink ?
}),
Divider(),
],
),
);
},
);
}
),
));
}
}
I want action ontap
IconButton Change data
Text(poduct[index].qty.toString()),
from StreamController
I don't want to use setState(() {});
void main() {
runApp(new MaterialApp(title: "Simple Material App", home: new MyHome()));
}
class MyHome extends StatefulWidget {
#override
MyHomeState createState() => new MyHomeState();
}
class Product {
String productName;
int qty;
Product({this.productName, this.qty});
}
class MyHomeState extends State<MyHome> {
List<Product> poduct = [ // <<<<<<<< TYPO HERE
Product(productName: "Nike",qty: 20),
Product(productName: "Vans",qty: 30)];
var listPoduct = StreamController<List<Product>>();
#override
void initState() {
listPoduct.sink.add(poduct);
super.initState();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("test stream"),
),
body: Container(
padding: EdgeInsets.all(8.0),
child: StreamBuilder(
stream: listPoduct.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length, // <<<<<<<< . note that listbuilder relies on snapshot not on your poduct property
padding: EdgeInsets.all(10),
itemBuilder: (BuildContext context, int index){
return Padding(
padding: const EdgeInsets.only(top: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(poduct[index].productName,style: TextStyle(fontSize: 24.0),), // <<<<<<<< you can also use here the snapshot.data
new IconButton(icon: new Icon(Icons.remove), onPressed: () {
_update(index, -1);
}),
Text(poduct[index].qty.toString()), // <<<<<<<< you can also use here the snapshot.data
new IconButton(icon: new Icon(Icons.add), onPressed: (){
_update(index, 1);
}),
Divider(),
],
),
);
},
);
} else {
return Container()
}
}
),
));
}
_update(int index, int difference) {
for (int i = 0; i < poduct.length; i++ ) {
if (i == index) {
poduct[i] =
Product(productName: poduct[i].productName,
qty: poduct[i].qty + difference);
}
}
listPoduct.add(poduct);
}
}
some helpful links:
StreamBuilder-class
Example

How can I set the state in a widget that has been mounted in Flutter?

I' m new in Flutter. I made an app that has a catalogue with categories, subcategories and items (as you can see in photo). My widget tree is:
Catalogue(cart):{[Category-List, Subcategory-List, Item-List:{ItemRow(ListTile)}]}.
I'm facing the following problem: I have items in my cart and their quantity is shown on the catalogue (item rows). When I delete an item from the cart, or clear all items, I can't set the controller of the text field of the item Row to zero cause that widget (current item row) has been mounted. I use Scoped model to add, delete or update items in the cart. So, my problem is just visual. When I click on another category and then go to the previous, the controller has been set to zero correctly (cause the item rows recreated again with initstate()).
Is there any solution to my problem? Thanks!
App Image:
Catalogue.dart Code:
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import '../widgets/categories/categories_manager.dart';
import '../widgets/subcats/subcat_manager.dart';
import '../widgets/items/items_list.dart';
import '../scoped-models/main.dart';
import '../models/item.dart';
class CataloguePage extends StatefulWidget {
final String _langSelected;
CataloguePage(this._langSelected, this.model);
final MainModel model;
#override
State<StatefulWidget> createState() {
return _CataloguePageState();
}
}
class _CataloguePageState extends State<CataloguePage> {
Widget currentPage;
SubcatManager subcatPage;
bool _loadingProgress;
List<Item> _listCart;
final SlidableController slidableController = SlidableController();
#override
void initState() {
_listCart = widget.model.itemsInCart;
_loadingProgress = true;
widget.model
.fetchCategories(widget.model.serverAddress, widget.model.serverPort)
.then((bool success) {
if (success) {
setState(() {
widget.model
.fetchSubcats(widget.model.serverAddress, widget.model.serverPort,
widget.model.categories[0].catid)
.then((bool success2) {
if (success2) {
setState(() {
widget.model
.fetchItems(
widget.model.serverAddress,
widget.model.serverPort,
widget.model.categories[0].catid,
widget.model.subcats[0].subcatid)
.then((bool success3) {
if (success3) {
_loadingProgress = false;
}
});
});
}
});
});
} else {
showDialog(
barrierDismissible: true,
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('An error has occured.'),
content: Text('Connection with Server failed!'),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.popUntil(
context, (_) => !Navigator.canPop(context));
Navigator.pushReplacementNamed(context, '/');
},
child: Text('OK'),
)
],
);
});
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).primaryColor,
title: Text('Catalogue'),
actions: <Widget>[
Stack(
children: <Widget>[
IconButton(
icon: Icon(
Icons.shopping_cart,
size: 30.0,
),
onPressed: () {
showModalBottomSheet(
context: context,
builder: (BuildContext contex) {
return _buildCartList(_listCart);
},
);
},
),
widget.model.itemsInCart.length > 0
? CircleAvatar(
radius: 10.0,
child: Text(widget.model.itemsInCart.length.toString()),
)
: Container()
],
)
],
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (_loadingProgress) {
return Container(
color: Theme.of(context).backgroundColor,
child: Center(
child: Theme.of(context).platform == TargetPlatform.iOS
? CupertinoActivityIndicator(
radius: 20.0,
)
: CircularProgressIndicator(
strokeWidth: 3.0,
),
),
);
} else {
return Container(
padding: EdgeInsets.all(20),
color: Theme.of(context).backgroundColor,
child: Row(
children: <Widget>[
Flexible(
flex: 3,
child: Column(
children: [
Flexible(
child: CategoriesManager(widget.model),
),
],
),
),
widget.model.subcats[0].subcatid == 0
? Container()
: VerticalDivider(
color: widget.model.themeBrightness == 1
? Colors.white
: Colors.black,
),
widget.model.subcats[0].subcatid == 0
? Container()
: Flexible(
flex: 3,
child: Column(
children: [
Flexible(
child: SubcatManager(widget.model),
),
],
),
),
VerticalDivider(
color: widget.model.themeBrightness == 1
? Colors.white
: Colors.black,
),
Flexible(
flex: 4,
child: Column(
children: [
Text('Items'),
SizedBox(
height: 20.0,
),
Flexible(
child: ItemList(widget.model),
),
],
),
),
],
),
);
}
}
Widget _buildCartList(List<Item> listCart) {
Widget itemCartCards;
if (listCart.length > 0) {
itemCartCards = Padding(
padding: EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text('Selected Items'),
Text('Total Quantity: ' +
widget.model.cartTotalItems.toString()),
],
),
SizedBox(
height: 10.0,
),
Expanded(
child: ListView.separated(
separatorBuilder: (contex, index) => Divider(
color: widget.model.themeBrightness == 1
? Colors.white
: Colors.black,
),
itemBuilder: (BuildContext context, index) {
return Slidable(
key: Key(listCart[index].itemid),
controller: slidableController,
delegate: SlidableDrawerDelegate(),
actionExtentRatio: 0.25,
secondaryActions: <Widget>[
IconSlideAction(
icon: Icons.delete,
caption: 'Delete',
color: Colors.red,
onTap: () {
widget.model
.deleteItemFromCart(listCart[index].itemid);
},
)
],
child: ListTile(
title: Text(
listCart[index].itemperi,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
widget.model.showListItemsPrices
? Text(listCart[index].itemCount.toString() +
' x ' +
listCart[index].itemprice.toString() +
' €')
: Text(listCart[index].itemCount.toString()),
],
),
),
);
},
itemCount: listCart.length,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
child: Text('Clear Order'),
color: Colors.red,
onPressed: () {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
title: Text('Warning!'),
content:
Text('Are you sure you want to empty your cart?'),
actions: <Widget>[
FlatButton(
child: Text('Yes'),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
widget.model.deleteAllCartItems();
}),
FlatButton(
child: Text('No'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
},
),
SizedBox(
width: 20.0,
),
RaisedButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
child: Text('Confirm Order'),
color: Colors.green,
onPressed: () {
_buildJsonOrder();
},
),
],
),
],
),
);
} else {
itemCartCards = Container(
child: Center(
child: Text('Your cart is empty.'),
),
);
}
return itemCartCards;
}
void _buildJsonOrder() {
final List<dynamic> _listItems = [];
for (Item item in widget.model.itemsInCart) {
final Map<String, dynamic> itemData = {
'hallid': [widget.model.hallNumber],
'tableid': [widget.model.tableNumber],
'itemid': ['${item.itemid}'],
'itemperi': ['${item.itemperi}'],
'kind': [0],
'catid': [item.itemCatId],
'subcatid': [item.itemSubcatId],
'quantity': [item.itemCount],
'price': [item.itemprice]
};
_listItems.add(itemData);
}
final Map<String, dynamic> orderData = {
'hallid': [widget.model.hallNumber],
'tableid': [widget.model.tableNumber],
'typeofpos': ['4'],
'posid': [600],
'userid': [widget.model.currentUserId],
'items': _listItems
};
widget.model
.sendOrder(
widget.model.serverAddress, widget.model.serverPort, orderData)
.then((bool success) {
if (success) {
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Success.'),
content: Text('Your order has been placed successfully!'),
actions: <Widget>[
FlatButton(
onPressed: () {
widget.model.deleteAllCartItems();
Navigator.pop(context);
Navigator.of(context).pop();
},
child: Text('OK'),
)
],
);
});
} else {
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('An error has occured.'),
content: Text('Something went wrong with your order.'),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('OK'),
)
],
);
});
}
});
}
}
ItemList.dart Code:
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import '../../models/item.dart';
import '../../scoped-models/main.dart';
import './item_row.dart';
class ItemList extends StatefulWidget {
final MainModel model;
ItemList(this.model);
#override
State<StatefulWidget> createState() {
return _ItemListState();
}
}
class _ItemListState extends State<ItemList> {
#override
void initState() {
super.initState();
}
Widget _buildItemList(List<Item> items) {
Widget itemCards;
if (items.length > 0) {
itemCards = ListView.separated(
separatorBuilder: (contex, index) => Divider(
color: widget.model.themeBrightness == 1
? Colors.white
: Colors.black,
),
itemBuilder: (BuildContext context, index) {
return ItemRow(widget.model, items[index]);
},
itemCount: items.length,
);
} else {
itemCards = Container();
}
return itemCards;
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: ScopedModelDescendant<MainModel>(
builder: (BuildContext context, Widget child, MainModel model) {
return _buildItemList(model.items);
},
),
),
],
);
}
}
ItemRow.dart Code:
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import '../../models/item.dart';
import '../../scoped-models/main.dart';
class ItemRow extends StatefulWidget {
final MainModel model;
final Item item;
ItemRow(this.model, this.item);
#override
State<StatefulWidget> createState() {
return _ItemRowState();
}
}
class _ItemRowState extends State<ItemRow> {
int _itemCount;
TextEditingController _itemCountController;
#override
void initState() {
setState(() {
_itemCount = widget.item.itemCount;
_itemCountController = TextEditingController(text: _itemCount.toString());
});
super.initState();
}
#override
void didUpdateWidget(ItemRow oldWidget) {
if (oldWidget.item.itemid != widget.item.itemid) {
setState(() {
_itemCount = widget.item.itemCount;
_itemCountController =
TextEditingController(text: _itemCount.toString());
});
}
super.didUpdateWidget(oldWidget);
}
#override
Widget build(BuildContext context) {
return ScopedModelDescendant<MainModel>(
builder: (BuildContext context, Widget child, MainModel model) {
return _buildItem(widget.item);
},
);
}
Widget _buildItem(Item item) {
return ListTile(
leading: CircleAvatar(
backgroundImage: item.itemimage == ''
? AssetImage('assets/noimage.png')
: NetworkImage(item.itemimage)),
title: Text(
item.itemperi,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
widget.model.showListItemsPrices
? Text(
item.itemprice.toString() + ' €',
)
: Container(),
widget.model.showListItemsCart ? VerticalDivider() : Container(),
widget.model.showListItemsCart
? _buildListItemCart(item)
: Container()
],
),
onTap: () {
if (widget.model.clickItems) {
Navigator.pushNamed(context, '/item/' + item.itemid);
}
},
);
}
Widget _buildListItemCart(Item item) {
return Container(
child: Row(
children: <Widget>[
GestureDetector(
onLongPress: () {
if (_itemCount != 0) {
setState(() {
_itemCount = 0;
_itemCountController =
TextEditingController(text: _itemCount.toString());
widget.model.deleteItemFromCart(item.itemid);
});
}
},
child: IconButton(
icon: Icon(Icons.remove),
onPressed: () {
if (_itemCount != 0) {
setState(() {
_itemCount--;
_itemCountController =
TextEditingController(text: _itemCount.toString());
if (_itemCount == 0) {
widget.model.deleteItemFromCart(item.itemid);
} else {
widget.model.updateItemCart(item.itemid, _itemCount);
}
});
}
},
),
),
Container(
width: 30.0,
child: TextField(
decoration: InputDecoration(border: InputBorder.none),
textAlign: TextAlign.center,
controller: _itemCountController,
keyboardType: TextInputType.numberWithOptions(),
onTap: () {
_itemCountController.selection = TextSelection(
baseOffset: 0,
extentOffset: _itemCountController.text.length);
},
onSubmitted: (value) {
if (value == 0.toString()) {
widget.model.deleteItemFromCart(item.itemid);
}
if (value != 0.toString() && _itemCount == 0) {
widget.model.addItemToCart(
item.itemid,
item.itemperi,
item.itemprice,
int.parse(value),
int.parse(widget.model.selectedCatid),
int.parse(widget.model.selectedSubcatId));
}
if (value != 0.toString() && _itemCount != 0) {
widget.model.updateItemCart(item.itemid, int.parse(value));
}
_itemCount = int.parse(value);
TextEditingController(text: _itemCount.toString());
},
),
),
IconButton(
icon: Icon(Icons.add),
onPressed: () {
setState(
() {
_itemCount++;
_itemCountController =
TextEditingController(text: _itemCount.toString());
if (_itemCount == 1) {
widget.model.addItemToCart(
item.itemid,
item.itemperi,
item.itemprice,
_itemCount,
int.parse(widget.model.selectedCatid),
int.parse(widget.model.selectedSubcatId));
} else {
widget.model.updateItemCart(item.itemid, _itemCount);
}
},
);
},
),
],
),
);
}
}
ScopedModelItems.dart Code:
import 'package:scoped_model/scoped_model.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../models/item.dart';
import '../scoped-models/main.dart';
mixin ItemsModel on Model {
MainModel model;
List<Item> _items = [];
List<Item> itemsEmpty = [];
int _cartTotalItems = 0;
List<Item> _itemsCartEmpty = [];
Item _itemDetails;
Item get itemDetails {
return _itemDetails;
}
List<Item> get items {
return List.of(_items);
}
List<Item> get itemsInCart {
return _itemsCartEmpty;
}
int get cartTotalItems {
if (_cartTotalItems == null) {
_cartTotalItems = 0;
}
return _cartTotalItems;
}
void initState() {
_items.forEach(_addItem);
}
void _addItem(Item item) {
itemsEmpty.add(item);
}
void addItemToCart(String itemid, String itemperi, num itemprice,
int itemQuantity, int itemCatid, int itemSubcatid) {
Item item = Item(
itemid: itemid,
itemperi: itemperi,
itemprice: itemprice,
itemCount: itemQuantity,
itemCatId: itemCatid,
itemSubcatId: itemSubcatid);
_itemsCartEmpty.add(item);
_cartTotalItems += itemQuantity;
notifyListeners();
}
void updateItemCart(String itemid, int itemQuantity) {
_itemsCartEmpty.forEach((item) {
if (item.itemid == itemid) {
_cartTotalItems -= item.itemCount;
item.itemCount = itemQuantity;
_cartTotalItems += itemQuantity;
}
});
notifyListeners();
}
void deleteItemFromCart(String itemid) {
_itemsCartEmpty.forEach((item) {
if (item.itemid == itemid) {
_cartTotalItems -= item.itemCount;
}
});
_itemsCartEmpty.removeWhere((item) => item.itemid == itemid);
notifyListeners();
}
void deleteAllCartItems() {
_itemsCartEmpty.removeRange(0, _itemsCartEmpty.length);
_cartTotalItems = 0;
notifyListeners();
}
Future<bool> fetchItems(String serverAddress, String serverPort,
dynamic catid, dynamic subcatid) {
return http.get(
'http://$serverAddress:$serverPort/cats/$catid/subcats/$subcatid/items/GR',
headers: {'Accept': 'application/json'}).then((http.Response response) {
if (response.statusCode == 200 || response.statusCode == 201) {
final List<Item> fetchedItemList = [];
final List<dynamic> itemListData = json.decode(response.body);
itemListData.forEach((dynamic itemData) {
String imageData = '', periData = '';
if (itemData['item_image_path'] != '') {
imageData = 'http://$serverAddress:$serverPort/photos/' +
itemData['item_image_path'];
}
if (itemData['item_webperi'] == '') {
periData = itemData['item_peri'];
} else {
periData = itemData['item_webperi'];
}
final Item item = Item(
itemid: itemData['item_id'],
itemperi: periData,
itemimage: imageData,
itemprice: itemData['item_price'],
itemCount: 0);
if (_itemsCartEmpty.isNotEmpty) {
for (Item itemCart in _itemsCartEmpty) {
if (itemCart.itemid == item.itemid) {
item.itemCount = itemCart.itemCount;
}
}
}
fetchedItemList.add(item);
});
_items = fetchedItemList;
notifyListeners();
return true;
} else {
return false;
}
}).catchError((error) {
print(error);
notifyListeners();
return false;
});
}
}
Since there's no code I'll just list the common errors that cause this when using scoped model:
1. Not calling notify
You have to call notifyListeners() in your scoped model to update the UI that's using your porperties.
2. Not telling model to rebuild
If you're not using a ScopedModelDescendent widget and instead you're getting your model using
appModel = ScopedModel.of<AppModel>(context);
change your code to
appModel = ScopedModel.of<AppModel>(context, rebuildOnChange: true);
If you're doing both of these then please post your code for your UI and scoped model so that it's easier to offer our help.

Flutter working with Data better

I have a FutureBuilder that gets DISTINCT dates from a local sqlite DB, then I take each date and get the messages for those dates to put them in the widget, this works fine, until you want to listen realtime to a stream or poll for new messages which rebuilds the widgets and flickers the page and then scrolls to the beginning each time. I am hoping to find a way to take all the data into some object or other widget and then group by date and order, etc.. This way I can listen to a stream for updated messages, etc..
Any help would be great, here is my code if it helps anyone see what I do, this is after I converted to Streambuilder, but same result.
new StreamBuilder(
initialData: myInitialData,
stream: msgstream,
builder: (BuildContext context, AsyncSnapshot<List<Map>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text('Waiting to start');
case ConnectionState.waiting:
return new Text('');
default:
if (snapshot.hasError) {
return new Text('Error: ${snapshot.error}');
} else {
myInitialData = snapshot.data;
return new RefreshIndicator(
child: new ListView.builder(
itemBuilder: (context, index) {
return new MyChatWidget(
datediv: snapshot.data[index]['msgdate'],
msgkey: snapshot.data[index]['msgkey'],
);
},
//itemBuilder: _itemBuilder,
controller: _scrollController,
reverse: true,
itemCount: snapshot.data.length,
),
onRefresh: _onRefresh
);
}
}
}),
This is the Widget that the StreamBuilder calls:
class MyChatWidget extends StatefulWidget {
MyChatWidget({Key key, this.datediv, this.msgkey}) : super(key: key);
final String datediv;
final String msgkey;
#override
_MyChatWidgetState createState() => new _MyChatWidgetState();
}
class _MyChatWidgetState extends State<MyChatWidget> {
List<Widget> messagelist;
int messagecount = 0;
var jsonCodec = const JsonCodec();
var mydate = '';
var _urlMessages = '';
PageStorageKey _key;
VideoPlayerController vcontroller;
//Future<http.Response> _responseFuture;
Future<List<Map>> _responseFuture;
List messList;
var mybytes;
File myimageview;
Image newimageview;
String imgStr;
String vidStr;
String vidimgstr;
bool submitting = false;
List<Map> myInitialData;
Stream<List<Map>> msgstream;
#override
void initState() {
super.initState();
if (new DateFormat.yMd().format(DateTime.parse(widget.datediv)) ==
new DateFormat.yMd().format(new DateTime.now())) {
mydate = 'Today';
} else {
mydate = new DateFormat.yMMMEd().format(DateTime.parse(widget.datediv));
}
DateChatMessage dcm =
new DateChatMessage(widget.msgkey, widget.datediv.toString());
var json = jsonCodec.encode(dcm);
_urlMessages =
'http://loop-dev.clinicalsoftworks.com/chat/messages/getbydate';
//_responseFuture = http.post(_urlMessages, body: json, headers: getAuthHeader());
_responseFuture =
ChatDB.instance.getMessagesByDate(widget.msgkey, widget.datediv);
msgstream = new Stream.fromFuture(_responseFuture);
//controller = new TabController(length: 4, vsync: this);
//_getMessages();
}
/*#override
void dispose() {
super.dispose();
if (vcontroller != null) {
vcontroller.dispose();
}
}*/
#override
Widget build(BuildContext context) {
_key = new PageStorageKey('${widget.datediv.toString()}');
return new Column(
children: <Widget>[
new Container(
child: new Text(
mydate,
textAlign: TextAlign.left,
style: new TextStyle(
color: Colors.grey,
fontWeight: FontWeight.bold,
),
),
alignment: Alignment.centerLeft,
padding: new EdgeInsets.only(left: 10.0),
),
new Container(
child: new Divider(
height: 5.0,
color: Colors.grey,
),
padding: new EdgeInsets.only(left: 10.0, right: 10.0),
),
/**/
new StreamBuilder(
initialData: myInitialData,
stream: msgstream,
builder: (BuildContext context, AsyncSnapshot<List<Map>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text('Waiting to start');
case ConnectionState.waiting:
return new Text('');
default:
myInitialData = snapshot.data;
List<dynamic> json = snapshot.data;
messagelist = [];
json.forEach((element) {
DateTime submitdate =
DateTime.parse(element['submitdate']).toLocal();
String myvideo = (element['chatvideo']);
String myimage = element['chatimage'];
String myvideoimage = element['chatvideoimage'];
File imgfile;
File vidfile;
File vidimgfile;
bool vidInit = false;
Future<Null> _launched;
String localAssetPath;
String localVideoPath;
String mymessage = element['message'].replaceAll("[\u2018\u2019]", "'");
//print('MYDATE: '+submitdate.toString());
_checkFile(File file) async {
var checkfile = await file.exists();
print('VIDEXISTS: '+checkfile.toString());
}
Future<Null> _launchVideo(String url, bool isLocal) async {
if (await canLaunchVideo(url, isLocal)) {
await launchVideo(url, isLocal);
} else {
throw 'Could not launch $url';
}
}
void _launchLocal() =>
setState(() => _launched = _launchVideo(localVideoPath, true)
);
Widget _showVideo() {
/*return new Flexible(
child: new vplayer.VideoCard(
controller: vcontroller,
title: element['referralname'],
subtitle: 'video',
),
);*/
return new Flexible(
child: new Card(
child: new Column(
children: <Widget>[
new ListTile(subtitle: new Text('Video'), title: new Text(element['referralname']),),
new GestureDetector(
onTap: _launchLocal,
child: new Image.file(
vidimgfile,
width: 150.0,
),
),
],
),
)
);
}
_initVideo() {
setState(() {vidInit = true;});
}
_onError() {
print('VIDEO INIT ERROR');
}
if (myimage != "") {
imgStr = element['chatimage'];
imgfile = new File(imgStr);
}
if (myvideo != "") {
vidStr = element['chatvideo'];
vidimgstr = element['chatvideoimage'];
vidimgfile = new File(vidimgstr);
//vidfile = new File(vidStr);
//_checkFile(vidfile);
//print('vidfile: '+vidfile.path);
localVideoPath = '$vidStr';
//print('LOCALVIDEO: '+localVideoPath);
//vcontroller = new VideoPlayerController('file://$vidStr')..initialize();
}
_showLgPic() {
Route route = new MaterialPageRoute(
settings: new RouteSettings(name: "/ShowPic"),
builder: (BuildContext context) => new ShowPic(
image: imgfile,
),
);
Navigator.of(context).push(route);
}
Widget _showGraphic() {
Widget mywidget;
if (myimage != "") {
mywidget = new GestureDetector(
child: new Image.file(
imgfile,
width: 300.0,
),
onTap: _showLgPic,
);
} else if (myvideo != "") {
mywidget = _showVideo();
} else {
mywidget = new Container();
}
return mywidget;
}
messagelist.add(
new Container(
//width: 300.0,
padding: new EdgeInsets.all(10.0),
child: new Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Container(
padding: new EdgeInsets.only(bottom: 5.0),
child: new Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new CircleAvatar(
child: new Text(
element['sendname'][0],
style: new TextStyle(fontSize: 15.0),
),
radius: 12.0,
),
new Text(' '),
new Text(
element['sendname'],
style: new TextStyle(
fontSize: 15.0,
fontWeight: FontWeight.bold),
),
new Text(' '),
new Text(
new DateFormat.Hm().format(submitdate),
style: new TextStyle(
color: Colors.grey, fontSize: 12.0),
),
],
),
),
new Row(
children: <Widget>[
new Text(' '),
new Flexible(
child: new Text(mymessage),
)
],
),
new Container(
width: 150.0,
child: new Row(
children: <Widget>[
new Text(' '),
_showGraphic()
/*myimage != ""
? new GestureDetector(
child: new Image.file(
imgfile,
width: 300.0,
),
onTap: _showLgPic,
)
: myvideo != "" ? _showVideo() : new Container(),*/
],
)),
],
),
),
);
});
return new Column(children: messagelist);
}
},
)
],
);
}
}
Thanks for any assistance
which rebuilds the widgets and flickers the page and then scrolls to the beginning each time
To solve problem with scrolling try ScrollController. Create your own, keep it between updates and inject into List you created.
To solve flickering you could use Key for List widgets. Key should be unique identifier of message, e.g. msgkey
This example how to keep scrolloffset works for me
class SomeWidget extends StatefulWidget {
#override
_SomeWidgetState createState() => new _SomeWidgetState();
}
class _SomeWidgetState extends State<SomeWidget> {
ScrollController _scrollController;
int _count;
#override
void initState() {
super.initState();
_count = 10;
_scrollController = new ScrollController();
}
void _add() {
setState(() => _count += 5);
}
#override
Widget build(BuildContext context) {
final _titles = new List<String>.generate(_count, (i) => 'Title ${i}');
return new Scaffold(
appBar: new AppBar(
title: new Text("Demo"),
actions: <Widget>[
new IconButton(icon: new Icon(Icons.add), onPressed: _add)
],
),
body: new ListView.builder(
controller: _scrollController,
itemCount: _titles.length,
itemBuilder: (context, index) => new ListTile(
title: new Text(_titles[index]),
),
),
);
}
}

Resources