Force TabController to create all tabs

273 views
Skip to first unread message

Julio Henrique Bitencourt

unread,
Sep 5, 2018, 3:24:15 PM9/5/18
to Flutter Dev

I have a default page with two tabs, using TabBar and TabController. All is working fine, except when I enter the page, the second tab's build method is only called when I click to change to the second tab. The problem is that when I enter the page I want both tabs to be already created (a.k.a their build methods executed). Any thoughts?

Illustration code:

//This widget is used inside a Scaffold
class TabsPage extends StatefulWidget {

  @override
  State<StatefulWidget> createState() => new TabsPageState();

}

class TabsPageState extends State<TabsPage> with TickerProviderStateMixin {

  List<Tab> _tabs;
  List<Widget> _pages;
  TabController _controller;

  @override
  void initState() {
    super.initState();
    _tabs = [
      new Tab(text: 'TabOne'),
      new Tab(text: 'TabTwo')
    ];
    _pages = [
      //Just normal stateful widgets
      new TabOne(),
      new TabTwo()
    ];
    _controller = new TabController(length: _tabs.length, vsync: this);
  }

  @override
  Widget build(BuildContext context) {
    return new Padding(
      padding: EdgeInsets.all(10.0),
      child: new Column(
        children: <Widget>[
          new TabBar(
            controller: _controller,
            tabs: _tabs
          ),
          new SizedBox.fromSize(
            size: const Size.fromHeight(540.0),
            child: new TabBarView(
                controller: _controller,
                children: _pages
            ),
          )
        ],
      ),
    );
  }

}


Question on stack

Kris Giesing

unread,
Sep 5, 2018, 3:27:55 PM9/5/18
to julio.he...@gmail.com, Flutter Dev
Can you say a little more about why you want the second tab's build method to be called?

In general, Flutter is designed around the idea that user interface elements don't carry application state, so the application wouldn't usually have a reason to need a UI element built that isn't visible.

--
You received this message because you are subscribed to the Google Groups "Flutter Dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to flutter-dev...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Julio Henrique Bitencourt

unread,
Sep 5, 2018, 3:34:11 PM9/5/18
to Flutter Dev
I have a Form with fields in both Tabs, When I first enter the page and submite the form, I need to validate and make shure all fields are filled. And because the second Tab content wasn't generated, those fields are ignored and may stay empty.

Kris Giesing

unread,
Sep 5, 2018, 3:42:00 PM9/5/18
to julio.he...@gmail.com, Flutter Dev
Ah, thanks for the context.

I would suggest holding the form state in an object outside any of the tabs, and having the tabs' edit text fields update that form state when data is entered. Then the validation logic can operate on the form state regardless of whether the user has clicked over to any particular tab. You can either pass the form data model down to the tabs from above, or use something like ScopedModel (https://pub.dartlang.org/packages/scoped_model) to handle deeply nested widget trees.

Does that make sense?

Julio Henrique Bitencourt

unread,
Sep 5, 2018, 4:37:52 PM9/5/18
to Flutter Dev
Thanks for the reply.
I already have my form state outside the Tabs, I created a new example that represents better my real code:

import 'package:flutter/material.dart';

class Profile {
 
String name;
 
String mail;

 
@override
  String toString() {
   
return 'Profile{name: $name, mail: $mail}';
 
}
}

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
 
@override
  Widget build(BuildContext context) {
   
return new MaterialApp(
      title
: 'Demo App',
      theme
: new ThemeData(
        primarySwatch
: Colors.blue,
     
),
      home
: new MyHomePage(),
   
);
 
}
}

class MyHomePage extends StatefulWidget {
 
@override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

 
final _keyScaffold = new GlobalKey<ScaffoldState>();
 
GlobalKey<FormState> _keyForm = new GlobalKey<FormState>();
 
Profile profile = new Profile();

 
@override
  Widget build(BuildContext context) {
   
return new Scaffold(
      key
: _keyScaffold,
      appBar
: new AppBar(title: new Text("Profile")),
        floatingActionButton
: new FloatingActionButton(
          onPressed
: () {
           
// Submit and save the form state
            final FormState form = _keyForm.currentState;

           
if(!form.validate()) {
             
_keyScaffold.currentState.showSnackBar(new SnackBar(content: new Text("Invalid, check both Tabs")));
             
return;
           
}

            form
.save();
           
_keyScaffold.currentState.showSnackBar(new SnackBar(content: new Text("${profile}")));
         
},
          child
: Icon(Icons.save),
       
),
        body
: new SingleChildScrollView(
          child
: new Container(
            child
: new Column(
              crossAxisAlignment
: CrossAxisAlignment.start,
              children
: <Widget>[
               
// A bunch of contents
                new Container(
                  padding
: EdgeInsets.all(20.0),
                  child
: new Text("A bunch of other contents"),
               
),
                _buildTabsWidget
()
             
],
           
),
         
),
       
)
   
);
 
}

 
Widget _buildTabsWidget() {
   
return new Form(
      key
: _keyForm,
      child
: new ProfileTabsPage(profile)
   
);
 
}
}

class ProfileTabsPage extends StatefulWidget {

 
Profile profile;

 
ProfileTabsPage(this.profile);

 
@override
  State<StatefulWidget> createState() => new ProfileTabsPageState();

}

class ProfileTabsPageState extends State<ProfileTabsPage> with TickerProviderStateMixin {


 
List<Tab> _tabs;
 
List<Widget> _pages;
 
TabController _controller;

 
@override
  void initState() {
   
super.initState();
   
_tabs = [

     
new Tab(text: 'Personal'),
     
new Tab(text: 'Contacts')

   
];
   
_pages = [
     
//Just normal stateful widgets
      new TabOne(widget.profile),
     
new TabTwo(widget.profile)

   
];
   
_controller = new TabController(length: _tabs.length, vsync: this);
 
}


 
@override
  Widget build(BuildContext context) {
   
return new Padding(
      padding
: EdgeInsets.all(10.0),
      child
: new Column(
        children
: <Widget>[
         
new TabBar(
            controller
: _controller,

            tabs
: _tabs,
            labelColor
: Colors.blue,
            indicatorColor
: Colors.blue,

         
),
         
new SizedBox.fromSize(
            size
: const Size.fromHeight(540.0),
            child
: new TabBarView(
                controller
: _controller,
                children
: _pages
            ),
         
)
       
],
     
),
   
);
 
}
}

class TabOne extends StatefulWidget {

 
Profile profile;
 
TabOne(this.profile);

 
@override
  State<StatefulWidget> createState() => new TabOneState();

}

class TabOneState extends State<TabOne> with AutomaticKeepAliveClientMixin {

 
@override
  Widget build(BuildContext context) {
   
return new TextFormField(
        decoration
: new InputDecoration(
          labelText
: "Name",
       
),
        validator
: validateEmptyText,
        keyboardType
: TextInputType.text,
        maxLength
: 100,
        onSaved
: (String newValue) {
         
widget.profile.name = newValue;
       
},
        initialValue
: ""
    );
 
}

 
@override
  bool get wantKeepAlive => true;

 
String validateEmptyText(String value) {
   
if (value == null || value.isEmpty) {
     
return "Can't be empty";
   
}

   
return null;
 
}
}

class TabTwo extends StatefulWidget {

 
Profile profile;
 
TabTwo(this.profile);

 
@override
  State<StatefulWidget> createState() => new TabTwoState();

}

class TabTwoState extends State<TabTwo> with AutomaticKeepAliveClientMixin {

 
@override
  Widget build(BuildContext context) {
   
return new TextFormField(
        decoration
: new InputDecoration(
          labelText
: "Mail",
       
),
        validator
: validateEmptyText,
        keyboardType
: TextInputType.text,
        maxLength
: 100,
        onSaved
: (String newValue) {
         
widget.profile.mail = newValue;
       
},
        initialValue
: ""
    );
 
}

 
@override
  bool get wantKeepAlive => true;

 
String validateEmptyText(String value) {
   
if (value == null || value.isEmpty) {
     
return "Can't be empty";
   
}

   
return null;
 
}
}

If you first enter the page, fill the name and save, the profile will have a name filled and the email null.
If you first enter the page, fill the name, go to second tab and save, there will be a message that email can't be empty.

Kris Giesing

unread,
Sep 6, 2018, 9:16:11 PM9/6/18
to julio.he...@gmail.com, Flutter Dev
I appear to have chosen some unfortunate terminology in my last reply... I didn't realize until looking at your example code that Flutter has a FormState object already. However, what it does is slightly different from what I was trying to propose.

The Flutter FormState validate() method invokes validation across the set of FormField objects that it knows about. As you've discovered, "know about" here means that the FormField object is currently instantiated, i.e. some widget's build() method has created one.

What I was suggesting is that there be data model object that exists outside of the widget hierarchy entirely, and that this data model object be what ultimately validates all the input. That validation can occur regardless of whether any particular UI object has been instantiated to modify a particular part of the data model. The data model can be provided to a particular widget hierarchy through the means that I mentioned before: passed down explicitly through arguments, or provided through the ScopedModel pattern.

In your case, this would translate to maintaining one FormState object per tab, and having their validate() calls only handle the fields that are local to that FormState. When that local validation succeeds the FormState could write the local data into the external data model.

You would then also need an overall validation that ran against the full data model object. There's a user interface question about what to do if a user does not have the right field displaying to fix the source of a validation error; in your example the solution appears to be to ask the user to check all tabs, but there might be a better way to structure the UI so that this isn't necessary (e.g. automatically switch to the tab that has the error).

Hope that helps,

- Kris

Reply all
Reply to author
Forward
0 new messages