Flutter: Is it possible to use single JSON file in bodies of more expansion panels? - flutter

In my app, I have one JSON file with some static data which I use to produce List of a widgets. Then, I have one screen with ExpansionPanelRadio showing few items and each of them, when expanded, (in their bodies) are containing that list of a widgets made using JSON file.
I am using provider and I am able to display that list of widgets inside body of expansionpanel but the lists are somehow repeating.
For example, I expand one panel and in its body list is displayed few times, like in a loop. I guess, that the problem is in provider but I don't understand it quite well.
I am pretty new to flutter and would appreciate if someone could explain me why is this happening and what approach should I use to solve it.
here is part of a code where i make those expansion panels with provided JSON in a body:
SingleChildScrollView(
child: ExpansionPanelList.radio(
elevation: 0,
children: MyList.map<ExpansionPanelRadio>((Item item) {
return ExpansionPanelRadio(
value: MyList.indexOf(item),
headerBuilder: (BuildContext context, bool isExpanded) {
return Row(
children: [
Padding(
padding: const EdgeInsets.all(10),
child: SvgPicture.asset(
"assets/images/some_image.svg"
),
),
Text('some label'),
],
);
},
body: ChangeNotifierProvider(
create: (context) => FeaturesProvider(),
builder: (context, child) {
Provider.of<FeaturesProvider>(context, listen: false)
.readFeatures();
return SingleChildScrollView(
child: Container(
child: Features(item.objectId.toString()),
),
);
}));
}).toList(),
))

Related

Flutter: Body of my ExpansionPanelRadio is repeating many times. (when I have more than one item listed)

I am using ExpansionPanelRadio() to make an list of expandable items. Each item, when expanded, contains another list of let's say 'features'.
Now, when I have only one expandable element, its list of features looks fine. As soon as I add two or more items, their expanded lists of features grows somehow. (they repeat themselves).
And, as more times, as I am clicking (opening) expansion panels, the lists of items grow more and more. Does anybody have an Idea whats going on?
Both of the lists (expansion tiles and their expanded bodies must be scrollable).
My 'simplified' code looks like:
SingleChildScrollView(
child: ExpansionPanelList.radio(
elevation: 0,
children: MyList.map<ExpansionPanelRadio>((Item item) {
return ExpansionPanelRadio(
value: MyList.indexOf(item),
headerBuilder: (BuildContext context, bool isExpanded) {
return Row(
children: [
Padding(
padding: const EdgeInsets.all(10),
child: SvgPicture.asset(
"assets/images/some_image.svg"
),
),
Text('some label'),
],
);
},
body: ChangeNotifierProvider(
create: (context) => FeaturesProvider(),
builder: (context, child) {
Provider.of<FeaturesProvider>(context, listen: false)
.readFeatures();
return SingleChildScrollView(
child: Container(
child: Features(item.objectId.toString()),
),
);
}));
}).toList(),
))

Recommended approach to lists with expandable elements in Flutter

I would like to create a list with expandable items/children based on data provided from the stream. Expanded items would include textual details and actions which the user can take.
One of the key requirements for this layout is that - at any time only one list item can be expanded.
From what I've managed to found out - with current out-of-the-box Flutter widgets this needs to be managed programmatically. Once user expands one tile to set values as not-expanded for all other tiles and rebuild UI.
I've managed to make this work in two ways:
Using ListView.builder & ExpansionTile widgets as children
Using ExpansionPanelList & ExpansionPanel widgets as children
Both approached were combined with Riverpod's StateProvider to keep state of the selected/expanded item and invoke rebuild once the state changes (user taps on a item/tile).
Both approaches have its trade-offs from UI perspective - e.g. ExpansionPanelList children padding is poor looking and cannot be modified. Then we have ExpansionTile approach which looses animation once "key" is set with unique PageStorageKey - a necessary config in order for ExpansionTile to save and restore its expanded state (basically programmatic expanding and collapsing of tiles does not work without this...).
What I don't understand is the performance impact of both approached and which one is better in this regard.
How does setting unique key: param and rebuilding ExpansionTile widgets compare ExpansionPanelList and generating list of its items (List<ExpansionPanel>) with each widget rebuild - in order to define which Panel is expanded and which is not.
Intuitively it seems that ListView.builder & ExpansionTile approach is better - but I am not sure what is the impact of setting "key:". Please help in understanding which way is better performance wise.
You can see bellow implementation for both approaches.
ListView.builder & ExpansionTile approach
return ListView.builder(
padding: EdgeInsets.all(10),
itemCount: data.length,
itemBuilder: ((context, index) {
return ExpansionTile(
key: PageStorageKey("${DateTime.now().millisecondsSinceEpoch}"),
initiallyExpanded: (selectedItem == data[index].id.toString()),
onExpansionChanged: ((value) =>
ref.read(selectedProductListItem.notifier).state = (value == true) ? data[index].id.toString() : null),
tilePadding: EdgeInsets.all(10),
leading: Icon(Icons.account_box),
title: Text(data[index].name, style: TextStyle(fontWeight: FontWeight.bold)),
childrenPadding: EdgeInsets.all(10),
expandedAlignment: Alignment.center,
children: [
Text(data[index].description),
SizedBox(height: 10),
ElevatedButton(onPressed: () {}, child: Text('Apply Now')),
],
);
}),
);
ExpansionPanelList & ExpansionPanel approach
final List<ExpansionPanel> listData = [];
for (var i = 0; i < data.length; i++) {
listData.add(
ExpansionPanel(
headerBuilder: (context, isExpanded) {
return Padding(padding: EdgeInsets.all(10), child: Text(data[i].name));
},
body: Padding(
padding: EdgeInsets.all(10),
child: Column(
children: [
Text(data[i].description),
SizedBox(height: 10),
ElevatedButton(onPressed: () {}, child: Text('Apply Now')),
],
),
),
isExpanded: (data[i].id == selectedItem),
canTapOnHeader: true,
),
);
}
return SingleChildScrollView(
padding: EdgeInsets.all(15),
child: ExpansionPanelList(
children: listData,
expansionCallback: (panelIndex, isExpanded) {
ref.read(selectedProductListItem.notifier).state = isExpanded ? null : data[panelIndex].id;
},
),
);

How to wrap a List of Widgets without changing the layout when unwrapped and spread in a Row?

Currently, the following structure gives me the intended layout:
Consumer<State>(
builder(context, state, child) => Row(
children: [
Widget1(...), // fixed width
...List.generate(state.length, (index) => Expanded(child: Widget2(index, state))),
]
)
)
Widget1 does not need to be hooked up to state, and so I don't want it to be rerendered when it changes. It would be great if List.generate could be hooked to a Consumer, but builder needs to return a single Widget, not a list of Widgets.
I've tried using the Wrap widget around the list, and hooking that to a Consumer. This accomplishes my goal of not rerendering Widget1, but it changes the layout - the Widget2's don't expand to fill the remaining space any longer.
Here's an example:
Row(
children: [
Widget1(...), // fixed width
Consumer<State>(
builder: (context, state, child) {
return Expanded(
child: Row(
children: List.generate(
state.length,
(index) {
return Expanded(
child: Widget2(index, state),
);
},
),
),
);
},
),
],
),

Flutter. Multiple heroes share same tag. ListView with buttons

Can't figure it out how to solve this problem. So, here is the simplest code which represents my problem:
Scaffold(
body: Stack(
children: <Widget>[
...
Scrollbar(
child: ListView.builder(
primary: false,
shrinkWrap: true,
itemCount: _mapBloc?.mapData?.companies?.count ?? 0,
itemBuilder: (context, index) {
final company = _mapBloc?.mapData?.companies?.data[index];
return InkWell(
child: Hero(
tag: company.id,
child: Card(
child: Container(
height: 50,
width: double.infinity,
),
),
),
onTap: () {
Navigator.of(context)
.pushNamed('/company', arguments: company)
.then(
(results) {
if (results is PopWithResults) {
PopWithResults popResult = results;
}
},
);
},
);
},
),
)
],
),
);
And stack trace:
The following assertion was thrown during a scheduler callback:
There are multiple heroes that share the same tag within a subtree.
Within each subtree for which heroes are to be animated (i.e. a PageRoute subtree), each Hero must
have a unique non-null tag.
...
The items count in ListView changes with every database request. If the size is up to 5 widgets in ListView, then clicking works correctly, as soon as the list expands to, for example, 8 elements, I get the error written above. With what it can be connected? I tried to use unique Hero tags, but this does not solve the problem.
I need some advice and hope you can help me. If you need more information, please, write the comment.
Thanks for your attention!

Flutter bottomNavigationBar (change only body section)

Ok so I have got the following bottomNavigationBar working, however it's not clean enough for me and I am hoping there is a better way that someone knows.
Basically I only want to change the body section and not the full page.
However I want to have each section in different classes to keep it neat (ideally new .dart files - as they will all have different functions)
Currently all the page info is inside the body tag
body: PageView(
controller: _myPage,
onPageChanged: (int) {
print('Page Changes to index $int');
},
children: <Widget>[
Center(
child: Container(
child: Text('Empty Body 0'),
),
),
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text('images/pic1.jpg'),
Text('images/pic2.jpg'),
Text('images/pic3.jpg'),
],
),
),
Center(
child: Container(
child: Text('Empty Body 2'),
),
),
Center(
child: Container(
child: Text('DRN1 app is made using Google Flutter. While every attempt was made to make sure that this app works on as many devices as possible. We are unable to test every device. If you have issues running this app please let us know'),
),
)
],
physics: NeverScrollableScrollPhysics(), // Comment this if you need to use Swipe.
),
what I would like is something like this.
body: PageView(
controller: _myPage,
onPageChanged: (int) {
print('Page Changes to index $int');
},
children: <Widget>[
home(),
news() , // this is the main body for home
shows(), // this one shows the class shows() which fetches JSON and returns a different body layout
about(), // about text
]
Does anyone know of a better way to do this?
What you can do is create separate Dart files and import them into your main.dart
Then inside your State class define a list which contains the pages that you want to show
List<Widget> _myPage = <Widget>[Page1class(),Page2class(),Page3class()];
After that you can use the below code which looks quite clean. Using the builder() method will allow you to create pages based on the size of _myPage,
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(title: Text('sdf')),
body: PageView.builder(
itemBuilder: (context, position) => _myPage[position],
itemCount: _myPage.length,
),
)
);
Here is a sample program: PageView sample