I want to execute a method while a user is pressing down on a button. In pseudocode:
while (button.isPressed) {
executeCallback();
}
In other words, the executeCallback method should fire repeatedly as long as the user is pressing down on the button, and stop firing when the button is released. How can I achieve this in Flutter?
Use a Listener and a stateful widget. I also introduced a slight delay after every loop:
import 'dart:async';
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(brightness: Brightness.dark),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
bool _buttonPressed = false;
bool _loopActive = false;
void _increaseCounterWhilePressed() async {
// make sure that only one loop is active
if (_loopActive) return;
_loopActive = true;
while (_buttonPressed) {
// do your thing
setState(() {
_counter++;
});
// wait a bit
await Future.delayed(Duration(milliseconds: 200));
}
_loopActive = false;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Listener(
onPointerDown: (details) {
_buttonPressed = true;
_increaseCounterWhilePressed();
},
onPointerUp: (details) {
_buttonPressed = false;
},
child: Container(
decoration: BoxDecoration(color: Colors.orange, border: Border.all()),
padding: EdgeInsets.all(16.0),
child: Text('Value: $_counter'),
),
),
),
);
}
}
A simpler way, without the listener, is as follows:
GestureDetector(
child: InkWell(
child: Icon(Icons.skip_previous_rounded),
onTap: widget.onPrevious,
),
onLongPressStart: (_) async {
isPressed = true;
do {
print('long pressing'); // for testing
await Future.delayed(Duration(seconds: 1));
} while (isPressed);
},
onLongPressEnd: (_) => setState(() => isPressed = false),
);
}
Building on the solution from ThinkDigital, my observation is that InkWell contains all the events necessary to do this without an extra GestureDetector (I find that the GestureDetector interferes with the ink animation on long press). Here's a control I implemented for a pet project that fires its event with a decreasing delay when held (this is a rounded button with an icon, but anything using InkWell will do):
/// A round button with an icon that can be tapped or held
/// Tapping the button once simply calls [onUpdate], holding
/// the button will repeatedly call [onUpdate] with a
/// decreasing time interval.
class TapOrHoldButton extends StatefulWidget {
/// Update callback
final VoidCallback onUpdate;
/// Minimum delay between update events when holding the button
final int minDelay;
/// Initial delay between change events when holding the button
final int initialDelay;
/// Number of steps to go from [initialDelay] to [minDelay]
final int delaySteps;
/// Icon on the button
final IconData icon;
const TapOrHoldButton(
{Key? key,
required this.onUpdate,
this.minDelay = 80,
this.initialDelay = 300,
this.delaySteps = 5,
required this.icon})
: assert(minDelay <= initialDelay,
"The minimum delay cannot be larger than the initial delay"),
super(key: key);
#override
_TapOrHoldButtonState createState() => _TapOrHoldButtonState();
}
class _TapOrHoldButtonState extends State<TapOrHoldButton> {
/// True if the button is currently being held
bool _holding = false;
#override
Widget build(BuildContext context) {
var shape = CircleBorder();
return Material(
color: Theme.of(context).dividerColor,
shape: shape,
child: InkWell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
widget.icon,
color:
Theme.of(context).textTheme.headline1?.color ?? Colors.white70,
size: 36,
),
),
onTap: () => _stopHolding(),
onTapDown: (_) => _startHolding(),
onTapCancel: () => _stopHolding(),
customBorder: shape,
),
);
}
void _startHolding() async {
// Make sure this isn't called more than once for
// whatever reason.
if (_holding) return;
_holding = true;
// Calculate the delay decrease per step
final step =
(widget.initialDelay - widget.minDelay).toDouble() / widget.delaySteps;
var delay = widget.initialDelay.toDouble();
while (_holding) {
widget.onUpdate();
await Future.delayed(Duration(milliseconds: delay.round()));
if (delay > widget.minDelay) delay -= step;
}
}
void _stopHolding() {
_holding = false;
}
}
Here it is in action:
To improve Elte Hupkes's solution, I fixed an issue where the number of clicks and the number of calls to the onUpdate callback did not match when tapping consecutively.
_tapDownCount variable is additionally used.
import 'package:flutter/material.dart';
/// A round button with an icon that can be tapped or held
/// Tapping the button once simply calls [onUpdate], holding
/// the button will repeatedly call [onUpdate] with a
/// decreasing time interval.
class TapOrHoldButton extends StatefulWidget {
/// Update callback
final VoidCallback onUpdate;
/// Minimum delay between update events when holding the button
final int minDelay;
/// Initial delay between change events when holding the button
final int initialDelay;
/// Number of steps to go from [initialDelay] to [minDelay]
final int delaySteps;
/// Icon on the button
final IconData icon;
const TapOrHoldButton(
{Key? key,
required this.onUpdate,
this.minDelay = 80,
this.initialDelay = 300,
this.delaySteps = 5,
required this.icon})
: assert(minDelay <= initialDelay, "The minimum delay cannot be larger than the initial delay"),
super(key: key);
#override
_TapOrHoldButtonState createState() => _TapOrHoldButtonState();
}
class _TapOrHoldButtonState extends State<TapOrHoldButton> {
/// True if the button is currently being held
bool _holding = false;
int _tapDownCount = 0;
#override
Widget build(BuildContext context) {
var shape = const CircleBorder();
return Material(
color: Theme.of(context).dividerColor,
shape: shape,
child: InkWell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
widget.icon,
color: Theme.of(context).textTheme.headline1?.color ?? Colors.white70,
size: 36,
),
),
onTap: () => _stopHolding(),
onTapDown: (_) => _startHolding(),
onTapCancel: () => _stopHolding(),
customBorder: shape,
),
);
}
void _startHolding() async {
// Make sure this isn't called more than once for
// whatever reason.
widget.onUpdate();
_tapDownCount += 1;
final int myCount = _tapDownCount;
if (_holding) return;
_holding = true;
// Calculate the delay decrease per step
final step = (widget.initialDelay - widget.minDelay).toDouble() / widget.delaySteps;
var delay = widget.initialDelay.toDouble();
while (true) {
await Future.delayed(Duration(milliseconds: delay.round()));
if (_holding && myCount == _tapDownCount) {
widget.onUpdate();
} else {
return;
}
if (delay > widget.minDelay) delay -= step;
}
}
void _stopHolding() {
_holding = false;
}
}
USING SETSTATE
You can achieve this by simply "onLongPressStart" and "onLongPressEnd" properties of a button.
In case you can't find "onLongPressStart" / "onLongPressEnd" properties in your widget, wrap your widget with the "GestureDetector" widget.
GestureDetector(
child: ..,
onLongPressStart: (_) async {
isTap = true;
do {
await Future.delayed(Duration(seconds: 1));
} while (isTap );
},
onLongPressEnd: (_) => setState(() => isTap = false),
);
}
Related
I want to check for internet connection at every screen on my app just like Telegram does and whenever user goes offline, show an Offline banner on the top of the screen.
I have tried using connectivity_plus and internet_connection_checker plugins to check for this but the problem is I have to subscribe to a stream for this and if the device goes offline once then there is no way to subscribe to it again without clicking a button.
getConnectivity() =>
subscription = Connectivity().onConnectivityChanged.listen(
(ConnectivityResult result) async {
isDeviceConnected = await InternetConnectionChecker().hasConnection;
if (!isDeviceConnected && isAlertSet == false) {
setState(() {
constants.offline = true;
print('Constants().offline ${constants.offline}');
isAlertSet = true;
});
}
print('off');
},
);
I'm using this code right now to check this issue but I don't want to replicate this code on each and every screen and even if I do replicate it then there will be a lot of subscriptions that I'll be subscribing to, which will mean that all the subscriptions will be disposed at the same time causing all sorts of issues.
If you have custom Scaffold, then you have to edit it. Otherwise, create a new one and change all Scaffolds to your custom one. This allows you to easily apply changes that should be on all pages.
Then, in the CustomScaffold create a Stack that contains page content and ValueListenableBuilder that listens to connection changes and if there is no internet displays error banner.
class CustomScaffold extends StatefulWidget {
const CustomScaffold({Key? key}) : super(key: key);
#override
State<CustomScaffold> createState() => _CustomScaffoldState();
}
class _CustomScaffoldState extends State<CustomScaffold> with WidgetsBindingObserver {
StreamSubscription? connectivitySubscription;
ValueNotifier<bool> isNetworkDisabled = ValueNotifier(false);
void _checkCurrentNetworkState() {
Connectivity().checkConnectivity().then((connectivityResult) {
isNetworkDisabled.value = connectivityResult == ConnectivityResult.none;
});
}
initStateFunc() {
_checkCurrentNetworkState();
connectivitySubscription = Connectivity().onConnectivityChanged.listen(
(ConnectivityResult result) {
isNetworkDisabled.value = result == ConnectivityResult.none;
},
);
}
#override
void initState() {
WidgetsBinding.instance.addObserver(this);
initStateFunc();
super.initState();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
_checkCurrentNetworkState();
}
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
connectivitySubscription?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
Scaffold(
...
),
ValueListenableBuilder(
valueListenable: isNetworkDisabled,
builder: (_, bool networkDisabled, __) =>
Visibility(
visible: networkDisabled,
child: YourErrorBanner(),
),
),
],
);
}
}
First I created an abstract class called BaseScreenWidget
used bloc state management to listen each time the internet connection changed then show toast or show upper banner with Blocbuilder
abstract class BaseScreenWidget extends StatelessWidget {
const BaseScreenWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Column(
children: [
baseBuild(context),
BlocConsumer<InternetConnectionBloc, InternetConnectionState>(
listener: (context, state) {
// if (!state.isConnected) {
// showToast("No Internet Connection");
// }
},
builder: (context, state) {
if (!state.isConnected) {
return const NoInternetWidget();
}
return const SizedBox.shrink();
},
),
],
);
}
Widget baseBuild(BuildContext context);
}
Made each screen only screen widgets contains Scaffold to extends BaseScreenWidget
class MainScreen extends BaseScreenWidget {
const MainScreen({super.key});
#override
Widget baseBuild(BuildContext context) {
return const Scaffold(
body: MainScreenBody(),
);
}
}
it's very helpful to wrap the Column with SafeArea in the build method in BaseScreen.
USE THIS SIMPLE TECHNIQUE only need this package: Internet Connection Checker. If you turn off your network it will tell you
connection_checker.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:internet_connection_checker/internet_connection_checker.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
class CheckMyConnection {
static bool isConnect = false;
static bool isInit = false;
static hasConnection(
{required void Function() hasConnection,
required void Function() noConnection}) async {
Timer.periodic(const Duration(seconds: 1), (_) async {
isConnect = await InternetConnectionChecker().hasConnection;
if (isInit == false && isConnect == true) {
isInit = true;
hasConnection.call();
} else if (isInit == true && isConnect == false) {
isInit = false;
noConnection.call();
}
});
}
}
base.dart
import 'package:flutter/material.dart';
import 'connection_checker.dart';
class Base extends StatefulWidget {
final String title;
const Base({Key? key, required this.title}) : super(key: key);
#override
State<Base> createState() => _BaseState();
}
class _BaseState extends State<Base> {
final snackBar1 = SnackBar(
content: const Text(
'Internet Connected',
style: TextStyle(color: Colors.white),
),
backgroundColor: Colors.green,
);
final snackBar2 = SnackBar(
content: const Text(
'No Internet Connection',
style: TextStyle(color: Colors.white),
),
backgroundColor: Colors.red,
);
#override
void initState() {
super.initState();
CheckMyConnection.hasConnection(hasConnection: () {
ScaffoldMessenger.of(navigatorKey.currentContext!)
.showSnackBar(snackBar1);
}, noConnection: () {
ScaffoldMessenger.of(navigatorKey.currentContext!)
.showSnackBar(snackBar2);
});
}
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
key: navigatorKey,
appBar: AppBar(
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
title: const Text('Tabs Demo'),
),
body: const TabBarView(
children: [
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
],
),
),
);
}
}
I myself use connectivity_plus and I have never found the problem you mentioned (if the device goes offline once then there is no way to subscribe to it again without clicking a button), you can use my example.
If the user's internet is disconnected, a modal will appear. If the user is connected again, the modal will be deleted automatically.
Anyway, I put the option to check the internet again in the modal
class CheckConnectionStream extends GetxController {
bool isModalEnable = false;
final loadingCheckConnectivity = false.obs;
ConnectivityResult _connectionStatus = ConnectivityResult.none;
final Connectivity _connectivity = Connectivity();
late StreamSubscription<ConnectivityResult> _connectivitySubscription;
Future<void> initConnectivity() async {
late ConnectivityResult result;
try {
result = await _connectivity.checkConnectivity();
loadingCheckConnectivity.value = false;
} on PlatformException {
return;
}
return _updateConnectionStatus(result);
}
Future<void> _updateConnectionStatus(ConnectivityResult result) async {
_connectionStatus = result;
if (result == ConnectivityResult.none) {
if (isModalEnable != true) {
isModalEnable = true;
showDialogIfNotConnect();
}
} else {
if (isModalEnable) {
Get.back();
}
isModalEnable = false;
}
}
showDialogIfNotConnect() {
Get.defaultDialog(
barrierDismissible: false,
title: "check your network".tr,
onWillPop: () async {
return false;
},
middleText: "Your device is not currently connected to the Internet".tr,
titleStyle: TextStyle(
color: Get.isDarkMode ? Colors.white : Colors.black,
),
middleTextStyle: TextStyle(
color: Get.isDarkMode ? Colors.white : Colors.black,
),
radius: 30,
actions: [
Obx(() => loadingCheckConnectivity.value
? const CustomLoading(
height: 30.0,
radius: 30.0,
)
: ElevatedButton(
onPressed: () async {
loadingCheckConnectivity.value = true;
EasyDebounce.debounce(
'check connectivity',
const Duration(milliseconds: 1000), () async {
await initConnectivity();
});
},
child: Text(
'try again'.tr,
style: const TextStyle(color: Colors.white),
),
))
]);
}
#override
void onInit() {
super.onInit();
initConnectivity();
_connectivitySubscription =
_connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
}
#override
void onClose() {
_connectivitySubscription.cancel();
super.onClose();
}
}
Recently i am trying to run the project but seem the snackbar is have some kind of error like this
the compiler show the error below the "context" message
how to solve it?
void showSnackBarSuccess(BuildContext context, String text) {
showTopSnackBar(
context,
CustomSnackBar.success(
message: text,
),
);
}
void showSnackBarInfo(BuildContext context, String text) {
showTopSnackBar(
context,
CustomSnackBar.info(
message: text,
),
);
}
void showSnackBarError(BuildContext context, String text) {
showTopSnackBar(
context,
CustomSnackBar.error(
message: text,
),
);
}
Here the error of the compile
Launching lib\main.dart on Edge in debug mode...
lib\main.dart:1
: Error: The argument type 'BuildContext' can't be assigned to the parameter type 'OverlayState'.
lib/…/snackar/show_snackbar.dart:16
- 'BuildContext' is from 'package:flutter/src/widgets/framework.dart' ('/C:/src/flutter_windows_3.0.4-stable/flutter/packages/flutter/lib/src/widgets/framework.dart').
package:flutter/…/widgets/framework.dart:1
- 'OverlayState' is from 'package:flutter/src/widgets/overlay.dart' ('/C:/src/flutter_windows_3.0.4-stable/flutter/packages/flutter/lib/src/widgets/overlay.dart').
package:flutter/…/widgets/overlay.dart:1
context,
^
: Error: The argument type 'BuildContext' can't be assigned to the parameter type 'OverlayState'.
lib/…/snackar/show_snackbar.dart:25
Here the full of source code of TopSnackBar
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:top_snackbar_flutter/safe_area_values.dart';
import 'package:top_snackbar_flutter/tap_bounce_container.dart';
typedef ControllerCallback = void Function(AnimationController);
enum DismissType { onTap, onSwipe, none }
OverlayEntry? _previousEntry;
/// The [overlayState] argument is used to add specific overlay state.
/// If you are sure that there is a overlay state in your [BuildContext],
/// You can get it [Overlay.of(BuildContext)]
/// Displays a widget that will be passed to [child] parameter above the current
/// contents of the app, with transition animation
///
/// The [child] argument is used to pass widget that you want to show
///
/// The [animationDuration] argument is used to specify duration of
/// enter transition
///
/// The [reverseAnimationDuration] argument is used to specify duration of
/// exit transition
///
/// The [displayDuration] argument is used to specify duration displaying
///
/// The [onTap] callback of [_TopSnackBar]
///
/// The [persistent] argument is used to make snack bar persistent, so
/// [displayDuration] will be ignored. Default is false.
///
/// The [onAnimationControllerInit] callback is called on internal
/// [AnimationController] has been initialized.
///
/// The [padding] argument is used to specify amount of outer padding
///
/// [curve] and [reverseCurve] arguments are used to specify curves
/// for in and out animations respectively
///
/// The [safeAreaValues] argument is used to specify the arguments of the
/// [SafeArea] widget that wrap the snackbar.
///
/// The [dismissType] argument specify which action to trigger to
/// dismiss the snackbar. Defaults to `TopSnackBarDismissType.onTap`
///
/// The [dismissDirection] argument specify in which direction the snackbar
/// can be dismissed. This argument is only used when [dismissType] is equal
/// to `DismissType.onSwipe`. Defaults to `[DismissDirection.up]`
void showTopSnackBar(
OverlayState overlayState,
Widget child, {
Duration animationDuration = const Duration(milliseconds: 1200),
Duration reverseAnimationDuration = const Duration(milliseconds: 550),
Duration displayDuration = const Duration(milliseconds: 3000),
VoidCallback? onTap,
bool persistent = false,
ControllerCallback? onAnimationControllerInit,
EdgeInsets padding = const EdgeInsets.all(16),
Curve curve = Curves.elasticOut,
Curve reverseCurve = Curves.linearToEaseOut,
SafeAreaValues safeAreaValues = const SafeAreaValues(),
DismissType dismissType = DismissType.onTap,
List<DismissDirection> dismissDirection = const [DismissDirection.up],
}) {
late OverlayEntry _overlayEntry;
_overlayEntry = OverlayEntry(
builder: (_) {
return _TopSnackBar(
onDismissed: () {
_overlayEntry.remove();
_previousEntry = null;
},
animationDuration: animationDuration,
reverseAnimationDuration: reverseAnimationDuration,
displayDuration: displayDuration,
onTap: onTap,
persistent: persistent,
onAnimationControllerInit: onAnimationControllerInit,
padding: padding,
curve: curve,
reverseCurve: reverseCurve,
safeAreaValues: safeAreaValues,
dismissType: dismissType,
dismissDirections: dismissDirection,
child: child,
);
},
);
if (_previousEntry != null && _previousEntry!.mounted) {
_previousEntry?.remove();
}
overlayState.insert(_overlayEntry);
_previousEntry = _overlayEntry;
}
/// Widget that controls all animations
class _TopSnackBar extends StatefulWidget {
const _TopSnackBar({
Key? key,
required this.child,
required this.onDismissed,
required this.animationDuration,
required this.reverseAnimationDuration,
required this.displayDuration,
required this.padding,
required this.curve,
required this.reverseCurve,
required this.safeAreaValues,
required this.dismissDirections,
this.onTap,
this.persistent = false,
this.onAnimationControllerInit,
this.dismissType = DismissType.onTap,
}) : super(key: key);
final Widget child;
final VoidCallback onDismissed;
final Duration animationDuration;
final Duration reverseAnimationDuration;
final Duration displayDuration;
final VoidCallback? onTap;
final ControllerCallback? onAnimationControllerInit;
final bool persistent;
final EdgeInsets padding;
final Curve curve;
final Curve reverseCurve;
final SafeAreaValues safeAreaValues;
final DismissType dismissType;
final List<DismissDirection> dismissDirections;
#override
_TopSnackBarState createState() => _TopSnackBarState();
}
class _TopSnackBarState extends State<_TopSnackBar>
with SingleTickerProviderStateMixin {
late final Animation<Offset> _offsetAnimation;
late final AnimationController _animationController;
Timer? _timer;
final _offsetTween = Tween(begin: const Offset(0, -1), end: Offset.zero);
#override
void initState() {
_animationController = AnimationController(
vsync: this,
duration: widget.animationDuration,
reverseDuration: widget.reverseAnimationDuration,
);
_animationController.addStatusListener(
(status) {
if (status == AnimationStatus.completed && !widget.persistent) {
_timer = Timer(widget.displayDuration, () {
if (mounted) {
_animationController.reverse();
}
});
}
if (status == AnimationStatus.dismissed) {
_timer?.cancel();
widget.onDismissed.call();
}
},
);
widget.onAnimationControllerInit?.call(_animationController);
_offsetAnimation = _offsetTween.animate(
CurvedAnimation(
parent: _animationController,
curve: widget.curve,
reverseCurve: widget.reverseCurve,
),
);
if (mounted) {
_animationController.forward();
}
super.initState();
}
#override
void dispose() {
_animationController.dispose();
_timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Positioned(
top: widget.padding.top,
left: widget.padding.left,
right: widget.padding.right,
child: SlideTransition(
position: _offsetAnimation,
child: SafeArea(
top: widget.safeAreaValues.top,
bottom: widget.safeAreaValues.bottom,
left: widget.safeAreaValues.left,
right: widget.safeAreaValues.right,
minimum: widget.safeAreaValues.minimum,
maintainBottomViewPadding:
widget.safeAreaValues.maintainBottomViewPadding,
child: _buildDismissibleChild(),
),
),
);
}
/// Build different type of [Widget] depending on [DismissType] value
Widget _buildDismissibleChild() {
switch (widget.dismissType) {
case DismissType.onTap:
return TapBounceContainer(
onTap: () {
widget.onTap?.call();
if (!widget.persistent && mounted) {
_animationController.reverse();
}
},
child: widget.child,
);
case DismissType.onSwipe:
var childWidget = widget.child;
for (final direction in widget.dismissDirections) {
childWidget = Dismissible(
direction: direction,
key: UniqueKey(),
dismissThresholds: const {DismissDirection.up: 0.2},
confirmDismiss: (direction) async {
if (!widget.persistent && mounted) {
if (direction == DismissDirection.down) {
await _animationController.reverse();
} else {
_animationController.reset();
}
}
return false;
},
child: childWidget,
);
}
return childWidget;
case DismissType.none:
return widget.child;
}
}
}
I am already try to add some "this" line beside the "context" line but still not worked
I am recently edit this post for some of comments requests and i am showing some post of showtopsnackbar and some error message that i am got.
I am hope this can become solution or reference to answer all of my error
As you can see in example page you need to pass Overlay like this:
showTopSnackBar(
Overlay.of(context)!,
CustomSnackBar.error(
message: text,
),
);
the package updated but they didn't update the readme page.
I am trying to make test project according to good practices.
Please note that I DON'T want any "hacky" approach. I am willing to learn good way of solving it.
My understanding of "lifting state up" is that any change updates the state, and then view is redrawn (rebuild) using current state. It is great in theory, but it DOES NOT work with TextFormField/TextEditingController.
I want to have a SharedState and bi-directonal TextFormField/TextEditingController, as follows:
case 1 (works):
TextFormField changes -> state is updated -> readonly Text (in WidgetTwo) is updated
case 2 (does not work):
button (in WidgetOne) is clicked -> state is updated -> TextFormField (in WidgetThree) shows new value from state
I have code in 3 different widgets + main file + SharedSate:
main.dart
void main() {
runApp(ChangeNotifierProvider(
create: (_) => sharedState(), child: const 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: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
const MyHomePage({Key? key, required this.title}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
WidgetOne(),
WidgetTwo(),
WidgetThree(),
]),
),
);
}
}
shared_state.dart
class SharedState extends ChangeNotifier {
int counter = 0;
void setCounter(int c) {
counter = c;
notifyListeners();
}
void incrementCounter() {
counter++;
notifyListeners();
}
void decrementCounter() {
counter--;
notifyListeners();
}
Future fetchCounterFromWeb() async {
// simulate external call
await Future.delayed(Duration(milliseconds: 500));
setCounter(42);
}
}
widget_one.dart
class WidgetOne extends StatelessWidget {
#override
Widget build(BuildContext context) {
var state = Provider.of<SharedState>(context, listen: false);
return Row(
children: [
ElevatedButton(
onPressed: () => state.decrementCounter(),
child: Text('decrement')),
ElevatedButton(
onPressed: () => state.incrementCounter(),
child: Text('increment')),
ElevatedButton(
onPressed: () => state.fetchCounterFromWeb(),
child: Text('fetch counter from web')),
],
);
}
}
widget_two.dart
class WidgetTwo extends StatelessWidget {
#override
Widget build(BuildContext context) {
var state = Provider.of<SharedState>(context, listen: true);
return Row(
children: [Text('Value of counter is: ${state.counter}')],
);
}
}
widget_three.dart (problem is here)
class WidgetThree extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return WidgetThreeState();
}
}
class WidgetThreeState extends State<WidgetThree> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late TextEditingController _controller;
#override
void initState() {
super.initState();
var state = Provider.of<SharedState>(context, listen: false);
_controller = TextEditingController(text: state.counter.toString());
}
#override
Widget build(BuildContext context) {
var state = Provider.of<SharedState>(context, listen: true);
// THE ISSUE:
// It is NOT possible to update Controller (or TextEditing field)
// without this hacky line (which is not good practice)
_controller.text = state.counter.toString();
return Form(
key: _formKey,
child: Column(children: [
TextFormField(
controller: _controller,
keyboardType: TextInputType.number,
onChanged: (v) {
state.setCounter(int.parse(v.isEmpty ? '0' : v));
},
)
]),
);
}
}
I know I can possible move TextEditingController to SharedState, but SharedState should be UI agnostic, and TextEditingController is a UI widget.
I need to change the fontSize of a textField by clicking on an Icon which is in another widget. So I have my custom textfield widget here.
class StateTextField extends StatefulWidget {
final FocusNode focusNode = FocusNode();
final Function(bool,Widget) callback;
final String fontFamily = FontFamily.Arial.toString().split('.')[1];
double fontSize = 18;
final Function(bool) selected;
final bool highlighted = false;
bool hasFocus() {
return focusNode.hasFocus;
}
increaseFontSize() {
fontSize += 2;
}
decreasefontSize() {
if (fontSize > 0) fontSize -= 2;
}
StateTextField({#required this.callback,#required this.selected});
#override
_StateTextFieldState createState() => _StateTextFieldState();
}
And in the second widget I used the function increaseFontSize and decreaseFontSize to change the size
onTap: () {
setState(() {
print(widget.textField.fontSize);
widget.textField.increaseFontSize();
print(widget.textField.fontSize);
});
}
the size increases on clicking the button but is not reflected. I realise it's because setState doesn't change the state of the textField. What approach should I follow then?
create a variable file and import it to your main.dart or wherever you need it.
double fontSize = 12;
variable.dart
Column(
children: <Widget>[
TextField(
style: TextStyle(
fontSize: fontSize
)
),
Container(
width: double.infinity,
height: 70,
child: RaisedButton(
onPressed: (){
setState((){
fontSize = fontSize +1; //or fontSize+=1;
});
},
child: Center(
child: Text("+")
)
)
),
Container(
width: double.infinity,
height: 70,
child: RaisedButton(
onPressed: (){
setState((){
fontSize = fontSize -1; //or fontSize-=1;
});
},
child: Center(
child: Text("-")
)
)
)
]
)
main.dart
There is this approach which will help you somehow.
STEP 1: Do not use increase/decrease method in the StatefulWidget
STEP 2: Store the value in a variable and do changes in the same widget itself
class StateTextField extends StatefulWidget {
final FocusNode focusNode = FocusNode();
final Function(bool,Widget) callback;
final String fontFamily = FontFamily.Arial.toString().split('.')[1];
double fontSize = 18;
final Function(bool) selected;
final bool highlighted = false;
bool hasFocus() {
return focusNode.hasFocus;
}
StateTextField({#required this.callback,#required this.selected});
#override
_StateTextFieldState createState() => _StateTextFieldState();
}
StateTextFieldState extends State<StateTextField>{
double _fontSize;
#override
void initState(){
super.initState();
// setting the value from the widget in initialization of the widget to the local variable which will be used to do the increase-decrease operation
_fontSize = widget.fontSize;
}
void increaseFontSize() {
setState(() => _fontSize += 2);
}
void decreasefontSize() {
if (_fontSize > 0){
setState(() => _fontSize -= 2);
}
}
//in your widget method, you can perform your operation now in `onTap`
onTap: () {
print(_fontSize);
//call your increase method here to increase
_increaseFontSize()
}
}
Let me know if this helps you in some extent. Thanks :)
Let's say I have 3 shapes in Stack widget which needs to be moved from point A to point B. I would like to start these 3 animations after specified delay 0ms 1000ms 2000ms .. . So for that I have 3 separated AnimationController objects but I don't see constructor parameter like delay:. I tried to run forward method 3 times in loop using
int delay = 0;
for (final AnimationController currentController in controllers) {
Future.delayed(Duration(milliseconds: delay), () {
currentController.forward(from: value);
});
delay += 1000;
}
or
await Future.delayed(Duration(milliseconds: delay));
currentController.forward(from: value);
or using Timer class instead of Future but it doesn't work properly. In foreground its working good but when I move application to background and go back to foreground the gap between each shape disappearing and they are in the same position sticked together and moving like one shape.
You can make a stateful widget like below. Change the animation according to your needs.
class SlideUpWithFadeIn extends StatefulWidget {
final Widget child;
final int delay;
final Curve curve;
SlideUpWithFadeIn({#required this.child, #required this.curve, this.delay});
#override
_SlideUpWithFadeInState createState() => _SlideUpWithFadeInState();
}
class _SlideUpWithFadeInState extends State<SlideUpWithFadeIn>
with TickerProviderStateMixin {
AnimationController _animController;
Animation<Offset> _animOffset;
#override
void initState() {
super.initState();
_animController =
AnimationController(vsync: this, duration: Duration(milliseconds: 1250));
final curve =
CurvedAnimation(curve: widget.curve, parent: _animController);
_animOffset =
Tween<Offset>(begin: const Offset(0.0, 0.75), end: Offset.zero)
.animate(curve);
if (widget.delay == null) {
_animController.forward();
} else {
Timer(Duration(milliseconds: widget.delay), () {
_animController.forward();
});
}
}
#override
void dispose() {
_animController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return FadeTransition(
child: SlideTransition(
position: _animOffset,
child: widget.child,
),
opacity: _animController,
);
}
}
And use it like
SlideUpWithFadeIn(
child: ...,
delay: 0,
curve: ...,
),
SlideUpWithFadeIn(
child: ...,
delay: 1000,
curve: ...,
),
SlideUpWithFadeIn(
child: ...,
delay: 2000,
curve: ...,
),