1

I'm trying to avoid rebuilding FutureBuilder in flutter. I have tried solution suggested in below Q's.

How to parse JSON only once in Flutter

Flutter Switching to Tab Reloads Widgets and runs FutureBuilder

still my app fires API every time I navigate to that page. Please point me where I'm going wrong.

Util.dart

//function which call API endpoint and returns Future in list
class EmpNetworkUtils {
    ...
    Future<List<Employees>> getEmployees(data) async {
        List<Employees> emps = [];
        final response = await http.get(host + '/emp', headers: { ... });
        final responseJson = json.decode(response.body);
        for (var empdata in responseJson) {
            Employees emp = Employees( ... );
            emps.add(emp);
        }
        return emps;
    }
}

EmpDetails.dart

class _EmpPageState extends State<EmpPage>{
    ...
    Future<List<Employees>>_getAllEmp;
    @override
    initState() {
        _getAllEmp = _getAll();
        super.initState();
    }
    Future <List<Employees>>_getAll() async {
        _sharedPreferences = await _prefs;
        String authToken = AuthSessionUtils.getToken(_sharedPreferences);
        return await EmpNetworkUtils().getEmployees(authToken);
    }

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: new AppBar( ... ),
            body: Container(
                child: FutureBuilder(
                    future: _getAllEmp,
                    builder: (BuildContext context, AsyncSnapshot snapshot) { ... }
        )))
    }
}

Update: I'm using bottomNavigationBar in my app, from which this page is loaded.

Community
  • 1
  • 1
Suroor Ahmmad
  • 1,110
  • 4
  • 23
  • 33

2 Answers2

2

You are calling your getEmployees function in initState, which is meant to be called every time your widget is inserted into the tree. If you want to save the data after calling your function the first time, you will have to have a widget that persists.

An easy implementation would be using an InheritedWidget and a data class:

class InheritedEmployees extends InheritedWidget {
  final EmployeeData employeeData;

  InheritedEmployees({
    Key key,
    @required Widget child,
  })  : assert(child != null),
        employeeData = EmployeeData(),
        super(key: key, child: child);

  static EmployeeData of(BuildContext context) => (context.inheritFromWidgetOfExactType(InheritedEmployees) as InheritedEmployees).employeeData;

  @override
  bool updateShouldNotify(InheritedEmployees old) => false;
}

class EmployeeData {
  List<Employees> _employees;

  Future<List<Employees>> get employees async {
    if (_employees != null) return _employees;
    _sharedPreferences = await _prefs;
    String authToken = AuthSessionUtils.getToken(_sharedPreferences);
    return _employees = await EmpNetworkUtils().getEmployees(authToken);
  }
}

Now, you would only have to place your InheritedEmployees somewhere that will not be disposed, e.g. about your home page, or if you want, even about your MaterialApp (runApp(InheritedEmployees(child: MaterialApp(..));). This way the data is only fetched once and cached after that. You could also look into AsyncMemoizer if that suits you better, but the example I provided should work fine.

Now, you will want to call this employees getter in didChangeDependencies because your _EmpPageState is dependent on InheritedEmployees and you need to look that up, which cannot happen in initState:

class _EmpPageState extends State<EmpPage>{
    Future<List<Employees>>_getAllEmp;

    @override
    void didChangeDependencies() {
      _getAllEmp = InheritedEmployees.of(context).employees;
      super.didChangeDependencies();
    }

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: new AppBar( ... ),
            body: Container(
                child: FutureBuilder(
                    future: _getAllEmp,
                    builder: (BuildContext context, AsyncSnapshot snapshot) { ... }
        )))
    }
}

I mentioned that your State is now dependent on your InheritedWidget, but that does not really matter as updateShouldNotify always returns false (there are not going to be any additional builds).

creativecreatorormaybenot
  • 114,516
  • 58
  • 291
  • 402
  • Thanks. I'll try this. I have few doubts like, -- what makes my code different than those solutions (from Q's I linked in this post). -- If I want to rebuild the widget is some cases like, If I add new record to emp list, I want to rebuild the widget, or if I want to keep timer, which will rebuild in T seconds, how can I implement that? – Suroor Ahmmad Aug 02 '19 at 17:25
  • This works great. There are similar pages in my app, where too I want to avoid this. How can I implement that? – Suroor Ahmmad Aug 02 '19 at 18:06
  • @SuroorAhmmad You can add more objects to your data class. – creativecreatorormaybenot Aug 03 '19 at 09:09
  • You mean adding object to EmployeeData class? That wont help I guess, because I need to fetch data from different APIs. – Suroor Ahmmad Aug 03 '19 at 13:49
  • @SuroorAhmmad You can give your class a different, more generic name and then have multiple objects, each with a getter. The `InheritedWidget` sits at the top of your app, so you never need to worry about it rebuilding. – creativecreatorormaybenot Aug 03 '19 at 14:09
  • Cool, I'll try that.Is there any way I can force `updateShouldNotify()` return `true` is some conditions, like timer or after adding new item to list. – Suroor Ahmmad Aug 04 '19 at 14:00
  • @SuroorAhmmad Well, that will not do anything because you are not rebuilding your `InheritedWidget` (see [this answer](https://stackoverflow.com/a/49492495/6509751)). You can create a `Stream` and listen to that instead. (You can listen to a timer). – creativecreatorormaybenot Aug 04 '19 at 16:00
2

I got another way to solve this issue and apply to my app also

  • Apply GetX controller to call API and render response data
  • Remove FutureBuilder to call API data

Apply GetX controller to call API data, Like

class NavMenuController extends GetxController {

  Api api = new Api();
  var cart = List<NavMenu>().obs;

  @override
  void onInit() {
    // TODO: implement onInit
    getNavMenuData();
    super.onInit();
  }

  Future<List<NavMenu>> getNavMenuData() async {
    var nav_data = await api.getNavMenus();
    if(nav_data!=null) {
      cart.value = nav_data;
    }
    return cart;
  }

}

Call API using controller on initState() into desired class

class NavMenuDrawer extends StatefulWidget {
  @override
  _NavMenuDrawerState createState() => _NavMenuDrawerState();
}

class _NavMenuDrawerState extends State<NavMenuDrawer> {

  final NavMenuController navMenuController = Get.put(NavMenuController());

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    navMenuController.getNavMenuData();
  }

Remove below FutureBuilder code for calling API, [if you use FutureBuilder/StreamBuilder whatever]

return FutureBuilder<List<NavMenu>>(
      future: api.getNavMenus(),
      builder: (context, snapshot) {
      
      return ListView.builder(
              scrollDirection: Axis.vertical,
              shrinkWrap: true,
              physics: ScrollPhysics(),
              itemCount: snapshot.data?.length ?? 0,
              itemBuilder: (context, index) {
                return Column(
                  children: [
                    ListTile(
                      title: Text("${snapshot.data[index].title}"),

Just use GetX controller to get data, like

return ListView.builder(
      scrollDirection: Axis.vertical,
      shrinkWrap: true,
      physics: ScrollPhysics(),
      itemCount: navMenuController.cart?.length ?? 0,
      itemBuilder: (context, index) {
        return Column(
          children: [
            ListTile(
              title: Obx(() {
                return Text(
                    "${navMenuController.cart.value[index].title}");
              }),

Note : For more info you can search on how to apply GetX

Mimu Saha Tishan
  • 2,402
  • 1
  • 21
  • 40