How to separate setState in Abc class?
I have lots of this kind of methods in one class. Now, my class is too long. Is there any way to separate this class to subclass or another way?
class _AbcState extends State<Abc> {
String name = 'name';
double number = 15.5;
int value = 10;
//REMOVE FROM ABC class
void changeName() {
setState(() {
name = 'new Name';
});
}
void changeNumber() {
setState(() {
number = 10.0;
});
}
void changeValue() {
setState(() {
value = 5;
});
}
//
#override
Widget build(BuildContext context) {
return Scaffold();
}
}
There are 2 ways to do this:
First:
You can separate the individual input fields into their stateful widgets if necessary and let them handle their individual states internally, only giving you the changed data in respective onChange callbacks.
This is the simplest and quickest solution.
A better approach is to:
Second:
Use a library/dependency/package for the state management in your application:
This is an example of the Provider flutter package with ChangeNotifier:
You can check the official docs here for detailed explanations and examples:
Provider
ChangeNotifier
Simple example:
Create a class extending the ChangeNotifier:
Whenever the some data changes in the ChangeNotifer you can call the notifyListeners() method.
class UserProvider extends ChangeNotifier {
String username;
String password;
void updateUsername(String username) {
this.username = username;
notifyListeners();
}
void updatePassword(String password) {
this.password = password;
notifyListeners();
}
}
Wrap your widget tree in a ChangeNotifierProvider widget:
This widget will let you access the instance of the ChangeNotifier class anywhere in down the widget tree like InheritedWidget.
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<UserProvider>(
lazy: false,
create: (context) => UserProvider(),
child: Home(),
);
}
}
Access the instance of ChangeNotifier from anywhere down the widget tree with and call any methods or data variables:
/// Get an instance of the UserProvider in the ancestors of the current widget tree like this.
UserProvider userProvider = Provider.of<UserProvider>(context);
/// Call any method inside the UserProvider class like this
userProvider.updateUsername("John Doe");
/// access any data variables inside the UserProvider class like this.
User userInfo = userProvider.username;
Changing UI based on data/state changes in ChangeNotifier:
You can use the Consumer and Selector widgets in the provider package, which provide an efficient ways to redraw the UI based on certain parameters of the ChangeNotifier class, when the notifyListeners() method is called from the ChangeNotifier class.
Consumer:
This widget will simply rebuild the entire widget tree wrapped inside it, whenever ANY data changes inside the ChangeNotifier with notifyListeners().
class Home extends StatelessWidget {
const Home({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<UserProvider>(
builder: (context, userProvider, _) {
return Scaffold(
appBar: AppBar(
title: const Text(userProvider.username),
),
body: Center(
child: Text(
"Your password is: ${userProvider.password}!",
),
),
);
},
);
}
}
Selector:
This widget will allow for more finer control over your widget rebuild. It will rebuild only part of the widget based on specific data variable changes inside the ChangeNotifier.
The below example will only rebuild the Text widget if the password data changes inside our UserProvider ChangeNotifier class.
class Home extends StatelessWidget {
const Home({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<UserProvider>(
builder: (context, userProvider, _) {
return Scaffold(
appBar: AppBar(
title: const Text(userProvider.username),
),
body: Center(
child: Selector<UserProvider, String>(
selector: (context, userProvider) => userProvider.password,
builder: (context, state, child) {
return Text(
"Your password is: ${userProvider.password}!",
);
},
),
),
);
},
);
}
}
Related
I have learned that it brings problems when initating a future- or streambuilder inside the build of a widget since it will lead to unwanted fetching of data.
To do this you pass the stream outside of the build-method. But this leads to another problem. In my case I have to get an id-argument from the last widget but that can only be reached inside the build. I have searched the internet for explanations on how to solve this but I cant find an easy and clean explanation on the best way to do this.
A good solution that initialize the future with the build outside of the build.
My material app:
MaterialApp(
routes: appRoutes,
);
My routes table:
var appRoutes = {
Screen1.routeName: (context) => Screen1(),
Screen2.routeName: (context) => Screen2(),
};
How I send the argument from screen1:
onTap: () {
Navigator.of(context).pushNamed(
Screen2.routeName,
arguments: id,
);
},
screen2: (which doesnt work this way)
class Screen2 extends StatefulWidget {
static const routeName = '/screen2';
#override
State<Screen2> createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
#override
final String id = ModalRoute.of(context)!.settings.arguments as String; <====== Impossible
final _stream = FirestoreService().getData(id);
Widget build(BuildContext context) {
return FutureBuilder<Map>(
future: _stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
print('succes');
} else {
print('Fail');
}
});
}
}
Way1: late keyword (unstable)
Use late keyword for lazy initialiation of your argument, stream.
class Screen2 extends StatefulWidget {
static const routeName = '/screen2';
#override
State<Screen2> createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
#override
late final String id = ModalRoute.of(context)!.settings.arguments as String; <====== Impossible
late final _stream = FirestoreService().getData(id);
Widget build(BuildContext context) {
...
}
}
Way2: onGerateRoute + constructor (good)
You can check this answer for using onGenerateRoute. It is more proper way.
Way3: Create extraction delegation screen (good)
In the docs, the best practice is creating a delegation screen for extract arguments.
It parse argument in build method and pass it to new screen.
// A Widget that extracts the necessary arguments from
// the ModalRoute.
class ExtractArgumentsScreen extends StatelessWidget {
const ExtractArgumentsScreen({super.key});
static const routeName = '/extractArguments';
#override
Widget build(BuildContext context) {
// Extract the arguments from the current ModalRoute
// settings and cast them as ScreenArguments.
final args = ModalRoute.of(context)!.settings.arguments as ScreenArguments;
return Scaffold(
appBar: AppBar(
title: Text(args.title),
),
body: Center(
child: Text(args.message),
),
);
}
}
Basically, I want to set the initial constructor value to StateProvider so I can use it in its child widgets.
final stateProvider = StateProvider((ref) => "default");
class ExampleWidget extends ConsumerWidget {
ExampleWidget({required String text}){
// how to override default hear as
// stateProvider = text
}
#override
Widget build(BuildContext context, ScopedReader watch) {
return Column(
children: [
Text(watch(stateProvider).state),
Container(
child: ChildWidget(),
),
],
);
}
}
class ChildWidget extends ConsumerWidget {
#override
Widget build(BuildContext context, ScopedReader watch) {
return Container(
child: Text(watch(stateProvider).state),
);
}
}
So how do I implement this?
I have tried this but the state value is not updating.
Edit ----------------
With riverpod_hook & hooks_flutter
final stateProvider = StateProvider((ref) => "default");
class ExampleWidget extends HookWidget {
#override
Widget build(BuildContext context) {
WidgetsBinding.instance?.addPostFrameCallback((_) {
useEffect((){
context.read(stateProvider).state = "new value";
},[]);
});
final state = useProvider(stateProvider).state;
return Column(
children: [
Text(state),
],
);
}
}
above code throwing this error
Hooks can only be called from the build method of a widget that mix-in Hooks``
So I tried to initialize under the build method like this
useEffect((){
context.read(stateProvider).state = "new value";
},[]);
but above code throws the following error:
The following Error was thrown building ExampleWidget(dirty, dependencies: [UncontrolledProviderScope]):
Instance of 'Error'
I just want to initialize the value once that's why I want to use useEffect hook.
You can't access providers in the constructor normally. You also shouldn't update the state directly in the build method, as this can cause build errors (the widget hasn't finished building before you set a state which would cause another rebuild).
You should be able to use addPostFrameCallback to change the state after the widget has built.
For example:
class ExampleWidget extends ConsumerWidget {
const ExampleWidget({required String text});
final String text;
#override
Widget build(BuildContext context, ScopedReader watch) {
WidgetsBinding.instance?.addPostFrameCallback((_) {
context.read(stateProvider).state = text;
});
return Column(
children: [
Text(watch(stateProvider).state),
Container(
child: ChildWidget(),
),
],
);
}
}
Regarding your edits:
Hooks can only be called from the build method of a widget that mix-in Hooks
This is because the callback function is not really in the build method, it's executed externally.
The following Error was thrown building ExampleWidget(dirty, dependencies: [UncontrolledProviderScope]):
Instance of 'Error'
This is an example of the build errors I mentioned earlier in this post.
I would say you're better off refactoring to just set the StateProvider to the text in a prior widget rather than accepting text as a parameter and trying to set it, but I think the following would work with your current approach, though again I'd recommend approaching your problem differently.
class ExampleWidget extends HookWidget {
const ExampleWidget({required String text});
final String text;
#override
Widget build(BuildContext context) {
final future = useMemoized(() => Future<void>.delayed(const Duration(milliseconds: 1)));
final snapshot = useFuture<void>(future, initialData: null);
if (snapshot.connectionState == ConnectionState.done) {
context.read(stateProvider).state = text;
}
return Column(
children: [
Text(useProvider(stateProvider).state),
Container(
child: ChildWidget(),
),
],
);
}
}
By delaying the state change until after the initial build we avoid build errors.
I am making a list of stateless widget as shown below and passing the id as the parameter to the widgets.
Code for cartPage:-
class Cart extends StatefulWidget {
#override
_CartState createState() => _CartState();
}
class _CartState extends State<Cart> {
bool loading=true;
List<CartTile> cartTiles=[];
#override
void initState() {
super.initState();
if(currentUser!=null)
getData();
}
getData()async
{
QuerySnapshot snapshot=await cartReference.doc(currentUser.id).collection('cartItems').limit(5).get();
snapshot.docs.forEach((doc) {
cartTiles.add(CartTile(id: doc.data()['id'],index: cartTiles.length,));
});
setState(() {
loading=false;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: loading?Center(child:CircularProgressIndicator():SingleChildScrollView(
child: Column(
children: cartTiles,
),
),
);
}
}
Code for CartTile:-
class CartTile extends StatelessWidget {
final String id;
CartTile({this.id,});
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: productReference.doc(id).snapshots(),
builder: (context,snapshot)
{
//here am using the snapshot to build the cartTile.
},
);
}
}
So, my question is whenever I will call setState in my homepage then will the stateless widget be rebuilt and increase my document reads. Because i read somewhere that when we pass the same arguments or parameters to a stateless widget then due to its cache mechanism it doesn't re build. If it will increase my reads then is there any other way to solve this problem?
I want to show a list of data with list view and JSON. This code in a file with stateless widget. And this page opened after login. When I try in stateful widget, the code RUN normally. In stateless widget, when I debug it, the code didn't call function getData(). But directly went to
Widget build(BuildContext context) {
return new Scaffold( ...
Here is complete code :
class frmClass extends StatelessWidget{
List<dynamic> dta;
get setState => null;
Future<String> getData() async {
var uri ='https://xxxx.com/class';
var map = new Map<String, dynamic>();
map["username"] = "abc";
map["password"] = "1234";
http.Response response = await http.post(
uri,
body: jsonEncode(map),
);
Map<String,dynamic> mp = jsonDecode(utf8.decode(response.bodyBytes));
this.setState(() {
dta = mp["data"];
debugPrint(dta.toString());
});
}
#override
void initState(){
this.getData();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
backgroundColor:Colors.transparent,
elevation: 0.0,
iconTheme: new IconThemeData(color: Color(0xFF18D191))),
body: new ListView.builder(
itemCount: dta == null ? 0 : dta.length,
itemBuilder: (BuildContext context, int index){
return new Card(
child: new Text(dta[index]["className"]),
);
}
),
);
}
}
How can I fix it?
You can use a FutureBuilder to call getData() into build() method of StatelessWidget:
https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
But, this will fire getData() every time your statelessWidget is rebuild.
Another way is to use reactive programing architecture (like Bloc, rxdart, etc..).
Depends on what do you want, fire getData() every time or just once (or when your conditions are true/false).
This is happening because stateless widgets don't have initState that's why the below code will never get called. Stateless widgets are more sort of rendering flat UI with no lifecycle methods if you want to use Stateless widget then pass data in the class constructor and use it where ever you want
#override
void initState(){
this.getData();
}
if you want to call it from a stateless widget? Well, that’s possible too. Use a stateful widget as a your root widget that you can provide a callback function too to execute your startup logic. See example below.
Create a StatefulWrapper that takes in a function to call and a child to display.
/// Wrapper for stateful functionality to provide onInit calls in stateles widget
class StatefulWrapper extends StatefulWidget {
final Function onInit;
final Widget child;
const StatefulWrapper({#required this.onInit, #required this.child});
#override
_StatefulWrapperState createState() => _StatefulWrapperState();
}
class _StatefulWrapperState extends State<StatefulWrapper> {
#override
void initState() {
if(widget.onInit != null) {
widget.onInit();
}
super.initState();
}
#override
Widget build(BuildContext context) {
return widget.child;
}
}
Wrap your stateles widget’s UI in a StatefulWrapper and pass on the onInit logic you would like to run
class StartupCaller extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StatefulWrapper(
onInit: () {
_getThingsOnStartup().then((value) {
print('Async done');
});
},
child: Container(),
);
}
Future _getThingsOnStartup() async {
await Future.delayed(Duration(seconds: 2));
}
}
and that’s it. The function will only be called once when the stateful widget is initialized.
This solution from fluttercampus solved my problem , where I wanted to call some method before build inside stateless widget
https://www.fluttercampus.com/guide/230/setstate-or-markneedsbuild-called-during-build/
#override
Widget build(BuildContext context) {
Future.delayed(Duration.zero,(){
CheckIfAlreadyLoggedIn();
});
return Container();
}
We want to show an AlertDialog after some asynchronous processing such as network processes.
When calling 'showAlertDialog ()' from an external class, I want to call it without context. Is there a good way?
class SplashPage extends StatelessWidget implements SplashView {
BuildContext _context;
#override
Widget build(BuildContext context) {
this._context = context;
...
}
I've considered the above method, but I'm worried about side issues.
Help
My current code
class SplashPage extends StatelessWidget implements SplashView {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: MyStoreColors.eats_white1_ffffff,
body: Center(
child: new SvgPicture.asset('assets/ic_splash.svg'),
),
);
}
#override
void showAlertDialog() {
showDialog<void>(
context: /*How to get context?*/,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Not in stock'),
content: const Text('This item is no longer available'),
actions: <Widget>[
FlatButton(
child: Text('Ok'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
#override
void moveToHomeContainer() {
}
#override
void moveToLoginContainer() {
}
}
To show an AlertDialog you need the context, but in StatelessWidget you do not have access to it directly as in StatefulWidget.
Few options are [1]:
passing it as GlobalKey [2]
passing build context as parameter to any other function inside StatelessWidget
use a service to inject the dialog without context [3]
Cheers.
You should trigger rebuild when the async event complete, either convert your widget to StatefulWidget and call setState() or use a state management solution like Bloc.
For example using StatefulWidget your code will look like this:
class SplashPage extends StatefulWidget {
#override
State<SplashPage> createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage> implements SplashView {
bool _asynOpDone = false;
/// Call this when the async operation is done.
void _onAsynOpDone() => setState(() => _asyncOpDone = true);
#override
Widget build(BuildContext context) {
if (_asyncOpDone) showAlertDialog(context);
return Scaffold(
...,
///
);
}
#override
void showAlertDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: ...,
);
}
}
You can apply Builder pattern concept to simplify this.
There is a little example here.
button_builder.dart