I am creating a form (not using the Form Widget) in Flutter where the user can add an arbitrary amount of items (treatments) which are rendered as InputChip widgets list in a Wrap widget.
The form uses a button (AddButton widget) which opens a form dialog which itself returns the newly created item (treatment) that is added to selectedItems:
class TreatmentsWidget extends StatefulWidget {
const TreatmentsWidget({super.key, required this.selectedItems});
final List<Treatment> selectedItems;
#override
State<TreatmentsWidget> createState() => _TreatmentsWidgetState();
}
class _TreatmentsWidgetState extends State<TreatmentsWidget> {
#override
Widget build(BuildContext context) {
var chips = widget.selectedItems.map(
(item) {
return InputChip(
label: Text('${item.name} - ${item.frequency}/${item.frequencyUnit.name})',
);
},
).toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
children: chips,
),
AddButton(onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return const TreatmentDialog();
}).then((value) {
if (value != null) {
Treatment item = value;
setState(() {
widget.selectedItems.add(item);
});
}
});
}),
],
);
}
}
For some reason, when a new item is added to selectedItem and that the item overflows the current line, the layout is not recomputed such that the Wrap widget overflows the button:
However, as soon as the user scroll (the whole screen content is inside a SingleChildScrollView), the layout is recomputed and the Wrap takes the right amount of space:
How can I force a redraw when a new item is added to prevent this glitch?
The issue seems to be that the Column does not recompute its size on the current frame when one of his child size changes.
I ended up forcing rebuilding the Column using a ValueKey whenever chips.length changes:
class _TreatmentsWidgetState extends State<TreatmentsWidget> {
#override
Widget build(BuildContext context) {
var chips = ...;
return Column(
key: ValueKey(chips.length),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
children: chips,
),
AddButton(...),
],
);
}
}
I am still interested in a cleaner solution if it exists. Using a provider seems overkill for this case.
Related
Summing up the situation, I'm making a simple App in Flutter, which displays a List of Items you've added (I won't detail the app, as it would be unnecessary).
The file I created (log.dart) has a Property Class
class LogItem { /* code and stuff inside */ }
And it has a List with items
List<LogItem> itemsList = [test01, test02];
I created a simple Widget to display data for each item in this List
class SpecificItem extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Row(
children: [
Text(
" R\$ ${itensList[i].price}",
),
Spacer(),
Text(
"${itensList[i].title}",
),
],
),
);
}
}
Just below in another widget, I created a for loop to make a variable "i" change, to display different items from this list.
class LogGraphical extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: ListView(
children: [
for (int item = 0; item < itensList.length; item++) SpecificItem(item),
],
),
);
}
}
Can someone explain to me exactly how I do the Widget ACCEPT PARAMETERS and change it? In this way creating multiple items?
I tried in many ways, but I could never get it to work!
(In the code I inserted here, I didn't put the Widget accepting anything.)
I have quite the problem wrapping my head around the whole state management of GetX so naturally I'm facing some issues.
I'm getting a collection from firebase which I put into a listview populated with Card widgets (CustomCard()), each document into a Card widget. In this Card widget I have a boolean that controls whether the card should be expanded (simply by Adding a Row()) or not if Card is tapped. The issue I'm facing is that if use GetX for this boolean, all the cards will trigger and not each individual card. In a way this seems logical because I only have one controller that manages this boolean.
So to clarify, bool isCardExpanded seem to be global for all Card widgets meaning that if I tap one card, ALL cards will expand, which is not what I want. I need them to act individually.
Do I need one separate controller for every Card in the list view or is this solvable in another way?
Controller
class Controller extends GetxController {
RxBool isCardExpanded = false.obs;
void changeExpanded() {
isCardExpanded.value = !isCardExpanded.value;
update();
}
}
ListView:
class CustomScreen extends State<CustomScreen>
implements ItemScreenInterface {
Controller ctrl = Get.find();
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseFirestore.instance
.collection(someCol)
.doc(SomeDoc)
.collection(anotherCol).snapshots(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
return Column(
children: [
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.docs.length,
itemBuilder: (_, i) {
return CustomCard( // <------------ Card widget
document: snapshot.data.docs[i]);
}),
)
],
);
},
);
}
Card class
class CustomCard extends StatefulWidget {
CustomCard({required this.doc});
var doc;
#override
_CustomCardState createState() => _CustomCardState();
}
class _CustomCardState extends State<CustomCard> {
Controller ctrl = Get.find();
RxBool _isCardExpanded = false.obs;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Card(
child: GestureDetector(
onTap: () {
ctrl.changeExpanded(); // <-- change bool
},
child: Padding(
padding: EdgeInsets.all(2),
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Text('Top Part')
]),
if (_isCardExpanded.value) Divider(thickness: 2),
if (_isCardExpanded.value) // Controlled by _isCardExpanded. Add Row if true
Row(
children: <Widget>[
Text('EXPANDED'),
],
),
],
),
),
),
);
}
}
Desired outcome:
Actual outcome:
Because your controller instance initializes only once.
To solve your problem, you need to make a List that has the status of the cards in the Controller.
Or don't use the State management tool, but separately place the _isExpanded value in the _CustomCardState class. And use the setState() function.
EDIT
There is another way by using the tag argument when creating the controller like Get.create(someController, tag: TAG_NAME);
I have a simple flutter app. It has a stateful widget in the Home Page with a simple Scaffold which only has a Column. One of the children in the column is a custom widget called buildBodyLayout(). This widget has its own column with some Text widgets, and another custom widget called buildButton(). This new widget has a button which needs to setState of a variable in the Home view. I pass the value of the variable when calling the widget. But each widget is in its own dart file since I am re-using the same widget in other pages.
How do I setState the main stateful widget from inside custom widgets?
If I write everything inside the same page, it all works fine. How do I use a widget in a different dart file to set the sate of a parent widget?
Sample Code
Home Stateful Widget
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int changeValue;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Text("Welcome to my App"),
Text("The Change Value is: $changeValue"),
buildBodyLayout(changeValue),
],
),
);
}
}
buildBodyLayouot Widget
Widget buildBodyLayout(int value){
return Column(
children: [
Text("Press the + and - Buttons to change Value"),
buildButtons(value),
],
);
buildButtons Widget
Widget buildButtons(int value){
return Column(
children: [
RaisedButton(
child: Text("Increase Value"),
onPressed: (){
value = value + 1; //THIS SHOULD SET STATE
}) ,
RaisedButton(
child: Text("Decrease Value"),
onPressed: (){
value = value - 1; //THIS SHOULD SET STATE
})
],
);
}
}
Thank you for any help!
Widgets in flutter are classes that extend the widget(StatefulWidget, StatelessWidget) class.
In your code your using functions to build your widgets, which is not recommended.
You can see the difference between class widgets and function widgets here:
What is the difference between functions and classes to create reusable widgets?
Aside from that, using function or classes, to solve your problem you need to use a callback.
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int changeValue = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Text("Welcome to my App"),
Text("The Change Value is: $changeValue"),
buildBodyLayout(changeValue,
addToValue: (int increment){
setState((){
changeValue += increment;
});
}
),
],
),
);
}
}
Widget buildBodyLayout(int value, Function(int newValue) addToValue){
return Column(
children: [
Text("Press the + and - Buttons to change Value"),
buildButtons(value, addToValue),
],
);
}
Widget buildButtons(int value, Function(int newValue) addToValue){
return Column(
children: [
RaisedButton(
child: Text("Increase Value"),
onPressed: (){
addToValue(1);
}),
RaisedButton(
child: Text("Decrease Value"),
onPressed: (){
addToValue(-1);
})
],
);
}
You also don't need to put your widgets in different files to reuse them, but it's recommended that you do that.
How to control specific widget to hide or to show in pageview. In PageView Widget in flutter for specific page only, how we can control over widget to be hidden or shown based on bool value. how we can utilize pageview controller to do this
you can change the children in the widget tree depending on a boolean value.
Example: child: condition ? WidgetWhenTrue : WidgetWhenFalse
UPDATE
The best way I found is, that you create your pages and page children dynamicly.
You could provide a List<Widget> which will represent the max. Content and then remove the widget you don't want to have.
Or you could add the widgets on the fly to the pages.
import 'package:flutter/material.dart';
class PageViewWidget extends StatefulWidget {
#override
_PageViewWidgetState createState() => _PageViewWidgetState();
}
class _PageViewWidgetState extends State<PageViewWidget> {
PageController _pageController;
#override
void dispose() {
_pageController.dispose();
super.dispose();
}
List<Text> maxContent = [
Text('text 1'),
Text('text 2'),
Text('text 3'),
Text('text 4'),
Text('text 5'),
Text('text 6')
];
bool condition = true;
Container dynamicPageChildren({Color color, List<int> delPos}) {
Container newPage;
List<Widget> newContent = List.from(maxContent);
// modify your Widget List
print('length = ${maxContent.length} ');
for (int i in delPos.reversed) {
// use reversed or provide the last elemt to remove first if not,
// your list will shrink and the element you want to remove last does not exist or is the wrong one
print('delete at pos $i');
newContent.removeAt(i);
}
// add it to the page
newPage = Container(
color: color,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: newContent));
return newPage;
}
List<Container> dynamicPages({List<Color> colorList}) {
// you could also pass the index positions into this function
// or call your logic to decide which index should not be displayed
List<Container> newPageList = [];
// modify your Widget List
int i = 0;
for (Color color in colorList) {
// example with given indices
// newPageList.add(dynamicPageChildren(color: color, delPos: [1, 3, 5]));
newPageList.add(dynamicPageChildren(color: color, delPos: [i]));
i = i + 1;
}
return newPageList;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: PageView(
controller: _pageController,
children: dynamicPages(
colorList: [Colors.red, Colors.orange, Colors.yellow])),
),
);
}
}
I started Flutter recently and my app required bottom navigation. I have created bottom navigation and manage to access the child widget based on the tab selected.
Under the child widget there is drop down selection where I can change the bottom navigation text in one of the tabs for different selections.
I have tried a few days but still could not figure out how the child widget can change the text.
I have tried callback but cannot get it work. I have tried navigation.push - material page route but it rebuild the whole widget and my selection gone. I have also tried to use GlobalKey or Sharedpreference to capture my selection so that when it rebuild, it will use back the stored selection but I couldn't get it work.
I only wish to change the bottom navigation text in one of the text from child widget drop down selection.
Which is the best method to achieve this?
I would recommend you try to use the bloc pattern with a StreamBuilder. I have an example below. Regardless, in the example there is a stateful widget, a bloc, and a data class. Try to understand this code and modify it to your needs.
import 'package:flutter/material.dart';
import 'dart:async';
class StreamScaffold extends StatefulWidget {
#override
_StreamScaffoldState createState() => _StreamScaffoldState();
}
class _StreamScaffoldState extends State<StreamScaffold> {
ScaffoldDataBloc bloc;
#override
void initState() {
super.initState();
bloc = ScaffoldDataBloc();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<ScaffoldDataState>(
stream: bloc.stream, // The stream we want to listen to.
initialData: bloc.initial(), // The initial data the stream provides.
builder: (context, snapshot) {
ScaffoldDataState state = snapshot.data;
Widget page;
if (state.index == 0) {
// TODO separate this into its own widget, this is messy.
page = Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () => bloc.updateText(state,"Sales"),
child: Text("Set text to Sales")
),
RaisedButton(
onPressed: () => bloc.updateText(state, "Purchases"),
child: Text("Set text to Purchases"),
)
]),
);
}
if (state.index == 1) {
// TODO separate this into its own widget, this is messy.
page = Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () => bloc.updateText(state, "Stock"),
child: Text("Set text to Stock"),
),
RaisedButton(
onPressed: () => bloc.updateText(state, "Budget"),
child: Text("Set text to Budget"),
)
]));
}
return Scaffold(
body: page,
bottomNavigationBar: BottomNavigationBar(
currentIndex: state.index,
onTap: (int) => bloc.updateIndex(state, int),
items: [
BottomNavigationBarItem(
icon: Icon(Icons.play_arrow),
// Obtain the text from the state
title: Text(state.variableText)),
BottomNavigationBarItem(
icon: Icon(Icons.play_arrow), title: Text("Test")),
]),
);
});
}
#override
void dispose() {
super.dispose();
bloc.dispose();
}
}
// A data class to hold the required data.
class ScaffoldDataState {
int index;
String variableText;
ScaffoldDataState({this.index = 0, this.variableText = "Hello"});
}
// A bloc to handle updates of the state.
class ScaffoldDataBloc {
StreamController<ScaffoldDataState> scaffoldDataStateController = StreamController<ScaffoldDataState>();
Sink get updateScaffoldDataState => scaffoldDataStateController.sink;
Stream<ScaffoldDataState> get stream => scaffoldDataStateController.stream;
ScaffoldDataBloc();
ScaffoldDataState initial() {
return ScaffoldDataState();
}
void dispose() {
scaffoldDataStateController.close();
}
// Needs to be called every time a change should happen in the UI
// Add updated states into the Sink to get the Stream to update.
void _update(ScaffoldDataState state) {
updateScaffoldDataState.add(state);
}
// Specific methods for updating the different fields in the state object
void updateText(ScaffoldDataState state, String text) {
state.variableText = text;
_update(state);
}
void updateIndex(ScaffoldDataState state, int index) {
state.index = index;
_update(state);
}
}
Hope it helps!
Additional Questions from comment:
The easiest solution would be to simply pass the bloc as a parameter to the widget. Create a new dart file in your project, create a StatelessWidget there, create the code for the page in the build method. Note: it would make sense for you to separate the bloc into its own file along with the data class.
import 'package:flutter/material.dart';
// Import the file where the bloc and data class is located
// You have to have a similar import in the parent widget.
// Your dart files should be located in the lib folder, hit ctrl+space for
// suggestions while writing an import, or alt+enter on a unimported class.
import 'package:playground/scaffold_in_stream_builder.dart';
class ChildPage extends StatelessWidget {
final ScaffoldDataBloc bloc;
final ScaffoldDataState state;
const ChildPage({Key key, this.bloc, this.state}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(); // TODO replace with your page
}
}
However, if the these child widgets get their own children in separate files it would be better to use a InheritedWidget instead, with the bloc and state. This avoids "passing state down". See this article on inherited widgets