2

I have a very simple setup. A TabBarView with 2 tabs. On the first tab the user would choose an option. With this first option, something would be loaded from an API to display on the second tab in a FutureBuilder. The problem is that the FutureBuilder gets stuck on ConnectionState.none. The only way to fix this is by waiting for a while before calling setState again so that the FutureBuilder updates. Any ideas on how to solve this? It has been bugging me for way too long.

Minimum reproducible example (tested on DartPad):

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: TabWidget(),
    );
  }
}

class TabWidget extends StatefulWidget {
  @override
  _TabWidgetState createState() => _TabWidgetState();
}

class _TabWidgetState extends State<TabWidget> with SingleTickerProviderStateMixin {
  late final TabController _tabController;
  Future<String>? _future;

  @override
  void initState() {
    super.initState();

    _tabController = TabController(length: 2, vsync: this);
  }
  
  Future<String> _loadStuff(int i) async {
    await Future.delayed(Duration(milliseconds: 100));
    return "Loaded $i!";
  }
  
  void onNext(int i) async {
    _tabController.index += 1;
    setState(() {
      _future = _loadStuff(i);
    });
    
    // Only works when calling setState after the tab has changed!
    // await Future.delayed(Duration(seconds: 1));
    // setState(() {});
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Example")),
      body: TabBarView(
        physics: NeverScrollableScrollPhysics(),
        controller: _tabController,
        children: [
          ListView(
            children: [
              ListTile(title: Text("Option 1"), onTap: () => onNext(1)),
              ListTile(title: Text("Option 2"), onTap: () => onNext(2)),
              ListTile(title: Text("Option 3"), onTap: () => onNext(3)),
            ],
          ),
          FutureBuilder<String>(
            future: _future,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.done) {
                return Center(child: Text(snapshot.data!));
              } else {
                return Center(child: CircularProgressIndicator());
              }
            },
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }
}
user3217163
  • 67
  • 1
  • 6

1 Answers1

0

I'd recommend just not using FutureBuilder. I find my code is more readable and predictable when I do the state management myself. For example, you could have a String? message in your state that is set by the async call, and check to see if it's null when building:

// inside your state class

String? _message;

Future<String> _loadStuff(int i) async {
  await Future.delayed(Duration(milliseconds: 100));
  return "Loaded $i!";
}
  
Future<void> onNext(int i) async {
  _tabController.index += 1;
  final message = await _loadStuff(i);
  setState(() => _message = message);
}

Then in your build() method, you can remove your FutureBuilder and simply use:

Center(child: _message == null ? CircularProgressIndicator() : Text(_message!))

P.S. you can get rid of your initState() by making use of late final:

late final _tabController = TabController(length: 2, vsync: this);

works just fine :)

cameron1024
  • 9,083
  • 2
  • 16
  • 36
  • That works. But I am very curious about what is going on with the FutureBuilder and why it is not working so I won't mark this as the accepted answer yet, unless nobody else answers. – user3217163 May 31 '21 at 23:08