Related
For some weird reason, my local state variable "_jobApplicationState" is not updating.
I see that it is updated in the database, but its not updating on my page.
If I leave the record and come back, everything works as expected.
I am driving this functionality by pressing the button 'Send inquiry'.
I took out a bunch of code to make it easy to read.
I got this to work for a minute at somepoint. but I forgot to save:(
class JobApplicationView extends StatefulWidget {
const JobApplicationView({Key? key}) : super(key: key);
#override
_JobApplicationViewState createState() => _JobApplicationViewState();
}
// https://youtu.be/VPvVD8t02U8?t=90350
class _JobApplicationViewState extends State<JobApplicationView> {
CloudJobApplication? _jobApplication;
final _formKey = GlobalKey<FormState>();
final currentUser = AuthService.firebase().currentUser!;
late final FirebaseCloudStorage _firebaseService;
//
late String _jobApplicationState;
//
late DateTime _jobApplicationStartDate;
late DateTime _jobApplicationEndDate;
//
bool? isJobCreatorSameAsJobApplicator;
String? _jobCreatorId;
String? _jobApplicatorId;
String? _jobDescription;
List? _jobUserData;
String? _jobAddress;
String? _jobType;
//
#override
void initState() {
super.initState();
_jobApplicationStartDate = DateTime.now();
_jobApplicationEndDate = DateTime.now();
_firebaseService = FirebaseCloudStorage();
// _jobDescriptionController = TextEditingController();
// _jobAreaCodeController = TextEditingController();
// _jobApplicationStateController = TextEditingController();
}
//Future<CloudJobApplication>
createOrGetExistingJob(BuildContext context) async {
final widgetJobApplication = context.getArgument<CloudJobApplication>();
if (widgetJobApplication != null) {
_jobApplication = widgetJobApplication;
_jobApplicationState = widgetJobApplication.jobApplicationState;
_jobApplicatorId = widgetJobApplication.jobApplicatorId;
_jobCreatorId = widgetJobApplication.jobCreatorId;
_jobDescription = widgetJobApplication.jobApplicationDescription;
return widgetJobApplication;
}
print('ELSE TRIGGERED!');
return widgetJobApplication;
}
void _updateJobField(localStateField, jobColumn, jobColumnValue) async {
//* localStateField: local field to update so that the build context is refreshed
//* jobColumn: the name of the column in the db
//* jobColumnValue: the value for the jobColumn
setState(() {
if (localStateField == '_jobApplicationState') {
_jobApplicationState = jobColumnValue;
}
});
await _firebaseService.updateJobApplicationColumn(
documentId: _jobApplication?.documentId as String,
fieldNameColumn: jobColumn,
fieldNameColumnValue: jobColumnValue,
);
}
sendInqury() {
print('setting job applications state!');
print('_jobApplicationState b4:: $_jobApplicationState');
_updateJobField(_jobApplicationState, jobApplicationStateColumn,
jobApplicationStateOpen);
print('_jobApplicationState after:: $_jobApplicationState');
}
#override
void dispose() {
//_deleteJobIfTextIsEmpty();
// _jobDescriptionController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('update job application'),
actions: [],
),
body: FutureBuilder(
future: createOrGetExistingJob(context),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
return Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(32.0),
children: [
//getStateChevrons(_jobApplicationState),
const Divider(
height: 20,
thickness: 5,
indent: 0,
endIndent: 0,
color: Colors.blue,
),
Text(_jobApplicationState),
TextButton(
style: TextButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: Colors.blue,
padding: const EdgeInsets.all(16.0),
textStyle: const TextStyle(fontSize: 20),
),
onPressed: sendInqury,
child: const Text('Send inquiry'),
)
],
),
);
default:
return const CircularProgressIndicator();
}
},
),
);
}
}
I figured out the answer, here is the answer code:
import 'dart:developer';
import 'package:flutter/material.dart';
import '../../services/cloud/cloud_job_application.dart';
import '/services/auth/auth_service.dart';
import '/utilities/generics/get_arguments.dart';
import '/services/cloud/firebase_cloud_storage.dart';
class JobApplicationView extends StatefulWidget {
const JobApplicationView({Key? key}) : super(key: key);
#override
_JobApplicationViewState createState() => _JobApplicationViewState();
}
// https://youtu.be/VPvVD8t02U8?t=90350
class _JobApplicationViewState extends State<JobApplicationView> {
CloudJobApplication? _jobApplication;
late final FirebaseCloudStorage cloudFunctions;
final _formKey = GlobalKey<FormState>();
final currentUser = AuthService.firebase().currentUser!;
// state varibles
String _jobApplicationState = 'default';
String _jobApplicationSubState = 'default';
late final TextEditingController _jobDescriptionController;
#override
void initState() {
super.initState();
cloudFunctions = FirebaseCloudStorage();
_jobDescriptionController = TextEditingController();
}
//Future<CloudJobApplication>
getExistingJobApplication(BuildContext context) async {
log('getExistingJobApplication()');
if (_jobApplicationState == 'default') {
var widgetJobApplication = context.getArgument<CloudJobApplication>();
log('first time openning job application, returning server data');
_jobApplication = widgetJobApplication;
_jobApplicationState =
widgetJobApplication?.jobApplicationState as String;
_jobDescriptionController.text =
widgetJobApplication?.jobApplicationDescription as String;
return widgetJobApplication;
} else {
log('job application has been updated, returnnig local data');
return cloudFunctions.getJobApplication(_jobApplication!.documentId);
}
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('update job application'),
actions: [],
),
body: FutureBuilder(
future: getExistingJobApplication(context),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
return Form(
key: _formKey,
child: ListView(padding: const EdgeInsets.all(32.0), children: [
Text(_jobApplicationState),
Text(_jobDescriptionController.text),
const Divider(
height: 20,
thickness: 5,
indent: 0,
endIndent: 0,
color: Colors.blue,
),
TextFormField(
controller: _jobDescriptionController,
maxLines: 5,
decoration: InputDecoration(
// enabled: _jobState == jobStateNew ? true : false,
hintText: "The toilet wont flush",
filled: true,
// fillColor: _jobState == jobStateNew ? Colors.white : Colors.grey,
label: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
color: Colors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
Padding(padding: EdgeInsets.only(left: 8.0)),
Icon(Icons.info_outline),
Padding(
padding: EdgeInsets.only(left: 8.0, right: 8.0),
child: Text("Job description"),
),
],
),
),
),
validator: (str) =>
str == '' ? "Job description can't be empty" : null,
),
TextButton(
onPressed: () async {
setState(() {
_jobApplicationState = 'Open';
});
await cloudFunctions.updateJobApplication(
documentId: _jobApplication?.documentId as String,
jobDescription: _jobDescriptionController.text,
jobApplicationState: 'Open',
);
},
child: const Text('update state')),
//
]),
);
default:
return const CircularProgressIndicator();
}
},
),
);
}
}
You should separate the UI and logic -> create a jobApplication Model.
Pack all your logic into a ChangeNotifier and notifyListeners on change.
This is also better for performance because it only rebuilds needed parts of the UI.
I can recommend using a ChangeNotifierProvider.
class JobApplicationProvider extends ChangeNotifier {
JobApplication jobapplication = BasicParam.standard;
void setJobApplication(json) async {
jobapplication = JobApplication.fromJson(json);
notifyListeners();
}
}
And in the build Method use it like this:
Widget build(BuildContext context) {
JobApplicationProvider jobApplication= Provider.of(context);
return Text(jobApplication.state);
}
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"),
),
),
),
);
}
How can I send the data I get from the API to the other pages? Before using getx i was sending with "widget.bla bla" but now i don't know how can i send it.
class HomePage extends StatelessWidget {
final AllCoinController allCoinController = Get.put(AllCoinController());
#override
Widget build(BuildContext context) {
return Scaffold(
body: Obx(
() => ListView.builder(
scrollDirection: Axis.vertical,
itemCount: allCoinController.coinList.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: () {
Get.to(CoinContent());
},
child: Container(
color: Colors.grey[700],
width: 150,
child: Row(
children: [
SizedBox(
width: 50,
height: 50,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Image.network(
allCoinController.coinList[index].image),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(allCoinController.coinList[index].name),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(allCoinController.coinList[index].symbol),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(allCoinController
.coinList[index].currentPrice
.toString()),
),
],
),
),
),
);
},
),
),
);
}
}
The page I want to send the data to:
class CoinContent extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("coin name"),
),
body: Obx(
() => Center(
child: Column(
children: [
Text("coin data 1"),
Text("coin data 2"),
Text("coin data 3"),
Text("coin data 4"),
Text("coin data 5"),
Text("coin data 6"),
],
),
),
),
);
}
}
And my last question codes are not found automatically when using Getx. Example:
Text(allCoinController.coinList[index].currentPrice.toString()),
I get the same data without using getx and there was no problem. But when using Getx the "currentPrice" code is not automatically found and does not appear. I need to copy the code to write.
My controller:
import 'dart:async';
import 'package:coin_finder/models/btc_eth_bnb_model.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
class AllCoinController extends GetxController {
var coinList = [].obs;
final url = Uri.parse("api url")
Future callAllCoins() async {
try {
final response = await http.get(url);
if (response.statusCode == 200) {
List<dynamic> values = [];
values = allCoinsFromJson(response.body);
coinList.assignAll(values);
if (values.length > 0) {
for (int i = 0; i < values.length; i++) {
if (values[i] != null) {
coinList.add(values[i]);
}
}
}
return coinList;
} else {
print(response.statusCode);
}
} catch (e) {
print(e.toString());
}
}
#override
void onInit() {
callAllCoins();
Timer.periodic(Duration(minutes: 5), (timer) => callAllCoins());
super.onInit();
}
}
Model:
import 'dart:convert';
List<AllCoins> allCoinsFromJson(String str) =>
List<AllCoins>.from(json.decode(str).map((x) => AllCoins.fromJson(x)));
String allCoinsToJson(List<AllCoins> data) =>
json.encode(List<dynamic>.from(data.map((x) => x.toJson())));
class AllCoins {
AllCoins({
required this.symbol,
required this.name,
required this.image,
required this.currentPrice,
});
String symbol;
String name;
String image;
num currentPrice;
factory AllCoins.fromJson(Map<String, dynamic> json) => AllCoins(
symbol: json["symbol"],
name: json["name"],
image: json["image"],
currentPrice: json["current_price"],
);
Map<String, dynamic> toJson() => {
"symbol": symbol,
"name": name,
"image": image,
"current_price": currentPrice,
};
}
Dart version: sdk: ">=2.12.0 <3.0.0"
in home page Get.to(CoinContent(),arguments:
allCoinController.coinList[index] )
//
class CoinContent extends StatelessWidget {
#override
Widget build(BuildContext context) {
final data = ModalRoute.of(context)!.settings.arguments as
AllCoins;
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("coin name"),
),
body:Center(
child: Column(
children: [
Text("${data.name}"),
],
),
),
);
}
}
in this case we aren't using agrs in constructor
instead of routing like that Get.to(MyScreen());
you should use Get.to(MyScreen(), arguments: [1,2,3]);
you can access them by using Get.arguments it should return a List
Note: The arguments will not change while using the app until you override it by routing to another screen with args
I'm new to Flutter Redux, I got a problem and I have no idea how to deal with it at all! I extracted the main code to keep this simple - tap indicators to switch PageView, scroll PageView to synchronise the indicator. Here is my code:
app state:
class AppState {
final List menuList;
final int currentIndex;
AppState({this.menuList, this.currentIndex});
}
the reducers:
AppState appReducer(AppState state, Object action) {
return AppState(
menuList: menuListReducer(state.menuList, action),
currentIndex: currentIndexReducer(state.currentIndex, action));
}
final menuListReducer = combineReducers<List>(
[TypedReducer<List, SetMenuListAction>(_setMenuList)]);
List _setMenuList(List menuList, SetMenuListAction action) {
menuList = action.menuList;
return menuList;
}
final currentIndexReducer = combineReducers<int>(
[TypedReducer<int, SetCurrentIndexAction>(_setCurrentIndex)]);
int _setCurrentIndex(int currentIndex, SetCurrentIndexAction action) {
currentIndex = action.index;
return currentIndex;
}
the action:
class SetMenuListAction {
List menuList;
SetMenuListAction(this.menuList);
}
class SetCurrentIndexAction {
int index;
SetCurrentIndexAction(this.index);
}
the main logic:
void main() {
final store = Store<AppState>(
appReducer,
initialState: AppState(menuList: [
{
'picUrl': 'http://pic3.16pic.com/00/55/42/16pic_5542988_b.jpg',
'description': 'this is the first image'
},
{
'picUrl': 'http://photo.16pic.com/00/38/88/16pic_3888084_b.jpg',
'description': 'this is the second image'
},
{
'picUrl':
'http://img4.imgtn.bdimg.com/it/u=3434394339,2114652299&fm=214&gp=0.jpg',
'description': 'this is the third image'
},
{
'picUrl': 'http://pic1.win4000.com/pic/2/07/8c57e143b1.jpg',
'description': 'this is the fourth image'
},
], currentIndex: 0),
);
runApp(App(
store: store,
));
}
// App
class App extends StatelessWidget {
final Store<AppState> store;
const App({Key key, this.store}) : super(key: key);
#override
Widget build(BuildContext context) {
return StoreProvider(
store: store,
child: MaterialApp(title: 'Flutter redux example', home: MyDetail()),
);
}
}
class MyDetail extends StatefulWidget {
#override
_MyDetailState createState() => _MyDetailState();
}
class _MyDetailState extends State<MyDetail> with TickerProviderStateMixin {
PageController _controller;
#override
void initState() {
_controller = PageController(initialPage: 0);
super.initState();
}
#override
Widget build(BuildContext context) {
return StoreConnector<AppState, int>(
converter: (store) => store.state.currentIndex,
onDidChange: (newIdx) {
//this won't work because the _controller hasn't been attached to PageView
_controller.jumpToPage(newIdx);
},
builder: (BuildContext context, int idx) {
return StoreConnector<AppState, List>(
converter: (store) => store.state.menuList,
onDidChange: (newList) {
//maybe do something further
},
builder: (BuildContext context, List menus) {
return Container(
color: Colors.white,
child: Column(
children: <Widget>[
//pageview
Expanded(
child: PageView(
children: menus.map((item) {
return Column(
children: <Widget>[
Image.network(item['picUrl']),
Text(
item['description'],
style: TextStyle(fontSize: 24.0),
)
],
);
}).toList(),
onPageChanged: (int index) {
StoreProvider.of<AppState>(context)
.dispatch(SetCurrentIndexAction(index));
},
physics: BouncingScrollPhysics(),
),
),
//indicators
Container(
margin: EdgeInsets.only(bottom: 50.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: menus
.asMap()
.map((i, item) => MapEntry(
i,
GestureDetector(
onTap: () {
//this won't work either maybe because the widgets is rebuilding
_controller.jumpToPage(i);
StoreProvider.of<AppState>(context)
.dispatch(SetCurrentIndexAction(i));
},
child: Container(
width: 10.0,
height: 10.0,
color: i == idx
? Colors.purpleAccent
: Colors.blue,
margin: EdgeInsets.only(right: 10.0),
),
)))
.values
.toList(),
),
)
],
),
);
},
);
},
);
}
}
Sorry for the long code, but I think maybe this can help to understand my problem:
When I tap the indicator, I want to synchronise the PageView, that is _controller.jumpToPage(i), but it will show Errors. So how to make this work?
I can change the currentIndex in another screen, how to synchronise the PageView?
Is there any method to watch the state changes(separately, not the whole state) and do something?
After debugging your code I found that you are missing controller: _controller in PageView, this should fix it:
Expanded(
child: PageView(
controller: _controller,
children: menus.map((item) {
return Column(
children: <Widget>[
Image.network(item['picUrl']),
Text(
item['description'],
style: TextStyle(fontSize: 24.0),
)
],
);
}).toList(),
onPageChanged: (int index) {
StoreProvider.of<AppState>(context)
.dispatch(SetCurrentIndexAction(index));
},
physics: BouncingScrollPhysics(),
),
),