Related
I am looking to create a grid with 4 custom widgets that can either add or subtract from a given starting number. See image for reference.
For example, if you press player one, the number would increase or decrease to 100 or 99. But the other 3 players would remain the same.
I had originally used one stateful widget with a separate function for each player, but I am sure there's a way to do it in a more modular way.
class CommanderDamage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return CommanderDamageState();
}
}
class CommanderDamageState extends State<CommanderDamage> {
int damage = 0;
void update() {
setState(() {
damage++;
});
}
#override
Widget build(context) {
return MaterialApp(
home: Scaffold(
body: GridView.builder(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemCount: 4,
itemBuilder: (BuildContext context, index) {
return Container(
child: Column(
children: <Widget>[
Text("Player " + index.toString()),
InkWell(
onTap: update,
child: Container(
width: 100.0,
height: 100.0,
child: Text(damage),
)
],
),
);
},
),
),
);
}
}
EDIT: I have edited my code to reflect my current. Currently, when the damage area is pressed, the damage increases for all 4 players instead of the one I am pressing.
Wrap your text widget inside InkWell(). Basically what InkWell does is creates a rectangular touch responsive area.
InkWell(
child: Text(
'Player One',
style: TextStyle(
fontSize: 20, color: Colors.white),
onTap: () {
// Your function
}
)
But this make the interactive tap area according to size of the text which is very small, so it's better to wrap it inside a container and provide height-width or some space with padding
InkWell(
child: Container(
width: 100,
height: 100,
child: Text(
'Player One',
style: TextStyle(
fontSize: 20, color: Colors.white), ),
onTap: () {
// Your function
}
)
An inside onTap you can your function and perform changes.
Read more about InkWell:
https://docs.flutter.io/flutter/material/InkWell-class.html
After lots of trial and error I managed to find an answer.
I had to set the state within the onTap instead of making a separate function and calling it in the onTap.
class CommanderDamage extends StatefulWidget {
int damage = 0;
CommanderDamage({this.damage, Key key});
#override
State<StatefulWidget> createState() {
return CommanderDamageState();
}
}
class CommanderDamageState extends State<CommanderDamage> {
var damage = [0, 0, 0, 0, 0, 0];
#override
Widget build(context) {
return MaterialApp(
home: Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft, end: Alignment.bottomRight,
colors: [Color(0xfff6921e), Color(0xffee4036)],
),
),
child: GridView.builder(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemCount: damage.length,
itemBuilder: (BuildContext context, index) {
return Container(
child: Column(
children: <Widget>[
InkWell(
onTap: () {
setState(() {
damage[index]++;
});
},
onLongPress: () {
setState(() {
damage[index] = 0;
});
},
child: Container(
width: 100.0,
height: 100.0,
child: Text(damage[index].toString()),
),
),
],
),
);
},
),
),
],
),
),
);
}
}
I have this piece of code which I got from Style clipboard in flutter
showMenu(
context: context,
// TODO: Position dynamically based on cursor or textfield
position: RelativeRect.fromLTRB(0.0, 600.0, 300.0, 0.0),
items: [
PopupMenuItem(
child: Row(
children: <Widget>[
// TODO: Dynamic items / handle click
PopupMenuItem(
child: Text(
"Paste",
style: Theme.of(context)
.textTheme
.body2
.copyWith(color: Colors.red),
),
),
PopupMenuItem(
child: Text("Select All"),
),
],
),
),
],
);
This code works great, except that the popup that is created is at a fixed position, how would I make it so that it pops up at the mouse/press/finger/cursor position or somewhere near that, kind of like when you want to copy and paste on your phone. (This dialog popup will not be used for copy and pasting)
I was able to solve a similar issue by using this answer:
https://stackoverflow.com/a/54714628/559525
Basically, I added a GestureDetector() around each ListTile and then you use onTapDown to store your press location and onLongPress to call your showMenu function. Here are the critical functions I added:
_showPopupMenu() async {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
await showMenu(
context: context,
position: RelativeRect.fromRect(
_tapPosition & Size(40, 40), // smaller rect, the touch area
Offset.zero & overlay.size // Bigger rect, the entire screen
),
items: [
PopupMenuItem(
child: Text("Show Usage"),
),
PopupMenuItem(
child: Text("Delete"),
),
],
elevation: 8.0,
);
}
void _storePosition(TapDownDetails details) {
_tapPosition = details.globalPosition;
}
}
And then here is the full code (you'll have to tweak a few things like the image, and filling in the list of devices):
import 'package:flutter/material.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'dart:core';
class RecentsPage extends StatefulWidget {
RecentsPage({Key key, this.title}) : super(key: key);
final String title;
#override
_RecentsPageState createState() => _RecentsPageState();
}
class _RecentsPageState extends State<RecentsPage> {
List<String> _recents;
var _tapPosition;
#override
void initState() {
super.initState();
_tapPosition = Offset(0.0, 0.0);
getRecents().then((value) {
setState(() {
_recents = value;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFFFFFFF),
body: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(height: 25),
Stack(
children: <Widget>[
Container(
padding: EdgeInsets.only(left: 40),
child: Center(
child: AutoSizeText(
"Recents",
maxLines: 1,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 32),
),
),
),
Container(
padding: EdgeInsets.only(left: 30, top: 0),
child: GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Transform.scale(
scale: 2.0,
child: Icon(
Icons.chevron_left,
),
)),
),
],
),
Container(
height: 15,
),
Container(
height: 2,
color: Colors.blue,
),
Container(
height: 10,
),
Flexible(
child: ListView(
padding: EdgeInsets.all(15.0),
children: ListTile.divideTiles(
context: context,
tiles: _getRecentTiles(),
).toList(),
),
),
Container(height: 15),
],
),
),
),
);
}
List<Widget> _getRecentTiles() {
List<Widget> devices = List<Widget>();
String _dev;
String _owner = "John Doe";
if (_recents != null) {
for (_dev in _recents.reversed) {
if (_dev != null) {
_dev = _dev.toUpperCase().trim();
String serial = "12341234";
devices.add(GestureDetector(
onTapDown: _storePosition,
onLongPress: () {
print("long press of $serial");
_showPopupMenu();
},
child: ListTile(
contentPadding: EdgeInsets.symmetric(vertical: 20),
leading: Transform.scale(
scale: 0.8,
child: Image(
image: _myImage,
)),
title: AutoSizeText(
"$_owner",
maxLines: 1,
style: TextStyle(fontSize: 22),
),
subtitle: Text("Serial #: $serial"),
trailing: Icon(Icons.keyboard_arrow_right),
)));
}
}
} else {
devices.add(ListTile(
contentPadding: EdgeInsets.symmetric(vertical: 20),
title: AutoSizeText(
"No Recent Devices",
maxLines: 1,
style: TextStyle(fontSize: 20),
),
subtitle:
Text("Click the button to add a device"),
onTap: () {
print('add device');
},
));
}
return devices;
}
_showPopupMenu() async {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
await showMenu(
context: context,
position: RelativeRect.fromRect(
_tapPosition & Size(40, 40), // smaller rect, the touch area
Offset.zero & overlay.size // Bigger rect, the entire screen
),
items: [
PopupMenuItem(
child: Text("Show Usage"),
),
PopupMenuItem(
child: Text("Delete"),
),
],
elevation: 8.0,
);
}
void _storePosition(TapDownDetails details) {
_tapPosition = details.globalPosition;
}
}
Use gesture detector's onTapDown like this
GestureDetector(
onTapDown: (TapDownDetails details) {
showPopUpMenu(details.globalPosition);
},
then in this method we use tap down details to find position
Future<void> showPopUpMenu(Offset globalPosition) async {
double left = globalPosition.dx;
double top = globalPosition.dy;
await showMenu(
color: Colors.white,
//add your color
context: context,
position: RelativeRect.fromLTRB(left, top, 0, 0),
items: [
PopupMenuItem(
value: 1,
child: Padding(
padding: const EdgeInsets.only(left: 0, right: 40),
child: Row(
children: [
Icon(Icons.mail_outline),
SizedBox(
width: 10,
),
Text(
"Menu 1",
style: TextStyle(color: Colors.black),
),
],
),
),
),
PopupMenuItem(
value: 2,
child: Padding(
padding: const EdgeInsets.only(left: 0, right: 40),
child: Row(
children: [
Icon(Icons.vpn_key),
SizedBox(
width: 10,
),
Text(
"Menu 2",
style: TextStyle(color: Colors.black),
),
],
),
),
),
PopupMenuItem(
value: 3,
child: Row(
children: [
Icon(Icons.power_settings_new_sharp),
SizedBox(
width: 10,
),
Text(
"Menu 3",
style: TextStyle(color: Colors.black),
),
],
),
),
],
elevation: 8.0,
).then((value) {
print(value);
if (value == 1) {
//do your task here for menu 1
}
if (value == 2) {
//do your task here for menu 2
}
if (value == 3) {
//do your task here for menu 3
}
});
hope it works
Here is a reusable widget which does what you need. Just wrap your Text or other Widget with this CopyableWidget and pass in the onGetCopyTextRequested. It will display the Copy menu when the widget is long pressed, copy the text contents returned to the clipboard, and display a Snackbar on completion.
/// The text to copy to the clipboard should be returned or null if nothing can be copied
typedef GetCopyTextCallback = String Function();
class CopyableWidget extends StatefulWidget {
final Widget child;
final GetCopyTextCallback onGetCopyTextRequested;
const CopyableWidget({
Key key,
#required this.child,
#required this.onGetCopyTextRequested,
}) : super(key: key);
#override
_CopyableWidgetState createState() => _CopyableWidgetState();
}
class _CopyableWidgetState extends State<CopyableWidget> {
Offset _longPressStartPos;
#override
Widget build(BuildContext context) {
return InkWell(
highlightColor: Colors.transparent,
onTapDown: _onTapDown,
onLongPress: () => _onLongPress(context),
child: widget.child
);
}
void _onTapDown(TapDownDetails details) {
setState(() {
_longPressStartPos = details?.globalPosition;
});
}
void _onLongPress(BuildContext context) async {
if (_longPressStartPos == null)
return;
var isCopyPressed = await showCopyMenu(
context: context,
pressedPosition: _longPressStartPos
);
if (isCopyPressed == true && widget.onGetCopyTextRequested != null) {
var copyText = widget.onGetCopyTextRequested();
if (copyText != null) {
await Clipboard.setData(ClipboardData(text: copyText));
_showSuccessSnackbar(
context: context,
text: "Copied to the clipboard"
);
}
}
}
void _showSuccessSnackbar({
#required BuildContext context,
#required String text
}) {
var scaffold = Scaffold.of(context, nullOk: true);
if (scaffold != null) {
scaffold.showSnackBar(
SnackBar(
content: Row(
children: <Widget>[
Icon(
Icons.check_circle_outline,
size: 24,
),
SizedBox(width: 8),
Expanded(
child: Text(text)
)
],
)
)
);
}
}
}
Future<bool> showCopyMenu({
BuildContext context,
Offset pressedPosition
}) {
var x = pressedPosition.dx;
var y = pressedPosition.dy;
return showMenu<bool>(
context: context,
position: RelativeRect.fromLTRB(x, y, x + 1, y + 1),
items: [
PopupMenuItem<bool>(value: true, child: Text("Copy")),
]
);
}
So basically there is this product edit page which behaves differently uppon called.
If the product is being created for first time, then it is shown in a tab view controller.
if the product is being updated, its body is returned in scaffold.
here are some screenshots
when I submit through create product, i encounter no error.
But when I submit through update product, though the logic works, i get a short red screen with
like this
error Another exception was thrown: No Material widget found.
Here is the code for the screen
`
import 'package:flutter/material.dart';
import 'package:academy_app/models/products.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:academy_app/scoped-model/Products.dart';
class ProductEdit extends StatefulWidget {
ProductEdit();
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return ProductEditState();
}
}
class ProductEditState extends State<ProductEdit> {
String title;
String description;
double price;
Product product;
final focusnode2 = FocusNode();
final focusnode3 = FocusNode();
Map<String, dynamic> formData = {
"name": null,
"desc": null,
"price": null,
"image": "asset/foood.jpg"
};
GlobalKey<FormState> formkey = GlobalKey<FormState>();
Widget buildTitle(productItem) {
return TextFormField(
initialValue: productItem != null ? productItem.title : "",
validator: (String value) {
if (value.isEmpty || value.length < 3) {
return 'title cannot be empty';
}
},
textInputAction: TextInputAction.next,
decoration: InputDecoration(labelText: "Title"),
onFieldSubmitted: (String value) {
FocusScope.of(context).requestFocus(focusnode2);
},
onSaved: (String valuee) {
setState(() {
formData["name"] = valuee;
});
},
);
}
Widget buildDesc(productItem) {
return TextFormField(
initialValue: productItem != null ? productItem.description : "",
validator: (String value) {
if (value.isEmpty || value.length < 3) {
return 'Cant have that short description';
}
},
textInputAction: TextInputAction.next,
onFieldSubmitted: (value) {
FocusScope.of(context).requestFocus(focusnode3);
},
focusNode: focusnode2,
maxLines: 3,
decoration: InputDecoration(labelText: "Description"),
onSaved: (String valuee) {
setState(() {
formData["desc"] = valuee;
});
},
);
}
Widget buildPrice(productItem) {
return TextFormField(
initialValue: productItem != null ? productItem.price.toString() : "",
textInputAction: TextInputAction.done,
focusNode: focusnode3,
decoration: InputDecoration(labelText: " How much"),
keyboardType: TextInputType.number,
onFieldSubmitted: (value) {
focusnode3.unfocus();
},
validator: (value) {
if (!RegExp(r'^(?:[1-9]\d*|0)?(?:\.\d+)?$').hasMatch(value)) {
return ' Enter numbers only';
}
},
onSaved: (String valuee) {
setState(() {
formData["price"] = double.parse(valuee);
});
},
);
}
void submitForm(Function addProduct, Function updateProduct, int index) {
if (!formkey.currentState.validate()) {
return;
}
formkey.currentState.save();
setState(() {
if (index == null) {
addProduct(Product(
price: formData["price"],
title: formData["name"],
description: formData["desc"],
image: "asset/foood.jpg"));
} else {
updateProduct(
Product(
price: formData["price"],
title: formData["name"],
description: formData["desc"],
image: "asset/foood.jpg"),
);
}
Navigator.pushReplacementNamed(context, '/');
});
}
Widget buildSubmitButton() {
return ScopedModelDescendant<ProductsModel>(
builder: (BuildContext context, Widget, ProductsModel) {
return RaisedButton(
child: Text("Save"),
onPressed: () => submitForm(ProductsModel.addProduct,
ProductsModel.updateProduct, ProductsModel.selected_index));
},
);
}
Widget _buildPageContent(BuildContext context, Product product) {
final double deviceWidth = MediaQuery.of(context).size.width;
final double targetWidth = deviceWidth > 550.0 ? 500.0 : deviceWidth * 0.95;
final double targetPadding = deviceWidth - targetWidth;
return GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Container(
margin: EdgeInsets.all(10.0),
child: Form(
key: formkey,
child: ListView(
padding: EdgeInsets.symmetric(horizontal: targetPadding / 2),
children: <Widget>[
buildTitle(product),
buildDesc(product),
buildPrice(product),
SizedBox(
height: 10.0,
),
buildSubmitButton()
],
),
),
),
);
}
#override
Widget build(BuildContext context) {
return ScopedModelDescendant<ProductsModel>(
builder: (context, Widget child, ProductsModel) {
product = ProductsModel.getproduct();
return ProductsModel.selected_index == null
? _buildPageContent(context, product)
: Scaffold(
appBar: AppBar(
title: Text("Update Item"),
),
body:_buildPageContent(context, product) ,
);
},
);
}
}
`
why am i getting that red screen error? i confused about passing the contexts. why arent the textfiled accessing the material parent through in scaffold?
As the error suggests that no material widget found, you need to wrap the Container of the _buildPateContent function into Material Widget. Here is the change that you can do:
Widget _buildPageContent(BuildContext context, Product product) {
final double deviceWidth = MediaQuery.of(context).size.width;
final double targetWidth = deviceWidth > 550.0 ? 500.0 : deviceWidth * 0.95;
final double targetPadding = deviceWidth - targetWidth;
return GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Material(
child: Container(
margin: EdgeInsets.all(10.0),
child: Form(
key: formkey,
child: ListView(
padding: EdgeInsets.symmetric(horizontal: targetPadding / 2),
children: <Widget>[
buildTitle(product),
buildDesc(product),
buildPrice(product),
SizedBox(
height: 10.0,
),
buildSubmitButton()
],
),
),
),
));
}
try wrapping you app in materialApp or wrap TextField in material Widget.
In my case I forgot to wrap my widget with Scaffold widget. A lot of widgets need to be wrapped with it to work properly. So change this
Widget build(BuildContext context) {
return YourScreenContent();
}
to this
Widget build(BuildContext context) {
return Scaffold(
body: YourScreenContent(),
);
}
I had same issue.
I got the error when I had code like this.
Widget _getLCSBar(int index) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black, width: 0.1),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_getLikeButton(index),
_getCommentButton(index),
_getShareButton(index),
],
));
}
What I have done is wrapped it with Material
Widget _getLCSBar(int index) {
return Material(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black, width: 0.1),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_getLikeButton(index),
_getCommentButton(index),
_getShareButton(index),
],
)));
}
Problem Solved!
Just wrap the Gesture Detector inside Material:
return Material(
child: GestureDetector(
...
),
);
I am working on Flutter TextField widget. I want to show an error message below the TextField widget if the user does not fill that TextField. I only have to use TextField Widget not TextFormField in this case.
A Minimal Example of what you Want:
class MyHomePage extends StatefulWidget {
#override
MyHomePageState createState() {
return new MyHomePageState();
}
}
class MyHomePageState extends State<MyHomePage> {
final _text = TextEditingController();
bool _validate = false;
#override
void dispose() {
_text.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('TextField Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Error Showed if Field is Empty on Submit button Pressed'),
TextField(
controller: _text,
decoration: InputDecoration(
labelText: 'Enter the Value',
errorText: _validate ? 'Value Can\'t Be Empty' : null,
),
),
RaisedButton(
onPressed: () {
setState(() {
_text.text.isEmpty ? _validate = true : _validate = false;
});
},
child: Text('Submit'),
textColor: Colors.white,
color: Colors.blueAccent,
)
],
),
),
);
}
}
Flutter handles error text itself, so we don't require to use variable _validate. It will check at runtime whether you satisfied the condition or not.
final confirmPassword = TextFormField(
controller: widget.confirmPasswordController,
obscureText: true,
decoration: InputDecoration(
prefixIcon: Icon(Icons.lock_open, color: Colors.grey),
hintText: 'Confirm Password',
errorText: validatePassword(widget.confirmPasswordController.text),
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
),
);
String validatePassword(String value) {
if (!(value.length > 5) && value.isNotEmpty) {
return "Password should contain more than 5 characters";
}
return null;
}
Note: User must add at least one character to get this error message.
I would consider using a TextFormField with a validator.
Example:
class MyHomePageState extends State<MyHomePage> {
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('TextFormField validator'),
),
body: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
decoration: InputDecoration(
hintText: 'Enter text',
),
textAlign: TextAlign.center,
validator: (text) {
if (text == null || text.isEmpty) {
return 'Text is empty';
}
return null;
},
),
RaisedButton(
onPressed: () {
if (_formKey.currentState.validate()) {
// TODO submit
}
},
child: Text('Submit'),
)
],
),
),
);
}
}
If you use TextFormField then you could easily implement 'Error
below your text fields'.
You can do this without using _validate or any other flags.
In this example, I have used validator method of TextFormField
widget. This makes the work a lot more easier and readable at the
same time.
I also used FormState to make the work more easier
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final _form = GlobalKey<FormState>(); //for storing form state.
//saving form after validation
void _saveForm() {
final isValid = _form.currentState.validate();
if (!isValid) {
return;
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Form(
key: _form, //assigning key to form
child: ListView(
children: <Widget>[
TextFormField(
decoration: InputDecoration(labelText: 'Full Name'),
validator: (text) {
if (!(text.length > 5) && text.isNotEmpty) {
return "Enter valid name of more then 5 characters!";
}
return null;
},
),
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
validator: (text) {
if (!(text.contains('#')) && text.isNotEmpty) {
return "Enter a valid email address!";
}
return null;
},
),
RaisedButton(
child: Text('Submit'),
onPressed: () => _saveForm(),
)
],
),
),
),
);
}
}
I hope this helps!
For TextFiled and TextFormFiled Validation you can use this Example I hope this will helpful for you people.
TextField(
enableInteractiveSelection: true,
autocorrect: false,
enableSuggestions: false,
toolbarOptions: ToolbarOptions(
copy: false,
paste: false,
cut: false,
selectAll: false,
),
controller: _currentPasswordController,
obscureText: passwordVisible,
decoration: InputDecoration(
errorText: Validators.password(
_currentPasswordController.text),
filled: true,
fillColor: Colors.white,
contentPadding:
const EdgeInsets.fromLTRB(20, 24, 12, 16),
border: const OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(8.0))),
// filled: true,
labelText: 'Password',
hintText: 'Enter your password',
suffixIcon: GestureDetector(
onTap: () {
setState(() {
passwordVisible = !passwordVisible;
});
},
child: Container(
margin: const EdgeInsets.all(13),
child: Icon(
passwordVisible
? FontAwesomeIcons.eyeSlash
: Icons.remove_red_eye_sharp,
color: ColorUtils.primaryGrey,
size: 25)),
),
),
),
Validation Message Example Code
static password(String? txt) {
if (txt == null || txt.isEmpty) {
return "Invalid password!";
}
if (txt.length < 8) {
return "Password must has 8 characters";
}
if (!txt.contains(RegExp(r'[A-Z]'))) {
return "Password must has uppercase";
}
if (!txt.contains(RegExp(r'[0-9]'))) {
return "Password must has digits";
}
if (!txt.contains(RegExp(r'[a-z]'))) {
return "Password must has lowercase";
}
if (!txt.contains(RegExp(r'[#?!#$%^&*-]'))) {
return "Password must has special characters";
} else
return;
}
I am pretty new to Flutter and Dart and I can't seem to find any hints for this particular topic. I am trying to put 3 RadioListTiles in a Row like so:
Row(
children: [
Expanded(
child:RadioListTile<GoalSelection>(
title: Text(
'Net',
style: Theme.of(context).textTheme.body1,
),
value: GoalSelection.net,
groupValue: _goalSelection,
onChanged: (GoalSelection value) {
setState(() {
_goalSelection = value;
});
},
),
),
Expanded(
child: RadioListTile<GoalSelection>(
title: Text(
'Gross',
style: Theme.of(context).textTheme.body1,
),
value: GoalSelection.gross,
groupValue: _goalSelection,
onChanged: (GoalSelection value) {
setState(() {
_goalSelection = value;
});
},
),
),
Expanded(
child: RadioListTile<GoalSelection>(
title: Text(
'Salary',
style: Theme.of(context).textTheme.body1,
),
value: GoalSelection.salary,
groupValue: _goalSelection,
onChanged: (GoalSelection value) {
setState(() {
_goalSelection = value;
});
},
),
),
],
),
The buttons layout fine, but there seems to be a lot of wasted space for the label. I put a screenshot of what it currently looks like below. I have tried wrapping the Expanded, the RadioListTile, and the Text in Padding widgets (all one at a time) to manually set the padding to 0, but it didn't do anything. I have also tried to change Expanded to Flexible even though I didn't think that would change anything. I am at a loss now. Is there any way to get this layout to work? I am kind of assuming it is something really dumb that I am doing.
You can use Radio + text widget instead of RadioListTile. For removing internal padding in Radio widget set:
Radio(
visualDensity: const VisualDensity(
horizontal: VisualDensity.minimumDensity,
vertical: VisualDensity.minimumDensity),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
.....
),
You can use a Radio and Text widget in a row. But the Radio also has the same padding problem. To remove the padding you can put the Radio as a child of a SizedBox.
eg:- SizedBox(height: 20, width: 20, child: Radio(.......))
RadioListTile is used with the purpose of taking the full width in a vertical scroll list.
If you don't want this behavior, don't use it. Use Radio instead.
just set contentPadding: EdgeInsets.zero
RadioListTile(contentPadding: EdgeInsets.zero)
We can control the padding of the RadioListTile using Flexible widget. As you want to arrange 3 RadioListTiles inside a Row Widget. Please try with the below code, it will work.
Row(
children: <Widget>[
Flexible(
fit: FlexFit.loose,
child:
RadioListTile(
title: const Text('hello'),
onChanged: (value) {
setState(() {});
},
),
),
Flexible(
fit: FlexFit.loose,
child:
RadioListTile(
title: const Text('Lafayette'),
onChanged: (value) {
setState(() {});
},
),
)
],
),
Do, let me know. Once you tried with the above code. If it resolved you problem, please accept my answer as useful and provide your valuable comments.
I got the same problem. You could try to customize with Radio, Text, InkWell, Padding.
class LabeledRadio extends StatelessWidget {
const LabeledRadio({
this.label,
this.padding,
this.groupValue,
this.value,
this.onChanged,
});
final String label;
final EdgeInsets padding;
final bool groupValue;
final bool value;
final Function onChanged;
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
if (value != groupValue)
onChanged(value);
},
child: Padding(
padding: padding,
child: Row(
children: <Widget>[
Radio<bool>(
groupValue: groupValue,
value: value,
onChanged: (bool newValue) {
onChanged(newValue);
},
),
Text(label),
],
),
),
);
}
}
// ...
bool _isRadioSelected = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <LabeledRadio>[
LabeledRadio(
label: 'This is the first label text',
padding: const EdgeInsets.symmetric(horizontal: 5.0),
value: true,
groupValue: _isRadioSelected,
onChanged: (bool newValue) {
setState(() {
_isRadioSelected = newValue;
});
},
),
LabeledRadio(
label: 'This is the second label text',
padding: const EdgeInsets.symmetric(horizontal: 5.0),
value: false,
groupValue: _isRadioSelected,
onChanged: (bool newValue) {
setState(() {
_isRadioSelected = newValue;
});
},
),
],
),
);
}
The documentation: https://api.flutter.dev/flutter/material/RadioListTile-class.html#material.RadioListTile.3
This is how I fix the padding:
enum ContactSex { nam, nu, khac }
class CreateContactScreen extends StatefulWidget {
static const routeName = './create_contact';
#override
_CreateContactScreenState createState() => _CreateContactScreenState();
}
class _CreateContactScreenState extends State<CreateContactScreen> {
ContactSex _contaxtSex = ContactSex.nu;
final _form = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'TẠO LIÊN HỆ',
style: kHeaderTextStyle,
),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('XONG', style: TextStyle(color: Colors.white),),
)
],
),
body: Container(
padding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 20.0),
child: Form(
key: _form,
child: SingleChildScrollView(
child: Column(
children: <Widget>[
TextFormField(
decoration: InputDecoration(
labelText: 'Tên*',
),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
// TODO: when submit this text field
},
validator: (value) {
if (value.isEmpty) {
return 'Hãy nhập tên cho liên hệ.';
}
return null;
},
onSaved: (value) {
// TODO : when save the whole form
},
),
TextFormField(
decoration: InputDecoration(
labelText: 'Họ',
),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
// TODO: when submit this text field
},
// validator: (value) {
// if (value.isEmpty) {
// return null;
// }
// return null;
// },
onSaved: (value) {
// TODO : when save the whole form
},
),
TextFormField(
decoration: InputDecoration(
labelText: 'Số điện thoại*',
),
keyboardType: TextInputType.phone,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
// TODO: when submit this text field
},
validator: (value) {
if (value.isEmpty) {
return 'Hãy nhập số điện thoại cho liên hệ.';
}
return null;
},
onSaved: (value) {
// TODO : when save the whole form
},
),
TextFormField(
decoration: InputDecoration(
labelText: 'Email',
),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
// TODO: when submit this text field
},
// validator: (value) {
// if (value.isEmpty) {
// return null;
// }
// return null;
// },
onSaved: (value) {
// TODO : when save the whole form
},
),
Container(
padding: EdgeInsets.only(top: 15.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Giới tính',
style: TextStyle(fontSize: 14.0),
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
LabeledRadio(
label: 'Nữ',
padding: EdgeInsets.all(0),
groupValue: _contaxtSex,
value: ContactSex.nu,
onChanged: (ContactSex newValue) {
setState(() {
_contaxtSex = newValue;
});
},
),LabeledRadio(
label: 'Nam',
padding: EdgeInsets.all(0),
groupValue: _contaxtSex,
value: ContactSex.nam,
onChanged: (ContactSex newValue) {
setState(() {
_contaxtSex = newValue;
});
},
),LabeledRadio(
label: 'Khác',
padding: EdgeInsets.all(0),
groupValue: _contaxtSex,
value: ContactSex.khac,
onChanged: (ContactSex newValue) {
setState(() {
_contaxtSex = newValue;
});
},
),
],
),
],
),
)
],
)),
),
),
);
}
}
class LabeledRadio extends StatelessWidget {
final String label;
final EdgeInsets padding;
final ContactSex groupValue;
final ContactSex value;
final Function onChanged;
const LabeledRadio(
{this.label, this.padding, this.groupValue, this.value, this.onChanged});
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
if (value != groupValue) {
onChanged(value);
}
},
child: Padding(
padding: padding,
child: Row(
children: <Widget>[
Radio<ContactSex>(
groupValue: groupValue,
value: value,
onChanged: (ContactSex newValue) {
onChanged(newValue);
},
),
Text(label),
],
),
),
);
}
}
You just need to set the "dense" property to true, example:
RadioListTile<String>(
title: "My radio",
dense: true, // <= here it is !
value: '1',
);
you should achieve this manually like
make a group of Radio() and Text() and wrap with InkWell() for state handling. now remove extra space of radio by materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, That's it. Get idea by sample code.
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: () {
setState(() {
_radioVenue = 0;
});
},
child: Row(
children: [
Radio(
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
activeColor: primaryColor,
groupValue: _radioVenue,
onChanged: (value) {},
value: 0,
),
Text('From our list')
],
),
),
InkWell(
onTap: () {
setState(() {
_radioVenue = 1;
});
},
child: Row(
children: [
Radio(
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
activeColor: primaryColor,
groupValue: _radioVenue,
onChanged: (value) {},
value: 1,
),
Text('From our list')
],
),
),
],
),
We covered both both the issues in this sample.
Removed extra spaces.
whole group is selectable radio + text, Now it behaves like RadioListTile().
Simply use RadioListTile and remove extra padding, by default it's 18
RadioListTile(contentPadding: EdgeInsets.symmetric(horizontal: 0.0)),
OR
RadioListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 0.0),
value: null,
groupValue: null,
onChanged: null,
),
glad to answer
I was looking for same question and ended up on Flutter Documentation
I was working on Column and RadioListTile and I faced same issue, there's a horizontal padding between content inside RadioListTile
So, here it's the answer
Looking for this documentation ! RadioListTile content padding
Just add contentPadding: EdgeInsets.symmetric(horizontal: 0) and here you go, there's no horizontal padding anymore
Just copy paste this code and enjoy
Container(
height:35,
child: Row(
children: [
Radio(
groupValue: data.selected,
value: e,
onChanged: (DataBindModel? value) {
listener.value = MultiChoiceData(selected: value, items: listener.value.items);
onChanged(value);
onSelected(value);
},
),
Text(
e.value,
style: body14,
)
],
),
)
Copy the RadioListTile code and create your on new new file and paste it in there.
Remove the imports causing errors:
import 'package:flutter/widgets.dart'; // leave it
import 'package:flutter/material.dart'; //add
import 'list_tile.dart'; //remove
import 'radio.dart'; //remove
import 'theme.dart'; //remove
import 'theme_data.dart'; //remove
Then add the following padding to it, like this:
//Inside the file locate this widget and Add the padding or remove it. I needed to remove it and add 5.
return MergeSemantics(
child: ListTileTheme.merge(
contentPadding: EdgeInsets.only( // Add this
left: 5,
right: 0,
bottom: 0,
top: 0
),
selectedColor: activeColor ?? Theme.of(context).accentColor,
child: ListTile(
leading: leading,
title: title,
subtitle: subtitle,
trailing: trailing,
isThreeLine: isThreeLine,
dense: dense,
enabled: onChanged != null,
onTap: onChanged != null && !checked ? () { onChanged(value); } : null,
selected: selected,
),
),
);
then Import the file into your project like this:
import 'package:Project_Name/common/customComponets/custom_radio_list_tile.dart' as CustomRadioListTile;
Then use it like this:
CustomRadioListTile.RadioListTile(); // and that's how I managed to do it. Thought I should share.
This is my way of reducing the space. I have three Radio in one row.
Row(
children: [
Expanded(
child: RadioListTile(
contentPadding: EdgeInsets.all(0.0),
value: DayoffType.Range,
groupValue: _dayoffType,
title: Transform.translate(offset: const Offset(-18, 0), child: Text('Range')),
onChanged: (DayoffType? val) {
setState(() {
_dayoffType = val!;
});
},
),
),
Expanded(...Radio2...),
Expanded(...Radio3...)
)