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

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. ...

Related

Deleting Items in ListView not working properly

I'm trying to delete multiple items in a ListView after selecting them. Everything is working fine, the problem comes when I finish the deletion process. I can see that the items were deleted but the widgets in place are still highlighted.
Here is my code for the App:
class MultiSelect extends StatefulWidget {
const MultiSelect({super.key});
#override
State<MultiSelect> createState() => _MultiSelectState();
}
class _MultiSelectState extends State<MultiSelect> {
late List<Transaction> selectedTransactions;
#override
void initState() {
super.initState();
HiveAPI.loadTransactions();
selectedTransactions = [];
}
void select(bool value, VoidCallback callback, Transaction item) {
setState(() {
value
? selectedTransactions.remove(item)
: selectedTransactions.add(item);
callback();
});
}
void delete() {
setState(() {
HiveAPI.deleteTransaction(selectedTransactions);
selectedTransactions = [];
HiveAPI.loadTransactions();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Multi Select Example"),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
delete();
},
),
],
),
body: ListView.builder(
itemCount: HiveAPI.transactions.length,
itemBuilder: (context, index) {
return CustomCard(
onPress: (value, callback) =>
select(value, callback, HiveAPI.transactions[index]),
transaction: HiveAPI.transactions[index],
);
}),
);
}
}
This is the code to my custom widget inside the ListView:
class CustomCard extends StatefulWidget {
const CustomCard({
Key? key,
required this.transaction,
required this.onPress,
}) : super(key: key);
final Transaction transaction;
final Function(bool, VoidCallback) onPress;
#override
State<CustomCard> createState() => _CustomCardState();
}
class _CustomCardState extends State<CustomCard> {
late bool selected;
#override
void initState() {
super.initState();
selected = false;
}
void updateUI() => setState(() => selected = !selected);
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => widget.onPress(selected, updateUI),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0),
decoration: BoxDecoration(
color: selected ? Colors.lime.shade100 : Colors.transparent,
borderRadius: const BorderRadius.all(Radius.circular(16)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: selected ? Colors.lime : Colors.black,
borderRadius: const BorderRadius.all(Radius.circular(200)),
),
child: Icon(
selected ? Icons.check : Icons.wallet,
color: selected ? Colors.black : Colors.white,
),
),
const SizedBox(width: 8),
Text(
widget.transaction.title,
style: Theme.of(context).textTheme.bodyText2,
),
],
),
),
);
}
}
My code for the HiveAPI and Transaction class:
class HiveAPI {
static const String boxKey = 'transactionBox';
static const String transactionsDataKey = 'transactions';
static final box = Hive.box(boxKey);
static final List<Transaction> transactions = [];
static void loadTransactions() {
for (var transaction in box.get(transactionsDataKey)) {
transactions.add(
Transaction(
transaction[0],
transaction[1],
transaction[2],
transaction[3],
transaction[4],
),
);
}
}
static void _updateTransactions() {
final List transactionsPrimitive = [];
for (var transaction in transactions) {
transactionsPrimitive.add([
transaction.title,
transaction.category,
transaction.amount,
transaction.type,
transaction.date,
]);
}
box.put(transactionsDataKey, transactionsPrimitive);
}
static void deleteTransaction(List<Transaction> selectedTransaction) {
transactions.removeWhere((item) => selectedTransaction.contains(item));
_updateTransactions();
}
}
class Transaction {
late String title;
late String category;
late double amount;
late int type;
late DateTime date;
Transaction(this.title, this.category, this.amount, this.type, this.date);
}
This is the list before deleting
This is the list after deleting
You need to use Key if you want to move or delete item in ListView.
For example:
return CustomCard(
key:Key(HiveAPI.transactions[index].title),
onPress: (value, callback) =>
select(value, callback, HiveAPI.transactions[index]),
transaction: HiveAPI.transactions[index],
);
or use other unique key if the title isn't unique

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

How can I implement AlertDialog box inside ListView in Flutter Android app automatically without pressing any button?

I am working on a project where I have created a RestApi where I have data of cars and which I keep updating every 3 sec. The cars are at a junction of 4 roads, now when a car is approaching towards a particular road denoted by id the "is_green" will become true and in the ListView -> the CircleAvtar will become green which will indicate that car is comming or not Now My Question is
how can I implement a AlertDialog box which will popup automically whithout pressing any button when bool isGreen = userData[index]["is_green"] will contain value true . I am new to flutter so I don't know how to do that... Please help me out
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:convert';
void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(),
home: Home(),
));
}
class Home extends StatefulWidget {
Home({Key key}) : super(key: key);
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
Map data;
List userData;
Future getData() async {
print("updating data...");
http.Response response = await http.get("http://10.100.101.154:3000/xd");
data = jsonDecode(response.body);
setState(() {
userData = data["data"];
});
}
#override
void initState() {
super.initState();
Timer.periodic(new Duration(seconds: 3), (timer) {
getData();
});
}
Color getColor(bool isGreen) {
if (isGreen == true) {
return Colors.green;
} else {
return Colors.red;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('SGH'),
centerTitle: true,
),
body: ListView.builder(
itemCount: userData == null ? 0 : userData.length,
itemBuilder: (BuildContext context, int index) {
bool isGreen = userData[index]["is_green"];
return Card(
child: Row(
children: <Widget>[
Padding(
padding: EdgeInsets.all(10),
child: CircleAvatar(
backgroundColor: getColor(isGreen),
minRadius: 6,
),
),
Padding(
padding: EdgeInsets.all(10),
child: Wrap(
direction: Axis.horizontal,
children: <Widget>[
Text(
"Road Id = ${userData[index]["id"]} \CarInQueue = ${userData[index]["car_que"]} \nIsGreen ? --> ${userData[index]["is_green"]} ",
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
],
),
),
],
),
);
}),
);
}
}
Just check any time you update list of cars... Then show a Dialog with informations about which car is crossing
Future getData() async {
print("updating data...");
http.Response response = await http.get("http://10.100.101.154:3000/xd");
data = jsonDecode(response.body);
setState(() {
userData = data["data"];
});
checkDialog();
}
checkDialog(){
var item = userData.where((car)=>car["is_green"]).first;//this is the first car where is_green is true
if(item!=null){
showDialog(context:context,
builder:(BuildContext context)=>AlertDialog(
//.... content here
));
}
}

how to use setState inside another class?

I am having issues with setting state of variable because i am using other class outside stateful widget. On line 115 inside buildActions method i want to set _selectedStores = selectedStores;. How can i set the state?
I tried using callback but got no luck.
import 'package:flutter/material.dart';
class SearchDemo extends StatefulWidget {
#override
_SearchDemoState createState() => _SearchDemoState();
}
class _SearchDemoState extends State<SearchDemo> {
final _SearchDemoSearchDelegate _delegate = _SearchDemoSearchDelegate();
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
String _lastSearchSelected;
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: const Text('Search Demo'),
actions: <Widget>[
IconButton(
tooltip: 'Search',
icon: const Icon(Icons.search),
onPressed: () async {
final String selected = await showSearch<String>(
context: context,
delegate: _delegate,
);
if (selected != null && selected != _lastSearchSelected) {
setState(() {
_lastSearchSelected = selected;
});
}
},
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Last search: ${_lastSearchSelected ?? 'NONE'}.'),
],
),
),
);
}
}
class Stores {
int id;
String name;
Stores(this.id, this.name);
static List<Stores> getStores() {
return <Stores>[
Stores(1, 'Amazon'),
Stores(2, 'Flipkart'),
Stores(3, 'Snapdeal'),
];
}
}
class _SearchDemoSearchDelegate extends SearchDelegate<String> {
List<Stores> _stores = Stores.getStores();
List<DropdownMenuItem<Stores>> _dropdownMenuItems;
Stores _selectedStores;
List<DropdownMenuItem<Stores>> buildDropdownMenuItems(List stores) {
List<DropdownMenuItem<Stores>> items = List();
for (Stores stores in stores) {
items.add(
DropdownMenuItem(
value: stores,
child: Text(stores.name),
),
);
}
return items;
}
#override
Widget buildLeading(BuildContext context) {
return IconButton(
tooltip: 'Back',
icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow,
progress: transitionAnimation,
),
onPressed: () {
close(context, null);
},
);
}
#override
Widget buildSuggestions(BuildContext context) {
return _SuggestionList(
query: query,
onSelected: (String suggestion) {
print(suggestion);
},
);
}
#override
Widget buildResults(BuildContext context) {}
#override
List<Widget> buildActions(BuildContext context) {
_dropdownMenuItems = buildDropdownMenuItems(_stores);
_selectedStores = _dropdownMenuItems[0].value;
void onChangeDropdownItem(Stores selectedStores) {
setState(() {
_selectedStores = selectedStores;
});
}
return <Widget>[
query.isEmpty
? Container(
padding: const EdgeInsets.only(right: 5.0, top: 5.0),
child: DropdownButtonHideUnderline(
child: DropdownButton(
elevation: 0,
value: _selectedStores,
items: _dropdownMenuItems,
onChanged: onChangeDropdownItem,
),
),
)
: IconButton(
tooltip: 'Clear',
icon: const Icon(Icons.clear),
onPressed: () {
query = '';
},
),
];
}
}
List<String> getHistory() {
//Get Last Searched products from device storage *Pending*
final List<String> _history = <String>[
"iPhone X 64GB Silver",
"Galaxy S10+ White",
"Apple Watch Series 3",
"Samson C01UPRO",
"Cooler Master masterbox 5"
];
return _history;
}
class _SuggestionList extends StatelessWidget {
const _SuggestionList({this.query, this.onSelected});
final String query;
final ValueChanged<String> onSelected;
#override
Widget build(BuildContext context) {
//Get Data From API *Pending*
final List<String> _data = <String>[
"iPhone X 64GB Silver",
"Galaxy S10+ White",
"Apple Watch Series 3",
"Samson C01UPRO",
"Cooler Master Masterbox 5"
];
final List<String> suggestions = query.isEmpty
? getHistory()
: _data
.where((p) => p.toLowerCase().contains(query.toLowerCase()))
.toList();
return ListView.builder(
itemCount: suggestions.length,
itemBuilder: (BuildContext context, int i) {
final String suggestion = suggestions[i];
return ListTile(
leading: query.isEmpty ? const Icon(Icons.history) : const Icon(null),
title: Text(suggestion),
onTap: () {
onSelected(suggestion);
},
);
},
);
}
}
The method setState is only part of StatefulWidgets and that information shouldn't be passed around. It's not recommended and is not a good development practice. Can you do it? Yes, like this:
class OtherClass {
final State state;
OtherClass(this.state);
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
#override
void initState() {
super.initState();
OtherClass(this);
}
}
But, again, I do not recommend this at all. You should be using some kind of Future or Stream to send your data to your StatefulWidget and then use your setState there, where it should be.

Flutter Stateful Widget State not Initializing

I'm making a command and control application using Flutter, and have come across an odd problem. The main status page of the app shows a list of stateful widgets, which each own a WebSocket connection that streams state data from a connected robotic platform. This worked well when the robots themselves were hardcoded in. However now that I'm adding them dynamically (via barcode scans), only the first widget is showing status.
Further investigation using the debugger shows that this is due to the fact that a state is only getting created for the first widget in the list. Subsequently added widgets are successfully getting constructed, but are not getting a state. Meaning that createState is not getting called for anything other than the very first widget added. I checked that the widgets themselves are indeed being added to the list and that they each have unique hash codes. Also, the IOWebSocketChannel's have unique hash codes, and all widget data is correct and unique for the different elements in the list.
Any ideas as to what could be causing this problem?
Code for the HomePageState:
class HomePageState extends State<HomePage> {
String submittedString = "";
StateContainerState container;
List<RobotSummary> robotList = [];
List<String> robotIps = [];
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
void addRobotToList(String ipAddress) {
var channel = new IOWebSocketChannel.connect('ws://' + container.slsData.slsIpAddress + ':' + container.slsData.wsPort);
channel.sink.add("http://" + ipAddress);
var newConnection = new RobotSummary(key: new UniqueKey(), channel: channel, ipAddress: ipAddress, state: -1, fullAddress: 'http://' + container.slsData.slsIpAddress + ':' + container.slsData.wsPort,);
scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text("Adding robot..."), duration: Duration(seconds: 2),));
setState(() {
robotList.add(newConnection);
robotIps.add(ipAddress);
submittedString = ipAddress;
});
}
void _onSubmit(String val) {
// Determine the scan data that was entered
if(Validator.isIP(val)) {
if(ModalRoute.of(context).settings.name == '/') {
if (!robotIps.contains(val)) {
addRobotToList(val);
}
else {
scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text("Robot already added..."), duration: Duration(seconds: 5),));
}
}
else {
setState(() {
_showSnackbar("Robot scanned. Go to page?", '/');
});
}
}
else if(Validator.isSlotId(val)) {
setState(() {
_showSnackbar("Slot scanned. Go to page?", '/slots');
});
}
else if(Validator.isUPC(val)) {
setState(() {
_showSnackbar("Product scanned. Go to page?", '/products');
});
}
else if (Validator.isToteId(val)) {
}
}
#override
Widget build(BuildContext context) {
container = StateContainer.of(context);
return new Scaffold (
key: scaffoldKey,
drawer: Drawer(
child: CategoryRoute(),
),
appBar: AppBar(
title: Text(widget.topText),
),
bottomNavigationBar: BottomAppBar(
child: new Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(icon: Icon(Icons.camera_alt), onPressed: scan,),
IconButton(icon: Icon(Icons.search), onPressed: _showModalSheet,),
],
),
),
body: robotList.length > 0 ? ListView(children: robotList) : Center(child: Text("Please scan a robot.", style: TextStyle(fontSize: 24.0, color: Colors.blue),),),
);
}
void _showModalSheet() {
showModalBottomSheet(
context: context,
builder: (builder) {
return _searchBar(context);
});
}
void _showSnackbar(String message, String route) {
scaffoldKey.currentState.showSnackBar(new SnackBar(
content: new Text(message),
action: SnackBarAction(
label: 'Go?',
onPressed: () {
if (route == '/') {
Navigator.popUntil(context,ModalRoute.withName('/'));
}
else {
Navigator.of(context).pushNamed(route);
}
},),
duration: Duration(seconds: 5),));
}
Widget _searchBar(BuildContext context) {
return new Scaffold(
body: Container(
height: 75.0,
color: iam_blue,
child: Center(
child: TextField(
style: TextStyle (color: Colors.white, fontSize: 18.0),
autofocus: true,
keyboardType: TextInputType.number,
onSubmitted: (String submittedStr) {
Navigator.pop(context);
_onSubmit(submittedStr);
},
decoration: new InputDecoration(
border: InputBorder.none,
hintText: 'Scan a tote, robot, UPC, or slot',
hintStyle: TextStyle(color: Colors.white70),
icon: const Icon(Icons.search, color: Colors.white70,)),
),
)));
}
Future scan() async {
try {
String barcode = await BarcodeScanner.scan();
setState(() => this._onSubmit(barcode));
} on PlatformException catch (e) {
if (e.code == BarcodeScanner.CameraAccessDenied) {
setState(() {
print('The user did not grant the camera permission!');
});
} else {
setState(() => print('Unknown error: $e'));
}
} on FormatException{
setState(() => print('null (User returned using the "back"-button before scanning anything. Result)'));
} catch (e) {
setState(() => print('Unknown error: $e'));
}
}
}
Code snippet for the RobotSummary class:
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:test_app/genericStateSummary_static.dart';
import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:test_app/StateDecodeJsonFull.dart';
import 'dart:async';
import 'package:test_app/dataValidation.dart';
class RobotSummary extends StatefulWidget {
final String ipAddress;
final String _port = '5000';
final int state;
final String fullAddress;
final WebSocketChannel channel;
RobotSummary({
Key key,
#required this.ipAddress,
#required this.channel,
this.state = -1,
this.fullAddress = "http://10.1.10.200:5000",
}) : assert(Validator.isIP(ipAddress)),
super(key: key);
#override
_RobotSummaryState createState() => new _RobotSummaryState();
}
class _RobotSummaryState extends State<RobotSummary> {
StreamController<StateDecodeJsonFull> streamController;
#override
void initState() {
super.initState();
streamController = StreamController.broadcast();
}
#override
Widget build(BuildContext context) {
return new Padding(
padding: const EdgeInsets.all(20.0),
child: new StreamBuilder(
stream: widget.channel.stream,
builder: (context, snapshot) {
//streamController.sink.add('{"autonomyControllerState" : 3, "pickCurrentListName" : "69152", "plannerExecutionProgress" : 82, "pickUpcCode" : "00814638", "robotName" : "Adam"}');
return getStateWidget(snapshot);
},
),
);
}
#override
void dispose() {
streamController.sink.close();
super.dispose();
}
}
Based on what Jacob said in his initial comments, I came up with a solution that works and is a combination of his suggestions. The code solution he proposed above can't be implemented (see my comment), but perhaps a modification can be attempted that takes elements of it. For the solution I'm working with now, the builder call for HomePageState becomes as follows:
Widget build(BuildContext context) {
List<RobotSummary> tempList = [];
if (robotList.length > 0) {
tempList.addAll(robotList);
}
container = StateContainer.of(context);
return new Scaffold (
key: scaffoldKey,
drawer: Drawer(
child: CategoryRoute(),
),
appBar: AppBar(
title: Text(widget.topText),
),
bottomNavigationBar: BottomAppBar(
child: new Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(icon: Icon(Icons.camera_alt), onPressed: scan,),
IconButton(icon: Icon(Icons.search), onPressed: _showModalSheet,),
],
),
),
body: robotList.length > 0 ? ListView(children: tempList) : Center(child: Text("Please scan a robot.", style: TextStyle(fontSize: 24.0, color: iam_blue),),),
);
}
The problem is you are holding on to the StatefulWidgets between build calls, so their state is always the same. Try separating RobotSummary business logic from the view logic. Something like
class RobotSummary {
final String ipAddress;
final String _port = '5000';
final int state;
final String fullAddress;
final WebSocketChannel channel;
StreamController<StateDecodeJsonFull> streamController;
RobotSummary({
#required this.ipAddress,
#required this.channel,
this.state = -1,
this.fullAddress = "http://10.1.10.200:5000",
}) : assert(Validator.isIP(ipAddress));
void init() => streamController = StreamController.broadcast();
void dispose() => streamController.sink.close();
}
And then in your Scaffold body:
...
body: ListView.builder(itemCount: robotList.length, itemBuilder: _buildItem)
...
Widget _buildItem(BuildContext context, int index) {
return new Padding(
padding: const EdgeInsets.all(20.0),
child: new StreamBuilder(
stream: robotList[index].channel.stream,
builder: (context, snapshot) {
//streamController.sink.add('{"autonomyControllerState" : 3, "pickCurrentListName" : "69152", "plannerExecutionProgress" : 82, "pickUpcCode" : "00814638", "robotName" : "Adam"}');
return getStateWidget(snapshot); // not sure how to change this.
},
),
);
}