Flutter: send Data from TabBarView (StatefullWidgets) back to main Scaffold - dart

I want to create an App with Tabs to get the users input. The Problem is, that the different Tabs get different inputs, but i have to collect the inputs for the Database. My idea her was, that the main scaffold collects the inputs from all Tabs and write it in a database! My problem is that I don't know to send data from the tab (statefullWidget in an other file) to the parent class (Scaffold) or run a function from there!
Please help me and sorry for my bad English!
Jonas

You can pass a Function that can be called whenever you want.
Small example
MamaBear class
...
class _MamaBear extends State<MamaBear> {
void hungryBear(String babyBear) {
print("$babyBear is hungry");
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Column(children: <Widget>[
BabyBear(
"Mark",
(babyBear) {
hungryBear(babyBear);
},
)])));}
BabyBear class
class BabyBear extends StatefulWidget {
final String babyBearName;
final Function onBearAction;
BabyBear(this.babyBearName, this.onBearAction);
#override
_BabyBear createState() => _BabyBear();
}
class _BabyBear extends State<BabyBear> {
#override
Widget build(BuildContext context) {
return Card(
child: RaisedButton(
child: Text("Mama I'm hungry"),
onPressed: () {
widget.onBearAction(widget.babyBearName);
}),
);
}
}

Related

Flutter / Dart using Scoped Model with descendants and Navigator

I've been struggling with this one since yesterday - I'm creating new ScopedModel obj in tab controller (ScreenTabs), and then passing it to multiple tabs in BottomNavigationBar (1st level descendants, for example: ScreenBook) by wrapping each tab as ScopedModelDescendant. So far so good - everything's working as expected.
The problem arises, when in one of my tabs, I'd like to navigate to it's child (2nd level descendant, for example: ScreenBookDetails) and use ScopedModel there. I've already tried using ScopedModelDescendant and ScopedModel.of(context) with no luck - getting Error: Could not find the correct ScopedModel everytime.
My code (skipped some for brevity):
TabController
...
class ScreenTabsState extends State<ScreenTabs> {
ModelTabs modelTabs = ModelTabs(); //new Model object
var screens = [new ScreenHome(),new ScreenBook(),];
#override
Widget build(BuildContext context) {
return ScopedModel<ModelTabs>(
model: modelTabs,
child: Scaffold(...)
body: Builder (builder: (context) => Container(child:
screens.elementAt(selectedIndex),)),
bottomNavigationBar: BottomNavigationBar(
currentIndex: selectedIndex,
items: [...],
onTap: onTabTapped,
),)...}
ScreenBook (1st ddescendant - ScpodedModel works as expected)
...
class ScreenBookState extends State<ScreenBook> {
#override
Widget build(BuildContext context) {
return new Scaffold(
body: ScopedModelDescendant<ModelTabs>(builder: (context, child, model) {
print(TAG + "model status:" + model.status.toString());
return model.status == Status.LOADING ? MyCircularProgressIndicator() :
Builder (builder: (context) =>
Card(...
child: InkWell(
onTap: (){
Navigator.of(context).pushNamed("/ScreenBookDetails");},))}}
ScreenBookDetails (1st ddescendant - error)
...
class ScreenBookDetailsState extends State<ScreenBookDetails>{
#override
Widget build(BuildContext context) {
return Scaffold(
body: ScopedModelDescendant<ModelTabs>(builder: (context, child, model) {
print(TAG + "scoped model status: " + model.status.toString()); //Error: Could not find the correct ScopedModel.
return model.status == Status.LOADING ? MyCircularProgressIndicator() : Builder(builder: (context) => Column(...)
...
can somebody point me the right way to fix thiis problem please? I'm new to Dart and Flutter, so there might be something that I did not understood clearly as I'm facing similar problem when creating ScopedModel obj in ScreenSplashScreen then navigating to ScreenLogIn and trying to use it there.
Thank you! :)
Finally found out how to handle ScopedModel including Navigator with help from ScopedModel's Github support - the key was to create a ScopeModel instance outside of build(BuildContext), then pass the instance through the constructor of next screen, like:
class HomePage extends StatelessWidget {
SampleScopedModel model = SampleScopedModel();
#override
Widget build(BuildContext context) {
return ScopedModel<SampleScopedModel>(
model: model,
child: Scaffold(
body: Container(),
floatingActionButton: FloatingActionButton(onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => NextPage(model))
);
}),
),
);
}
}
class NextPage extends StatelessWidget {
final SampleScopedModel model;
NextPage(this.model);
#override
Widget build(BuildContext context) {
return ScopedModel<SampleScopedModel>(
model: model,
child: Scaffold(
body: ScopedModelDescendant<SampleScopedModel>(
builder: (context, child, model) {
return Text(model.payload.toString());
})
),
);
}
}

How does the flutter parent component call a child component?

How does the parent component trigger the methods of the child component? How do I trigger the click method of son in fathor? As shown below:
As mentioned, you need to use a callback. What this means it that you pass a function to your child that it calls when it needs to. You'll be working from the inside up, not top down.
You'll store the function as a member variable and when your child is clicked you'll call that function. The function will be called in your parent, you can pass whatever data you want back to the parent just like calling a normal function.
Here's some pseudo-code you can use to make the adjustment
class son extends StatelessWidget {
Function onClicked;
son({this.onClicked});
Widget build(...) {
return GestureDetector(
child: Container(...),
onTap: onClicked
)
}
}
class father extends StatelessWidget {
Widget build(...) {
return Container(
child:son(onClicked: _clicked)
)
}
void _clicked() {
print('clicked');
}
}
Also, Definitely change your class names to start with a Capital letter.
class Father extends StatelessWidget {
var child;
#override
Widget build(BuildContext context) {
child = son();
return Scaffold(
body: child,
floatingActionButton: FloatingActionButton(
onPressed: () => child?.childFunction(),
),
);
}
}
class Son extends StatelessWidget {
void childFunction() => print('called in child widget');
#override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
);
}
}

Initialize class scope member with method in StatelessWidget

I have found a little issue while learning Flutter and I'm wondering which is the better way to fix it.
Here is a very simple example code of the issue:
class SubWidget extends StatelessWidget {
final void Function() mainOnPressed;
SubWidget(this.mainOnPressed);
#override
Widget build(BuildContext context) => RaisedButton(onPressed: mainOnPressed,);
void actionA() { /* Do A */ };
void actionB() { /* Do B */ };
}
class MainWidget extends StatelessWidget {
final SubWidget _subWidget;
MainWidget() : _subWidget = SubWidget(_onSubPressed);
Widget _buildChildA() => RaisedButton(onPressed: _subWidget.actionA,);
Widget _buildChildB() => RaisedButton(onPressed: _subWidget.actionB,);
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
_buildChildA(),
_buildChildB(),
_subWidget,
]
);
}
void _onSubPressed() { /* Do something */ }
}
The above code has an error, because I'm passing the _onSubPressed method as argument in the MainWidget constructor and it cannot be done because its initialization isn't complete.
I also can't move the initialization of _subWidget outside the constructor because it would give me an error because it's final and I can't remove the final because I'd get a warning for having a non-final member in an immutable class.
For the same reason, I can't defer the initialization of mainOnPressed in the SubWidget class.
I thought about moving the _subWidget member inside the build() method and pass it to the _buildChildX() methods, but while it is quite simple in this example, it would be more annoying having to do it with multiple members or methods that have the same issue.
Another solution I found is to move the _subWidget member and the two _buildChildX() inside the build() method like in the following code:
class MainWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
final SubWidget _subWidget = SubWidget(_onSubPressed);
Widget _buildChildA() => RaisedButton(onPressed: _subWidget.actionA,);
Widget _buildChildB() => RaisedButton(onPressed: _subWidget.actionB,);
return Column(
children: <Widget>[
_buildChildA(),
_buildChildB(),
_subWidget,
]
);
}
void _onSubPressed() { /* Do something */ }
}
While it works as expected, I am a little worried about the readability of the code with longer and more complex methods nested inside the build method.
Which is the best way to solve this issue?
The reason why you're getting the error "This expression has a type of 'void' so its value can't be used." on MainWidget() : _subWidget = SubWidget(_onSubPressed()); is because _onSubPressed() may contain an unexpected expression. This is explained on the link provided in the error message: https://dart.dev/tools/diagnostic-messages#use_of_void_result
To solve this issue, you can move the function's contents to the constructor to initialize _subWidget.
final SubWidget _subWidget;
MainWidget() : _subWidget = SubWidget((){
// Do something
});
Here's a complete sample that you can try out.
import 'package:flutter/material.dart';
void main() {
runApp(MainWidget());
}
class SubWidget extends StatelessWidget {
final void Function() mainOnPressed;
SubWidget(this.mainOnPressed);
#override
Widget build(BuildContext context) => ElevatedButton(
child: Text('SubWidget'),
onPressed: mainOnPressed,
);
void actionA() {
/* Do A */
debugPrint('Child A');
}
void actionB() {
/* Do B */
debugPrint('Child B');
}
}
class MainWidget extends StatelessWidget {
final SubWidget _subWidget;
MainWidget() : _subWidget = SubWidget((){
debugPrint('SubWidget');
});
Widget _buildChildA() => ElevatedButton(
child: Text('Child A'),
onPressed: _subWidget.actionA,
);
Widget _buildChildB() => ElevatedButton(
child: Text('Child B'),
onPressed: _subWidget.actionB,
);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Column(children: <Widget>[
_buildChildA(),
_buildChildB(),
_subWidget,
]),
),
),
);
}
}

Flutter list view rendering issue

I'm currently working through learning Flutter. I'm starting with trying to build the basic list app. The current flow I have is my Stateful TasksManager Widget saving user input to the state, and then pushing it to the _taskList List, which I have being sent over to a Stateless TaskList widget, which is rendering the list.
I expect the list view to be updated with the new task after the "Save" button is clicked, but what I'm getting is after I "Save", the list view only updates during the subsequent change of state. For example, if I were to "Save" the string "Foo" to add to the list, I'm only seeing that update in the view after I go to type in another item, such as "Bar".
TaskManager
class TasksManager extends StatefulWidget{
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return TasksManagerState();
}
}
class TasksManagerState extends State<TasksManager>{
final List _taskList = [];
String _newTask = '';
#override
Widget build(BuildContext context) {
// TODO: implement build
return Column(
children: <Widget>[
TextField(
onChanged: (value){
setState(() {
_newTask = value;
});
print(_taskList);
},
),
RaisedButton(
child: Text('Save'),
onPressed: () => _taskList.add(_newTask),
),
Expanded(child: TaskList(_taskList))
],
);
}
}
TaskList
class TaskList extends StatelessWidget {
final List taskList;
TaskList(this.taskList);
Widget _buildListItem(BuildContext context, int index) {
return ListTile(
title: Text(taskList[index]),
);
}
#override
Widget build(BuildContext context) {
// TODO: implement build
return ListView.builder(
itemBuilder: _buildListItem,
itemCount: taskList.length,
);
}
}
Yes, Flutter only updates when it is instructed to.
setState() marks the widget dirty and causes Flutter to rebuild:
onPressed: () => setState(() => _taskList.add(_newTask)),

How to force Flutter to rebuild / redraw all widgets?

Is there a way to force Flutter to redraw all widgets (e.g. after locale change)?
Your Widget should have a setState() method, everytime this method is called, the widget is redrawn.
Documentation : Widget setState()
Old question, but here is the solution:
In your build method, call the rebuildAllChildren function and pass it the context:
#override
Widget build(BuildContext context) {
rebuildAllChildren(context);
return ...
}
void rebuildAllChildren(BuildContext context) {
void rebuild(Element el) {
el.markNeedsBuild();
el.visitChildren(rebuild);
}
(context as Element).visitChildren(rebuild);
}
This will visit all children and mark them as needing to rebuild.
If you put this code in the topmost widget in your widgets tree, it will rebuild everything.
Also note you must order that specific widget to rebuild. Also you could have some boolean so that the rebuild of that widget only rebuilds all of its children when you really need it (it's an expensive operation, of course).
IMPORTANT: This is a hack, and you should only do this if you know what you are doing, and have strong reason to do so. One example where this is necessary is in my internationalization package: i18_extension. As Collin Jackson explained in his answer, you are really not supposed to do this in general.
This type of use case, where you have data that children can read but you don't want to explicitly pass the data to the constructor arguments of all your children, usually calls for an InheritedWidget. Flutter will automatically track which widgets depend on the data and rebuild the parts of your tree that have changed. There is a LocaleQuery widget that is designed to handle locale changes, and you can see how it's used in the Stocks example app.
Briefly, here's what Stocks is doing:
Put a callback on root widget (in this case, StocksApp) for handling locale changes. This callback does some work and then returns a customized instance of LocaleQueryData
Register this callback as the onLocaleChanged argument to the MaterialApp constructor
Child widgets that need locale information use LocaleQuery.of(context).
When the locale changes, Flutter only redraws widgets that have dependencies on the locale data.
If you want to track something other than locale changes, you can make your own class that extends InheritedWidget, and include it in the hierarchy near the root of your app. Its parent should be a StatefulWidget with key set to a GlobalKey that accessible to the children. The State of the StatefulWidget should own the data you want to distribute and expose methods for changing it that call setState. If child widgets want change the State's data, they can use the global key to get a pointer to the State (key.currentState) and call methods on it. If they want to read the data, they can call the static of(context) method of your subclass of InheritedWidget and that will tell Flutter that these widgets need to rebuilt whenever your State calls setState.
Refreshing the whole widget tree might be expensive and when you do it in front of the users eyes that wouldn't seem sweet.
so for this purpose flutter has ValueListenableBuilder<T> class. It allows you to rebuild only some of the widgets necessary for your purpose and skip the expensive widgets.
you can see the documents here ValueListenableBuilder flutter docs
or just the sample code below:
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:'),
ValueListenableBuilder(
builder: (BuildContext context, int value, Widget child) {
// This builder will only get called when the _counter
// is updated.
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Text('$value'),
child,
],
);
},
valueListenable: _counter,
// The child parameter is most helpful if the child is
// expensive to build and does not depend on the value from
// the notifier.
child: goodJob,
)
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.plus_one),
onPressed: () => _counter.value += 1,
),
);
And also never forget the power of setState(() {});
I explain how to create a custom 'AppBuilder' widget in this post.
https://hillelcoren.com/2018/08/15/flutter-how-to-rebuild-the-entire-app-to-change-the-theme-or-locale/
You can use the widget by wrapping your MaterialApp with it, for example:
Widget build(BuildContext context) {
return AppBuilder(builder: (context) {
return MaterialApp(
...
);
});
}
You can tell the app to rebuild using:
AppBuilder.of(context).rebuild();
Simply Use:
Navigator.popAndPushNamed(context,'/screenname');
Whenever you need to refresh :)
What might work for your use case is using the Navigator to reload the page. I do this when switching between "real" and "demo" mode in my app. Here's an example :
Navigator.of(context).push(
new MaterialPageRoute(
builder: (BuildContext context){
return new SplashPage();
}
)
);
You can replace "new SplashPage()" in the above code with whatever main widget (or screen) you would like to reload. This code can be called from anywhere you have access to a BuildContext (which is most places in the UI).
Just use a Key on one of your high-level widgets, everything below this will lose state:
Key _refreshKey = UniqueKey();
void _handleLocalChanged() => setState((){
_refreshKey = UniqueKey()
});
Widget build(BuildContext context){
return MaterialApp(
key: _refreshKey ,
...
)
}
You could also use a value key like:
return MaterialApp(
key: ValueKey(locale.name)
...
);
Why not just have Flutter.redrawAllWidgetsBecauseISaidSo();? –
TimSim
There kinda is:
Change to key to redraw statefull child widgets.
Jelena Lecic explained it good enough for me on medium.
import 'package:flutter/material.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: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
var _forceRedraw; // generate the key from this
void _incrementCounter() {
setState(() {
_counter++;
_forceRedraw = Object();
});
}
#override
void initState() {
_forceRedraw = Object();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
MyStatefullTextWidget(
key: ValueKey(_forceRedraw),
counter: _counter,
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class MyStatefullTextWidget extends StatefulWidget {
final int counter;
const MyStatefullTextWidget({
required this.counter,
Key? key,
}) : super(key: key);
#override
_MyStatefullTextWidgetState createState() => _MyStatefullTextWidgetState();
}
class _MyStatefullTextWidgetState extends State<MyStatefullTextWidget> {
#override
Widget build(BuildContext context) {
return Text(
'You have pushed the button this many times:${widget.counter}',
);
}
}
Simply Use:
Navigator.popAndPushNamed(context,'/xxx');
I my case it was enough to reconstruct the item.
Changed:
return child;
}).toList(),
To:
return SetupItemTypeButton(
type: child.type,
icon: child.icon,
active: _selected[i] == true,
onTap: ...,
);
}).toList(),
class SetupItemTypeButton extends StatelessWidget {
final dynamic type;
final String icon;
estureTapCallback onTap;
SetupItemTypeButton({Key? key, required this.type, required this.icon, required this.onTap}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container();
}
}
class SetupItemsGroup extends StatefulWidget {
final List<SetupItemTypeButton> children;
final Function(int index)? onSelect;
SetupItemsGroup({required this.children, this.onSelect});
#override
State<SetupItemsGroup> createState() => _SetupItemsGroupState();
}
class _SetupItemsGroupState extends State<SetupItemsGroup> {
final Map<int, bool> _selected = {};
#override
Widget build(BuildContext context) {
int index = 0;
return Container(
child: GridView.count(
children: widget.children.map((child) {
return SetupItemTypeButton(
type: child.type,
icon: child.icon,
active: _selected[i] == true,
onTap: () {
if (widget.onSelect != null) {
int i = index++;
child.active = _selected[i] == true;
setState(() {
_selected[i] = _selected[i] != true;
child.onTap();
widget.onSelect!(i);
});
}
},
);
}).toList(),
),
);
}
}

Resources