Holding a state within StatefulWidget - flutter

I have a settings page where I'm holding a path for a keyfile in SavedPreferences. It is also possible to redefine the path for keyfile in this settings page.
class Settings extends StatefulWidget {
const Settings({Key? key}) : super(key: key);
#override
_SettingsState createState() => _SettingsState();
}
class _SettingsState extends State<Settings> {
void initState() {
getSettings();
super.initState();
}
void getSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_keyPath = prefs.getString('keyPath')!;
_keyFile = _keyPath.split('/').last;
}
String _keyPath = '';
String _keyFile = '';
Future<void> openFile() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_keyPath = (await FlutterFileDialog.pickFile())!;
setState(() {
print(_keyPath);
_keyFile = _keyPath.split('/').last;
prefs.setString('keyPath', _keyPath);
});
}
#override
Widget build(BuildContext context) {
getSettings();
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
),
body: Column(
children: [
Row(
children: [
Expanded(
child: Column(
children: [
Text('Key File: '),
Text(_keyFile),
],
),
),
Expanded(
child: ElevatedButton(onPressed: openFile, child: Text('Open')),
)
],
)
],
),
);
}
}
This works fine when initializing for first time but when the Widget is already initialized and the navigated back second time I'm having trouble to use the saved key in SharedPreferences when navigating back to this page.
I know I'm getting the value for _keyFile and _keyPath when renavigating in
String _keyPath = '';
String _keyFile = '';
Cant figure out how to call async function when renavigating to widget without initState to use SharedPreferences
I guess this should be done via state and not to query the items from SharedPreferences but I'm little clueless how to do this exactly.

I would suggest you to use a FutureBuilder instead of getting SharedPreferences in InitState:
FutureBuilder(
future: SharedPreferences.getInstance(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
// set keys and show your Column widget
} else if (snapshot.hasError) {
// Show error widget
} else {
// Show loading Widget
}
},
),
);
Like this you will get the saved value in your SharedPreferences everytime you navigate to this widget. For more information, you can check the doc link.

For example you can use GlobalKey to store the scaffold state in use to show snackbar and etc...

Related

Flutter Unhandled Exception: This widget has been unmounted, so the State no longer has a context (and should be considered defunct)

I've already checked the previous answer but can't help me.
I'm checking that user has internet or not if not then I'm showing pic of no internet. Now I've a button of Retry where user can click on it to check if he has internet back.
Now here I'm facing error when i click on Retry button
Unhandled Exception: This widget has been unmounted, so the State no longer has a context (and should be considered defunct).
E/flutter (25542): Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.
My Updated splash screen
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
#override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
Future<bool> isLoggedIn() async {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString("token");
print("Token obtained: $token");
return token != null;
}
#override
void initState() {
_init();
super.initState();
}
Future<void> _init() async {
bool isConnected = await NetworkHelper.checkInternet();
if (!isConnected) {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => CheckConnection(
onRetry: _init,
)),
);
}
} else {
await Future.delayed(const Duration(seconds: 3), () async {
final isTokenValid = await isLoggedIn();
if (isTokenValid) {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => BottomNav()),
);
}
} else {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => Signup()),
);
}
}
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.white,
child: FlutterLogo(size: MediaQuery.of(context).size.height),
),
);
}
}
Here is my image showing if there is no internet.
class CheckConnection extends StatelessWidget {
final VoidCallback onRetry;
const CheckConnection({Key? key, required this.onRetry}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset("assets/image/no_internet.jpg"),
const SizedBox(height: 20),
ElevatedButton(
onPressed: onRetry,
child: const Text("Retry"),
),
],
),
);
}
}
You have problems in your code. You are trying to access the context after what's called asynchronous suspension, i.e., you are accessing the context after some Future has done its work, while at that instance, the context might be in an invalid state, maybe because the Element tree has changed during this "asyncronois suspension". I also assume you are getting a warning about this but you are ignoring it.
The solution to this problem is, as the error suggests, use the getter mounted which is available inside the State class, after your Future does its work and before you access the context:
await Future.delayed(...);
if(mounted){
// you can use the context
}
else{
// you can't use the context
}
In your case, you need to check if the context is still valid by checking mounted variable before using it after a Future returns:
if(mounted) // you should put this check in all places where you use context after an asyncronous suspension
{
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => BottomNav()),
);
}
else{
// the context is not valid
// you should handle this case according to your app needs
}
Also see this answer.

Flutter - How to pass a future to a widget without executing it?

I am using FutureBuilder in one of my widgets and it requires a future. I pass the future to the widget through its constructor. The problem is that while passing the future to the widget it gets automatically executed. Since the FutureBuilder accepts only a Future and not a Future Function() i am forced to initialize a variable which in turn calls the async function. But i don't know how to pass the Future without it getting executed.
Here is the complete working example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final icecreamSource = DataService.getIcecream();
final pizzaSource = DataService.getPizza();
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Column(
children: [
MenuButton(label: 'Ice Cream', dataSource: icecreamSource),
MenuButton(label: 'Pizza', dataSource: pizzaSource),
]
),
),
),
);
}
}
class MenuButton extends StatelessWidget {
final String label;
final Future<String> dataSource;
const MenuButton({required this.label, required this.dataSource});
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton(
child: Text(label),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => AnotherPage(label: label, dataSource: dataSource)))
),
);
}
}
// Mock service to simulate async data sources
class DataService {
static Future<String> getIcecream() async {
print('Trying to get ice cream...');
return await Future.delayed(const Duration(seconds: 3), () => 'You got Ice Cream!');
}
static Future<String> getPizza() async {
print('Trying to get pizza...');
return await Future.delayed(const Duration(seconds: 2), () => 'Yay! You got Pizza!');
}
}
class AnotherPage extends StatefulWidget {
final String label;
final Future<String> dataSource;
const AnotherPage({required this.label, required this.dataSource});
#override
State<AnotherPage> createState() => _AnotherPageState();
}
class _AnotherPageState extends State<AnotherPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.label)),
body: Center(
child: FutureBuilder<String>(
future: widget.dataSource,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if(snapshot.hasData) {
return Text('${snapshot.data}');
} else if(snapshot.hasError) {
return Text('Error occurred ${snapshot.error}');
} else {
return Text('Fetching ${widget.label}, please wait...');
}
}
),
),
);
}
}
The intended behaviour is that when i press the "Ice Cream" or "Pizza" button on the main page, the widget/screen named "Another Page" should appear and the async request should get executed during which the loading message should be displayed. However, what is happening is that on loading the homepage, even before pressing any of the buttons, both the async requests are getting executed. On pressing any of the buttons, the loading message does not appear as the request is already completed so it directly shows the result, which is totally undesirable. I am now totally confused about Futures and Future Functions. Someone please help me out.
Instead of passing the Future you could pass the function itself which returns the Future. You can try this example here on DartPad.
You have to modify MyApp like this:
final icecreamSource = DataService.getIcecream; // No () as we want to store the function
final pizzaSource = DataService.getPizza; // Here aswell
In MenuButton and in AnotherPage we need:
final Future<String> Function() dataSource; // Instead of Future<String> dataSource
No we could pass the future directly to the FutureBuilder but it's bad practice to let the FutureBuilder execute the future directly as the build method gets called multiple times. Instead we have this:
class _AnotherPageState extends State<AnotherPage> {
late final Future<String> dataSource = widget.dataSource(); // Gets executed right here
...
}
Now we can pass this future to the future builder.
instead passing Future function, why you dont try pass a string ?
Remove all final Future<String> dataSource;. You dont need it.
you can use the label only.
.....
body: Center(
child: FutureBuilder<String>(
future: widget.label == 'Pizza'
? DataService.getPizza()
: DataService.getIcecream(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
....
i just test it in https://dartpad.dev . its work fine.
you dont have to make complex, if you can achive with simple way.

How to use FAB with FutureBuilder

What I would like to achieve: show a FAB only if a webpage responds with status 200.
Here are the necessary parts of my code, I use the async method to check the webpage:
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
late Future<Widget> futureWidget;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
void initState() {
super.initState();
futureWidget = _getFAB();
}
Future<Widget> _getFAB() async {
final response = await http
.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// return something to create FAB
return const Text('something');
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load url');
}
}
And with the following FutureBuilder I am able to get the result if the snapshot has data:
body: Center(
child: FutureBuilder<Widget>(
future: futureWidget,
builder: (context, snapshot) {
if (snapshot.hasData) {
return FloatingActionButton(
backgroundColor: Colors.deepOrange[800],
child: Icon(Icons.add_shopping_cart),
onPressed:
null); // navigate to webview, will be created later
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
// By default, show a loading spinner.
return const CircularProgressIndicator();
},
)
My problem is that I want to use it here, as a floatingActionButton widget:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
[further coding...]
),
body: // Indexed Stack to keep data
IndexedStack(
index: _selectedIndex,
children: _pages,
),
floatingActionButton: _getFAB(),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>
[further coding...]
But in this case Flutter is throwing the error
The argument type 'Future' can't be assigned to the parameter type 'Widget?'.
Sure, because I am not using the FutureBuilder this way. But when I use FutureBuilder like in the coding above then Flutter expects further positional arguments like column for example. This ends in a completely different view as the FAB is not placed over the indexedstack in the typical FAB position anymore.
I have searched for several hours for a similar question but found nothing. Maybe my code is too complicated but Flutter is still new to me. It would be great if someone could help me :)
You can use the just _getFAB() method to do it. You can't assign _getFab() method's return value to any widget since it has a return type Future. And also, when you are trying to return FAB from the FutureBuilder it will return FAB inside the Scaffold body.
So, I would suggest you fetch the data from the _getFAB() method and assign those data to a class level variable. It could be bool, map or model class etc. You have to place conditional statements in the widget tree to populate the state before the data fetching and after the data fetching. Then call setState((){}) and it will rebuild the widget tree with new data. Below is an simple example.
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class FabFuture extends StatefulWidget {
const FabFuture({Key? key}) : super(key: key);
#override
State<FabFuture> createState() => _FabFutureState();
}
class _FabFutureState extends State<FabFuture> {
bool isDataLoaded = false;
#override
void initState() {
super.initState();
_getFAB();
}
Future<void> _getFAB() async {
final response = await http
.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
if (response.statusCode == 200) {
isDataLoaded = true;
setState(() {});
} else {
isDataLoaded = false;
//TODO: handle error
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: const Center(
child: Text('Implemet body here'),
),
floatingActionButton: isDataLoaded
? FloatingActionButton(
backgroundColor: Colors.deepOrange[800],
child: const Icon(Icons.add_shopping_cart),
onPressed: null)
: const SizedBox(),
);
}
}
Here I used a simple bool value to determine if I should show the FAB or not. The FAB will only show after the data is successfully fetched.
After practicing these ways and you get confident about them, I would like to suggest learning state management solutions to handle these types of works.

Flutter provider profile picture not updating

I am building a method that the user can select a prefered profile picture to show arround the app, using provider package. I used shared_preferences to save the profile picture preferences on locally as a int value. And it worked, means I can save the profile picture to local system. But the problem is, the provider package completely became useless in this case, because I have to convert the widget to statefull and call the setState method when ever I insert a profilePicture widget inside the widget tree. And even the profilePicture widget in the HomeScreen not updating this way. I want to know how can I use the provider package for this issue instead of using statefulWidgets.
watch the Gif or video
This is the Provider class I created:
class ProfilePicProvider with ChangeNotifier {
ProfilePicPref profilePicPreferences = ProfilePicPref();
int _svgNumber = 1;
int get svgNumber => _svgNumber;
set svgNumber(int value) {
_svgNumber = value;
profilePicPreferences.setProfilePic(value);
notifyListeners();
}
void changePic(int val) {
_svgNumber = val;
profilePicPreferences.setProfilePic(val);
notifyListeners();
}
}
This is the sharedPreferences class
class ProfilePicPref {
static const PRO_PIC_STS = 'PROFILESTATUS';
setProfilePic(int svgNo) async {
SharedPreferences profilePref = await SharedPreferences.getInstance();
profilePref.setInt(PRO_PIC_STS, svgNo);
}
Future<int> getProfilePicture() async {
SharedPreferences profilePref = await SharedPreferences.getInstance();
return profilePref.getInt(PRO_PIC_STS) ?? 1;
}
}
This is the image selection screen and save that data to sharedPreferences class
class SelectProfilePicture extends StatefulWidget {
const SelectProfilePicture({Key? key}) : super(key: key);
#override
State<SelectProfilePicture> createState() => _SelectProfilePictureState();
}
class _SelectProfilePictureState extends State<SelectProfilePicture> {
int svgNumber = 1;
ProfilePicProvider proProvider = ProfilePicProvider();
#override
void initState() {
getCurrentProfilePicture();
super.initState();
}
void getCurrentProfilePicture() async {
proProvider.svgNumber =
await proProvider.profilePicPreferences.getProfilePicture();
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
CurrentAccountPicture(
path: 'assets/svg/${proProvider.svgNumber}.svg'),
Expanded(
child: GridView.builder(
itemCount: 15,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
setState(() {
svgNumber = index + 1;
});
proProvider.changePic(index + 1);
proProvider.svgNumber = index + 1;
},
child: SvgPicture.asset('assets/svg/${index + 1}.svg'),
);
},
),
),
],
),
);
}
}
This is the HomeScreen which is not updating the profile image whether it is statefull or stateless
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final proPicProvider = Provider.of<ProfilePicProvider>(context);
return Scaffold(
body:
Column(
children: [
Row(
children: [
CurrentAccountPicture(
path: 'assets/svg/${proPicProvider.svgNumber}.svg'),
],
),
],
),
);
}
}
example:
I have to convert the widget to statefull and call setState method to get the current profile picture from sharedPreferences. You may find this screen from the GIF I provided.
class Progress extends StatefulWidget {
const Progress({Key? key}) : super(key: key);
#override
State<Progress> createState() => _ProgressState();
}
class _ProgressState extends State<Progress> {
ProfilePicProvider proProvider = ProfilePicProvider();
#override
void initState() {
getCurrentProfilePicture();
super.initState();
}
void getCurrentProfilePicture() async {
proProvider.svgNumber =
await proProvider.profilePicPreferences.getProfilePicture();
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SizedBox(
height: 130.0,
width: 130.0,
child: SvgPicture.asset(
'assets/svg/${proProvider.svgNumber}.svg'),
),
),
);
}
}
The problem is in _SelectProfilePictureState when you create new instance of your ChangeNotifier:
ProfilePicProvider proProvider = ProfilePicProvider();. It means you are not using the provider available across the context but creating new one every time. So when the value of your provider changed it has effect only inside _SelectProfilePictureState. Instead of creating new instance you must call it always using the context:
class SelectProfilePicture extends StatefulWidget {
const SelectProfilePicture({Key? key}) : super(key: key);
#override
State<SelectProfilePicture> createState() => _SelectProfilePictureState();
}
class _SelectProfilePictureState extends State<SelectProfilePicture> {
int svgNumber = 1;
// [removed] ProfilePicProvider proProvider = ProfilePicProvider();
//removed
/*void getCurrentProfilePicture() async {
proProvider.svgNumber =
await proProvider.profilePicPreferences.getProfilePicture();
setState(() {});
}*/
#override
Widget build(BuildContext context) {
//use provider from the context
final proProvider = Provider.of<ProfilePicProvider>(context,listen:true);
return Scaffold(
body: Column(
children: [
CurrentAccountPicture(
path: 'assets/svg/${proProvider.svgNumber}.svg'),
Expanded(
child: GridView.builder(
itemCount: 15,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
setState(() {
svgNumber = index + 1;
});
proProvider.changePic(index + 1);
proProvider.svgNumber = index + 1;
},
child: SvgPicture.asset('assets/svg/${index + 1}.svg'),
);
},
),
),
],
),
);
}
}
If you enter the application you may want send initially selected image to your provider:
Add parameter to the constructor of ProfilePicProvider:
ProfilePicProvider(SharedPreferences prefs): _svgNumber = prefs.getInt(ProfilePicPref.PRO_PIC_STS) ?? 1;
In main.dart:
Future<void> main()async{
WidgetsFlutterBinding.ensureInitialized();
var prefs = await SharedPreferences.getInstance();
runApp(
MultiProvider(
providers:[
ChangeNotifierProvider( create:(_) => ProfilePicProvider(prefs))
],
child: yourtopWidget
)
);
}

SharedPreference does not read local data in Flutter

In a Flutter project, on clicking the "SignUp" button, I need 2 things,
Saving data to cloud
Saving data to local storage.
I user SharedPreferece to save the data and to retrieve it. The problem is, the data I saved into local storage is available immediately after I Sign Up, but if I hot reload the emulator, the data shows null!
The function by which I saved the data to both cloud and local storage:
Future <void> _saveDataToFirestore(User? currentUser) async{
await FirebaseFirestore.instance.collection("sellers").doc(currentUser!.uid).set({
"sellerUID": currentUser.uid,
"sellerName": _fullNameController.text.trim(),
"sellerAvatarUrl": sellerImageUrl,
"sellerEmail": currentUser.email,
"phone": _phoneNumberController.text.trim(),
"address": completeAddress,
"status": "approved",
"earnings": 0.0,
"lat": position!.latitude,
"lng": position!.longitude
});
// save 3 data locally
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
await sharedPreferences.setString("uid", currentUser.uid);
await sharedPreferences.setString("email", currentUser.email.toString()); // do not take from controllers, because it will not be null if sign up fail
await sharedPreferences.setString("name", _fullNameController.text);
await sharedPreferences.setString("image", sellerImageUrl);
print("${currentUser.uid},${currentUser.email.toString()}, ${_fullNameController.text}, ${sellerImageUrl} ");}
I initialized SharedPreference in main.dart
Future <void> main() async {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
await SharedPreferences.getInstance();
await Firebase.initializeApp();
runApp(const MyApp());
}
The home_screen where I needed the local storage data
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:food_fancy_chef/authentication/auth_screen.dart';
import 'package:food_fancy_chef/authentication/login.dart';
import 'package:food_fancy_chef/authentication/register.dart';
import 'package:food_fancy_chef/global/global.dart';
import 'package:shared_preferences/shared_preferences.dart';
class HomeScreen extends StatefulWidget {
static const routeName = "home_screen";
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
String sharedName = "Null value";
Future<void> _getIntFromSharedPref()async{
final pref = await SharedPreferences.getInstance();
final startupName = pref.getString("name");
if(startupName == null){
sharedName = "no name";
} else{
setState(() {
sharedName = startupName;
});
}
}
#override
void initState() {
_getIntFromSharedPref();
super.initState();
}
#override
void didChangeDependencies() {
_getIntFromSharedPref();
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.cyan,
Colors.amber
],
begin: FractionalOffset(0.0, 0.0),
end: FractionalOffset(1.0, 1.6),
stops: [0.0,1.0],
tileMode: TileMode.mirror
)
),
),
title: Text(
sharedName // I need "name" from local storage
// sharedPreferences!.getString("name")! == null? "null value":sharedPreferences!.getString("name")!
)
),
body: Column(
children: [
Center(
child: ElevatedButton(
onPressed: (){
setState(() {
firebaseAuth.signOut();
Navigator.pushReplacement(context, MaterialPageRoute(builder: (c)=> LoginScreen()));
});
},
child: Text("Log Out"),
),
),
Center(
child: ElevatedButton(
onPressed: (){
setState(() {
firebaseAuth.signOut();
Navigator.pushReplacement(context, MaterialPageRoute(builder: (c)=> AuthScreen()));
});
},
child: Text("signout"),
),
),
],
),
);
}
}
What I have tried:
I have tried to get the data directly without any init() method, it returns null:
sharedPreferences!.getString("name")! == null? "null value":sharedPreferences!.getString("name")!
I have declared the variable first and assigned the value via a function run at init(), code is below.
I tried the same process above but with didChangeDependencies() method.
I used both init() and didChangeDependencies()
I deleted the emulator and reinstalled it.
I also saved SharePreferences() in a global.dart file, so that, I can access them anywhere in the project.
Try this
Future<void> _getIntFromSharedPref() async{
SharedPreferences? pref = await SharedPreferences.getInstance();
String startupName = pref!.getString("name") ?? 'no name';
setState(() {});
}
Try removing SharedPreferences.setMockInitialValues({}); from main.dart.
When hot restart the widget tree re built it means only this
Widget build(BuildContext context) method triggered. initState is called only once for a widget and didChangeDependencies may be called multiple times per widget lifecycle. So initializing some thing on didChangeDependencies you have to be careful. Calling like this will resolve your issue whille hot restart
#override
Widget build(BuildContext context) {
_getIntFromSharedPref();
return Scaffold(
);
}