Related
I saw this code in KindaCode. This is link ( https://www.kindacode.com/article/flutter-hive-database/#single__comments ) . I want the added item not to be added again. How can I do that? This code, we just add items and delete and olsa upgrade. We just write the name and quantity and adding the item. But if i write the same name as the other one, adds again.
// main.dart
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
await Hive.openBox('shopping_box'); //verileri icerisinde barındırıcak kutuyu olusturma
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'KindaCode.com',
theme: ThemeData(
primarySwatch: Colors.green,
),
home: const HomePage(),
);
}
}
// Home Page
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<Map<String, dynamic>> _items = [];
final _shoppingBox = Hive.box('shopping_box');
#override
void initState() {
super.initState();
_refreshItems(); // Load data when app starts
}
// Get all items from the database
void _refreshItems() {
final data = _shoppingBox.keys.map((key) {
final value = _shoppingBox.get(key);
return {"key": key, "name": value["name".toLowerCase()], "quantity": value['quantity']};
}).toList(); //verileri listede gosterme
setState(() {
_items = data.reversed.toList(); //en sondan en eskiye dogru siralamak icin reversed
// we use "reversed" to sort items in order from the latest to the oldest
});
}
// Create new item
Future<void> _createItem(Map<String, dynamic> newItem) async {
await _shoppingBox.add(newItem); //yeni veri olusturma
_refreshItems(); // update the UI
}
// Retrieve a single item from the database by using its key
// Our app won't use this function but I put it here for your reference
// Update a single item
Future<void> _updateItem(int itemKey, Map<String, dynamic> item) async {
await _shoppingBox.put(itemKey, item); //tablo icerisine veriyi koyma
_refreshItems(); // Update the UI
}
// Delete a single item
Future<void> _deleteItem(int itemKey) async {
await _shoppingBox.delete(itemKey);
_refreshItems(); // update the UI
// Display a snackbar
ScaffoldMessenger.of(context).showSnackBar( //ekranin alt kisminda itemin silindigini belirtme
const SnackBar(content: Text('An item has been deleted')));
}
// TextFields' controllers
final TextEditingController _nameController = TextEditingController();
final TextEditingController _quantityController = TextEditingController();
void _showForm(BuildContext ctx, int? itemKey) async { //yeni item eklerken ve butona basildiginda tetiklenen flotingbutton
// itemKey == null -> create new item
// itemKey != null -> update an existing item
if (itemKey != null) { //itemi guncelleme
final existingItem =
_items.firstWhere((element) => element['key'] == itemKey);
_nameController.text = existingItem['name'];
_quantityController.text = existingItem['quantity'];
}
showModalBottomSheet(
context: ctx,
elevation: 5,
isScrollControlled: true,
builder: (_) => Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(ctx).viewInsets.bottom,
top: 15,
left: 15,
right: 15),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
TextField(
controller: _nameController,
decoration: const InputDecoration(hintText: 'Name'),
),
const SizedBox(
height: 10,
),
TextField(
controller: _quantityController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: 'Quantity'),
),
const SizedBox(
height: 20,
),
ElevatedButton( //here the create or upgrade the item
onPressed: () async {
// Save new item
if (itemKey == null) { //yeni item
if() {
_createItem({
"name": _nameController.text,
"quantity": _quantityController.text
});
}
}
// update an existing item
if (itemKey != null) {
_updateItem(itemKey, {
'name': _nameController.text.trim(),
'quantity': _quantityController.text.trim()
});
}
// Clear the text fields
_nameController.text = '';
_quantityController.text = '';
Navigator.of(context).pop(); // Close the bottom sheet
},
child: Text(itemKey == null ? 'Create New' : 'Update'),
),
const SizedBox(
height: 15,
)
],
),
));
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('KindaCode.com'),
),
body: _items.isEmpty
? const Center(
child: Text(
'No Data',
style: TextStyle(fontSize: 30),
),
)
: ListView.builder(
// the list of items
itemCount: _items.length,
itemBuilder: (_, index) {
final currentItem = _items[index];
return Card(
color: Colors.orange.shade100,
margin: const EdgeInsets.all(10),
elevation: 3,
child: ListTile(
title: Text(currentItem['name']),
subtitle: Text(currentItem['quantity'].toString()),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Edit button
IconButton(
icon: const Icon(Icons.edit), //düzenleme butonu
onPressed: () =>
_showForm(context, currentItem['key'])),
// Delete button
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _deleteItem(currentItem['key']),
),
],
)),
);
}),
// Add new item button
floatingActionButton: FloatingActionButton( //ekranin alt kosesinde bulunan ekleme butonu
onPressed: () => _showForm(context, null),
child: const Icon(Icons.add),
),
);
}
} ```
I have a big Flutter project which is using my Woocommerce website as backend. Everything is working fine, but it was missing the search function on this recipes screen. I'm completly new to Flutter, but because I have Java experience with some luck and miracle I was able to create this function [the search] using the list view and call the dedicated endpoint if I enter the search term. This is working (hooray), but the problem is, that the list of elements are not refreshing if I call the search. It stays on the "all" view, only if I pull down the screen and make a refresh, only than I will see the search results... I tried with the suggested "key" for the widgets, but because I'm not really familiar with Flutter most likely I use is wrong or not on the right element... What is the best way to make this work? Can I call the refresh function somehow (I tried to find it, but failed) after calling the search or is it possible to force the widget re-draw in this case?
Thank you very much.
Edit3.:
This is the searchRecipeModel class:
import '../../../models/entities/blog.dart';
import '../../../models/paging_data_provider.dart';
import '../repositories/search_recipe_repository.dart';
export '../../../models/entities/blog.dart';
class SearchRecipeModel extends PagingDataProvider<Blog> {
SearchRecipeModel() : super(dataRepo: SearchRecipeRepository());
List<Blog> get recipes => data;
Future<void> searchRecipes() => getData();
}
This is the SearchRecipeRepository class:
import '../../../common/base/paging_repository.dart';
import '../../../models/entities/blog.dart';
import '../../../models/entities/paging_response.dart';
class SearchRecipeRepository extends PagingRepository<Blog> {
#override
Future<PagingResponse<Blog>> Function(dynamic) get requestApi =>
service.api.searchRecipes;
}
This is the Blog class, it's a Wordpress entity:
import 'dart:convert';
import 'package:html_unescape/html_unescape.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import '../../common/packages.dart';
import '../../services/index.dart';
import '../serializers/blog.dart';
class Blog {
final dynamic id;
final String title;
final String subTitle;
final String date;
final String content;
final String author;
final String imageFeature;
const Blog({
this.id,
this.title,
this.subTitle,
this.date,
this.content,
this.author,
this.imageFeature,
});
const Blog.empty(this.id)
: title = '',
subTitle = '',
date = '',
author = '',
content = '',
imageFeature = '';
factory Blog.fromJson(Map<String, dynamic> json) {
switch (Config().type) {
case ConfigType.woo:
return Blog._fromWooJson(json);
case ConfigType.shopify:
return Blog._fromShopifyJson(json);
case ConfigType.strapi:
return Blog._fromStrapiJson(json);
case ConfigType.mylisting:
case ConfigType.listeo:
case ConfigType.listpro:
return Blog._fromListingJson(json);
default:
return const Blog.empty(0);
}
}
Blog._fromShopifyJson(Map<String, dynamic> json)
: id = json['id'],
author = json['authorV2']['name'],
title = json['title'],
subTitle = null,
content = json['contentHtml'],
imageFeature = json['image']['transformedSrc'],
date = json['publishedAt'];
factory Blog._fromStrapiJson(Map<String, dynamic> json) {
var model = SerializerBlog.fromJson(json);
final id = model.id;
final author = model.user.displayName;
final title = model.title;
final subTitle = model.subTitle;
final content = model.content;
final imageFeature = Config().url + model.images.first.url;
final date = model.date;
return Blog(
author: author,
title: title,
subTitle: subTitle,
content: content,
id: id,
date: date,
imageFeature: imageFeature,
);
}
Blog._fromListingJson(Map<String, dynamic> json)
: id = json['id'],
author = json['author_name'],
title = HtmlUnescape().convert(json['title']['rendered']),
subTitle = HtmlUnescape().convert(json['excerpt']['rendered']),
content = json['content']['rendered'],
imageFeature = json['image_feature'],
date = DateFormat.yMMMMd('en_US').format(DateTime.parse(json['date']));
factory Blog._fromWooJson(Map<String, dynamic> json) {
String imageFeature;
var imgJson = json['better_featured_image'];
if (imgJson != null) {
if (imgJson['media_details']['sizes']['medium_large'] != null) {
imageFeature =
imgJson['media_details']['sizes']['medium_large']['source_url'];
}
}
if (imageFeature == null) {
var imgMedia = json['_embedded']['wp:featuredmedia'];
if (imgMedia != null &&
imgMedia[0]['media_details'] != null &&
imgMedia[0]['media_details']['sizes']['large'] != null) {
imageFeature =
imgMedia[0]['media_details']['sizes']['large']['source_url'];
}
/**
* Netbloom
* Featured image fix
*/
if(imageFeature == null &&
imgMedia[0]['media_details'] != null &&
imgMedia[0]['media_details']['sizes']['medium_large'] != null){
imageFeature =
imgMedia[0]['media_details']['sizes']['medium_large']['source_url'];
}
if(imageFeature == null &&
imgMedia[0]['media_details'] != null &&
imgMedia[0]['media_details']['file'] != null){
imageFeature =
"https://okosgrill.hu/wp-content/uploads/" + imgMedia[0]['media_details']['file'];
}
if(imageFeature == null && json['featured_image_urls'] != null && json['featured_image_urls']['medium_large'] != null){
imageFeature = json['featured_image_urls']['medium_large'];
}
if(imageFeature == null && json['featured_image_urls'] != null && json['featured_image_urls']['medium'] != null){
imageFeature = json['featured_image_urls']['medium'];
}
//Fallback
if(imageFeature == null){
imageFeature =
"https://okosgrill.hu/wp-content/uploads/okosgrill-tippek.jpg";
}
}
final author = json['_embedded']['author'] != null
? json['_embedded']['author'][0]['name']
: '';
final date =
DateFormat.yMMMMd('hu_HU').format(DateTime.parse(json['date']));
final id = json['id'];
final title = HtmlUnescape().convert(json['title']['rendered']);
final subTitle = json['excerpt']!= null ? HtmlUnescape().convert(json['excerpt']['rendered']) : '';
final content = json['content']['rendered'];
return Blog(
author: author,
title: title,
subTitle: subTitle,
content: content,
id: id,
date: date,
imageFeature: imageFeature,
);
}
static Future getBlogs({String url, categories, page = 1}) async {
try {
var param = '_embed&page=$page';
if (categories != null) {
param += '&categories=$categories';
}
final response =
await http.get('$url/wp-json/wp/v2/posts?$param'.toUri());
if (response.statusCode != 200) {
return [];
}
return jsonDecode(response.body);
} on Exception catch (_) {
return [];
}
}
static Future<dynamic> getBlog({url, id}) async {
final response =
await http.get('$url/wp-json/wp/v2/posts/$id?_embed'.toUri());
return jsonDecode(response.body);
}
#override
String toString() => 'Blog { id: $id title: $title}';
}
This is the BlogListItem class:
import 'package:flutter/material.dart';
import 'package:html/parser.dart';
import '../../../../common/constants.dart' show RouteList;
import '../../../../common/tools.dart' show Tools, kSize;
import '../../../../models/entities/blog.dart';
import '../../../../routes/flux_navigate.dart';
class BlogListItem extends StatelessWidget {
final Blog blog;
const BlogListItem({#required this.blog});
#override
Widget build(BuildContext context) {
var screenWidth = MediaQuery.of(context).size.width;
if (blog.id == null) return const SizedBox();
return InkWell(
onTap: () => FluxNavigate.pushNamed(
RouteList.detailBlog,
arguments: blog,
),
child: Container(
padding: const EdgeInsets.only(right: 15, left: 15),
child: Column(
children: <Widget>[
const SizedBox(height: 20.0),
ClipRRect(
borderRadius: BorderRadius.circular(3.0),
child: Tools.image(
url: blog.imageFeature,
width: screenWidth,
height: screenWidth * 0.5,
fit: BoxFit.fitWidth,
size: kSize.medium,
),
),
SizedBox(
height: 30,
width: screenWidth,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
blog.date ?? '',
style: TextStyle(
fontSize: 14,
color: Theme.of(context).accentColor.withOpacity(0.5),
),
maxLines: 2,
),
const SizedBox(width: 20.0),
if (blog.author != null)
Text(
blog.author.toUpperCase(),
style: const TextStyle(
fontSize: 11,
height: 2,
fontWeight: FontWeight.bold,
),
maxLines: 2,
),
],
),
),
const SizedBox(height: 20.0),
Text(
blog.title ?? '',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
maxLines: 2,
),
const SizedBox(height: 10.0),
Text(
blog.subTitle != null
? parse(blog.subTitle).documentElement.text
: '',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
height: 1.3,
color: Theme.of(context).accentColor.withOpacity(0.8),
),
maxLines: 2,
),
const SizedBox(height: 20.0),
],
),
),
);
}
}
Edit2.:
This is the recipe_helper global class:
library globals;
String recipeSerachTerm = "";
Edit.:
This is the class of the BaseScreen:
import 'package:flutter/material.dart';
abstract class BaseScreen<T extends StatefulWidget> extends State<T> {
#override
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => afterFirstLayout(context));
}
void afterFirstLayout(BuildContext context) {}
/// Get size screen
Size get screenSize => MediaQuery.of(context).size;
}
This is class of this screen:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../common/constants.dart';
import '../../../generated/l10n.dart';
import '../../../models/entities/blog.dart';
import '../../../widgets/common/skeleton.dart';
import '../../../widgets/paging_list.dart';
import '../../base.dart';
import '../models/list_recipe_model.dart';
import '../models/search_recipe_model.dart';
import '../helpers/recipe_helper.dart' as globals;
import 'widgets/blog_list_item.dart';
class ListRecipeScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() => _ListRecipeScreenState();
}
class _ListRecipeScreenState extends BaseScreen<ListRecipeScreen> {
#override
Widget build(BuildContext context) {
key: UniqueKey();
return Scaffold(
appBar: !kIsWeb
? AppBar(
elevation: 0.1,
title: Text(
S.of(context).recipe,
style: const TextStyle(color: Colors.white),
),
leading: Center(
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: const Icon(
Icons.arrow_back_ios,
color: Colors.white,
),
),
),
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
color: Colors.white,
onPressed: () {
showSearch(
context: context,
delegate: CustomSearchDelegate(),
);
},
),
],
)
: null,
body: PagingList<ListRecipeModel, Blog>(
itemBuilder: (context, blog) => BlogListItem(blog: blog),
loadingWidget: _buildSkeleton(),
lengthLoadingWidget: 3
),
);
}
Widget _buildSkeleton() {
key: UniqueKey();
return Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
bottom: 24.0,
top: 12.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Skeleton(height: 200),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Skeleton(width: 120),
const Skeleton(width: 80),
],
),
const SizedBox(height: 16),
const Skeleton(),
],
),
);
}
}
class CustomSearchDelegate extends SearchDelegate {
#override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
icon: Icon(Icons.clear),
onPressed: () {
query = '';
},
),
];
}
#override
Widget buildLeading(BuildContext context) {
return IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
close(context, null);
},
);
}
#override
Widget buildResults(BuildContext context) {
if (query.length < 4) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: Text(
"Search term must be longer than three letters.",
),
),
],
);
}else{
globals.recipeSerachTerm = query;
}
return Scaffold(
appBar: !kIsWeb
? AppBar(
elevation: 0.1,
title: Text(
S.of(context).recipe,
style: const TextStyle(color: Colors.white),
),
leading: Center(
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: const Icon(
Icons.arrow_back_ios,
color: Colors.white,
),
),
),
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
color: Colors.white,
onPressed: () {
showSearch(
context: context,
delegate: CustomSearchDelegate(),
);
},
),
],
)
: null,
body: PagingList<SearchRecipeModel, Blog>(
itemBuilder: (context, blog) => BlogListItem(blog: blog),
loadingWidget: _buildSkeleton(),
lengthLoadingWidget: 3,
),
);
}
Widget _buildSkeleton() {
key: UniqueKey();
return Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
bottom: 24.0,
top: 12.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Skeleton(height: 200),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Skeleton(width: 120),
const Skeleton(width: 80),
],
),
const SizedBox(height: 16),
const Skeleton(),
],
),
);
}
#override
Widget buildSuggestions(BuildContext context) {
// This method is called everytime the search term changes.
// If you want to add search suggestions as the user enters their search term, this is the place to do that.
return Column();
}
}
Solution
#VORiAND is using the Library Provider.
The value watched in the Consumer is a List of Objects.
To 'force' the re-draw of the view, he had to either
Set his list of Objects to null, notify the listeners, update his list, notify the listeners.
_list = null;
notifyListeners();
_list = await fetchDatasFromService();
notifyListeners();
or
Re-create a new List Object and notify the Listeners
final datasFromService = await fetchDatasFromService();
_list = List.from(datasFromService);
notifyListeners();
Original Answer:
There are multiple ways to refresh a view after some data manipulation.
Without any State Management library :
If you're developing in 'vanilla' : you'll have to execute your data operations and then 'force' a refresh of the UI once it's done.
The method to use in order to refresh the UI is setState((){});
Note : For this to work, you HAVE to be in a StatefulWidget
Here is a fully working example :
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
#override
void initState() {
super.initState();
//Triggering my async loading of datas
calculateCounter().then((updatedCounter){
//The `then` is Triggered once the Future completes without errors
//And here I can update my var _counter.
//The setState method forces a rebuild of the Widget tree
//Which will update the view with the new value of `_counter`
setState((){
_counter = updatedCounter;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Current counter value:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
);
}
Future<int> calculateCounter() async {
//Demo purpose : it'll emulate a query toward a Server for example
await Future.delayed(const Duration(seconds: 3));
return _counter + 1;
}
}
Important note : Consider triggering your async requests in the initState or in your afterFirstLayout methods.
If you trigger it in the build method you'll end up with unwanted loops.
The above solution will work as long as you want to update the Widget which triggered the request.
If you want to update the ListRecipeScreen widget after some data manipulation in your CustomSearchDelegate, you'll have to call the setState method IN the ListRecipeScreen.
To trigger this setState in the parent Widget, you could use a Callback method.
In the following example, MyHomePage would be your ListRecipeScreen and OtherWidget would be your CustomSearchDelegate
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Current counter value:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
OtherWidget(callback: (counterValue) {
//This callback can be called any time by the OtherWidget widget
//Once it's trigger, the method I'm writing in will be triggered.
//Since I want to update my Widget MyHomePage, I call setState here.
setState(() {
_counter = counterValue;
});
})
],
),
),
);
}
}
class OtherWidget extends StatefulWidget {
const OtherWidget({required this.callback, Key? key}) : super(key: key);
final Function(int counter) callback;
#override
State<OtherWidget> createState() => _OtherWidgetState();
}
class _OtherWidgetState extends State<OtherWidget> {
#override
void initState() {
super.initState();
//Triggering my async loading of datas
calculateCounter().then((updatedCounter) {
//The `then` is Triggered once the Future completes without errors
//And here I can trigger the Callback Method.
//You can call here the Callback method passed as parameter,
//Which will trigger the method written in the parent widget
widget.callback(updatedCounter);
});
}
#override
Widget build(BuildContext context) {
return Container();
}
Future<int> calculateCounter() async {
//Demo purpose : it'll emulate a query toward a Server for example
await Future.delayed(const Duration(seconds: 3));
return 12;
}
}
Note: It looks like your delegate is updating a value stored as a Global variable.
In this case, you don't even need to create a Callback method with a parameter (like I did in the OtherWidget : you could simply use a Function without any params, or a VoidCallback
With a State Management Library
As you can see with my answer above, it's not that hard to refresh a view after some data manipulations.
But what if you have to refresh a Widget which isn't a direct parent of the Widget manipulating the datas ?
You could use a cascade of Callbacks (don't do that please) or an InheritedWidget, but those two solutions will get harder to maintain as your project grows.
For this reason, there are a lot of State Management libraries which were developed.
The following example showcases how it'd work with the Library Provider :
I create a Controller for my page which will manipulate my datas.
This controller extends ChangeNotifier so I can notify when the manipulation is done.
class HomePageController extends ChangeNotifier {
// I exported your global var in this Controller
String _searchTerms = '';
String get searchTerms => _searchTerms;
Future<void> calculateCounter() async {
//Demo purpose : it'll emulate a query toward a Server for example
await Future.delayed(const Duration(seconds: 3));
//Updating the class variable
_searchTerms = 'New value entered by the user';
//Method provided by the ChangeNotifier extension
//It'll notify all the Consumers that a value has been changed
notifyListeners();
}
}
Injection of the Controller in the Widgets Tree and Consuming of the value it holds.
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
//Injecting our HomePageController in the tree, and listening to it's changes
body: ChangeNotifierProvider<HomePageController>(
create: (_) => HomePageController(),
builder: (context, _) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Current counter value:',
),
//The Consumer listens to every changes in the HomePageController
//It means that every time the notifyListeners() is called
//In the HomePageController, the cildren of the Consumer
//Will check if they have to be re-drawn
Consumer<HomePageController>(
builder: ((_, controller, __) {
return Text(
controller.searchTerms,
style: Theme.of(context).textTheme.headline4,
);
}),
),
const OtherWidget()
],
),
);
},
),
);
}
}
In the child widget, I retrieve a reference to my HomePageController and trigger the async request.
Once the data manipulation is done, the notifyListeners() method will trigger every Consumer<HomePageController>
class OtherWidget extends StatefulWidget {
const OtherWidget({Key? key}) : super(key: key);
#override
State<OtherWidget> createState() => _OtherWidgetState();
}
class _OtherWidgetState extends State<OtherWidget> {
#override
void initState() {
super.initState();
//Getting the instance of the HomePageController defined in the parent widget
final parentController = Provider.of<HomePageController>(context, listen: false);
//Triggering the data manipulation
parentController.calculateCounter();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
The code above is specific to the Provider lib, but the logic is similar in every State Management library :)
To make the widget "redraw", you need to call the setState() method like this:
setState(() {
// Here you can fix widget vars values;
});
For that you just need to call setState((){}), this will notify the framework that the internal state of the object has changed and will redraw the widget.
Documentation SetState
So your listing inside any dialog box because haven't run anything but i guess i know the answer please let me know are your doing in any dialog or in main screen.
So if you are showing into the any dialog then i have added a code for example like you need to statefulbuilder which is comes with it's own setState for inner rebuilt the inner UI
int i = 0;
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Center(
child: GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (c) {
return StatefulBuilder(builder: (context, setStateInner) {
return Dialog(
backgroundColor: Colors.transparent,
elevation: 0,
child: InkWell(
onTap: () {
setStateInner(() {
++i;
print("$i");
});
},
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
//width: 100,
color: Theme.of(context)
.dialogBackgroundColor,
padding: const EdgeInsets.all(15),
child: Column(
children: <Widget>[Text("$i")]))
],
),
)));
});
});
},
child: Text("Tap me"),
),
),
),
);
}
I have a list of cards and each card has a long press function which when clicked, pops up an alert dialog. I would like the card to change color based on the option chosen in the alert dialog. My alert dialog has 3 options:
Completed (Card should change to color green),
In Progress ( Color orange),
Cancel (Color grey).
At first, when the screen loads, it should show list of cards each painted the color based on the value saved in the database. Then, when the user long presses a card and chooses an option from the alert dialog, the card's color should change based on the chosen option. Only that particular card's color should change.
I have read somewhere that this might be achievable using valuechangenotifier. So here's what I did so far:
First I created my changenotifier class like below:
import 'package:flutter/material.dart';
class ColorChanger with ChangeNotifier{
Color _color = Colors.white;
ColorChanger(this._color);
getColor() => _color;
setTheme (Color color) {
_color = color;
notifyListeners();
}
}
Then I used it in my dart class. However, the color does not seem to change. What am I missing here?
class OrderItem extends StatefulWidget {
final ord.OrderItem order;
OrderItem(this.order);
#override
_OrderItemState createState() => _OrderItemState();
}
class _OrderItemState extends State<OrderItem> {
var _expanded = false;
var mycolor = Colors.white;
#override
Widget build(BuildContext context) {
ColorChanger _color = Provider.of<ColorChanger>(context);
var listProducts = widget.order.products;
return Card(
color: widget.order.orderStatus=='completed'
?Colors.lightGreen:widget.order.orderStatus=='inprogress'?
Colors.orangeAccent:
widget.order.orderStatus=='cancelled'?Colors.grey:mycolor,
margin: EdgeInsets.all(10),
child: Column(
children: <Widget>[
ListTile(
title: RichText(
text: new TextSpan(
style: new TextStyle(
fontSize: 14.0,
color: Colors.black,
),
children: <TextSpan>[
new TextSpan(
text: 'Order Number : ',
style: new TextStyle(fontWeight: FontWeight.bold)),
new TextSpan(text: widget.order.uniqueOrderNumber),
],
),
),
trailing: IconButton(
icon: Icon(_expanded ? Icons.expand_less : Icons.expand_more),
onPressed: () {
setState(() {
_expanded = !_expanded;
});
},
),
onLongPress: toggleSelection,
),
],
),
);
}
void toggleSelection() {
ColorChanger _color = Provider.of<ColorChanger>(context,listen:false);
Widget completeOrder = FlatButton(
child: Text('Completed'),
onPressed: () async {
try {
Navigator.of(context).pop(true);
// setState(() {
_color.setTheme(Colors.lightGreen);
// });
await Provider.of<Orders>(context, listen: false)
.updateOrder(widget.order,'completed');
} catch (error) {
}
});
Widget startOrder = FlatButton(
child: Text('In progress'),
onPressed: () async {
try {
Navigator.of(context).pop(true);
// setState(() {
_color.setTheme(Colors.orangeAccent);
//});
//Update Db to mark order in progress
await Provider.of<Orders>(context, listen: false)
.updateOrder(widget.order,'inprogress');
} catch (error) {
}
});
Widget cancelOrder = FlatButton(
child: Text('Cancel'),
onPressed: () async {
try {
Navigator.of(context).pop(false);
// setState(() {
_color.setTheme(Colors.grey);
// });
//Update Db to mark order as cancelled
await Provider.of<Orders>(context, listen: false)
.updateOrder(widget.order,'cancelled');
} catch (error) {
}
});
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text('Take Action'),
content: Text('What do you want to do with the order?'),
actions: <Widget>[
startOrder,
completeOrder,
cancelOrder
],
),
);
});
}
}
SECOND TRY based on Loren's answer.
import 'package:flutter/material.dart';
class ColorChanger with ChangeNotifier{
Color color = Colors.white;
setTheme (Color newColor) {
color = newColor;
notifyListeners();
}
}
class OrderItem extends StatefulWidget {
final ord.OrderItem order;
OrderItem(this.order);
#override
_OrderItemState createState() => _OrderItemState();
}
class _OrderItemState extends State<OrderItem> {
var _expanded = false;
//Set the color based on what was last saved in the DB
void didChangeDependencies() async {
var colorChanger = Provider.of<ColorChanger>(context, listen: false);
if(widget.order.orderStatus=='completed')
colorChanger.setTheme(Colors.lightGreen);
else if(widget.order.orderStatus=='inprogress')
colorChanger.setTheme(Colors.orangeAccent);
else if(widget.order.orderStatus=='cancelled')
colorChanger.setTheme(Colors.grey);
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
var listProducts = widget.order.products;
return Consumer<ColorChanger>(
builder: (context, colorChanger, child) {
return Card(
color: widget.order.orderStatus=='completed'
?Colors.lightGreen:widget.order.orderStatus=='inprogress'?
Colors.orangeAccent:
widget.order.orderStatus=='cancelled'?Colors.grey:mycolor,
margin: EdgeInsets.all(10),
child: Column(
children: <Widget>[
ListTile(
title: RichText(
text: new TextSpan(
style: new TextStyle(
fontSize: 14.0,
color: Colors.black,
),
children: <TextSpan>[
new TextSpan(
text: 'Order Number : ',
style: new TextStyle(fontWeight: FontWeight.bold)),
new TextSpan(text: widget.order.uniqueOrderNumber),
],
),
),
trailing: IconButton(
icon: Icon(_expanded ? Icons.expand_less : Icons.expand_more),
onPressed: () {
setState(() {
_expanded = !_expanded;
});
},
),
onLongPress: toggleSelection,
),
],
),
)};
}
void toggleSelection() {
ColorChanger _color = Provider.of<ColorChanger>(context,listen:false);
Widget completeOrder = FlatButton(
child: Text('Completed'),
onPressed: () async {
try {
Navigator.of(context).pop(true);
// setState(() {
_color.setTheme(Colors.lightGreen);
// });
await Provider.of<Orders>(context, listen: false)
.updateOrder(widget.order,'completed');
} catch (error) {
}
});
Widget startOrder = FlatButton(
child: Text('In progress'),
onPressed: () async {
try {
Navigator.of(context).pop(true);
// setState(() {
_color.setTheme(Colors.orangeAccent);
//});
//Update Db to mark order in progress
await Provider.of<Orders>(context, listen: false)
.updateOrder(widget.order,'inprogress');
} catch (error) {
}
});
Widget cancelOrder = FlatButton(
child: Text('Cancel'),
onPressed: () async {
try {
Navigator.of(context).pop(false);
// setState(() {
_color.setTheme(Colors.grey);
// });
//Update Db to mark order as cancelled
await Provider.of<Orders>(context, listen: false)
.updateOrder(widget.order,'cancelled');
} catch (error) {
}
});
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text('Take Action'),
content: Text('What do you want to do with the order?'),
actions: <Widget>[
startOrder,
completeOrder,
cancelOrder
],
),
);
});
}
}
When I do it this way, it changes the color of all the cards instead of just that one card. What am I doing wrong here?
Sharing order.dart
class OrderItem {
final String id;
final double amount;
final int deliveryFee;
final List<CartItem> products;
final DateTime dateTime;
final String deliveryMethod;
final String uniqueOrderNumber;
final String orderStatus;
final String userId;
final String customMessage;
final String customerName;
final String phoneNumber;
OrderItem(
{#required this.id,
#required this.amount,
#required this.products,
#required this.dateTime,
#required this.deliveryMethod,
#required this.uniqueOrderNumber,
#required this.isOrderComplete,
this.orderStatus,
#required this.customMessage,
#required this.deliveryFee,
this.customerName,
this.phoneNumber,
#required this.userId});
}
class Orders with ChangeNotifier {
final String authToken;
final String userId;
Orders(this.authToken, this.userId);
List<OrderItem> _orders = [];
List<OrderItem> get orders {
return [..._orders];
}
Future<void> updateOrder(OrderItem order,String orderStatus) async {
final id = order.id;
final customerId = order.userId;
final url =
'https://cv.firebaseio.com/orders/$customerId/$id.json?auth=$authToken';
try {
await http.patch(url,
body: json.encode({
'orderStatus':orderStatus
}));
} catch (error) {
print(error);
}
notifyListeners();
}
UPDATED ANSWER:
So when trying to do this with Provider I kept getting errors that would have required me to keep bugging you for more and more code to try and replicate everything you have going on, and I didn't want to get into that.
So this solution may or may not be acceptable to you because it uses GetX State Management, but it works. In addition it doesn't require wrapping your whole app in provider widgets so dealing with scope etc...is a non issue.
Let's add a statusColor property to your OrderItem model. This is what will get changed.
Color statusColor = Colors.white; // or whatever you you want the default color to be
Your updated Orders class that uses GetX instead of ChangeNotifier (again, not because Provider can't do this, but because I was dealing with too many errors and frankly GetX is easier in my opinion anyway)
class Orders extends GetxController {
final String authToken;
final String userId;
Orders(this.authToken, this.userId);
List<OrderItem> orders = []; // back to what I said earlier about no point in getters and setters here
// temp function just to test this on my end
void addOrder(OrderItem order) {
orders.add(order);
update();
}
// this loops through the list to find the matching order number,
// then updates the color for just that order
void updateOrderStatusColor({OrderItem updatedOrder, String status}) {
for (final order in orders) {
if (order.uniqueOrderNumber == updatedOrder.uniqueOrderNumber) {
switch (status) {
case 'completed':
{
order.statusColor = Colors.greenAccent;
}
break;
case 'inprogress':
{
order.statusColor = Colors.orangeAccent;
}
break;
case 'cancelled':
{
order.statusColor = Colors.grey;
}
break;
}
}
}
update(); // equivelent of notifyListeners();
}
// ...the rest of your class
}
A few small changes to your card. didChangeDependencies can go away entirely.
// it seems like you had 2 classes with the same name, which is not recommended
class OrderItemCard extends StatefulWidget {
final OrderItem order;
OrderItemCard(this.order);
#override
_OrderItemCardState createState() => _OrderItemCardState();
}
class _OrderItemCardState extends State<OrderItemCard> {
var _expanded = false;
final controller = Get.find<Orders>(); // equivilent of Provider.of... finds the same instance without needing context
void toggleSelection() {
Widget completeOrder = TextButton(
child: Text('Completed'),
onPressed: () async {
try {
Navigator.of(context).pop(true);
controller.updateOrderStatusColor(
updatedOrder: widget.order, status: 'completed'); // calling new function here
} catch (error) {}
});
Widget startOrder = FlatButton(
child: Text('In progress'),
onPressed: () async {
try {
Navigator.of(context).pop(true);
controller.updateOrderStatusColor(
updatedOrder: widget.order, status: 'inprogress');
} catch (error) {}
});
Widget cancelOrder = FlatButton(
child: Text('Cancel'),
onPressed: () async {
controller.updateOrderStatusColor(
updatedOrder: widget.order, status: 'cancelled');
try {
Navigator.of(context).pop(false);
} catch (error) {}
});
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text('Take Action'),
content: Text('What do you want to do with the order?'),
actions: <Widget>[startOrder, completeOrder, cancelOrder],
),
);
}
#override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.all(10),
color: widget.order.statusColor, // new color property added to your model
child: Column(
children: <Widget>[
ListTile(
title: RichText(
text: new TextSpan(
style: new TextStyle(
fontSize: 14.0,
color: Colors.black,
),
children: <TextSpan>[
new TextSpan(
text: 'Order Number : ${widget.order.uniqueOrderNumber} ',
style: new TextStyle(fontWeight: FontWeight.bold)),
],
),
),
trailing: IconButton(
icon: Icon(_expanded ? Icons.expand_less : Icons.expand_more),
onPressed: () {
setState(() {
_expanded = !_expanded;
});
},
),
onLongPress: toggleSelection,
),
],
),
);
}
}
Not sure what you have going on in your UI but here's a quick demo of how it would work in GetX. It's a simple ListView.builder populated from the orders list from the GetX Class. The GetBuilder<Orders> widget rebuilds when update() is called. Also a simple button that adds a dummy item for demo purposes. I don't know how you're generating your unique order # but I'm just using the list index for this. Both inside a column within a scaffold on a demo page.
// Equivilent of Consumer but doesn't need context nor any provider widget above it
GetBuilder<Orders>(
builder: (controller) => Expanded(
child: ListView.builder(
itemCount: controller.orders.length,
itemBuilder: (context, index) =>
OrderItemCard(controller.orders[index])),
),
),
TextButton(
onPressed: () {
final controller = Get.find<Orders>();
final orderItem = OrderItem(
orderStatus: ' ',
uniqueOrderNumber: controller.orders.length
.toString(), // just a hack to generate a unique order # for demo
);
controller.addOrder(orderItem);
},
child: Text('Add Item'),
)
Last thing is just initializing the GetX Controller. It can be done anywhere as long as its before you try and use it.
void main() {
// initialing the GetX GetxController
// not sure how you're generating the required auth and user id
// but I'm just passing in empty strings for now
Get.put(Orders('', ''));
runApp(MyApp());
}
So if you're open to GetX here, you can leave Provider for any other ChangeNotifier classes you may have in place if you want. For this you would just need to replace any Consumer<Orders> with GetBuilder<Order> and then get rid of the Provider<Orders>(create:... widget entirely.
OLD ANSWER:
You're missing a couple things in order to be using Provider properly and get the color changing the way you want.
For starters, your Card needs to be wrapped in a Consumer widget that gets notified of changes and rebuilds its children. Inside the Consumer, you need to be using the color property of the ChangeNotifier class. It doesn't need to know or care about the orderStatus because you're already explicitly telling it to change color when you call the setTheme method.
Consumer<ColorChanger>( // this is what rebuilds and changes the color
builder: (context, colorChanger, child) {
return Card(
color: colorChanger.color, // colorChanger here is equivalent of declaring final colorChanger = Provider.of<ColorChanger>(context...
child: Column(
children: <Widget>[
ListTile(
title: RichText(
text: new TextSpan(
style: new TextStyle(
fontSize: 14.0,
color: Colors.black,
),
children: <TextSpan>[
new TextSpan(
text: 'Order Number : ',
style: new TextStyle(fontWeight: FontWeight.bold)),
new TextSpan(text: widget.order.uniqueOrderNumber),
],
),
),
trailing: IconButton(
icon: Icon(_expanded ? Icons.expand_less : Icons.expand_more),
onPressed: () {
setState(() {
_expanded = !_expanded;
});
},
),
onLongPress: toggleSelection,
),
],
),
);
});
Next, see this link as to why you're not gaining anything with using the private _color and public getColor in your ChangeNotifier class.
So lets simplify that a bit.
class ColorChanger with ChangeNotifier {
Color color = Colors.white;
ColorChanger(this.color);
setTheme(Color newColor) {
color = newColor;
notifyListeners();
}
}
Now, whenever you call the setTheme function from your dialog, that card will change to whatever color you pass into it because the Consumer widget is notified, and will rebuild with the updated color value of the ChangeNotifier class.
Something like this would be the simplest way to the thing you want to achieve:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// define a list of colors:
final colors = <Color>[
Colors.white, // this is the inital color
Colors.green,
Colors.orange,
Colors.grey
];
int index = 0;
Future<int> showMyDialog(BuildContext context) async {
// Since all Navigator.push(...) and showDialog(...) calls are futures
// we can send values alongside them when we pop the context:
// final value = await Navigator.push(...);
// or
// final value = await showDialog(...);
// then we do a:
// Navigator.pop(context, SOME_VALUE,);
// the value variable will be assigned to the one we sent
return await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Take Action'),
content: Text('What do you want to do with the order?'),
actions: <Widget>[
TextButton(
child: Text('Completed',
style: TextStyle(
color: Colors.green,
)),
onPressed: () => Navigator.pop(context, 1)),
TextButton(
child: Text('In progress',
style: TextStyle(
color: Colors.orange,
)),
onPressed: () => Navigator.pop(context, 2)),
TextButton(
child: Text('Cancel',
style: TextStyle(
color: Colors.grey,
)),
onPressed: () => Navigator.pop(context, 3)),
],
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: <Widget>[
Card(
color: colors[index],
child: Container(width: 50, height: 50),
),
ElevatedButton(
child: Text('Show dialog'),
onPressed: () async {
// call the showMyDialog function, it returns
// a future int so we have to await it
final int _index = await showMyDialog(context);
// if the returned value (_index) is null we use
// the old one value to avoid erros in the code
setState(() => index = _index ?? index);
}),
]),
);
}
}
A very simple workaround would be to declare a global color variable cardColor and assign it to the color property of the card. Then on the alertdialog, change the 'onChange'or 'onTap' property of the widget so that on tapping, the widget changes the value of the global variable cardColor to a different color. Don't forget to do the final step i.e. changing the value of the variable, inside setState()
The best way to achieve it by using AwesomeDialog
https://pub.dev/packages/awesome_dialog
AwesomeDialog(
context: context,
dialogType: DialogType.INFO,
animType: AnimType.BOTTOMSLIDE,
title: 'Dialog Title',
desc: 'Dialog description here.............',
btnCancelOnPress: () {},
btnOkOnPress: () {},
)..show();
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.
I have a form (a bunch of TextField widgets), i get the data from a REST API and load it to the view using a ViewModelProvider.
Now i want to press a button "Save", get the text value of the TextField widgets and pass the data to the REST API.
I searched for how to get the value of the TextField to use it elsewhere and all i found was similar to this :
class _MyCustomFormState extends State<MyCustomForm> {
// Create a TextEditingController as a variable
final myController = TextEditingController();
#override
void dispose() {
myController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Retrieve Text Input'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
// Set the created TextEditingController as the controller of the Text Field
controller: myController,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
// Use the text attribute of the TextEditingController variable
content: Text(myController.text),
);
},
);
},
tooltip: 'Show me the value!',
child: Icon(Icons.text_fields),
),
);
}
}
(This code snippet was taken from the Flutter website)
My problem is that i use a ViewModelProvider and i need to create the TextEditingController inside the
builder attribute of the ViewModelProvider to set the text that was retreived from the HTTP call.
Here is my current code, i left only one TextField because they are practically the same.
class SettingsView extends StatelessWidget {
const SettingsView({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ViewModelProvider<SettingsViewModel>.withConsumer(
viewModel: SettingsViewModel(),
onModelReady: (model) => model.getSettings(),
builder: (context, model, child) => Center(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
TextField(
decoration: new InputDecoration(
labelText: 'Host',
),
controller: TextEditingController.fromValue(
TextEditingValue(
text: model.settings == null? '' :model.settings.host,
),
),
),
GestureDetector(
child: CallToAction("Save"),
onTap: saveData,
),
],
),
),
),
);
}
void saveData() {
}
}
So the question is, how to pass the text value of the TextField to the saveData method?
Thank you.
Are you trying to init the TextEditingController?
If so, you can initialize the text of the controler on onModelReady, and declare the controler outside of build.
class _SettingsViewState extends State<SettingsView> {
TextEditingController myController = TextEditingController();
#override
Widget build(BuildContext context) {
return ViewModelProvider<SettingsViewModel>.withConsumer(
viewModel: SettingsViewModel(),
onModelReady: (model) {
myController.text =
model.settings == null ? '' : model.settings.host;
model.getSettings();
},
builder: (context, model, child) => Center(
child: Column(
children: <Widget>[
Expanded(
child: TextField(
decoration: new InputDecoration(labelText: 'Host'),
controller: myController,
),
),
Expanded(
child: GestureDetector(
child: CallToAction("Save"),
onTap: saveData,
),
),
],
),
),
);
}
void saveData() {
print('save data ${myController.text}');
}
}
Thanks to Thiago Dong Chen i managed to find the solution to my problem.
Thiago suggested that i create a TextEditingController variable and set its text value in onModelReady as follows
onModelReady: (model) {
myController.text = model.settings == null ? '' : model.settings.host;
model.getSettings();
},
The problem is even in onModelReady the model.settings is still null. But his solution put me in the right path. I'm new to Flutter, Dart and the anonymous functions. Seeing that i can change the => to a more classic {} made me realize that i can do the same in builder and do more that just setting the widget tree.
Here is the full code :
class SettingsView extends StatefulWidget {
const SettingsView({Key key}) : super(key: key);
#override
_SettingsViewState createState() => _SettingsViewState();
}
class _SettingsViewState extends State<SettingsView> {
TextEditingController _hostController = TextEditingController();
#override
Widget build(BuildContext context) {
return ViewModelProvider<SettingsViewModel>.withConsumer(
viewModel: SettingsViewModel(),
onModelReady: (model) => model.getSettings(),
builder: (context, model, child) {
_hostController.text = model.settings == null ? null : model.settings.host;
return Center(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
TextField(
decoration: new InputDecoration(
labelText: "Host",
),
controller: _hostController,
inputFormatters: [WhitelistingTextInputFormatter.digitsOnly],
),
SizedBox(
height: 30,
),
GestureDetector(
child: CallToAction("Save"),
onTap: saveData,
)
],
),
),
);
}
);
}
void saveData() async{
var _api = locator<Api>();
bool b = await _api.setSettings(SettingItemModel(
host: _hostController.text));
String message = '';
if (b) {
message = 'Data saved to the database!';
} else {
message = 'Couldn\'t save the data';
}
final snackBar = SnackBar(content: Text(message));
Scaffold.of(context).showSnackBar(snackBar);
}
}