How to not load the children of a ExpansionTile - flutter

I have the follow structure: A list of Expansion tiles > clicking on it, opens another list of ExpansionTiles > Clicking in one of them, it should open some widgets according to a SQL query.
The problem is, when I tap in the first Expansion Tile it loads all the widgets from all the Expansion Tiles inside the first option making the query very slow. I want to only load the widgets when I tap in the second one (loading only the necessary ones)
Here is the code:
1st list:
class ListItemsScreen extends StatefulWidget {
#override
_ListItemsScreenState createState() => _ListItemsScreenState();
}
class _ListItemsScreenState extends State<ListItemsScreen> {
final Widget appBar = AppBar(
title: Text('ITEMS'),
actions: [
Builder(
builder: (context) => IconButton(
icon: Icon(Icons.shopping_bag_outlined),
onPressed: () {
Navigator.of(context)
.pushNamed(ROUTE_CHART);
},
),
),
],
);
#override
Widget build(BuildContext context) {
final List items = ModalRoute.of(context).settings.arguments;
return Scaffold(
appBar: appBar,
body: items == null || items.isEmpty ?
Center(child: Text("0 items here"),)
:
ListView(
children: [
...items.map<Widget>(
(item) {
return ExpansionTile(
leading: Image.asset(ASSET_IMAGE,
fit: BoxFit.cover
),
title: Text('${item.code} | ${item.description}'),
subtitle:
Text('${item.color}'),
children: [
Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: ProductWidget(item),
),
],
),
);
},
)
],
)
);
2nd list (ProductWidget):
class ProductWidget extends StatefulWidget {
final Product product;
ProductWidget(this.produto);
#override
_ProductWidgetState createState() => _ProductWidgetState();
}
class _ProdutoGradeWidgetState extends State<ProdutoGradeWidget> {
#override
Widget build(BuildContext context) {
CustomScrollView(
slivers: [
StreamBuilder(
stream: product.stream,
builder: (ctx, snapshot) {
return SliverList(
delegate: SliverChildBuilderDelegate((ctx, i) {
if (i == 0) {
return Column(
children: [
Align(
alignment: Alignment.center,
child: Padding(
padding: EdgeInsets.only(top: 5),
child: Text(
'I HAVE THIS PRODUCT IN THESE COLORS',
style: TextStyle(
fontSize: 20,
color:
Theme.of(context).textTheme.caption.color,
),
)
),
),
const SizedBox(height: 20.0),
ProductColorsWidget(color: snapshot.data[i]),
],
);
} else if (i == snapshot.data.length - 1) {
return Column(
children: [
ProductColorsWidget(color: snapshot.data[i]),
const SizedBox(height: 20.0),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'Qtd',
style: TextStyle(
fontSize: 16,
color:
Theme.of(context).textTheme.caption.color,
),
),
),
),
const SizedBox(height: 20.0),
],
);
}
return ProductColorsWidget(color: snapshot.data[i]);
}, childCount: snapshot.data.length),
);
}
},
),
],
);
}
}
}
}
3rd part (Product Colors Widget where I list the second Expansion Tiles):
class ProductColorsWidget extends StatelessWidget {
final ColorProduct color;
ProdutoCorGradeWidget({this.color});
#override
Widget build(BuildContext context) {
return ExpansionTile(
maintainState: true,
tilePadding: EdgeInsets.all(15.0),
title: Text(
'${color.id} - ${color.description}',
style: Theme.of(context)
.textTheme
.subtitle1
.copyWith(fontWeight: FontWeight.w600),
),
childrenPadding: EdgeInsets.all(10.0),
children: [
Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...color.sizes.map<Widget>(
(item) {
return Column(
children: [
Expanded(
child: Text(
item.description, textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
)
),
...item.prices.map<Widget>((size) {
return PricesWidget( //here it should show the widgets according to the second ExpansionTiles
color: color,
size: size
);
})
]
);
}
)
],
)
)
],
);
}
}
So, to be clear, what I want is: First It lists the products (with expansionTiles), expanding one it should show the second Tiles (with sizes) and after selecting one it should show the widgets.
..But what is happening now is: List the products and when I select one the app loads all the widget from all the second 'expansionTiles' making it slow to show the second list. What should I do?

I think the problem is with
ExpansionTile(
maintainState: true,
.....
)
I had a similar issue in which I had an ExpansionTile that was its own child and it caused a stack overflow because it was building them all. After setting maintainState to false the problem was solved.
You might have to adapt your state management according to that the children state may not be saved

Related

fl_chart custom Legend gets pushed down when section is empty

I'm using the PieChart of fl_chart to display the distribution of locally saved documents. The percentages displayed in the chart are the result of the length of the two document type lists (See image below).
But when one List is empty I have a weird bug were my custom Legend gets pushed downwards. The PieChart and the Legend are positioned inside of a Row with flex factors on each children (2 for the PieChart and 4 for the Legend).
I really don't understand what pushes the Legend downwards because my Expanded widgets are always positioned inside of Rows so that the PieChart and Legend only take up the available, horizontal space and not the vertical space which happens in the bug (image 2).
PieChart widget:
class PersonalFilesCircularGraph extends StatefulWidget {
const PersonalFilesCircularGraph();
#override
_PersonalFilesCircularGraphState createState() =>
_PersonalFilesCircularGraphState();
}
class _PersonalFilesCircularGraphState
extends State<PersonalFilesCircularGraph> {
late List<FileTypeData> data;
List<PieChartSectionData> getSections() => data
.asMap()
.map<int, PieChartSectionData>((index, data) {
final value = PieChartSectionData(
color: data.color,
value: data.percent,
showTitle: false,
radius: 3,
);
return MapEntry(index, value);
})
.values
.toList();
#override
void initState() {
/* Example getFileTypeData result
[
FileTypeData(
"Patient Questionnaire",
patientQuestionnaires.length /
(patientQuestionnaires.length +
receivedPatientQuestionnaires.length) *
100,
const Color(0xFF3861FB),
),
FileTypeData(
"Received Patient Questionnaire",
receivedPatientQuestionnaires.length /
(receivedPatientQuestionnaires.length +
patientQuestionnaires.length) *
100,
Colors.teal.shade400,
),
];
*/
data = context.read<SessionBloc>().state.getFileTypeData;
super.initState();
}
#override
Widget build(BuildContext context) {
return BlocConsumer<SessionBloc, SessionState>(
listenWhen: (previous, current) {
final bool listenWhen = previous.patientQuestionnaires.length !=
current.patientQuestionnaires.length ||
previous.receivedPatientQuestionnaires.length !=
current.receivedPatientQuestionnaires.length;
return !listenWhen;
},
listener: (context, state) {
data = context.read<SessionBloc>().state.getFileTypeData;
},
builder: (context, state) {
return Row(
children: [
Expanded(
flex: 2,
child: Container(
constraints: const BoxConstraints(
maxWidth: 60,
maxHeight: 60,
),
child: PieChart(
PieChartData(
sections: getSections(),
),
),
),
),
const SizedBox(
width: kMediumPadding,
),
Expanded(
flex: 4,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: data
.map(
(data) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: buildLegend(
percent: data.percent,
text: data.fileName == "Patient Questionnaire"
? L.of(context).patientQuestionnaires
: L.of(context).receivedPatientQuestionnaire,
color: data.color,
),
),
)
.toList(),
),
),
],
);
},
);
}
Widget buildLegend({
required double percent,
required String text,
required Color color,
}) =>
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
Container(
width: 10,
height: 10,
color: color,
),
const SizedBox(
width: kSmallPadding,
),
Expanded(
child: Text(
text,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Text(
"${percent.toStringAsFixed(0)}%",
overflow: TextOverflow.ellipsis,
)
],
);
}
I display the chart widget inside a CustomScrollView, wrapped with a SliverToBoxAdapter inside of my home screen:
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
#override
Widget build(BuildContext context) {
return CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
elevation: 0.0,
floating: true,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
title: Text(
"Home",
style: Theme.of(context).textTheme.headline5,
),
centerTitle: true,
),
const SliverPadding(
padding: EdgeInsets.symmetric(
vertical: kSmallPadding,
horizontal: kMediumPadding,
),
sliver: SliverToBoxAdapter(
child: PersonalFilesCircularGraph(),
),
)
],
);
}
}
EDIT:
I just did some more investigation on this bug and placed a colored Container in my CustomScrollView, below the SliverPadding of the CircularGraph to check if the Column of labels expands downwards. But as you can see the colored Container is not effected. It just looks like the Legend is inside a Stack and positioned without effecting other widgets above and below.
const SliverPadding(
padding: EdgeInsets.symmetric(
vertical: kSmallPadding,
horizontal: kMediumPadding,
),
sliver: SliverToBoxAdapter(
child: PersonalFilesCircularGraph(),
),
),
SliverToBoxAdapter(
child: Container(
width: double.infinity,
height: 60,
color: Colors.green,
),
)

Set State doesn't change the value just if hot reload the page

I have the following situation
Column(
children: [
Tabs(),
getPage(),
],
),
the getPage method
Widget getPage() {
if (tab1IsSelected == true) {
return Container(
child: Center(
child: Text('Tab1'),
),
);
}
if (tab1IsSelected == false) {
return Container(
child: Center(
child: Text('Tab2'),
),
);
}
}
and globally I have declared a variable
bool tab1IsSelected = true;
In the Tabs Class (statefull):
class Tabs extends StatefulWidget {
#override
_TabsState createState() => _TabsState();
}
class _TabsState extends State<Tabs> {
#override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
tab1IsSelected = true;
});
},
child: Container(
decoration: BoxDecoration(
color: tab1IsSelected ? primary : second,
),
child: Padding(
padding:
EdgeInsets.only(top: 1.5 * SizeConfig.heightMultiplier),
child: Center(
child: Text(
'New Hunt',
style: Theme.of(context).textTheme.bodyText1,
),
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
tab1IsSelected = false;
});
},
child: Container(
decoration: BoxDecoration(
color: tab1IsSelected ? second : primary,
),
child: Padding(
padding:
EdgeInsets.only(top: 1.5 * SizeConfig.heightMultiplier),
child: Center(
child: Text(
'My Hunts',
style: Theme.of(context).textTheme.bodyText2,
),
),
),
),
),
),
],
);
}
}
I change the value of that bool, but only if I hot reload the page the content is changing. Why?
Can you guide me please?
I've tried to use ? : in that Column but the same result and if I declare that variable in the Main Class where the Column is, I can't access it in the Tabs class, so that's why I declared it globally, maybe that's the cause I have to hot reload, but how can I implement that to do what I want. Thank you in advance
setState is inside _TabsState so it will only affect/rebuilt that particular widget, not getPage(), you could try using ValueChanged<bool> to retrieve the new value and then using setState in the widget that wraps the getPage()
class Tabs extends StatefulWidget {
final ValueChanged<bool> onChanged;
Tabs({this.onChanged});
#override
_TabsState createState() => _TabsState();
}
class _TabsState extends State<Tabs> {
#override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => widget.onChanged(true), //pass the value to the onChanged
child: Container(
decoration: BoxDecoration(
color: tab1IsSelected ? primary : second,
),
child: Padding(
padding:
EdgeInsets.only(top: 1.5 * SizeConfig.heightMultiplier),
child: Center(
child: Text(
'New Hunt',
style: Theme.of(context).textTheme.bodyText1,
),
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () => widget.onChanged(false), //pass the value to the onChanged
child: Container(
decoration: BoxDecoration(
color: tab1IsSelected ? second : primary,
),
child: Padding(
padding:
EdgeInsets.only(top: 1.5 * SizeConfig.heightMultiplier),
child: Center(
child: Text(
'My Hunts',
style: Theme.of(context).textTheme.bodyText2,
),
),
),
),
),
),
],
);
}
}
Now on the widget with the column (That should be a StatefulWidget for setState to work)
Column(
children: [
Tabs(
onChanged: (bool value) => setState(() => tab1IsSelected = value);
),
getPage(),
],
),
everytime you change the value of tab1IsSelected it will update getPage()
If you want to rebuild a widget when something in its state changes you need to call the setState() of the widget.
The variable is referenced to the State class and when you call setState() Flutter will rebuild the widget itself by calling the build() method of the State class.
If you want to have some variables outside the widgets I suggest you to use a state management approach listed here: https://flutter.dev/docs/development/data-and-backend/state-mgmt/options.
For example you could use Provider to store the active tab and reference the provider variable in both widgets.
You can try to handle the setstate in the parent class holding the Tab Widget then pass a the function to tab class and execute it in the gesture detector.

How to add Progress Indicator on Cards while tap in Flutter?

I am using Cards in Flutter and want Progress Indicator at the left bottom position for 2 seconds while Tap on the card so that another page load successfully.
Does anyone know how to add?
Container(
height: 130,
child: Card(
child: Row(
children: <Widget>[
Expanded(
child: ListTile(
title: Text(
'My card Location',
style: TextStyle(
fontSize: 15, fontWeight: FontWeight.w700),
),
leading: Icon(Icons.setting),
// color: Colors.blueAccent, size: mediumIconSize),
trailing: Icon(Icons.keyboard_arrow_right),
selected: true,
onTap: () async {
// I try this one but not working
// Flushbar(
//
// showProgressIndicator: true,
// duration: Duration(seconds: 2),
// );
getDetails().then((myCardlocations) {
Navigator
.of(context)
.pushNamed('/myCardlocations',
arguments: ObjectLocations(locations, 'myCardlocations'));
}
);
}
),
),
],
),
),
),
You can do something like this using Stack and CircularProgressIndicator..
class _MyWidgetState extends State<MyWidget> {
bool isLoading = false;
#override
Widget build(BuildContext context) {
return Container(
height: 130,
child: Stack(
children: [
Container(
height: 130,
child: Card(
child: Row(
children: <Widget>[
Expanded(
child: ListTile(
title: Text(
'My card Location',
style: TextStyle(
fontSize: 15, fontWeight: FontWeight.w700),
),
leading: Icon(Icons.settings),
// color: Colors.blueAccent, size: mediumIconSize),
trailing: Icon(Icons.keyboard_arrow_right),
selected: true,
onTap: () async {
setState(() {
isLoading = true;
});
getDetails().then((myCardLocations) {
setState(() {
isLoading = false;
});
// navigation code here
});
},
),
),
],
),
),
),
Align(
alignment: Alignment.bottomLeft,
child: isLoading
? Padding(
padding: EdgeInsets.fromLTRB(15,0,0,15),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(),
),
)
: SizedBox(),
),
],
),
);
}
}
Edit:
Looks like I misunderstood the question a bit. Specifically, the place where to show the progress indicator. Anyways, if you get the idea, you can put the indicator at a different place as per your requirement.
There are certain things, which I would like to mention before I give the actual answer.
Read about Flutter.delayed constructor, very useful thing to make some thing wait for a while and do the operation by providing Duration. Whatever you want to do after that duration, it will implement in the callback function
Future.delayed(Duration(seconds: your_time, (){
//it will perform this operation after that much of seconds
}));
You can always show/hide a Widget using bool value, and make changes accordingly
Use a column and Add the LinearProgressIndicator at the end of the Widget. Show/hide it based up on the data
Also, use MediaQuery to give out the height. It is more efficient way of giving the dimensions according to all phone size. Like match-parent in Android Studio. Do the math accordingly, I have shown in the code also
Column(
children: [
Row(),
bool val ? LinearProgressIndicator() : Container() // Container() is nothing but an empty widget which shows nothing
]
)
Some heads up: I have not used getData, since it is not defined properly but you can call it the in function which I will show you in the code, that is pageTransit(). Follow the comments and you are good to go
class _MyHomePageState extends State<MyHomePage> {
// this takes care of the show/hide of your progress indicator
bool _showProgress = false;
// this takes care of the operation
void pageTransit(){
// first show when the ListTile is clicked
setState(() => _showProgress = true);
Future.delayed(Duration(seconds: 2), (){
// hide it after 2 seconds
setState(() => _showProgress = false);
// do the page trnasition here
//getDetails().then((myCardlocations) {
//Navigator.of(context).pushNamed('/myCardlocations',
//arguments: ObjectLocations(locations, 'myCardlocations'));
//}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
height: MediaQuery.of(context).size.height * 0.1,
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// use your items here, based upon the bool value show hide your
// progress indicator
Row(
children: <Widget>[
Expanded(
child: ListTile(
title: Text(
'My card Location',
style: TextStyle(
fontSize: 15, fontWeight: FontWeight.w700),
),
leading: Icon(Icons.settings),
// color: Colors.blueAccent, size: mediumIconSize),
trailing: Icon(Icons.keyboard_arrow_right),
selected: true,
onTap: () => pageTransit()
)
)
]
),
// show/hide in the card
_showProgress ? LinearProgressIndicator() : Container()
]
)
)
)
);
}
}
Result
Look at the ProgressIndicator, it remains there for 2 seconds, and then goes away
1. You need to define a GlobalKey for the Scaffold so that you can use a SnackBar (you can define the GloablKey in your page's State).
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
2. You need to set the key for the Scaffold.
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
...
3. You need to wrap the Card with a GestureDetector and set the onTap function to call showLoading which shows a SnackBar on the bottom of the screen. Call your getDetails function in the showLoading. Full code (except the define key step):
void _showLoading() {
_scaffoldKey.currentState.showSnackBar(new SnackBar(
duration: new Duration(seconds: 2),
content: new Row(
children: <Widget>[
new CircularProgressIndicator(),
new Text("Loading...")
],
),
));
// call to your getDetails and its steps should be here
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text("My app"),
),
body: Center(
child: GestureDetector(
child: Card(
child: Row(children: <Widget>[
Expanded(
child: ListTile(
title: Text(
'My card Location',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700),
),
leading: Icon(Icons.settings),
// color: Colors.blueAccent, size: mediumIconSize),
trailing: Icon(Icons.keyboard_arrow_right),
selected: true,
)),
])),
onTap: () => _showLoading(),
)),
);
}
}
Note: you can also style the SnackBar.
Result:

Passing value to previous widget

I have simple form , inside it have CircularAvatar when this is pressed show ModalBottomSheet to choose between take picture from gallery or camera. To make my widget more compact , i separated it to some file.
FormDosenScreen (It's main screen)
DosenImagePicker (It's only CircularAvatar)
ModalBottomSheetPickImage (It's to show ModalBottomSheet)
The problem is , i don't know how to passing value from ModalBottomSheetPickImage to FormDosenScreen. Because value from ModalBottomSheetPickImage i will use to insert operation.
I only success passing from third Widget to second Widget , but when i passing again from second Widget to first widget the value is null, and i think the problem is passing from Second widget to first widget.
How can i passing from third Widget to first Widget ?
First Widget
class FormDosenScreen extends StatefulWidget {
static const routeNamed = '/formdosen-screen';
#override
_FormDosenScreenState createState() => _FormDosenScreenState();
}
class _FormDosenScreenState extends State<FormDosenScreen> {
String selectedFile;
#override
Widget build(BuildContext context) {
final detectKeyboardOpen = MediaQuery.of(context).viewInsets.bottom;
print('trigger');
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('Tambah Dosen'),
actions: <Widget>[
PopupMenuButton(
itemBuilder: (_) => [
PopupMenuItem(
child: Text('Tambah Pelajaran'),
value: 'add_pelajaran',
),
],
onSelected: (String value) {
switch (value) {
case 'add_pelajaran':
Navigator.of(context).pushNamed(FormPelajaranScreen.routeNamed);
break;
default:
}
},
)
],
),
body: Stack(
fit: StackFit.expand,
children: <Widget>[
SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
SizedBox(height: 20),
DosenImagePicker(onPickedImage: (file) => selectedFile = file),
SizedBox(height: 20),
Card(
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextFormFieldCustom(
onSaved: (value) {},
labelText: 'Nama Dosen',
),
SizedBox(height: 20),
TextFormFieldCustom(
onSaved: (value) {},
prefixIcon: Icon(Icons.email),
labelText: 'Email Dosen',
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 20),
TextFormFieldCustom(
onSaved: (value) {},
keyboardType: TextInputType.number,
inputFormatter: [
// InputNumberFormat(),
WhitelistingTextInputFormatter.digitsOnly
],
prefixIcon: Icon(Icons.local_phone),
labelText: 'Telepon Dosen',
),
],
),
),
),
SizedBox(height: kToolbarHeight),
],
),
),
Positioned(
child: Visibility(
visible: detectKeyboardOpen > 0 ? false : true,
child: RaisedButton(
onPressed: () {
print(selectedFile);
},
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
color: colorPallete.primaryColor,
child: Text(
'SIMPAN',
style: TextStyle(fontWeight: FontWeight.bold, fontFamily: AppConfig.headerFont),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
textTheme: ButtonTextTheme.primary,
),
),
bottom: kToolbarHeight / 2,
left: sizes.width(context) / 15,
right: sizes.width(context) / 15,
)
],
),
);
}
}
Second Widget
class DosenImagePicker extends StatefulWidget {
final Function(String file) onPickedImage;
DosenImagePicker({#required this.onPickedImage});
#override
DosenImagePickerState createState() => DosenImagePickerState();
}
class DosenImagePickerState extends State<DosenImagePicker> {
String selectedImage;
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.center,
child: InkWell(
onTap: () async {
await showModalBottomSheet(
context: context,
builder: (context) => ModalBottomSheetPickImage(
onPickedImage: (file) {
setState(() {
selectedImage = file;
widget.onPickedImage(selectedImage);
print('Hellooo dosen image picker $selectedImage');
});
},
),
);
},
child: CircleAvatar(
foregroundColor: colorPallete.black,
backgroundImage: selectedImage == null ? null : MemoryImage(base64.decode(selectedImage)),
radius: sizes.width(context) / 6,
backgroundColor: colorPallete.accentColor,
child: selectedImage == null ? Text('Pilih Gambar') : SizedBox(),
),
),
);
}
}
Third Widget
class ModalBottomSheetPickImage extends StatelessWidget {
final Function(String file) onPickedImage;
ModalBottomSheetPickImage({#required this.onPickedImage});
#override
Widget build(BuildContext context) {
return SizedBox(
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Wrap(
alignment: WrapAlignment.spaceEvenly,
children: <Widget>[
InkWell(
onTap: () async {
final String resultBase64 =
await commonFunction.pickImage(quality: 80, returnFile: ReturnFile.BASE64);
onPickedImage(resultBase64);
},
child: CircleAvatar(
foregroundColor: colorPallete.white,
backgroundColor: colorPallete.green,
child: Icon(Icons.camera_alt),
),
),
InkWell(
onTap: () async {
final String resultBase64 =
await commonFunction.pickImage(returnFile: ReturnFile.BASE64, isCamera: false);
onPickedImage(resultBase64);
},
child: CircleAvatar(
foregroundColor: colorPallete.white,
backgroundColor: colorPallete.blue,
child: Icon(Icons.photo_library),
),
),
],
),
),
);
}
}
The cleanest and easiest way to do this is through Provider. It is one of the state management solutions you can use to pass values around the app as well as rebuild only the widgets that changed. (Ex: When the value of the Text widget changes). Here is how you can use Provider in your scenario:
This is how your model should look like:
class ImageModel extends ChangeNotifier {
String _base64Image;
get base64Image => _base64Image;
set base64Image(String base64Image) {
_base64Image = base64Image;
notifyListeners();
}
}
Don't forget to add getters and setters so that you can use notifyListeners() if you have any ui that depends on it.
Here is how you can access the values of ImageModel in your UI:
final model=Provider.of<ImageModel>(context,listen:false);
String image=model.base64Image; //get data
model.base64Image=resultBase64; //set your image data after you used ImagePicker
Here is how you can display your data in a Text Widget (Ideally, you should use Selector instead of Consumer so that the widget only rebuilds if the value its listening to changes):
#override
Widget build(BuildContext context) {
//other widgets
Selector<ImageModel, String>(
selector: (_, model) => model.base64Image,
builder: (_, image, __) {
return Text(image);
},
);
}
)
}
You could achieve this easily. If you are using Blocs.

How to save downloaded data from an API to RAM in Flutter?

I'm writing a really simple app in Flutter, but I have a problem with state management.
Here's the video of what I have. Link: https://streamable.com/ir3ztr
The video shows my application, but when I switch a screen using Bottom Navigation Bar, the data loads again and again from the API. I don't want that. I want the once downloaded data to be saved in RAM and not being downloaded again from the API. Is that possible? I heard about Provider, but I don't know how to use that in my case.
Is there anyone who can help me?
My code:
World
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_placeholder_textlines/flutter_placeholder_textlines.dart';
import '../../models/world.dart';
import '../../data/world_service.dart';
class WorldScreenAndroid extends StatefulWidget {
#override
_WorldScreenAndroidState createState() => _WorldScreenAndroidState();
}
class _WorldScreenAndroidState extends State<WorldScreenAndroid> {
Future<World> futureWorld;
#override
void initState() {
super.initState();
futureWorld = fetchWorld();
}
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(8.0),
child: FutureBuilder<World> (
future: futureWorld,
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView(
children: [
Card(
child: ListTile(
leading: Icon(Icons.public),
title: Text('coronavirus_cases').tr(context: context),
subtitle: Text(NumberFormat('#,###,###', 'en_US').format(snapshot.data.cases).toString())
),
),
Card(
child: ListTile(
leading: Icon(Icons.public),
title: Text('deaths').tr(context: context),
subtitle: Text(NumberFormat('#,###,###', 'en_US').format(snapshot.data.deaths).toString())
),
),
Card(
child: ListTile(
leading: Icon(Icons.public),
title: Text('recovered').tr(context: context),
subtitle: Text(NumberFormat('#,###,###', 'en_US').format(snapshot.data.recovered).toString())
),
)
],
);
}
return ListView(
children: [
Card(
child: ListTile(
leading: Icon(Icons.public),
title: Text('coronavirus_cases').tr(context: context),
subtitle: PlaceholderLines(
count: 1,
animate: true,
color: Colors.grey,
minWidth: 0.10,
maxWidth: 0.50,
),
),
),
Card(
child: ListTile(
leading: Icon(Icons.public),
title: Text('deaths').tr(context: context),
subtitle: PlaceholderLines(
count: 1,
animate: true,
color: Colors.grey,
minWidth: 0.10,
maxWidth: 0.50,
),
),
),
Card(
child: ListTile(
leading: Icon(Icons.public),
title: Text('recovered').tr(context: context),
subtitle: PlaceholderLines(
count: 1,
animate: true,
color: Colors.grey,
minWidth: 0.10,
maxWidth: 0.50,
),
),
)
],
);
},
)
);
}
}
Countries
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import '../../models/country.dart';
import '../../data/countries_service.dart';
class CountriesScreenAndroid extends StatefulWidget {
#override
_CountriesScreenAndroidState createState() => _CountriesScreenAndroidState();
}
class _CountriesScreenAndroidState extends State<CountriesScreenAndroid> {
Future<List<Country>> futureCountries;
#override
void initState() {
super.initState();
futureCountries = fetchCountries();
}
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(8.0),
child: FutureBuilder(
future: futureCountries,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
children: [
TextField(),
SizedBox(height: 10.0),
Expanded(
child: ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
final List<String> _countriesAllArgs = [
NumberFormat('#,###,###', 'en_US').format(snapshot.data[index].cases),
NumberFormat('#,###,###', 'en_US').format(snapshot.data[index].todayCases),
NumberFormat('#,###,###', 'en_US').format(snapshot.data[index].active),
NumberFormat('#,###,###', 'en_US').format(snapshot.data[index].deaths),
NumberFormat('#,###,###', 'en_US').format(snapshot.data[index].todayDeaths),
NumberFormat('#,###,###', 'en_US').format(snapshot.data[index].recovered),
NumberFormat('#,###,###', 'en_US').format(snapshot.data[index].critical)
];
return Card(
child: Padding(
padding: EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
snapshot.data[index].country,
style: TextStyle(
fontSize: 18.0,
color: Colors.grey[600],
),
),
SizedBox(height: 6.0),
Text(
'countries_all',
style: TextStyle(
fontSize: 14.0,
color: Colors.grey[800],
),
).tr(args: _countriesAllArgs),
],
),
)
);
}
),
)
],
);
}
return Center(
child: CircularProgressIndicator(),
);
},
)
);
}
}
Make sure to keep the loaded data in a parent widget, not in the widget your switching.
Have you considered using the BLOC framework?
In that case, you could nicely sepearte Business Logic from UI code and keep the loaded data in the bloc.
In detail have a blocProvider on top, wrapping your app.
https://bloclibrary.dev/#/flutterbloccoreconcepts?id=blocprovider
Alternatively, but be carefull with the handling though, you can just create an object, anywhere outside of any widget, and access it from anywhere.
class Data {
List<String> stringsLoadedFromWeb;
}
Data data = Data();
// from anywhere else, where you import the above file
data.stringsLoadedFromWeb = ...