ListView infinite loop when parsing data from API response - flutter

I'm trying to read data from some mock endpoint. Mock endpoint I'm invoking (HTTP GET) is here.
Essentially, the JSON structure is result > toolList[] > category > tools[]. I'd like to display these items on my page in such a way that the category name is displayed first, then items belonging to that category under it. I am trying to achieve this with ListView.builder but I somehow managed to get some sort of infinite loop and the items keep getting populated until my device freezes.
What I'm trying to achieve:
Category Title
Item 1
Item 2
Category Title 2
Item 1
Item 2
Itme 3
And finally the Widget:
class OpticsSelectorWidget extends StatefulWidget {
const OpticsSelectorWidget({Key key}) : super(key: key);
#override
_OpticsSelector createState() => _OpticsSelector();
}
class _OpticsSelector extends State<OpticsSelectorWidget> {
PageController pageViewController;
final scaffoldKey = GlobalKey<ScaffoldState>();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: StandardAppbarWidget(appBarTitle: "some title"),
body: SizedBox(
child: FutureBuilder<ApiCallResponse>(
future: ConfigurationController.getOpticsTools2(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: SizedBox(
width: 50,
height: 50,
child: CircularProgressIndicator(
color: Colors.red,
),
),
);
}
final gridViewGetToolsOpticsResponse = snapshot.data;
var toolCategories = getJsonField(
gridViewGetToolsOpticsResponse.jsonBody,
r'''$.result.toolList''',
).toList();
return Builder(
builder: (context) {
return ListView.builder(itemBuilder: (context, itemIndex) {
final widgets = <Widget>[];
for (int i = 0; i < toolCategories.length; i++) {
var currentToolCategory = getJsonField(
toolCategories[i],
r'''$.category''',
);
widgets.add(Text(
currentToolCategory,
style: Colors.white,
));
var toolListInCategory = getJsonField(
toolCategories[itemIndex],
r'''$.tools''',
);
for (int j = 0; j < toolListInCategory.length - 1; j++) {
var toolDisplayName = getJsonField(
toolListInCategory[j],
r'''$.displayName''',
);
widgets.add(Text(toolDisplayName));
}
}
return SingleChildScrollView(
child: Column(
children: widgets,
));
});
},
);
},
),
),
);
}
}
I'm especially confused about the itemIndex expression. That number I thought would be the item count that I receive from my API call, but I guess I'm mixing something badly.
If it helps, here's the bit where I'm making the API call. But feel free to just grab the JSON your way (from mock response)
static Future<ApiCallResponse> getOpticsTools2() async {
HttpOverrides.global = new MyHttpOverrides();
var client = http.Client();
try {
var response = await client.get(Uri.https('stoplight.io'
, "mocks/ragingtortoise/test/82311857/configuration/tools/optics"));
return createResponse(response, true);
} finally {
client.close();
}
}
static ApiCallResponse createResponse(http.Response response, bool returnBody) {
var jsonBody;
try {
jsonBody = returnBody ? json.decode(response.body) : null;
} catch (_) {}
return ApiCallResponse(jsonBody, response.statusCode);
}
And the return type, which is ApiCallResponse:
class ApiCallResponse {
const ApiCallResponse(this.jsonBody, this.statusCode);
final dynamic jsonBody;
final int statusCode;
bool get succeeded => statusCode >= 200 && statusCode < 300;
}
Finally adding the screen recording of what's happening, if it helps.

In here builder you should use,itemCount parameter
ListView.builder(
itemCount: list.length,
itemBuilder: (context, index) {
return Your Widget;
}),

Create a state variable for future and include itemCount: list.length,
final myFuture = ConfigurationController.getOpticsTools2();
And use it on
child: FutureBuilder<ApiCallResponse>(
future: myFuture ,
builder: (context, snapshot) {

I struggled for so long but clearly, the issue was not passing in the itemCount argument into the ListView.builder() method. Also, the outer loop was invalid as now I need to use the actual itemIndex within the builder. Thanks for pointing out the itemCount all! Here's the fixed code and the solution in case anyone needs it later.
#override
Widget build(BuildContext context) {
final opticsToolsMockResponse = ConfigurationController.getOpticsTools2();
return Scaffold(
backgroundColor: Colors.black,
appBar: StandardAppbarWidget(appBarTitle: "some title"),
body: SizedBox(
child: FutureBuilder<ApiCallResponse>(
future: opticsToolsMockResponse,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: SizedBox(
width: 50,
height: 50,
child: CircularProgressIndicator(
color: Colors.red,
),
),
);
}
final gridViewGetToolsOpticsResponse = snapshot.data;
var toolCategories = getJsonField(
gridViewGetToolsOpticsResponse.jsonBody,
r'''$.result.toolList''',
).toList();
return Builder(
builder: (context) {
return ListView.builder(
itemCount: toolCategories.length,
itemBuilder: (context, itemIndex) {
final widgets = <Widget>[];
var currentToolCategory = getJsonField(
toolCategories[itemIndex],
r'''$.category''',
);
widgets.add(Text(
currentToolCategory,
style: Colors.white,
));
var toolListInCategory = getJsonField(
toolCategories[itemIndex],
r'''$.tools''',
);
for (int j = 0; j < toolListInCategory.length; j++) {
var toolDisplayName = getJsonField(
toolListInCategory[j],
r'''$.displayName''',
);
widgets.add(Text(toolDisplayName));
}
return SingleChildScrollView(
child: Column(
children: widgets,
));
});
},
);
},
),
),
);
}

You just forgot to specify the size of the list, you should do it with the itemCount property in the ListView.builder widget
itemCount: list.length,

Related

Make a list of Flutter Firebase field

Hi, I want to make a list inside the Flutter Firebase field. I'm creating an id for followers in the Field. In Firebase, there is a collection, user ID and followers id in the field. My encodings are as follows. But I'm not making a list. What are the changes I will make?
Followers_card
class FollowersCard extends StatefulWidget {
final snap;
const FollowersCard({
Key? key,
required this.snap,
}) : super(key: key);
#override
State<FollowersCard> createState() => _FollowersCardState();
}
class _FollowersCardState extends State<FollowersCard> {
List<dynamic> followersList = []; // shouldn't use dynamic
getdata() async {
await FirebaseFirestore.instance
.collection("users")
.doc(FirebaseAuth.instance.currentUser!.uid)
.get()
.then((value) async {
// get followerIds
List<String> follwerIds = List.from(value.data()!['followers']);
// loop through all ids and get associated user object by userID/followerID
for (int i = 0; i < follwerIds.length; i++) {
var followerId = follwerIds[i];
var data = await FirebaseFirestore.instance
.collection("users")
.doc(followerId)
.get();
// push that data into followersList variable as we are going
// to use that in listViewBuilder
followersList.add(data);
}
setState(() {});
});
#override
void initState() {
super.initState();
getdata();
}
}
#override
Widget build(BuildContext context) {
// use the listView builder to render the list of followers card
return SingleChildScrollView(
physics: NeverScrollableScrollPhysics(),
child: ListView.builder(
shrinkWrap: true,
itemCount: followersList.length,
itemBuilder: (context, index) {
var followerItem = followersList[index];
print('photoUrl');
return _buildFollowersCard(
followerItem['photoUrl'], followerItem['username']);
}),
);
}
Widget _buildFollowersCard(String photoUrl, String username) {
return Container(
height: 70,
width: double.infinity,
color: mobileBackgroundColor,
child: Card(
child: Column(children: [
//Header
Container(
height: 40,
width: double.infinity,
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 16,
).copyWith(right: 0),
child: Row(
children: [
CircleAvatar(
radius: 16,
backgroundImage: NetworkImage(
photoUrl,
),
),
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
username,
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
),
],
),
)
]),
),
);
}
}
followers_screen
class FollowersScreen extends StatefulWidget {
const FollowersScreen({Key? key}) : super(key: key);
#override
State<FollowersScreen> createState() => _FollowersScreenState();
}
class _FollowersScreenState extends State<FollowersScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: mobileBackgroundColor,
centerTitle: true,
title: Image.asset(
'Resim/logo.png',
height: 50,
),
),
body: StreamBuilder(
stream: FirebaseFirestore.instance.collection('users').snapshots(),
builder: (context,
AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) => FollowersCard(
snap: snapshot.data!.docs[index].data(),
),
);
},
),
);
}
}
The problem is that _FollowersScreenState.initState is in the wrong place. It's inside the function getdata that it is trying to call. The initState is never called. That's why there is no list being built.
Also, setState is the one that assigns State values. So first, populate a temporary list of followers and then assign it to the State one inside the setState callback.
Below is the fixed snippet code for _FollowersScreenState:
class _FollowersCardState extends State<FollowersCard> {
List<dynamic> followersList = []; // shouldn't use dynamic
getdata() async {
List<dynamic> followers = [];
final currentUserSnapshot = await FirebaseFirestore.instance
.collection('users')
.doc(FirebaseAuth.instance.currentUser!.uid)
.get();
// get followerIds
List<String> follwerIds =
List.from(currentUserSnapshot.data()!['followers']);
// loop through all ids and get associated user object by userID/followerID
for (int i = 0; i < follwerIds.length; i++) {
var followerId = follwerIds[i];
var data = await FirebaseFirestore.instance
.collection('users')
.doc(followerId)
.get();
// push that data into the temp list variable as we are going
// to use that in to setState
followers.add(data);
}
setState(() => followersList = followers);
}
#override
void initState() {
super.initState();
getdata();
}
...

How can i use a Future Int using a Provider?

I am trying to show a live count of total documents in an Appbar. I get the right information in my console, but when i try to pass it with an Provider it returns an Instance of 'Future'. Can someone tell why i am getting still an Instence even if i await the result and the result is printed correctly in my console?
this is where i get the Future int and print the result to my console.
class AuthenticationService extends ChangeNotifier {
Future<int> totalJumps(jumpDict) async {
var respectsQuery = _db.collection(jumpDict);
var querySnapshot = await respectsQuery.get();
var result = querySnapshot.docs.length;
print(result);
// notifyListeners();
return result;
}
}
This is were it should show the result as a int in the title of the appBar
class LazyListOnline extends StatefulWidget {
static const String id = 'Lazy_list_online';
#override
_LazyListOnlineState createState() => _LazyListOnlineState();
}
class _LazyListOnlineState extends State<LazyListOnline> {
#override
Widget build(BuildContext context) {
String userDict = Provider.of<AuthenticationService>(context).findJumpDict;
var _firestoreDb =
FirebaseFirestore.instance.collection(userDict).snapshots();
var totalJump = Provider.of<AuthenticationService>(context)
.totalJumps(userDict)
.toString();
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
Navigator.popAndPushNamed(context, HomeDrawer.id);
}),
title: Text('totalJumps'),
body: Stack(children: [
Padding(
padding: const EdgeInsets.all(18.0),
child: Container(
decoration: BoxDecoration(),
),
StreamBuilder<QuerySnapshot>(
stream: _firestoreDb,
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
return ListView.builder(
itemCount: snapshot.data.docs.length,
itemBuilder: (context, int index) {
return JumpItem(
snapshot: snapshot.data,
index: index,
);
});
}),
]),
);
}
}
I think you just need to store only the amount of documents as an int in Provider like.
class DocumentData extends ChangeNotifier {
int documentLength;
void setCurrentLengthOfDocuments(int length) {
this. documentLength = length;
notifyListeners();
}
}
Then in StreamBuilder. Every time data has been changed. You just need to update. Regrading to your example be something like.
StreamBuilder<QuerySnapshot>(
stream: _firestoreDb,
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
// Update amount of documents length
Provider.of<DocumentData>(context, listen: false)
.setCurrentLengthOfDocuments(lengthOfCurrentDocuments);
return ListView.builder(
itemCount: snapshot.data.docs.length,
itemBuilder: (context, int index) {
return JumpItem(
snapshot: snapshot.data,
index: index,
);
});
}),
]),
Then when you can get the length of documents every where on this page by just use Consumer widget. Or get the value directly from Provider.

Flutter how to change the background color of a selected tile from a ListTile

I am trying to change the background of a selected tile from a ListTile.
I searched and found the following two posts, however non of them worked with my problem.
Post1
Post2
The better I got was with the help from #CopsOnRoad's answere.
With the following code, if I select multiple tiles, all remain select. How to select only one at the time and deselect the previous selected?
The tile index is limited by itemCount: is books.length.
List<Favorited> books;
// todo: this needs to be changed, has a hard coded value of 200
List<bool> _selected = List.generate(200, (i) => false); // Pre filled list
#override
Widget build(BuildContext context) {
final booksProvider = Provider.of<Model>(context);
return Container(
child: StreamBuilder(
stream: booksProvider.getUserFavList('103610812025'),
builder: (context, AsyncSnapshot<List<Favorited>> snapshot) {
if (snapshot.hasData) {
books= snapshot.data.toList();
return ListView.builder(
itemCount: books.length,
itemBuilder: (buildContext, index) {
return Container(
color: _selected[index] ? Colors.amber : Colors.transparent,
child: ListTile(
title: InkWell(
child: Text(snapshot.data[index].title),
onTap:() {
setState(() {
_selected[index] = !_selected[index];
});
}),
subtitle: Text(snapshot.data[index].name),
),
);
});
} else {
return Text('Fetching');
}
}),
);
Let a one variable to save selected tile index.
List<Favorited> books;
// todo: this needs to be changed, has a hard coded value of 200
List<bool> _selected = List.generate(200, (i) => false); // Pre filled list
int selectedIndex;
#override
Widget build(BuildContext context) {
final booksProvider = Provider.of<Model>(context);
return Container(
child: StreamBuilder(
stream: booksProvider.getUserFavList('103610812025'),
builder: (context, AsyncSnapshot<List<Favorited>> snapshot) {
if (snapshot.hasData) {
books= snapshot.data.toList();
return ListView.builder(
itemCount: books.length,
itemBuilder: (buildContext, index) {
return Container(
color: selectedIndex == index ? Colors.amber : Colors.transparent,
child: ListTile(
title: InkWell(
child: Text(snapshot.data[index].title),
onTap:() {
setState(() {
selectedIndex = index;
});
}),
subtitle: Text(snapshot.data[index].name),
),
);
});
} else {
return Text('Fetching');
}
}),
);

How to Refresh the UI in ListView.Builder using flutter GetX when data is changed?

I'm refactoring my app to GetX state management for less boilerplate code.
I make the Controller and the API provider (code below).
But when I want to refresh the data (Manually too) it won't change.
home_page.dart
class HomeUI extends GetView<HomeController> {
...
GetX<HomeController>(
initState: (state) => Get.find<HomeController>().getAll(),
builder: (_) {
return _.goalList.length < 1 ||
_.goalList == null
? Center(
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
Text('0 goals found, please wait',
style: Theme.of(context)
.textTheme
.headline6
.copyWith(
color: kTextColor))
],
))
: ListView.builder(
itemBuilder: (context, index) {
GoalModel goalModel =
GoalModel.fromMap(
_.goalList[index]);
return ListTile(
title: Text(goalModel.text),
subtitle:
Text(goalModel.updated_at),
);
});
}
home_controller.dart
class HomeUI extends GetView<HomeController> {
...
class HomeController extends GetxController {
final MyRepository repository = MyRepository();
final _goalsList = RxList();
get goalList => this._goalsList.value;
set goalList(value) => this._goalsList.value = value;
getAll() {
repository.getAll().then((data) {
this.goalList = data;
update();
});
}
delete(id) {
repository.delete(id).then((message) {
this.goalList;
return message;
});
}
add(goal) {
repository.add(goal).then((data) {
this.goalList = data;
});
}
edit(editedItem, text, achievementDate) {
repository.edit(editedItem, text, achievementDate).then((data) {
this.goalList = data;
});
}
}
goals_repository.dart
class MyRepository {
final MyApiClient apiClient = MyApiClient();
getAll() {
return apiClient.getAll();
}
delete(id) {
return apiClient.deleteGoal(id);
}
edit(editedItem, text, achievementDate) {
return apiClient.updateGoal(editedItem, text, achievementDate);
}
add(goal) {
return apiClient.postGoal(goal);
}
}
api.dart (getAll() method)
getAll() async {
try {
var _token = await _sharedPrefsHelper.getTokenValue();
var response = await httpClient.get(baseUrl, headers: {
'Authorization': 'Bearer $_token',
});
if (response.statusCode == 200) {
print('json decode response is: ${json.decode(response.body)}');
return json.decode(response.body);
} else
print('erro -get');
} catch (error) {
print(error);
}
}
I followed this article to make the implementation:
getx_pattern
After updating manually your list, do:
this._goalsList.refresh()
After that your UI will be updated
Just Wrap the ListView.builder list with Obx or Getx. For widgets that are not in the list, you can wrap them individually with obx or getx.
Example:
Obx(() => ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: item.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return Card()...
},
),
),
Obs Getx variables are only observed within an Obx or Getx as stated above. You need to wrap them up. Just be careful not to use Obx / Getx when there are no variables observed inside, as it will generate an error.
This answer is for #mjablecnik's comment:
class Other extends StatelessWidget {
final Counter c = Get.find();
final _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
final Random _rnd = Random();
/* ---------------------------------------------------------------------------- */
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Obx(() => ListView.builder(
scrollDirection: Axis.vertical,
padding: EdgeInsets.all(10),
itemCount: c.testList.length,
itemBuilder: (context, index) => Card(
color: Colors.amber[600],
child: Padding(
padding: const EdgeInsets.all(10),
child: Center(
child: Text('${c.testList[index]}'),
),
),
),
)),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => c.addToList(getRandomString(15)),
),
);
}
/* ---------------------------------------------------------------------------- */
// source: https://stackoverflow.com/questions/61919395/how-to-generate-random-string-in-dart
String getRandomString(int length) => String.fromCharCodes(Iterable.generate(
length, (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length))
)
);
}
Update 1:
Another little change I did was for the controller:
class Counter extends GetxController {
var count = 0.obs;
var testList = <String>['test1', 'test2'].obs;
/* ---------------------------------------------------------------------------- */
void incremenent() => count++;
/* ---------------------------------------------------------------------------- */
void addToList(String item) {
print('adding: $item');
testList.add(item);
}
}

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.