Flutter TabBarView build all tab widgets once at startup - flutter

I'm currently trying to get a multi tabbed form working. I am using the package flutter_form_builder for that.
The problem is, that the FormBuilder requires all descendant form fields to be loaded at least once to run validations and to provide the correct value of the field. When the user visits every tab once the thing works fine, but I cannot assume that this will always happen.
The simplest solution in my eyes would be, to somehow get the TabBarView to render all tab pages at startup, so that every tab-widget and the form-fields exist.
I have the main page where I define the form builder and the tabview inside of it. The important part looks like this:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: 'title',
bottom: TabBar(
controller: tabController,
tabs: <Widget>[
Tab(icon: Icon(Icons.title)),
Tab(icon: Icon(Icons.image)),
Tab(icon: Icon(Icons.category)),
],
),
),
body: FormBuilder(
key: formBuilderKey,
autovalidate: true,
child: TabBarView(
controller: tabController,
children: <Widget>[
MainTab(),
CoverTab(),
GroupsTab()
],
)
)
);
}
One tab page looks more or less like this:
class GroupsTab extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return GroupsTabState();
}
}
class GroupsTabState extends State<GroupsTab> with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: EdgeInsets.all(10.0),
child: Column(
children: <Widget>[
FormBuilderTextField(
attribute: "name",
decoration: InputDecoration(
labelText: "Name",
icon: Icon(Icons.title),
),
validators: [FormBuilderValidators.required()],
),
// Page content with form fields
],
),
);
}
#override
bool get wantKeepAlive => true;
}
I'm already using the AutomaticKeepAliveClientMixin to only call the build method of every tab page once but I need a bit more.
So my question: Is it possible to get the TabBarView to render all the widgets at startup or do you have an idea for a different solution.
Thank you all guys in advance

Related

Flutter tabbar/tabview is not displaying the correct view after navigating to/from the page

I am having some issue getting the tabbar/TabBarView to work.
When I open the app and access the page containing the tab view , it works as designed. Meaning , clicking on each tab display the correct view content . However, if I select tab 2 and then go to another page in the app and then come back to this page with the tabs I see tab 1 selected but the contents shown is from tab 2. It seems the tabview is not getting updated.
What I want is every time I access the page it should display the first tab and content
Here is a snippet of my code. Please have a look and let me know how I can get it to work..please
class MySettingPage extends StatefulWidget {
#override
_MySettingPageState createState() => _MySettingPageState();
class _MySettingPageState extends State<MySettingPage>
with SingleTickerProviderStateMixin {
TabController tabController;
#override
void initState() {
super.initState();
tabController = TabController(length: 2, vsync: this);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: const RappBar("my appbar"),
body: if (auth.isLoggedIn) ...
[buildTabUI(myRModel.empId)]
)
}
Widget buildTabUI(String EmpID) {
return Container(
height: MediaQuery.of(context).size.height,
child: Column(
children: [
Container(
width: MediaQuery.of(context).size.height,
decoration: ...
child: Column(
children: [
Padding(
padding: EdgeInsets.all(1),
child: TabBar(
controller: tabController,
tabs: [
Tab(
text: 'self service',
),
Tab(
text: 'settings',
),
],
),
),
],
),
),
Expanded(
child: TabBarView(
controller: tabController,
children: [
Container(
child: Padding(
padding: const EdgeInsets.only(top: 10.0),
child: buildSelfServiceItems(),
),
),
Container(
child: Padding(
padding: const EdgeInsets.only(top: 10.0),
child: buildGeneralList(context),
)),
],
),
)
],
),
);
}
}
Image 1 & 2 are the original tab. Navigating back and forth within the tab work.
Image 3: I selected tab 2 and navigate away form the page, when I navigate back to the page with the tab this is shown.
As you can see , the correct tab is selected but the view is for tab 2. I would like tab 1 to be shown with tab 1 content
I'm not sure if there is a way to save the index of the tab after you navigate to a different screen. The way I would do it though is as follows:
Create int variable:
int tabIndex = 0;
In your TabController initState you can add the attribute initialIndex
#override
void initState() {
super.initState();
tabController = TabController(length: 2, vsync: this, initialIndex: tabIndex);
}
So when you navigate to next page, you add .then to retrieve the value when returning from another page.
You can then pass the tabIndex value when you return from the other page:
Navigating away:
Navigator.pushNamed(context,'example').then((value){
setState((){
tabIndex = value;
});
});
Navigating back:
Navigator.pop(context, 1);

Keyboard is still showing when changing the to other Tab in a TabBarView

In one tab I have a TextFormField and in the other only a list of texts. When I select the Text field the keyboard is open, then I jump to the second Tab and the keyboard is still displayed.
I can even write and when I go back to the Tab 1 I see why I typed.
Do you know how can I give an action to the second Tab in order to take the focus out from the text field?
DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text('Manage Products'),
bottom: TabBar(tabs: <Widget>[
Tab(icon: Icon(Icons.create), text: 'Create Product'),
Tab(icon: Icon(Icons.list), text: 'My Products'),
]),
),
body: TabBarView(
children: <Widget>[
ProductEditPage(addProduct: addProduct),
ProductListPage(products, updateProduct),
],
)),
);
Tab1
Tab2
SOLVING CODE
After applying #nick.tdr suggestion an example code can be as follow:
class _Test extends State<Test> with TickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: 2);
_tabController.addListener(() {
if (_tabController.indexIsChanging) {
FocusScope.of(context).requestFocus(new FocusNode());
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('2 Tabs'),
bottom: TabBar(controller: _tabController, tabs: <Widget>[
Tab(text: 'Tab with Text Field'),
Tab(text: 'Empty Tab'),
]),
),
body: TabBarView(
controller: _tabController,
children: <Widget>[
Container(
child: TextFormField(
decoration: InputDecoration(labelText: 'Title'),
),
),
Container()
],
),
);
}
}
You can add gesture detector to you scaffold and remove the focus. But that won't work for the tabs. For the tabs you need to do the following:
controller.addListener((){
if(controller.indexIsChanging)
{
FocusScope.of(context).detach();
}
});
Where controller is your tab controller. Hope that helps
I improved #nick.tdr 's answer.
For the tabs you need to do the following;
controller.addListener((){
if(controller.indexIsChanging)
{
FocusScope.of(context).unfocus();
}
});
If you want to work this when swiping between tabs instead of clicking tab buttons, try the following;
controller.addListener((){
FocusScope.of(context).unfocus();
});
Where the controller is your tab controller.
For me I found the best way is to request a new FocusNode on tabChange listener that has been set in didChangeDependencies() callback:
in build() method:
TabBar(
controller: tabController,
.
.
.
),
didChangeDependencies callback:
#override
void didChangeDependencies() {
setState(() {
tabController.addListener(handleTabChange);
});
super.didChangeDependencies();
}
The listener implementation:
handleTabChange() {
// do whatever handling required first
setState(() {
FocusScope.of(context).requestFocus(new FocusNode());
});
}
I think wrapping up your whole Scaffold body into a GestureDetector should solve your problem.
new Scaffold(
body: new GestureDetector(
onTap: () {
// call this method here to hide keyboard
FocusScope.of(context).requestFocus(new FocusNode());
},
child: new Container(
/*Remaining code goes here*/
)
)
This simply gains focus on the widget you tapped on removing focus from previous one.

Why does Sliver App Bar's flexibleSpace parameter have to take a Widget with a const constructor?

I am trying to build a custom scroll view which contains a sliver app bar to achieve something similar to what's shown here:
https://medium.com/#diegoveloper/flutter-collapsing-toolbar-sliver-app-bar-14b858e87abe
However I want to have the word Search and below it I want 3 IconButtons Evenly spaced, when the page (CustomScrollView) is scrolled, I want the 3 IconButtons to be pinned to the top of the SliverAppBar and the Search Text to disappear...
I Tried to achieve the above via the following code:
class SearchPage extends StatelessWidget {
const SearchPage();
#override
Widget build(BuildContext context) {
return CustomScrollView(slivers: <Widget>[
const SliverAppBar(
pinned: true,
expandedHeight: 250.0,
flexibleSpace: _buildSliverAppBarFlex(),
);
}
Widget _buildSliverAppBarFlex() {
return Container(
child: Column(
children: <Widget>[
Text("Search", style: TextStyle(fontSize: 24.0,
color: Colors.white,
fontWeight: FontWeight.bold)),
Row(children: <Widget>[
IconButton(icon: Icon(Icons.flight)),
IconButton(icon: Icon(Icons.hotel)),
IconButton(icon: Icon(Icons.drive_eta))
])
],
)
);
}
}
However I get a warning that flexibleSpace must take a const constructor widget and that the _buildSilverAppBarFlex Widget I've made is not - I cannot add const or final to it either... Any ideas how I can achieve what I want?
The warning comes because you are using const ahead of your SliverAppBar, remove this, and warning will be gone.
So, instead of this
const SliverAppBar(...)
Use this.
SliverAppBar(...)
If you want to use const there, make sure your FlexibleSpaceBar is also a const then.

How to make tabs scrollable in flutter

I've been developing android apps for a while now and I'm currently porting an android app to flutter which I started not long ago. I have been able to make tabs scrollable in android but I'm finding it difficult to do that in flutter. I am able to create the tabs but they are about 7 and they exceed the screen width. Hence I want to make it scrollable to avoid that.
Below is what I did in the android app and I want to achieve something similar. Any help would be appreciated. Thanks.
You can use a DefaultTabController widget , also the Tab widget has a property called : isScrollable , set true and you will see.
This is a basic sample:
final List<Tab> myTabs = List.generate(
10,
(index) => Tab(text: 'TAB $index'),
);
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: myTabs.length,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
isScrollable: true,
tabs: myTabs,
),
),
body: TabBarView(
children: myTabs.map((Tab tab) {
return Center(child: Text(tab.text));
}).toList(),
),
),
);
}
You can find more info here: https://docs.flutter.io/flutter/material/DefaultTabController-class.html

Flutter Dynamic maxHeight

I build a scrolling textfield like proposed in https://github.com/flutter/flutter/issues/9365. Now I want the maxHeight of the ConstrainedBox change dynamically, according to the shown or not shown keyboard. Is there a way to accomplish this?
Widget _buildTextInput() {
return new Container(
padding: new EdgeInsets.all(7.0),
child: new ConstrainedBox(
constraints: new BoxConstraints(
maxHeight: 150.0 <-- This should be dynamic
),
child: new SingleChildScrollView(
scrollDirection: Axis.vertical,
reverse: true,
// here's the actual text box
child: new TextField(
keyboardType: TextInputType.multiline,
maxLines: null, //grow automatically
decoration: new InputDecoration.collapsed(
hintText: 'Please enter a lot of text',
),
),
),
),
);
}
The red box should be the constrained box with open keyboard.
And like so with a closed keyboard.
EDIT:
I'm trying to build an input field that's kind of like posting on Twitter. I need to combine a CircleAvatar, a TextField and a GridView to display the user's avatar, his post and a few images. Like on Twitter, I want the whole thing to scroll, not only the TextField - both while typing and while reviewing what the user typed or uploaded. Besides, the (multiline) TextField should scroll while typing in the visible area (keeping the open or closed keyboard in mind), so the user can see what he's typing.
Even though the Flutter TextField autoscrolls now, I can't get this whole cluster working. Any idea?
Autoscroll has been supported natively in the textfield widget since I believe the beginning of June (2018) - I think this is the commit that added it. You may need to update to the most recent flutter build for it to work, but that is included in version 5.5.
This simplifies matters a bit - this should be all you need to do to get it to work as you want:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => new MyAppState();
}
class MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("Example"),
),
body: new Container(
padding: new EdgeInsets.all(7.0),
child: new TextField(
keyboardType: TextInputType.multiline,
maxLines: null,
decoration: new InputDecoration.collapsed(
hintText: 'Please enter a lot of text',
),
),
),
),
);
}
}
EDIT: To answer the OP's edited question - wanting to have other elements within the same scrolling pane as the textview, I've made this:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => new MyAppState();
}
class MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("Example"),
),
body: new SingleChildScrollView(
child: new Container(
padding: new EdgeInsets.all(7.0),
child: new Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
new CircleAvatar(
backgroundColor: Colors.blue,
child: new Text("AB"),
),
new Expanded(
child: new Column(
children: [
new TextField(
keyboardType: TextInputType.multiline,
maxLines: null,
decoration: new InputDecoration.collapsed(
hintText: 'Please enter a lot of text',
),
),
new Container(
height: 300.0,
width: 100.0,
color: Colors.green,
),
],
),
),
],
),
),
),
),
);
}
}
By using a SingleChildScrollView, we're still allowing the children to set the size of the viewport (as opposed to a MultiChildLayoutDelegate etc which has to have that size set). The textview grows as big as it needs to be, but doesn't scroll itself as its height is not constrained. The Expanded is needed within the row to make sure that the right side (with the text & pictures) is as large as it can be horizontally.