Following question:
I managed to get a lot further with my dosage calculator app and the state management procedure and now I'm trying to scale things up visually speaking.
So I wanted to change the built widget based on a dropdown menu which actually worked out fine but I'm trying to implement an AnimatedSwitcher so every time the user changes the dropdown menu, the old widget fades out and the new one in instead of just switching. Searched for solutions, found one but I don't know if I implemented it the right way, since I'm not getting any animation, but no error message neither.
I'm supposing I either used the wrong child or something like a unique key is missing (which I don't know how to implement)
Here are the necessary parts of my code:
DropdownMenu:
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child:DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedItem,
onChanged: (String string) => setState(() {
streamController.sink.add(string);
return selectedItem = string;
}),
selectedItemBuilder: (BuildContext context) {
return items.map<Widget>((String item) {
return Text(item,
//style: TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.right,
);
}).toList();
},
items: items.map((String item) {
return DropdownMenuItem<String>(
child: Text('$item',
//style: TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.right,
),
value: item,
);
}).toList(),
),
),
);
}
}
StreamBuilder and AnimatedSwitcher:
StreamBuilder(
stream: streamController.stream,
builder: (context, snapshot) {
return AnimatedSwitcher(
duration: Duration(seconds: 1),
child: updateBestandteile(snapshot.data),
);
},
),
Example of condition:
Padding updateBestandteile(String i) {
switch (i) {
case "MMF":
{
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: 200,
decoration: BoxDecoration(
color: b,
borderRadius: BorderRadius.circular(10.0)
),
child: Align(
alignment: Alignment.center,
child: Row(
children: [
Column(
children: [
Text('Zu verwendende Präparate:',
style: TextStyle(fontWeight: FontWeight.bold)),
Text('Medetomidin 1mg/ml'),
Text('Midazolam 5mg/ml'),
Text('Fentanyl 0.5mg/ml'),
Text('NaCl 0,9%'),
],
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
),
Column(
children: [
Text('Anzumischende Menge:',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text((MedetomidindosierungmgprokgKGW*selectedamount*selectedweight/(1000*Medetomidinmgproml)).toString()+"ml"),
Text((MidazolamdosierungmgprokgKGW*selectedamount*selectedweight/(1000*Midazolammgproml)).toString()+"ml"),
Text((FentanyldosierungmgprokgKGW*selectedamount*selectedweight/(1000*Fentanylmgproml)).toString()+"ml"),
Text((((MedetomidindosierungmgprokgKGW*selectedamount*selectedweight/(1000*Medetomidinmgproml))+(MidazolamdosierungmgprokgKGW*selectedamount*selectedweight/(1000*Midazolammgproml))+(FentanyldosierungmgprokgKGW*selectedamount*selectedweight/(1000*Fentanylmgproml)))*4).toString()+"ml"),
],
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
),
],
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
),
),
),
);
}
break;
Hope you might be able to help as you did last time :) Thanks in advance!
Cheers,
P
The issue might be that you are not setting a key. If the new child widget is of the same type as the old widget type then AnimatedSwitcher will NOT animate between them since as as far as the framework is concerned, they are the same widget. Set a unique ValueKey on each child child widget that you wish to animate.
Please refer to Flutter Docs for AnimatedSwitcher and check out the AnimatedSwitcher Widget of the Week video by Flutter Team.
If the "new" child is the same widget type and key as the "old" child,
but with different parameters, then AnimatedSwitcher will not do a
transition between them, since as far as the framework is concerned,
they are the same widget and the existing widget can be updated with
the new parameters. To force the transition to occur, set a Key on
each child widget that you wish to be considered unique (typically a
ValueKey on the widget data that distinguishes this child from the
others).
Related
I was building my Widgets from a list that was predefined in a file of MyClass I created. This worked but I wanted to be able to store persisted data for adding a Boolean favorite field.
I created the Hive Types/Fields for my class, generated the type adapters, and successfully loaded the Hive box on first run of the app, and I can print values to the console, so I know the data is all there and correct.
In the class I have, name, image url path to asset image and a favorite field.
Before when I was using the list to get my data I was able to get the image URL like this:
Expanded(child: Image.asset(widget.MyClass.imageURL)),
Now I want to get this from the Hive box
Box<MyClass> box = Hive.box<MyClass>('myClassBox');
//This is where I am stuck
Expanded(child: Image.asset(box.???)),
I tried box.values.where and box.get() to then get to imageURL field. But get requires a key, which I don't have to pass it from
Widget build(BuildContext context)
And I then have the same issue when trying to access the favorite field, which I am using the Favorite Button package (favorite_button 0.0.4). And I will then update the true/false value based on the button being tapped.
If someone can point me in the right direction that would be great.
Thanks.
Edit:
Here is the Widget:
Widget build(BuildContext context) => GestureDetector(
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => TaskPage(job: widget.job), //Need to get data from Hive now
)),
child: Container(
padding: const EdgeInsets.all(16),
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Expanded(flex: 3, child: buildText()),
Expanded(child: Image.asset(widget.job.imageUrl)),//Need to get data from Hive now
GestureDetector(
child: Icon(
widget.job.fav ? Icons.favorite : Icons.favorite_border, //Need to get data from Hive now
),
onTap: () {
// add/remove from favorites list
}
),
],
),
),
);
Second Edit: Here is the same code after implementing the suggestion given
Widget build(BuildContext context) => GestureDetector(
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => TaskPage(job: Hive.box<Job>('jobBox').get(context)), //This bit is still broken so I need to look at this
)),
child: Column(
children:
Hive.box<Job>('jobBox').values.toList().map(
(elementList) => Container(
padding: const EdgeInsets.all(16),
height: 100,
decoration: BoxDecoration(
color: white,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Expanded(flex: 3, child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
elementList.name,
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20),
),
SizedBox(height: 10),
//Text('Num tasks in job'),
],
)),
Expanded(child: Image.asset(elementList.imageURL)),
GestureDetector(
child: Icon(
elementList.fav
? Icons.favorite
: Icons.favorite_border,
color: elementList.fav ? Colors.red.shade200 : Colors.grey,
),
onTap: () {
//To do
}
// )
),
],
),
),
)
.toList(),
),
);
Assuming that you have only 1 data in the box, you can access that stored data like this.
Box<MyClass> box = Hive.box<MyClass>('myClassBox');
if(box.isNotEmpty) {
final data = box.values.first;
// use data
} else {
// empty state
}
Hive values could have keys, depending on how you use it. If you used box.put(key, value), you can use box.get(key) to work with keys and values.
If you used box.add(value), it stores the data with auto assigned indexes starting from 0. So you can usebox.getAt(index) to get a data with index.
I have List View and I have inside each item in the list a button called "Delete item". When I press that button inside each item I want to delete only that item from the list.
But it does not delete item, it just display Toast message that I have specified.
How I can solve this?
This is the code:
Widget build(BuildContext context) {
listItems = buildVCsFromAPI(context);
return Container(
child: ListView.builder(
itemBuilder: (context, index) =>
_buildListItem(context, listItems[index], index),
itemCount: listItems.length,
physics: AlwaysScrollableScrollPhysics()),
);
}
Widget _buildListItem(
BuildContext context, _VerifiableCredentialListItem cert, int index) {
return GestureDetector(
child: AnimatedAlign(
curve: Curves.ease,
duration: Duration(milliseconds: 500),
heightFactor: selectedPosition == index ? factorMax : factorMin,
alignment: Alignment.topCenter,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10)), //here
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
offset: Offset(0, -1),
blurRadius: 10.0)
]),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
HeadingRow(title: cert.fullTitle, appIcon: cert.appIcon),
displayListItem(index, selectedPosition, cert)
],
),
),
),
}
Column displayListItem(
int index, int selectedIndex, _VerifiableCredentialListItem cert) {
CredentialListGroupType groupType = cert.groupType;
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: UIConstants.s2,
),
buildAnotherWidget(),
SizedBox(
height: UIConstants.s3,
),
buildDeleteAndExportButtons(),
],
);
}
Column buildDeleteAndExportButtons() {
return Column(
children: [
Padding(
padding: EdgeInsets.symmetric(
vertical: UIConstants.s1, horizontal: UIConstants.s2),
child: Row(
children: [
Expanded(
flex: 1,
child: BlueButtonWithIcon(
text: 'Delete item',
icon: 'assets/icons/delete-icon.svg',
onPressed: () {
setState(() {
AppToaster.pop(ToasterType.info, "Delete");
listItems.removeAt(0);
});
},
),
),
SizedBox(width: UIConstants.s1),
Expanded(
flex: 1,
child: BlueButtonWithIcon(
text: 'Export',
icon: 'assets/icons/export.svg',
onPressed: null,
),
)
],
),
),
SizedBox(height: UIConstants.s1)
],
);
}
Calling setState doesn't mean that flutter would actually full repaint the screen it means that it will check your widget tree with the last rendered widget tree and it will paint only the differences and it first compares widgets type and then widget keys to find that there is a difference between the current widget and the previous one and because of this when you remove an item from your list of items flutter checks your returned widgets to the currently rendered widget it doesn't found any difference and it won't repaint the screen and continues showing the last render
So for you to tell the flutter that one of the items in the listView is changed you could assign a uniqueKey key for each list item widget note that for this topic your keys should be unique to the data of that widget otherwise you will face performance issues because if your widget key is changed without any change in the representation of that widget in next time that builds method is called which could happen frequently flutter compares widgets key with the previous widgets key which is rendered to the screen and exist on the render tree and it founds that the keys are different and it repaints that widget which is a redundant operation because your widgets UI and representation are the same
For example, assign a unique id base on the index or content of your data to each data model in the listItems and use that to create a ValueKey() for the widget that is represented by that data
here is a working example of the list which when you click on the list item first list item will be removed
class ListItemDataModel {
final String id;
final Color color;
ListItemDataModel(this.id, this.color);
}
class _MyHomePageState extends State<MyHomePage> {
List<ListItemDataModel> items = [];
#override
void initState() {
super.initState();
items = [
ListItemDataModel("A", Colors.red),
ListItemDataModel("B", Colors.amber),
ListItemDataModel("C", Colors.green),
ListItemDataModel("D", Colors.lightBlueAccent),
ListItemDataModel("E", Colors.pink),
];
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
child: ListView.builder(
itemBuilder: (context, index) {
return GestureDetector(
key: ValueKey(items[index].id),
//Tap to Remove first item from list
onTap: () {
items.removeAt(0);
setState(() {});
},
child: Container(
height: 60,
color: items[index].color,
child: Center(
child: Text(
"This is a unique item with id = ${items[index].id}"),
),
),
);
},
itemCount: items.length,
),
),
);
}
}
So,
We don't have acces to the code above.. so.. where does listItems came from?
Maybe you are retrieving the value of listItems after the init state? if so it's normal that you are retrieving always the same result..
What you should do is the following:
get listItems value from params, global vars, databse ecc
display the list
when you delete a single item you should update the original list
on state updated now the list will be loaded with updated values
If you delete an item from a list but the list is then reloaded in its original form your updates will be lost
I have multiple forms inside a PageView, Forms are in different files like registration_form.dart contains the Sign-Up form and so on. In my App, each page contains a different Form. I want that when the user clicks on "Continue", the form will be validated and in an error situation, the user will be warned. I call all the pages in one class called Body as shown below. The "Continue" button is inside of it in the Opacity container. If there is a better approach to follow as a solution I am open to recommendations.
#override
Widget build(BuildContext context) {
return SafeArea(
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: MediaQuery.of(context).size.height * 0.65,
child: Flex(
direction: Axis.horizontal,
children: [
Flexible(
child: PageView(
controller: _controller,
//physics: new NeverScrollableScrollPhysics(),
children: [
RegisterForm(),
WelcomeForm(),
//CompanyForm(),
//CompanyNextForm(),
//CompanyLogoForm(),
//FinancingDataForm(),
//UtilityForm(),
//MatrixInformationForm(),
//MatrixInformationNextForm(),
//MatrixInformationLastForm(),
//PriceBuildingForm(),
//InstallKitForm(),
//InstallKitDetailedForm(),
//CustomPricingForm(),
//CustomPricingNextForm(),
//FillRow1Form(),
//FillItem1Row1Form(),
//FillItem2Row1Form(),
//FillItem3Row1Form(),
//FillRow2Form(),
//FillItem1Row2Form(),
//FillItem2Row2Form(),
//FillItem3Row2Form(),
//FillRow3Form(),
//FillItem1Row3Form(),
//FillItem2Row3Form(),
//FillItem3Row3Form(),
//InvoicingForm(),
//FinancingForm(),
//FinancingNextForm(),
//FinancingLastForm(),
//FinalizeForm(),
//DoneForm(),
//BookingForm(),
],
),
),
],
),
),
SizedBox(
height: ResponsiveLayout.isSmallScreen(context)
? 10
: ResponsiveLayout.isMediumScreen(context)
? 10
: 10,
),
Opacity(
opacity: 1, //currentIndex == 20 ? 0 : 1,
child: Container(
height: 50,
decoration: BoxDecoration(
color: Color.fromRGBO(16, 88, 198, 1),
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: GestureDetector(
onTap: () {
_controller.nextPage(
duration: Duration(milliseconds: 300),
curve: Curves.easeIn);
},
child: Container(
width: MediaQuery.of(context).size.width,
height: 100,
decoration: BoxDecoration(
color: Color.fromRGBO(16, 88, 198, 1),
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
height: 100.0,
child: Center(
child: RichText(
text: TextSpan(children: [
WidgetSpan(
child: Text(
'Continue ',
style: TextStyle(
color: Colors.white,
fontSize: ResponsiveLayout
.isSmallScreen(context)
? 12
: ResponsiveLayout.isMediumScreen(
context)
? 12
: 15,
),
)),
WidgetSpan(
child: Icon(
Icons.arrow_forward,
size: ResponsiveLayout.isSmallScreen(
context)
? 12
: ResponsiveLayout.isMediumScreen(
context)
? 12
: 15,
color: Colors.white,
),
),
]),
),
)),
],
),
),
)),
),
],
),
),
);
}
Okay. I was struggling with the same question recently and was looking for a good approach. Maybe this answer will be helpful for any other developer looking for the answer.
Approach
Currently, in my case, I used form keys for validations and function callbacks. This solution did the job for me because I needed to take input as raw text. Others like multiple choice options similar where there were predefined outputs.
To describe my solution more explicitly. Consider this example, we want to get basic details of the user like name, age, city etc. For user input like the name, we can use TextFormField. This will give access to the onChanged callback for validation. To access the response in the PageView widget containing the class. You can use the TextEditingController.
Now, we can then simply add the Form widget at the parent of the basic form widget build method.
Finally for multiple choice questions. We can provide a callback function like onTap to the widget of PageView. This function will be called whenever the user interacts with the dropdown or similar widget.
Note: If we have multiple forms in the PageView widget. You will be needing separate form keys for individual forms.
Code Example
So, we have the main form_screen.dart containing the PageView widget and basic_profile.dart containing our form. Both the files should look something like this:
form_screen.dart
...
// define the variables and keys here
final _basicProfileKey = GlobalKey<FormState>();
final _userName = TextEditingController();
late String _userGender;
...
// callback function that we will be passing to the BasicProfile
// widget on the other page
void _userGender(String value) {
_userGender = value;
}
...
// the submission callback that will be called whenever the user
// clicks on the next or save button available in the class file
// (this file) containing the PageView widget
void _submissionCallback(){
if(_pageViewIndex == 0) {
final validationStatus = _basicProfileKey.currentState?.validate() ?? false;
if(validationStatus) {
// implement your logic here and then move to next page in the pageview
}
}
}
...
// Build method widget tree containing the PageView and BasicProfile
// widgets
child: PageView(
children: [
BasicProfile(
basicProfileKey: _basicProfileKey,
userName: _userName,
userGenderCallback: userGender
),
]
),
basic_profile.dart
...
// declare the variables for this widget which we will be initialised
// via constructor
final GlobalKey<FormState> basicProfileKey;
final TextEditingController userName;
final Function(String) userGenderCallback;
...
#override
void initState(){
// initialise the default values here if any and call the
// callback function received above
userGenderCallback(_defaultValue);
}
...
Widget build(BuildContext context){
...
child: Form(
key: basicProfileKey,
...
TextFieldForm(
onValidate: (){
// do the validation here
}
)
...
DropDown(
onChanged: (value) {
// logic for validation
userGenderCallback(value);
}
)
}
In my approach used setState as the state management solution but other state solutions can also be used for easier state sharing between the widgets.
Hope this helps!
I am using CustomPainter widget to draw on a canvas. After some time Streambuilder provides me with new data for CustomPainter. And I would like to draw on the same CustomPainter with the new updated data i.e I want to send tempList from Streambuilder to Drawing widget in CustomPaint.
Should i be using a callback or a key, how will that be used if so, since I'm new to those concepts. I read similar questions but couldn't understand how it will help solve my problem.
Here is my widget hierarchy, please ignore my formatting
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Expanded(
child: Stack(
children: <Widget>[
Positioned(
child: GestureDetector(
// some other widgets
child: Container(
child: ClipRect(
child: CustomPaint(
size: Size.infinite,
painter: Drawing(
pointsList: pointsList,
),
),
),
color: canvasBackgroundColor,
),
),
),
]
),
),
Expanded(
child:
StreamBuilder(
stream: channel.stream,
builder: (context, snapshot) {
if(snapshot.hasData && !snapshot.hasError){
String receivedData = snapshot.data.toString();
tempList = receivedMessage(receivedData);
print(tempList);
return Text("tempList");
}
else
return Text("No Data");
}
)
),
],
),
Wrap the widget that you want to rebuild after getting data again in-stream builder and when every time new data comes your widget will rebuild automatically that's why it have a builder method.
I have a certain Text widget , when it overflows I have 3 options. Either fade ,visible, ellipsis or clip. But I don't want to choose between them . I want if a text has overflow then don't show the text.
Edit :
I'm working on a code clone to this design
Assuming that the textStyle is unknown.
How could I achieve that?
Code:
class SwipeNavigationBar extends StatefulWidget {
final Widget child;
SwipeNavigationBar({this.child});
#override
_SwipeNavigationBarState createState() => _SwipeNavigationBarState();
}
class _SwipeNavigationBarState extends State<SwipeNavigationBar> {
#override
Widget build(BuildContext context) {
return Consumer<Controller>(
builder: (_, _bloc, __) {
return SafeArea(
child: AnimatedContainer(
duration: Duration(seconds: 01),
color: Colors.white,
curve: Curves.easeIn,
height: !_bloc.x ? 50 : 200,
child: Row(
children: [
Column(
verticalDirection: VerticalDirection.up,
children: [
Expanded(child: Icon(Icons.dashboard)),
Expanded(
child: RotatedBox(
quarterTurns: -45,
child: Text(
'data',
softWrap: false,
style: TextStyle(
textBaseline: TextBaseline.alphabetic
),
),
),
),
],
)
],
),
),
);
},
);
}
}
To mimic the design you might want to look into using the Stack widget. However, to answer your question, you'd want to set softWrap to false.
Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 100,
child: Text(
'Some text we want to overflow',
softWrap: false,
),
),
)
softWrap is really the key here. Although, I added the Align and SizedBox widgets to allow this to be used anywhere, regardless of what parent widget you are using (since some widgets set tight constraints on their children and will override their children's size preference).
CodePen Example
Edit: 5/6/2020
With the release of Flutter v1.17 you now have access to a new Widget called NavigationRail which may help you with the design you're looking for.
Use ternary operator to check the length of the text that you are passing to the Text widget and based on that pass the text itself or an empty string.
String yourText;
int desiredLengthToShow = 10; //Change this according to you.
...
Text(
child: yourText.length > desiredLengthToShow ? "" : yourText,
);