I am developing a calculation app where a variable is increased/decreased by a plus button and a minus button. I would like to implement a continuous increment/decrement when long-pressing (holding) the plus or minus button.
How can this be done with dart/flutter?
Just a quick sample code.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: NewCode(),
debugShowCheckedModeBanner: false,
);
}
}
class NewCode extends StatefulWidget {
#override
_NewCodeState createState() => _NewCodeState();
}
class _NewCodeState extends State<NewCode> {
Timer timer;
var value = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
child: Container(
width: 200,
height: 200,
color: Colors.blue,
child: Center(
child: Text(
'value $value',
style: TextStyle(fontSize: 40),
)),
),
onTapDown: (TapDownDetails details) {
print('down');
timer = Timer.periodic(Duration(milliseconds: 500), (t) {
setState(() {
value++;
});
print('value $value');
});
},
onTapUp: (TapUpDetails details) {
print('up');
timer.cancel();
},
onTapCancel: () {
print('cancel');
timer.cancel();
},
),
),
);
}
}
For those who have problems with above answer's code where timer.cancel() does not work. Depending on what state management method you are using (in my case Riverpod 2.0), a widget rebuild will cause the assigned timer variable to be null again, thus, cancel() method will not work (since it is null then). The initial timer will continue to run since the reference gets lost.
Related
App is a simple memory/guessing game with a grid of squares. Floating action button triggers a "New game" dialog, and a Yes response triggers setState() on the main widget. The print() calls show it is building all the Tile widgets in the grid, but as it returns, the old grid values are still showing. Probably done something stupid but not seeing it. Basic code is below. TIA if anyone can see what is missing/invalid/broken/etc.
Main.dart is the usual main() that creates a stateless HomePage which creates a stateful widget which uses this State:
class MemHomePageState extends State<MemHomePage> {
GameBoard gameBoard = GameBoard();
GameController? gameController;
int gameCount = 0, winCount = 0;
#override
void initState() {
super.initState();
gameController = GameController(gameBoard, this);
}
#override
Widget build(BuildContext context) {
if (kDebugMode) {
print("MemHomepageState::build");
}
gameBoard.newGame(); // Resets secrets and grids
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: GridView.count(
crossAxisCount: Globals.num_columns,
children: List.generate(Globals.num_columns * Globals.num_rows, (index) {
int x = index~/Globals.NR, y = index%Globals.NR;
int secret = gameBoard.secretsGrid![x][y];
var t = Tile(x, y, Text('$secret'), gameController!);
gameBoard.tilesGrid![x].add(t);
if (kDebugMode) {
print("Row $x is ${gameBoard.secretsGrid![x]} ${gameBoard.tilesGrid![x][y].secret}");
}
return t;
}),
),
// Text("You have played $gameCount games and won $winCount."),
),
floatingActionButton: FloatingActionButton(
onPressed: () => newGameDialog("Start a new game?"),
tooltip: 'New game?',
child: const Icon(Icons.refresh_outlined),
),
);
}
/** Called from the FAB and also from GameController "won" logic */
void newGameDialog(String message) {
showDialog<void>(
context: context,
barrierDismissible: false, // means the user must tap a button to exit the Alert Dialog
builder: (BuildContext context) {
return AlertDialog(
title: Text("New game?"),
content: Text(message),
//),
actions: <Widget>[
TextButton(
child: const Text('Yes'),
onPressed: () {
setState(() {
gameCount++;
});
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('No'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
}
);
}
The Tile class is a StatefulWidget whose state determines what that particular tile should show:
import 'package:flutter/material.dart';
import 'gamecontroller.dart';
enum TileMode {
SHOWN,
HIDDEN,
CLEARED,
}
/// Represents one Tile in the game
class Tile extends StatefulWidget {
final int x, y;
final Widget secret;
final GameController gameController;
TileState? tileState;
Tile(this.x, this.y, this.secret, this.gameController, {super.key});
#override
State<Tile> createState() => TileState(x, y, secret);
setCleared() {
tileState!.setCleared();
}
}
class TileState extends State<Tile> {
final int x, y;
final Widget secret;
TileMode tileMode = TileMode.HIDDEN;
TileState(this.x, this.y, this.secret);
_unHide() {
setState(() => tileMode = TileMode.SHOWN);
widget.gameController.clicked(widget);
}
reHide() {
print("rehiding");
setState(() => tileMode = TileMode.HIDDEN);
}
setCleared() {
print("Clearing");
setState(() => tileMode = TileMode.CLEARED);
}
_doNothing() {
//
}
#override
Widget build(BuildContext context) {
switch(tileMode) {
case TileMode.HIDDEN:
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
),
onPressed: _unHide,
child: Text(''));
case TileMode.SHOWN:
return ElevatedButton(
onPressed: _doNothing,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
child: secret);
case TileMode.CLEARED:
return ElevatedButton(
onPressed: _doNothing,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black12,
),
child: const Icon(Icons.check));
}
}
}
it looks like you are calling the following in your build function. That would cause everything to reset everytime it builds. Perhaps it belongs in init instead?
gameBoard.newGame(); // Resets secrets and grids
The original problem is that the Tile objects, although correctly created and connected to the returned main widget, did not have distinct 'key' values so they were not replacing the originals. Adding 'key' to the Tile constructor and 'key: UniqueKey()' to each Tile() in the loop, solved this problem. It exposed a related problem but is out of scope for this question. See the github link in the OP for the latest version.
I want to listen to some changes to the stream's event. Now for that, I am doing it with the provider's StreamProvider.value(). It is an API call. Now what I want is that if the stream's event comes as loading state I want to show loading, If there is any failure I want to call the failure method. If there is a success then I want to call the success method.
I have created a separate widget button which is Consumer that changes according to the stream's event. I have added SchedulerBinding.instance.addPostFrameCallback() inside the consumer's builder method, but it is getting called many times as widget rebuilds and it fires the last event, which is undesirable.
So my question is on stream's event change how can I call those methods or show toast and update UI also?
ConsumerButtonWithProgress
class ConsumerButtonWithProgress<T> extends StatelessWidget {
final void Function(BuildContext context) onTap;
final void Function(T result) onSuccess;
final void Function() onError;
final void Function() onNoInternet;
ConsumerButtonWithProgress(
this.onTap,
this.onSuccess, {
this.onError,
this.onNoInternet,
}) : assert(T != dynamic);
#override
Widget build(BuildContext context) {
return Consumer<APIResult<T>>(
builder: (BuildContext context, APIResult<T> value, Widget child) {
SchedulerBinding.instance.addPostFrameCallback((_) {
if (value != null) {
if (value.apiResultType == APIResultType.NO_INTERNET) {
showSnackBar(context, value.message);
if (onNoInternet != null) Function.apply(onNoInternet, []);
} else if (value.apiResultType == APIResultType.FAILURE) {
showSnackBar(context, value.message);
if (onError != null) Function.apply(onError, []);
} else if (value.apiResultType == APIResultType.SUCCESS) {
showSnackBar(context, value.message);
Function.apply(onSuccess, [value.data]);
}
}
});
return Container(
width: MediaQuery.of(context).size.width,
height: Dimensions.h50,
child: OutlineButton(
color: LocalColors.PRIMARY_COLOR,
textColor: LocalColors.PRIMARY_COLOR,
disabledBorderColor: LocalColors.PRIMARY_COLOR,
highlightedBorderColor: LocalColors.PRIMARY_COLOR,
borderSide: BorderSide(color: LocalColors.PRIMARY_COLOR),
child: APIResult.isLoading(value)
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: Dimensions.h25,
height: Dimensions.h25,
child: CircularProgressIndicator(
strokeWidth: 3,
)),
],
)
: Text(
"Button",
style: TextStyle(
fontSize: Dimensions.sp15,
fontWeight: FontAsset.DEMI_BOLD,
),
),
onPressed: () {
if (onTap != null &&
(value == null ||
value.apiResultType != APIResultType.LOADING))
Function.apply(onTap, [context]);
}),
);
},
);
}
}
And this is how I use that button in my widget
class ForgotPasswordScreen extends StatefulWidget {
#override
_ForgotPasswordScreenState createState() => _ForgotPasswordScreenState();
}
class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
ForgotPasswordProvider _forgotPasswordProvider;
#override
void initState() {
super.initState();
_forgotPasswordProvider = ForgotPasswordProvider();
}
#override
Widget build(BuildContext mainContext) {
return Scaffold(body: Builder(builder: (BuildContext context) {
return StreamProvider.value(
lazy: true,
value: _forgotPasswordProvider.forgotPasswordStream,
child: ConsumerButtonWithProgress<SignInResponseEntity>(
_onSendLinkClick,
(result) {},
),
);
}));
}
void _onSendLinkClick(BuildContext context) {
if (_isDataValid(context)) {
_forgotPasswordProvider.callForgotPasswordAPI();
}
}
}
So is there any way in which I can call methods and show toast from the common widget after update in the stream's event and also update the UI? Just like the executing the code inside the SchedulerBinding.instance.addPostFrameCallback() block.
I have a GestureDetector that need to launch a url. But if the gesture gets multiple taps, then launch is called multiple times.
In the current code im trying to use a state _isButtonTapped to control the tap. But the .whenComplete is somehow call before the launch is preformed?
_isButtonTapped = false
Widget _buildButton(String key, Text title, String url) {
_onTapped() async {
if (await canLaunch(url)) {
launch(url).whenComplete(
() => setState(() {
_isButtonTapped = false;
}),
);
}
}
return GestureDetector(
onTap: () {
_isButtonTapped ? null : _onTapped();
setState(() {
_isButtonTapped = true;
});
},
child: Container(
child: Padding(
padding: EdgeInsets.all(6.0),
child: Center(child: title),
),
),
);
}
Try this:
class _HomePageState extends State<HomePage> {
bool _isButtonTapped = false;
String _url = "https://google.ca";
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
color: Colors.blue,
width: 100,
height: 100,
child: GestureDetector(
onTap: () async {
if (!_isButtonTapped) { // only allow click if it is false
_isButtonTapped = true; // make it true when clicked
if (await canLaunch(_url)) {
await launch(_url);
_isButtonTapped = false; // once url is launched successfully, we again make it false, allowing tapping again
}
}
},
),
),
),
);
}
}
Try this? It should solve your problem.
class SafeOnTap extends StatefulWidget {
SafeOnTap({
Key? key,
required this.child,
required this.onSafeTap,
this.intervalMs = 500,
}) : super(key: key);
final Widget child;
final GestureTapCallback onSafeTap;
final int intervalMs;
#override
_SafeOnTapState createState() => _SafeOnTapState();
}
class _SafeOnTapState extends State<SafeOnTap> {
int lastTimeClicked = 0;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
final now = DateTime.now().millisecondsSinceEpoch;
if (now - lastTimeClicked < widget.intervalMs) {
return;
}
lastTimeClicked = now;
widget.onSafeTap();
},
child: widget.child,
);
}
}
You can wrap any kind of widget if you want.
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(
children: [
// every click need to wait for 500ms
SafeOnTap(
onSafeTap: () => log('500ms'),
child: Container(
width: double.infinity,
height: 200,
child: Center(child: Text('500ms click me')),
),
),
// every click need to wait for 2000ms
SafeOnTap(
intervalMs: 2000,
onSafeTap: () => log('2000ms'),
child: Container(
width: double.infinity,
height: 200,
child: Center(child: Text('2000ms click me')),
),
),
],
),
),
),
);
}
}
the easiest way is in inkWell widget put doubleTap: () {},
it will do nothing, when user click multiple time
You have a bug in your code.
You are setting _isButtonTapped to true everytime you press it.
Correct you onTap function:
return GestureDetector(
onTap: () {
if (_isButtonTapped == false){
_onTapped();
setState(() {
_isButtonTapped = true;
});
},
}
//...
Regarding why the whenComplete is not beign called when you expected, that's another problem. I never used it but tacking a quick look into the docs (https://api.flutter.dev/flutter/scheduler/TickerFuture/whenComplete.html) show us that are multiple ways of achiving this, including wraping the function in an try block and use thr finally cloused as the whenCompleted. You should take a look at he docs and tried it out. Can't help more with that detail.
Hope it helps you.
I'm only able to run the animation when the object is tapped in this way?
class _MyHomePageState extends State<MyHomePage> {
bool isOpen = false;
#override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
setState(() {
isOpen = !isOpen;
});
},
child: FlareActor('assets/heart.flr',
animation: isOpen ? 'run' : 'x'),
),
);
}
}
but what I need is to run the same animation on every tap, this doesn't work:
GestureDetector(
onTap: () {
setState(() {
});
},
child: FlareActor('assets/heart.flr',
animation: 'run'),
)
I also tried this:
GestureDetector(
onTap: () {
setState(() {
isOpen = !isOpen;
});
},
child: FlareActor('assets/heart.flr',
animation: isOpen ? 'run' : 'run'),
)
I recently had the same problem when I was implementing some things. And I got my question answered, You can use the FlareControls and just call play instead of using the keys to play the animation. Original post.
class _MyHomePageState extends State<MyHomePage> {
// Store a reference to some controls for the Flare widget
final FlareControls controls = FlareControls();
void _playSuccessAnimation() {
// Use the controls to trigger an animation.
controls.play("run");
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: FlareActor("assets/heart.flr",
animation: "run",
fit: BoxFit.contain,
alignment: Alignment.center,
// Make sure you use the controls with the Flare Actor widget.
controller: controls),
floatingActionButton: FloatingActionButton(
onPressed: _playSuccessAnimation,
tooltip: 'Play',
child: Icon(Icons.play_arrow),
),
);
}
}
I also have a video explaining how to integrate these animations and replay the same one.
I am learning app development on Flutter and can't get my Slider to work within the AlertDialog. It won't change it's value.
I did search the problem and came across this post on StackOverFlow:
Flutter - Why slider doesn't update in AlertDialog?
I read it and have kind of understood it. The accepted answer says that:
The problem is, dialogs are not built inside build method. They are on a different widget tree. So when the dialog creator updates, the dialog won't.
However I am not able to understand how exactly does it have to be implemented as not enough background code is provided.
This is what my current implementation looks like:
double _fontSize = 1.0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(qt.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.format_size),
onPressed: () {
getFontSize(context);
},
),
],
),
body: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 15.0),
itemCount: 3,
itemBuilder: (context, index) {
if (index == 0) {
return _getListTile(qt.scripture, qt.reading);
} else if (index == 1) {
return _getListTile('Reflection:', qt.reflection);
} else {
return _getListTile('Prayer:', qt.prayer);
}
})
);
}
void getFontSize(BuildContext context) {
showDialog(context: context,builder: (context){
return AlertDialog(
title: Text("Font Size"),
content: Slider(
value: _fontSize,
min: 0,
max: 100,
divisions: 5,
onChanged: (value){
setState(() {
_fontSize = value;
});
},
),
actions: <Widget>[
RaisedButton(
child: Text("Done"),
onPressed: (){},
)
],
);
});
}
Widget parseLargeText(String text) {...}
Widget _getListTile(String title, String subtitle) {...}
I understand that I will need to make use of async and await and Future. But I am not able to understand how exactly. I've spent more than an hour on this problem and can't any more. Please forgive me if this question is stupid and noobish. But trust me, I tried my best.
Here is a minimal runnable example. Key points:
The dialog is a stateful widget that stores the current value in its State. This is important because dialogs are technically separate "pages" on your app, inserted higher up in the hierarchy
Navigator.pop(...) to close the dialog and return the result
Usage of async/await
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
double _fontSize = 20.0;
void _showFontSizePickerDialog() async {
// <-- note the async keyword here
// this will contain the result from Navigator.pop(context, result)
final selectedFontSize = await showDialog<double>(
context: context,
builder: (context) => FontSizePickerDialog(initialFontSize: _fontSize),
);
// execution of this code continues when the dialog was closed (popped)
// note that the result can also be null, so check it
// (back button or pressed outside of the dialog)
if (selectedFontSize != null) {
setState(() {
_fontSize = selectedFontSize;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Font Size: ${_fontSize}'),
RaisedButton(
onPressed: _showFontSizePickerDialog,
child: Text('Select Font Size'),
)
],
),
),
);
}
}
// move the dialog into it's own stateful widget.
// It's completely independent from your page
// this is good practice
class FontSizePickerDialog extends StatefulWidget {
/// initial selection for the slider
final double initialFontSize;
const FontSizePickerDialog({Key key, this.initialFontSize}) : super(key: key);
#override
_FontSizePickerDialogState createState() => _FontSizePickerDialogState();
}
class _FontSizePickerDialogState extends State<FontSizePickerDialog> {
/// current selection of the slider
double _fontSize;
#override
void initState() {
super.initState();
_fontSize = widget.initialFontSize;
}
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Font Size'),
content: Container(
child: Slider(
value: _fontSize,
min: 10,
max: 100,
divisions: 9,
onChanged: (value) {
setState(() {
_fontSize = value;
});
},
),
),
actions: <Widget>[
FlatButton(
onPressed: () {
// Use the second argument of Navigator.pop(...) to pass
// back a result to the page that opened the dialog
Navigator.pop(context, _fontSize);
},
child: Text('DONE'),
)
],
);
}
}
You just need to warp the AlertDialog() with a StatefulBuilder()