CircularProgressIndicator not animating - flutter

I have two widgets that both use the same Future to display CircularProgressIndicators and then show the widget once the future completes. But the CircularProgressIndicators do not animate, so they are just small squares. They animate the very first time after a full compile, but they do not animate ever again, not even after an app refresh. I have tried with other animating widgets to confirm, and they are indeed static:
#override
Widget build(BuildContext context) {
_mapLoadedNotifier = ref.read(mapLoadedProvider.notifier);
_mapLoaded = ref.watch(mapLoadedProvider);
theVm = ref.watch(searchPageVmProvider);
return Row(children: [ _mapOverlay(),_mapWidget(),]);
}
Widget _mapOverlay() {
return theVm.when(
data: (store) {
vm = store;
if (!vm!.hasSearched) {
vm!.isMapLoading = true;
}
vm!.ref = ref;
return SearchFormBuilder(
initState: vm!.initState,
model: Search(search: SearchByTermSuggestion('')),
builder: (context, formModel, child) {
return Container(
padding: PAD_M_TOP,
alignment: Alignment.topLeft,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: PAD_M),
width: sizingInfo.maxWidthS,
color: Colors.transparent,
child: VpCombinationAutocompleteField(
formControl: formModel.searchControl,
labelText: '',
hintText: 'Search',
widgetRef: vm!.ref!,
widgetOnSuggestionSelected:
(Suggestion suggestion) =>
suggestion.onSelected())));
});
},
error: (error, s) => Text(error.toString()),
loading: () {
return const Center(
child: SizedBox(
height: 30,
width: 30,
child: Center(child: CircularProgressIndicator())),
);
});
}
Widget _mapWidget() {
return FutureBuilder<SearchPageVm>(
future: ref.watch(searchPageVmProvider.future),
builder: ((context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: SizedBox(
height: 30,
width: 30,
child: Center(child: CircularProgressIndicator())),
);
}
vm = snapshot.data;
return StreamBuilder<dynamic>(
stream: vm!.map$,
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
vm!.vpMap = snapshot.data;
if (!vm!.hasSearched) {
vm!.isMapLoading = true;
}
vm!.ref = ref;
}
return _googleMap(vm!);
});
}));
}
When I remove the StreamBuilder, they both animate correctly. This does not appear to be a riverPod issue, since I have tried plain Flutter FutureBuilders and it has the same issue. I've tried so many alternatives. The StreamBuilder stops the FutureBuilders CircularProgressIndicators from animating. Why?
It is the same issue as here:
Flutter CircularProgressIndicator() animation is not working inside Riverpod`s ref.watch().when
The provider:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:vepo/src/_common/extensions/vegan_item_establishments.dart';
import '../../../_common/constants/presentation/colors.dart';
import '../../../_common/enums/vegan_item_type.dart';
import '../../../_common/services/theme_mode.dart';
import '../../../_common/services/user.dart';
import '../../../application/local/form_capabilities.dart';
import '../../../application/local/map_data/map.dart';
import '../../../application/local/suggestion/search_by_term/search_by_term_suggestion.dart';
import '../../../application/search_params/vegan_item/vegan_item_search_params.dart';
import '../../../domain/vegan_item_establishment/_common/vegan_item_establishment.dart';
import 'search.dart';
import 'search_results_provider.dart';
final searchPageVmProvider = FutureProvider<SearchPageVm>((ref) async {
final userWithLocation = await ref.watch(userWithLocationProvider.future);
final vm = SearchPageVm(
userWithLocation: userWithLocation,
);
vm.search();
return vm;
});
class SearchPageVm {
bool hasSearched = false;
late GoogleMap? googleMapWidget;
bool isMapLoading = true;
VpMap? vpMap;
WidgetRef? ref;
ThemeMode? themeMode;
Color? statusBarColor;
String? searchText;
Stream<VpMap>? map$;
late final StreamController<VpMap> mapController =
StreamController.broadcast();
late UserWithLocation _userWithLocation;
Set<Marker> markers = <Marker>{};
Stream<String?>? searchTerm;
GoogleMapController? googleMapController;
SearchPageVm({required UserWithLocation userWithLocation}) {
map$ = mapController.stream;
_userWithLocation = userWithLocation;
}
#override
Search get model => Search(search: SearchByTermSuggestion(''));
Position get userPosition => _userWithLocation.userPosition;
/// Add an array of search results to the map
void addResults(Iterable<VgnItmEst> searchResults) {
mapController.add(VpMap(
position: _userWithLocation.userPosition,
markers: searchResults.markers));
_animateCamera(searchResults);
}
#override
void initState(BuildContext context, SearchForm formModel, [dynamic args]) {
if (!hasSearched) {
search();
hasSearched = true;
}
themeMode = ref?.read(themeModeServiceProvider);
statusBarColor = themeMode == ThemeMode.light
? BACKGROUND_COLOR
: DARK_THEME_BACKGROUND_COLOR_LIGHT;
}
/// Search for results and add them to the map
Future<List<VgnItmEst>?> search(
{String searchTerm = '', VgnItmType? category}) async {
final searchResults =
await _search(searchTerm: searchTerm, category: category);
addResults(searchResults ?? []);
return searchResults;
}
void _animateCamera(Iterable<VgnItmEst> searchResults) {
googleMapController?.animateCamera(CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(
searchResults.first.establishment!.location!.latitude,
searchResults.first.establishment!.location!.longitude),
zoom: 13)));
}
Future<List<VgnItmEst>?> _search(
{required String searchTerm, VgnItmType? category}) async {
final result = await ref?.watch(searchResultsProvider(VgnItmSearchParams(
searchTerm: searchTerm,
page: 1,
pageSize: 10,
vgnItmDiscriminators: category != null ? [category.name] : null))
.future);
return result;
}
}
final searchResultsProvider =
FutureProvider.family<List<VgnItmEst>, VgnItmSearchParams>(
(ref, searchParams) async {
List<VgnItmEst>? searchResults;
final allVgnItmsGooglePlacesRepo = ref.read(allVgnItmEstsRepoProvider);
searchResults = (await allVgnItmsGooglePlacesRepo.search(searchParams))?.data;
return searchResults!;
});

Just Make your own progress dialog for just do single change and change will reflect whole code.
class ProgressBar extends StatelessWidget {
final Color color;
ProgressBar({this.color = Colors.amber});
Widget build(BuildContext context) {
return Center(
child: SizedBox(
height: 40,
width: 40,
child: CircularProgressIndicator(color:color))
);}

The way you handle different state of your FutureBuilder is wrong, also when you use FutureBuilder, you should use ref.read, you need to change your _mapWidget to this:
Widget _mapWidget() {
return FutureBuilder<SearchPageVm>(
future: ref.read(searchPageVmProvider.future), // <=== change this
builder: (context, snapshot) { // <=== change this
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return const Center(
child: SizedBox(
height: 30,
width: 30,
child: Center(child: CircularProgressIndicator())),
);
default:
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
vm = snapshot.data;
return StreamBuilder<dynamic>(
stream: vm!.map$,
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
vm!.vpMap = snapshot.data;
if (!vm!.hasSearched) {
vm!.isMapLoading = true;
}
vm!.ref = ref;
}
return _googleMap(vm!);
});
}
}
});
}

Related

API's data is not showing on Listview using flutter

I'm trying to get data from APIs, The data is being successfully fetched from the server but the issue is that when the data is provided to Listview it cant be shown. How can I show the data on Listview in a flutter/dart?
Following is the code for fetching data from API's
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
Map mapResponse = {};
Map dataResponse = {};
List listResponse = {} as List;
class teamapilab extends StatefulWidget {
#override
State<teamapilab> createState() => _teamapilab();
}
// ignore: camel_case_types
class _teamapilab extends State<teamapilab> {
Future team() async {
http.Response response;
response = await http
.get(Uri.parse("https://www.archmage.lk/api/v1/webapi/get-teams"));
if (response.statusCode == 200) {
setState(() {
//stringResponse = response.body;
mapResponse = json.decode(response.body);
listResponse = mapResponse['data'];
});
}
}
#override
void initState() {
team();
super.initState();
}
#override
Widget build(BuildContext context) {
// ignore: avoid_unnecessary_containers
return ListView.builder(
itemBuilder: (context, index) {
return Container(
child: Column(children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: CircleAvatar(
backgroundImage:
NetworkImage(listResponse[index]['member_image']),
),
),
Text(listResponse[index]['name'].toString()),
Text(listResponse[index]['status'].toString()),
]),
);
},
// ignore: unnecessary_null_comparison
itemCount: listResponse == null ? 0 : listResponse.length,
);
}
}
It is Not good to use async in initState instead using FutureBuilder. Try this:
class Teamapilab extends StatefulWidget {
#override
State<Teamapilab> createState() => _Teamapilab();
}
class _Teamapilab extends State<Teamapilab> {
Future<List> team() async {
http.Response response;
response = await http
.get(Uri.parse("https://www.archmage.lk/api/v1/webapi/get-teams"));
if (response.statusCode == 200) {
Map mapResponse = json.decode(response.body);
return mapResponse['data'] as List;
} else {
return [];
}
}
#override
void initState() {
// team();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FutureBuilder<List?>(
future: team(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Text('Loading....');
default:
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
List data = snapshot.data ?? [];
return ListView.builder(
itemBuilder: (context, index) {
return Column(children: [
Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
),
height: 50,
width: 50,
child: Image(
errorBuilder: (context, object, trace) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.red,
),
);
},
image: NetworkImage(
data[index]['member_image'] ?? '',
)),
),
Text('${data[index]['name']}'),
Text('${data[index]['status']}'),
]);
},
itemCount: data.length,
);
}
}
}),
),
);
}
}
List listResponse = {} as List; should be replaced with List listResponse = [] as List;
In dart {} is empty map and [] is empty list.
You can use below code.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Map mapResponse = {};
Map dataResponse = {};
List listResponse = {} as List;
class teamapilab extends StatefulWidget {
#override
State<teamapilab> createState() => _teamapilab();
}
// ignore: camel_case_types
class _teamapilab extends State<teamapilab> {
bool dataLoaded = false;
Future team() async {
http.Response response;
response = await http
.get(Uri.parse("https://www.archmage.lk/api/v1/webapi/get-teams"));
if (response.statusCode == 200) {
setState(() {
//stringResponse = response.body;
mapResponse = json.decode(response.body);
print("DAata " + mapResponse.toString());
listResponse = mapResponse['data'];
print("DAata " + listResponse.toString());
dataLoaded = true;
setState(() {});
});
}
}
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) async{
await team();
});
}
#override
Widget build(BuildContext context) {
// ignore: avoid_unnecessary_containers
return Scaffold(
body: dataLoaded
? ListView.builder(
scrollDirection: Axis.vertical,
itemBuilder: (context, index) {
return Column(children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: CircleAvatar(
backgroundImage:
NetworkImage(listResponse[index]['member_image']),
),
),
Text(listResponse[index]['name'].toString()),
Text(listResponse[index]['status'].toString()),
]);
},
// ignore: unnecessary_null_comparison
itemCount: listResponse == null ? 0 : listResponse.length,
)
: const CircularProgressIndicator(),
);
}
}

How to add List Item to FutureBuilder ListView without reloading the data from remote server? [Flutter]

I'm new in flutter, I'd like to know how to add an item list dynamically to ListView without reloading data in FutureBuilder.
When I add an item to the ListView, it duplicate the list and then added the item to that list.
The Following code, include Model clas called Job.
JobListView is a stateful widget that include the dynamic ListView.
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
class Job {
#required
String company;
String description;
String employmentType;
int id;
String location;
String position;
List<String> skillsRequired;
Job(
this.company,
this.description,
this.employmentType,
this.id,
this.location,
this.position,
this.skillsRequired);
Job.fromJson(Map<String, dynamic> json) {
company = json['company'];
description = json['description'];
employmentType = json['employmentType'];
id = json['id'];
location = json['location'];
position = json['position'];
if (json['skillsRequired'] != null) {
skillsRequired = new List<String>();
json['skillsRequired'].forEach((v) {
skillsRequired.add(v);
});
}
}
}
class JobListView extends StatefulWidget {
#override
_JobListViewState createState() => _JobListViewState();
}
class _JobListViewState extends State<JobListView> {
List<Job> data = List<Job>();
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<List<Job>>(
future: _getJob(),
builder: (context, snapshot) {
if (snapshot.hasData) {
data = snapshot.data;
return _listViewFormat(data);
} else if (snapshot.hasError) {
return Container();
}
return Center(
child: Container(
width: 50,
height: 50,
child: CircularProgressIndicator(),
),
);
},
) ,
floatingActionButton: (FloatingActionButton(child: Icon(Icons.add),onPressed: (){
setState(() {
var j = Job("CompanyX","Eng.5 position","Full-time",0,"Cairo","Senior",null);
data.add(j);
});
},)),
);
}
}
ListView _listViewFormat(List<Job> data) {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return _tile(data[index].position, data[index].description, Icons.work);
});
}
ListTile _tile(String title, String subtitle, IconData iconData) {
return ListTile(
title: Text(title, style: TextStyle(fontSize: 20)),
subtitle: Text(
subtitle,
style: TextStyle(fontSize: 12),
),
leading: Icon(iconData),
trailing: Icon(Icons.arrow_right),
);
}
Future<List<Job>> _getJob() async {
String baseUrl = 'https://mock-json-service.glitch.me';
var response = await get(baseUrl);
if (response.statusCode == 200) {
List jsonResponse = json.decode(response.body);
return jsonResponse.map((job) => new Job.fromJson(job)).toList();
}
}
Check out this more explanation How to deal with unwanted widget build?
if future changes you will see changes
Move _getJob method inside initState like this:
class _JobListViewState extends State<JobListView> {
List<Job> data = List<Job>();
Future<List<Job>> getJobFuture;
#override
void initState() {
super.initState();
getJobFuture = _getJob();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<List<Job>>(
future: getJobFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
data = snapshot.data;
return _listViewFormat(data);
} else if (snapshot.hasError) {
return Container();
}
return Center(
child: Container(
width: 50,
height: 50,
child: CircularProgressIndicator(),
),
);
},
) ,
floatingActionButton: (FloatingActionButton(child: Icon(Icons.add),onPressed: (){
setState(() {
var j = Job("CompanyX","Eng.5 position","Full-time",0,"Cairo","Senior",null);
data.add(j);
});
},)),
);
}
}

how to make this lazyload scrolling working with provider

it take about 7 days trying make a working example for lazyload listview with provider in flutter with real world example and it's still not working because i think something is missing
As a note : the first load , works good and when i scroll it's print (scroll) but nothing happened it's still in the same page
if i try to return _todolist variable in the _onScrollUpdated it not change page correctly and after three times i see this error
E/flutter ( 7713): [ERROR:flutter/lib/ui/ui_dart_state.cc(166)]
Unhandled Exception: type 'String' is not a subtype of type
'List' E/flutter ( 7713): #0 TodoService.fetchTodos
(package:flutter_todo_provider/services/todo_service.dart:32:21)
json example
https://jsonformatter.org/52c83e
todos_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_todo_provider/helpers/http_exception.dart';
import 'package:provider/provider.dart';
import 'package:flutter_todo_provider/.env.dart';
import 'package:flutter_todo_provider/services/todo_service.dart';
class TodosScreen extends StatefulWidget {
#override
_TodosScreenState createState() => _TodosScreenState();
}
class _TodosScreenState extends State<TodosScreen> {
ScrollController _controller;
List<dynamic> _todoList;
bool _isLoading ;
#override
void initState() {
super.initState();
_controller = ScrollController();
_controller.addListener(_onScrollUpdated);
}
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(Configuration.AppName),
),
body: FutureBuilder(
future: _fetchListItems(),
builder: (context, snapshot){
if(snapshot.hasData){
return _listItems(snapshot.data);
}
return _buildProgressIndicator();
}
),
);
}
_fetchListItems() async {
try {
await Provider.of<TodoService>(context, listen: false).loadNextPage();
_todoList = Provider.of<TodoService>(context, listen: false).items;
} on HttpException catch (e) {
EasyLoading.showError(e.message);
}
return _todoList ;
}
Widget _listItems(data){
_isLoading = Provider.of<TodoService>(context, listen: false).isLoading ;
return ListView.builder(
controller: _controller,
itemCount: data.length ,
itemBuilder: (context, index) {
return ListTile(
title: Text(data[index].title),
subtitle:Text(data[index].content),
trailing: Icon(Icons.print),
);
},
);
}
Future<void> _onScrollUpdated() async {
print("Scroll11");
var maxScroll = _controller.position.maxScrollExtent;
var currentPosition = _controller.position.pixels;
if (currentPosition == maxScroll ) {
try {
await Provider.of<TodoService>(context, listen: false).loadNextPage();
_todoList = Provider.of<TodoService>(context, listen: false).items;
// return _todoList ; if use this line i see the error
} on HttpException catch (e) {
EasyLoading.showError(e.message);
}
}
}
Widget _buildProgressIndicator() {
_isLoading = Provider.of<TodoService>(context, listen: false).isLoading ;
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: _isLoading ? 1.0 : 00,
child: new CircularProgressIndicator(),
),
),
);
}
}
todo_service.dart
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_todo_provider/.env.dart';
import 'package:flutter_todo_provider/models/todo.dart';
class TodoService with ChangeNotifier {
bool isLoading = false;
bool isFetching = false;
int currentPage = 1;
int totalRows = 10;
List<Todo> items = [];
loadNextPage() async {
await fetchTodos(currentPage);
currentPage++;
notifyListeners();
}
Future fetchTodos(int currentPage) async {
try {
//404
var options = Options(headers: {
HttpHeaders.authorizationHeader: 'Basic ${Configuration.authToken}'
});
Map<String, dynamic> qParams = {
'current_page': currentPage,
};
Response response = await Dio().get('${Configuration.ApiUrl}/todos/my_todos', options: options, queryParameters: qParams);
List<dynamic> responseBode = response.data["data"];
responseBode.forEach(( dynamic json) {
items.add(Todo.fromJson(json));
});
notifyListeners();
} on DioError catch (e) {
print("Error Message" + e.response.statusMessage);
return items=[];
}
}
}
Here is the code:
class TodoScreen extends StatefulWidget {
// Your service goes here
// (the class extending ChangeNotifier)
#override
_TodoScreenState createState() => _TodoScreenState();
}
class _TodoScreenState extends State<TodoScreen> {
final TodoService todoService = TodoService();
ScrollController _controller;
#override
void initState() {
super.initState();
_controller = ScrollController();
_controller.addListener(_onScrollUpdated);
loadNextPage();
}
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Configuration.AppName'),
),
body: ChangeNotifierProvider.value(
value: todoService,
child: Consumer<TodoService>(builder: (_, ctl, __) {
if (todoService.isLoading) {
return _buildProgressIndicator();
} else {
return _listItems(todoService.items);
}
}),
),
);
}
Widget _listItems(data) {
return ListView.builder(
controller: _controller,
itemCount: data.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(data[index].title),
subtitle: Text(data[index].content),
trailing: Icon(Icons.print),
);
},
);
}
Widget _buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: CircularProgressIndicator(),
),
);
}
Future<void> _onScrollUpdated() async {
var maxScroll = _controller.position.maxScrollExtent;
var currentPosition = _controller.position.pixels;
if (currentPosition == maxScroll) {
todoService.loadNextPage();
}
}
}
Note that i didn't make changes to your service. The notifyListeners will do all the job for us.
When you are using Provider, the idea is to keep all your data inside the controller or service (the class that extends ChangeNitifier) and just use the variables with notifyListeners to change the behavior of your screen.
The screen needs to be listening for changes, for this we use the pair ChangeNotifierProvider.value with Consumer(builder: (_, ctl, __) {}).
Use ChangeNotifierProvider in some upper level of the widget tree and use Consumer only where you need the widget to be updated. You can even use more than one Consumer, they all just need to be under ChangeNotifierProvider.

How to change items of a gridview in flutter

I am new to flutter and i current have an app that has a grid view that gets its list from an api. Some of the grid view items have child nodes in them, so what i want to achieve is to set a click function that checks if there is a child node and if that is true; i would want to re-populate the same grid view but with only members of the child node. is this possible in flutter?
import 'package:bringam/network/Models/ProductGroupModel.dart';
import 'package:bringam/network/sharedpreferences/SharedPreferences.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:http/http.dart' as http;
import 'dart:convert';
class Product_Category extends StatefulWidget {
#override
_Product_CategoryState createState() => _Product_CategoryState();
}
class _Product_CategoryState extends State<Product_Category> {
Future<List<ProductGroupModel>> _getChildrenCategories(String tag) async {
List<ProductGroupModel> categories = [];
SharedPref sharedPref = SharedPref();
var cacheCategories =
json.decode(await sharedPref.read('PRODUCT_CATEGORY'));
// FILTERING THE LIST STARTS
var filteredJson =
cacheCategories.where((i) => i["ParentGroupId"] == tag).toList();
// FILTERING THE LIST ENDS
for (var u in filteredJson) {
ProductGroupModel productCat = ProductGroupModel(
u["Description"],
u["IconURL"],
u["ProductGroup"],
u["ParentGroupId"],
u["HasChildNode"],
u["Order"]);
categories.add(productCat);
}
print(categories);
return categories;
}
Future<List<ProductGroupModel>> _getCategories() async {
List<ProductGroupModel> categories = [];
SharedPref sharedPref = SharedPref();
var cacheCategories =
json.decode(await sharedPref.read('PRODUCT_CATEGORY'));
if (cacheCategories.isEmpty) {
var data = await http.get(
'PRIVATE API ENDPOINT PLEASE');
var jsonData = json.decode(data.body);
// FILTERING THE LIST STARTS
var filteredJson =
jsonData.where((i) => i["ParentGroupId"] == '0').toList();
// FILTERING THE LIST ENDS
for (var u in filteredJson) {
ProductGroupModel productCat = ProductGroupModel(
u["Description"],
u["IconURL"],
u["ProductGroup"],
u["ParentGroupId"],
u["HasChildNode"],
u["Order"]);
categories.add(productCat);
}
} else {
// FILTERING THE LIST STARTS
var filteredJson =
cacheCategories.where((i) => i["ParentGroupId"] == '0').toList();
// FILTERING THE LIST ENDS
for (var u in filteredJson) {
ProductGroupModel productCat = ProductGroupModel(
u["Description"],
u["IconURL"],
u["ProductGroup"],
u["ParentGroupId"],
u["HasChildNode"],
u["Order"]);
categories.add(productCat);
}
return categories;
}
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _getCategories(),
builder: (BuildContext context,
AsyncSnapshot<List<ProductGroupModel>> snapshot) {
if (snapshot.data == null) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return GridView.builder(
itemCount: snapshot.data.length,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount:
2),
itemBuilder: (BuildContext context, int index) {
return Card(
elevation: 0,
color: Colors.transparent,
child: Hero(
tag: snapshot.data[index].ProductGroup,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
bool hasChild = snapshot.data[index].HasChildNode;
if (hasChild == true) {
setState(() {
_getChildrenCategories(
snapshot.data[index].ProductGroup);
});
} else {
Scaffold.of(context).showSnackBar(SnackBar(
content: new Text("Nothing found!"),
duration: const Duration(milliseconds: 500)));
}
},
child: GridTile(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircleAvatar(
backgroundImage:
NetworkImage(snapshot.data[index].IconURL),
radius: 75.0,
),
Text(
snapshot.data[index].Description,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20.0,
color: Colors.white,
),
),
],
),
),
),
),
),
);
});
}
},
);
}
}
//THE MODEL CLASS
class ProductGroupModel {
final String Description;
final String IconURL;
final String ProductGroup;
final String ParentGroupId;
final bool HasChildNode;
final int Order;
ProductGroupModel(
this.Description,
this.IconURL,
this.ProductGroup,
this.ParentGroupId,
this.HasChildNode,
this.Order,
);
}

Provider Object Requests Rebuild During Existing Build

I'm learning Provider and my test app draws images from a Firestore database into a ListView. I'd like a LongPress on any image to make the whole list toggle and redraw with checkbox selector icons, as below, similar to the way the gallery works:
My code works, but it throws an exception on every LongPress stating that "setState() or markNeedsBuild() was called during build," and I'm pulling my hair out trying to figure out how to either delay the ChangeNotifier until the widget tree is built? Or some other way to accomplish this task?
My Provider class simply accepts a List of my PictureModel class and has a toggleSelectors() method which notifies listeners. Here's the code:
class PicturesProvider with ChangeNotifier {
List<PictureModel> _pictures = [];
bool visible = false;
UnmodifiableListView<PictureModel> get allPictures => UnmodifiableListView(_pictures);
UnmodifiableListView<PictureModel> get selectedPictures =>
UnmodifiableListView(_pictures.where((pic) => pic.selected));
void addPictures(List<PictureModel> picList) {
_pictures.addAll(picList);
notifyListeners();
}
void toggleSelectors() {
visible = !visible;
_pictures.forEach((pic) {
pic.selectVisible = visible;
});
notifyListeners();
}
}
I have a SinglePicture UI class that loads a network image into an AspectRatio widget and wraps it with a GestureDetector to toggle the selectors and present them on the top of a Stack widget, like so:
Widget build(BuildContext context) {
int originalHeight, originalWidth;
return AspectRatio(
aspectRatio: pictureModel.aspectRatio,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
FutureBuilder<ui.Image>(
future: _getImage(),
builder: (BuildContext context, AsyncSnapshot<ui.Image> snapshot) {
if (snapshot.hasData) {
ui.Image image = snapshot.data;
originalHeight = image.height;
originalWidth = image.width;
return GestureDetector(
onLongPress: () => Provider.of<PicturesProvider>(context, listen: false).toggleSelectors(),
child: RawImage(
image: image,
fit: BoxFit.cover,
// if portrait image, move down slightly for headroom
alignment: Alignment(0, originalHeight > originalWidth ? -0.2 : 0),
),
);
} else {
return Center(child: CircularProgressIndicator());
}
},
),
Positioned(
left: 10.0,
top: 10.0,
child: pictureModel.selectVisible == false
? Container(
height: 0.0,
width: 0.0,
)
: pictureModel.selected == false
? Icon(
Icons.check_box_outline_blank,
size: 30.0,
color: Colors.white,
)
: Icon(
Icons.check_box,
size: 30.0,
color: Colors.white,
),
)
],
),
);
}
This SinglePicture class is then called from my PicturesList UI class which simply builds a ListView, like so:
class PicturesList extends StatelessWidget {
final List<PictureModel> pictures;
PicturesList({#required this.pictures});
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: pictures.length,
cacheExtent: 3,
itemBuilder: (context, index) {
return SinglePicture(
pictureModel: pictures[index],
);
},
);
}
The whole shebang is then called from a FutureBuilder in my app, which builds the app, like so:
body: FutureBuilder(
future: appProject.fetchProject(), // Snapshot of database
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
// Get all picture URLs from project snapshot
List<dynamic> picUrls = snapshot.data['pictures'].map((pic) => pic['pic_cloud']).toList();
// Create list of PictureModel objects for Provider
List<PictureModel> pictures = picUrls.map((url) => PictureModel(imageUrl: url, imageHeight: 250.0, selectVisible: false)).toList();
// Add list of PictureModel objects to Provider for UI render
context.watch<PicturesProvider>().addPictures(pictures);
return SafeArea(
child: PicturesList(
pictures: context.watch<PicturesProvider>().allPictures,
),
);
} else if (snapshot.hasError) {
print('Error');
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
),
Please, if anybody has a hint about how I can accomplish this toggle action without throwing exceptions, I'd be very grateful. Thank you in advance!
Thanks to Remi Rousselet for the answer:
I have been using .builder methods wrong since the get-go and now need to go revisit ALL of my code and make sure they are clean.
To make this code work, I moved the Future out of my FutureBuilder and called it in the initState method, per Remi's guidance. I also had to create a new initializer method in my Provider class that did NOT notify listeners, so I could build the list for the first time.
Here are the code snippets to make my images 'selectable' with a LongPress and to be able to individually select them with a tap, as seen in the following image:
My PictureModel:
class PictureModel {
final String imageUrl;
final double aspectRatio;
double imageHeight;
bool selectVisible;
bool selected;
PictureModel({
#required this.imageUrl,
#required this.imageHeight,
this.aspectRatio = 4.0 / 3.0,
this.selectVisible = false,
this.selected = false,
});
#override
String toString() {
return 'Image URL: ${this.imageUrl}\n'
'Image Height: ${this.imageHeight}\n'
'Aspect Ratio: ${this.aspectRatio}\n'
'Select Visible: ${this.selectVisible}\n'
'Selected: ${this.selected}\n';
}
}
My PictureProvider model:
class PicturesProvider with ChangeNotifier {
List<PictureModel> _pictures = [];
bool visible = false;
UnmodifiableListView<PictureModel> get allPictures => UnmodifiableListView(_pictures);
UnmodifiableListView<PictureModel> get selectedPictures =>
UnmodifiableListView(_pictures.where((pic) => pic.selected));
void initialize(List<PictureModel> picList) {
_pictures.addAll(picList);
}
void addPictures(List<PictureModel> picList) {
_pictures.addAll(picList);
notifyListeners();
}
void toggleSelected(int index) {
_pictures[index].selected = !_pictures[index].selected;
notifyListeners();
}
void toggleSelectors() {
this.visible = !this.visible;
_pictures.forEach((pic) {
pic.selectVisible = visible;
});
notifyListeners();
}
}
My SinglePicture UI class:
class SinglePicture extends StatelessWidget {
final PictureModel pictureModel;
const SinglePicture({Key key, this.pictureModel}) : super(key: key);
Future<ui.Image> _getImage() {
Completer<ui.Image> completer = new Completer<ui.Image>();
new NetworkImage(pictureModel.imageUrl).resolve(new ImageConfiguration()).addListener(
new ImageStreamListener(
(ImageInfo image, bool _) {
completer.complete(image.image);
},
),
);
return completer.future;
}
#override
Widget build(BuildContext context) {
int originalHeight, originalWidth;
return AspectRatio(
aspectRatio: pictureModel.aspectRatio,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
FutureBuilder<ui.Image>(
future: _getImage(),
builder: (BuildContext context, AsyncSnapshot<ui.Image> snapshot) {
if (snapshot.hasData) {
ui.Image image = snapshot.data;
originalHeight = image.height;
originalWidth = image.width;
return RawImage(
image: image,
fit: BoxFit.cover,
// if portrait image, move down slightly for headroom
alignment: Alignment(0, originalHeight > originalWidth ? -0.2 : 0),
);
} else {
return Center(child: CircularProgressIndicator());
}
},
),
Positioned(
left: 10.0,
top: 10.0,
child: pictureModel.selectVisible == false
? Container(
height: 0.0,
width: 0.0,
)
: pictureModel.selected == false
? Icon(
Icons.check_box_outline_blank,
size: 30.0,
color: Colors.white,
)
: Icon(
Icons.check_box,
size: 30.0,
color: Colors.white,
),
)
],
),
);
}
}
My PicturesList UI class:
class PicturesList extends StatelessWidget {
PicturesList(this.listOfPics);
final List<PictureModel> listOfPics;
#override
Widget build(BuildContext context) {
context.watch<PicturesProvider>().initialize(listOfPics);
final List<PictureModel> pictures = context.watch<PicturesProvider>().allPictures;
return ListView.builder(
itemCount: pictures.length,
cacheExtent: 3,
itemBuilder: (context, index) {
return GestureDetector(
onLongPress: () => Provider.of<PicturesProvider>(context, listen: false).toggleSelectors(),
onTap: () {
if (Provider.of<PicturesProvider>(context, listen: false).visible) {
Provider.of<PicturesProvider>(context, listen: false).toggleSelected(index);
}
},
child: SinglePicture(
pictureModel: pictures[index],
),
);
},
);
}
}
And last but not least, the FutureBuilder in the app from where all of this was called...
body: FutureBuilder(
future: projFuture,
// ignore: missing_return
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
// Get all picture URLs from project snapshot
List<dynamic> picUrls = snapshot.data['pictures'].map((pic) => pic['pic_cloud']).toList();
// Create list of PictureModel objects for Provider
List<PictureModel> pictures = picUrls
.map((url) => PictureModel(imageUrl: url, imageHeight: 250.0, selectVisible: false))
.toList();
// Add list of PictureModel objects to Provider for UI render
// context.watch<PicturesProvider>().addPictures(pictures);
return SafeArea(
child: PicturesList(pictures),
);
} else if (snapshot.hasError) {
print('error');
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
),
Sorry for the long follow-up, but I figured I'd try to detail as much as possible how to make this work, in case it is useful to anybody else. Also, if anybody has further suggestions on how to improve this code, PLEASE let me know.
Thanks in advance.