How to dispose/recreate a Widget after an action - flutter

I am trying to figure out how to destroy/recreate a Widget based on an action (e.g: onPressed).
The way I thought about this, is to have my Widget rapped within an Opacity object, and control the state of the Opacity object based on user interaction somewhere in my app(i.e: hide/show the widget instead of dispose/recreate ). However, what I am asking here, how to destroy/recreating a Widget after clicking a Button?
I have created the following dummy example to show what I mean.
The red Icon should be disposed when I press the cloud Icon and recreated when I press the RaisedButton.
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Destroy/Recreate Example"),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Stack(fit: StackFit.passthrough,
children: <Widget>[
new IconButton(icon: new Icon(
Icons.cloud_circle, size: 40.0, color: Colors.blue,),
onPressed: null /*_destroyWidget*/),
new Positioned(child: new DecoratedBox(
decoration: new BoxDecoration(shape: BoxShape.circle),
child: new Icon(
Icons.add_circle, size: 20.0, color: Colors.red,),),
top: 3.0,
left: 3.0)
],
),
new RaisedButton(onPressed: null /*_recreateWidget*/,
child: new Text("Recreate!"),
),
])
)
);
}
How to start on this idea?
Update
The following code achieves the same thing but by manipulating the Opacity of the red Icon (show/hide).
var _myOpacity = 0.0;
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Destroy/Recreate Example"),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Stack(fit: StackFit.passthrough,
children: <Widget>[
new IconButton(icon: new Icon(
Icons.cloud_circle, size: 40.0, color: Colors.blue,),
onPressed: ()=>setState((){
_myOpacity =0.0;
}) /*_destroyWidget*/),
new Positioned(child: new Opacity(
opacity: _myOpacity,
child:
new DecoratedBox(
decoration: new BoxDecoration(shape: BoxShape.circle),
child: new Icon(
Icons.add_circle, size: 20.0, color: Colors.red,),),),
top: 3.0,
left: 3.0)
],
),
new RaisedButton(onPressed: ()=>
setState((){
_myOpacity = 1.0;
})
/*_recreateWidget*/,
child: new Text("Recreate!"),
),
])
)
);
}

You will have to wrap your red icon in a StatefulWidget if you want to be notified when it is initialized and disposed. A StatelessWidget doesn't have these callbacks.
You can replace the StatefulWidget with null in a setState and its State will be disposed. You can set it back to normal in a second setState callback, and a new State will be created and its initState will be called.
If you want to dispose and initState without going through the intermediate step of having a widget be replaced with null, try giving your StatefulWidget a new UniqueKey when the recreate button is pressed. This will prevent Flutter from associating the old State with the new StatefulWidget.
It seems like you're mostly doing this out of curiosity rather than a genuine use case. The shopping card example you've described probably doesn't need to use initState and dispose at all and you should just use a StatelessWidget.

Related

instant update of the displayed variable in Flutter (after changing the variable value)

I designed a project for weighing, in the flutter app I display the weight in real time (via sockets). The answer I get through the socket I would like to see instantly in the application. But the problem is that the weight does not change in real time (although in the terminal I see the actual weight coming). Here's how I gain weight through sockets:
getWeight() async {
Socket socket = await Socket.connect('192.168.0.1', 4005);
print('connected');
// send hello
socket.add(utf8.encode('pass'));
socket.listen((List<int> event) {
weightSO = utf8.decode(event);
print(utf8.decode(event));
});
socket.add(utf8.encode('pass'));
await Future.delayed(Duration(seconds: 10));
socket.close();
}
}
My Widget:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(height: 35.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
FlatButton(
disabledColor: Colors.pink,
textColor: Colors.blueGrey,
color: Colors.transparent,
child: Text('Weight', textScaleFactor: 2),
onPressed:
getWeight,
padding: EdgeInsets.fromLTRB(30.0, 10.0, 30, 10.0),
),
],
),
Container(
child: Text(('$weightSO' ), style: TextStyle(fontSize: 45.0, color: Colors.blueGrey)),
padding: EdgeInsets.fromLTRB(0, 0.0, 0, 20.0),
),
]),
);
}
Does anyone know how to make it display in real time in the variable weightSO, I will be grateful for help.
In flutter there is something called state managment.
Streams,StreamController also you have many libraries you can use based on stream like GetX,Provider,etc...
When the vairable Change sink to the stream when the stream get updated with new variable use streambuilder in the Ui to inform it with the new value to rebuild the widget .

Dialog state does not change after restarting (closing and opening again) in flutter

I have defined one button inside one StatelessWidget (This will have the bloc creation logic and injecting using bloc provider, ), on click of the button i am showing a dialog and passing the bloc instance to it, as shown in the code.
//EsignBloc is defined here in parent statelessWidget. Defined i.e. creating the bloc instance and passing through the BlocProvider. Removed the code for simplicity
//This listener will be called when Button defined inside statelessWidget will be clicked. this is responsible for showing the dialog.
void _onClickHere(
BuildContext context,
) {
final dialog = Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
),
elevation: 0.0,
backgroundColor: Colors.transparent,
child: _GetSignUsingOtpView(),
);
showDialog(
context: context,
builder: (_) => BlocProvider<EsignBloc>(
create: (_) => BlocProvider.of<EsignBloc>(context), // passing already created bloc to dialog
child: WillPopScope(
onWillPop: () => Future.value(false),
child: dialog,
),
),
barrierDismissible: false,
);
}
Pasting some code of _GetSignUsingOtpView()
class _GetSignUsingOtpView extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<EsignBloc, EsignState>(builder: (context, state) {
return Container(
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(
AppConstants.borderRadius,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Align(
alignment: Alignment.centerRight,
child: InkWell(
onTap: () => _closeDialog(context),
child: Padding(
padding: const EdgeInsets.only(top: 8.0, right: 8),
child: Icon(
Icons.cancel,
color: AppColor.primaryDark,
size: SizeConfig.safeBlockVertical * 2,
),
),
),
),
Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
PrimaryText(text: state.otp), // data does not change after closing and opeing dialog again
PrimaryText(text: state.remainingTime), // data does not change after closing and opeing dialog again
],
),
),
],
),
);
});
}
void _closeDialog(BuildContext context) {
Navigator.pop(context);
}
}
The problem that I am facing is whenever the dialog opens again after closing, it doesn't show the latest data from the bloc. The dialog just shows whatever previous data is in the bloc. Can someone point it out, where i am making the mistake?
The reason that your dialog is not showing new data is because you are creating a new instance of the bloc. Even though you say that you're not.
BlocProvider<EsignBloc>(
create: (_) => BlocProvider.of<EsignBloc>(context), // passing already created bloc to dialog
child: ...
In some cases, BlocProvider can be used to provide an existing cubit to a new portion of the widget tree. This will be most commonly used when an existing cubit needs to be made available to a new route. In this case, BlocProvider will not automatically close the cubit since it did not create it.
To not create a new instance, you need to use BlocProvider.value. You can read more about it here
BlocProvider.value(
value: BlocProvider.of<EsignBloc>(context),
child: ...,
);
Instead of creating a new instance, this will use the previous instance and provide it to the child widget and will NOT close the bloc when its done using it. This is handy for dialogs and navigation.

Flutter snackbar alternative or easier method than wrapping everything in Scaffold?

I'm working on my first Flutter app (debugging on my Android phone). I have a list with row items. When you long-press the row, it copies the content into the user's clipboard. This is working great!
But I need to let the user know that the content was copied.
I've attempted to follow many tutorials on trying to get the row surrounded by a build method or inside a Scaffold, but I can't get any to work. Is there an alternative method to notifying the user (simply) that something like "Copied!" took place?
Notice the commented out Scaffold.of(... below. It just seems like there must be an easier method to notifying the user other than wrapping everything in a Scaffold. (and when I try, it breaks my layout).
import 'package:flutter/material.dart';
import 'package:my_app/Theme.dart' as MyTheme;
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/services.dart';
class RowRule extends StatelessWidget {
final DocumentSnapshot ruleGroup;
RowRule(this.ruleGroup);
_buildChildren() {
var builder = <Widget>[];
if (!ruleGroup['label'].isEmpty) {
builder.add(new Text(ruleGroup['label'],
style: MyTheme.TextStyles.articleContentLabelTextStyle));
}
if (!ruleGroup['details'].isEmpty) {
builder.add(new Text(ruleGroup['details'],
style: MyTheme.TextStyles.articleContentTextStyle));
}
return builder;
}
#override
Widget build(BuildContext context) {
return new GestureDetector(
onLongPress: () {
Clipboard.setData(new ClipboardData(text: ruleGroup['label'] + " " + ruleGroup['details']));
// Scaffold.of(context).showSnackBar(SnackBar
// (content: Text('text copied')));
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 3.0),
child: new FlatButton(
color: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 0.0),
child: new Stack(
children: <Widget>[
new Container(
margin: const EdgeInsets.symmetric(
vertical: MyTheme.Dimens.ruleGroupListRowMarginVertical),
child: new Container(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 32.0, vertical: 8.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildChildren(),
),
)),
)
],
),
),
));
}
}
The goal is to have a page like this (see image), which I have, and it works and scrolls...etc, but I cannot get it to work with a Scaffold, and therefore, haven't been able to use the snackbar. Each "Row" (which this file is for) should show a snackbar on longPress.
You can use GlobalKey to make it work the way you want it.
Since I don't have access to your database stuff, this is how I gave you an idea to do it. Copy and paste this code in your class and make changes accordingly. I also believe there is something wrong in your RowRule class, can you just copy the full code I have given you and run?
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatelessWidget {
final GlobalKey<ScaffoldState> _key = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFFFFFFF).withOpacity(0.9),
key: _key,
body: Column(
children: <Widget>[
Container(
color: Color.fromRGBO(52, 56, 245, 1),
height: 150,
alignment: Alignment.center,
child: Container(width: 56, padding: EdgeInsets.only(top: 12), decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.yellow)),
),
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: 120,
itemBuilder: (context, index) {
return Container(
color: Colors.white,
margin: const EdgeInsets.all(4),
child: ListTile(
title: Text("Row #$index"),
onLongPress: () => _key.currentState
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text("Copied \"Row #$index\""))),
),
);
},
),
),
],
),
);
}
}
These is a simple plugin replacement for the Snackbar named "Flushbar".
You can get the plugin here - https://pub.dartlang.org/packages/flushbar
You don't have to take care of any wrapping of widgets into scaffold also you get a lot of modifications for you like background gradient, adding forms and so on into Snackbar's and all.
Inside your onLongPressed in GestureDetectore you can do this.
onLongPressed:(){
Clipboard.setData(new ClipboardData(text: ruleGroup['label'] + " " + ruleGroup['details']));
Flushbar(
message: "Copied !!",
duration: Duration(seconds: 3),
)..show(context);
}
This will display the snackbar in you app where you would want to see it also you can get a lot of modification available to you so the you can make it look as per your app.
There are couple of things you need to do, like use onPressed property of the FlatButton it is mandatory to allow clicks, wrap your GestureDetector in a Scaffold. I have further modified the code so that it uses GlobalKey to make things easy for you.
Here is the final code (Your way)
class RowRule extends StatelessWidget {
final GlobalKey<ScaffoldState> globalKey = GlobalKey();
final DocumentSnapshot ruleGroup;
RowRule(this.ruleGroup);
_buildChildren() {
var builder = <Widget>[];
if (!ruleGroup['label'].isEmpty) {
builder.add(new Text(ruleGroup['label'], style: MyTheme.TextStyles.articleContentLabelTextStyle));
}
if (!ruleGroup['details'].isEmpty) {
builder.add(new Text(ruleGroup['details'], style: MyTheme.TextStyles.articleContentTextStyle));
}
return builder;
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: globalKey,
body: GestureDetector(
onLongPress: () {
Clipboard.setData(new ClipboardData(text: ruleGroup['label'] + " " + ruleGroup['details']));
globalKey.currentState
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text('text copied')));
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 3.0),
child: new FlatButton(
onPressed: () => print("Handle button press here"),
color: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 0.0),
child: new Stack(
children: <Widget>[
new Container(
margin: const EdgeInsets.symmetric(vertical: MyTheme.Dimens.ruleGroupListRowMarginVertical),
child: new Container(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 32.0, vertical: 8.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildChildren(),
),
),
),
)
],
),
),
),
),
);
}
}
I made a dropdown banner package on pub that allows you to easily notify users of errors or confirmation of success. It's a work in progress as I continue to add visually rich features.
I am not sure if your build() method is completed or you are yet to change it, because it consist of many widgets which are just redundant. Like there is no need to have Container in Container and further Padding along with a FlatButton which would make complete screen clickable. Also having Column won't be a good idea because your screen may overflow if you have more data. Use ListView instead.
So, if you were to take my advice, use this simple code that should provide you what you are really looking for. (See the build() method is of just 5 lines.
class RowRule extends StatelessWidget {
final GlobalKey<ScaffoldState> globalKey = GlobalKey();
final DocumentSnapshot ruleGroup;
RowRule(this.ruleGroup);
_buildChildren() {
var builder = <Widget>[];
if (!ruleGroup['label'].isEmpty) {
builder.add(
ListTile(
title: Text(ruleGroup['label'], style: MyTheme.TextStyles.articleContentLabelTextStyle),
onLongPress: () {
globalKey.currentState
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text("Clicked")));
},
),
);
}
if (!ruleGroup['details'].isEmpty) {
builder.add(
ListTile(
title: Text(ruleGroup['details'], style: MyTheme.TextStyles.articleContentTextStyle),
onLongPress: () {
globalKey.currentState
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text("Clicked")));
},
),
);
}
return builder;
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: globalKey,
body: ListView(children: _buildChildren()),
);
}
}
I read your comments on all answers and here is my conslusion:
You need ScaffoldState object that is just above the widget in tree to show Snackbar. You can either get it through GlobalKey as many have suggested. Fairly simple if the Scaffold is created inside build of the widget, but if it is outside the widget (in your case) then it becomes complicated. You need to pass that key, wherever you need it through Constructor arguments of child widgets.
Scaffold.of(context) is a very neat way to just do that. Just like an InheritedWidget, Scaffold.of(BuildContext context) gives you access of the closest ScaffoldState object above the tree. Else it could be a nightmare to get that instance (by passing it through as constructor arguments) if your tree was very deep.
Sorry, to disappoint but I don't think there is any better or cleaner method than this, if you want to get the ScaffoldState that is not built inside build of that widget. You can call it in any widget that has Scaffold as a parent.

bottomNavigationBar do what I want only while hot reloading

I would like to have a static bottom bar for my app.
This bar should content the hour of a device in the middle, and the temperature read by another device on the right side.
My problem is just about the positioning/sizing.
When I start it (with android studio, on real or virtual device), it doesn't work : the preview is not what I excepted, the dimensions I choose seem to have changed.
But when I hot-reload it, it becomes exactly what I want like this :
The problem is that the final app is not the hot-reloaded one ..
I may have identify the problem,
I use a MediaQuery.of(context).size and a builder to obtain the context in order to fit with all the devices considering the size. This solution must be a dirty one but I don't find anything else..
Here is my code, hope the situation is clear...
return new MediaQuery(
data: MediaQueryData.fromWindow(ui.window),
child: Builder(
builder: (context) {
return new Builder(
builder: (
context) {
return
new MaterialApp(
home: new DefaultTabController(
length: 6,
child: Scaffold(
bottomNavigationBar:
new Container(
color: Colors.white,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height*0.1,
alignment: Alignment.centerRight,
child: new Container(
color: Colors.blue,
width: MediaQuery.of(context).size.width* 2/3,
alignment: Alignment.centerRight,
child: new Row(
children: <Widget>[
new Expanded(child: new Container(
color : Colors.yellow,
child: new Text('22 : 22' ),
alignment: Alignment.center,
)),
new Expanded(child:new Container(
color: Colors.pink,
child: new Text(' 37.8 °C'),
margin: new EdgeInsets.all(MediaQuery.of(context).size.width*0.02),
alignment: Alignment.centerRight,
))
],
),
),
),
Thank you for helping.
You could use a Stack and position one of the widget absolute over the other one:
new Stack(
children: <Widget> [
new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget> [
new Text(time),
],
),
new Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget> [
new Text(temperature)
],
),
],
),
where time & temperature are variables that were set within the state (Stateful Widgets). Then just include it in bottomNavigationBar.
I eventually added my previous code in a StatelessWidget and call it in the bottomNavigationBar field of the Scaffold : it does the job.
And I call MediaQuery.of(context) within the StatelessWidget created
Thank you.

How to achieve expansion of a widget in both vertical (height) and horizontal (width) direction

The code below lays out a chart in which I'd need to achieve for the chart to be expanded in both vertical (height) and horizontal (width) direction. The suggested method (e.g. https://docs.flutter.io/flutter/widgets/Row-class.html) is to use Expanded in Row or Column.
The chart widget I am trying to expand extends CustomPaint, with no children, everything is painted using a CustomPainter on canvas, in the CustomPainter.paint(canvas, size).
This code
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'vvvvvvvv:',
),
new RaisedButton(
color: Colors.green,
onPressed: _chartStateChanger,
),
new Text(
'vvvvvvvv:',
),
new Expanded( // Expanded in Column, no expansion vertically
child: new Row(
children: [
new Text('>>>'),
new Expanded(// Expanded in Row, expands horizontally
child: new Chart( // extends CustomPaint
// size: chartLogicalSize,
painter: new ChartPainter( // extends CustomPainter
chartData: _chartData,
chartOptions: _chartOptions,
),
),
),
new Text('<<<'),
],
), // row
),
new Text('^^^^^^:'),
new RaisedButton(
color: Colors.green,
onPressed: _chartStateChanger,
),
],
),
),
);
result looks like this: (code of ChartPainter is not shown for brevity)
Inside the ChartPainter.paint(canvas, size) there is a print() printing the size.
print(" ### Size: paint(): passed size = ${size}");
The result from the paint->print above is:
I/flutter ( 4187): ### Size: paint(): passed size = Size(340.0, 0.0)
The print along with the image shows, that the width expansion on the row level was passed to the CustomPainter.print(canvas, size) (width = 340.0), but the height expansion on the column did not get passed to the custom painter print (height = 0.0). Although the result shows that the row did get it's expanded height, if was not passed inside the row to the CustomPainter - 0 height was received.
What do I need to change to achieve the height expansion as well?
Thanks
Here is a reduced test case for the issue you are seeing. The solution is to give your Row a crossAxisAlignment of CrossAxisAlignment.stretch. Otherwise it will try to determine the intrinsic height of your CustomPaint which is zero because it doesn't have a child.
import 'package:flutter/material.dart';
// from https://stackoverflow.com/questions/45875334/how-to-achieve-expansion-of-a-widget-in-both-vertical-height-and-horizontal-w
class MyCustomPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
// NOT using crossAxisAlignment: CrossAxisAlignment.stretch => width = 222.0, height=0.0
// using crossAxisAlignment: CrossAxisAlignment.stretch => width = 222.0, height=560.0
print("width = ${size.width}, height=${size.height}");
canvas.drawRect(Offset.zero & size, new Paint()..color = Colors.blue);
}
#override
bool shouldRepaint(MyCustomPainter other) => false;
}
void main() {
runApp(new MaterialApp(
home: new Scaffold(
body: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text('Above Paint'),
// Expanded - because we are in Column, expand the
// contained row's height
new Expanded(
child: new Row(
// The crossAxisAlignment is needed to give content height > 0
// - we are in a Row, so crossAxis is Column, so this enforces
// to "stretch height".
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Text('Left of Paint'),
// Expanded - because we are in Row, expand the
// contained Painter's width
new Expanded(
child: new CustomPaint(
painter: new MyCustomPainter(),
),
),
new Text('Right of Paint'),
],
),
),
new Text('Below Paint'),
],
)
),
));
}
There is a better way than nesting Row, Expanded and Column widget. You can use the Container widget with Constraints to BoxConstraints.expand().
Example Code:
Widget build(BuildContext context) {
return Container(
constraints: BoxConstraints.expand(),
child: FutureBuilder(
future: loadImage(),
builder: (BuildContext context, AsyncSnapshot<ui.Image> snapshot) {
switch(snapshot.connectionState) {
case ConnectionState.waiting :
return Center(child: Text("loading..."),);
default:
if (snapshot.hasError) {
return Center(child: Text("error: ${snapshot.error}"),);
} else {
return ImagePainter(image: snapshot.data);
}
}
},
),
);
}
Use SizedBox.expand:
SizedBox.expand(
child: YourWidget() // Could be anything like `Column`, `Stack`...
)
For those who struggled to get gradient together with Material behaviour:
return new Stack(
children: <Widget>[
new Material(
elevation: 10,
borderRadius: new BorderRadius.all(new Radius.circular(30.0)),
color: Colors.transparent,
child: new Container(
constraints: BoxConstraints.expand(height: 50),
),
),
new Container(
constraints: BoxConstraints.expand(height: 50),
decoration: BoxDecoration(
borderRadius: new BorderRadius.all(new Radius.circular(30.0)),
gradient: new LinearGradient(
colors: [color1, color2],
begin: Alignment.topCenter,
end: Alignment.bottomCenter),
),
child: new FloatingActionButton.extended(
backgroundColor: Colors.transparent,
foregroundColor: Colors.transparent,
highlightElevation: 0,
elevation: 0,
onPressed: () {
onPressed();
},
label: new Text(this.caption,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.body1),
),
)
],
)