I want to know how to move between pages in flutter introduction screen. To clarify the problem, I want to go back a page when skip button is pressed.
So far I have done this:
class _IntroPageState extends State<IntroPage> {
int currentIndex = 0;
void _onIntroEnd(context) {
getIt<IntroLocalDataSource>().setIntroSeenState(true);
Navigator.of(context).pushReplacementNamed(SignInPage.id);
}
#override
Widget build(BuildContext context) {
final strings = Languages.of(context);
final bodyStyle = Theme.of(context)
.textTheme
.subtitle2
?.copyWith(fontSize: k16TextFontSize);
final titleStyle = Theme.of(context)
.textTheme
.headline1
?.copyWith(fontSize: k20TextFontSize);
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Flexible(
child: Container(
child: IntroductionScreen(
onChange: (index) {
setState(() {
currentIndex = index;
});
},
rtl: true,
globalBackgroundColor: Colors.transparent,
showNextButton: false,
rawPages: strings.introScreenTitles.asMap().entries.map((entry) {
int idx = entry.key;
String val = entry.value;
return Center(
child: IntroCardWidget(
index: idx,
title: val,
bodyStyle: bodyStyle,
titleStyle: titleStyle,
image: Assets.introImages[idx],
description: strings.introScreenDescriptions[idx],
));
}).toList(),
showDoneButton: false,
curve: Curves.fastLinearToSlowEaseIn,
showSkipButton: currentIndex != 0,
skip: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
primary: kLightTextColor,
shape: CircleBorder(),
),
onPressed: () {
setState(() {
currentIndex--; // <<--------- This does not work
});
},
child: Icon(
Icons.keyboard_arrow_left_rounded,
color: Theme.of(context).primaryColor,
)),
dotsDecorator: DotsDecorator(
color: kLightTextColor.withOpacity(.15),
activeSize: Size(20, 10),
activeShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25.0)),
activeColor: kLightTextColor),
controlsPadding: kIsWeb
? const EdgeInsets.all(12.0)
: const EdgeInsets.symmetric(
vertical: Spacings.paddingSm,
horizontal: Spacings.paddingXs),
),
),
),
Container(
constraints: BoxConstraints(
maxHeight: Spacings.margin5Xl, minHeight: Spacings.margin5Xl),
margin: EdgeInsets.only(bottom: Spacings.margin5Xl),
child: currentIndex != 3
? Container()
: ElevatedButton(
onPressed: () => _onIntroEnd(context),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Spacings.padding5Xl,
vertical: Spacings.paddingSm),
child: Text(
strings.goToVerificationPageBtnText,
style: TextStyle(
fontSize: k16TextFontSize, color: kDarkTextColor),
),
),
style: ElevatedButton.styleFrom(
elevation: 1,
primary: kLightTextColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(Spacings.radiusLg),
),
),
),
),
],
);
}
But the page does not change. It seems to make sense, as the index is not passed to the IntroductionScreen widget. So I was wondering how to navigate in these pages as will.
I found a way to do what I wanted.
It could be done with using keys & calling IntroductionScreenState's methods.
After going through the source code of library, it seems it is changing pages using this public method:
Future<void> animateScroll(int page) async {
setState(() => _isScrolling = true);
await _pageController.animateToPage(
max(min(page, getPagesLength() - 1), 0),
duration: Duration(milliseconds: widget.animationDuration),
curve: widget.curve,
);
if (mounted) {
setState(() => _isScrolling = false);
}
}
and also has these methods for going forward & backward:
void next() => animateScroll(_currentPage.round() + 1);
void previous() => animateScroll(_currentPage.round() - 1);
From this point, it was just a matter of calling the methods of this state when needed. This could be done using keys:
GlobalKey<IntroductionScreenState> _introScreenKey =
GlobalKey<IntroductionScreenState>();
IntroductionScreen(
key: _introScreenKey,
skip: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
primary: kLightTextColor,
shape: CircleBorder(),
),
onPressed: () {
_introScreenKey.currentState?.previous(); // <<----- This here
},
child: Icon(
Icons.keyboard_arrow_left_rounded,
color: Theme.of(context).primaryColor,
)),
)
And ofcourse it is pretty easy to navigate directly to any page using the animateScroll().
Since you're using navigator, you should give a try to Navigator.pop()
onPressed: () { Navigator.pop(context); }
here you can find some examples
Whenever I save my project or switches from light mode to dark mode, I receive OTP message that code has been sent again or go to recaptcha screen and then again I get the timeout message. Why so? when I am already logged in? Why user goes to recaptcha screen again when he is already in? Help me out.. Here is code below:-
This is first screen code from where I get the no.
child: Text("GET OTP", style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
onPressed: () async {
if(_formKey.currentState.validate()) {
Map<String, dynamic> userinfo = {
'countryCode': phonecode,
'phone': _phoneController.text,
};
Navigator.pushNamed(context, '/OTPphoneLogin', arguments: userinfo);
}
},
This is second screen, where user has to put OTP:-
class _OTPphoneLogin extends State<OTPphoneLogin> {
final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
TextEditingController _pincontroller = TextEditingController();
final auth = FirebaseAuth.instance;
final CollectionReference _reference = FirebaseFirestore.instance.collection("users");
String _countryCode = '';
String _phone = '';
String vid;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
final dynamic _args = ModalRoute.of(context).settings.arguments;
_maxScreenWidth = (MediaQuery.of(context).size.width <= 500) ? MediaQuery.of(context).size.width : 500;
print(_args);
_phone = _args['phone'];
_countryCode = _args['countryCode'];
verifyNumber(_phone);
return WillPopScope(
//onWillPop: () async => false,
child: GestureDetector(
onTap:() => FocusScope.of(context).unfocus(),
child: Scaffold(
body: Center(
child: Container(
width: _maxScreenWidth, //how to set it to max
height: MediaQuery.of(context).size.height, //how to set it to max
child: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
SizedBox(height: 50,),
Image.asset("assets/images/practicelogo.png"),
Padding(
padding: EdgeInsets.fromLTRB(25.0, 45.0, 25.0, 10.0),
child: Text("OTP sent on:- " "\n" +_countryCode.toString()+_phone.toString(),
style: TextStyle(fontSize: 25,color: mRed,
fontWeight: FontWeight.w500),textAlign: TextAlign.center,
),
),
SizedBox(height: 10),
Padding(
padding: EdgeInsets.fromLTRB(25.0, 15.0, 25.0, 10.0),
child: TextFormField(
keyboardType: TextInputType.number,
validator: _validateCode,
controller: _pincontroller ,
style: Theme.of(context).textTheme.subtitle1,
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20.0)),
borderSide: BorderSide(color: lRed)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20.0)),
borderSide: BorderSide(color: mRed ,width: 3)),
hintText: "Enter OTP"),
),
),
SizedBox(
height: 80,
width: MediaQuery.of(context).size.width,
child: Padding(
padding: EdgeInsets.fromLTRB(25.0, 15.0, 25.0, 10.0),
child: ElevatedButton (
child: Text("Verify", style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
onPressed:()=>verifyPhone(_pincontroller.text.trim()),
style: ElevatedButton.styleFrom(
primary: mRed,
onPrimary: mYellow,
elevation: 5,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.7)
)
)
),
),
),
],
),
),
)
),
),
),
),
);
}
Future<void> verifyNumber(String mobile) async {
await FirebaseAuth.instance.verifyPhoneNumber(
phoneNumber: _countryCode+mobile,
timeout: const Duration(seconds: 60),
verificationCompleted: (PhoneAuthCredential credential) async {
try {
await FirebaseAuth.instance.signInWithCredential(credential);
Navigator.push(context, MaterialPageRoute(builder: (context)=>BottomNav()));
} on FirebaseAuthException catch(e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.message)));
}
},
verificationFailed: (FirebaseAuthException e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.message)));
},
codeSent: (String verificationId, int resendToken) {
vid = verificationId;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Code Sent"+verificationId)));
},
codeAutoRetrievalTimeout: (String verificationId) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Timeout"+verificationId)));
},
);
}
Future<void> verifyPhone(String code) async {
PhoneAuthCredential phoneAuthCredential = PhoneAuthProvider.credential(verificationId: vid, smsCode: code);
try {
await FirebaseAuth.instance.signInWithCredential(phoneAuthCredential);
Navigator.push(context, MaterialPageRoute(builder: (context)=>BottomNav()));
} on FirebaseAuthException catch(e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.message)));
}
}
And gets this in console:-
E/zzf (30955): **Problem retrieving SafetyNet** Token: 7:
W/System (30955): Ignoring header X-Firebase-Locale because its value was null.
W/System (30955): **A resource failed to call end.**
I/FirebaseAuth(30955): [FirebaseAuth:] Preparing to create service connection to fallback implementation
W/System (30955): Ignoring header X-Firebase-Locale because its value was null.
W/FirebaseAuth(30955): **[SmsRetrieverHelper] Timed out waiting for SMS.**
W/System (30955): Ignoring header X-Firebase-Locale because its value was null.
D/FirebaseAuth(30955): Notifying id token listeners about user ( ZuaQMaw8FxZb6jeiYKzXZJRplk52 ).
[FirebaseAuth:] Preparing to create service connection to fallback implementation
The problem is that you’re calling verifyNumber in the build command. This gets called every time the State _OTPphoneLogin updates (any time it gets re-rendered, such as when you hot reload).
You should move this to initState which only gets called when the State is created, and best-practice, wrap it in a login check. You can do that by checking if FirebaseAuth.instance.currentUser?.uid is null before invoking verifyNumber.
I have a flutter project with a FloatingSearchBar from this library (pub.dev). I have four buttons, one of which is a GestureDetector (which is not really important but explains some behind scenes info).
This all looks like this:
or this:
or one other variation.
These buttons all work fine, although I've spent a lot of time getting them that way. They are toggleable, and their Icons are decided by a variable that can be updated using setState.
There's also, however, this layout:
which should be able to be toggled on and off by this switch/button inside the menu:
It too just updates the offlineMode variable inside a setState. But this setState doesn't update the search bar until I force the bar to update by clicking any of the buttons.
Why is setState not working in this particular circumstance?
Code for search bar:
Widget buildFloatingSearchBar(AsyncSnapshot snapshot) {
const bool isPortrait = true;
return DescribedFeatureOverlay( //Don't worry about these I don't think
featureId: 'search',
tapTarget: Icon(Icons.search),
title: Text('Search'),
description: Text(
'Tap the bar at the top to open the search interface, where you can search for GeoTags by name, approximate location or by author.'),
overflowMode: OverflowMode.extendBackground,
backgroundColor: Theme.of(context).primaryColor,
targetColor: Colors.white,
textColor: Colors.white,
child: FloatingSearchBar(
borderRadius: BorderRadius.all(
Radius.circular(50),
),
progress: loadingSearch,
title: offlineMode //Checking offlineMode variable Should update with setState
? Row(
children: [
Text(
'Geotagger',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
Text(
' - Offline Mode',
style: TextStyle(
//fontWeight: FontWeight.bold,
fontSize: 15,
),
),
],
)
: null,
backgroundColor: Theme.of(context).backgroundColor,
hint: 'Find a GeoTag...', //Shown if offlineMode is off (title is null)
transitionDuration: const Duration(milliseconds: 800),
transitionCurve: Curves.easeInOut,
physics: const BouncingScrollPhysics(),
axisAlignment: isPortrait ? 0.0 : -1.0,
openAxisAlignment: 0.0,
maxWidth: isPortrait ? 600 : 500,
height: compassExpanded ? 62 : 48,
debounceDelay: const Duration(milliseconds: 1500),
onQueryChanged: (inQuery) {
String query = inQuery.trim();
if (offlineMode || query == null || query == '' || query == '#') {
return;
}
if (query.startsWith('#')) {}
setState(() {
loadingSearch = true;
});
// Call your model, bloc, controller here.
Future.delayed(const Duration(seconds: 3), () {
setState(() {
loadingSearch = false;
});
});
},
// Specify a custom transition to be used for
// animating between opened and closed stated.
transition: ExpandingFloatingSearchBarTransition(),
accentColor: Theme.of(context).accentColor,
actions: [
FloatingSearchBarAction.searchToClear(
showIfClosed: false,
),
FloatingSearchBarAction( //This one is to check current GPS state
showIfOpened: false,
child: StreamBuilder<Object>(
stream: Stream.periodic(Duration(seconds: 5), (x) => x),
builder: (context, _) {
return FutureBuilder(
future: Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.medium,
timeLimit: Duration(seconds: 5),
),
builder: (context, compassDir) {
if (compassDir.hasError) {
//print('failure');
return Padding(
padding: const EdgeInsets.only(
right: 7,
bottom: 2,
),
child: Icon(
Icons.wrong_location,
color: Colors.red,
),
);
} else {
return Container(width: 0, height: 0);
}
});
}),
),
FloatingSearchBarAction(
showIfOpened: false,
child: DescribedFeatureOverlay(
featureId: 'toggleLocationSnapping',
tapTarget: Icon(Icons.control_camera),
title: Text('Location Snapping'),
description: Text(
'This button toggles the map mode between snapping (default) and free. When the icon shown is a circle with a line across it, tap it to switch to free mode, and you\'ll be able to move, zoom and rotate the map freely. In snapping mode, you will be snapped to your current location and orientation, and you\'ll also be unable to pan, rotate or zoom (usually).'),
//overflowMode: OverflowMode.extendBackground,
backgroundColor: Theme.of(context).primaryColor,
targetColor: Colors.white,
textColor: Colors.white,
child: CircularButton(
icon: Icon(moveMapToUser
? Icons.location_disabled
: Icons.my_location),
onPressed: () {
setState(() {
moveMapToUser = !moveMapToUser;
rotateMapToUser = false;
});
},
),
),
),
FloatingSearchBarAction(
showIfOpened: false,
child: Visibility(
visible: moveMapToUser,
child: DescribedFeatureOverlay(
featureId: 'rotationAndNorth',
tapTarget: Icon(Icons.screen_rotation),
title: Text('Rotation & Panning Mode'),
description: Text(
'This button has three states. When showing an upward facing arrow in free mode, tap it to orientate the map north, and remain in free mode. When showing a lock symbol in snapping mode, tap it to renable automatic rotation and prevent zooming. When showing an unlock symbol in snapping mode, tap it to allow rotation and zooming, but still prevent panning away from your current location.'),
//overflowMode: OverflowMode.extendBackground,
backgroundColor: Theme.of(context).primaryColor,
targetColor: Colors.white,
textColor: Colors.white,
child: CircularButton(
icon: Icon(rotateMapToUser ? Icons.lock_open : Icons.lock),
onPressed: () {
setState(() {
rotateMapToUser = !rotateMapToUser;
if (!rotateMapToUser) {
controller.rotate(0);
}
});
},
),
),
),
),
FloatingSearchBarAction(
showIfOpened: false,
child: Visibility(
visible: !moveMapToUser,
child: CircularButton(
icon: Icon(Icons.north),
onPressed: () {
setState(() {
controller.rotate(0);
});
},
),
),
),
FloatingSearchBarAction(
showIfOpened: false,
child: Visibility(
visible: !offlineMode,
child: DescribedFeatureOverlay(
featureId: 'refresh',
tapTarget: Icon(Icons.refresh),
title: Text('Refresh'),
description: Text(
'Tap this button to search the area visible on your device for GeoTags after panning the map.'),
//overflowMode: OverflowMode.extendBackground,
backgroundColor: Theme.of(context).primaryColor,
targetColor: Colors.white,
textColor: Colors.white,
child: CircularButton(
icon: const Icon(Icons.refresh),
onPressed: () {
//setState(() {
paintGeoTags(
true,
() => setState(() {
loadingSearch = true;
}),
() => setState(() {
loadingSearch = false;
}));
//});
}),
),
),
),
FloatingSearchBarAction( //Profile Pic icon
showIfOpened: !compassExpanded, //Don't worry about compassExpanded
child: Visibility(
visible: !offlineMode, //Should update on setState
child: DescribedFeatureOverlay(
featureId: 'profile',
tapTarget: Icon(Icons.account_circle),
title: Text('Public Profile'),
description: Text(
'Tap this button to view your public profile, and view information such as rank, total points and various other information. You can manage your profile by tapping the settings cog icon in that screen, or by choosing Manage My Profile from the menu. The colored border represents the color of your current rank, and the point will always face north. You can hold down on this icon to toggle the visibility of the compass point and to expand or reduce the height of the search bar. \n\nAfter you\'ve tapped the icon above to move to the next button introduction, tap the menu button in the top left, and your introduction will continue there!'),
overflowMode: OverflowMode.extendBackground,
backgroundColor: Theme.of(context).primaryColor,
targetColor: Colors.white,
textColor: Colors.white,
child: Hero(
tag: 'profileImage',
child: GestureDetector(
onTap: () => Navigator.pushNamed(
context,
ProfileScreen.routeName,
arguments: ScreenArguments(
globals.userData.displayName,
//'test user',
FirebaseFirestore.instance
.doc('users/' + globals.userData.uid),
true,
),
),
onLongPressStart: (e) {
setState(() {
compassExpanding = true;
});
},
onLongPressEnd: (e) => {
setState(() {
compassExpanded = !compassExpanded;
compassExpanding = false;
}),
//showLogoutAlert(context)
},
child: StreamBuilder<Object>(
stream: FlutterCompass.events,
builder: (context, compassDir) {
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'));
} else {
if (snapshot.data == null) {
return Container();
}
}
return Padding(
padding: const EdgeInsets.only(
left: 4,
),
child: Container(
width: 41,
height: 41,
transform: Matrix4Transform()
.rotateDegrees(
double.parse(((compassDir.data) ?? 0)
.toString()) *
-1 +
45,
origin: Offset(41 / 2, 41 / 2))
.matrix4,
decoration: ShapeDecoration(
//shape: BoxShape.circle,
shape: CustomRoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(21),
bottomRight: Radius.circular(21),
topRight: Radius.circular(21),
topLeft: Radius.circular(
compassExpanded ? 0 : 21),
),
leftSide: BorderSide(
width: 2,
color: changeMedalColor(
snapshot.data.points),
),
bottomLeftCornerSide: BorderSide(
width: 2,
color: changeMedalColor(
snapshot.data.points),
),
rightSide: BorderSide(
width: 2,
color: changeMedalColor(
snapshot.data.points),
),
bottomRightCornerSide: BorderSide(
width: 2,
color: changeMedalColor(
snapshot.data.points),
),
bottomSide: BorderSide(
width: 2,
color: changeMedalColor(
snapshot.data.points),
),
topSide: BorderSide(
width: 2,
color: changeMedalColor(
snapshot.data.points),
),
topRightCornerSide: BorderSide(
width: 2,
color: changeMedalColor(
snapshot.data.points),
),
),
color: //!compassExpanding
/*?*/ changeMedalColor(
snapshot.data.points)
//: Theme.of(context).accentColor,
/*border: Border.all(
width: 2,
color: changeMedalColor(snapshot.data.points),
),*/
),
//duration: Duration(milliseconds: 250),
child: SizedBox(
child: !compassExpanding
? Transform.rotate(
angle: vmath.radians(double.parse(
((compassDir.data) ?? 0)
.toString()) -
45),
child: CircleAvatar(
minRadius: 1000,
maxRadius: 5000,
backgroundImage: NetworkImage(
globals.userData.photoURL,
),
backgroundColor: changeMedalColor(
snapshot.data.points),
),
)
: Transform.rotate(
angle: vmath.radians(double.parse(
((compassDir.data) ?? 0)
.toString()) -
45),
child: Icon(Icons.expand,
color: Colors.white),
),
),
),
);
}),
),
),
),
),
),
],
builder: (context, transition) {
return ClipRRect(
//borderRadius: BorderRadius.circular(8),
child: Material(
color: Colors.white,
//elevation: 4.0,
child: !offlineMode
? Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Search for a GeoTag by name, approximate location. To search by author, use \'#\' in front of the search query.',
textAlign: TextAlign.center,
),
),
SizedBox(height: 15),
Column(
mainAxisSize: MainAxisSize.min,
children: Colors.accents.map((color) {
return Container(height: 112, color: color);
}).toList(),
),
],
)
: Column(
children: [
Center(
child: Icon(
Icons.cloud_off,
size: 40,
),
),
SizedBox(height: 10),
Text(
'You\'re in Offline Mode',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
'Whilst in Offline Mode, the search feature is disabled.'),
],
),
),
);
},
),
);
}
Code inside drawer to update variable:
DescribedFeatureOverlay(
featureId: 'offlineMode',
tapTarget: Icon(Icons.file_download),
title: Text('Offline Mode'),
description: Text(
'One of the most important and complex features of this app. Allows for fully offline functioning, including downloading large areas of maps. You can see more detail about it\'s features and drawbacks by tapping this menu item after the introduction is complete.'),
overflowMode: OverflowMode.extendBackground,
backgroundColor: Theme.of(widget.mainContext).primaryColor,
targetColor: Colors.white,
textColor: Colors.white,
child: ListTile(
title: Text(offlineMode
? 'Offline Manager'
: (internetConnected
? 'Enable Offline Mode'
: 'Offline Mode Disabled')),
onTap: offlineMode
? () {
Navigator.pushNamed(
widget.mainContext, OfflineManagerScreen.routeName);
}
: (internetConnected //Don't worry about internetConnected
? () {
showAlert(
context,
widget.mainContext,
'Offline Mode',
<Widget>[
//Some text widgets
],
'Agree & Enable Offline Mode',
'Cancel', () {
StorageManager.saveData('offlineMode', 'true');
setState(() {
offlineMode = true; //Should Update
});
if (!Directory(globals.saveDir.path + '/tiles')
.existsSync()) {
Directory(globals.saveDir.path + '/tiles')
.createSync(recursive: true);
}
});
}
: null),
leading: Icon(offlineMode
? Icons.offline_pin
: (internetConnected
? Icons.file_download
: Icons.cloud_off)),
trailing: Switch(
value: offlineMode,
onChanged: offlineMode && internetConnected
? (bool newVal) {
showAlert(
context,
widget.mainContext,
'Disable Offline Mode',
<Widget>[
const Text(
'Disabling Offline Mode will delete all saved tiles, and resync information with the server. Any GeoTags tagged will be uploaded, and the appropriate point value will be added to your account. Are you sure you want to disable Offline Mode?',
textAlign: TextAlign.justify,
),
],
'Disable',
'Stay Offline', () {
setState(() {
offlineMode = false; //Should update
});
StorageManager.saveData('offlineMode', 'false');
Directory(globals.saveDir.path + '/tiles')
.deleteSync(recursive: true);
setState(() {
totalSize = 0;
filesList = [];
sizeList = [];
});
});
}
: null,
activeColor: Theme.of(widget.mainContext).primaryColorLight,
),
),
),
If you haven't already realised, this is my first big Flutter project, so tips about other things are also appreciated!
Widget is updating on inner widget. but all state is not being changed.
you should pass state to all your inner widgets like this (in showalert in your case)
StatefulBuilder(builder: (context, newState) {
return
showAlert(
context,
widget.mainContext,
'Offline Mode',
<Widget>[
//Some text widgets
],
'Agree & Enable Offline Mode',
'Cancel', () {
StorageManager.saveData('offlineMode', 'true');
newState(() {
offlineMode = true; //Should Update
});
if (!Directory(globals.saveDir.path + '/tiles')
.existsSync()) {
Directory(globals.saveDir.path + '/tiles')
.createSync(recursive: true);
}
});
});
Please try this! I didnt test, but the logic is you should pass the state.
Maybe you should wrap listtile with new state..
Questions:
1-) I have a data lists coming from sqlite database and I want to show them in UI. Also when user dismiss a card I want to delete it from database but if user taps undo then I want to bring it back. The problem is when I dismiss a card it's giving error:
A dismissed Dismissible widget is still part of the tree.
I searched solution for this problem but none of them fit my case because I use Bloc for these operations and other solutions were about setState
You can see in here:
GIF
2-) Also when I swipe card card's background color clipping:
Photo
How to fix these issues?
return Dismissible(
key: Key(widget.task.id.toString()),
background: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.red,
),
),
onDismissed: (direction) async {
Task copied = widget.task;
taskBloc.deleteTask(widget.task.id);
changeValue(widget.task.categoryId, false);
Flushbar(
messageText: Text('Task deleted', style: bodyText.copyWith(
color: Colors.white
)),
duration: Duration(seconds: 10),
margin: EdgeInsets.all(8),
borderRadius: 8,
icon: Icon(FontAwesomeIcons.infoCircle, color: widget.color),
flushbarStyle: FlushbarStyle.FLOATING,
dismissDirection: FlushbarDismissDirection.HORIZONTAL,
mainButton: FlatButton(
onPressed: () async {
taskBloc.addTask(copied);
changeValue(widget.task.categoryId, true);
},
child: Text('Undo', style: bodyText.copyWith(color: widget.color)),
),
)..show(context);
}),
You can achieve this passing confirmDismiss in the Dismissible widget as given below which should return either true else false;
confirmDismiss: (val) async {
return await AlertDialog(
title: Text('Delete'),
content: Text('Are you sure to delete?'),
actions:[
FlatButton(
child:Text("Yes"),
onPressed:()=>Navigator.pop(context,true)
),
FlatButton(
child:Text("No"),
onPressed:()=>Navigator.pop(context,false)
),
],
)??false;
}
After that the onDismissed is called only when the Yes button is clicked . after the dismissed is called remove the item from the current list
void _onDismissed(BuildContext context, index) => setState(
() {
FavouriteSong favouriteSong = favouriteSongs[index];
favouriteSongs.removeAt(index);
BlocProvider.of<FavouriteSongBloc>(context)
.add(DeleteFavouriteSong(favouriteSong: favouriteSong));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
AutoSizeText("${favouriteSong.title} ကိုဖျက်လိုက်ပါပြီ။"),
duration: const Duration(seconds: 3),
action: SnackBarAction(
label: 'မဖျက်တော့ပါ',
onPressed: () {
favouriteSongs.insert(index, favouriteSong);
BlocProvider.of<FavouriteSongBloc>(context).add(
CreateFavouriteSong(favouriteSong: favouriteSong),
);
},
),
),
);
},
);
You just forget to remove element to current list and refresh with setState(() {})
return Dismissible(
key: Key(widget.task.id.toString()),
background: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.red,
),
),
onDismissed: (direction) async {
Task copied = widget.task;
taskBloc.deleteTask(widget.task.id);
changeValue(widget.task.categoryId, false);
setState(() {})
Flushbar(
messageText: Text('Task deleted', style: bodyText.copyWith(
color: Colors.white
)),
duration: Duration(seconds: 10),
margin: EdgeInsets.all(8),
borderRadius: 8,
icon: Icon(FontAwesomeIcons.infoCircle, color: widget.color),
flushbarStyle: FlushbarStyle.FLOATING,
dismissDirection: FlushbarDismissDirection.HORIZONTAL,
mainButton: FlatButton(
onPressed: () async {
// to success rollback you must change id because of (Key(widget.task.id.toString())),
// otherwise you will get the same error
taskBloc.addTask(copied);
changeValue(widget.task.categoryId, true);
},
child: Text('Undo', style: bodyText.copyWith(color: widget.color)),
),
)..show(context);
}),