I have a form to create/update activity which is a stateful widget like this:
class ActivityForm extends StatefulWidget {
final Activity activity;
final bool isEdit;
const ActivityForm({Key key, this.activity, this.isEdit}) : super(key: key);
#override
_ActivityFormState createState() => _ActivityFormState();
}
class _ActivityFormState extends State<ActivityForm> {
final _formKey = GlobalKey<FormState>();
List<int> activityDetailIdToBeDeleted;
...
void submitHandler() async {
setState(() {
_isLoading = true;
});
// Mapping input field to activity model
Activity activity = Activity(
id: widget.activity.id,
tanggal: _tanggalController.text,
uraianKegiatan: _uraianKegiatanController.text,
pic: _picController.text,
jumlahTim: int.parse(_jumlahTimController.text),
kendala: _kendalaController.text,
penyelesaian: _penyelesaianController.text,
approverUserId: selectedApprovalUser,
rincianPekerjaan: rincianPekerjaanList,
status: 'PENDING');
if (widget.isEdit) {
await Provider.of<ActivityProvider>(context, listen: false)
.updateActivity(activity, activityDetailIdToBeDeleted);
} else {
await Provider.of<ActivityProvider>(context, listen: false)
.createActivity(activity);
}
Navigator.pop(context);
}
in my form there are some detail list of activities. When i press delete it adds the id of
the activity detail to the list of "to be deleted" :
IconButton(
icon: Icon(Icons.delete),
onPressed: () => removeRincianPekerjaan(
activityDetail.description),),
here's the function:
void removeRincianPekerjaan(desc) {
if (widget.isEdit) {
int detailId = rincianPekerjaanList
.where((element) => element.description == desc)
.first
.id;
if (!activityDetailIdToBeDeleted.contains(detailId))
activityDetailIdToBeDeleted.add(detailId);
print(activityDetailIdToBeDeleted);
}
setState(() {
rincianPekerjaanList
.removeWhere((element) => element.description == desc);
});
}
that list of ids will be sent to provider which looks like this:
Future<bool> updateActivity(
Activity activity, List<int> activityDetailToBeDeleted) async {
final response = await ActivityService()
.updateActivity(activity.id.toString(), activity.toMap());
if (response != null) {
// Update Success.
if (response.statusCode == 200) {
var body = json.decode(response.body);
removeActivityToList(activity);
activity = Activity.fromMap(body['activity']);
addActivityToList(activity);
if (activityDetailToBeDeleted.isNotEmpty) {
print("to be deleted is not empty.");
print("ID to be deleted: ${activityDetailToBeDeleted.join(",")}");
final res = await ActivityService()
.deleteActivityDetail(activityDetailToBeDeleted.join(","));
if (res.statusCode == 204) {
print('ID DELETED.');
return true;
}
}
return true;
}
// Error unauthorized / token expired
else if (response.statusCode == 401) {
var reauth = await AuthProvider().refreshToken();
if (reauth) {
return await createActivity(activity);
} else {
return Future.error('unauthorized');
}
}
return Future.error('server error');
}
return Future.error('connection failed');
}
the problem is if the list is empty, or i didn't add the id to the list, builder context is not null but if i add an id to the list "to be deleted", the Navigator.pop(context) will throw an error because context is null. I don't understand why it becomes null.
full code in here
Your [context] must came from the build() method. Like this.
void submitHandler(BuildContext context) async {
setState(() {
_isLoading = true;
});
// Mapping input field to activity model
Activity activity = Activity(
id: widget.activity.id,
tanggal: _tanggalController.text,
uraianKegiatan: _uraianKegiatanController.text,
pic: _picController.text,
jumlahTim: int.parse(_jumlahTimController.text),
kendala: _kendalaController.text,
penyelesaian: _penyelesaianController.text,
approverUserId: selectedApprovalUser,
rincianPekerjaan: rincianPekerjaanList,
status: 'PENDING');
if (widget.isEdit) {
await Provider.of<ActivityProvider>(context, listen: false)
.updateActivity(activity, activityDetailIdToBeDeleted);
} else {
await Provider.of<ActivityProvider>(context, listen: false)
.createActivity(activity);
}
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator.pop(context);
});
}
Related
I have a simple controller like this
class UserController with ChangeNotifier {
UserData user = UserData();
UserData get userdata => user;
void setUser(UserData user) {
user = user;
print(user.sId);
notifyListeners();
}
login(data) async {
var response = await ApiService().login(data);
final databody = json.decode(response);
if (databody['success']) {
UserData authUser = UserData.fromJson(databody['data']);
setUser(authUser);
notifyListeners();
return true;
} else {
return false;
}
}
}
I am trying to just print it like this on both widget and in initstate function but values are showing null. I can see in set function value is not null.
print('id ${context.watch<UserController>().user.sId.toString()}');
print(
'id2 ${Provider.of<UserController>(context, listen: false).user.sId.toString()}');
I already have added
ChangeNotifierProvider(create: (_) => UserController()),
],
in main.dart in MultiProvider
Also on Tap of login button I am doing this
showLoader(context);
UserController auth = Provider.of<UserController>(
context,
listen: false);
var data = {
"userEmail":
emailController.text.trim().toLowerCase(),
"userPassword": passwordController.text.trim(),
};
auth.login(data).then((v) {
if (v) {
hideLoader(context);
context.go('/homeroot');
} else {
hideLoader(context);
Fluttertoast.showToast(
backgroundColor: green,
textColor: Colors.white,
msg:
'Please enter correct email and password');
}
});
Try to include this while naming is same,
void setUser(UserData user) {
this.user = user;
print(user.sId);
notifyListeners();
}
Follow this structure
class UserController with ChangeNotifier {
UserData user = UserData();
UserData get userdata => user;
void setUser(UserData user) {
this.user = user;
print(user.sId);
notifyListeners();
}
Future<bool> login(String data) async {
await Future.delayed(Duration(seconds: 1));
UserData authUser = UserData(sId: data);
setUser(authUser);
notifyListeners();
return true;
}
}
class HPTest extends StatelessWidget {
const HPTest({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: Consumer<UserController>(
builder: (context, value, child) {
return Text(value.user.sId);
},
),
floatingActionButton: FloatingActionButton(onPressed: () async {
final result = await Provider.of<UserController>(context, listen: false)
.login("new ID");
print("login $result");
;
}),
);
}
}
I have a StateNotifierProvider that calls an async function which loads some images from the internal storage and adds them to the AsyncValue data:
//Provider declaration
final paginationImagesProvider = StateNotifierProvider.autoDispose<PaginationNotifier, AsyncValue<List<Uint8List?>>>((ref) {
return PaginationNotifier(folderId: ref.watch(localStorageSelectedFolderProvider), itemsPerBatch: 100, ref: ref);
});
//Actual class with AsyncValue as State
class PaginationNotifier extends StateNotifier<AsyncValue<List<Uint8List?>>> {
final int itemsPerBatch;
final String folderId;
final Ref ref;
int _numberOfItemsInFolder = 0;
bool _alreadyFetching = false;
bool _hasMoreItems = true;
PaginationNotifier({required this.itemsPerBatch, required this.folderId, required this.ref}) : super(const AsyncValue.loading()) {
log("PaginationNotifier created with folderId: $folderId, itemsPerBatch: $itemsPerBatch");
init();
}
final List<Uint8List?> _items = [];
void init() {
if (_items.isEmpty) {
log("fetchingFirstBatch");
_fetchFirstBatch();
}
}
Future<List<Uint8List?>> _fetchNextItems() async {
List<AssetEntity> images = (await (await PhotoManager.getAssetPathList())
.firstWhere((element) => element.id == folderId)
.getAssetListRange(start: _items.length, end: _items.length + itemsPerBatch));
List<Uint8List?> newItems = [];
for (AssetEntity image in images) {
newItems.add(await image.thumbnailData);
}
return newItems;
}
void _updateData(List<Uint8List?> result) {
if (result.isEmpty) {
state = AsyncValue.data(_items);
} else {
state = AsyncValue.data(_items..addAll(result));
}
_hasMoreItems = _numberOfItemsInFolder > _items.length;
}
Future<void> _fetchFirstBatch() async {
try {
_numberOfItemsInFolder = await (await PhotoManager.getAssetPathList()).firstWhere((element) => element.id == folderId).assetCountAsync;
state = const AsyncValue.loading();
final List<Uint8List?> result = await _fetchNextItems();
_updateData(result);
} catch (e, stk) {
state = AsyncValue.error(e, stk);
}
}
Future<void> fetchNextBatch() async {
if (_alreadyFetching || !_hasMoreItems) return;
_alreadyFetching = true;
log("data updated");
state = AsyncValue.data(_items);
try {
final result = await _fetchNextItems();
_updateData(result);
} catch (e, stk) {
state = AsyncValue.error(e, stk);
log("error catched");
}
_alreadyFetching = false;
}
}
Then I use a scroll controller attached to a CustomScrollView in order to call fetchNextBatch() when the scroll position changes:
#override
void initState() {
if (!controller.hasListeners && !controller.hasClients) {
log("listener added");
controller.addListener(() {
double maxScroll = controller.position.maxScrollExtent;
double position = controller.position.pixels;
if ((position > maxScroll * 0.2 || position == 0) && ref.read(paginationImagesProvider.notifier).mounted) {
ref.read(paginationImagesProvider.notifier).fetchNextBatch();
}
});
}
super.initState();
}
The problem is that when the StateNotifierProvider is fetching more data in the async function fetchNextBatch() and I go back on the navigator (like navigator.pop()), Flutter gives me an error:
[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Bad state: Tried to use PaginationNotifier after dispose was called.
Consider checking mounted.
I think that the async function responsible of loading data completes after I've popped the page from the Stack (which triggers a Provider dispose).
I'm probably missing something and I still haven't found a fix for this error, any help is appreciated.
I have 2 data provider classes that extend ChangeNotifier. Within each, there's a function to fetch data and at the end of them, I use notifyListeners() to notify the screens/listeners that the data changed. However, it seems that the listeners start getting notified endlessly instead of once and that creates a loop of reloading, circle indicators that don't go away, and a frozen screen. I don't get it.
Data providers:
class UsersDataProvider extends ChangeNotifier {
UsersDataProvider() : super();
static Map<int, QueryDocumentSnapshot<Object?>> usersMap = {};
Future<void> fetchUsers() async {
final userRef = FirebaseFirestore.instance.collection('users');
final QuerySnapshot result = await userRef.get();
final docs = result.docs.asMap();
usersMap = docs;
print(usersMap.length);
notifyListeners();
}
}
class PostsDataProvider extends ChangeNotifier {
PostsDataProvider() : super();
static Map<int, QueryDocumentSnapshot<Object?>> postsMap = {};
Future<void> fetchPosts() async {
UsersDataProvider.usersMap.forEach((index, resultValue) async {
final postsRef = FirebaseFirestore.instance
.collection('users')
.doc(resultValue.id)
.collection('posts');
final QuerySnapshot postsResult = await postsRef.get();
final postDocs = postsResult.docs.asMap();
postsMap = postDocs;
print('Post map: ${postsMap.length}');
notifyListeners();
});
}
}
Add listeners and reload data:
Future<void> fetchUsersAndPosts(bool initial) async {
if (!initial) {
setState(() {
postsLoading = true;
});
usersDataProvider.fetchUsers();
postsDataProvider.fetchPosts();
}
if (initial) {
usersDataProvider.addListener(() {
print('changed');
setState(() {
fetchUsersAndPosts(false);
});
});
}
if (initial) {
postsDataProvider.addListener(() {
setState(() {
fetchUsersAndPosts(false);
});
});
}
UsersDataProvider.usersMap.forEach((index, value) async {
List<Post> posts = [];
PostsDataProvider.postsMap.forEach((index, value) {
final post = Post.fromJson(value.data() as Map<String, dynamic>);
posts.add(post);
setState(() {});
if (posts.length == PostsDataProvider.postsMap.length) {
setState(() {
postsList = posts;
postsList.sort((a, b) {
return b.date.compareTo(a.date);
});
postsLoading = false;
});
}
});
final profileInfo =
ProfileInfoObject.fromJson(value.data() as Map<String, dynamic>);
Profile profile = Profile(profileInfo, postsList.where((p) => p.uid == value.id).toList());
UserSearchResult user = (UserSearchResult(profile, value.id));
if (usersList.where((u) => u.uid == user.uid).toList().isEmpty) {
setState(() {
usersList.add(user);
});
}
});
setState(() {
postsList.sort((a, b) {
return b.date.compareTo(a.date);
});
});
}
I have been trying to make a app in flutter where an api is called and data is updated in TextField
Used provider for state management, here is the code for it.
class ProfileProvider with ChangeNotifier {
var profileData;
String _url = "http://10.0.2.2:3000/api/v1/user/loggedin_user";
void getData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var token = prefs.getString('token');
var data = await http.get(
_url,
headers: {
"accept": "application/json",
"content-type": "application/json",
'Token': token,
},
);
var infoOfPerson = json.decode(data.body);
profileData = new ProfileObject(
name: infoOfPerson['name'],
mobile: infoOfPerson['mobile'],
email: infoOfPerson['email'],
role: infoOfPerson['role'],
);
notifyListeners();
}
ProfileObject get profileInfo {
return profileData;
}
}
I am getting the data fine, now i have to show it in the UI, but sometime data is populated, sometime its not. Can someone please point me the right direction why this is happening.
Here is the code for UI.
class Profile extends StatefulWidget {
#override
_ProfileState createState() => _ProfileState();
}
class _ProfileState extends State<Profile> {
final emailController = TextEditingController(text: '');
final nameController = TextEditingController(text: '');
final mobileController = TextEditingController(text: '');
var _isInit = true;
#override
void didChangeDependencies() {
if (_isInit) {
final profileData = Provider.of<ProfileProvider>(context);
profileData.getData();
if (profileData.profileInfo != null) {
emailController.text = profileData.profileInfo.name;
nameController.text = profileData.profileInfo.email;
mobileController.text = profileData.profileInfo.mobile;
}
_isInit = false;
super.didChangeDependencies();
}
}
#override
Widget build(BuildContext context) {
final profileData = Provider.of<ProfileProvider>(context);
return Scaffold(
drawer: NavigationDrawer(),
body: profileData.profileInfo == null
? Center(
child: CircularProgressIndicator(),
)
: Builder(
builder: (context) => SingleChildScrollView(
child: Padding(.....
Below the padding, there is normal TextField, can someone tell me why the data is being populated sometime and sometime its coming empty, even I wrapped it with CircularProgressIndicator() and a check the notifyListeners(); is not working there. The loader is not being shown and data is not being loaded.
Thanks
for StatelessWidget.
Inside the build method use:
Future.microtask(() async {
context.read<SomeProvider>().fetchSomething();
});
For StatefulWidgets if you want to call it once. Do this inside the initState() or didChangeDependencies (better if the latter). This will be called at the end of the frame which means after the build or rendering finishes..
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<SomeProvider>().fetchSomething();
});
}
EDIT: WidgetsBinding will also work on build. I forgot on why I used microtask lol
i've created a function which called nextTick, i call it in initState and it works for now, but want to see others method
void nextTick(Function callback, [int milliseconds = 0]) {
Future.delayed(Duration(milliseconds: 0)).then((_) {
callback();
});
}
then use it like below
#override
void initState() {
super.initState();
nextTick((){
ProfileProvider profileProvider = Provider.of<ProfileProvider>(context);
profileProvider.getProfile();
});
}
Edit: i store couple of variables to manage them on ui, like isLoading, hasError and errorMessage. Here is my provider class
class ProfileProvider extends ChangeNotifier {
bool _hasError = false;
bool _isLoading = true;
String _errorMsg = '';
Profile _profileResponse;
bool get hasError => _hasError;
bool get isLoading => _isLoading;
String get errorMsg => _errorMsg;
Profile get profileResponse => _profileResponse;
Future<void> getProfile() async {
this.setLoading = true;
this.setError = false;
this.setErrorMsg = '';
try {
await dio.post('$api/p/view', data: {}).then((res) {
print(res.data);
_profileResponse = Profile.fromJson(jsonDecode(res.data));
print(_profileResponse.data);
notifyListeners();
}).whenComplete(() {
this.setLoading = false;
});
} catch (e) {
this.setError = true;
this.setErrorMsg = '$e';
}
this.setLoading = false;
}
set setError(bool val) {
if (val != _hasError) {
_hasError = val;
notifyListeners();
}
}
set setErrorMsg(String val) {
if (val != null && val != '') {
_errorMsg = val;
notifyListeners();
}
}
set setLoading(bool val) {
_isLoading = val;
notifyListeners();
}
}
I have a basic login form, with my LoginModel.
But I do not understand how I can call to the function notifyListeners to display a dialog in my view.
The login widget:
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new ScopedModel<LoginModel>(
model: _loginModel,
child: Center(child: ScopedModelDescendant<LoginModel>(
builder: (context, child, model) {
if (model.status == Status.LOADING) {
return Loading();
}
else return showForm(context);
}))));
}
And the login model:
class LoginModel extends Model {
Status _status = Status.READY;
Status get status => _status;
void onLogin(String username, String password) async {
_status = Status.LOADING;
notifyListeners();
try {
await api.login();
_status = Status.SUCCESS;
notifyListeners();
} catch (response) {
_status = Status.ERROR;
notifyListeners();
}
}
I need to display a dialog when the status is Error
Finally I got this, just returning a Future in the method onLogin
Future<bool> onLogin(String username, String password) async {
_status = Status.LOADING;
notifyListeners();
try {
await api.login();
_status = Status.SUCCESS;
notifyListeners();
return true;
} catch (response) {
_status = Status.ERROR;
notifyListeners();
return false;
}
}
And in the widget:
onPressed: () async {
bool success = await _loginModel.onLogin(_usernameController.text, _passwordController.text);
if(success) {
Navigator.pop(context, true);
}
else{
_showDialogError();
}
}