ToggleButton in Bloc - flutter

I am trying to implement a ToggleButton using the Bloc pattern provided by Flutter. The code is very simple, but there is a problem. When I try to update my index so that it can be emitted from the Bloc to change my value from bool, the whole thing will do nothing. I have tried rewriting the code to look for the problem, but my guess is that the update will not go through. Does anybody know how to solve this issue?
Here is my Bloc:
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/cupertino.dart';
part 'calculate_event.dart';
part 'calculate_state.dart';
class CalculateBloc extends Bloc<CalculateEvent, CalculateState> {
///Constructor
CalculateBloc()
: super(CalculateInitial(
selection: List.filled(3, false),
totalAmount: '',
textEditingController: TextEditingController())) {
///On-Method to emit the State
on<CalculateEvent>((event, emit) {
///UpdateSelectionEvent
if (event is UpdateSelectionEvent) {
///emit the Index of the button-press
final updateSelect = List.of(state.selection);
for (int i = 0; i < updateSelect.length; i++) {
updateSelect[i] = event.selectIndex == i;
}
emit(UpdateSelectionState(
selection: updateSelect,
totalAmount: state.TotalAmount,
tip: state.tip,
textEditingController: state.textEditingController));
}
///CalculatePercentageEvent
if (event is CalculatePercentageEvent) {
final controller = state.textEditingController;
final totalAmount = double.parse(controller.text);
final selectedIndex = state.selection.indexWhere((element) => element);
final tipPercentage = [0.1, 0.15, 0.2][selectedIndex];
final tipTotal = (totalAmount * tipPercentage).toStringAsFixed(2);
emit(CalculatePercentageState(
selection: state.selection,
totalAmount: totalAmount.toString(),
tip: tipTotal,
textEditingController: state.textEditingController));
}
});
}
}
Here is my State-class:
part of 'calculate_bloc.dart';
#immutable
abstract class CalculateState extends Equatable {
final List<bool> selection;
final String? tip;
final String TotalAmount;
final TextEditingController textEditingController;
CalculateState(
{this.tip = '',
required this.TotalAmount,
required this.selection,
required this.textEditingController});
#override
List<Object?> get props => [selection, tip, TotalAmount];
}
class CalculateInitial extends CalculateState {
CalculateInitial(
{required List<bool> selection,
String? tip = '',
required String totalAmount,
required TextEditingController textEditingController})
: super(
selection: selection,
TotalAmount: totalAmount,
textEditingController: textEditingController);
}
class CalculatePercentageState extends CalculateState {
CalculatePercentageState(
{required List<bool> selection,
String? tip = '',
required String totalAmount,
required TextEditingController textEditingController})
: super(
selection: selection,
TotalAmount: totalAmount,
tip: tip,
textEditingController: textEditingController);
}
class UpdateSelectionState extends CalculateState {
UpdateSelectionState(
{required List<bool> selection,
String? tip = '',
required String totalAmount,
required TextEditingController textEditingController})
: super(
selection: selection,
TotalAmount: totalAmount,
tip: tip,
textEditingController: textEditingController);
}
Here is my Event-class:
part of 'calculate_bloc.dart';
#immutable
abstract class CalculateEvent {}
class CalculatePercentageEvent extends CalculateEvent {}
class UpdateSelectionEvent extends CalculateEvent {
final int selectIndex;
UpdateSelectionEvent(this.selectIndex);
}
And my UI:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_day_1/bloc/calculate_bloc.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CalculateBloc(),
child: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BlocBuilder<CalculateBloc, CalculateState>(
bloc: BlocProvider.of<CalculateBloc>(context),
builder: (context, state) {
if (state.tip != null) {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text(state.tip ?? '',
style: const TextStyle(fontSize: 30)),
),
const Text('Total Amount'),
SizedBox(
width: 80,
child: TextField(
controller: state.textEditingController,
keyboardType:
const TextInputType.numberWithOptions(),
textAlign: TextAlign.center,
decoration:
const InputDecoration(hintText: '\$100.00'),
),
),
Padding(
padding: const EdgeInsets.all(20),
child: ToggleButtons(
onPressed: (index) =>
BlocProvider.of<CalculateBloc>(context)
.add(UpdateSelectionEvent(index)),
isSelected: state.selection,
children: const [
Text('10%'),
Text('15%'),
Text('20%'),
],
),
),
],
);
} else {
return Container(
child: const Text('flase'),
);
}
}),
ElevatedButton(
onPressed: () {
BlocProvider.of<CalculateBloc>(context)
.add(CalculatePercentageEvent());
},
child: const Text('Calculate Amount'),
),
],
),
),
),
);
}
}
I have tried to utalize some of the BlocListener but it will not work. My other guess is to use additionl states to emit it.

I have found the problem. On the HomePage, the BlocProvider keeps building the app with the CalculateInitialState so that the new value is not passed through.

Related

Access data from custom widget created on different class in flutter

I new to Flutter and i was trying to find a solution for the below issue for several hours. I have searched and every solution provided does not work form me.
I have page where one of the widgets is the autocomplete text input. I have created this autocomplete widget on different class. I have added this widget as StatefulBuilder within my main widget. it is working fine however, i am not able to access its value so I can store it with other fields.
My code look like
class ItemDetails extends StatefulWidget {
const ItemDetails({Key? key}) : super(key: key);
static const routeName = '/item_details';
#override
State<ItemDetails> createState() => _ItemDetails();
}
class _ItemDetails extends State<ItemDetails> {
late TextEditingController labelController;
late TextEditingController valueController;
late TextEditingController notesController;
bool _submitted = false;
late var args;
String _itemLabel2 = "";
// var labelAutoComp = LabelSugg();
#override
void initState() {
super.initState();
labelController = TextEditingController();
valueController = TextEditingController();
notesController = TextEditingController();
}
#override
void dispose() {
labelController.dispose();
valueController.dispose();
notesController.dispose();
// Hive.close();
super.dispose();
}
String? _labelErrorText(context) {
final text = labelController.value.text;
if (text.isEmpty) {
// return 'Can\'t be empty';
return AppLocalizations.of(context)!.noEmpty;
}
}
String? _valueErrorText(context) {
final text = valueController.value.text;
if (text.isEmpty) {
// return 'Can\'t be empty';
return AppLocalizations.of(context)!.noEmpty;
}
}
#override
Widget build(BuildContext context) {
try {
args = ModalRoute.of(context)!.settings.arguments as Map;
} on Exception catch (e) {
// print(e);
}
// print(args);
return Scaffold(
appBar: AppBar(
title: Text(args['title']),
),
body: Container(
padding: const EdgeInsets.all(20),
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(20),
child: Column(
// mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
LabelSugg(getLabelText: (String val) {
print(val);
_itemLabel2 = val;
}),
TextField(
autofocus: true,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.label,
hintText: AppLocalizations.of(context)!.labelHint,
errorText:
_submitted ? _labelErrorText(context) : null,
),
controller: labelController,
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 5),
TextField(
autofocus: false,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.value,
hintText: AppLocalizations.of(context)!.valueHint,
errorText:
_submitted ? _valueErrorText(context) : null,
),
controller: valueController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true, signed: false),
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(
RegExp(r"[0-9.]")),
TextInputFormatter.withFunction(
(oldValue, newValue) {
try {
final text = newValue.text;
if (text.isNotEmpty) double.parse(text);
return newValue;
} catch (e) {}
return oldValue;
}),
], // Only numbers can be entered
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 5),
TextField(
autofocus: true,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.notes,
hintText: AppLocalizations.of(context)!.noteHint,
),
controller: notesController,
onChanged: (_) => setState(() {}),
),
]),
// ],
),
Expanded(
child: Align(
alignment: FractionalOffset.bottomCenter,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () {
setState(() => _submitted = true);
if (_labelErrorText(context) == null &&
_valueErrorText(context) == null) {
//insert
var localLabel = labelController.value.text;
var _localValue = 0.0;
if (valueController.value.text != '') {
_localValue =
double.parse(valueController.value.text);
} else {
_localValue = 0.0;
}
var localNotes = notesController.value.text;
addItemToList(
localLabel, _localValue, localNotes);
Navigator.of(context).pop();
labelController.clear();
valueController.clear();
notesController.clear();
}
},
label: Text(AppLocalizations.of(context)!.add),
icon: const Icon(Icons.save, size: 18),
),
const SizedBox(width: 10),
ElevatedButton.icon(
onPressed: () => {Navigator.pop(context)},
label: Text(AppLocalizations.of(context)!.cancel),
icon: const Icon(Icons.cancel, size: 18),
),
],
)),
),
// )
],
)));
}
void addItemToList(String localLabel, double localValue, String localNotes) {
var _item = YearItems()..yearID = args['year'];
_item.itemLabel = localLabel;
_item.itemValue = localValue;
_item.itemNote = localNotes;
print(_itemLabel2);
final itemsBox = ItemsBoxes.getTransactions();
itemsBox.add(_item);
}
}
my labelAutoComp widget code look like
class LabelSugg extends StatefulWidget {
final ValueChanged<String> getLabelText;
const LabelSugg({Key? key, required this.getLabelText}) : super(key: key);
#override
State<LabelSugg> createState() => _LabelSugg();
}
class _LabelSugg extends State<LabelSugg> {
late TextEditingController fieldTextEditingController2;
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
getLabel() {
return widget.getLabelText(fieldTextEditingController2.text);
}
#override
Widget build(BuildContext context) {
List<LabelsAc> labelOptions = <LabelsAc>[
LabelsAc(label: AppLocalizations.of(context)!.labelClothes),
LabelsAc(label: AppLocalizations.of(context)!.labelFood),
LabelsAc(label: AppLocalizations.of(context)!.labelPerfumes),
LabelsAc(label: AppLocalizations.of(context)!.labelCapital),
];
return Autocomplete<LabelsAc>(
optionsBuilder: (TextEditingValue textEditingValue) {
return labelOptions
.where((LabelsAc _label) => _label.label
.toLowerCase()
.startsWith(textEditingValue.text.toLowerCase()))
.toList();
},
displayStringForOption: (LabelsAc option) => option.label,
fieldViewBuilder: (BuildContext context,
TextEditingController fieldTextEditingController,
// fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted) {
return TextField(
controller: fieldTextEditingController,
focusNode: fieldFocusNode,
style: const TextStyle(fontWeight: FontWeight.bold),
// onChanged: getLabel(),
onChanged: (String val) {
fieldTextEditingController2 = fieldTextEditingController;
getLabel();
});
},
onSelected: (LabelsAc selection) {
fieldTextEditingController2 =
TextEditingController(text: selection.label);
getLabel();
},
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<LabelsAc> onSelected,
Iterable<LabelsAc> options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
child: Container(
// width: 350,
// color: Colors.cyan,
child: ListView.builder(
padding: const EdgeInsets.all(10.0),
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final LabelsAc option = options.elementAt(index);
return GestureDetector(
onTap: () {
onSelected(option);
},
child: ListTile(
title: Text(option.label,
style: const TextStyle(color: Colors.black)),
),
);
},
),
),
),
);
},
);
// ),
// );
}
}
class LabelsAc {
LabelsAc({required this.label});
String label;
}
first is redundant when you wrap your class that extend StatefullWidget with StatefullBuilder. LabelSugg is a component Widget. you can use it like other widget.
benefit to separate widget with StatefullWidget class is, we can update the value inside the class without re-build the current page. which is good for performance. that's why developer recomend to separete with class insted compared to make local method.
as you see, when you create LabelSugg extend StatefullWidget class , we will have _LabelSugg . underscore means that: all variable only accessible on current file.
thats why we can't call getLabel() or other variable from different file.
its used for handle the State in 'LabelSugg` widget.
now how to pass the value from LabelSugg is by created variable outside the state. here you are:
class LabelSugg extends StatefulWidget {
// use this to pass any changes when we use LabelSugg
final ValueChanged<String> getLabelText;
const LabelSugg({Key? key, required this.getLabelText}) : super(key: key);
#override
State<LabelSugg> createState() => _LabelSugg();
}
then we can call the onChaged inside _LabelSugg state. because its Statefull widget, we can acces by : widget.getLabelText()
class _LabelSugg extends State<LabelSugg> {
late TextEditingController fieldTextEditingController;
.....
getLabel() {
return widget.getLabelText(fieldTextEditingController.text);
}
then in other class we call LabelSugg like common widget
import 'package:../labelsug.dart';
class ItemDetails extends StatefulWidget {
.....
return Scaffold(
appBar: AppBar(
title: Text(args['title']),
),
body: Container(
padding: const EdgeInsets.all(20),
child: Column(
children: <Widget>[
// now use it like a widget
LabelSug(
getLabelText: (String val){
print(val);
}
:)

How to refresh screen to update elements list?

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"),
),
),
),
);
}

Infinite-scroll listview.builder - to expand or not to expand... and more provider value not updating and how to fix "RenderFlex overflowed"

I am trying to build a view/route that will list items fetched from a REST source.
I want to show a notification item below the list while the data is being fetched.
But my ListView builder is constructed around the fetched data's structure, so I figured just have a ListTile fit some appropriate UX elements below the generated list inside a Column - which was kinda working great - or so I thought - until the list grows to fill the screen causing RenderFlex overflowed error. Wrapping the ListView builder in Expanded fixed that but moved the indicator to the bottom of the screen.
In trying to fix it I seem to have broken more of the plumbing and the boolean variable that should control the idicator widget; isLoading: stockSet.isBusyLoading doesn't seem to update.
At the moment if I hardcode it as `` it does sit in the appropraite position but I am back with the RenderFlex overflow.
Once all of this is working I'll be wanting to automatically load items untill the screen is full - not sure where I'll be triggering that from yet.
class MyStockSet extends StatefulWidget {
const MyStockSet({super.key});
static const indexStr = 'stocks';
static const labelStr = 'Properties';
#override
State<MyStockSet> createState() => _MyStockSetState();
}
class _MyStockSetState extends State<MyStockSet> {
#override
Widget build(BuildContext context) {
const String imagePath = 'assets/images/${MyStockSet.indexStr}.png';
var assetImage = const AssetImage(imagePath);
//var stockSet = context.watch<StockSet>(); <- didn't work either
var stockSet = Provider.of<StockSet>(context,listen: false);
return Scaffold(
appBar: AppBar(
title: Row(
children: [
AscHero(
assetImage: assetImage,
tag: MyStockSet.indexStr,
title: MyStockSet.labelStr,
radius: 32,
),
const SizedBox(width: 12),
const Text(MyStockSet.labelStr),
],
),
actions: [
IconButton(
onPressed: () {
var stockSet = context.read<StockSet>();
int newNr = stockSet.stocks.length + 1;
Stock tmpstock = Stock(
id: newNr,
title: 'test$newNr',
thumbUrl: 'url',
description: 'desc');
stockSet.add(tmpstock);
},
icon: const Icon(Icons.add),
),
IconButton(
onPressed: () {
developer.log('btn before isBusyLoading ${stockSet.isBusyLoading}');
stockSet.fetch();
developer.log('after btn isBusyLoading ${stockSet.isBusyLoading}');
},
icon: const Icon(Icons.handshake),
),
],
),
body: Column(
children: [
Row(
// these will be filters, order toggle etc.
children: [
ElevatedButton(
onPressed: () => developer.log('Btn pressed.'),
child: Text('Btn')),
],
),
Expanded(
child: Column(children: [
_StockListView(),
LoadingStockListItemNotif(
isLoading: true,
),
]),
),
],
),
);
}
}
class _StockListView extends StatefulWidget {
#override
State<_StockListView> createState() => _StockListViewState();
}
class _StockListViewState extends State<_StockListView> {
#override
void didChangeDependencies() {
super.didChangeDependencies();
developer.log('_StockListView didChangeDependencies()');
// developer.log('scroll pos ${_scrollController.position}');
}
#override
Widget build(BuildContext context) {
var stockSet = context.watch<StockSet>();
return ListView.builder(
// controller: _scrollController,
shrinkWrap: true,
itemCount: stockSet.stocks.length,
itemBuilder: (context, index) => InkWell(
child: StockListItem(
stock: stockSet.stocks[index],
),
onTap: () => Navigator.pushNamed(
context,
'/stocks/stock',
arguments: ScreenArguments(stockSet.stocks[index]),
),
),
);
}
void _scrollListener() {
developer.log('_scrollListener');
}
}
and
class StockSet extends ChangeNotifier {
final List<Stock> _stocks = [];
late bool isBusyLoading = false;
List<Stock> get stocks => _stocks;
void add(Stock stock) {
_stocks.add(stock);
developer.log('added stock :${stock.title}');
notifyListeners();
}
void remove(Stock stock) {
_stocks.remove(stock);
notifyListeners();
}
Future<void> fetch() async {
developer.log('fetch() iL T');
isBusyLoading = true;
notifyListeners();
Stock tmpStock = await _fetchAStock();
developer.log('fetch() iL F');
isBusyLoading = false;
notifyListeners();
add(tmpStock);
}
Future<Stock> _fetchAStock() async {
developer.log('fetch stock ');
final response = await http.get(
Uri.https(
//...
),
);
developer.log('response.statusCode:${response.statusCode}');
if (response.statusCode == 200) {
final Map<String, dynamic> map = json.decode(response.body);
return Stock(
id: map['id'] as int,
title: map['title'] as String,
description: map['description'] as String,
thumbUrl: map['thumbUrl'] as String,
);
}
throw Exception('error fetching stocks:');
}
}
Apologies for the convoluted question.
Add mainAxisSize : MainAxisSize.min for the column inside the expanded widget. The expanded doesn't have any bounds and that's why it throws an error. You can wrap the column with a SingleChildScrollView if you have long content to display
This worked for me!
Just set the shrinkWrap attribute to true
Main lesson:
Don't fight the framework.
Answer:
Instead of tying yourself into Möbius knots trying to put the ListView's functionality outside of itself; use the fact that the ListView.builder allows you to sculpt the logic of how it gets built and what it will contain - given that the provider can trigger its rebuild when the variable in the data set changes.
In other words; by increasing the loop of the builder, you can insert a kind of footer to the Listview. The appearance (or not) of that can depend on the provider, provided it fires the appropriate notifyListeners()s etc.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:equatable/equatable.dart';
import 'dart:async';
class ItemSetRoute extends StatefulWidget {
const ItemSetRoute({Key? key}) : super(key: key);
#override
State<ItemSetRoute> createState() => _ItemSetRouteState();
}
class _ItemSetRouteState extends State<ItemSetRoute> {
#override
Widget build(BuildContext context) {
var itemSet = Provider.of<ItemSet>(
context,
listen: true /* in order to rebuild */,
);
return Scaffold(
appBar: AppBar(title: const Text('Test'), actions: [
IconButton(
onPressed: () {
itemSet.fetch();
},
icon: const Icon(Icons.download),
)
]),
body: Column(
//screen
children: [
Row(
children: [
ElevatedButton(
onPressed: () {
itemSet.fetch();
},
child: const Text('Btn')),
],
),
Expanded(
child: ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: itemSet.items.length + 1,
itemBuilder: (context, index) {
/* logic here to create a kind of footer of the ListView */
if (index <= itemSet.items.length - 1) {
return InkWell(
child: ItemListItem(
item: itemSet.items[index],
),
onTap: () {
//('Item tapped, navigate etc.');
});
} else {
return LoadingItemNotifier(
isLoading: itemSet.isBusyLoading,
);
}
},
),
),
],
),
);
}
}
//Models
class ItemListItem extends StatelessWidget {
const ItemListItem({Key? key, required this.item}) : super(key: key);
final Item item;
#override
Widget build(BuildContext context) {
return Material(
child: ListTile(
title: Text(item.title),
subtitle: Text(item.description),
),
);
}
}
class LoadingItemNotifier extends StatefulWidget {
const LoadingItemNotifier({Key? key, this.isLoading = false})
: super(key: key);
final bool isLoading;
#override
State<LoadingItemNotifier> createState() => _LoadingItemNotifierState();
}
class _LoadingItemNotifierState extends State<LoadingItemNotifier> {
#override
Widget build(BuildContext context) {
if (widget.isLoading) {
return Material(
child: ListTile(
leading: SizedBox(
width: 48,
height: 48,
child: ClipOval(
child: Material(
color: Colors.lightBlue.withOpacity(0.25),
child: const Center(
child: Icon(Icons.download),
),
),
),
),
title: const Text('Loading'),
isThreeLine: true,
subtitle: const Text('One moment please...'),
dense: true,
),
);
} else {
return const SizedBox(height: 0);
}
}
}
class ItemSet extends ChangeNotifier {
final List<Item> _items = [];
late bool isBusyLoading = false;
List<Item> get items => _items;
void add(Item item) {
_items.add(item);
notifyListeners();
}
void remove(Item item) {
_items.remove(item);
notifyListeners();
}
Future<void> fetch() async {
isBusyLoading = true;
notifyListeners();
/* handling REST call here */
await Future.delayed(const Duration(milliseconds: 500));
Item newItem = const Item(id: 123, title: 'Title', description: 'Desc');
isBusyLoading = false;
add(newItem);
}
}
class Item extends Equatable {
const Item({
required this.id,
required this.title,
required this.description,
});
final int id;
final String title;
final String description;
#override
List<Object> get props => [id, title, description];
}
Caveats
I don't know if this is the most efficient way of doing this - perhaps there should be fewer states, etc. ...

Troubles with making a Favorite page with Hive DB Flutter

Hello everyone here's my test app and I have some problems with making a Favorite page section where you can tap on button and add the item into fav page.
I'm receiving a data from API and implementing it by Listview.builder
Here are some photos of how it should look like:
Home page
Favorite page
main.dart, here I'm openning a box called 'favorites_box'
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
void main() async{
await GetStorage.init();
await Hive.openBox('favorites_box');
runApp(MainPage());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(name: '/', page: () => MyApp()),
GetPage(name: '/main-page', page: () => MainPage()),
GetPage(name: '/favorite_page', page: () => FavoritePage()),
// Dynamic route
],
home: MainPage(),
);
}
}
Well here's a code of home page:
main_page.dart
import 'package:flutter/material.dart';
import '../View/listview_api.dart';
class MainPage extends StatefulWidget {
#override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int currentIndex = 0;
List<BottomNavigationBarItem>? items;
final screens = [
HomePage(),
HomePage()
FavoritePage(),
HomePage()
];
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: SafeArea(
child: Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
title: Container(
width: double.infinity,
height: 40,
color: Colors.white,
child: Center(
child: TextField(
decoration: InputDecoration(
border: OutlineInputBorder(
),
hintText: 'Searching',
prefixIcon: Icon(Icons.search),
suffixIcon: Icon(Icons.notifications)),
),
),
),
),
body: screens[currentIndex],
bottomNavigationBar: BottomNavigationBar(
unselectedItemColor: Colors.grey,//AppColors.unselectedBottomNavItem,
selectedItemColor: Colors.blue,//AppColors.assets,
onTap: (index) => setState(() {
currentIndex = index;
}),//controller.setMenu(BottomMenu.values[pos]),
//currentIndex: ,//controller.bottomMenu.index,
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
currentIndex: currentIndex,
selectedLabelStyle: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
),
unselectedLabelStyle: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
),
elevation: 8,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
backgroundColor: Colors.blue,
),
BottomNavigationBarItem(
icon: Icon(Icons.add_shopping_cart),
label: 'Shopping cart',
backgroundColor: Colors.red,
),
BottomNavigationBarItem(
icon: Icon(Icons.favorite),
label: 'Favorite',
backgroundColor: Colors.green,
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Profile',
backgroundColor: Colors.yellow,
),
],
),
),
),
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return SafeArea(
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Center(
child: Padding(
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
//Image.asset('images/image0.jpg'),
SizedBox(
height: 25.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'New!',
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 25.0,
fontWeight: FontWeight.bold,
),
),
IconButton(
onPressed: () {},
icon: Icon(
Icons.arrow_forward_outlined,
),
),
],
),
SizedBox(
height: 25.0,
),
SizedBox(
height: 300.0,
width: double.infinity,
child: ListViewAPI(),
),
],
),
),
),
),
);
}
}
And now, below is a code of ListViewAPI(), here I've added the elements which I tap to the box('favorites_box'): listview_api.dart
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
String? stringResponse;
Map? mapResponse;
Map? dataResponse;
List? listResponse;
class ListViewAPI extends StatefulWidget {
const ListViewAPI({Key? key}) : super(key: key);
#override
_ListViewAPIState createState() => _ListViewAPIState();
}
class _ListViewAPIState extends State<ListViewAPI> {
Future apiCall() async {
http.Response response;
response = await http.get(Uri.parse("https://api.client.macbro.uz/v1/product"));
if(response.statusCode == 200) {
setState(() {
// stringResponse = response.body;
mapResponse = jsonDecode(response.body);
listResponse = mapResponse!['products'];
});
}
}
#override
void initState() {
super.initState();
apiCall();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scrollbar(
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Stack(
children: [
Card(
child: Image.network(
listResponse![index]['image'],
),
),
Positioned(
right: 0,
child: InkWell(
child: IconButton(
onPressed: () async {
await Hive.box('favorites_box').put(listResponse![index]['image'], listResponse);
},
icon: Icon(
Icons.favorite_rounded,
color: Colors.red,
),
),
),
),
],
);
},
itemCount: listResponse == null ? 0 : listResponse!.length,
),
),
);
}
}
So here, I created a list, and tried to save the elements from box named "favorites_box" and got data which was added while I tap favorite IconButton upper but without success( :
favorite_page.dart
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../View/gridview_api.dart';
class FavoritePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ValueListenableBuilder(
valueListenable: Hive.box('favorites_box').listenable(),
builder: (context, box, child) {
List posts = List.from(Hive.box('favorites_box').values);
return ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Column(
children: [
Text(
'List of favorite products'
),
Card(
child: posts[index] == null ? Text('nothing(') : posts[index],
// child: Hive.box('favorites_box').get(listResponse),
),
],
);
},
);
},
),
);
}
}
I'll be grateful if someone could help me with this problem, as I'm trying to fix this issue for a couple of days
P.s. I'm so sorry for some inconveniences, I'm a novice yet that's why hope you'll understand me
Thanks!
Alright. I now have a solution. It is a bit more complex than what you started with but it worked during testing.
Using https://marketplace.visualstudio.com/items?itemName=hirantha.json-to-dart I created a model class from the API data JSON. One for the Product and one for the Price map inside of Product.
product_model.dart
import 'dart:convert';
import 'package:equatable/equatable.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'price.dart';
part 'product_model.g.dart';
#HiveType(typeId: 1)
class ProductModel extends Equatable {
#HiveField(0)
final String? id;
#HiveField(1)
final String? name;
#HiveField(2)
final String? slug;
#HiveField(3)
final bool? active;
#HiveField(4)
final String? image;
#HiveField(5)
final String? code;
#HiveField(6)
final String? order;
#HiveField(7)
final int? cheapestPrice;
#HiveField(8)
final Price? price;
#HiveField(9)
final int? discount;
const ProductModel({
this.id,
this.name,
this.slug,
this.active,
this.image,
this.code,
this.order,
this.cheapestPrice,
this.price,
this.discount,
});
factory ProductModel.fromMap(Map<String, dynamic> data) => ProductModel(
id: data['id'] as String?,
name: data['name'] as String?,
slug: data['slug'] as String?,
active: data['active'] as bool?,
image: data['image'] as String?,
code: data['code'] as String?,
order: data['order'] as String?,
cheapestPrice: data['cheapest_price'] as int?,
price: data['price'] == null
? null
: Price.fromMap(data['price'] as Map<String, dynamic>),
discount: data['discount'] as int?,
);
Map<String, dynamic> toMap() => {
'id': id,
'name': name,
'slug': slug,
'active': active,
'image': image,
'code': code,
'order': order,
'cheapest_price': cheapestPrice,
'price': price?.toMap(),
'discount': discount,
};
/// `dart:convert`
///
/// Parses the string and returns the resulting Json object as [ProductModel].
factory ProductModel.fromJson(String data) {
return ProductModel.fromMap(json.decode(data) as Map<String, dynamic>);
}
/// `dart:convert`
///
/// Converts [ProductModel] to a JSON string.
String toJson() => json.encode(toMap());
ProductModel copyWith({
String? id,
String? name,
String? slug,
bool? active,
String? image,
String? code,
String? order,
int? cheapestPrice,
Price? price,
int? discount,
}) {
return ProductModel(
id: id ?? this.id,
name: name ?? this.name,
slug: slug ?? this.slug,
active: active ?? this.active,
image: image ?? this.image,
code: code ?? this.code,
order: order ?? this.order,
cheapestPrice: cheapestPrice ?? this.cheapestPrice,
price: price ?? this.price,
discount: discount ?? this.discount,
);
}
#override
bool get stringify => true;
#override
List<Object?> get props {
return [
id,
name,
slug,
active,
image,
code,
order,
cheapestPrice,
price,
discount,
];
}
}
price.dart
import 'dart:convert';
import 'package:equatable/equatable.dart';
import 'package:hive_flutter/hive_flutter.dart';
part 'price.g.dart';
#HiveType(typeId: 2)
class Price extends Equatable {
#HiveField(0)
final int? price;
#HiveField(1)
final int? oldPrice;
#HiveField(2)
final int? uzsPrice;
#HiveField(3)
final int? secondPrice;
#HiveField(4)
final int? secondUzsPrice;
const Price({
this.price,
this.oldPrice,
this.uzsPrice,
this.secondPrice,
this.secondUzsPrice,
});
factory Price.fromMap(Map<String, dynamic> data) => Price(
price: data['price'] as int?,
oldPrice: data['old_price'] as int?,
uzsPrice: data['uzs_price'] as int?,
secondPrice: data['second_price'] as int?,
secondUzsPrice: data['second_uzs_price'] as int?,
);
Map<String, dynamic> toMap() => {
'price': price,
'old_price': oldPrice,
'uzs_price': uzsPrice,
'second_price': secondPrice,
'second_uzs_price': secondUzsPrice,
};
/// `dart:convert`
///
/// Parses the string and returns the resulting Json object as [Price].
factory Price.fromJson(String data) {
return Price.fromMap(json.decode(data) as Map<String, dynamic>);
}
/// `dart:convert`
///
/// Converts [Price] to a JSON string.
String toJson() => json.encode(toMap());
Price copyWith({
int? price,
int? oldPrice,
int? uzsPrice,
int? secondPrice,
int? secondUzsPrice,
}) {
return Price(
price: price ?? this.price,
oldPrice: oldPrice ?? this.oldPrice,
uzsPrice: uzsPrice ?? this.uzsPrice,
secondPrice: secondPrice ?? this.secondPrice,
secondUzsPrice: secondUzsPrice ?? this.secondUzsPrice,
);
}
#override
bool get stringify => true;
#override
List<Object?> get props {
return [
price,
oldPrice,
uzsPrice,
secondPrice,
secondUzsPrice,
];
}
}
I then used https://docs.hivedb.dev/#/custom-objects/generate_adapter to create adapters for both of those. You can read the documentation to see how that is done using build_runner and the hive_generator packages.
In main.dart I registered both of the adapters and opened up a box with the ProductModel type from product_model.dart.
main.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:test/product_model/price.dart';
import 'package:test/product_model/product_model.dart';
import 'favorite_page.dart';
import 'homepage.dart';
void main() async {
// await GetStorage.init();
await Hive.initFlutter();
Hive.registerAdapter(PriceAdapter());
Hive.registerAdapter(ProductModelAdapter());
await Hive.openBox<ProductModel>('favorites_box');
runApp(MainPage());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(name: '/', page: () => MyApp()),
GetPage(name: '/main-page', page: () => MainPage()),
GetPage(name: '/favorite_page', page: () => FavoritePage()),
// Dynamic route
],
home: MainPage(),
);
}
}
listview_api.dart is mostly the same with the exception of mapping the products from listResponse to ProductModel objects.
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:test/product_model/product_model.dart';
String? stringResponse;
Map? mapResponse;
Map? dataResponse;
List? listResponse;
class ListViewAPI extends StatefulWidget {
const ListViewAPI({Key? key}) : super(key: key);
#override
_ListViewAPIState createState() => _ListViewAPIState();
}
class _ListViewAPIState extends State<ListViewAPI> {
Future apiCall() async {
http.Response response;
response =
await http.get(Uri.parse("https://api.client.macbro.uz/v1/product"));
if (response.statusCode == 200) {
setState(() {
// stringResponse = response.body;
mapResponse = jsonDecode(response.body);
listResponse = mapResponse!['products'];
listResponse =
listResponse!.map((e) => ProductModel.fromMap(e)).toList(); // Map all of the products in listResponse to a ProductModel object.
});
}
}
#override
void initState() {
super.initState();
apiCall();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scrollbar(
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Stack(
children: [
Card(
child: Image.network(
listResponse![index].image!,
),
),
Positioned(
right: 0,
child: InkWell(
child: IconButton(
onPressed: () async {
await Hive.box<ProductModel>('favorites_box').put(
listResponse![index].image, listResponse![index]);
},
icon: Icon(
Icons.favorite_rounded,
color: Colors.red,
),
),
),
),
],
);
},
itemCount: listResponse == null ? 0 : listResponse!.length,
),
),
);
}
}
homepage.dart is unchanged.
favorite_page.dart was changed to a stateful widget and then gets the box values on init.
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:test/product_model/product_model.dart';
class FavoritePage extends StatefulWidget {
#override
State<FavoritePage> createState() => _FavoritePageState();
}
class _FavoritePageState extends State<FavoritePage> {
var posts;
#override
void initState() {
super.initState();
posts = Hive.box<ProductModel>('favorites_box').values.toList();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Stack(
children: [
Card(
child: Image.network(
posts[index].image!,
),
),
Positioned(
right: 0,
child: InkWell(
child: IconButton(
onPressed: () async {
await Hive.box<ProductModel>('favorites_box')
.delete(posts[index]);
},
icon: Icon(
Icons.favorite_rounded,
color: Colors.red,
),
),
),
),
],
);
},
itemCount: posts == null ? 0 : posts.length,
),
);
}
}
I really encourage you to read the documentation on Hive as it contains a wealth of information. Another tip when coding with hive is to make sure you are clearing out the storage and cache for your emulator or physical device regularly. I have had too many headaches dealing with errors in Hive simply because I forgot to clear the storage and cache which was resulting in bad data despite having changed my source code.
I don't believe this is a problem with your code. However, I do recommend creating a model class for your data and maybe using a FutureBuilder https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html.
I believe the problem is that you have not updated your AndroidManifest.xml file to allow for internet connectivity.
Try adding:
<uses-permission android:name="android.permission.INTERNET" />
to your android\app\src\main\AndroidManifest.xml, above <application.
Further reading: https://flutter-examples.com/add-permissions-in-androidmanifest-xml-file/
After taking a closer look at your issue, I think I figured out the problem.
Hive requires an init:
void main() async {
// await GetStorage.init(); // Not sure why this was here but doesn't seem to be needed.
await Hive.initFlutter();
await Hive.openBox('favorites_box');
runApp(MainPage());
}
You were also missing a comma in main_page.dart
final screens = [
HomePage(),
HomePage() <----
FavoritePage(),
HomePage()
];
For your favorites page, I replaced the ValueListenableBuilder with just a ListView.builder:
class FavoritePage extends StatelessWidget {
List posts = List.from(Hive.box('favorites_box').values);
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Stack(
children: [
Card(
child: Image.network(
posts[index]['image'],
),
),
Positioned(
right: 0,
child: InkWell(
child: IconButton(
onPressed: () async {
await Hive.box('favorites_box').delete(posts[index]);
},
icon: Icon(
Icons.favorite_rounded,
color: Colors.red,
),
),
),
),
],
);
},
itemCount: posts == null ? 0 : posts.length,
),
);
}
}
There is still an error when you try to use this that says that posts[index]['image'] type 'String' is not a subtype of type 'int' of 'index' but you can easily fix this by creating a model class and accessing everything with those properties. Using model class in flutter (dart) here is an example of a model class. Instead of using DocumentSnapshot, you can add a toList() or toMap() method.
Hope this helps. It is working on my emulator. but I am just printing out the full string instead of using the image in the Card child.
Example Model Class:
import 'dart:convert';
void main() async {
String data = '{"id":"626694d4f1ce2a0012f0fe1c","name":"JBL Party Box On-The-Go","slug":"jbl-party-box-on-the-go-juqgil2ep8ult","active":true,"image":"https://cdn.macbro.uz/macbro/1fad4f47-51f4-4f12-975b-657d780c98af","code":"","order":"0","cheapest_price":0,"price":{"price":520,"old_price":0,"uzs_price":5994000,"second_price":0,"second_uzs_price":7012500},"discount":0}';
var test = new ProductModel.fromJson(json.decode(data));
print(test.image);
}
class ProductModel {
String? name;
String? image;
ProductModel.fromJson(Map json) {
this.name = json['id'];
this.image = json['image'];
}
}

Flutter make a form with provider and riverpod

I'm new with Flutter and I want to upgrade my code. I have a form that uses multiple textformfields and I want to convert this code using provider and riverpod to improve readability but I'm not sure how to do it.
For the example I simplified my code to only one distance field but there are many others.
This is my CalculatorScreen :
import 'dart:async' show Future;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:app/core/models/model_form_calculator.dart';
import 'package:app/core/services/service_form_validator.dart';
import 'package:app/core/utils/utils_app_color.dart';
class CalculatorScreen extends StatefulWidget
{
CalculatorScreen({Key key}) : super(key: key);
#override
_CalculatorScreenState createState() => _CalculatorScreenState();
}
class _CalculatorScreenState extends State<CalculatorScreen>
{
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final _formKey = GlobalKey<FormState>();
FormCalculatorModel _formData = FormCalculatorModel();
bool _autoValidateForm = false;
final TextEditingController _controllerDistance = TextEditingController();
#override
void initState() {
super.initState();
}
#override
void dispose()
{
_controllerDistance.dispose();
super.dispose();
}
#override
Widget build(BuildContext context)
{
return GestureDetector(
onTap: (() => FocusScope.of(context).requestFocus(FocusNode())),
child: Scaffold(
key: _scaffoldKey,
backgroundColor: AppColors.colorBgDark,
body : _buildBody()
),
);
}
Widget _buildBody()
{
return SingleChildScrollView(
child: Column(
children: [
Form(
key: _formKey,
autovalidate: _autoValidateForm,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: _controllerDistance,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: "Enter a value",
),
validator: (value){
return FormValidatorService.isDistanceValid(value);
},
onSaved: (var value) {
_formData.distance = num.tryParse(value).round();
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: FlatButton(
child: Text("Erase"),
onPressed: _buttonResetAction
),
),
Expanded(
child: FlatButton(
child: Text("Send"),
onPressed: _buttonSubmitAction
),
),
],
),
]
),
),
],
),
);
}
void _buttonResetAction()
{
_eraseForm();
}
void _eraseForm(){
setState(() {
_formKey.currentState.reset();
_formData = FormCalculatorModel();
_autoValidateForm = false;
_controllerDistance.clear();
});
}
void _buttonSubmitAction() async
{
if (!_formKey.currentState.validate()) {
setState(() {
_autoValidateForm = true;
});
return;
}
_formKey.currentState.save();
try{
// some actions
}catch(e){
_eraseForm();
print(e.toString());
}
}
}
This is my formModel (This model contains all the fields that I can fill in my form and allows me to store the values ​​of the form once validated to then make calculations with these values
):
class FormCalculatorModel{
int distance;
FormCalculatorModel({
this.distance,
});
#override
String toString() {
return '{ '
'${this.distance}, '
'}';
}
}
And my FormValidatorService :
class FormValidatorService{
static String isDistanceValid(String value)
{
num _distance = num.tryParse(value);
if (_distance == null) {
return "is required";
}
if (_distance < 200) {
return "Min distance is 200";
}
if (_distance > 1000) {
return "Max dist is 1000";
}
return null;
}
}
Now I want to convert this with riverpod. I'm a little lost, there are few examples on the internet and I don't really see how to manage my form
At first I'm just trying to handle the validation of the form but it doesn't work.
My calculatorScreen :
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class CalculatorScreen extends HookWidget{
final _formKey = GlobalKey<FormState>();
bool _autoValidateForm = false;
FormCalculatorModel _formData = FormCalculatorModel();
final TextEditingController _controllerDistance = TextEditingController();
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (() => FocusScope.of(context).requestFocus(FocusNode())),
child: Scaffold(
body : _buildBody(context)
),
);
}
Widget _buildBody(BuildContext context){
final _formModel = useProvider(formCalculatorProvider.state);
return SingleChildScrollView(
child: Column(
children: [
TitleComponent(
title: "Calcul",
description: "Parametrer",
),
ContainerComponent(
background: AppColors.colorBgLight,
children: [
Form(
key : _formKey,
autovalidate: _autoValidateForm,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
decoration: InputDecoration(
labelText: "Distance",
//errorText: _formModel.distance.error,
),
controller: _controllerDistance,
validator: (String value){
return FormValidatorService.isDistanceValid(value);
},
onSaved: (var value) {_formData.distance = num.tryParse(value).round();}
),
],
),
),
ButtonComponent.primary(
text: "Calculer",
context: context,
onPressed : context.read(formCalculatorProvider).submitData(key: _formKey),
),
],
)
],
),
);
}
}
And my FormCalculatorNotifier :
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
enum FormState
{
EMPTY,
SUCCESS,
ERROR
}
class FormCalculatorModelNew {
const FormCalculatorModelNew({this.formState, this.autoValidate, this.distance});
final FormState formState;
final bool autoValidate;
final String distance;
}
class FormCalculatorNotifier extends StateNotifier<FormCalculatorModelNew>
{
FormCalculatorNotifier() : super(_initial);
static const FormState _initialState = FormState.EMPTY;
static const _initial = FormCalculatorModelNew(
formState : _initialState,
autoValidate: false,
distance: null
);
submitData({key}){
print(key);
if (!key.currentState.validate()) {
state = FormCalculatorModelNew(
autoValidate: true,
);
return;
}
key.currentState.save();
}
}
The provider :
final formCalculatorProvider = StateNotifierProvider((ref) => FormCalculatorNotifier());
It does not really make sense to use Provider in your example code because I don't see anywhere listen to the state of formCalculatorProvider. Also, the form itself should be managed in the form widget itself.
I assume you want to share the distance value with other widgets. Here are what I will do:
_autoValidate: leave it inside the widget and handle it by Hook
add copyWith inside FormCalculatorModelNew (can easily update partial value)
formCalculatorProvider part:
final formCalculatorProvider = StateNotifierProvider((ref) => FormCalculatorNotifier());
enum MyFormState { EMPTY, SUCCESS, ERROR }
class FormCalculatorModelNew {
const FormCalculatorModelNew({this.formState, this.distance});
final MyFormState formState;
final int distance;
FormCalculatorModelNew copyWith({
MyFormState formState,
int distance,
}) {
return FormCalculatorModelNew(
formState: formState ?? this.formState,
distance: distance ?? this.distance,
);
}
}
class FormCalculatorNotifier extends StateNotifier<FormCalculatorModelNew> {
FormCalculatorNotifier() : super(_initial);
static const MyFormState _initialState = MyFormState.EMPTY;
static const _initial =
FormCalculatorModelNew(formState: _initialState, distance: null);
void update(int distance) {
state = state.copyWith(distance: distance, formState: MyFormState.SUCCESS);
}
void error() {
state = state.copyWith(distance: null, formState: MyFormState.ERROR);
}
void clear() {
state = state.copyWith(distance: null, formState: MyFormState.EMPTY);
}
}
CalculatorScreen part: (simplify)
class CalculatorScreen extends HookWidget {
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
final _autoValidate = useState<bool>(false);
final _controller = useTextEditingController();
return Scaffold(
body: Form(
key: _formKey,
autovalidate: _autoValidate.value,
child: Column(
children: [
TextFormField(
controller: _controller,
keyboardType: TextInputType.number,
validator: (value) {
return FormValidatorService.isDistanceValid(value);
},
onSaved: (value) {
context.read(formCalculatorProvider).update(num.tryParse(value).round());
},
),
Row(
children: [
FlatButton(
child: Text('Erase'),
onPressed: () {
_formKey.currentState.reset();
_controller.clear();
_autoValidate.value = false;
context.read(formCalculatorProvider).clear();
},
),
FlatButton(
child: Text('Send'),
onPressed: () {
if(_formKey.currentState.validate()){
_formKey.currentState.save();
}else{
_autoValidate.value = true;
context.read(formCalculatorProvider).error();
}
},
),
],
),
],
),
),
);
}
}
You can use TextEditingController.
Further create a provider like so, and you may now listen to text changes and store them where desired using the same provider
final formControllerProvider =
StateProvider<TextEditingController>((ref) => TextEditingController());