Navigator.push().then not calling a Function - flutter

This is my students list screen, which has an add student button and upon taping save on the form it pops back to the students list.
class _StudentListState extends State<StudentList> {
var students = new List<Student>();
_getStudents() {
APIServices.fetchStudents().then((response) {
setState(() {
Iterable list = json.decode(response);
students = list.map((model) => Student.fromJson(model)).toList();
});
});
}
#override
void initState() {
super.initState();
_getStudents();
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton:_buidFloatingButton(),
appBar: _buildAppBar(context),
bottomNavigationBar: BottomAppBar(
// ================== REFRESH BUTTON ============================
child: FlatButton(
child:Icon(Icons.refresh),
onPressed: () {
_getStudents();
},
),
// =======================================================
),
// ================== ADD STUDENT BUTTON =======================
Widget _buidFloatingButton() {
return FloatingActionButton(
child:Icon(Icons.person_add),
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => AddStudent())).then((value) {
_getStudents();
});
},
);
}
// =======================================================
}
}
I'm trying to refresh the student list after the form pop with this code, as seen in above code:
Navigator.push(context, MaterialPageRoute(builder: (context) => AddStudent())).then((value) => () {
_getStudents();
});
It's not refreshing the student list, but it will refresh if I tap the refresh button, both attempts are executing the same _getStudentes() function.
At the end of the save button I just do a:
Navigator.pop(context);
What am I missing?
Thanks.

You are returning a function with (value) => () { ... }.
The shorthand syntax in JavaScript and Dart differs a little in that regard, let me explain:
// Expression that returns a function.
() {
...
}
// You could also assign it to a variable:
final foo = () { return 3; };
// Now, you can call foo:
final bar = foo();
Thus, you are returning a function with (value) => () { ... }.
What you want to do instead is either of the following:
(value) => _getStudents()
// or
(value) {
_getStudents();
}
Learn more about functions in Dart.

Related

How to call a method when navigating back from another screen in flutter?

I have a list view of items. For each item in the list, there is an edit option, when on click that im navigating from ListScreen to the EditScreen. Edit Screen has a static update button but the editing view is changed according to the item selected to be edited From the ListScreen.
Now when I update the items in Edit screen, and press update button I can navigate to ListScreen back but I need the list view to rebuild on that. The function to build the list is given inside of a FutureBuilder in ListScreen it self. In the initial navigate to the ListScreen im calling that method in initState().
Is there a way that I can manage this?
ListScreen
//provider
late AddItemProvider? _updateItemProvider;
#override
void initState() {
_dataFuture = _getAddedData().whenComplete(() => refresh());
super.initState();
}
void _gotoEditAccess(BuildContext ctx, String title) async {
var nav =
Navigator.pushNamed(ctx, suggestionUpdateUIRoute, arguments: title);
// of(context)
if (nav != null && nav == true) {
await _dataFuture; //_getAddedData().whenComplete(() => refresh());
}
}
//in the body I have this list view
//added item is the list view item which holds Function parameters for onEdit and onDelete
//used as onTap: () => widget.onEdit!(),
ListView.builder(
shrinkWrap: true,
itemCount: _listOfItems.length,
itemBuilder: (context, index) {
return AddedItem(
icon: _listOfItems[index].icon,
title: _listOfItems[index].title,
content: _listOfItems[index].content,
onEdit: () async {
_gotoEditAccess(
context, _listOfItems[index].title!);
},
onDelete: () {
_deleteItem(_listOfItems[index].title!);
},
);
},
),
edit screen have a base view with common update button and the body is being replaced with a actual editing view matching what needs to be edited.
EditScreen
#override
Widget build(BuildContext context) {
_updateItemProvider = Provider.of<AddItemProvider>(context, listen: false);
...
//rest of the body for matched editing screen...
//inside the base view of edit screen
_goBack() {
Navigator.pop(context, true);}
#override
Widget build(BuildContext context) {
_updateItemProvider = Provider.of<AddItemProvider>(context, listen: false);
_submitBtn() => Consumer<AddItemProvider>(
builder: (_, AddItemProvider updateItemProvider, __) {
_updateItemProvider = updateItemProvider;
return ButtonWidget(
btnColor: CustomColors.green600,
borderColor: CustomColors.green600,
textColor: CustomColors.mWhite,
text: "Update",
eButtonType: eButtonType.bText,
eButtonState: _submitBtnState,
onPressed: () async {
await saveSelectedList(updateItemProvider.updateItems!);
_updateItemProvider!.removeAll();
_goBack();
},
);
},
);
You need to write this code were you want to navigate
onTap:() async{
var test = await Navigator.of(context).push(
MaterialPageRoute(builder: (context) =>EditProfile()));
if(test != null || test == true){
// perform your function
}
}
You need to pass any content when navigate back from edit screen
Navigator.pop(context, true);

How to prevent multiple network calls from a flutter bloc?

I am building an app using flutter and I'm using the flutter_bloc package for managing state. I have a simple Avatar widget used for displaying for displaying profile photo of a user.
/// A widget for displaying a user profile avatar.
class UserAvatar extends StatelessWidget {
/// The unique identifier for a particular user.
final int userId;
/// The size of the avatar.
final double radius;
UserAvatar({required this.userId, this.radius = 30}) {}
#override
Widget build(BuildContext context) {
context.read<UserInfoBloc>().add(UserInfoEvent.getUserInfo(userId));
return BlocBuilder<UserInfoBloc, UserInfoState>(
builder: (context, state) {
return state.when(initial: () {
return Text('Initial');
}, loading: () {
return Text('Loading');
}, loaded: (user) {
print(user);
return Container(
child: Avatar(
shape: AvatarShape.circle(radius),
loader: Center(
child: ClipOval(
child: Skeleton(
height: radius * 2,
width: radius * 2,
),
),
),
useCache: true,
sources: [NetworkSource(user.avatarUrl ?? '')],
name: user.firstName!.trim(),
onTap: () {
//TODO implement Navigation to profile page
},
),
);
}, error: () {
return Text('error');
});
},
);
}
}
My problem is that the widget will be used multiple times (when displaying a feed and there are contents by the same user). I initially just have the id of the user, then I try to make a network call and try to get the user. I've implemented some form of caching in my repository:
#LazySingleton(as: UserInfoRepository)
class UserInfoRepositoryImpl extends UserInfoRepository {
final GetUserInfoRemoteDataSource remoteDataSource;
final GetUserInfoLocalDataSource localDataSource;
UserInfoRepositoryImpl(
{required this.remoteDataSource, required this.localDataSource});
#override
Future<Either<Failure, User>> getUserInfo(int id) async {
try {
final existsInCache = localDataSource.containsUserModel(id);
if (existsInCache) {
return right(localDataSource.getUserModel(id));
} else {
final result = await remoteDataSource.getUserInfo(id);
localDataSource.cacheUserModel(result);
return right(result);
}
} on ServerExceptions catch (e) {
return left(e.when(() => Failure(),
requestCancelled: () => Failure.requestCancelled(),
unauthorisedRequest: () => Failure.unauthorisedRequest(),
badRequest: (e) => Failure.badRequest(e),
notFound: () => Failure.notFound(),
internalServerError: () => Failure.internalServerError(),
receiveTimeout: () => Failure.receiveTimeout(),
sendTimeout: () => Failure.sendTimeout(),
noInternetConnection: () => Failure.noInternetConnection()));
} on CacheException {
return left(Failure.cacheFailure());
}
}
}
Of course, I used the injectable package for dealing with my dependencies, and I've used the #LazySingleton annotation on the repository. But unfortunately, if I try to display two avatars of the same user, two separate network calls will be made. Of course, I don't want that. How can I solve this problem?

Flutter: Edit an existing item using provider

I am creating an app that works like so.
User creates a report EquipmentReport and on that report, they can add a list of AdditionalWN via a form called NotificationForm
The user should be able to add / delete and change any of the AdditionalWN items on the report
As the AdditionalWN is quite a complex model, I have implemented a ChangeNotifier on both the EquipmentReport and the AdditionalWN
EquipmentReport
class EquipmentReport extends ChangeNotifier {
List<AdditionalWN> _notifications;
List<ReportImage> _imgs;
List<bool> _inspectionResultSelectedStates;
String _locationIDVerification;
String _inspectionComments;
UnmodifiableListView<AdditionalWN> get notifications => UnmodifiableListView(_notifications);
void addNotification(AdditionalWN notification) {
_notifications.add(notification);
notifyListeners();
}
void removeRemoveNotification(AdditionalWN notification) {
_notifications.remove(notification);
notifyListeners();
}
.........
AdditionalWN
class AdditionalWN extends ChangeNotifier {
String _id = UniqueKey().toString();
String _notificationText = '';
String _notificationTitle = '';
int _isFault = 1;
int _isBreakdown = 0;
int _isWorkComplete = 0;
int _probabilityOfFailure = 0;
int _consequencePeople = 0;
int _consequenceEnvironment = 0;
int _consequenceReputation = 0;
int _consequenceAsset = 0;
int _equipmentID = 0;
String get id => _id;
int get equipmentID => _equipmentID;
set equipmentID(int value) {
_equipmentID = value;
notifyListeners();
}
String get notificationText => _notificationText;
set notificationText(String value) {
_notificationText = value;
notifyListeners();
}
String get notificationTitle => _notificationTitle;
set notificationTitle(String value) {
_notificationTitle = value;
notifyListeners();
}
..........
Adding a new AdditionalWN to the report is working fine using this button on the EquipmentReport where I pass a new instance of the ChangeNotifierProvider<AdditionalWN> to my NotificationForm()
ElevatedButton(
child: Text('Add'),
onPressed: () {
Navigator.of(context).push(
//here we create a new instance of the AdditionalWM and change provider to send to the new page
MaterialPageRoute(
builder: (BuildContext context) => ChangeNotifierProvider<AdditionalWN>(
create: (context) => AdditionalWN(),
builder: (context, child) => NotificationForm(),
),
),
);
},
)
Where I am struggling is how do I let me user edit an existing AdditionalWN?
Currently when a user clicks the edit button on the list of AdditionalWNs they get an error.
A AdditionalWN was used after being disposed.
Once you have called dispose() on a AdditionalWN, it can no longer be used.
The relevant error-causing widget was
NotificationForm
Here is my code to allow them to navigate to the NotificationForm
ListTile(
leading: IconButton(
onPressed: () {
Navigator.of(context).push(
//here we need to pass an existing model and change provider using ChangeNotifierProvider.value so we can edit
MaterialPageRoute(
builder: (BuildContext context) => ChangeNotifierProvider.value(
value: model.notifications[index],
child: NotificationForm(),
),
),
);
},
icon: Icon(Icons.edit),
),
I'm really lost on how to be able to pass both a new AdditionalWN to the NotificationForm as well as an exsiting AdditionalWN to the same NotificationForm

Flutter setState not updating child element

I have an InkWell which uses onTap to perform some actions. When the button is tapped, I like an indicator to be shown (in case the action is long-running). However, the setState in the InkWell does not trigger its children to be re-rendered. The code is as follows:
class PrimaryButtonState extends State<PrimaryButton> {
bool _apiCall;
Widget getWidget() {
if(_apiCall) {
return new CircularProgressIndicator();
} else {
return Text(
widget.label,
);
}
}
#override
Widget build(BuildContext context) {
final List<Color> colors = //omitted
return InkWell(
child: Container(
decoration: // omitted
child: getWidget(), // not updated when _apiCall changes !!!!!
),
onTap: () {
setState(() {
_apiCall = true;
});
widget.onTab(context);
setState(() {
_apiCall = false;
});
}
);
}
}
How can I solve this that getWidget returns the correct widget dependent on _apiCall?
EDIT:
The widget.onTap contains the following:
void performLogin(BuildContext context) {
final String userName = _userName.text.trim();
final String password = _password.text.trim();
UserService.get().loginUser(userName, password).then((val) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => MainLayout()));
}).catchError((e) {
// omitted
});
}
it is passed with the widget:
class PrimaryButton extends StatefulWidget {
final bool isPrimary;
final String label;
final Function(BuildContext context) onTab;
PrimaryButton(this.label, this.isPrimary, this.onTab);
#override
State<StatefulWidget> createState() => PrimaryButtonState();
}
My main concern is, that the given onTap method should not know it is "bound" to a UI widget and therefore should not setState. Also, as this is a general button implementation I like it to be interchangeable (therefore, onTap is not hardcoded)
It looks like your problem is because you are calling setState() twice in your onTap() function. Since onTap() is not an async function it will set _apiCall = true in the first setState, then immediately run widget.onTab(context) and then immediately perform the second setState() to set _apiCall = false so you never see the loading widget.
To fix this you will need to make your onTab function an async function and await for a value in your onTap function for your InkWell:
onTap: () async {
setState(() {
_apiCall = true;
});
await widget.onTab(context);
setState(() {
_apiCall = false;
});
}
This will also let you use the results of your onTab function to show errors or other functionality if needed.
If you are unsure how to use async functions and futures here is a good guide on it that goes over this exact kind of use case.

Need to execute task after setState finish

I need to execute a task, after setState() method complete its whole job. I will describe my problem at below with some code.
I have a login screen and it has a widgets as below:
...
child: TextField(
errorText: getErrorStringOfEmail(),
...
),
...
child: MaterialButton(
onPressed: () => onClickLoginButton(),
...
),
"getErrorStringOfEmail" method is as below: (This method is called when the Textfield is updated by calling "setState()".)
String getErrorStringOfEmail(
if(loginEmailTextEditingController.text.toString() == "a") {
isValidLogin = false;
return 'Wrong password or email';
}
isValidLogin = true;
return null;
}
"onClickLoginButton" is as below:
void onClickLoginButton() {
setState(() {
//in here a boolean is changed to update Textfield.
});
if (isValidLogin) {
Navigator.pushReplacement (
context,
MaterialPageRoute(builder: (context) => HomeWidget()),
);
}
}
As you can see, "isValidLogin" boolean is assigned when Textfield widget is updated. However, getErrorStringOfEmail method is called after onClickLoginButton. I need to execute the following part,
if (isValidLogin) {
Navigator.pushReplacement (
context,
MaterialPageRoute(builder: (context) => HomeWidget()),
);
}
after getErrorStringOfEmail method is called. To achieve this, i need to call this part after setState update widgets.
How can i do this?