how to dynamically build and display widgets based off a string value? - flutter

I've created an app with a large amount of buttons that update a string value.
there is a container in the middle that needs to display different widgets for each button chosen. and when a new button is pressed, the middle container will need to pop the old displaying widgets and build a new one in it's place.
I thought about layering the middle container with all the widgets to display and use the visibility widget connected to the string value using if statements to show/hide them but I don't think this is the best way to do it.
I've put together sample code below
class TestPage extends StatefulWidget {
const TestPage({Key? key}) : super(key: key);
#override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
String displayText = 'Choose option';
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
Expanded(
child: Column(
children: [
SizedBox(
height: 50,
),
Button(
text: 'option A',
onTap: () {
displayText = 'Option A';
},
),
//what would I put in the onTap property to get the app to build the OneOfTheWidgetsToBuild widget in the Display Container below based off the updated string value?
SizedBox(
height: 5,
),
Button(text: 'option B', onTap: null),
],
),
),
Expanded(
child: Column(
children: [
SizedBox(
height: 50,
),
Center(
child: Container(
//DISPLAY CONTAINER
height: 40,
width: 100,
color: Colors.yellow,
child: Center(
child: Text(
'$displayText',
style: TextStyle(color: Colors.black),
),
),
),
)
],
),
),
Expanded(
child: Column(
children: [
SizedBox(
height: 50,
),
Button(text: 'option C', onTap: null),
SizedBox(
height: 5,
),
Button(text: 'option D', onTap: null),
],
),
),
],
),
);
}
}
and here is the widget to display in the middle box:
class OneOfTheWidgetsToBuild extends StatelessWidget {
const OneOfTheWidgetsToBuild({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
height: 40,
width: 100,
color: Colors.green,
);
}
}
thanks so much for your help

If I understand you correctly: You want to show a bunch of different widgets based on the string's value.
One way to go about it, instead of using a bunch of if/else statements is to use a switch case and a function to correctly render the correct widgets.
For example:
Create a widget:
var _displayWidget = Container();
then, create a function to update that widget
void updateWidget(String option) {
switch (option) {
case 'Option A':
_displayWidget = widgetOne();
break;
case 'Option B':
_displayWidget = widgetTwo();
break;
default:
_displayWidget = Container();
break;
}
}
and then, in your Build method:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Test'),
),
body: Column(
children: [
DropdownButton<String>(
items: const [
DropdownMenuItem(
child: Text('Option A'),
value: 'Option A',
),
DropdownMenuItem(
child: Text('Option B'),
value: 'Option B',
),
],
onChanged: (value) {
setState(() {
updateWidget(value!);
});
},
),
_displayWidget,
],
),
);
}

Related

Flutter: how to use DropDownButton?

i'm trying to build a DropdownButton widget of multiple elements, but I'm miserably failing even if I read multiple tutorials on the Internet.
How can I go about creating a simple DropdownButton of 4 elements ?
Thanks for your time
Here's what I tried:
import 'package:flutter/material.dart';
class ForgotPassScreen extends StatefulWidget {
#override
_ForgotPassScreenState createState() => _ForgotPassScreenState();
}
class _ForgotPassScreenState extends State<ForgotPassScreen> {
int _value = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Dropdown Button"),
),
body: Container(
padding: EdgeInsets.all(20.0),
child: DropdownButton(
value: _value,
items: [
DropdownMenuItem(
child: Text("Item 0"),
value: 0,
),
DropdownMenuItem(
child: Text("First Item"),
value: 1,
),
DropdownMenuItem(
child: Text("Second Item"),
value: 2,
),
DropdownMenuItem(
child: Text("Third Item"),
value: 3,
),
DropdownMenuItem(
child: Text("Fourth Item"),
value: 4,
)
],
onChanged: (value) {
setState(() {
_value = value;
});
}),
));
}
}
So this code has basically 3 parts to it. First is the object which stores the icon and the title. The second is the list of these objects, you can have as many as you want. And third is the button itself which constructs the boxes
OBJECT
class Choice {
const Choice({this.title, this.icon});
final String title;
final IconData icon;
}
LIST
List<Choice> choices = <Choice>[
const Choice(title: 'Profile', icon: Icons.account_circle),
const Choice(title:"Log in", icon: Icons.exit_to_app),
]
POPUP BUTTON
PopupMenuButton<Choice>(
color:Colors.white,
onSelected: onItemMenuPress,
itemBuilder: (BuildContext context) {
return choices.map((Choice choice) {
return PopupMenuItem<Choice>(
value: choice,
child: Row(
children: <Widget>[
Icon(
choice.icon,
color:Colors.black
),
Container(
width: 10.0,
),
Text(
choice.title,
style: TextStyle(),
),
],
));
}).toList();
},
)
This is the best way to create the button as you can modify it without having to change every single piece of code

widget communication using void callback flutter

I have a two widget classes , one is the main. I have a class widget have a slider and I want to change a value in the main widget which its contain a row have this slider widget as a child and that text.
here is what I'm trying to do
class MyHomePage extends StatelessWidget {
final double distance ;
final VoidCallback distanceChanged;
const MyHomePage({#required this.distanceChanged,this.distance});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Progress Widget'),
centerTitle: true,
),
body:
Column(
children: [
DistanceSlider(),
Container(height: 120,
),
Text('$distance',
),
],
),
);
}
}
and here is the slider widget class
class DistanceSlider extends StatefulWidget {
#override
_DistanceSliderState createState() => _DistanceSliderState();
}
class _DistanceSliderState extends State<DistanceSlider> {
double _currentSliderValue = 10;
#override
Widget build(BuildContext context) {
return
Container(
height: 150,
child:
Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
child: Padding(
padding: EdgeInsets.all(5),
child: Column(
children: <Widget> [
Expanded(child:
Padding(padding: EdgeInsets.all(20),
child: Row(
children: [Text('Max Distance',
style: TextStyle(
fontSize: 18,
color: Colors.black26,
),
),
Spacer(),
Text(_currentSliderValue.toInt().toString()+'Kms',
style: TextStyle(
fontSize: 18,
),
),
],
),
),
),
SliderTheme(
data: SliderTheme.of(context).copyWith(
inactiveTrackColor: Colors.black12,
trackHeight: 3.0,
),
child: Slider(
value: _currentSliderValue,
min: 0,
max: 100,
divisions: 5,
// _currentSliderValue.round().toString(),
onChanged: (double value) {
setState(() {
_currentSliderValue = value;
}
);
MyHomePage(distance: value,);
},
),
),
],
),
),
),
);
}
}
how can I change the value of distance in the first class based on the slider value ?
I think what you're looking for is a state management solution. Get started here.
I tend to use value holders (if you mix them with ChangeNotifier you can easily react to changes) that I pass to child widgets.
In your case something like this:
class ValueHolder with ChangeNotifier {
double _value;
double get value => _value;
void set(double newValue) {
_value = newValue;
notifyListeners();
}
}
And then instead of declaring _currentSliderValue in the child widget, pass the ValueHolder to the child and use this instead. In the parent widget, attach a listener to the ValueHolder which calls setState on the parent (or update only the Text widget using a StreamBuilder).

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 change CupertinoSwitch size in flutter?

I want to change the size of the CupertinoSwitch in flutter. I have tried putting the switch in Container but changing the size of the container does not affect the switch.
You can copy paste run full code below
You can use Transform.scale and set scale, 1 means normal size, 0.8 means smaller size
code snippet
Transform.scale(
scale: 0.8,
child: CupertinoSwitch(
value: _switchValue,
onChanged: (bool value) {
setState(() {
_switchValue = value;
});
},
),
)
working demo
full code
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CupertinoSwitchDemo(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class CupertinoSwitchDemo extends StatefulWidget {
static const String routeName = '/cupertino/switch';
#override
_CupertinoSwitchDemoState createState() => _CupertinoSwitchDemoState();
}
class _CupertinoSwitchDemoState extends State<CupertinoSwitchDemo> {
bool _switchValue = false;
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: const Text('Switch'),
// We're specifying a back label here because the previous page is a
// Material page. CupertinoPageRoutes could auto-populate these back
// labels.
previousPageTitle: 'Cupertino',
//trailing: CupertinoDemoDocumentationButton(CupertinoSwitchDemo.routeName),
),
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Semantics(
container: true,
child: Column(
children: <Widget>[
Transform.scale(
scale: 0.8,
child: CupertinoSwitch(
value: _switchValue,
onChanged: (bool value) {
setState(() {
_switchValue = value;
});
},
),
),
Text(
"Enabled - ${_switchValue ? "On" : "Off"}"
),
],
),
),
Semantics(
container: true,
child: Column(
children: const <Widget>[
CupertinoSwitch(
value: true,
onChanged: null,
),
Text(
'Disabled - On'
),
],
),
),
Semantics(
container: true,
child: Column(
children: const <Widget>[
CupertinoSwitch(
value: false,
onChanged: null,
),
Text(
'Disabled - Off'
),
],
),
),
],
),
),
),
),
);
}
}
The accepted answer solves almost everything you need. But keep in mind that, if you make your widget smaller with Transform.scale you still have the same invisible space that the widget had before scaling it. That means: even if you scale your Switch, it still occupies the original size. A workaround for that is just to wrap it with a container and give it a desired width and height.
Note: After scaling your Switch, in order to not apply the transformation when performing hit tests, set transformHitTests to false. That way you can more easily control the area where you can tap or click.
Container(
color: Colors.red,
height: 30, //set desired REAL HEIGHT
width: 35, //set desired REAL WIDTH
child: Transform.scale(
transformHitTests: false,
scale: .5,
child: CupertinoSwitch(
value: switchValue,
onChanged: (value) {
setState(() {
switchValue = value;
});
},
activeColor: Colors.green,
),
),
),
using Transform.scale is really a good way of doing it but can cause you some trouble designing or arranging widgets on the screen.
So instead, you can wrap your CupertinoSwitch in FittedBox which is inside another Container, giving you more control over your widget.
You can copy-paste the below code,
you only need to set height and width and make your FittedBox to BoxFit.contain.
Container(
height: 200.0,
width: 200.0,
child: FittedBox(
fit: BoxFit.contain,
child: CupertinoSwitch(
value: _switchValue,
onChanged: (value) {
setState(() {
_switchValue = value;
});
},
),
),
),

How do i make it possible to add a new Hero-Page inside my app?

In my app on the Devices page i currently have 2 example devices(as Hero widget), but in the end i want the user to be able to add and delete devices.
So i added a Floating Action Button to add a new device(first image).
When it gets pressed there should pop up a field to enter a Name.
After choosing a name the new Device should be visible on the Devices (Hero)page. And if it gets selected i wanna come to a Device Page like on the second image.
If someone has an idea how i could realize that i would be very thankful!
Code of the Hero Devices Page:
class DevicesPageHero extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: color_1,
title: Text('Devices'),
),
body: Center(
child:Row(
children: [
Hero(
tag: 'matrix1',
child: GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => MatrixPageOne())),
child: Column(
children: <Widget>[
Image( image: new AssetImage('imgs/matrix1.png'),
height: 100,
width: 100,),
Text("Matrix Kitchen", style: mytextStyle,),
]
)
),
),
Hero(
tag: 'matrix2',
child: GestureDetector(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => MatrixPageTwo())),
child: Column(
children: <Widget>[
Image( image: new AssetImage('imgs/matrix2.png'),
height: 100,
width: 100,),
Text("PARTY ROOM", style: mytextStyle,),
]
)
),
),
] // wrap children
)
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Create new matrix page
},
child: Icon(Icons.add),
backgroundColor: color_3,
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat
);
}
}
Code of the Selected-Device-Page
class MatrixPageOne extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: color_1,
title: Text('Matrix Kitchen'),
),
body: Column(
children: [
Hero(
tag: 'matrix1',
child:Image( image: new AssetImage('imgs/matrix1.png'),
height: 150,
width: 150
)
),
Container(
width: MediaQuery.of(context).size.width, // returns Screen width
child: Text(" Live update Matrix Kitchen", style: TextStyle( fontSize: 15, fontFamily: "Arial", fontWeight: FontWeight.bold) ,/*textAlign: TextAlign.left*/),
),
Divider(),
Spacer(flex: 1),
Container(
width: MediaQuery.of(context).size.width,
child:Text(" Option 1 ...", textAlign: TextAlign.left,),
),
Divider(),
Spacer(flex: 1),
Container(
width: MediaQuery.of(context).size.width,
child:Text(" Option 2 ...", textAlign: TextAlign.left,),
),
Divider(),
Spacer(flex: 80),
IconButton(
icon: Icon(Icons.delete, color: Colors.red,),
onPressed: (){
//Delete Hero Page
}
)
], // Column children
),
);
}
}
You can create a new single page, then send parameter like tagName and Image to that page. On that page, you can display the previously sent data up to your liking.
For example:
..
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => MatrixPageTwo(
'tagName': 'matrix2',
'image': new AssetImage('imgs/matrix2.png'), height: 100, width: 100)
))),
..
And then, on that MatrixPageTwo page:
class MatrixPageTwo extends StatefulWidget {
const MatrixPageTwo({ Key key, this.image, this.tagName }) : super(key: key);
final Image image;
final String tagName;
#override
_MatrixPageTwoState createState() => _MatrixPageTwoState();
}
class _MatrixPageTwoState extends State<MatrixPageTwo> {
#override
Widget build(BuildContext context) {
return Center(
child: Text(widget.tagName),
);
}
}