How to rebuild all grid items in flutter? - dart

I have a dashboard, represented by grid, that supposed to delete item on long press event (using flutter_bloc), but it deletes last item instead of selected. All debug prints show, that needed element actually removed from list, but view layer still keeps it.
My build function code:
Widget build(BuildContext context) {
double pyxelRatio = MediaQuery.of(context).devicePixelRatio;
double width = MediaQuery.of(context).size.width * pyxelRatio;
return BlocProvider(
bloc: _bloc,
child: BlocBuilder<Request, DataState>(
bloc: _bloc,
builder: (context, state) {
if (state is EmptyDataState) {
print("Uninit");
return Center(
child: CircularProgressIndicator(),
);
}
if (state is ErrorDataState) {
print("Error");
return Center(
child: Text('Something went wrong..'),
);
}
if (state is LoadedDataState) {
print("empty: ${state.contracts.isEmpty}");
if (state.contracts.isEmpty) {
return Center(
child: Text('Nothing here!'),
);
} else{
print("items count: ${state.contracts.length}");
print("-------");
for(int i = 0; i < state.contracts.length; i++){
if(state.contracts[i].isFavorite)print("fut:${state.contracts[i].name} id:${state.contracts[i].id}");
}
print("--------");
List<Widget> testList = new List<Widget>();
for(int i = 0; i < state.contracts.length; i++){
if(state.contracts[i].isFavorite) testList.add(
InkResponse(
enableFeedback: true,
onLongPress: (){
showShortToast();
DashBLOC dashBloc = BlocProvider.of<DashBLOC>(context);
dashBloc.dispatch(new UnfavRequest(state.contracts[i].id));
},
onTap: onTap,
child:DashboardCardWidget(state.contracts[i])
)
);
}
return GridView.count(
crossAxisCount: width >= 900 ? 2 : 1,
padding: const EdgeInsets.all(2.0),
children: testList
);
}
}
})
);
}
full class code and dashboard bloc
Looks like grid rebuilds itself, but don't rebuild its tiles.
How can I completely update grid widget with all its subwidgets?
p.s i've spent two days fixing it, pls help

I think you should use a GridView.builderconstructor to specify a build function which will update upon changes in the list of items, so when any update occur in your data the BlocBuilder will trigger the build function inside yourGridView.
I hope this example makes it more clear.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Test(),
);
}
}
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
List<int> testList = List<int>();
#override
void initState() {
for (int i = 0; i < 20; i++) {
testList.add(i);
}
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
//Here we can remove an item from the list and using setState
//or BlocBuilder will rebuild the grid with the new list data
onPressed: () => setState(() {testList.removeLast();})
),
body: GridView.builder(
// You must specify the items count of your grid
itemCount: testList.length,
// You must use the GridDelegate to specify row item count
// and spacing between items
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
childAspectRatio: 1.0,
crossAxisSpacing: 1.0,
mainAxisSpacing: 1.0,
),
// Here you can build your desired widget which will rebuild
// upon changes using setState or BlocBuilder
itemBuilder: (BuildContext context, int index) {
return Text(
testList[index].toString(),
textScaleFactor: 1.3,
);
},
),
);
}
}

Your code is always sending the last value of int i.
So instead of
for(int i = 0; i < state.contracts.length; i++){
if(state.contracts[i].isFavorite) testList.add(
InkResponse(
enableFeedback: true,
onLongPress: (){
showShortToast();
DashBLOC dashBloc = BlocProvider.of<DashBLOC>(context);
dashBloc.dispatch(new UnfavRequest(state.contracts[i].id));
},
onTap: onTap,
child:DashboardCardWidget(state.contracts[i])
)
);
Do
List<Widget> testList = new List<Widget>();
state.contracts.forEach((contract){
if(contract.isFavorite) testList.add(
InkResponse(
enableFeedback: true,
onLongPress: (){
showShortToast();
DashBLOC dashBloc = BlocProvider.of<DashBLOC>(context);
dashBloc.dispatch(new UnfavRequest(contract.id));
},
onTap: onTap,
child:DashboardCardWidget(contract)
)
));

Is it actually rebuilds? I'm just don't understand why you use the State with BLoC. Even if you use the State you should call the setState() method to update the widget with new data.
On my opinion the best solution to you will be to try to inherit your widget from StatelessWidget and call the dispatch(new UpdateRequest()); in the DashBLOC constructor.
Also always keep in mind this link about the bloc, there are lots of examples:
https://felangel.github.io/bloc/#/

just give children a key
return GridView.builder(
itemCount: children.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(3),
itemBuilder: (context, index) {
return Container(
key: ValueKey(children.length+index),
);
});

Related

How to animate list changes in Flutter

If I have a list (of ListTiles for example) that can be added to, removed from, and swapped, what would be the best way to animate these changes? I am using a reorderable list if that makes a difference. Right now my list has no animations, I just call setState when data is changed.
I think you need AnimatedList...I wrote an example.
You can only set Duration when you want to insert into the list or delete from the List and this is achieved by creating a GlobalKey of AnimatedListState...
I wrote an example code for inserting
class Pool extends StatelessWidget {
final keys = GlobalKey<AnimatedListState>();
var list = List.generate(3, (i) => "Hello $i");
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: AnimatedList(
key: keys,
initialItemCount: list.length,
itemBuilder: (context, index, animation) {
return SlideTransition(
position: animation.drive(
Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
.chain(CurveTween(curve: Curves.ease))),
child: ListTile(
title: Text(list[index]),
),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
list.insert(0, "NothingYay");
keys.currentState.insertItem(0, duration: Duration(seconds: 2));
},
),
);
}
}
I hope this helps you.
Check Flutter Widget Of The Week (AnimatedList)
You can also try using AutomaticAnimatedList
https://pub.dev/packages/automatic_animated_list
It automatically computes which items to animate for you.
class ItemsAnimatedList extends StatelessWidget {
final List<ItemModel> items;
const ItemsList({
Key key,
this.items,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return AutomaticAnimatedList<ItemModel>(
items: items,
insertDuration: Duration(seconds: 1),
removeDuration: Duration(seconds: 1),
keyingFunction: (ItemModel item) => Key(item.id),
itemBuilder:
(BuildContext context, ItemModel item, Animation<double> animation) {
return FadeTransition(
key: Key(item.id),
opacity: animation,
child: SizeTransition(
sizeFactor: CurvedAnimation(
parent: animation,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
),
child: ListTile(title: Text(item.name)),
),
);
},
);
}
}
I assume you are trying to implement the swipe feature in your list.
There is a gesture named Dissmisable
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
// MyApp is a StatefulWidget. This allows us to update the state of the
// Widget whenever an item is removed.
class MyApp extends StatefulWidget {
MyApp({Key key}) : super(key: key);
#override
MyAppState createState() {
return MyAppState();
}
}
class MyAppState extends State<MyApp> {
final items = List<String>.generate(3, (i) => "Item ${i + 1}");
#override
Widget build(BuildContext context) {
final title = 'Dismissing Items';
return MaterialApp(
title: title,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return Dismissible(
// Each Dismissible must contain a Key. Keys allow Flutter to
// uniquely identify Widgets.
key: Key(item),
// We also need to provide a function that tells our app
// what to do after an item has been swiped away.
onDismissed: (direction) {
// Remove the item from our data source.
setState(() {
items.removeAt(index);
});
// Then show a snackbar!
Scaffold.of(context)
.showSnackBar(SnackBar(content: Text("$item dismissed")));
},
// Show a red background as the item is swiped away
background: Container(color: Colors.red),
child: ListTile(title: Text('$item')),
);
},
),
),
);
}
}

Auto-Refresh UI when data change

I need to refresh my UI when data changes. I have a ListView to display Cards that contain my events, and these events are sorted with a datepicker. When I change the date with the datepicker I need to reload the page to display the correct pages.
I try to pass the datepicker data as a parameter of the ListView to sort the events in the ListView, I also tried to sort the data before ListView is built with a parameter containing the list of sorted data.
Widget of my HomePage class :
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
leading: Image.asset('assets/logo2.PNG', fit: BoxFit.contain),
title: Text(widget.title,style: TextStyle(fontFamily: 'IndieFlower',fontSize: 30,fontWeight: FontWeight.bold),),
actions: <Widget>[ // Add 3 lines from here...
new IconButton(icon: const Icon(Icons.account_circle, color: Color(0xFFf50057)), onPressed: _pushSaved, iconSize: 35,),
], // ... to here.
centerTitle: true,
backgroundColor: new Color(0xFF263238),
),
body: FutureBuilder<List<Event>>(
future: fetchPosts(http.Client()),
builder: (context, snapshot) {
//print(convertIntoMap(snapshot.data));
if (snapshot.hasError) print(snapshot.error);
return snapshot.hasData
? ListViewEvents(posts: sortEvents(snapshot.data), pickerDate: '${dobKey.currentState.dobDate} ' +dobKey.currentState.dobStrMonth +' ${dobKey.currentState.dobYear}')
: Center(child: CircularProgressIndicator(backgroundColor: Color(0xFFf50057),));
},
),
bottomNavigationBar : BottomAppBar(
child: Container(height: 100.0,
alignment: Alignment.topCenter,
child:
DatePicker(
key: dobKey,
setDate: _setDateOfBirth,
customItemColor: Color(0xFFf50057),
customGradient:
LinearGradient(begin: Alignment(-0.5, 2.8), colors: [
Color(0xFFf50057),
Color(0xFFffcece),
Color(0xFFf50057),
]),
),
),
),
);
}
}
This is my map:
List<Event> sortEvents(List<Event> data) {
List<Event> eventsSelected = new List<Event>();
for(var index = 0; index < data.length; index++){
if (data[index].date ==
//callback of datepicker
'${dobKey.currentState.dobYear}-${dobKey.currentState.month}-
${dobKey.currentState.dobDate}') {
eventsSelected.add(data[index]);
}
}
return eventsSelected;
}
And this is how I render my cards:
class ListViewEvents extends StatefulWidget {
ListViewEvents({Key key, this.posts, this.pickerDate}) : super(key: key);
final posts;
final pickerDate;
#override
_ListViewEventsState createState() => _ListViewEventsState();
}
class _ListViewEventsState extends State<ListViewEvents> with
SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) {
if(widget.posts.isEmpty) {
return Center(
child: Text(
'No events for this date'
),
);
}
return ListView.builder(
itemCount: widget.posts.length,
padding: const EdgeInsets.all(15.0),
itemBuilder: (context, index) {
return Center(
child: Text(
'Title : ${widget.posts[index].title}'
),
);
},
);
}
}
I actually have a system to display my events's Cards that works but it's not in real-time, I would like to refresh the UI when the data of the datepicker changes.
You need to call setState() when list data changes.
for(var index = 0; index < data.length; index++){
if (data[index].date ==
//callback of datepicker
'${dobKey.currentState.dobYear}-${dobKey.currentState.month}-
${dobKey.currentState.dobDate}') {
setState(() { eventsSelected.add(data[index]); } ); <--- add it here.
}
}
you can use setState() for this problem so setState() call when anything change in the screen

How to write a double back button pressed to exit app using flutter

I'm new to flutter, and I saw many android apps can exit when double press back button.
The first time press back button, app shows a toast"press again to exit app".
The following second press, app exits.
Of course, the time between two press must be not long.
How to do it in flutter?
This is an example of my code (I've used "fluttertoast" for showing toast message, you can use snackbar or alert or anything else)
DateTime currentBackPressTime;
#override
Widget build(BuildContext context) {
return Scaffold(
...
body: WillPopScope(child: getBody(), onWillPop: onWillPop),
);
}
Future<bool> onWillPop() {
DateTime now = DateTime.now();
if (currentBackPressTime == null ||
now.difference(currentBackPressTime) > Duration(seconds: 2)) {
currentBackPressTime = now;
Fluttertoast.showToast(msg: exit_warning);
return Future.value(false);
}
return Future.value(true);
}
You can try this package.
Inside a Scaffold that wraps all your Widgets, place the DoubleBackToCloseApp passing a SnackBar:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: DoubleBackToCloseApp(
child: Home(),
snackBar: const SnackBar(
content: Text('Tap back again to leave'),
),
),
),
);
}
}
The solution below must be considered deprecated because it causes a few issues that were tackled in the package mentioned. For instance, the app closes if the snack bar was dismissed by the user (see hcbpassos/double_back_to_close_app#2).
Old answer
You can also opt for a solution involving SnackBar. It's not as simple as Andrey Turkovsky's answer, but it's quite more elegant and you won't depend on a library.
class _FooState extends State<Foo> {
static const snackBarDuration = Duration(seconds: 3);
final snackBar = SnackBar(
content: Text('Press back again to leave'),
duration: snackBarDuration,
);
DateTime backButtonPressTime;
#override
Widget build(BuildContext context) {
return Scaffold(
// The BuildContext must be from one of the Scaffold's children.
body: Builder(
builder: (context) {
return WillPopScope(
onWillPop: () => handleWillPop(context),
child: Text('Place your child here'),
);
},
),
);
}
Future<bool> handleWillPop(BuildContext context) async {
final now = DateTime.now();
final backButtonHasNotBeenPressedOrSnackBarHasBeenClosed =
backButtonPressTime == null ||
now.difference(backButtonPressTime) > snackBarDuration;
if (backButtonHasNotBeenPressedOrSnackBarHasBeenClosed) {
backButtonPressTime = now;
Scaffold.of(context).showSnackBar(snackBar);
return false;
}
return true;
}
}
Unfortunately none of them worked for me, I have written one generic class (widget) to handle double tap exit. If someone is interested
class DoubleBackToCloseWidget extends StatefulWidget {
final Widget child; // Make Sure this child has a Scaffold widget as parent.
const DoubleBackToCloseWidget({
#required this.child,
});
#override
_DoubleBackToCloseWidgetState createState() =>
_DoubleBackToCloseWidgetState();
}
class _DoubleBackToCloseWidgetState extends State<DoubleBackToCloseWidget> {
int _lastTimeBackButtonWasTapped;
static const exitTimeInMillis = 2000;
bool get _isAndroid => Theme.of(context).platform == TargetPlatform.android;
#override
Widget build(BuildContext context) {
if (_isAndroid) {
return WillPopScope(
onWillPop: _handleWillPop,
child: widget.child,
);
} else {
return widget.child;
}
}
Future<bool> _handleWillPop() async {
final _currentTime = DateTime.now().millisecondsSinceEpoch;
if (_lastTimeBackButtonWasTapped != null &&
(_currentTime - _lastTimeBackButtonWasTapped) < exitTimeInMillis) {
Scaffold.of(context).removeCurrentSnackBar();
return true;
} else {
_lastTimeBackButtonWasTapped = DateTime.now().millisecondsSinceEpoch;
Scaffold.of(context).removeCurrentSnackBar();
Scaffold.of(context).showSnackBar(
_getExitSnackBar(context),
);
return false;
}
}
SnackBar _getExitSnackBar(
BuildContext context,
) {
return SnackBar(
content: Text(
'Press BACK again to exit!',
color: Colors.white,
),
backgroundColor: Colors.red,
duration: const Duration(
seconds: 2,
),
behavior: SnackBarBehavior.floating,
);
}
}
Use this class following way:
class Dashboard extends StatelessWidget {
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: DoubleBackToCloseWidget(
child: Container(
child: Column(
children: [
const Text('Hello there'),
const Text('Hello there again'),
],
),
),
),
),
);
}
}
The first time press back button, app shows a AlertDialog"press yes to exit app and press No to can't exit application".
This is an example of my code (I've used 'AlertDialog')
#override
Widget build(BuildContext context) {
return new WillPopScope(
onWillPop: _onBackPressed,
child: DefaultTabController(
initialIndex: _selectedIndex,
length: choices.length,
child: Scaffold(
appBar: AppBar(
),
),
),
);
}
Future<bool> _onBackPressed() {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Are you sure?'),
content: Text('Do you want to exit an App'),
actions: <Widget>[
FlatButton(
child: Text('No'),
onPressed: () {
Navigator.of(context).pop(false);
},
),
FlatButton(
child: Text('Yes'),
onPressed: () {
Navigator.of(context).pop(true);
},
)
],
);
},
) ?? false;
}
This is my answer. I used AlertDialog() to achieve this
#override
Widget build(BuildContext context) {
return new WillPopScope(
onWillPop: _onBackPressed,
child: Scaffold(
appBar: AppBar(),
body: Container(),
),
);
}
Future<bool> _onBackPressed() {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Confirm'),
content: Text('Do you want to exit the App'),
actions: <Widget>[
FlatButton(
child: Text('No'),
onPressed: () {
Navigator.of(context).pop(false); //Will not exit the App
},
),
FlatButton(
child: Text('Yes'),
onPressed: () {
Navigator.of(context).pop(true); //Will exit the App
},
)
],
);
},
) ?? false;
}
Simply use double_back_to_close_app library
https://pub.dev/packages/double_back_to_close_app
Add double_back_to_close_app under dependencies in pubspec.yaml file
dependencies:
double_back_to_close_app: ^1.2.0
Here example code
import 'package:double_back_to_close_app/double_back_to_close_app.dart';
import 'package:flutter/material.dart';
void main() => runApp(Example());
class Example extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: DoubleBackToCloseApp(
snackBar: const SnackBar(
content: Text('Tap back again to leave'),
),
child: Center(
child: OutlineButton(
child: const Text('Tap to simulate back'),
// ignore: invalid_use_of_protected_member
onPressed: WidgetsBinding.instance.handlePopRoute,
),
),
),
),
);
}
}
Just move your body contents to "DoubleBackToCloseApp's" child
The best solution without using a package use System
SystemChannels.platform.invokeMethod<void>('SystemNavigator.pop');
or
SystemNavigator.pop();
Full Code
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
class ExitApp extends StatefulWidget {
final Widget child;
const ExitApp({
Key? key,
required this.child,
}) : super(key: key);
#override
_ExitAppState createState() => _ExitAppState();
}
class _ExitAppState extends State<ExitApp> {
#override
build(BuildContext context) {
DateTime timeBackPressed = DateTime.now();
return WillPopScope(
child: widget.child,
onWillPop: () async {
final differeance = DateTime.now().difference(timeBackPressed);
timeBackPressed = DateTime.now();
if (differeance >= Duration(seconds: 2)) {
final String msg = 'Press the back button to exit';
Fluttertoast.showToast(
msg: msg,
);
return false;
} else {
Fluttertoast.cancel();
SystemNavigator.pop();
return true;
}
},
);
}
}
https://pub.dev/packages/flutter_close_app
This is my solution, it is very flexible and simple, does not depend on routing navigation, any page can close the App, such as my login page, and if it is Drawer and PageView, it can also flexibly support custom conditions, and does not need to rely on native method. The following functions are supported:
✅ Press back 2 times to close app
✅ Custom time interval
✅ Customize the prompt message
✅ Customize matching conditions
✅ Support Android
✅ One click to close app
✅ Support iOS
✅ Support MacOS
✅ Support Windows
✅ Support Linux
Easy to Use and Understand, double tap to exit;
Change the duration to 10000, and short toast message time;
import 'dart:io';
bool back = false;
int time = 0;
int duration = 1000;
Future<bool> willPop() async{
int now = DateTime.now().millisecondsSinceEpoch;
if(back && time >= now){
back = false;
exit(0);
}else{
time = DateTime.now().millisecondsSinceEpoch+ duration;
print("again tap");
back = true;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Press again the button to exit")));
}
return false;
}
return WillPopScope(
onWillPop: onWill,
child: Scaffold()
);
If you want a snackbar you should provide a scaffold key as it's related to a scaffold, so this key should make the trick of calling a snackbar outside of it's scaffold parent.
Here is a solution :
class Home extends StatelessWidget {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async{
DateTime initTime = DateTime.now();
popped +=1;
if(popped>=2) return true;
await _scaffoldKey.currentState.showSnackBar(
SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('Tap one more time to exit.',textAlign: TextAlign.center,),
duration: Duration(seconds: 2),
)).closed;
// if timer is > 2 seconds reset popped counter
if(DateTime.now().difference(initTime)>=Duration(seconds: 2)) {
popped = 0;
}
return false;
},
child: Scaffold(
key: _scaffoldKey,
appBar: AppBar(title : Text("Demo")),
body: Text("body")
);
)
}
This is my solution, you can change backPressTotal value to the number of pressed you want!
int backPressCounter = 0;
int backPressTotal = 2;
#override
Widget build(BuildContext context) {
return Scaffold(
...
body: WillPopScope(child: getBody(), onWillPop: onWillPop),
);
}
Future<bool> onWillPop() {
if (backPressCounter < 2) {
Fluttertoast.showToast(msg: "Press ${backPressTotal - backPressCounter} time to exit app");
backPressCounter++;
Future.delayed(Duration(seconds: 1, milliseconds: 500), () {
backPressCounter--;
});
return Future.value(false);
} else {
return Future.value(true);
}
}
If the condition is that the user presses only twice, you can use the first solution of course.
If you want to increase the number of times you click, you can use this solution. Where the user has to press 3 times within two seconds so he can get out
DateTime currentBackPressTime;
/// init counter of clicks
int pressCount=1;
then :
Future<bool> onWillPop() async {
DateTime now = DateTime.now();
/// here I check if number of clicks equal 3
if(pressCount!=3){
///should be assigned at the first click.
if(pressCount ==1 )
currentBackPressTime = now;
pressCount+=1;
return Future.value(false);
}else{
if (currentBackPressTime == null ||
now.difference(currentBackPressTime) > Duration(seconds: 2)) {
currentBackPressTime = now;
pressCount=0;
return Future.value(false);
}
}
return Future.value(true);
}
You can look for time duration between the two consecutive back button clicks, and if the difference is within the desired duration then exit the app.
Here is the complete code sample for the counter app, which exits the app only if the difference between two consecutive back button clicks is less than 1 second (1000 ms)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
void showSnackBar() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
behavior: SnackBarBehavior.floating,
duration: Duration(milliseconds: 600),
margin: EdgeInsets.only(bottom: 0, right: 32, left: 32),
content: Text('Tap back button again to exit'),
),
);
}
void hideSnackBar() {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
}
DateTime oldTime = DateTime.now();
DateTime newTime = DateTime.now();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: WillPopScope(
onWillPop: () async {
newTime = DateTime.now();
int difference = newTime.difference(oldTime).inMilliseconds;
oldTime = newTime;
if (difference < 1000) {
hideSnackBar();
return true;
} else {
showSnackBar();
return false;
}
},
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
DateTime BackPressTime = DateTime.now();
#override
Widget build(BuildContext context) {
return Scaffold(
body: WillPopScope(
child: Home(),
onWillPop: exiteApp,
),
);
}
Future<bool> exiteApp() {
print("exite app");
DateTime now = DateTime.now();
if(now.difference(BackPressTime)< Duration(seconds: 2)){
return Future(() => true);
}
else{
BackPressTime = DateTime.now();
Fluttertoast.showToast(msg: "Press agin");
return Future(()=> false);
}
}

How can I have an AppBar icon trigger a rendering?

I think I have the design of my app wrong. I am new to flutter/dart and am finding myself confused by previous experience with other languages (specifically C# and JavaScript).
I have an app that currently consists of a 3 x 3 GridView of 9 colored circular tiles, named Tiled_Surface. Each tile is assigned the following onTap handler:
void on_tile_tapped(int index) {
setState(() {
tile_tapped = index;
});
} // on_tile_tapped
where index has an arbitrary value in the range [0..8). Whenever a tile is tapped, the color of the tile changes to a lighter value (actually the "accent color" of the tile's color). All of that works file.
The AppBar contains a title ("Tiled Surface Demo") and two actions that consist of two IconButtons (Icons.swap_horiz and Icons.replay). It is intended that when the swap icon is tapped that the tile colors are shuffled into a new random order. And when the replay icon is tapped the tile colors are restored to their original order. When the two AppBar icons are tapped there is no apparent change to the display until a tile is tapped. Then, the changes made by the AppBar taps are displayed.
This is not the desired effect. My problem is how to render Tiled_Surface when the AppBar icons are tapped.
The code for the app follows. Thanks for your thoughts.
// ignore_for_file: camel_case_types
// ignore_for_file: constant_identifier_names
// ignore_for_file: non_constant_identifier_names
import 'package:flutter/material.dart';
import 'dart:math';
const int NUMBER_TILES = 9;
final int cross_axis_count = (sqrt (NUMBER_TILES)).toInt();
final double cross_axis_spacing = 4.0;
final double main_axis_spacing = cross_axis_spacing;
List<int> indices = [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ];
List normal_colors = [
Colors.red,
Colors.orange,
Colors.yellow,
Colors.green,
Colors.blue,
Colors.purple,
Colors.amber,
Colors.cyan,
Colors.indigo,
]; // normal_colors
List bright_colors = [
Colors.pinkAccent,
Colors.orangeAccent,
Colors.yellowAccent,
Colors.lightGreenAccent,
Colors.blue.shade200,
Colors.purpleAccent,
Colors.amberAccent,
Colors.cyanAccent,
Colors.indigoAccent,
]; // bright_colors
void reinitialize_tiles() {
indices.clear();
for (int i = 0; (i < NUMBER_TILES); i++) {
indices.add(i);
}
} // reinitialize_tiles
void randomize_tiles() {
var random = new Random();
indices.clear();
for (int i = 0; (i < NUMBER_TILES); i++) {
var varient = random.nextInt(9);
if (indices.length > 0) {
while (indices.contains(varient)) {
varient = random.nextInt(9);
}
}
indices.add(varient);
}
} // randomize_tiles
void main() => runApp(new MyApp());
class Tiled_Surface extends StatefulWidget {
Tiled_Surface({Key key}) : super(key: key);
#override // Tiled_Surface
Tiled_Surface_State createState() => Tiled_Surface_State();
}
class Tiled_Surface_State extends State<Tiled_Surface> {
List<GridTile> grid_tiles = <GridTile>[];
int tile_tapped = -1;
void on_tile_tapped(int index) {
setState(() {
tile_tapped = index;
});
} // on_tile_tapped
GridTile new_surface_tile(Color tile_color, int index) {
GridTile tile = GridTile(
child: GestureDetector(
onTap: () => on_tile_tapped(index),
child: Container(
decoration: BoxDecoration(
color: tile_color,
shape: BoxShape.circle,
),
),
)
);
return (tile);
} // new_surface_tile
List<GridTile> create_surface_tiles() {
grid_tiles = new List<GridTile>();
for (int i = 0; (i < NUMBER_TILES); i++) {
Color tile_color = ( tile_tapped == i ) ?
bright_colors[indices[i]] :
normal_colors[indices[i]];
grid_tiles.add(new_surface_tile(tile_color, i));
}
return (grid_tiles);
} // create_surface_tiles
#override // Tiled_Surface_State
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: cross_axis_count,
childAspectRatio: 1.0,
padding: const EdgeInsets.all(4.0),
mainAxisSpacing: main_axis_spacing,
crossAxisSpacing: cross_axis_spacing,
children: create_surface_tiles(),
);
}
} // class Tiled_Surface_State
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Tiled Surface Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Tiled Surface Demo'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.swap_horiz),
onPressed: () {
randomize_tiles();
},
),
IconButton(
icon: Icon(Icons.replay),
onPressed: () {
reinitialize_tiles();
},
)
]
),
body: Column(
children: [
Tiled_Surface(),
],
),
),
);
}
}
Problem:
Flutter widgets(Stateful) will react to state variables only. Not for global and local. In your example indices is a global variable.
I updated the code with
Moved indices into MyApp as
Mutable global variables are not good
We want our MyApp to reflect for changes in indices
As MyApp started holding state changed it as StatefulWidget
Moved randomize_tiles and reinitialize_tiles into _MyAppState and added setState on change of indices so that widgets will get re-rendered.
As Tiled_Surface also need indices, injecting(passing) them in the constructor.
Please have a look
import 'package:flutter/material.dart';
import 'dart:math';
const int NUMBER_TILES = 9;
final int cross_axis_count = (sqrt(NUMBER_TILES)).toInt();
final double cross_axis_spacing = 4.0;
final double main_axis_spacing = cross_axis_spacing;
List normal_colors = [
Colors.red,
Colors.orange,
Colors.yellow,
Colors.green,
Colors.blue,
Colors.purple,
Colors.amber,
Colors.cyan,
Colors.indigo,
]; // normal_colors
List bright_colors = [
Colors.pinkAccent,
Colors.orangeAccent,
Colors.yellowAccent,
Colors.lightGreenAccent,
Colors.blue.shade200,
Colors.purpleAccent,
Colors.amberAccent,
Colors.cyanAccent,
Colors.indigoAccent,
]; // bright_colors
void main() => runApp(new MyApp());
class Tiled_Surface extends StatefulWidget {
List<int> indices;
Tiled_Surface(this.indices, {Key key}) : super(key: key);
#override // Tiled_Surface
Tiled_Surface_State createState() => Tiled_Surface_State(indices);
}
class Tiled_Surface_State extends State<Tiled_Surface> {
List<GridTile> grid_tiles = <GridTile>[];
int tile_tapped = -1;
List<int> indices;
Tiled_Surface_State(this.indices);
void on_tile_tapped(int index) {
setState(() {
tile_tapped = index;
});
} // on_tile_tapped
GridTile new_surface_tile(Color tile_color, int index) {
GridTile tile = GridTile(
child: GestureDetector(
onTap: () => on_tile_tapped(index),
child: Container(
decoration: BoxDecoration(
color: tile_color,
shape: BoxShape.circle,
),
),
));
return (tile);
} // new_surface_tile
List<GridTile> create_surface_tiles() {
grid_tiles = new List<GridTile>();
for (int i = 0; (i < NUMBER_TILES); i++) {
Color tile_color = (tile_tapped == i)
? bright_colors[indices[i]]
: normal_colors[indices[i]];
grid_tiles.add(new_surface_tile(tile_color, i));
}
return (grid_tiles);
} // create_surface_tiles
#override // Tiled_Surface_State
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: cross_axis_count,
childAspectRatio: 1.0,
padding: const EdgeInsets.all(4.0),
mainAxisSpacing: main_axis_spacing,
crossAxisSpacing: cross_axis_spacing,
children: create_surface_tiles(),
);
}
} // class Tiled_Surface_State
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
List<int> indices = [0, 1, 2, 3, 4, 5, 6, 7, 8];
void randomize_tiles() {
var random = new Random();
indices.clear();
for (int i = 0; (i < NUMBER_TILES); i++) {
var varient = random.nextInt(9);
if (indices.length > 0) {
while (indices.contains(varient)) {
varient = random.nextInt(9);
}
}
indices.add(varient);
}
setState(() {});
}
void reinitialize_tiles() {
indices.clear();
for (int i = 0; (i < NUMBER_TILES); i++) {
indices.add(i);
}
setState(() {});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Tiled Surface Demo',
home: Scaffold(
appBar: AppBar(title: Text('Tiled Surface Demo'), actions: <Widget>[
IconButton(
icon: Icon(Icons.swap_horiz),
onPressed: () {
randomize_tiles();
},
),
IconButton(
icon: Icon(Icons.replay),
onPressed: () {
reinitialize_tiles();
},
)
]),
body: Column(
children: [
Tiled_Surface(indices),
],
),
),
);
}
}

Maintain state for StatefulWidget during build in Flutter

I have a list of stateful widgets where the user can add, remove, and interact with items in the list. Removing items from the list causes subsequent items in the list to rebuild as they shift to fill the deleted row. This results in a loss of state data for these widgets - though they should remain unaltered other than their location on the screen. I want to be able to maintain state for the remaining items in the list even as their position changes.
Below is a simplified version of my app which consists primarily of a list of StatefulWidgets. The user can add items to the list ("tasks" in my app) via the floating action button or remove them by swiping. Any item in the list can be highlighted by tapping the item, which changes the state of the background color of the item. If multiple items are highlighted in the list, and an item (other than the last item in the list) is removed, the items that shift to replace the removed item lose their state data (i.e. the background color resets to transparent). I suspect this is because _taskList rebuilds since I call setState() to update the display after a task is removed. I want to know if there is a clean way to maintain state data for the remaining tasks after a task is removed from _taskList.
void main() => runApp(new TimeTrackApp());
class TimeTrackApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Time Tracker',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new TimeTrackHome(title: 'Task List'),
);
}
}
class TimeTrackHome extends StatefulWidget {
TimeTrackHome({Key key, this.title}) : super(key: key);
final String title;
#override
_TimeTrackHomeState createState() => new _TimeTrackHomeState();
}
class _TimeTrackHomeState extends State<TimeTrackHome> {
TextEditingController _textController;
List<TaskItem> _taskList = new List<TaskItem>();
void _addTaskDialog() async {
_textController = TextEditingController();
await showDialog(
context: context,
builder: (_) => new AlertDialog(
title: new Text("Add A New Task"),
content: new TextField(
controller: _textController,
decoration: InputDecoration(
border: InputBorder.none, hintText: 'Enter the task name'),
),
actions: <Widget>[
new FlatButton(
onPressed: () => Navigator.pop(context),
child: const Text("CANCEL")),
new FlatButton(
onPressed: (() {
Navigator.pop(context);
_addTask(_textController.text);
}),
child: const Text("ADD"))
],
));
}
void _addTask(String title) {
setState(() {
// add the new task
_taskList.add(TaskItem(
name: title,
));
});
}
#override
void initState() {
_taskList = List<TaskItem>();
super.initState();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Align(
alignment: Alignment.topCenter,
child: ListView.builder(
padding: EdgeInsets.all(0.0),
itemExtent: 60.0,
itemCount: _taskList.length,
itemBuilder: (BuildContext context, int index) {
if (index < _taskList.length) {
return Dismissible(
key: ObjectKey(_taskList[index]),
onDismissed: (direction) {
if(this.mounted) {
setState(() {
_taskList.removeAt(index);
});
}
},
child: _taskList[index],
);
}
}),
),
floatingActionButton: new FloatingActionButton(
onPressed: _addTaskDialog,
tooltip: 'Click to add a new task',
child: new Icon(Icons.add),
),
);
}
}
class TaskItem extends StatefulWidget {
final String name;
TaskItem({Key key, this.name}) : super(key: key);
TaskItem.from(TaskItem other) : name = other.name;
#override
State<StatefulWidget> createState() => new _TaskState();
}
class _TaskState extends State<TaskItem> {
static final _taskFont =
const TextStyle(fontSize: 26.0, fontWeight: FontWeight.bold);
Color _color = Colors.transparent;
void _highlightTask() {
setState(() {
if(_color == Colors.transparent) {
_color = Colors.greenAccent;
}
else {
_color = Colors.transparent;
}
});
}
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
Material(
color: _color,
child: ListTile(
title: Text(
widget.name,
style: _taskFont,
textAlign: TextAlign.center,
),
onTap: () {
_highlightTask();
},
),
),
Divider(
height: 0.0,
),
]);
}
}
I ended up solving the problem by creating an intermediate class which contains a reference to the StatefulWidget and transferred over all the state variables. The State class accesses the state variables through a reference to the intermediate class. The higher level widget that contained and managed a List of the StatefulWidget now access the StatefulWidget through this intermediate class. I'm not entirely confident in the "correctness" of my solution as I haven't found any other examples of this, so I am still open to suggestions.
My intermediate class is as follows:
class TaskItemData {
// StatefulWidget reference
TaskItem widget;
Color _color = Colors.transparent;
TaskItemData({String name: "",}) {
_color = Colors.transparent;
widget = TaskItem(name: name, stateData: this,);
}
}
My StatefulWidget and its corresponding State classes are nearly unchanged, except that the state variables no longer reside in the State class. I also added a reference to the intermediate class inside my StatefulWidget which gets initialized in the constructor. Previous uses of state variables in my State class now get accessed through the reference to the intermediate class. The modified StatefulWidget and State classes is as follows:
class TaskItem extends StatefulWidget {
final String name;
// intermediate class reference
final TaskItemData stateData;
TaskItem({Key key, this.name, this.stateData}) : super(key: key);
#override
State<StatefulWidget> createState() => new _TaskItemState();
}
class _TaskItemState extends State<TaskItem> {
static final _taskFont =
const TextStyle(fontSize: 26.0, fontWeight: FontWeight.bold);
void _highlightTask() {
setState(() {
if(widget.stateData._color == Colors.transparent) {
widget.stateData._color = Colors.greenAccent;
}
else {
widget.stateData._color = Colors.transparent;
}
});
}
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
Material(
color: widget.stateData._color,
child: ListTile(
title: Text(
widget.name,
style: _taskFont,
textAlign: TextAlign.center,
),
onTap: () {
_highlightTask();
},
),
),
Divider(
height: 0.0,
),
]);
}
}
The widget containing the List of TaskItem objects has been replaced with a List of TaskItemData. The ListViewBuilder child now accesses the TaskItem widget through the intermediate class (i.e. child: _taskList[index], has changed to child: _taskList[index].widget,). It is as follows:
class _TimeTrackHomeState extends State<TimeTrackHome> {
TextEditingController _textController;
List<TaskItemData> _taskList = new List<TaskItemData>();
void _addTaskDialog() async {
_textController = TextEditingController();
await showDialog(
context: context,
builder: (_) => new AlertDialog(
title: new Text("Add A New Task"),
content: new TextField(
controller: _textController,
decoration: InputDecoration(
border: InputBorder.none, hintText: 'Enter the task name'),
),
actions: <Widget>[
new FlatButton(
onPressed: () => Navigator.pop(context),
child: const Text("CANCEL")),
new FlatButton(
onPressed: (() {
Navigator.pop(context);
_addTask(_textController.text);
}),
child: const Text("ADD"))
],
));
}
void _addTask(String title) {
setState(() {
// add the new task
_taskList.add(TaskItemData(
name: title,
));
});
}
#override
void initState() {
_taskList = List<TaskItemData>();
super.initState();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Align(
alignment: Alignment.topCenter,
child: ListView.builder(
padding: EdgeInsets.all(0.0),
itemExtent: 60.0,
itemCount: _taskList.length,
itemBuilder: (BuildContext context, int index) {
if (index < _taskList.length) {
return Dismissible(
key: ObjectKey(_taskList[index]),
onDismissed: (direction) {
if(this.mounted) {
setState(() {
_taskList.removeAt(index);
});
}
},
child: _taskList[index].widget,
);
}
}),
),
floatingActionButton: new FloatingActionButton(
onPressed: _addTaskDialog,
tooltip: 'Click to add a new task',
child: new Icon(Icons.add),
),
);
}
}

Resources