Flutter ListView with nested ListView throws RangeError for offscreen widgets - flutter

First time working in Flutter and not really a software wizard. I've built an app that connects to Firebase and retrieves a data structure as a Map (all after login). This works fine. However, the data is (kind of) a list of lists (or Map of Maps), etc. Basic structure is:
{
title1: {
item-A: {
0: line1,
1: line2,
2: line3,
},
item-B: {
0: line1,
1: line2,
2: line3,
}
},
title2: {
item-A: {
0: line1,
...
}
The schema can have more or fewer titles, items, and lines, but the structure is the same.
The code I've implemented gets the values from the database (Firebase Realtime Database) via FutureBuilder, formats the results, and displays them. Retrieving the results and the general display formatting works as intended, but I can only see one or two of the list widgets before a huge red box is displayed and I get the following error:
The following IndexError was thrown building:
RangeError (index): Index out of range: index should be less than 1: 1
If I run a Hot Refresh or Hot Reload, or just reload the browser, I get a few more rows of Widgets and the error changes to the next index increment:
The following IndexError was thrown building:
RangeError (index): Index out of range: index should be less than 4: 5
If I keep refreshing, eventually the whole list shows up and the error goes away. Obviously this is not the desired behavior, however. I've been working this and testing different approaches but I don't really understand what the issue is.
My code (with a couple of details left out) is as follows:
Widget build(BuildContext context) {
return Consumer<ApplicationState>(
builder: (context, appState, child) {
selectedIndex.add(-1);
return Scaffold(
...
body: FutureBuilder(
future: futureDataRetreiverFunction(),
builder: (BuildContext context, AsyncSnapshot<Map> snapshot) {
if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
... // convert the snapshot.data here //
return Container(
...
child: ListView(
shrinkWrap: true,
children: [
Form(
key: _formKey,
child: Column(
children: [
ListView.builder(
scrollDirection: Axis.vertical,
controller: ScrollController(),
shrinkWrap: true,
itemCount: snapshotData.keys.length, // the data is a Map
itemBuilder: (BuildContext context, int index) =>
Column(
children: [
...
Row(
children: [
Text(snapshotData.keys.elementAt(index)),
]
),
Row(
children: [
Text(selectedIndex[index].toString())
]
),
Row(
children: [
Flexible(
fit: FlexFit.loose,
child: Padding(
padding: const EdgeInsets.fromLTRB(5, 0, 0, 10),
child: ListView.builder(
shrinkWrap: true,
primary: false,
itemCount: snapshotData.values.elementAt(index)[1].length,
itemBuilder: (BuildContext context, int index2) =>
Text(snaptshotData.values.elementAt(index)[1][index2])
)
)
)]
)
])
)
])
])
);
...

Vishal pointed out the answer in his first comment to this question. I am using "selectedIndex" as part of a setState({}) call to change some visuals as part of an onTap: () {} call. I was initiating the list with a simple initiation call earlier in the class (List selectedIndex = [];), but then was only adding 1 item to the index in the builder. However, in this case, there are a total of 6 items which get built (more/less in other scenarios), and that was throwing the index error. Which is why it would increment on reload (another seletedIndex.add(-1); was called). I dropped in a quick function to pre-populate the index based on the size of the snapshot data, along the lines of:
for (var counter=0; counter<snapshotData.keys.length; counter++) {
selectedIndex.add(-1);
}
This solved the index RangeError.

Related

How to wait for Streambuilder to return a value before coming off a loading screen

Question
I have an app that loads a list of habits as a Stream from Firestore, and I want the app to show a loading screen until habit loading has completed, and then show the list if there is one. Is there a way to keep a loading screen showing until we've either finished loading the first value in the stream or determined there won't be one?
The issue I'm having is that while my app is showing the loading screen, it briefly loads with a "no habits found" view before switching to show the list of habits.
Setup
This view uses three components:
I have a model-view-viewmodel architecture based on the Stacked Package.
(1) My view is a ViewModelBuilder widget with a StreamBuilder inside it. It looks at a (2) DailyViewModel, where the relevant components are an isBusy boolean property and a Stream<List<HabitCompletionViewModel> property that the view's StreamBuilder looks at to display the habits. The habits are loaded from Firestore via a FirestoreHabitService (this is an asynchronous call - will be described in a minute).
The View works as follows:
If DailyViewModel.isBusy is true, show a Loading... text.
If isBusy is false, it will show a Stream of Habits, or the text "No Habits Found" if the stream is not returning any habits (either snapshot.hasData is false, or data.length is less than 1).
#override
Widget build(BuildContext context) {
return ViewModelBuilder.reactive(
viewModelBuilder: () => vm,
disposeViewModel: false,
builder: (context, DailyViewModel vm, child) => vm.isBusy
? Center(child: Text('Loading...'))
: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'BaseDate of ${DateFormat.yMd().format(vm.week.baseDate)}'),
Text(
'Week ${DateFormat.yMd().format(vm.week.beginningOfWeek)} - ${DateFormat.yMd().format(vm.week.endOfWeek)}'),
SizedBox(height: 20),
Row(
children: [
Flexible(
child: Text('Today',
style: Theme.of(context).textTheme.headline6),
),
],
),
const Padding(padding: EdgeInsets.all(2)),
StreamBuilder<List<HabitCompletionViewModel>>(
stream: vm.todaysHabits,
builder: ((context, snapshot) {
if (snapshot.hasData == false ||
snapshot.data == null ||
snapshot.data!.length < 1) {
return Center(child: Text('No Habits Found'));
} else {
return Column(children: [
ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: snapshot.data!.length,
itemBuilder: (context, i) => HabitCompletionTile(
key: ValueKey(snapshot.data![i].habit.id),
vm: snapshot.data![i],
),
),
]);
}
})),
SizedBox(height: 40),
TextButton(
child: Text('Create a New Habit'),
onPressed: () => vm.navigateToCreateHabitPage(),
),
],
),
),
);
}
}
Process of Loading the Data
My loading happens as follows:
The ViewModel is initialized, and setBusy is set to true.
DailyViewModel({required WeekDates week}) {
setBusy(true);
_log.v('initializing the daily viewmodel');
pageSubtitle =
'Week of ${week.startWeekday.title} ${DateFormat.Md().format(week.beginningOfWeek)}';
_log.v('page subtitle is $pageSubtitle');
mainAsyncCode();
}
Then it starts this mainAsyncCode() method, which gets a stream of habits from the FirestoreHabitService (this returns a Future<Stream<List<Habit>>> because there is a potential modification function performed on the habits before returning them), and once that is completed, transforms that stream into a Stream<List<HabitCompletionViewModel>>, and then sets isBusy on the ViewModel to false.
void mainAsyncCode() async {
_myHabits = await _habitService.loadActiveHabitsByUserFuture(_loginAndUserService.loggedInUser!.id!);
todaysHabits = _myHabits!.transform(currentViewModelCompletion);
await Future.delayed(Duration(seconds: 5));
setBusy(false);
}
Issue
The problem is that there is a temporary delay where the screen goes from "Loading..." to "No Habits Found" before it shows the list of Habits. I want it to wait on "Loading" until the stream list has been published, but I can't think of a way to do that.
Are there any options for doing this that others are aware of?

Create a page view with 2 different scrollable list with different index

I want to create a screen that can display a scrollable list.
Based on a condition, I want to be able to display another list.
My program works however the index is the same for the 2 lists so when I scroll a list, the second list is scroll too
My code :
return PageView.builder(
scrollDirection: Axis.vertical,
itemCount: category[state.indexView].length,
itemBuilder: category[state.indexView][0].containsKey('video')
? (context2, index2) {
return Stack(
children: [
VideoWidget(
videoUrl: category[state.indexView][index2]['video'],
asset: true,
videoFile: File(''),
),
if (category[state.indexView][index2].containsKey('image'))
Center(child: Image(image: AssetImage(category[state.indexView][index2]['image']))),
_postContent(d_height)
],
);
}
: (context, index) {
return Stack(
children: [
if (category[state.indexView][index].containsKey('image'))
Center(child: Image(image: AssetImage(category[state.indexView][index]['image']))),
_postContent(d_height)
],
);
});
You can create second page in pageview similar to first page when you want to jump on second page then pass that index to another page and use it as current index in listview.

AnimatedList out of range when StreamBuilder refreshes

StreamBuilder(
stream: users.where("invites", arrayContains: uid).snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasData) {
_items = snapshot.data!.docs;
return AnimatedList(
key: listKey,
initialItemCount: _items.length,
itemBuilder: (context, index, animation) {
return _buildItem(index, _items[index], animation);
},
);
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Center(
child: SizedBox(
height: 50,
width: 50,
child: CupertinoActivityIndicator()),
),
],
);
}
},
)
I got this StreamBuilder with a stream from FirebaseFirestore. E.g. I have 3 entries in Firebase which match my condition (saved in _items). So far, so good. But if I delete one of those, the stream refreshes but the list throw an error: RangeError (index): Invalid value: Not in inclusive range 0..1: 2
Other way round same problem: Only two are shown and the third is not shown.
Does anyone have an idea why or how to fix this.
Thanks :)
This does not work with Animated List because your index is decreasing. You need to build a new Animated List or I would recommend to use a normal list view.

The ParentDataWidget Flexible(flex: 1) wants to apply ParentData of type FlexParentData to a RenderObject

I am trying to apply if condition on the result of a retrieving query from Firebase real-time database. What I am trying to do is to tell flutter if a specific field of the result is equal to what I specified then and only then show the result.
I am getting this error as a result of running my code:
Screenshot of the error message
and my code:
body: SingleChildScrollView(
child: Flexible(
flex: 1,
child: FirebaseAnimatedList(
shrinkWrap: true,
physics: BouncingScrollPhysics(),
query: _dBRefAppt,
itemBuilder: (
BuildContext context,
DataSnapshot snapShot,
Animation<double> animation,
int index,
) {
if (snapShot.value['tutor_id'] == currentUId &&
snapShot.value['status'] == 'accepted') {
return new ListTile(
title: new Text(snapShot.value['date']),
subtitle: new Text(snapShot.value['time']),
);
} else {
return null;
}
},
),
),
),
The problem is when I remove the if condition, the code works and shows all the results. How can I show only the results that I want. Any help is very much appreciated!
Just remove this:
child: Flexible(
flex: 1,
Use Flexible and Expanded only inside Rows, Columns and Flex widgets. Keep the if statement, it's not causing the error.

RangeError (index): Invalid value: Not in range 0..1, inclusive: 2. How can this be fixed?

I am building a static ListView. How can I solve this error? Also how can I add a UI to the ListView?
Following is my class MatchData code:
class MatchData {
String date, team1, team2, time;
MatchData({#required this.date, #required this.team1, #required this.team2, #required this.time});
}
Following is the Data I want to show in the ListView:
final List<MatchData> dayMatch = [
MatchData(
date: '12/02/2020',
team1: 'Mumbai Indians',
team2: 'Bangalore',
time: '16:00'),
MatchData(
date: '12/02/2020',
team1: 'Mumbai Indians',
team2: 'Bangalore',
time: '16:00')
];
match() {
return dayMatch;
}
Following is the body of my Widget:
body: Center(child: ListView.builder(itemBuilder: (context, index) {
return Card(
child: Row(
children: <Widget>[
Text(dayMatch[index].date),
Text(dayMatch[index].team1),
Text(dayMatch[index].time),
Text(dayMatch[index].team2),
],
),
);
}
If you don't specify itemCount and the screen is big enough to display ten items, ListView.builder() builds a little more than ten children, and even more on demand if you scroll down.
In your case, ListView.builder() tries to build more than two children, while your list (dayMatch) only has two elements, which is why the error occurs.
To fix it, just pass the number of items to the itemCount argument, or it'll be somewhat better to use the default constructor of ListView instead if the number is fixed and small. ListView.builder() does more computing under the hood to be flexible, which is a little too much for a small list.
You can use "itemCount".
child: ListView.builder(
itemCount: dayMatch.length,
itemBuilder: (context, index) {
return Card(
child: Row(
children: <Widget>[
Text(dayMatch[index].date),
Text(dayMatch[index].team1),
Text(dayMatch[index].time),
Text(dayMatch[index].team2),
],
),
);
},
),