Flutter: Edit an existing item using provider - flutter

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

Related

How to pass values to the another screen by the onTap function in Dart?

I have two pages. one is Route, the second is Stops. Also, my code contains an algorithm that is sorted stops by the routes. When I did the test example and pass the stops on the same page as routes, so everything works fine, but for better UI I want to put arguments in the constructor and in onTap method. How can I pass arguments from this algorithm and terms from another screen into another screen?
the first screen:
body: FutureBuilder(
future: getMarshrutWithStops(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
List<RouteWithStops> routes = snapshot.data;
print(routes?.toString());
return (routes == null)
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StopsPage(
stId: routes[index].stop[index].stId
)));
},
//the algorithm which is sorted everything by id's
Future<List<RouteWithStops>> getMarshrutWithStops() async {
List<Routes> routes = [];
List<ScheduleVariants> variants = [];
List<StopList> stops = [];
final TransportService transService = TransportService();
routes.addAll((await transService.fetchroutes()).toList());
stops.addAll(await transService.fetchStops());
variants.addAll(await transService.fetchSchedule());
List<RouteWithStops> routesWithStops = [];
for (Routes route in routes) {
final routeWithStops = RouteWithStops();
routesWithStops.add(routeWithStops);
routeWithStops.route = route;
routeWithStops.variant =
variants.where((variant) => variant.mrId == route.mrId).first;
List<RaceCard> cards = [];
cards.addAll(
await transService.fetchRaceCard(routeWithStops.variant.mvId));
print(cards);
List<StopList> currentRouteStops = [];
cards.forEach((card) {
stops.forEach((stop) {
if (card.stId == stop.stId) {
currentRouteStops.add(stop);
}
});
});
routeWithStops.stop = currentRouteStops;
}
return routesWithStops;
}
The second page where I want all sorted stops be stored:
class StopsPage extends StatelessWidget {
final int stId;
const StopsPage({Key key, this.stId}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: FutureBuilder(
future: getMarshrutWithStops(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
List<RouteWithStops> routes = snapshot.data;
print(routes?.toString());
return (routes == null)
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: routes.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(routes[index].stop.toString()),
);
});
},
),
);
}
Future<List<RouteWithStops>> getMarshrutWithStops() async {
List<Routes> routes = [];
List<ScheduleVariants> variants = [];
List<StopList> stops = [];
final TransportService transService = TransportService();
routes.addAll((await transService.fetchroutes()).take(10).toList());
stops.addAll(await transService.fetchStops());
variants.addAll(await transService.fetchSchedule());
List<RouteWithStops> routesWithStops = [];
for (Routes route in routes) {
final routeWithStops = RouteWithStops();
routesWithStops.add(routeWithStops);
routeWithStops.route = route;
routeWithStops.variant =
variants.where((variant) => variant.mrId == route.mrId).first;
List<RaceCard> cards = [];
cards.addAll(
await transService.fetchRaceCard(routeWithStops.variant.mvId));
print(cards);
List<StopList> currentRouteStops = [];
cards.forEach((card) {
stops.forEach((stop) {
if (card.stId == stop.stId) {
currentRouteStops.add(stop);
}
});
});
routeWithStops.stop = currentRouteStops;
}
return routesWithStops;
}
}
I just thought that I didn’t need to copy and paste the entire algorithm on all pages, maybe I only need a part of the algorithm that starts with a for-loop and transfer it to the second page, where all the filtered stops should be. I can't figure out what to put in the onTap function and what to pass to the constructor on the Stops page.
you can do something like this
In onTap of First Page pass the value as name parameter
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) => StopPage(stId: routes[index].stop[index].stId))
);
}
Excess the same on StopPage, by using the constructor
class StopPage extends StatefulWidget {
final dynamic stId;
StopPage({this.stId});
}

Getx How to refresh list by using Obx

I'm working with ReorderableSliverList but I have no idea how to observe the list based on my data dynamically.
Screen 1
ReorderableSliverList(
delegate: ReorderableSliverChildBuilderDelegate(
(BuildContext context, int index) {
final data = controller.products[index];
return ItemView(data);
},
childCount: controller.products.length),
onReorder: _onReorder,
)
At screen2 will have a add button to call controller insert new data into list
controller
var products = List<Product>.empty().obs;
void add(String name) {
if (name != '') {
final date = DateTime.now().toIso8601String();
ProductProvider().postProduct(name, date).then((response) {
final data = Product(
id: response["name"],
name: name,
createdAt: date,
);
products.add(data);
Get.back();
});
} else {
dialogError("Semua input harus terisi");
}
}
The code above need to click Hot reload in order to show data in screen 1 if data has changed from screen 2.
I'm trying to use Obx to make it refresh automatically but the result it still the same.
Code
ReorderableSliverList(
delegate: ReorderableSliverChildBuilderDelegate(
(BuildContext context, int index) {
final data = controller.products[index];
return Obx(
() => controller.products.isEmpty
? Center(
child: Text("BELUM ADA DATA"),
)
: ItemView(data)
);
}, childCount: controller.products.length),
onReorder: _onReorder,
)
You need to wrap the whole ReorderableSliverList with Obx like this:
Obx(()=>ReorderableSliverList(
...
...
));

Flutter provider to pass a singe object out of list to the child view

I have a page where it shows ListView that contains list of schools. So I load all the schools (in initState() method) when the user clicks on My Schools.
Now I want to pass the School object when user clicks on a school's in the ListView and show the edit school page.
I want to use the provider patter for this so I can pass a single school object to the edit page and once user done with edit and press back, it automatically reflects the updated school in the ListView.
I am really confused with how to pass a single object from a list to the edit page. Following is my code where I have model, provider and state classes. As I am not getting how to access the school object in the edit page I have not created it yet.
I do not want to use the constructor to pass the school object to edit.
// School model class
class SchoolModel {
String name;
double points;
}
// school provider
class SchoolProvider with ChangeNotifier {
School _school;
School get school => _school;
set School(School value) {
_school = value;
notifyListeners();
}
}
// School list page
class ListSchool extends StatefulWidget {
#override
_ListSchoolState createState() => _ListSchoolState();
}
// List page state
class _ListSchoolState extends State<ListSchool> {
List<School> _schools;
#override
void initState() {
_schools = FirebaseCall(); // loading schools
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => SchoolProvider(),
),
],
child: ListView.builder(
itemCount: _schools.length,
itemBuilder: (BuildContext ctxt, int index) {
return ListTile(
title: Text(_schools[index].name);
onTap() {
// Open Edit page and pass the clicked school object to edit page
}
);
}
)
)
);
Thank you
I think you need to change your change notifier, to be the list that you want to update.
class SchoolsProvider with ChangeNotifier {
List<School> _schools;
School get schools => _schools;
SchoolProvider(this._schools = const []);
void addSchool(School newSchool) {
_schools.add(newSchool);
notifyListeners();
}
void removeShool(String schoolName) {
_schools.removeWhere((s) => s.name == schoolName);
notifyListeners();
}
void updateSchool(String shoolName, double points) {
_schools.removeWhere((s) => s.name == schoolName);
addSchool(School(schoolName, points));
}
}
When you tap on the ListTile you just push the new page, and pass in the desired school.
Consumer<SchoolsProvider>(
builder: (_, schools, __) => ListView.builder(
itemCount: schools.length,
itemBuilder: (BuildContext ctxt, int index) {
return ListTile(
title: Text(schools[index].name),
onTap() {
Navigator.push(context, MaterialRoutePage(
context,
builder: (_) => YourNewPage(school: schools[index]),
));
},
);
},
),
),
From the new page, you can just access the methods declared in the Provider, this will update the listeners
class NewPage extends StatelessWidget {
final School school;
NewPage({this.school});
...
//Here you can perform any action on the schools Provider
// It will notify it's listeners (Consumer) so it gets updated
schools[index].add
schools[index].remove
schools[index].update
...

How to append new data in ListView Flutter

I Wanna ask for my problem in flutter. How to append new data from another screen (has pop) to main screen (contain list view data from json) this is my code from main screen.
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => FormUser()))
.then((VALUE) {
/////////
Here for adding new data from VALUE into list view
/////
});
});
This my listview
Widget _buildListView(List<User> users) {
return ListView.separated(
separatorBuilder: (BuildContext context, int i) =>
Divider(color: Colors.grey[400]),
itemCount: users.length,
itemBuilder: (context, index) {
User user = users[index];
return ListTile(
leading: Icon(Icons.perm_identity),
title: Text(capitalize(user.fullName)),
subtitle: Text(user.gender),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Text(user.grade.toUpperCase()),
Text(user.phone)
],
),
);
},
);
}
}
This is return data from add screen
User{id: null, fullname: Camomi, gender: pria, grade: sma, phone: 082232, email: ade#gmul.com}
This is my class model of typedata
class User {
int id;
String fullName, gender, grade, phone, email;
User(
{this.id,
this.fullName,
this.gender,
this.grade,
this.phone,
this.email});
}
You add an element to your list of users. Then you call setState so your widget's build function gets called and builds the new view with the list of users that now contains the new element.
And please do not use .then() in your onPressed method. Make the method async and use await or if you really want to use .then(), at least return the Future it gives you.
You will need to do extra steps:
Wait for the result when calling push() method:
final result = await Navigator.push(...);
In your FormUser widget, when you finish entering data and press on Done or similar, you should return the created user:
// userData will be assigned to result on step 1 above. Then you add that result (user) to your list
Navigator.pop(context, userData);
You can find a very good tutorial here.
First of all make sure that Your class extends StatefulWidget.
I assume your list of users looks like this (or it's empty like [] ) :
List<User> users = [user1, user2, user3];
In Your FloatingActionButton in onPressed you should have this method (copy the content to onPressed or write a method like this somewhere in your code).
The method must be async to await the user
void _addUser() async {
final user = await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FormUser()));
if (user != null) {
setState(() => users.add(user));
}
}
This code should work fine, enjoy :)

Flutter How to refresh StreamBuilder?

Consider the following code:
StreamBuilder<QuerySnapshot> _createDataStream(){
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection("data").limit.(_myLimit).snapshots(),
builder: (context, snapshot){
return Text(_myLimit.toString);
}
);
}
I want that the StreamBuilder refreshes when the _myLimit Variable changes.
It's possible doing it like this:
void _incrementLimit(){
setState(() =>_myLimit++);
}
My Question is if there is another, faster way, except the setState((){}); one.
Because I don't want to recall the whole build() method when the _myLimit Variable changes.
I figured out another Way but I feel like there is a even better solution because I think I don't make use of the .periodic functionality and I got a nested Stream I'm not sure how usual this is:
Stream<int> myStream = Stream.periodic(Duration(), (_) => _myLimit);
...
#override
Widget build(BuildContext context){
...
return StreamBuilder<int>(
stream: myStream,
builder: (context, snapshot){
return _createDataStream;
},
),
...
}
Solution(s)
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
int myNum = 0;
final StreamController _myStreamCtrl = StreamController.broadcast();
Stream get onVariableChanged => _myStreamCtrl.stream;
void updateMyUI() => _myStreamCtrl.sink.add(myNum);
#override
void initState() {
super.initState();
}
#override
void dispose() {
_myStreamCtrl.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child:
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
StreamBuilder(
stream: onVariableChanged,
builder: (context, snapshot){
if(snapshot.connectionState == ConnectionState.waiting){
updateMyUI();
return Text(". . .");
}
return Text(snapshot.data.toString());
},
),
RaisedButton(
child: Text("Increment"),
onPressed: (){
myNum++;
updateMyUI();
},
)
],
),
)));
}
}
Some other ideas, how the StreamBuilder also could look like:
StreamBuilder(
stream: onVariableChanged,
builder: (context, snapshot){
if(snapshot.connectionState == ConnectionState.waiting){
return Text(myNum.toString());
}
return Text(snapshot.data.toString());
},
),
StreamBuilder(
stream: onVariableChanged,
initialData: myNum,
builder: (BuildContext context, AsyncSnapshot snapshot){
if(snapshot.data == null){
return Text("...");
}
return Text(snapshot.data.toString());
},
),
Declare a StreamController with broadcast, then set a friendly name to the Stream of this StreamController, then everytime you want to rebuild the wraped widget (the child of the StreamBuilder just use the sink property of the StreamController to add a new value that will trigger the StreamBuilder.
You can use StreamBuilder and AsyncSnapshot without setting the type.
But if you use StreamBuilder<UserModel> and AsyncSnapshot<UserModel> when you type snapshot.data. you will see all variables and methods from the UserModel.
final StreamController<UserModel> _currentUserStreamCtrl = StreamController<UserModel>.broadcast();
Stream<UserModel> get onCurrentUserChanged => _currentUserStreamCtrl.stream;
void updateCurrentUserUI() => _currentUserStreamCtrl.sink.add(_currentUser);
StreamBuilder<UserModel>(
stream: onCurrentUserChanged,
builder: (BuildContext context, AsyncSnapshot<UserModel> snapshot) {
if (snapshot.data != null) {
print('build signed screen, logged as: ' + snapshot.data.displayName);
return blocs.pageView.pagesView; //pageView containing signed page
}
print('build login screen');
return LoginPage();
//print('loading');
//return Center(child: CircularProgressIndicator());
},
)
This way you can use a StatelessWidget and refresh just a single sub-widget (an icon with a different color, for example) without using setState (that rebuilds the entire page).
For performance, streams are the best approach.
Edit:
I'm using BLoC architecture approach, so it's much better to declare the variables in a homePageBloc.dart (that has a normal controller class with all business logic) and create the StreamBuilder in the homePage.dart (that has a class that extends Stateless/Stateful widget and is responsible for the UI).
Edit: My UserModel.dart, you can use DocumentSnapshot instead of Map<String, dynamic> if you are using Cloud Firestore database from Firebase.
class UserModel {
/// Document ID of the user on database
String _firebaseId = "";
String get firebaseId => _firebaseId;
set firebaseId(newValue) => _firebaseId = newValue;
DateTime _creationDate = DateTime.now();
DateTime get creationDate => _creationDate;
DateTime _lastUpdate = DateTime.now();
DateTime get lastUpdate => _lastUpdate;
String _displayName = "";
String get displayName => _displayName;
set displayName(newValue) => _displayName = newValue;
String _username = "";
String get username => _username;
set username(newValue) => _username = newValue;
String _photoUrl = "";
String get photoUrl => _photoUrl;
set photoUrl(newValue) => _photoUrl = newValue;
String _phoneNumber = "";
String get phoneNumber => _phoneNumber;
set phoneNumber(newValue) => _phoneNumber = newValue;
String _email = "";
String get email => _email;
set email(newValue) => _email = newValue;
String _address = "";
String get address => _address;
set address(newValue) => _address = newValue;
bool _isAdmin = false;
bool get isAdmin => _isAdmin;
set isAdmin(newValue) => _isAdmin = newValue;
/// Used on first login
UserModel.fromFirstLogin() {
_creationDate = DateTime.now();
_lastUpdate = DateTime.now();
_username = "";
_address = "";
_isAdmin = false;
}
/// Used on any login that isn't the first
UserModel.fromDocument(Map<String, String> userDoc) {
_firebaseId = userDoc['firebaseId'] ?? '';
_displayName = userDoc['displayName'] ?? '';
_photoUrl = userDoc['photoUrl'] ?? '';
_phoneNumber = userDoc['phoneNumber'] ?? '';
_email = userDoc['email'] ?? '';
_address = userDoc['address'] ?? '';
_isAdmin = userDoc['isAdmin'] ?? false;
_username = userDoc['username'] ?? '';
//_lastUpdate = userDoc['lastUpdate'] != null ? userDoc['lastUpdate'].toDate() : DateTime.now();
//_creationDate = userDoc['creationDate'] != null ? userDoc['creationDate'].toDate() : DateTime.now();
}
void showOnConsole(String header) {
print('''
$header
currentUser.firebaseId: $_firebaseId
currentUser.username: $_username
currentUser.displayName: $_displayName
currentUser.phoneNumber: $_phoneNumber
currentUser.email: $_email
currentUser.address: $_address
currentUser.isAdmin: $_isAdmin
'''
);
}
String toReadableString() {
return
"displayName: $_displayName; "
"firebaseId: $_firebaseId; "
"email: $_email; "
"address: $_address; "
"photoUrl: $_photoUrl; "
"phoneNumber: $_phoneNumber; "
"isAdmin: $_isAdmin; ";
}
}