Flutter, pass object to viewModel trigger recreating of the widget - flutter

I would like to pass a model to a widget and his viewmodel.
I use this widget for 2 purposes: Add a new item and Edit the item.
So i will pass an empty object in case of new item and an already filled object in case of an edit.
Here i create a new post:
onTap: () {
Navigator.pop(context);
Navigator.of(context).push(
CupertinoPageRoute(
builder: (_) => CreatePost(PostPizza()),
),
).then((value) {
callback.call();
});
},
Here i set the post in the viewmodel:
class CreatePost extends StatefulWidget {
final PostPizza post;
#override
_CreatePostState createState() => _CreatePostState();
CreatePost(this.post);
}
class _CreatePostState extends State<CreatePost> {
#override
Widget build(BuildContext context) {
print("Create Post Widget build called");
PostsViewModel viewModel = Provider.of<PostsViewModel>(context);
viewModel.setPost(widget.post);
Setting the post object will trigger the text controllers :
acquaController.text = post.acqua ?? "";
saleController.text = post.acqua ?? "";
lievitoTipoController.text = post.lievito?.a ?? "";
lievitoAmountController.text = post.lievito?.b ?? "";
This will trigger the widget to rebuild continiously.
Any idea how can i solve the issue without all this?
This will

You can try this:
class _CreatePostState extends State<CreatePost> {
bool isSet = false, isLoading = true;
PostsViewModel? viewModel;
#override
Widget build(BuildContext context) {
print("Create Post Widget build called");
viewModel = Provider.of<PostsViewModel>(context);
if(isSet == false) {
isSet = true;
viewModel.setPost(widget.post);
isLoading = false;
}
return ....

Try to use this:
viewModel = Provider.of<PostsViewModel>(context, listen: false);
It will not perform rebuild.
Updated Answer, it will be called once:
#override
void initState() {
viewModel = Provider.of<PostsViewModel>(context, listen: false);
SchedulerBinding.instance.addPostFrameCallback((_) {
viewModel.setPost(widget.post);
});
super.initState();
}
#override
Widget build(BuildContext context) {
print("Create Post Widget build called");
/// Remove this:
/// PostsViewModel viewModel = Provider.of<PostsViewModel>(context);
/// viewModel.setPost(widget.post);
/// ...
}

Related

How to wait until datas are loaded?

I try to learn flutter and i face an issue with data loading.
I get information from sqlite database to display them in my homepage.
When starting my app, i have an error :
LateInitializationError: Field 'child' has not been initialized.
late MonneyServices monneyServices;
late ChildDAO childDAO;
late Child child;
void initState() {
super.initState();
this.monneyServices = MonneyServices();
monneyServices.getChild().then((Child child) {
this.child = child;
setState(() {});
});
the getChild method is async
Future<Child> getChild() async {
//return Child(1, 'Alice2', 100);
Child child = Child(1, 'A', 1);
this.childDAO.insertChild(Child(1, "Alice", 10));
List<Child> childList = await this.childDAO.getChilds();
child = childList.first;
print(childList.first);
return child;
}
I use so datas in
#override
Widget build(BuildContext context)
How can i wait until datas are loaded ?
Thanks for your help
You could use FutureBuilder.
It lets you to await for a future to complete and return a different widget according to the future status.
In your case you should use it in the build method and not in initState.
You should use it more or less like so:
Widget build(BuildContext context) {
return FutureBuilder<Widget>(context, snapshot){
if(snapshot.hasData){ //If the future has completed
return snapshot.data; //You return the widget it completed to
} else {
return CircularProgressIndicator(); //Otherwise, return a progress indicator
}
}
}
you can use a boolean variable be sure the data is loaded and reflect this in the build
late MonneyServices monneyServices;
late ChildDAO childDAO;
late Child child;
bool isLoading = true; // <--
void initState() {
super.initState();
this.monneyServices = MonneyServices();
monneyServices.getChild().then((Child child) {
this.child = child;
isLoading = false; // <--
setState(() {});
});
and in the build:
#override
Widget build(BuildContext context) {
if(isLoading) {
return Text('loading...');
}
return child;
}

Call setState from class that extends StatefulWidget

If I update a variable using class object, the build method should get called, but I am unable to call setState from the StatefulWidget class.
class CustomErrorFormField extends StatefulWidget {
#override
_CustomErrorFormFieldState createState() {
return _CustomErrorFormFieldState();
}
List<String> errorList = []; //this variable will get updated using below function
void setErrorList(List<String> listOfError) {
errorList = listOfError;
}
}
class _CustomErrorFormFieldState extends State<CustomErrorFormField> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
print(widget.errorList); //this is not printing updated value
return .....
}
}
Now in some other class i will update errorList Variable
nameTextFild = CustomErrorFormField(
key: ValueKey(count),
labelName: "Name",
iContext: context,
onChanged: (String value) {
setState(() {
count++;
if (!value.contains(RegExp(r'[0-9]'))) {
nameTextFild!.setErrorList([]); //updating but changes not appearing (setState of this widget is not getting called)
} else {
nameTextFild!.setErrorList(["Invalid characters, use letters only."]);
}
});
},
);
It's not recommended that you change the state of a widget from outside the widget.
What you should do instead is pass the validation logic as a function and let the widget handle the state change.
CustomFormField:
import 'package:flutter/material.dart';
class CustomErrorFormField extends StatefulWidget {
//Take the validation logic as a parameter.
final List<String> Function(String value) validator;
const CustomErrorFormField({required this.validator});
#override
_CustomErrorFormFieldState createState() {
return _CustomErrorFormFieldState();
}
}
class _CustomErrorFormFieldState extends State<CustomErrorFormField> {
//Keep the state inside the widget itself
List<String> errorList = [];
//Update the state from inside the widget
void setErrorList(List<String> listOfError) {
setState(() {
errorList = listOfError;
});
}
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Form(
child: TextFormField(
validator: (String value){
//Use the validation logic to decide the error.
setErrorList(widget.validator(value))
}
}
),
);
}
}
I have used TextFormField as an example, you can use any widget that accepts a callback upon change.
If you're making everything from scratch you can attach the validator function to a callback that fires when the text is changed. Usually this is done with the help of a controller.
usage:
final nameTextFild = CustomErrorFormField(
key: ValueKey(count),
labelName: "Name",
iContext: context,
validator: (String value) {
if (!value.contains(RegExp(r'[0-9]'))) {
return [];
} else {
return ["Invalid characters, use letters only."];
}
},
);

I want to use data from a Future inside a ChangeNotifier Provider and a ListView

I can't figure out how to get the data from the myProvider before I call the getWalletItems(). Should I do 2 seperate providers??
My goal here is just to get all these items from a Future<List<Wallet'>> and return them into a listview that is able to have each item be selectable with a checkbox which will then pass on all the selected items to a different page. They will not be rebuilt there so I don't think I need another model but if I do just let me know. Here is my code for the ChangeNotifier:
class WalletModel extends ChangeNotifier {
List<Wallet> _wallet = [];
List<Wallet> get wallet => _wallet;
set wallet(List<Wallet> newValue) {
_wallet = newValue;
notifyListeners();
}
myProvider() {
loadValue();
}
Future<void> loadValue() async {
wallet = await WalletApi.getWalletItems();
}
UnmodifiableListView<Wallet> get allWalletItems =>
UnmodifiableListView(_wallet);
UnmodifiableListView<Wallet> get incompleteTasks =>
UnmodifiableListView(_wallet.where((_wallet) => !_wallet.isSelected));
UnmodifiableListView<Wallet> get completedTasks =>
UnmodifiableListView(_wallet.where((_wallet) => _wallet.isSelected));
void toggleWallet(Wallet wallet) {
final walletIndex = _wallet.indexOf(wallet);
_wallet[walletIndex].toggleSelected();
notifyListeners();
}
}
Here is the checkbox to select
Checkbox(
value: wallet.isSelected,
onChanged: (bool? checked) {
Provider.of<WalletModel>(context, listen: false)
.toggleWallet(wallet);
},
),
Here is the listview and if I need to post anyother code just let me know because I'm quite lost on what to do.
class WalletList extends StatelessWidget {
final List<Wallet> wallets;
WalletList({required this.wallets});
#override
Widget build(BuildContext context) {
return ListView(
children: getWalletListItems(),
);
}
List<Widget> getWalletListItems() {
return wallets
.map((walletItem) => WalletListItem(wallet: walletItem))
.toList();
}
}
make myProvider() a future and then use below code for WalletList Widget
before build runs for WalletList we want to get the items from the provider so we have used didChangedDependencies() as it runs before build and can be converted to future.
when the list is got we use the list that was set by above the make the UI
Note : Consumer changes its state whenever notifyListener() is called in Provider.
import 'package:flutter/material.dart';
class WalletList extends StatefulWidget {
#override
_WalletListState createState() => _WalletListState();
}
class _WalletListState extends State<WalletList> {
bool _isInit = true;
#override
void didChangeDependencies() async {
//boolean used to run the set list fucntion only once
if (_isInit) {
//this will save the incoming data to list before build runs
await Provider.of<WalletModel>(context, listen: false).myProvider();
_isInit = false;
}
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
return Consumer<WalletModel>(builder: (context, providerInstance, _) {
return ListView(
children: providerInstance
.wallet
.map<Widget>((walletItem) => WalletListItem(wallet: walletItem))
.toList(),
);
});
}
// List<Widget> getWalletListItems() {
// return Provider.of<WalletModel>(context, listen: false)
// .wallet
// .map((walletItem) => WalletListItem(wallet: walletItem))
// .toList();
// }
}

Flutter - Show only one Dialog at the time

I am working on a flutter application using several dialogs for several purposes.
In our code, there are some cases where the user can open a Dialog. Inside this dialog, there are some buttons that will also open another dialog. It results with 2 dialogs on top of each other and with a very dark background screen.
What we would like to do is to only display one dialog at the time. How can we achieve that ?
Here is a simple code to illustrate our issue:
class MyScreen extends StatelessWidget {
#override
build(BuildContext context) {
return FlatButton(
child: Text('Button'),
onPressed: () async {
final resultDialog = await showDialog<ResultDialog1>(
context: context,
builder: (BuildContext context) => MyFirstDialog(),
);
// Do some stuff with the result, so this part of the tree cannot be destroyed
},
);
}
}
class MyFirstDialog extends StatelessWidget {
#override
build(BuildContext context) {
return FlatButton(
child: Text('Button in first dialog'),
onPressed: () async {
final resultDialog = await showDialog<ResultDialog2>(
context: context,
builder: (BuildContext context) => MySecondDialog(), // <- This will appear on top of the first dialog
);
// Do some stuff with the result, so this part of the tree cannot be destroyed
},
);
}
}
class MySecondDialog extends StatelessWidget {
#override
build(BuildContext context) {
return Text('Second Dialog');
}
}
let me give you a widget for that
class MultiDialog extends StatefulWidget {
final Widget child;
const MultiDialog({Key key, this.child}) : super(key: key);
#override
_MultiDialogState createState() => _MultiDialogState();
static void addDialog(
{#required BuildContext context, #required Widget dialog}) {
assert(context != null, "the context cannot be null");
assert(dialog != null, "the dialog cannot be null");
context.findAncestorStateOfType<_MultiDialogState>()._addDialog(dialog);
}
static void remove({#required BuildContext context}) {
assert(context != null, "the context cannot be null");
context.findAncestorStateOfType<_MultiDialogState>()._remove();
}
}
class _MultiDialogState extends State<MultiDialog> {
final _allDialogs = <Widget>[];
void _addDialog<T>(Widget dialog) {
assert(dialog != null, "The dialog cannot be null");
setState(() {
_allDialogs.add(dialog);
});
}
void _remove() {
if (_allDialogs.isEmpty) {
print("No dialogs to remove");
return;
}
setState(() {
_allDialogs.removeLast();
});
}
#override
Widget build(BuildContext context) {
return Stack(
children: [
widget.child,
if (_allDialogs.isNotEmpty) _allDialogs.last,
],
);
}
}
and when you want to add a dialog just call
MultiDialog.addDialog(
context: context,
dialog: AlertDialog(),
);
call Navigator.pop to remove the dialog, if there is another dialog which you pushed exist it will be shown, you can further pop them all with results, PS:this code isn't tested, let me know in the comments if this works for you
call MultiDialog.remove(context:context) to pop the visible dialog and bring back the previous dialog,
and if you receive a error that the addDialog is called on null, its because how flutter works, after MultiDialog use a Builder to introduce a new context use it call showDialog,
PS:ABOVE CODE IS TESTED
i made a stream out of the events that cause the dialog to pop up and used rx darts exhaust map to wait for the result (i was already using rxdart)
dialogEventStream
.exhaustMap((_) => maybeShowDialog().asStream())
.listen((_) {});
Future<bool> maybeShowOfflineModeDialog() async {
final isOfflineModeEnabled = await _sharedPreferencesService.isOfflineModeEnabled();
if (!isOfflineModeEnabled) {
final isLoginOffline = await _navigationService.showDialog(NoConnectionDialog());
if (isLoginOffline == true) {
await _sharedPreferencesService.setIsOfflineModeEnabled(isOfflineModeEnabled: true);
return await _navigationService.pushReplacement(AppShellOffline.routeName) ?? true;
} else {
return false;
}
}
return false;
}
something like this

How to reference variable in method in FutureBuilder (builder:)?

I want to use the variable dbRef in inputData() in future Builder builder: you can see the variable in between asterisk .
void inputData() async {
FirebaseUser user = await FirebaseAuth.instance.currentUser();
final uid = user.uid;
final **dbRef** = FirebaseDatabase.instance.reference().child("Add Job Details").child(uid).child("Favorites");
}
#override
Widget build(BuildContext context) {
return FutureBuilder (
future: **dbRef**.once(),
builder: (context, AsyncSnapshot<DataSnapshot> snapshot) {
if (snapshot.hasData) {
List<Map<dynamic, dynamic>> list = [];
for (String key in snapshot.data.value.keys) {
list.add(snapshot.data.value[key]);
}
This is one more approach to tackle the problem.
The idea is to use a variable _loading and set it to true initially.
Now, after in your inputData() function, you can set it to false once you get the dbref.
Store dbref, the way I stored _myFuture in the code below i.e., globally within the class.
Use your _loading variable to return a progress bar if its true else return FutureBuilder with your dbref.once() in place. Now, that you have loaded it, it should be available at this point.
class MyWidget extends StatefulWidget {
#override
createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
// Is the future being loaded?
bool _loading;
// This is the future we will be using in our FutureBuilder.
// It is currently null and we will assign it in _loadMyFuture function.
// Until assigned, we will keep the _loading variable as true.
Future<String> _myFuture;
// Load the _myFuture with the future we are going to use in FutureBuilder
Future<void> _loadMyFuture() async {
// Fake the wait for 2 seconds
await Future.delayed(const Duration(seconds: 2));
// Our fake future that will take 2 seconds to return "Hello"
_myFuture = Future(() async {
await Future.delayed(const Duration(seconds: 2));
return "Hello";
});
}
// We initialize stuff here. Remember, initState is called once in the beginning so hot-reload wont make flutter call it again
#override
initState() {
super.initState();
_loading = true; // Start loading
_loadMyFuture().then((x) => setState(() => _loading = false)); // Set loading = false when the future is loaded
}
#override
Widget build(BuildContext context) {
// If loading, show loading bar
return _loading?_loader():FutureBuilder<String>(
future: _myFuture,
builder: (context, snapshot) {
if(!snapshot.hasData) return _loader(); // still loading but now it's due to the delay in _myFuture
else return Text(snapshot.data);
},
);
}
// A simple loading widget
Widget _loader() {
return Container(
child: CircularProgressIndicator(),
width: 30,
height: 30
);
}
}
Here is the output of this approach
This does the job but, you might need to do it for every class where you require your uid.
========================================
Here is the approach I described in the comments.
// Create a User Manager like this
class UserManager {
static String _uid;
static String get uid => _uid;
static Future<void> loadUID() async {
// Your loading code
await Future.delayed(const Duration(seconds: 5));
_uid = '1234'; // Let's assign it directly for the sake of this example
}
}
In your welcome screen:
class MyWidget extends StatefulWidget {
#override
createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
bool _loading = true;
#override
void initState() {
super.initState();
UserManager.loadUID().then((x) => setState(() => _loading = false));
}
#override
Widget build(BuildContext context) {
return _loading ? _loader() : Text('Welcome User ${UserManager.uid}!');
}
// A simple loading widget
Widget _loader() {
return Container(child: CircularProgressIndicator(), width: 30, height: 30);
}
}
The advantage of this method is that once you have loaded the uid, You can directly access it like this:
String uid = UserManager.uid;
thus eliminating use of futures.
Hope this helps!