Clicked Button multiple times same time, open pages multiple times. How to fix this issue? I also uploaded the gif file on my application(double click on the image).
Container(
padding: EdgeInsets.all(10.0),
child: ButtonTheme(
minWidth: 10.0,
height: 40.0,
child: RaisedButton(
child: Text(
AppTranslations.of(context)
.text("loginpage_button"),
style: TextStyle(
color: Colors.white, fontSize: 15.0),
),
onPressed: () async{
(isOffline)
? _showSnackBar()
: checking2(usernameController, context, _url);
},
color: Colors.blue,
padding: EdgeInsets.all(20.0),
),
),
margin: EdgeInsets.only(top: 0.0),
)
I used this code, it's working, but user types username incorrectly, user cant click button second type. this is my code.
onPressed: () async {
if (_firstClick) {
_firstClick = false;
(isOffline)
? _showSnackBar()
: checking2(usernameController, context, _url);
}
Solved this in my application based on calculating time difference.
First, declare a DateTime variable and define the function as follows:
DateTime loginClickTime;
bool isRedundentClick(DateTime currentTime) {
if (loginClickTime == null) {
loginClickTime = currentTime;
print("first click");
return false;
}
print('diff is ${currentTime.difference(loginClickTime).inSeconds}');
if (currentTime.difference(loginClickTime).inSeconds < 10) {
// set this difference time in seconds
return true;
}
loginClickTime = currentTime;
return false;
}
In the login button call the function as follows to check for redundancy:
RaisedButton(
child: Text('Login'),
onPressed: () {
if (isRedundentClick(DateTime.now())) {
print('hold on, processing');
return;
}
print('run process');
},
),
Create a bool variable which will be true when the button is pressed, (hence, initial value is set to false).
bool _clicked = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: RaisedButton(
child: Text('Button'),
onPressed: _clicked
? null
: () {
setState(() => _clicked = true); // set it to true now
},
),
);
}
You can use a bool variable to save the state of your RaisedButton:
First create the variable a set its initial value :
var _firstPress = true;
Then add _firstPress inside your onPressed function :
Container(
padding: EdgeInsets.all(10.0),
child: ButtonTheme(
minWidth: 10.0,
height: 40.0,
child: RaisedButton(
child: Text(
AppTranslations.of(context).text("loginpage_button"),
style: TextStyle(color: Colors.white, fontSize: 15.0),
),
onPressed: () async {
// This is what you should add in your code
if (_firstPress) {
_firstPress = false;
(isOffline) ? _showSnackBar() : checking2(usernameController, context, _url);
}
},
color: Colors.blue,
padding: EdgeInsets.all(20.0),
),
),
margin: EdgeInsets.only(top: 0.0),
),
This way your onPressed function will only respond to the RaisedButton's first click.
I've written two classes for myself that may be helpful for others. They encapsulate the answer given by others in this thread so that you don't have a bunch of bools and assignment statements floating everywhere.
You pass your function to the class, and use the class' "invoke" method in place of the function. This currently does not support functions that need parameters, but is useful for the void case.
typedef void CallOnceFunction();
class CallOnce {
bool _inFunction = false;
final CallOnceFunction function;
CallOnce(CallOnceFunction function) :
assert(function != null),
function = function
;
void invoke() {
if (_inFunction)
return;
_inFunction = true;
function();
_inFunction = false;
}
}
typedef Future<void> CallOnceFuture();
class CallFutureOnce {
bool _inFunction = false;
final CallOnceFuture future;
CallFutureOnce(CallOnceFuture future) :
assert(future != null),
future = future
;
Future<void> invoke() async {
if (_inFunction)
return;
_inFunction = true;
await this.future();
_inFunction = false;
}
}
Update: Here's an example of both of these classes in action
/*Example*/
import 'package:flutter/material.dart';
class MyWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new MyWidgetState();
}
}
class MyWidgetState extends State<MyWidget> {
CallOnce _callOnce;
CallFutureOnce _callFutureOnce;
void myFunction() {
/*Custom Code*/
}
Future<void> myFutureFunction() async {
/*Custom Code*/
//await something()
}
#override
void initState() {
super.initState();
this._callOnce = CallOnce(this.myFunction);
this._callFutureOnce = CallFutureOnce(this.myFutureFunction);
}
#override
Widget build(BuildContext context) {
return Scaffold (
body: Center (
child: RaisedButton (
child: Text('Try Me'),
onPressed: this._callOnce.invoke,
),
),
floatingActionButton: FloatingActionButton (
child: Icon(Icons.save),
onPressed: this._callFutureOnce.invoke,
),
);
}
}
Some of the other solutions do not work for me, and some of them are not isolated in their own state and I implemented my solution to encapsulate the functionality in my custom widget. I implemented it for IconButton but you could modify it with any tappable widget. Cheers:
import 'package:flutter/material.dart';
class AppIconButton extends StatefulWidget {
const AppIconButton({
Key? key,
required this.onPressed,
required this.icon,
this.disableAfterClick = const Duration(milliseconds: 500),
}) : super(key: key);
final Function onPressed;
final Widget icon;
final Duration disableAfterClick;
#override
State<AppIconButton> createState() => _AppIconButtonState();
}
class _AppIconButtonState extends State<AppIconButton> {
bool _acceptsClicks = true;
#override
Widget build(BuildContext context) {
return IconButton(
onPressed: () {
if (_acceptsClicks) {
//if you want to disable the button
//use the variable with setState method
//but it's not my case
_acceptsClicks = false;
widget.onPressed();
Future.delayed(widget.disableAfterClick, () {
if (mounted) {
_acceptsClicks = true;
}
});
}
// else {
// debugPrint("Click ignored");
// }
},
icon: widget.icon,
);
}
}
Disabling multiple click events in a flutter with StatelessWidget.
Using as a shareable widget.
Simple example:
class SingleTapEvent extends StatelessWidget {
final Widget child;
final Function() onTap;
bool singleTap = false;
SingleTapEvent(
{Key? key, required this.child, required this.onTap, singleTap = false})
: super(key: key);
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
if (!singleTap) {
Function.apply(onTap, []);
singleTap = true;
Future.delayed(const Duration(seconds: 3)).then((value) => singleTap = false);
}
},
child: child);
}
}
Usage:
SingleTapEvent(
onTap: () {
print("Clicked");
},
child: Text("Click me"),
);
This question is answered here How do I disable a Button in Flutter?
All you need to use statefulWidget and create a variable to hold your condition, And change it according to your event. Your button will be enable or disable according to your variable's value.
Suppose initial state of your variable, isDisable = false,that means - your button is enable by default. And after first clicking change the value of your state variable isDisable = true.
Instead of using RaisedButton directly, you can turn it into a StatefulWidget. Then use the ChangeNotifier to change it state from enable to disable and control button press function.It will also help you to reuse it in different places. Here is an example how can you do that
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final ValueNotifier<MyButtonState> _myButtonStateChangeNotifier =
ValueNotifier(MyButtonState.enable);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: MyButton(
buttonStateChangeNotifier: _myButtonStateChangeNotifier,
onPressed: _onButtonPressed,
text: "Click Me",
),
),
);
}
_onButtonPressed() {
print("Button Pressed");
_myButtonStateChangeNotifier.value = MyButtonState.disable;
}
}
enum MyButtonState {enable, disable}
class MyButton extends StatefulWidget {
final VoidCallback onPressed;
final String text;
final TextStyle textStyle;
final ValueNotifier<MyButtonState> buttonStateChangeNotifier;
MyButton({
#required this.onPressed,
this.text = "",
this.textStyle,
this.buttonStateChangeNotifier,
});
#override
_MyButtonState createState() => _MyButtonState();
}
class _MyButtonState extends State<MyButton> {
MyButtonState _myButtonState = MyButtonState.enable;
#override
void initState() {
super.initState();
if (widget.buttonStateChangeNotifier != null) {
widget.buttonStateChangeNotifier.addListener(_handleButtonStateChange);
_myButtonState = widget.buttonStateChangeNotifier.value;
}
}
#override
Widget build(BuildContext context) {
return RaisedButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(widget.text)
],
),
onPressed: _myButtonState == MyButtonState.enable
? _handleOnPress
: null,
);
}
_handleButtonStateChange() {
setState(() {
_myButtonState = widget.buttonStateChangeNotifier.value;
});
}
_handleOnPress() {
if (_myButtonState == MyButtonState.enable) {
widget.onPressed();
}
}
}
Thanks to #Mazin Ibrahim's suggestion above, setting a basic bool toggle flag works fine.
This implementation is based on handling the enable/disable logic at the callback level, independent of the widget layout details.
bool _isButtonEnabled = true;
MessageSql _messageSql = new MessageSql(); // DB helper class
final TextEditingController eCtrl = new TextEditingController();
_onSendMessage(String message) {
if (! _isButtonEnabled) {
return;
}
_isButtonEnabled = false;
_messageSql.insert(message).then((resultId) {
// only update all if save is successful
eCtrl.clear();
AppUi.dismissKeyboard();
_isButtonEnabled = true;
Future.delayed(const Duration(milliseconds: 400), () {
setState(() {});
})
.catchError((error, stackTrace) {
print("outer: $error");
});
}
Similar to what PreciseSpeech and Sharman implemented, I made a few changes and it works.
DateTime loginClickTime = DateTime.now();
#override
void initState() {
loginClickTime;
super.initState();
}
bool isRedundentClick(DateTime currentTime) {
if (loginClickTime == '') {
loginClickTime = currentTime;
print("first click");
return false;
}
print('diff is ${currentTime.difference(loginClickTime).inSeconds}');
if (currentTime.difference(loginClickTime).inSeconds < 10) {
//set this difference time in seconds
return true;
}
loginClickTime = currentTime;
return false;
}
Then in the OnPressed function
MaterialButton(
child:Text('Login'),
onPressed: (){
if(isRedundentClick(DateTime.now())){
print('hold on, processing');
return;
}
print('run process');
},
)
Related
In the following class, I am trying to set _isFavorited to the value that I get from the SharedPreference. However, I guess the widget is already build by the time I get the value from Sharedpreference. How can I set the value from sharedpreference and then display my widget?
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/material.dart';
class FavoriteWidget extends StatefulWidget {
final dish_name;
final dish_pic;
#override
_FavoriteWidgetState createState() => _FavoriteWidgetState();
FavoriteWidget(this.dish_name, this.dish_pic,{Key key})
: super(key: key);
}
class _FavoriteWidgetState extends State<FavoriteWidget> {
bool _isFavorited = false;
#override
void initState() {
// TODO: implement initState
super.initState();
isDishFavorited(this.context);
}
isDishFavorited(BuildContext context) async{
SharedPreferences prefs = await SharedPreferences.getInstance();
if(prefs.getString(widget.dish_name) != null){
//rebuilding the context to display Icons.favorite
build(context);
_isFavorited = true; <== the widget is built before this is set to true.
}
debugPrint("isfavorite inside method is" + _isFavorited.toString());
}
// #docregion _toggleFavorite
void _toggleFavorite() {
setState(() {
if (_isFavorited) {
//Remove from favorites
_removeFromFavorites();
_isFavorited = false;
debugPrint("Removed from favorites");
} else {
//Add to favorites
_addToFavorites();
_isFavorited = true;
debugPrint("Added to favorites");
}
});
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: (_isFavorited
? Icon(Icons.favorite, size: 35, color: Colors.green)
: Icon(Icons.favorite_border, size: 35, color: Colors.green)),
color: Colors.red[500],
onPressed: _toggleFavorite,
),
Container(
margin: const EdgeInsets.only(top: 8.0),
child: Text(
"FAVORITE",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.w400,
color: Colors.white,
),
),
),
],
);
}
}
What I tried to do was to rebuild the widget after setting is_Favorited to true but that didn't work either. So, I am not sure how else to make this work. Any ideas?
you need to fix the isDishFavorited() function in your code
isDishFavorited(BuildContext context) async{
SharedPreferences prefs = await SharedPreferences.getInstance();
if(prefs.getString(widget.dish_name) != null){
setState(() {
_isFavorited = true;
});
}
debugPrint("isfavorite inside method is" + _isFavorited.toString());
}
setState will rebuild your widget after you fetch the data fromt the shared preferences
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);
}
}
I'm starting with Flutter and I cannot make drag and drop functionality to work. I followed the documentation but have no idea what I'm doing wrong.
This sample app displays three squares and the blue is draggable. The other ones have DragTarget set, one inside the square and one outside the square. When I drag the blue square it prints info that the drag started but there is no print info when dragging or dropping over the DragTargets.
Here is the code:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.red,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Container(
constraints: BoxConstraints.expand(),
color: Colors.grey[900],
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
width: 100,
height: 100,
color: Colors.red,
child: DragTarget(
onWillAccept: (d) => true,
onAccept: (d) => print("ACCEPT 1!"),
onLeave: (d) => print("LEAVE 1!"),
builder: (a,data,c) {
print(data);
return Center();
},
),
),
DragTarget(
onWillAccept: (d){return true;},
onAccept:(d) => print("ACCEPT 2!"),
onLeave: (d) => print("LEAVE 2!"),
builder: (context, candidateData, rejectedData){
return Container(
width: 150,
height: 150,
color: Colors.purple
);
}
),
Draggable(
data: ["SOME DATA"],
onDragStarted: () => print("DRAG START!"),
onDragCompleted: () => print("DRAG COMPLETED!"),
onDragEnd: (details) => print("DRAG ENDED!"),
onDraggableCanceled: (data, data2) => print("DRAG CANCELLED!"),
feedback: SizedBox(
width: 100,
height: 100,
child: Container(
margin: EdgeInsets.all(10),
color: Colors.green[800],
),
),
child: SizedBox(
width: 100,
height: 100,
child: Container(
margin: EdgeInsets.all(10),
color: Colors.blue[800],
),
),
),
],
)
),
)
);
}
}
Apparently the Draggable and DragTarget need to have the generic type specified if you are passing data, otherwise the onAccept and onWillAccept will not be fired.
For example, if you want to pass data as int then use Draggable<int> and DragTarget<int> — this also applies to onAccept and onWillAccept, they need to accept int as a parameter.
You should setState when you call onAccept and add a boolean value to your stateful widget.
bool accepted = false;
onAccept: (data){
if(data=='d'){
setState(() {
accepted = true;
});
},
I used ChangeNotifyProvider and a model to manage my Draggable and Dragable Target multiplication challenge and results. I built a simple multiplication game using ChangeNotify that updates the Provider that is listening for changes. The GameScore extends the ChangeNotifier which broadcast to the provider when changes occur in the model. The Provider can either be listening or not listening. If the user get the right answer than the Model updates its score and notifies the listeners. The score is then displayed in a text box. I think the provider model is a simplier way to interact with the widget for managing state.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'dart:math';
class Multiplication
{
int value1;
int value2;
int result;
int answerKey;
int fakeResult;
Multiplication(this.value1,this.value2,this.result,this.answerKey,this.fakeResult);
}
class GameScore with ChangeNotifier
{
int score=0;
int number=0;
List<Multiplication> lstMultiplication=[];
late Multiplication currentMultiplication;
GameScore()
{
var rng = Random();
for(int i=0; i<=25; i++)
{
for(int j=0; j<=25; j++)
{
var answerKey=rng.nextInt(2);
var fakeAnswer=rng.nextInt(25)*rng.nextInt(25);
lstMultiplication.add(Multiplication(i,j,i*j,answerKey,fakeAnswer));
}
}
}
int getChallengeValue(int key)
{
int retVal=0;
if (currentMultiplication.answerKey==key)
{
retVal=currentMultiplication.result;
}
else
{
retVal=currentMultiplication.fakeResult;
}
return retVal;
}
String displayMultiplication()
{
String retVal="";
if (currentMultiplication!=null)
{
retVal=currentMultiplication.value1.toString()+ " X "+currentMultiplication.value2.toString();
}
return retVal;
}
nextMultiplication()
{
var rng = Random();
var index=rng.nextInt(lstMultiplication.length);
currentMultiplication= lstMultiplication[index];
}
changeAcceptedData(int data) {
var rng = Random();
score += 1;
number=rng.nextInt(100);
notifyListeners();
}
changeWrongData(int data) {
var rng = Random();
score -= 1;
number=rng.nextInt(100);
notifyListeners();
}
}
void main() {
runApp(const MyApp());
//runApp(Provider<GameScore>(create: (context) => GameScore(), child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: //TestDraggableWidget(),
ChangeNotifierProvider(create:(context)=>GameScore(),child: TestDraggableWidget())
);
}
}
class TestDraggableWidget extends StatefulWidget {
TestDraggableWidget({Key? key}) : super(key: key);
#override
State<TestDraggableWidget> createState() => _TestDraggableWidgetState();
}
class _TestDraggableWidgetState extends State<TestDraggableWidget> {
#override
Widget build(BuildContext context) {
Provider.of<GameScore>(context,listen:false).nextMultiplication();
return Scaffold(appBar: AppBar(title:Text("Draggable")),body:
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
EvenContainerWidget(),
NumberContainerWidget(),
OddContainerWidget(),
SizedBox(width:100,child:Text("Score: ${Provider.of<GameScore>(context, listen: true).score}",style:TextStyle(color:Colors.green,fontSize:14)))
],)
],));
}
}
class EvenContainerWidget extends StatefulWidget {
EvenContainerWidget({Key? key}) : super(key: key);
#override
State<EvenContainerWidget> createState() => _EvenContainerWidgetState();
}
class _EvenContainerWidgetState extends State<EvenContainerWidget> {
int? valueAccepted;
_onAccept(BuildContext context, int data)
{
if (data==valueAccepted){
Provider.of<GameScore>(context, listen: false).changeAcceptedData(data);
setState(() {
valueAccepted=data;
});
}
else
{
Provider.of<GameScore>(context, listen: false).changeWrongData(data);
}
}
bool _willAccept(int? data)
{
return true;
}
#override
Widget build(BuildContext context) {
valueAccepted=Provider.of<GameScore>(context, listen: false).getChallengeValue(1);
return Container(
width:60,
height:60,
decoration:BoxDecoration(borderRadius: BorderRadius.circular(10),color:Colors.blueAccent),
child:
DragTarget<int>(
onAccept: (data)=> _onAccept(context,data),
onWillAccept: _willAccept,
builder:(context, candidateData, rejectedData) {
return Center(child:Text("Choice 1: ${valueAccepted==null?'':valueAccepted.toString()}"));
},
)
);
}
}
class OddContainerWidget extends StatefulWidget {
OddContainerWidget({Key? key}) : super(key: key);
#override
State<OddContainerWidget> createState() => _OddContainerWidgetState();
}
class _OddContainerWidgetState extends State<OddContainerWidget> {
int? valueAccepted;
_onAccept(BuildContext context, int data)
{
if(data==valueAccepted)
{
Provider.of<GameScore>(context, listen: false).changeAcceptedData(data);
setState(() {
valueAccepted=data;
});
}
else
{
Provider.of<GameScore>(context, listen: false).changeWrongData(data);
}
}
bool _willAccept(int? data)
{
/*if (data!.isOdd)
{
setState(() {
valueAccepted=data;
});
}*/
return true;
}
#override
Widget build(BuildContext context) {
valueAccepted=Provider.of<GameScore>(context, listen: false).getChallengeValue(0);
return Container(
width:60,
height:60,
decoration:BoxDecoration(borderRadius: BorderRadius.circular(10),color:Colors.blueAccent),
child:
DragTarget<int>(
onAccept: (data)=> _onAccept(context,data),
onWillAccept: _willAccept,
builder:(context, candidateData, rejectedData) {
return Center(child:Text("Choice 2: ${valueAccepted==null?'':valueAccepted.toString()}"));
},
)
);
}
}
class NumberContainerWidget extends StatelessWidget {
const NumberContainerWidget({Key? key}) : super(key: key);
_dragCompleted(BuildContext context){
}
#override
Widget build(BuildContext context) {
return Draggable(
//information dropped by draggable at dragtarget
data: Provider.of<GameScore>(context, listen: true).currentMultiplication.result,
onDragCompleted: _dragCompleted(context) ,
//Widget to be displayed when drag is underway
feedback: Container(
width:60,
height:60,
decoration:BoxDecoration(borderRadius: BorderRadius.circular(10),color:Colors.black26),
child: Center(child:Text("${Provider.of<GameScore>(context, listen: false).displayMultiplication()}",style:TextStyle(color:Colors.green,fontSize:14))),
),
child:
Container(
width:60,
height:60,
decoration:BoxDecoration(borderRadius: BorderRadius.circular(10),color:Colors.black26),
child: Center(child:Text("${Provider.of<GameScore>(context, listen: false).displayMultiplication()}",style:TextStyle(color:Colors.blue,fontSize:14))),
));
}
}
I'm trying to get ExpansionTile to collapse after I choose an item, but it does not close the list that was opened.
I tried to use the onExpansionChanged property but I did not succeed
How could you solve this problem?
Insert a gif demonstrating that ExpansionTile does not collapse after choosing an item, and below is also the code used.
import 'package:flutter/material.dart';
void main() {
runApp(new ExpansionTileSample());
}
class ExpansionTileSample extends StatefulWidget {
#override
ExpansionTileSampleState createState() => new ExpansionTileSampleState();
}
class ExpansionTileSampleState extends State<ExpansionTileSample> {
String foos = 'One';
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('ExpansionTile'),
),
body: new ExpansionTile(
title: new Text(this.foos),
backgroundColor: Theme.of(context).accentColor.withOpacity(0.025),
children: <Widget>[
new ListTile(
title: const Text('One'),
onTap: () {
setState(() {
this.foos = 'One';
});
},
),
new ListTile(
title: const Text('Two'),
onTap: () {
setState(() {
this.foos = 'Two';
});
},
),
new ListTile(
title: const Text('Three'),
onTap: () {
setState(() {
this.foos = 'Three';
});
},
),
]
),
),
);
}
}
Here is a workaround. Just add a global key (or a value key that changes after selecting an item) and it will force ExpansionTile to rebuild. The downside is losing animation for collapsing.
ExpansionTile(
key: GlobalKey(),
title: Text(title),
children: listTiles,
...
)
Here is a solution. We just add a expand, collapse and toggle functionality to ExpansionTile.
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
void main() {
runApp(new ExpansionTileSample());
}
class ExpansionTileSample extends StatefulWidget {
#override
ExpansionTileSampleState createState() => new ExpansionTileSampleState();
}
class ExpansionTileSampleState extends State<ExpansionTileSample> {
final GlobalKey<AppExpansionTileState> expansionTile = new GlobalKey();
String foos = 'One';
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('ExpansionTile'),
),
body: new AppExpansionTile(
key: expansionTile,
title: new Text(this.foos),
backgroundColor: Theme
.of(context)
.accentColor
.withOpacity(0.025),
children: <Widget>[
new ListTile(
title: const Text('One'),
onTap: () {
setState(() {
this.foos = 'One';
expansionTile.currentState.collapse();
});
},
),
new ListTile(
title: const Text('Two'),
onTap: () {
setState(() {
this.foos = 'Two';
expansionTile.currentState.collapse();
});
},
),
new ListTile(
title: const Text('Three'),
onTap: () {
setState(() {
this.foos = 'Three';
expansionTile.currentState.collapse();
});
},
),
]
),
),
);
}
}
// --- Copied and slightly modified version of the ExpansionTile.
const Duration _kExpand = const Duration(milliseconds: 200);
class AppExpansionTile extends StatefulWidget {
const AppExpansionTile({
Key key,
this.leading,
#required this.title,
this.backgroundColor,
this.onExpansionChanged,
this.children: const <Widget>[],
this.trailing,
this.initiallyExpanded: false,
})
: assert(initiallyExpanded != null),
super(key: key);
final Widget leading;
final Widget title;
final ValueChanged<bool> onExpansionChanged;
final List<Widget> children;
final Color backgroundColor;
final Widget trailing;
final bool initiallyExpanded;
#override
AppExpansionTileState createState() => new AppExpansionTileState();
}
class AppExpansionTileState extends State<AppExpansionTile> with SingleTickerProviderStateMixin {
AnimationController _controller;
CurvedAnimation _easeOutAnimation;
CurvedAnimation _easeInAnimation;
ColorTween _borderColor;
ColorTween _headerColor;
ColorTween _iconColor;
ColorTween _backgroundColor;
Animation<double> _iconTurns;
bool _isExpanded = false;
#override
void initState() {
super.initState();
_controller = new AnimationController(duration: _kExpand, vsync: this);
_easeOutAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeOut);
_easeInAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeIn);
_borderColor = new ColorTween();
_headerColor = new ColorTween();
_iconColor = new ColorTween();
_iconTurns = new Tween<double>(begin: 0.0, end: 0.5).animate(_easeInAnimation);
_backgroundColor = new ColorTween();
_isExpanded = PageStorage.of(context)?.readState(context) ?? widget.initiallyExpanded;
if (_isExpanded)
_controller.value = 1.0;
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void expand() {
_setExpanded(true);
}
void collapse() {
_setExpanded(false);
}
void toggle() {
_setExpanded(!_isExpanded);
}
void _setExpanded(bool isExpanded) {
if (_isExpanded != isExpanded) {
setState(() {
_isExpanded = isExpanded;
if (_isExpanded)
_controller.forward();
else
_controller.reverse().then<void>((Null value) {
setState(() {
// Rebuild without widget.children.
});
});
PageStorage.of(context)?.writeState(context, _isExpanded);
});
if (widget.onExpansionChanged != null) {
widget.onExpansionChanged(_isExpanded);
}
}
}
Widget _buildChildren(BuildContext context, Widget child) {
final Color borderSideColor = _borderColor.evaluate(_easeOutAnimation) ?? Colors.transparent;
final Color titleColor = _headerColor.evaluate(_easeInAnimation);
return new Container(
decoration: new BoxDecoration(
color: _backgroundColor.evaluate(_easeOutAnimation) ?? Colors.transparent,
border: new Border(
top: new BorderSide(color: borderSideColor),
bottom: new BorderSide(color: borderSideColor),
)
),
child: new Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconTheme.merge(
data: new IconThemeData(color: _iconColor.evaluate(_easeInAnimation)),
child: new ListTile(
onTap: toggle,
leading: widget.leading,
title: new DefaultTextStyle(
style: Theme
.of(context)
.textTheme
.subhead
.copyWith(color: titleColor),
child: widget.title,
),
trailing: widget.trailing ?? new RotationTransition(
turns: _iconTurns,
child: const Icon(Icons.expand_more),
),
),
),
new ClipRect(
child: new Align(
heightFactor: _easeInAnimation.value,
child: child,
),
),
],
),
);
}
#override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
_borderColor.end = theme.dividerColor;
_headerColor
..begin = theme.textTheme.subhead.color
..end = theme.accentColor;
_iconColor
..begin = theme.unselectedWidgetColor
..end = theme.accentColor;
_backgroundColor.end = widget.backgroundColor;
final bool closed = !_isExpanded && _controller.isDismissed;
return new AnimatedBuilder(
animation: _controller.view,
builder: _buildChildren,
child: closed ? null : new Column(children: widget.children),
);
}
}
solution below would work, but it is quite hacky and might not be the best one:
import 'package:flutter/material.dart';
import 'dart:math';
void main() {
runApp(new ExpansionTileSample());
}
class ExpansionTileSample extends StatefulWidget {
#override
ExpansionTileSampleState createState() => new ExpansionTileSampleState();
}
class ExpansionTileSampleState extends State {
String foos = 'One';
int _key;
_collapse() {
int newKey;
do {
_key = new Random().nextInt(10000);
} while(newKey == _key);
}
#override
void initState() {
super.initState();
_collapse();
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('ExpansionTile'),
),
body: new ExpansionTile(
key: new Key(_key.toString()),
initiallyExpanded: false,
title: new Text(this.foos),
backgroundColor: Theme
.of(context)
.accentColor
.withOpacity(0.025),
children: [
new ListTile(
title: const Text('One'),
onTap: () {
setState(() {
this.foos = 'One';
_collapse();
});
},
),
new ListTile(
title: const Text('Two'),
onTap: () {
setState(() {
this.foos = 'Two';
_collapse();
});
},
),
new ListTile(
title: const Text('Three'),
onTap: () {
setState(() {
this.foos = 'Three';
_collapse();
});
},
),
]
),
),
);
}
}
I found that ExpansionTile has initiallyExpanded property, which is the only way to make it collapsed. As property works only initially you want to make ExpansionTile to be recreated everytime build is called. To force it you just assign different key everytime you build it. This might not be best solution performance wise, but ExpansionTile is quite simple, so this should not be a problem.
None of the provided solutions pleased me.
I ended up creating a custom ExpandableListTile. As you can see below, its code is very brief and easy to customize.
I also had to create two supporting classes (that only handle the required animations) to build my widget:
ExpandableSection: a widget that can be easily controlled by one parameter "expanded".
RotatableSection: a widget to rotate the "Expand More" icon based on one parameter.
The main class:
class ExpandableListTile extends StatelessWidget {
const ExpandableListTile({Key key, this.title, this.expanded, this.onExpandPressed, this.child}) : super(key: key);
final Widget title;
final bool expanded;
final Widget child;
final Function onExpandPressed;
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
ListTile(
title: title,
onTap: onExpandPressed,
trailing: IconButton(
onPressed: onExpandPressed,
// icon: Icon(Icons.expand_more),
icon: RotatableSection(
rotated: expanded,
child: SizedBox(height: 30, width: 30, child: Icon(Icons.expand_more),)
),
),
),
ExpandableSection(child: child, expand: expanded,)
]);
}
}
Usage (simplified):
//...
return ExpandableListTile(
onExpandPressed: (){ setState((){ _expandedItem = 0;}) },
title: Text('Item'),
expanded: _expandedItem==0,
child: Padding(
padding: const EdgeInsets.fromLTRB(8,0,0,0),
child: Container(
color: Color.fromRGBO(0, 0, 0, .2),
child: Column(children: <Widget>[
ListTile(title: Text('Item 1')),
ListTile(title: Text('Item 2')),
ListTile(title: Text('Item 3')),
ListTile(title: Text('Item 4'))
],),
),
),
),
//...
The ExpandableSection class:
class ExpandableSection extends StatefulWidget {
final Widget child;
final bool expand;
ExpandableSection({this.expand = false, this.child});
#override
_ExpandableSectionState createState() => _ExpandableSectionState();
}
class _ExpandableSectionState extends State<ExpandableSection> with SingleTickerProviderStateMixin {
AnimationController animationController;
Animation<double> sizeAnimation;
Animation<double> opacityAnimation;
#override
void initState() {
super.initState();
prepareAnimations();
_runExpandCheck();
}
///Setting up the animation
void prepareAnimations() {
animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 300),);
sizeAnimation = CurvedAnimation(parent: animationController, curve: Curves.fastOutSlowIn,);
opacityAnimation = CurvedAnimation(parent: animationController, curve: Curves.slowMiddle,);
}
void _runExpandCheck() {
if(widget.expand) { animationController.forward(); }
else { animationController.reverse(); }
}
#override
void didUpdateWidget(ExpandableSection oldWidget) {
super.didUpdateWidget(oldWidget);
_runExpandCheck();
}
#override
void dispose() {
animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return FadeTransition(
opacity: opacityAnimation,
child: SizeTransition(
axisAlignment: 1.0,
sizeFactor: sizeAnimation,
child: widget.child
)
);
}
}
The RotatableSection class:
class RotatableSection extends StatefulWidget {
final Widget child;
final bool rotated;
final double initialSpin;
final double endingSpin;
RotatableSection({this.rotated = false, this.child, this.initialSpin=0, this.endingSpin=0.5});
#override
_RotatableSectionState createState() => _RotatableSectionState();
}
class _RotatableSectionState extends State<RotatableSection> with SingleTickerProviderStateMixin {
AnimationController animationController;
Animation<double> animation;
#override
void initState() {
super.initState();
prepareAnimations();
_runCheck();
}
final double _oneSpin = 6.283184;
///Setting up the animation
void prepareAnimations() {
animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 300),
lowerBound: _oneSpin * widget.initialSpin, upperBound: _oneSpin * widget.endingSpin, );
animation = CurvedAnimation( parent: animationController, curve: Curves.linear, );
}
void _runCheck() {
if(widget.rotated) { animationController.forward(); }
else { animationController.reverse(); }
}
#override
void didUpdateWidget(RotatableSection oldWidget) {
super.didUpdateWidget(oldWidget);
_runCheck();
}
#override
void dispose() {
animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animationController,
child: widget.child,
builder: (BuildContext context, Widget _widget) {
return new Transform.rotate(
angle: animationController.value,
child: _widget,
);
},
);
}
}
Use this package and follow my code. Hope this will help you :). Easy to use. https://pub.dev/packages/expansion_tile_card/example
final List<GlobalKey<ExpansionTileCardState>> cardKeyList = [];
... ListView.builder(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
cardKeyList.add(GlobalKey(debugLabel: "index :$index"));
return ExpansionTileCard(
title: Text('title'),
key: cardKeyList[index],
onExpansionChanged: (value) {
if (value) {
Future.delayed(const Duration(milliseconds: 500), () {
for (var i = 0; i < cardKeyList.length; i++) {
if (index != i) {
cardKeyList[i].currentState?.collapse();
}
}
});
}
},
);
}),
Use UniqueKey:
ExpansionTile(
key: UniqueKey(),
// Other properties
)
I've made a TreeView widget.
It uses ExpansionTile to simulate the hierarchy.
Each ExpansionTile could host a collection of ExpansionTile which can host ...etc.
Everything worked fine until I wanted to add 2 features : expand all / collapse all.
What helped me to overcame this problem is the GlobalKey.
My TreeView widget, is hosted in a page and is used with a global key.
I expose a VoidCallback. The implementation sets a new key in the setState method.
// TreeView host page
GlobalKey<TreeViewState> _key = GlobalKey();
void redrawWidgetCallback() {
setState(() {
// Triggers a rebuild of the whole TreeView.
_key = GlobalKey();
});
}
[...]
// In the Scaffold body :
TreeView(
key: _key,
treeViewItems: widget.treeViewItems,
redrawWidgetCallback: redrawWidgetCallback,
)
Then in my collapse/expand method in the widget, at the end, I call widget.redrawWidgetCallback.
No need to deal with a key for each level of the treeView : the root element widget is enough.
It may have perf issues / not the right way to go. But since my TreeView won't be used with more than 50 nodes, it's ok for me until I found a better solution which doesn't involve to create an ExpandableTile because I believe this behavior will be available oneday on the ExpansionTile itself.
PS : notice that this workaround doesn't run the expand animation.
Create a clone from ExpansionTile class and replace build method code by the following:
#override
Widget build(BuildContext context) {
final bool closed = !_isExpanded && _controller.isDismissed;
return AnimatedBuilder(
animation: _controller.view,
builder: _buildChildren,
child: closed ? null : GestureDetector(
child: Column(children: widget.children),
onTap: _handleTap,
),
);
}
and then ExpansionTile will collapse after click on each item.
Note:
if one of children has onTap call back, this solution doesn't work.
in this case you must provide onChildTap handler to pass index of tapped child in use case.(contact me for complete code)
I have modified the custom code, And its works fine for me.
Here is the solution.
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
const Duration _kExpand = Duration(milliseconds: 200);
/// A single-line [ListTile] with a trailing button that expands or collapses
/// the tile to reveal or hide the [children].
///
/// This widget is typically used with [ListView] to create an
/// "expand / collapse" list entry. When used with scrolling widgets like
/// [ListView], a unique [PageStorageKey] must be specified to enable the
/// [AppExpansionTile] to save and restore its expanded state when it is scrolled
/// in and out of view.
///
/// This class overrides the [ListTileTheme.iconColor] and [ListTileTheme.textColor]
/// theme properties for its [ListTile]. These colors animate between values when
/// the tile is expanded and collapsed: between [iconColor], [collapsedIconColor] and
/// between [textColor] and [collapsedTextColor].
///
/// See also:
///
/// * [ListTile], useful for creating expansion tile [children] when the
/// expansion tile represents a sublist.
/// * The "Expand and collapse" section of
/// <https://material.io/components/lists#types>
class AppExpansionTile extends StatefulWidget {
/// Creates a single-line [ListTile] with a trailing button that expands or collapses
/// the tile to reveal or hide the [children]. The [initiallyExpanded] property must
/// be non-null.
const AppExpansionTile({
GlobalKey<AppExpansionTileState>? key,
this.leading,
required this.title,
this.subtitle,
this.onExpansionChanged,
this.children = const <Widget>[],
this.trailing,
this.initiallyExpanded = false,
this.maintainState = false,
this.tilePadding,
this.expandedCrossAxisAlignment,
this.expandedAlignment,
this.childrenPadding,
this.backgroundColor,
this.collapsedBackgroundColor,
this.textColor,
this.collapsedTextColor,
this.iconColor,
this.collapsedIconColor,
}) : assert(initiallyExpanded != null),
assert(maintainState != null),
assert(
expandedCrossAxisAlignment != CrossAxisAlignment.baseline,
'CrossAxisAlignment.baseline is not supported since the expanded children '
'are aligned in a column, not a row. Try to use another constant.',
),
super(key: key);
/// A widget to display before the title.
///
/// Typically a [CircleAvatar] widget.
final Widget? leading;
/// The primary content of the list item.
///
/// Typically a [Text] widget.
final Widget title;
/// Additional content displayed below the title.
///
/// Typically a [Text] widget.
final Widget? subtitle;
/// Called when the tile expands or collapses.
///
/// When the tile starts expanding, this function is called with the value
/// true. When the tile starts collapsing, this function is called with
/// the value false.
final ValueChanged<bool>? onExpansionChanged;
/// The widgets that are displayed when the tile expands.
///
/// Typically [ListTile] widgets.
final List<Widget> children;
/// The color to display behind the sublist when expanded.
final Color? backgroundColor;
/// When not null, defines the background color of tile when the sublist is collapsed.
final Color? collapsedBackgroundColor;
/// A widget to display instead of a rotating arrow icon.
final Widget? trailing;
/// Specifies if the list tile is initially expanded (true) or collapsed (false, the default).
final bool initiallyExpanded;
/// Specifies whether the state of the children is maintained when the tile expands and collapses.
///
/// When true, the children are kept in the tree while the tile is collapsed.
/// When false (default), the children are removed from the tree when the tile is
/// collapsed and recreated upon expansion.
final bool maintainState;
/// Specifies padding for the [ListTile].
///
/// Analogous to [ListTile.contentPadding], this property defines the insets for
/// the [leading], [title], [subtitle] and [trailing] widgets. It does not inset
/// the expanded [children] widgets.
///
/// When the value is null, the tile's padding is `EdgeInsets.symmetric(horizontal: 16.0)`.
final EdgeInsetsGeometry? tilePadding;
/// Specifies the alignment of [children], which are arranged in a column when
/// the tile is expanded.
///
/// The internals of the expanded tile make use of a [Column] widget for
/// [children], and [Align] widget to align the column. The `expandedAlignment`
/// parameter is passed directly into the [Align].
///
/// Modifying this property controls the alignment of the column within the
/// expanded tile, not the alignment of [children] widgets within the column.
/// To align each child within [children], see [expandedCrossAxisAlignment].
///
/// The width of the column is the width of the widest child widget in [children].
///
/// When the value is null, the value of `expandedAlignment` is [Alignment.center].
final Alignment? expandedAlignment;
/// Specifies the alignment of each child within [children] when the tile is expanded.
///
/// The internals of the expanded tile make use of a [Column] widget for
/// [children], and the `crossAxisAlignment` parameter is passed directly into the [Column].
///
/// Modifying this property controls the cross axis alignment of each child
/// within its [Column]. Note that the width of the [Column] that houses
/// [children] will be the same as the widest child widget in [children]. It is
/// not necessarily the width of [Column] is equal to the width of expanded tile.
///
/// To align the [Column] along the expanded tile, use the [expandedAlignment] property
/// instead.
///
/// When the value is null, the value of `expandedCrossAxisAlignment` is [CrossAxisAlignment.center].
final CrossAxisAlignment? expandedCrossAxisAlignment;
/// Specifies padding for [children].
///
/// When the value is null, the value of `childrenPadding` is [EdgeInsets.zero].
final EdgeInsetsGeometry? childrenPadding;
/// The icon color of tile's [trailing] expansion icon when the
/// sublist is expanded.
///
/// Used to override to the [ListTileTheme.iconColor].
final Color? iconColor;
/// The icon color of tile's [trailing] expansion icon when the
/// sublist is collapsed.
///
/// Used to override to the [ListTileTheme.iconColor].
final Color? collapsedIconColor;
/// The color of the tile's titles when the sublist is expanded.
///
/// Used to override to the [ListTileTheme.textColor].
final Color? textColor;
/// The color of the tile's titles when the sublist is collapsed.
///
/// Used to override to the [ListTileTheme.textColor].
final Color? collapsedTextColor;
#override
AppExpansionTileState createState() => AppExpansionTileState();
}
class AppExpansionTileState extends State<AppExpansionTile>
with SingleTickerProviderStateMixin {
static final Animatable<double> _easeOutTween =
CurveTween(curve: Curves.easeOut);
static final Animatable<double> _easeInTween =
CurveTween(curve: Curves.easeIn);
static final Animatable<double> _halfTween =
Tween<double>(begin: 0.0, end: 0.5);
final ColorTween _borderColorTween = ColorTween();
final ColorTween _headerColorTween = ColorTween();
final ColorTween _iconColorTween = ColorTween();
final ColorTween _backgroundColorTween = ColorTween();
late AnimationController _controller;
late Animation<double> _iconTurns;
late Animation<double> _heightFactor;
late Animation<Color?> _borderColor;
late Animation<Color?> _headerColor;
late Animation<Color?> _iconColor;
late Animation<Color?> _backgroundColor;
bool _isExpanded = false;
#override
void initState() {
super.initState();
_controller = AnimationController(duration: _kExpand, vsync: this);
_heightFactor = _controller.drive(_easeInTween);
_iconTurns = _controller.drive(_halfTween.chain(_easeInTween));
_borderColor = _controller.drive(_borderColorTween.chain(_easeOutTween));
_headerColor = _controller.drive(_headerColorTween.chain(_easeInTween));
_iconColor = _controller.drive(_iconColorTween.chain(_easeInTween));
_backgroundColor =
_controller.drive(_backgroundColorTween.chain(_easeOutTween));
_isExpanded = PageStorage.of(context)?.readState(context) as bool? ??
widget.initiallyExpanded;
if (_isExpanded) _controller.value = 1.0;
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void expand() {
_isExpanded = true;
handleTap();
}
void collapse() {
_isExpanded = false;
handleTap();
}
#override
void didUpdateWidget(covariant AppExpansionTile oldWidget) {
if (widget.initiallyExpanded) {
expand();
} else {
collapse();
}
super.didUpdateWidget(oldWidget);
}
void handleTap() {
setState(() {
if (_isExpanded) {
_controller.forward();
} else {
_controller.reverse().then<void>((void value) {
if (!mounted) return;
setState(() {
// Rebuild without widget.children.
});
});
}
PageStorage.of(context)?.writeState(context, _isExpanded);
});
// if (widget.onExpansionChanged != null)
// widget.onExpansionChanged!(_isExpanded);
}
Widget _buildChildren(BuildContext context, Widget? child) {
final Color borderSideColor = _borderColor.value ?? Colors.transparent;
return Container(
decoration: BoxDecoration(
color: _backgroundColor.value ?? Colors.transparent,
border: Border(
top: BorderSide(color: borderSideColor),
bottom: BorderSide(color: borderSideColor),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTileTheme.merge(
iconColor: _iconColor.value,
textColor: _headerColor.value,
child: ListTile(
onTap: () {
if (widget.onExpansionChanged != null) {
widget.onExpansionChanged!(_isExpanded);
}
},
contentPadding: widget.tilePadding,
leading: widget.leading,
title: widget.title,
subtitle: widget.subtitle,
trailing: widget.trailing ??
RotationTransition(
turns: _iconTurns,
child: const Icon(Icons.expand_more),
),
),
),
ClipRect(
child: Align(
alignment: widget.expandedAlignment ?? Alignment.center,
heightFactor: _heightFactor.value,
child: child,
),
),
],
),
);
}
#override
void didChangeDependencies() {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
_borderColorTween.end = theme.dividerColor;
_headerColorTween
..begin = widget.collapsedTextColor ?? theme.textTheme.subtitle1!.color
..end = widget.textColor ?? colorScheme.secondary;
_iconColorTween
..begin = widget.collapsedIconColor ?? theme.unselectedWidgetColor
..end = widget.iconColor ?? colorScheme.secondary;
_backgroundColorTween
..begin = widget.collapsedBackgroundColor
..end = widget.backgroundColor;
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
final bool closed = !_isExpanded && _controller.isDismissed;
final bool shouldRemoveChildren = closed && !widget.maintainState;
final Widget result = Offstage(
child: TickerMode(
child: Padding(
padding: widget.childrenPadding ?? EdgeInsets.zero,
child: Column(
crossAxisAlignment:
widget.expandedCrossAxisAlignment ?? CrossAxisAlignment.center,
children: widget.children,
),
),
enabled: !closed,
),
offstage: closed,
);
return AnimatedBuilder(
animation: _controller.view,
builder: _buildChildren,
child: shouldRemoveChildren ? null : result,
);
}
}
Usage
late int _tileIndex=-1;
return AppExpansionTile(
title: Text(
'Tile $index',
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
initiallyExpanded: _tileIndex == index,
onExpansionChanged: (s) {
if (_tileIndex == index) {
_tileIndex = -1;
setState(() {});
} else {
setState(() {
_tileIndex = index!;
});
}
},
);
I think it is impossible with expansion tile but, there's a package named accordion and has much more comfortabilities.
Link:https://pub.dev/packages/accordion
For List of items using #simon solution
List<GlobalKey<AppExpansionTileState> > expansionTile;
instantiate your expansionTile
expansionTile=List<GlobalKey<AppExpansionTileState>>.generate(listItems.length, (index) => GlobalKey());
and use like so inside a ListView.builder()
key: expansionTile[index],
onExpansionChanged: (value) {
if (value) {
for (var tileKey in expansionTile) {
if (tileKey.currentState !=
expansionTile[index]
.currentState) {
tileKey.currentState.collapse();
} else {
tileKey.currentState.expand();
}
}
}
},
I'm learning Flutter and would like to make a Widget just like the built-in CircleAvatar. However, I would like the behaviour to be
specify both an Image (NetworkImage) and initials (ie, BB)
while the image isn't loaded, show the initials
if the image does load, show the image and remove the initials
The following code sort of works, but when used in the Chat demo it falls apart as multiple MyAvatars are added.
Breakpointing on initState shows that it is always called with the first message text that is entered - not what I expected.
It also flickers as images "reload". It appears that the widgets are being reused in a way I don't understand.
class MyAvatar extends StatefulWidget {
NetworkImage image;
MyAvatar({this.text}) {
debugPrint("MyAvatar " + this.text);
if (text.contains('fun')) {
this.image = new NetworkImage("https://cdn3.iconfinder.com/data/icons/minicons-for-web-sites/24/minicons2-14-512.png");
}
}
final String text;
#override
MyAvatarState createState() {
return new MyAvatarState();
}
}
class MyAvatarState extends State<MyAvatar> {
bool showImage = false;
#override
initState() {
super.initState();
if (widget.image != null) {
var completer = widget.image.load(widget.image);
completer.addListener((info, sync) {
setState(() {
showImage = true;
});
});
}
}
#override
Widget build(BuildContext context) {
return !showImage ? new CircleAvatar(radius: 40.0, child: new Text(widget.text[0]))
: new CircleAvatar(radius: 40.0, backgroundImage: widget.image);
}
}
I'm still having trouble - full code
import 'package:flutter/material.dart';
// Modify the ChatScreen class definition to extend StatefulWidget.
class ChatScreen extends StatefulWidget { //modified
ChatScreen() {
debugPrint("ChatScreen - called on hot reload");
}
#override //new
State createState() {
debugPrint("NOT on hot reload");
return new ChatScreenState();
} //new
}
// Add the ChatScreenState class definition in main.dart.
class ChatScreenState extends State<ChatScreen> {
final List<ChatMessage> _messages = <ChatMessage>[];
final TextEditingController _textController = new TextEditingController(); //new
ChatScreenState() {
debugPrint("ChatScreenState - not called on hot reload");
}
#override //new
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text("Friendlychat")),
body: new Column( //modified
children: <Widget>[ //new
new Flexible( //new
child: new ListView.builder( //new
padding: new EdgeInsets.all(8.0), //new
reverse: true, //new
itemBuilder: (_, int index) => _messages[index], //new
itemCount: _messages.length, //new
) //new
), //new
new Divider(height: 1.0), //new
new Container( //new
decoration: new BoxDecoration(
color: Theme.of(context).cardColor), //new
child: _buildTextComposer(), //modified
), //new
] //new
), //new
);
}
Widget _buildTextComposer() {
return new IconTheme(
data: new IconThemeData(color: Theme
.of(context)
.accentColor),
child:
new Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: new Row(
children: <Widget>[
new Container( //new
margin: new EdgeInsets.symmetric(horizontal: 4.0), //new
child: new IconButton( //new
icon: new Icon(Icons.send),
onPressed: () =>
_handleSubmitted(_textController.text)), //new
),
new Flexible(
child: new TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration: new InputDecoration.collapsed(
hintText: "Send a message"),
)
),
])
)
);
}
void _handleSubmitted(String text) {
_textController.clear();
ChatMessage message = new ChatMessage(text: text);
setState(() {
_messages.insert(0, message);
});
}
}
const String _name = "Hardcoded Name";
class ChatMessage extends StatelessWidget {
ChatMessage({this.text, this.image, this.useImage});
final String text;
final NetworkImage image;
final Map useImage;
#override
Widget build(BuildContext context) {
var use = true; //useImage != null && useImage['use'];
var image = new NetworkImage("https://cdn3.iconfinder.com/data/icons/minicons-for-web-sites/24/minicons2-14-512.png");
if (text.contains('bad')) {
image = new NetworkImage("https://cdn3.iconfinder.com/data/icons/minicons-for-web-sites/24/minicons2-14-512.pngz");
}
return new Container(
margin: const EdgeInsets.symmetric(vertical: 10.0),
child: new Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Container(
margin: const EdgeInsets.only(right: 16.0),
child : new CustomCircleAvatar(initials: text[0], myImage: image)
),
new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Text(_name, style: Theme.of(context).textTheme.subhead),
new Container(
margin: const EdgeInsets.only(top: 5.0),
child: new Text(text),
),
],
),
],
),
);
}
}
class CustomCircleAvatar extends StatefulWidget {
NetworkImage myImage;
String initials;
CustomCircleAvatar({this.myImage, this.initials}) {
debugPrint(initials);
}
#override
_CustomCircleAvatarState createState() => new _CustomCircleAvatarState();
}
class _CustomCircleAvatarState extends State<CustomCircleAvatar>{
bool _checkLoading = true;
#override
void initState() {
if (widget.myImage != null) {
widget.myImage.resolve(new ImageConfiguration()).addListener((image, sync) {
if (mounted && image != null) {
setState(() {
_checkLoading = false;
});
}
});
}
}
#override
Widget build(BuildContext context) {
return _checkLoading == true ? new CircleAvatar(child: new Text(widget.initials))
: new CircleAvatar(backgroundImage: widget.myImage);
}
}
Enter 'fun' as a message, then 'bad' as the second -
image
The idea is that depending on what you enter, different images might load (or not). In the 'failed to load' case, the initials should remain.
You can achieve this functionality by adding a listener to ImageStream that you can obtain from ImageConfiguration,
Here, I am feeding the same data to my ListView you can of course customize this yourself by adding a List of images and initials as a field in any class and use ListView.builder instead to be able to loop on them by index.
class CustomCircleAvatar extends StatefulWidget {
NetworkImage myImage;
String initials;
CustomCircleAvatar({this.myImage, this.initials});
#override
_CustomCircleAvatarState createState() => new _CustomCircleAvatarState();
}
class _CustomCircleAvatarState extends State<CustomCircleAvatar>{
bool _checkLoading = true;
#override
void initState() {
widget.myImage.resolve(new ImageConfiguration()).addListener((_, __) {
if (mounted) {
setState(() {
_checkLoading = false;
});
}
});
}
#override
Widget build(BuildContext context) {
return _checkLoading == true ? new CircleAvatar(
child: new Text(widget.initials)) : new CircleAvatar(
backgroundImage: widget.myImage,);
}
}
Now you can use it like this:
void main() {
runApp(new MaterialApp (home: new MyApp()));
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text("Custom Circle Avatar"),),
body: new ListView(children: new List.generate(20, (int index) {
return new Container(
height: 100.0,
width: 100.0,
child: new CustomCircleAvatar(myImage: new NetworkImage(
"https://www.doginni.cz/front_path/images/dog_circle.png"),
initials: "Dog",
),
);
}),),
);
}
}
This works really well and easy. Use the CachetNetworkImage and build the appropriate CircleAvatar.
return CachedNetworkImage(
httpHeaders: headers,
imageUrl: general.HOST + 'api/media/v2/' + id,
imageBuilder: (context, imageProvider) => new CircleAvatar(
radius: radius,
backgroundImage: imageProvider,
backgroundColor: backgroundColor),
errorWidget: (context, url, error) => CircleAvatar(
backgroundColor: backgroundColor,
radius: radius,
child: new Text(initials, style: textStyle,)),
);
The answer from #aziza was really the only one I could find on the topic for a while and it took me a while to read it and understand. I tried implementing it and there were some issues though I did get it to work eventually. I think I have a more readable (for me at least!)/up to date answer that might help someone stumbling upon this question:
class FallBackAvatar extends StatefulWidget {
final AssetImage image;
final String initials;
final TextStyle textStyle;
final Color circleBackground;
FallBackAvatar({#required this.image, #required this.initials, #required this.circleBackground, #required this.textStyle});
#override
_FallBackAvatarState createState() => _FallBackAvatarState();
}
class _FallBackAvatarState extends State<FallBackAvatar> {
bool _checkLoading = true;
#override
initState() {
super.initState();
// Add listeners to this class
ImageStreamListener listener = ImageStreamListener(_setImage, onError: _setError);
widget.image.resolve(ImageConfiguration()).addListener(listener);
}
void _setImage(ImageInfo image, bool sync) {
setState(() => _checkLoading = false);
//DO NOT DISPOSE IF IT WILL REBUILD (e.g. Sliver/Builder ListView)
dispose();
}
void _setError(dynamic dyn, StackTrace st) {
setState(() => _checkLoading = true);
dispose();
}
#override
Widget build(BuildContext context) {
return _checkLoading == true ? new CircleAvatar(
backgroundColor: widget.circleBackground,
child: new Text(widget.initials, style: widget.textStyle)) : new CircleAvatar(
backgroundImage: widget.image,
backgroundColor: widget.circleBackground,);
}
}
A couple of points, I'm manually disposing because I know after this there should be no more rebuilds (did you get the image? good! no more rebuilds unless you are part of a sliver or something OR did the image fail to load? well that's it then - no more rebuilds). This also handles the error case where the AssetImage (in my case, its Asset image but you could use any kind of image provider) is not there for whatever reason.
Second edit, because I have personal problems best left out of this answer. So I noticed that there was a slight delay in loading the profile images (like a second). But then the images came flooding in. Didn't like that transition so here is one with an AnimatedSwitcher:
class FallBackAvatar extends StatefulWidget {
final AssetImage image;
final String initials;
final TextStyle textStyle;
final Color circleBackground;
final double radius;
final int msAnimationDuration;
FallBackAvatar({#required this.image, #required this.initials, #required this.circleBackground, #required this.textStyle, #required this.radius, this.msAnimationDuration});
#override
_FallBackAvatarState createState() => _FallBackAvatarState();
}
class _FallBackAvatarState extends State<FallBackAvatar> {
bool _imgSuccess = false;
#override
initState() {
super.initState();
// Add listeners to this class
ImageStreamListener listener = ImageStreamListener(_setImage, onError: _setError);
widget.image.resolve(ImageConfiguration()).addListener(listener);
}
void _setImage(ImageInfo image, bool sync) {
setState(() => _imgSuccess = true);
}
void _setError(dynamic dyn, StackTrace st) {
setState(() => _imgSuccess = false);
dispose();
}
Widget _fallBackAvatar() {
return Container(
height: widget.radius*2,
width: widget.radius*2,
decoration: BoxDecoration(
color: widget.circleBackground,
borderRadius: BorderRadius.all(Radius.circular(widget.radius))
),
child: Center(child: Text(widget.initials, style: widget.textStyle))
);
}
Widget _avatarImage() {
return CircleAvatar(
backgroundImage: widget.image,
backgroundColor: widget.circleBackground
);
}
#override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: Duration(milliseconds: widget.msAnimationDuration ?? 500),
child: _imgSuccess ? _avatarImage() : _fallBackAvatar(),
);
}
}
Actually the code can be even simpler:
if you want to put a text when the image is unavailable you should simply use foregroundImage instead of backgroundImage.
The text will displayed by default, when the image is loaded it will cover the text without having to deal with image loading status etc.
If you need to know if the image had an error you can intercept it with onForegroundImageError.
Example function:
Widget CircleAvatarTest(
{String? imageUrl,
String? text,
double radius = 35,
Color? backgroundColor}) {
return CircleAvatar(
radius: radius,
child: (text != null)
? Center(
child: Text(text,
style: TextStyle(
color: Colors.white,
fontSize: radius * 2 / text.length - 10,
)),
)
: null,
foregroundImage: imageUrl == null ? null : NetworkImage(imageUrl),
backgroundColor: backgroundColor,
//onForegroundImageError: (e,trace){/*....*/},
);
}
Here is the sample with stacked architecture where fallback is person icon.
ViewBuilder and ViewModel are just extended widgets from stacked architecture alternatives. #swidget is functional widget. You can achieve the same functionality via StatefulWidget.
#swidget
Widget avatarView({String userId, double radius = 24}) =>
ViewBuilder<AvatarViewModel>(
viewModelBuilder: () => AvatarViewModel(),
builder: (model) => CircleAvatar(
radius: radius,
backgroundColor: CColors.blackThird,
backgroundImage: NetworkImage(
Config.photoUrl + userId ?? userService.id,
),
child: model.isFailed ? Icon(EvaIcons.person, size: radius) : null,
onBackgroundImageError: (e, _) => model.isFailed = e != null,
),
);
class AvatarViewModel extends ViewModel {
bool _isFailed = false;
bool get isFailed => _isFailed;
set isFailed(bool isFailed) {
_isFailed = isFailed;
notifyListeners();
}
}