Related
In my program, users can create posts. And every post is called stuff. When an user created a stuff, automatically a stuffID assigns to that stuff. Also, I can list those stuffs in user's profile. When a user presses the edit button of one of their stuffs, a form appears in the screen. So the thing i want to do is when user press the edit button, I want to pass the stuffID from profile_sheet.dart class to update_stuff_form.dart class, so I can know to update the which stuff i will.
I have two .dart file. I used this function in my edit button in profile_sheet.dart file:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => updateStuffForm(
stuffID: data['stuffID'].toString())));
And I added my key function into update_stuff_form.dart like so:
class updateStuffForm extends StatefulWidget {
//const updateStuffForm({Key? key}) : super(key: key);
updateStuffForm({Key? key, this.stuffID}) : super(key: key);
final String? stuffID;
#override
_updateStuffFormState createState() => _updateStuffFormState();
}
And I called stuffID from end of the update_stuff_form.dart like this:
widget.stuffID
But whenever I run the program and press the edit button, It gaves me error below:
ErrorSummary('No Material widget found.'),
ErrorDescription(
'${context.widget.runtimeType} widgets require a Material '
'widget ancestor.\n'
'In material design, most widgets are conceptually "printed" on '
"a sheet of material. In Flutter's material library, that "
'material is represented by the Material widget. It is the '
'Material widget that renders ink splashes, for instance. '
'Because of this, many material library widgets require that '
'there be a Material widget in the tree above them.',
),
ErrorHint(
'To introduce a Material widget, you can either directly '
'include one, or use a widget that contains Material itself, '
'such as a Card, Dialog, Drawer, or Scaffold.',
),
Here is my full profile_sheet.dart file:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:unistuff_main/screens/home/update_stuff_form.dart';
class profileSheet extends StatefulWidget {
const profileSheet({Key? key}) : super(key: key);
#override
_profileSheetState createState() => _profileSheetState();
}
class _profileSheetState extends State<profileSheet> {
final _formkey = GlobalKey<FormState>();
//funcs
#override
Widget build(BuildContext context) {
return Container(
child: userStuffList(),
);
}
}
class userStuffList extends StatelessWidget {
const userStuffList({Key? key}) : super(key: key);
void _editStuff(BuildContext context) {
//stuff_form link.
showModalBottomSheet<dynamic>(
isScrollControlled: true,
context: context,
builder: (context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 20.0, horizontal: 60.0),
child: updateStuffForm());
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: _userStuffList(),
);
}
}
class _userStuffList extends StatelessWidget {
void _editStuff(BuildContext context) {
//stuff_form'a yönlendirme.
showModalBottomSheet<dynamic>(
isScrollControlled: true,
context: context,
builder: (context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 20.0, horizontal: 60.0),
child: updateStuffForm());
});
}
#override
Widget build(BuildContext context) {
final FirebaseAuth auth = FirebaseAuth.instance;
final User? user = auth.currentUser;
final _uid = user!.uid;
Query _stuffStream = FirebaseFirestore.instance
.collection('Stuffs')
.where('userID', isEqualTo: _uid);
return Material(
child: StreamBuilder<QuerySnapshot>(
//veri akışı başlatılıyor
//akış oluşturuluyor
stream: _stuffStream.snapshots(),
builder:
(BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something is wrong.');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("Loading");
}
return ListView(
//showing the data
children: snapshot.data!.docs.map((DocumentSnapshot document) {
Map<String, dynamic> data =
document.data()! as Map<String, dynamic>;
String stuffID = data['stuffID'];
return ListTile(
title: Text(data['title']),
subtitle: Column(
children: <Widget>[
Text(data['details']),
TextButton(
child: const Text('Düzenle'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => updateStuffForm(
stuffID: data['stuffID'].toString())));
_editStuff(context);
})
],
),
);
}).toList(),
);
}),
);
}
}
And my full update_stuff_form.dart (My key reference at first class, my widget.stuffID call all end of it.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
//import 'package:unistuff_main/screens/home/profile_sheet.dart';
class updateStuffForm extends StatefulWidget {
//const updateStuffForm({Key? key}) : super(key: key);
updateStuffForm({Key? key, this.stuffID}) : super(key: key);
final String? stuffID;
#override
_updateStuffFormState createState() => _updateStuffFormState();
}
class _updateStuffFormState extends State<updateStuffForm> {
final _formkey = GlobalKey<FormState>();
final List<String> categories = ["Spor", "Vasıta", "Elektronik"];
editStuff(stuffID) {
final FirebaseAuth auth = FirebaseAuth.instance;
final User? user = auth.currentUser;
final _uid = user!.uid;
FirebaseFirestore.instance.collection('Stuffs').doc(stuffID).update({
'title': _currentTitle,
'details': _currentDetails,
'price': _currentPrice,
'category': _currentCategory,
'userID': _uid
});
}
bool validatePrice(String str) {
RegExp _numeric = RegExp(r'^-?[0-9]+$');
return _numeric.hasMatch(str);
}
//form values
String? _currentTitle;
String? _currentDetails;
String? _currentPrice;
String? _currentCategory;
#override
Widget build(BuildContext context) {
return Form(
key: _formkey,
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Text(
'Edit your stuff',
style: TextStyle(fontSize: 18.0),
),
SizedBox(height: 20.0),
TextFormField(
decoration: InputDecoration(
fillColor: Colors.brown, hintText: 'Title', filled: true),
validator: (val) {
if ((validatePrice(val!) == false)) {
return "Please enter a number";
}
if (val.isEmpty == true) {
return "Please enter a value";
}
return null;
},
onChanged: (val) => setState(() => _currentTitle = val),
),
SizedBox(height: 20.0),
TextFormField(
keyboardType: TextInputType.multiline,
minLines: 1, //Normal textInputField will be displayed
maxLines: 5,
decoration: InputDecoration(
fillColor: Colors.brown, hintText: 'Details', filled: true),
validator: (val) =>
val!.isEmpty ? 'Please add a detail' : null,
onChanged: (val) => setState(() => _currentDetails = val),
),
SizedBox(height: 20.0),
TextFormField(
decoration: InputDecoration(
fillColor: Colors.brown, hintText: 'Price', filled: true),
validator: (val) =>
val!.isEmpty ? 'Please enter a price' : null,
onChanged: (val) => setState(() => _currentPrice = val),
),
SizedBox(height: 20.0),
DropdownButtonFormField(
items: categories.map((category) {
return DropdownMenuItem(
value: category,
child: Text('$category'),
);
}).toList(),
onChanged: (val) =>
setState(() => _currentCategory = val as String?),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Colors.pink[400],
),
child: Text('Ekle', style: TextStyle(color: Colors.white)),
onPressed: () async {
editStuff(widget.stuffID);
},
),
],
),
),
);
}
}
With the information about screens provided, I can see that your profile_sheet.dart is not having MaterialApp() as the first ancestor. So in place of Material(), use MaterialApp().
Inside build method of profile_sheet.dart, replace Material() to:
return MaterialApp()
I am managing state using provider but ChangeNotifierProvider does not change the value of variable.
I want to show progress indicator when user is registering. But ChangeNotifierProvider does not provide me update value rather it always return me _isLoading = false;
My Code:
AuthServices.dart
class AuthServices with ChangeNotifier {
bool _isLoading = false;
bool get loading => _isLoading;
///Register User
Future registerUser(
{#required String email, #required String password}) async {
try {
_isLoading = true;
notifyListeners();
await http.post(
BackEndConfigs.baseUrl +
BackEndConfigs.version +
BackEndConfigs.auth +
EndPoints.register,
body: {
"email": email,
"password": password
}).then((http.Response response) {
_isLoading = false;
notifyListeners();
return RegisterUser.fromJson(json.decode(response.body));
});
} catch (e) {
print(e);
}
}
}
RegisterScreen.dart
class RegisterScreen extends StatefulWidget {
#override
_RegisterScreenState createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
AuthServices _authServices = AuthServices();
ProgressDialog pr;
#override
Widget build(BuildContext context) {
pr = ProgressDialog(context, isDismissible: true);
// print(status.loading);
return Scaffold(
appBar:
customAppBar(context, title: 'Register Yourself', onPressed: () {}),
body: _getUI(context),
);
}
Widget _getUI(BuildContext context) {
return LoadingOverlay(
isLoading: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
VerticalSpace(10.0),
GreyBoldText('Account Information'),
VerticalSpace(20.0),
IconsButtonRow([
Icon(
FontAwesomeIcons.linkedinIn,
color: Color(0xff0e76a8),
),
Icon(
FontAwesomeIcons.facebook,
color: Color(0xff3b5998),
),
Icon(
FontAwesomeIcons.google,
color: Color(0xff4285F4),
),
]),
VerticalSpace(10.0),
GreyNormalText('or sign up with Email'),
VerticalSpace(10.0),
BlackBoldText("Email"),
AppTextField(
label: 'Email',
),
BlackBoldText("Password"),
AppTextField(
label: 'Password',
),
BlackBoldText("Confirm Password"),
AppTextField(
label: 'Confirm Password',
),
VerticalSpace(20.0),
ChangeNotifierProvider(
create: (_) => AuthServices(),
child: Consumer(
builder: (context, AuthServices user, _) {
return Text(user.loading.toString());
},
),
),
AppButton(
buttonText: 'Next',
onPressed: () async {
// await pr.show();
_registerNewUser();
}),
VerticalSpace(30.0),
ToggleView(ToggleViewStatus.SignUpScreen),
VerticalSpace(20.0),
],
),
),
),
);
}
_registerNewUser() async {
_authServices.registerUser(
email: 'sdjfkldsdf#ssdfdsddfdgsffd.com', password: 'sldjsdfkls');
}
}
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
scaffoldBackgroundColor: Colors.white,
hintColor: FrontEndConfigs.hintColor,
cursorColor: FrontEndConfigs.hintColor,
fontFamily: 'Gilory'),
home: RegisterScreen(),
);
}
}
You created two different instances of AuthServices.
One at the start of your State class and one using the ChangeNotifierProvider.
When calling _registerNewUser you use the AuthServices created in your state class not the provided one.
When you call registerUser on the first AuthServices, the value does not change for the second AuthServices provided by the ChangeNotifierProvider down in the Widget tree.
Try deleting the AuthServices instance created by the state class and move your ChangeNotifierProvider up the widget tree so all your functions share the same instance of AuthServices.
I'm learning Flutter and I'm trying to use the SQFLite package to persist data on the device. Everything works perfect but there's one problem, every time I add a new element or I upgrade it, I need to restart my app to see the changes and I don't know why, there are no errors or anything.
I uploaded it to github so you can try it, maybe is something in my emulator or something I dont know.
https://github.com/Rodrigogzmn6/sql_demo
or you can see part of the complete project here
Home Page
import 'package:flutter/material.dart';
import 'package:sql_demo/models/product.dart';
import 'package:sql_demo/pages/add_product_page.dart';
import 'package:sql_demo/pages/update_product_page.dart';
import 'package:sql_demo/pages/view_product_page.dart';
import 'package:sql_demo/services/dbhelper.dart';
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
DbHelper helper;
#override
void initState() {
super.initState();
helper = DbHelper();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home Page'),
),
body: FutureBuilder(
future: helper.getProducts(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
Product product = Product.fromMap(snapshot.data[index]);
return ListTile(
contentPadding: const EdgeInsets.all(16.0),
title: Text(
'${product.name} - \$${(product.price).toString()}'),
subtitle: Text(product.description),
trailing: Column(
children: [
Expanded(
child: IconButton(
icon: Icon(
Icons.delete,
color: Colors.red,
),
onPressed: () {
setState(() {
helper.deleteProduct(product.id);
});
}),
),
SizedBox(
height: 20.0,
),
Expanded(
child: IconButton(
icon: Icon(
Icons.edit,
color: Colors.blue,
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UpdateProductPage(
product: product,
)),
);
}),
),
],
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailsPage(
product: product,
)));
},
);
});
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
),
floatingActionButton: FloatingActionButton(
child: Icon(
Icons.add,
color: Colors.white,
),
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => AddProductPage()));
}),
);
}
}
Creating Item page
import 'package:flutter/material.dart';
import 'package:sql_demo/models/product.dart';
import 'package:sql_demo/services/dbhelper.dart';
class AddProductPage extends StatefulWidget {
#override
_AddProductPageState createState() => _AddProductPageState();
}
class _AddProductPageState extends State<AddProductPage> {
String name, description;
num price;
DbHelper helper;
//Instantiate Helper
#override
void initState() {
super.initState();
helper = DbHelper();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Add new product"),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
TextFormField(
decoration: InputDecoration(
hintText: 'Enter product name',
labelText: 'Product name',
),
onChanged: (value) {
setState(() {
name = value;
});
},
),
SizedBox(height: 16),
TextFormField(
decoration: InputDecoration(
hintText: 'Enter product description',
labelText: 'Product description',
),
onChanged: (value) {
setState(() {
description = value;
});
},
),
SizedBox(height: 16),
TextFormField(
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: 'Enter product price',
labelText: 'Product price',
),
onChanged: (value) {
setState(() {
price = double.parse(value);
});
},
),
SizedBox(height: 16),
RaisedButton(
child: Text('Save'),
onPressed: () {
Product product = Product({
'name': name,
'description': description,
'price': price,
});
setState(() {
helper.createProduct(product);
Navigator.pop(context);
});
},
),
],
),
);
}
}
Database operations
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sql_demo/models/product.dart';
class DbHelper {
//Creates a singleton class (it only creates once)
static final DbHelper _singleton = DbHelper._internal();
factory DbHelper() => _singleton;
DbHelper._internal();
//Creating an object of type database
static Database _database;
//Initializing or creating the database named mydb.db
//Creating a products table with necessary fields
Future<Database> initDatabase() async {
//If this class was never instaciated, we will execute this method, otherwise return
if (_database != null) return _database;
String path = join(await getDatabasesPath(), 'mydb.db');
_database = await openDatabase(
path,
version: 1,
onCreate: (Database db, int v) async {
await db.execute(
'CREATE TABLE Products (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, price REAL)');
},
);
return _database;
}
//Select all products
Future<List> getProducts() async {
Database db = await initDatabase();
return await db.query('Products');
}
//Create new product
Future<int> createProduct(Product product) async {
Database db = await initDatabase();
print('Product added');
return await db.insert(
'Products',
product.toMap(),
);
}
//Update product
Future<int> updateProduct(Product product) async {
Database db = await initDatabase();
return await db.update(
'Products',
product.toMap(),
where: 'id=?',
whereArgs: [product.id],
);
}
//Delete a product
Future<int> deleteProduct(int productID) async {
Database db = await initDatabase();
return await db.delete('Products', where: 'id=?', whereArgs: [productID]);
}
}
You can copy paste run full code below
You can await Navigator.push to AddProductPage and then call setState(() {});
code snippet
onPressed: () async {
await Navigator.push(context,
MaterialPageRoute(builder: (context) => AddProductPage()));
setState(() {});
}),
working demo
full code
import 'package:flutter/material.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'dart:convert';
class Product {
int _id;
String _name;
String _description;
double _price;
//Getters because the attributes are private
int get id => _id;
String get name => _name;
String get description => _description;
double get price => _price;
//Constructor
Product(dynamic obj) {
_id = obj['id'];
_name = obj['name'];
_description = obj['description'];
_price = obj['price'];
}
//Named objet (takes a Map as parameter)
Product.fromMap(Map<String, dynamic> data) {
_id = data['id'];
_name = data['name'];
_description = data['description'];
_price = data['price'];
}
//Method that converts the object into a map
Map<String, dynamic> toMap() {
return {
'id': _id,
'name': _name,
'description': _description,
'price': _price,
};
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
DbHelper helper;
#override
void initState() {
super.initState();
helper = DbHelper();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home Page'),
),
body: FutureBuilder(
future: helper.getProducts(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
Product product = Product.fromMap(snapshot.data[index]);
return ListTile(
contentPadding: const EdgeInsets.all(16.0),
title: Text(
'${product.name} - \$${(product.price).toString()}'),
subtitle: Text(product.description),
trailing: Column(
children: [
Expanded(
child: IconButton(
icon: Icon(
Icons.delete,
color: Colors.red,
),
onPressed: () {
setState(() {
helper.deleteProduct(product.id);
});
}),
),
SizedBox(
height: 20.0,
),
Expanded(
child: IconButton(
icon: Icon(
Icons.edit,
color: Colors.blue,
),
onPressed: () {
/*Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UpdateProductPage(
product: product,
)),
);*/
}),
),
],
),
onTap: () {
/*Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailsPage(
product: product,
)));*/
},
);
});
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
),
floatingActionButton: FloatingActionButton(
child: Icon(
Icons.add,
color: Colors.white,
),
onPressed: () async {
await Navigator.push(context,
MaterialPageRoute(builder: (context) => AddProductPage()));
setState(() {});
}),
);
}
}
class AddProductPage extends StatefulWidget {
#override
_AddProductPageState createState() => _AddProductPageState();
}
class _AddProductPageState extends State<AddProductPage> {
String name, description;
num price;
DbHelper helper;
//Instantiate Helper
#override
void initState() {
super.initState();
helper = DbHelper();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Add new product"),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
TextFormField(
decoration: InputDecoration(
hintText: 'Enter product name',
labelText: 'Product name',
),
onChanged: (value) {
setState(() {
name = value;
});
},
),
SizedBox(height: 16),
TextFormField(
decoration: InputDecoration(
hintText: 'Enter product description',
labelText: 'Product description',
),
onChanged: (value) {
setState(() {
description = value;
});
},
),
SizedBox(height: 16),
TextFormField(
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: 'Enter product price',
labelText: 'Product price',
),
onChanged: (value) {
setState(() {
price = double.parse(value);
});
},
),
SizedBox(height: 16),
RaisedButton(
child: Text('Save'),
onPressed: () {
Product product = Product({
'name': name,
'description': description,
'price': price,
});
setState(() {
helper.createProduct(product);
Navigator.pop(context);
});
},
),
],
),
);
}
}
class DbHelper {
//Creates a singleton class (it only creates once)
static final DbHelper _singleton = DbHelper._internal();
factory DbHelper() => _singleton;
DbHelper._internal();
//Creating an object of type database
static Database _database;
//Initializing or creating the database named mydb.db
//Creating a products table with necessary fields
Future<Database> initDatabase() async {
//If this class was never instaciated, we will execute this method, otherwise return
if (_database != null) return _database;
String path = join(await getDatabasesPath(), 'mydb.db');
_database = await openDatabase(
path,
version: 1,
onCreate: (Database db, int v) async {
await db.execute(
'CREATE TABLE Products (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, price REAL)');
},
);
return _database;
}
//Select all products
Future<List> getProducts() async {
Database db = await initDatabase();
return await db.query('Products');
}
//Create new product
Future<int> createProduct(Product product) async {
Database db = await initDatabase();
print('Product added');
return await db.insert(
'Products',
product.toMap(),
);
}
//Update product
Future<int> updateProduct(Product product) async {
Database db = await initDatabase();
return await db.update(
'Products',
product.toMap(),
where: 'id=?',
whereArgs: [product.id],
);
}
//Delete a product
Future<int> deleteProduct(int productID) async {
Database db = await initDatabase();
return await db.delete('Products', where: 'id=?', whereArgs: [productID]);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomePage(),
);
}
}
Hot reload: Hot reload feature quickly compile the newly added code in our file and sent the code to Dart Virtual Machine. If you are using States in your application then Hot Reload preservers the States so they will not update on Hot Reload our set to their default values.
And, initState() does not rebuild after hot reload as its state is preserved. So, you have to perform hot restart to see database changes:
#override
void initState() {
super.initState();
helper = DbHelper();
}
When stateful widget is build, you have helper, but after you hot reload, the helper will be same as initState only called hot restart or you launch the widget. So, helper variable is preserved. Hence, you have to hot restart.
Links that might help you: Understanding Flutter hot reload and hot restart the simplest way | Refs
I'm developing a new Flutter Mobile app using the BLoC pattern . But I've got a problem and I don't find the solution yet.
The first one is my home page (with the MultiBlocProvider)
When I press on the FloatingActionButton.
It push a new screen to add a new "FicheMvt"
When I hit the add button.
It uses an onSave callback function to notify its parent of newly created "FicheMvt"
It gives me an error.
BlocProvider.of() called with a context that does not contain a Bloc
of type FicheMvtBloc.
No ancestor could be found starting from the context that was passed
to BlocProvider.of().
This can happen if the context you used comes from a widget above the
BlocProvider.
This is the home page (render 5 tab body)
class EtatCollecteScreen extends StatelessWidget {
final FicheMvtDAO ficheMvtDAO = FicheMvtDAO();
final FicheMvtReferenceDAO ficheMvtReferenceDAO = FicheMvtReferenceDAO();
#override
Widget build(BuildContext context) {
final FicheModel fiche = ModalRoute.of(context).settings.arguments;
return MultiBlocProvider(
providers: [
BlocProvider<TabEtatCollecteBloc>(
create: (context) => TabEtatCollecteBloc(),
),
BlocProvider<FicheMvtBloc>(
create: (context) => FicheMvtBloc(
ficheMvtDAO: ficheMvtDAO,
)..add(FicheMvtRequested(idFiche: fiche.id)),
),
BlocProvider<FicheMvtReferenceBloc>(
create: (context) => FicheMvtReferenceBloc(
ficheMvtReferenceDAO: ficheMvtReferenceDAO,
)..add(FicheMvtReferenceRequested(idFiche: fiche.id)),
),
],
child: EtatCollecteContent(
ficheModel: fiche,
),
);
}
}
class EtatCollecteContent extends StatelessWidget {
final FicheModel ficheModel;
const EtatCollecteContent({Key key, #required this.ficheModel});
#override
Widget build(BuildContext context) {
return BlocBuilder<TabEtatCollecteBloc, EtatCollecteTab>(
builder: (context, activeTab) {
return Scaffold(
appBar: AppBar(
title: Text("${ficheModel.id} - ${ficheModel.description}"),
actions: <Widget>[
RefreshMvtButton(
visible: activeTab == EtatCollecteTab.completed,
ficheModel: ficheModel,
),
SendMvtButton(
visible: activeTab == EtatCollecteTab.uncommitted,
ficheModel: ficheModel,
),
],
),
body: EtatCollecteBody(
activeTab: activeTab,
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return FicheMvtAddScreen(onSaveCallback: (idFiche, indicateurModel, codeSite) {
BlocProvider.of<FicheMvtBloc>(context).add(
FicheMvtAdded(
idFiche: idFiche,
indicateurModel: indicateurModel,
codeSite: codeSite,
),
);
});
},
settings: RouteSettings(
arguments: ficheModel,
),
),
);
},
child: Icon(Icons.add),
tooltip: "Add",
),
bottomNavigationBar: TabEtatCollecteSelector(
activeTab: activeTab,
onTabSelected: (tab) => BlocProvider.of<TabEtatCollecteBloc>(context).add(TabEtatCollecteUpdated(tab)),
),
);
},
);
}
}
And this is the code of the form to add new "FicheMvt" which contains another block that manages the dynamic form (FicheMvtAddBloc).
typedef OnSaveCallback = Function(
int idFiche,
IndicateurModel indicateurModel,
String codeSite,
);
class FicheMvtAddScreen extends StatelessWidget {
final OnSaveCallback onSaveCallback;
const FicheMvtAddScreen({Key key, #required this.onSaveCallback}) : super(key: key);
#override
Widget build(BuildContext context) {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final FicheModel fiche = ModalRoute.of(context).settings.arguments;
final FicheMvtRepository ficheMvtRepository = FicheMvtRepository();
return Scaffold(
key: _scaffoldKey,
appBar: new AppBar(
title: new Text("${fiche.id} - ${fiche.description}"),
),
backgroundColor: Colors.white,
body: BlocProvider<FicheMvtAddBloc>(
create: (context) => FicheMvtAddBloc(
ficheMvtRepository: ficheMvtRepository,
idFiche: fiche.id,
)..add(NewFicheMvtFormLoaded(idFiche: fiche.id)),
child: FicheMvtAddBody(
ficheModel: fiche,
onSave: onSaveCallback,
),
),
);
}
}
This is the content of the form
class FicheMvtAddBody extends StatefulWidget {
final FicheModel ficheModel;
final OnSaveCallback onSave;
#override
_FicheMvtAddBodyState createState() => _FicheMvtAddBodyState();
FicheMvtAddBody({Key key, #required this.ficheModel, #required this.onSave});
}
class _FicheMvtAddBodyState extends State<FicheMvtAddBody> {
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
void _onIndicateurChanged(String indicateur) =>
BlocProvider.of<FicheMvtAddBloc>(context).add(NewFicheMvtIndicateurChanged(indicateur: indicateur));
void _onSiteChanged(String site) => BlocProvider.of<FicheMvtAddBloc>(context).add(NewFicheMvtSiteChanged(site: site));
final FicheModel fiche = ModalRoute.of(context).settings.arguments;
final txtIndicateur = Text("Indicateur");
final txtSite = Text("Site");
return BlocBuilder<FicheMvtAddBloc, FicheMvtAddState>(
builder: (context, state) {
return Form(
key: _formKey,
child: Center(
child: ListView(
shrinkWrap: true,
padding: EdgeInsets.only(left: 24.0, right: 24.0),
children: <Widget>[
SizedBox(height: 24.0),
txtIndicateur,
DropdownButtonFormField<String>(
isExpanded: true,
hint: Text("Choisissez l'indicateur"),
value: state.indicateur?.code ?? null,
icon: Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
onChanged: (String newValue) {
_onIndicateurChanged(newValue);
},
items: state.indicateurs?.isNotEmpty == true
? state.indicateurs
.map((CodeDescriptionModel model) => DropdownMenuItem(value: model.code, child: Text(model.description)))
.toList()
: const [],
validator: (value) {
if (value == null || value.isEmpty) {
return 'Entrer l\'indicateur s\'il vous plait';
}
return null;
},
),
SizedBox(height: 24.0),
txtSite,
DropdownButtonFormField<String>(
isExpanded: true,
hint: Text("Choisissez le site"),
value: state.site?.code ?? null,
icon: Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
onChanged: (String newValue) {
_onSiteChanged(newValue);
},
items: state.sites?.isNotEmpty == true
? state.sites.map((CodeDescriptionModel model) => DropdownMenuItem(value: model.code, child: Text(model.description))).toList()
: const [],
validator: (value) {
if (value == null || value.isEmpty) {
return 'Entrer le site s\'il vous plait';
}
return null;
},
),
Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: RaisedButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
onPressed: () {
if (_formKey.currentState.validate()) {
widget.onSave(
fiche.id,
state.indicateur,
state.site?.code ?? null,
);
Navigator.pop(context);
}
},
padding: EdgeInsets.all(12),
color: Colors.blue,
child: Text('Create', style: TextStyle(color: Colors.white)),
),
)
],
),
),
);
},
);
}
}
Thanks for your help
You are using the wrong context in onSaveCallback. Here is a simplified hierarchy of your widgets:
- MaterialApp
- EtatCollecteScreen
- MultiBlocProvider
- FicheMvtAddScreen
So in your onSaveCallback you are accessing the context of FicheMvtAddScreen and it's obvious from the hierarchy above that BlocProvider couldn't find the requested Bloc. It's easy to fix this:
MaterialPageRoute(
builder: (pageContext) {
return FicheMvtAddScreen(onSaveCallback: (idFiche, indicateurModel, codeSite) {
BlocProvider.of<FicheMvtBloc>(context).add(
FicheMvtAdded(
idFiche: idFiche,
indicateurModel: indicateurModel,
codeSite: codeSite,
),
);
});
},
settings: RouteSettings(
arguments: ficheModel,
),
),
I've renamed context variable to pageContext in route builder function (so it wouldn't shadow required context). Now BlocProvider should able to find requested Bloc by accessing right context.
Another way to fix is to put MultiBlocProvider higher in widgets hierarchy.
After validating a form and sending a request from flutter to the server backend: I want to set any potential error message from the server to be displayed in the original form. Preferably exactly like a validation error.
For instance:
Widget build(BuildContext context) {
...
TextFormField(
onFieldSubmitted: (value) => _signIn(),
validator: (input) {
if (input.length < 6)
return 'Your password is too short';
return null;
},
onSaved: (input) => _password = input,
decoration: InputDecoration(
labelText: 'Password',
),
obscureText: true,
)
...
}
Future<void> _signIn() async {
final formState = _formKey.currentState;
if (!formState.validate()) return;
formState.save();
try {
... // do fancy request stuff
} catch (e) {
// this is where I want to set the "validation" error
}
}
It's actually super simple and the validation error still works aswell.
String? _errorMsg;
Widget build(BuildContext context) {
...
TextFormField(
onFieldSubmitted: (value) => _signIn(),
validator: (input) {
if (input.length < 6)
// will set the errorText directly, no need for a variable here
return 'Your password is too short';
return null;
},
onSaved: (input) => _password = input,
decoration: InputDecoration(
labelText: 'Password',
errorText: _errorMsg,
),
obscureText: true,
)
...
}
Future<void> _signIn() async {
setState(() {
_errorMsg = null; // clear any existing errors
});
final formState = _formKey.currentState;
if (!formState.validate()) return;
formState.save();
try {
... // do fancy request stuff
} catch (e) {
setState(() {
_errorMsg = 'Wrong password.';
});
}
}
I suppose, I could think of a solution, but I think it's kind of ugly.
I could have an "error" variable, that is set when the request fails.
I would then call formState.validate() a second time, in there: check the error variable and return it if it's not null.
You can use flutter_form_bloc and use addError method of TextFieldBloc.
usernameField.addError('That username is taken. Try another.');
Keep in mind that you can also use asynchronous validators.
This is a complete example:
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^0.21.0
form_bloc: ^0.5.0
flutter_form_bloc: ^0.4.1+1
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_bloc/flutter_form_bloc.dart';
import 'package:form_bloc/form_bloc.dart';
void main() {
runApp(MaterialApp(home: SignUpForm()));
}
class SignUpFormBloc extends FormBloc<String, String> {
final usernameField = TextFieldBloc();
final passwordField =
TextFieldBloc(validators: [Validators.passwordMin6Chars]);
#override
List<FieldBloc> get fieldBlocs => [usernameField, passwordField];
#override
Stream<FormBlocState<String, String>> onSubmitting() async* {
// Form logic...
try {
await _signUp(
throwException: true,
username: usernameField.value,
password: passwordField.value,
);
yield currentState.toSuccess();
} catch (e) {
// When get the error from the backend you can
// add the error to the field:
usernameField.addError('That username is taken. Try another.');
yield currentState
.toFailure('The error was added to the username field.');
}
}
Future<void> _signUp({
#required bool throwException,
#required String username,
#required String password,
}) async {
print(username);
print(password);
await Future<void>.delayed(Duration(seconds: 2));
if (throwException) throw Exception();
}
}
class SignUpForm extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider<SignUpFormBloc>(
builder: (context) => SignUpFormBloc(),
child: Builder(
builder: (context) {
final formBloc = BlocProvider.of<SignUpFormBloc>(context);
return Scaffold(
appBar: AppBar(title: Text('Sign Up Form')),
body: FormBlocListener<SignUpFormBloc, String, String>(
onSubmitting: (context, state) {
// Show the progress dialog
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => WillPopScope(
onWillPop: () async => false,
child: Center(
child: Card(
child: Container(
width: 80,
height: 80,
padding: EdgeInsets.all(12.0),
child: CircularProgressIndicator(),
),
),
),
),
);
},
onSuccess: (context, state) {
// Hide the progress dialog
Navigator.of(context).pop();
// Navigate to success screen
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => SuccessScreen()));
},
onFailure: (context, state) {
// Hide the progress dialog
Navigator.of(context).pop();
// Show snackbar with the error
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(state.failureResponse),
backgroundColor: Colors.red[300],
),
);
},
child: ListView(
children: <Widget>[
TextFieldBlocBuilder(
textFieldBloc: formBloc.usernameField,
decoration: InputDecoration(labelText: 'Username'),
),
TextFieldBlocBuilder(
textFieldBloc: formBloc.passwordField,
decoration: InputDecoration(labelText: 'Password'),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
onPressed: formBloc.submit,
child: Center(child: Text('SUBMIT')),
),
),
],
),
),
);
},
),
);
}
}
class SuccessScreen extends StatelessWidget {
const SuccessScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.green[300],
body: Center(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Icon(
Icons.sentiment_satisfied,
size: 100,
),
RaisedButton(
color: Colors.green[100],
child: Text('Sign out'),
onPressed: () => Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => SignUpForm())),
)
],
),
),
),
);
}
}
A simple solution:
Make a key for the widgets state:
GlobalKey<_CustomErrorTextField> _passwordTextFieldState = GlobalKey();
Set the Error message using the key:
_passwordTextFieldState.currentState.updateError(errorMsg);
Reset the error after 2 seconds:
Future.delayed(Duration(seconds: 2), () {
// Runs after duration sec
_passwordTextFieldState.currentState.updateError(null);
});
Set up the widget (be sure to set the key):
CustomErrorTextField(
key: _passwordTextFieldState,
label: "Password",
currentPassword: password,
validator: yourValidator,
callback: passwordCallback,
obscureText: hidePassword.value - a bool value show/hide password
)
Here is the Widget:
class CustomErrorTextField extends StatefulWidget {
CustomErrorTextField({
Key key,
this.label,
this.currentPassword,
this.validator,
this.callback,
this.obscureText = false
}): super(key: key);
final String label;
final String currentPassword;
final FormFieldValidator<String> validator;
final Function callback;
final obscureText;
#override
_CustomErrorTextField createState() => _CustomErrorTextField();
}
class _CustomErrorTextField extends State<CustomErrorTextField> {
String errorMsg;
updateError(String errorMsg){
setState(() {
this.errorMsg = errorMsg;
});
}
#override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(
labelText: widget.label,
errorText: errorMsg
),
initialValue: widget.currentPassword,
keyboardType: TextInputType.visiblePassword,
validator: widget.validator,
onSaved: (String val) {
widget.callback(val);
},
obscureText: widget.obscureText,
);
}
}