Dialogue box keeps popping up - flutter

I am using Flutter to build an app and trying to show an input dialogue box when app is run for the first time. For this I use shared preferences library. However, when I open the app, the dialogue box is shown every time regardless if it is the first time or not. Also, as I type a single letter in the input box, another dialogue box opens up in front of the previous one. I have attached the code below:
class HomeMenu extends StatefulWidget {
const HomeMenu({Key? key}) : super(key: key);
#override
State<HomeMenu> createState() => _HomeMenuState();
}
class _HomeMenuState extends State<HomeMenu> with TickerProviderStateMixin {
late TabController _controller;
TextEditingController textController = TextEditingController();
var newLaunch;
Future<void> showDialogIfFirstLoaded(BuildContext thisContext) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool _newLaunch = prefs.getBool('newLaunch') ?? true;
newLaunch = _newLaunch;
if (newLaunch) {
showDialog(
context: thisContext,
builder: (BuildContext context) {
return AlertDialog(
content: TextField(
controller: textController,
onChanged: (value) =>
setState(() => name = textController.text),
),
actions: <Widget>[
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.pop(thisContext);
prefs.setBool(newLaunch, false);
},
),
],
);
},
);
}
}
#override
void initState() {
super.initState();
_controller = TabController(length: 3, vsync: this);
}
#override
Widget build(BuildContext context) {
Future.delayed(Duration.zero, () => showDialogIfFirstLoaded(context));
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _controller,
tabs: tabs,
),
),
body: TabBarView(
controller: _controller,
children: [
null
],
),
);
}
}

you are calling showDialogIfFirstLoaded in build method, and on TextFiled onChange method you are calling setState. Every time you call setState build method re-build again and it calling showDialogIfFirstLoaded on every setState.
I don't know your requirement but it not good to call setState in onChange method of TextField. You can use ValueListenableBuilder if you require to update some part of UI on every single char type in text filed
If you want's to open that dialog only one time then create a method to get whether it's first launch or not and if yes then call showDialogIfFirstLoaded method.
For more details see below code -
#override
void initState() {
super.initState();
_controller = TabController(length: 3, vsync: this);
_isFirstLaunch();
}
_isFirstLaunch()async{
final prefs = await SharedPreferences.getInstance();
final bool? isfirstLaunch = prefs.getBool('first_launch');
if(!isfirstLaunch ?? true){
await prefs.setBool('first_launch', true);
showDialogIfFirstLoaded(context);
}
and update your build method like this -
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _controller,
tabs: tabs,
),
),
body: TabBarView(
controller: _controller,
children: [
null
],
),
);
}

Related

How to dismiss an open dropdown in Flutter?

I've a dropdown on my screen, which I want to close every time the user minimises the app (Pause State). I looked into the DropDownButton widget, and it seems it uses Navigator.push() method, so ideally it should be dismissed with Navigator.pop(), if I'm not wrong.
Here is what I've tried already -
Use global key to get the context of dropdown widget and implement Navigator.pop(key.currentContext) inside didChangeAppLifecycleState() function.
Use focusNode to implement .unfocus() to remove the focus from the dropdown.
Maintain a isDropdownDialogOpen boolean which I set to true on onTap() and false on onChange(). And then simple pop() if its true on app minimisation. But this approach fails when the user opens the dropdown and then closes it by tapping outside the dropdown dialog. I can't set the boolean to false in that case.
My requirement is that - I've to dismiss the dropdown whenever the user minimises the app, if it was open in the first place.
I've gone through bunch of SO questions and GitHub comments, that are even remotely similar, but couldn't find anything helpful.
Loose Code -
class Auth extends State<Authentication> with WidgetsBindingObserver {
bool isDropdownOpen = false;
GlobalKey _dropdownKey = GlobalKey();
FocusNode _focusNode;
#override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
// To pop the open dropdown dialog whenever app is minimised (and opened back on again)
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
if(isDropdownOpen)
pop();
// Also tried pop(_dropdownKey.currentContext)
break;
case AppLifecycleState.paused:
break;
case AppLifecycleState.inactive:
break;
case AppLifecycleState.detached:
break;
}
}
build() {
return Scaffold(
// Dummy Dropdown button with random values
body: DropdownButton(
key: _dropdownKey,
focusNode: _focusNode,
value: "one",
items: ["one", "two", "three", "four", "five"],
//boolean values changing fine, but when user taps outside the dropdown dialog, dropdown is closed and neither of onChanged() and onTap() is called. Need a call back (or something similar) for this particular case.
onChange(value) => isDropdownOpen = false;
onTap() => isDropdownOpen = ! isDropdownOpen;
)
);
}
}
For your ref - https://github.com/flutter/flutter/issues/87989
Does this work for you?
import 'package:flutter/material.dart';
void main() async {
runApp(
MyApp(),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({Key key}) : super(key: key);
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
AppLifecycleState currentAppLifecycleState;
bool isDropdownMenuShown = false;
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(
() => currentAppLifecycleState = state,
);
}
String value;
#override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (<AppLifecycleState>[
AppLifecycleState.inactive,
AppLifecycleState.detached,
AppLifecycleState.paused,
].contains(currentAppLifecycleState) &&
isDropdownMenuShown) {
print('Closing dropdown');
Navigator.pop(context);
}
});
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo'),
),
body: Center(
child: GestureDetector(
onLongPressStart: (LongPressStartDetails details) async {
final left = details.globalPosition.dx;
final top = details.globalPosition.dy;
setState(() => isDropdownMenuShown = true);
final value = await showMenu<String>(
context: context,
position: RelativeRect.fromLTRB(left, top, left, top),
items: <PopupMenuEntry<String>>[
PopupMenuItem(
child: const Text('Option 1'),
value: 'Option 1',
),
PopupMenuItem(
child: const Text('Option 2'),
value: 'Option 2',
),
PopupMenuItem(
child: const Text('Option 3'),
value: 'Option 3',
),
],
);
setState(() => isDropdownMenuShown = false);
if (value == null) return;
print('You chose: $value');
},
child: InkWell(
onTap: () {},
child: Container(
alignment: Alignment.center,
child: Text(
'Long press to show dropdown',
textAlign: TextAlign.center,
),
),
),
),
),
);
}
}
How does it work?
We keep track of the current app state using the didChangeAppLifecycleState method.
Whenever the dropdown is shown the isDropdownMenuShown variable is set to true.
Whenever the dropdown is closed/exited the variable is set to false.
We add a post frame callback (it is called after each rebuild) and we check if the app went into background and if the dropdown is shown. If yes, the dropdown is closed.
You could use FocusManager.instance.primaryFocus?.unfocus();

Flutter how to use form with tabview

I am trying to create a form with multiple tabs but for some reason if i call validate or save on form i can get only values from tab that is active and same is true for error i think it may be because form only gets values from fields that are currently rendered on screen.
so can some one tell me how can i make form work with multiple tab-view so that after changing tab i can validate tabs that have't been visited and also from vested ones as well.
there is AutomaticKeepAliveClientMixin but it can only keep state alive but i am more interested in onSave or validator as i am managing state in parent element not in tabviews
Thanks in advance
I have the same need as you and I'm trying to manage the different forms with a Provider that has a List <GlobalKey <FormState>> formKeys = [GlobalKey <FormState> (), GlobalKey <FormState> () ...]; a key for each form. Then in a button on the tabbar onPressed: form.validate (formKey) for each form. If all forms are fine, save info, else error message.
For now what I am using is a mix of pageview and tab view for pages instead of default flutter pageview I am useing this package preload_page_view (as in flutter default page view there is no option for preloading but once a page is loaded we can tell flutter to save it so this package actually provide an option on how many pages should be preloaded etc.)
and then Tabbar for switching pages like this
class IssuesEditScreen extends StatefulWidget {
static final routeName = "/issues_edit";
#override
_IssuesEditScreenState createState() => _IssuesEditScreenState();
}
class _IssuesEditScreenState extends State<IssuesEditScreen>
with SingleTickerProviderStateMixin {
final GlobalKey<FormState> _form = GlobalKey();
final _scaffold = GlobalKey<ScaffoldState>();
Issue _instance;
TabController _tabController;
PreloadPageController _pageController = PreloadPageController(
initialPage: 0, keepPage: true, viewportFraction: 0.99);
bool _canChange = true;
bool _loading = false;
Map<String, String> _errors = {};
Map<String, dynamic> _formData = {};
#override
void dispose() {
super.dispose();
_tabController.dispose();
_pageController.dispose();
}
#override
void initState() {
// TODO: implement initState
_tabController = TabController(length: 3, vsync: this);
_tabController.addListener(() {
if (_tabController.indexIsChanging) {
changePage(_tabController.index, page: true);
}
});
super.initState();
}
void _submit() {
if (!_form.currentState.validate()) {
_scaffold.currentState
.showSnackBar(SnackBar(content: Text("Please resolve given errors")));
return;
}
_formData.clear();
_form.currentState.save();
}
void changePage(index, {page = false, tab = false}) async {
if (page) {
_canChange = false;
await _pageController.animateToPage(index,
duration: Duration(milliseconds: 500), curve: Curves.ease);
_canChange = true;
} else {
_tabController.animateTo(index);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffold,
appBar: AppBar(
title: Text("Form"),
bottom: TabBar(
controller: _tabController,
tabs: [
Tab(
text: "Page1",
),
Tab(
text: "Page2",
),
Tab(text: "Page3"),
],
),
actions: [
FlatButton(
child: Text("Save"),
onPressed: __submit)
],
),
body: Form(
key: _form,
child: PreloadPageView(
preloadPagesCount: 5,
physics: AlwaysScrollableScrollPhysics(),
controller: _pageController,
onPageChanged: (index) {
if (_canChange) {
changePage(index);
}
},
children: [
Page1(formData: _formData, errors: _errors,),
Page2(formData: _formData, errors: _errors),
Page3(formData: _formData, errors: _errors)
],
),
),
);
}
}
class Page1 extends StatelessWidget {
const Page1 ({
Key key,
#required Map<String,dynamic > formData,
#required Map<String, String> errors,
}) : _formData = formData, _errors = errors, super(key: key);
final Map<String,dynamic > _formData;
final Map<String, String> _errors;
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
elevation: 3,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: SingleChildScrollView(
child: Column(
children: [
TextFormField(
onSaved: (value) =>
_formData['field1'] = value,
decoration: BorderInputDecorator(
errorText: _errors['field1'],
label: "Field1",
),
validator: (value) {
if (value.isEmpty) {
return "This Field is required";
}
return null;
},
),
],
),
),
)
)
)
);
}
}
as you can see with that you can use onsave validators and add more page just can save or validate on form to get all data in _submit
There can be syntax issue

Flutter-web TextFormField issue

usually I can disable/grey-out a button until a TextFormField meets certain parameters in flutter by something like this:
TextFormField(
controller: _controller
value: (value)
)
SubmitButton(
onPressed: _controller.text.isNotEmpty ? _submit : null;
)
But when compiled as a website the Button seems no longer aware of the controller value...
I have tried targeting in several different ways, e.g. _controller.value.text.isEmpty and _controller.text.isEmpty...
I'm guessing I'm missing something or this method just isn't possible for web ... Is there any other way to get the same result?
To be honest, your code shouldn't work in flutter mobile either, but may be works because of screen keyboard causes widget rebuild when showing or hiding.
To fix this issue we have to use stateful widget with state variable like canSubmit and update it in textField's listener onChange with setState method. Then every time the text changes, our stateful widget will update the submit button..
class Page extends StatefulWidget {
#override
_PageState createState() => _PageState();
}
class _PageState extends State<Page> {
bool canSubmit;
#override
void initState() {
canSubmit = false;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
onChanged: (value) {
setState(() {
canSubmit = value.isNotEmpty;
});
},
),
RaisedButton(
onPressed: canSubmit ? _submit : null,
child: Text('Submit'),
)
],
),
),
);
}
void _submit() {
print('Submitted');
}
}

does setState work on objects in dart/flutter?

I have a flutter widget that attempts to solve soduku grids. I have class called SodukuSolver which does all the calculations and provides a List<String> of the current results. I call setState to refresh the list, but it does not update the screen.
Below, I'll try to include as much of the relevant code as I can. Full source is at https://github.com/mankowitz/soduku
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(title: "Soduku solver", home: Soduku());
}
}
class SodukuState extends State<Soduku> {
SodukuSolver ss;
List<String> _list;
int _changes = 0;
int _remaining = 81;
#override
Widget build(BuildContext context) {
final String _starting =
"750943002024005090300020000140089005093050170500360024000070009070400810400198057";
ss = new SodukuSolver(_starting);
_list = ss.getList();
return Scaffold(
appBar: AppBar(title: Text('Soduku solver'), actions: <Widget>[
// action button
IconButton(
icon: Icon(Icons.directions_walk),
onPressed: () {
_iterate();
},
),
]),
body: _buildGrid(),
);
}
Widget _buildGrid() {
return Column(children: <Widget>[
AspectRatio(
aspectRatio: 1.0,
child: Container(
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 9,
),
itemBuilder: _buildGridItems,
itemCount: 81,
),
),
),
]);
}
Widget _buildGridItems(BuildContext context, int index) {
return GestureDetector(
child: GridTile(
child: Container(
child: Center(
child: Text(_list[index]),
),
),
),
);
}
void _iterate() {
setState(() {
_changes = ss.iterateSoduku();
_remaining = ss.empties();
_list = ss.getList();
});
}
}
class Soduku extends StatefulWidget {
#override
SodukuState createState() => SodukuState();
}
So the problem is that _iterate() is being called, and I can use the debugger to see that the internal state of SodukuSolver is being updated and it is even passing _list correctly, but the grid on screen doesn't update, even though _changes and _remaining do update.
You are creating new SodukuSolver with same _starting every time the widget builds and then obtaining _list from it. So you are overriding changes from previous iteration.
Looks like SodukuSolver creation should be performed once. You can override initState in SodukuState and initialise SodukuSolver there or initialise it in the same place where it is declared
Just add your code in the initState() method as following
#override
void initState() {
super.initState();
final String _starting =
"750943002024005090300020000140089005093050170500360024000070009070400810400198057";
ss = new SodukuSolver(_starting);
_list = ss.getList();
}
In your case, your list is not getting updated as setState() method will call your SodukuSolver() and ss.getList(); methods every time. because, setSate() ultimately calls build method to render every time.
So adding it inside your initState method will solve your issue. As it is getting called only once when the screen/route initialises.

Update SnackBar content in Flutter

I have a button that displays a SnackBar (or toast) before moving to the next page. I have a countdown and after 5 seconds I push Page2.
RaisedButton(
onPressed: () {
_startTimer();
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text(
'Prepare yourself to start in ${widget._current.toString()}!'), // doesn't work here
duration: new Duration(seconds: widget._start),
action: SnackBarAction(
label: widget._current.toString(), // and neither does here
onPressed: () {
// Some code to undo the change.
},
),
);
Scaffold.of(context).showSnackBar(snackBar);
},
child: Text(
"I'm ready",
style: TextStyle(fontSize: 20),
),
),
Nothing to see on the countdown but I'll paste it just in case:
void _startTimer() {
CountdownTimer countDownTimer = new CountdownTimer(
new Duration(seconds: widget._start),
new Duration(seconds: 1),
);
var sub = countDownTimer.listen(null);
sub.onData((duration) {
setState(() {
widget._current = widget._start - duration.elapsed.inSeconds;
});
});
sub.onDone(() {
print("Done");
sub.cancel();
});
}
So if I display the countdown somewhere else (inside a Text for example) it works but it seems that the SnackBar doesn't change its contain, it always get the max number of the countdown.
you need to implement a custom widget with countdown logic in side for the content field of snackbar, like this:
class TextWithCountdown extends StatefulWidget {
final String text;
final int countValue;
final VoidCallback? onCountDown;
const TextWithCountdown({
Key? key,
required this.text,
required this.countValue,
this.onCountDown,
}) : super(key: key);
#override
_TextWithCountdownState createState() => _TextWithCountdownState();
}
class _TextWithCountdownState extends State<TextWithCountdown> {
late int count = widget.countValue;
late Timer timer;
#override
void initState() {
super.initState();
timer = Timer.periodic(Duration(seconds: 1), _timerHandle);
}
#override
void dispose() {
timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
child: Text("[$count] " + widget.text),
);
}
void _timerHandle(Timer timer) {
setState(() {
count -= 1;
});
if (count <= 0) {
timer.cancel();
widget.onCountDown?.call();
}
}
}
That is because the snack bar is built once, when the button is clicked. When the state updates, it rebuilds the widget tree according to the changes. The snack bar initially isn't in the widget tree, so it doesn't update.
Try to use stack and show a snack bar, and then you should be able to manipulate it however you need.
Hope it helps.
Update SnackBar Content
SnackBar content can be updated/rebuilt while it's visible by making its content: widget dynamic.
In the code sample below we're using a ValueNotifier and ValueListenableBuilder to dynamically rebuild the content of the SnackBar whenever ValueNotifier is given a new value.
(There are many ways to to maintain state values & rebuild widgets when it changes, such as RiverPod, GetX, StreamBuilder, etc. This example uses the Flutter native ValueListenableBuilder.)
When running this Flutter page, click the FAB to show the SnackBar, then click on the center text to update the SnackBar's content (multiple times if you like).
Example
Use SnackBarUpdateExample() widget in your MaterialApp home: argument to try this example in an emulator or device.
class SimpleCount {
int count = 0;
}
class SnackBarUpdateExample extends StatelessWidget {
static const _initText = 'Initial text here';
/// This can be "listened" for changes to trigger rebuilds
final ValueNotifier<String> snackMsg = ValueNotifier(_initText);
final SimpleCount sc = SimpleCount();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('SnackBar Update'),
),
/// ↓ Nested Scaffold not necessary, just prevents FAB being pushed up by SnackBar
body: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Click FAB to show SnackBar'),
SizedBox(height: 20,),
Text('Then...'),
SizedBox(height: 20,),
InkWell(
child: Text('Click → here ← to update SnackBar'),
onTap: () {
sc.count++;
snackMsg.value = "Hey! It changed! ${sc.count}";
},
), /// When snackMsg.value changes, the ValueListenableBuilder
/// watching this value, will call its builder function again,
/// and update its SnackBar content widget
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () => _showSnackBar(context),
),
),
);
}
void _showSnackBar(BuildContext context) {
var _controller = ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: SnackContent(snackMsg))
);
/// This resets the snackBar content when it's dismissed
_controller.closed.whenComplete(() {
snackMsg.value = _initText;
sc.count = 0;
});
}
}
/// The ValueListenableBuilder rebuilds whenever [snackMsg] changes.
class SnackContent extends StatelessWidget {
final ValueNotifier<String> snackMsg;
SnackContent(this.snackMsg);
#override
Widget build(BuildContext context) {
/// ValueListenableBuilder rebuilds whenever snackMsg value changes.
/// i.e. this "listens" to changes of ValueNotifier "snackMsg".
/// "msg" in builder below is the value of "snackMsg" ValueNotifier.
/// We don't use the other builder args for this example so they are
/// set to _ & __ just for readability.
return ValueListenableBuilder<String>(
valueListenable: snackMsg,
builder: (_, msg, __) => Text(msg));
}
}