1

I am new to Flutter and GETX...

My requirement is to get tab names from API... than get tab data from API, based on every tab selection...

I found getx usage in tabview here, but I don't know how to make it to work with API response. I tried to achieve this by adding api call in onInit method in my controller class but have no luck with it....

Here is my controller....

class MyTabController extends GetxController with SingleGetTickerProviderMixin {
  var isLoading = true.obs;
  var tabNames= List<TabNameModel>().obs;
  List<Tab> myTabs = <Tab>[].obs;

  TabController controller;

 void fetchApiData() async {
    isLoading(true);
    try {
      var response = await <HTTP API Call>;
      
      tabNames
          .assignAll(response != null ? tabNamesFromJson(response) : null);
      for (TabNameModel tabname in TabNameModel.value) {
        myTabs.add(Tab(text: tabname.name));
      }
    } finally {
      isLoading(false);
    }
  }


  @override
  void onInit() {
    fetchApiData()
    super.onInit();
    controller = TabController(vsync: this, length: myTabs.length);
  }

  @override
  void onClose() {
    controller.dispose();
    super.onClose();
  }
}

some times I get empty tabs and some time I get error like this.... error screen

Here is my screen......

class MyTabbedWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyTabController _tabx = Get.put(MyTabController());
    // ↑ init tab controller

return Scaffold(
  appBar: AppBar(
    bottom: TabBar(
      controller: _tabx.controller,
      tabs: _tabx.myTabs,
    ),
  ),
  body: TabBarView(
    controller: _tabx.controller,
    children: _tabx.myTabs.map((Tab tab) {
      final String label = tab.text.toLowerCase();
      return Center(
        child: Text(
          'This is the $label tab',
          style: const TextStyle(fontSize: 36),
        ),
      );
    }).toList(),
  ),
);
  }
}
Praveen
  • 346
  • 1
  • 6
  • 18

2 Answers2

2

Problem

MyTabbedWidget is trying to use myTabs from the Controller before fetchApiData async call has finished populating myTabs. myTabs is empty until fetch call completes.

TabBarView will try to access myTabs which will be zero-length until API call finishes. Flutter TabController length cannot be zero, or it will throw an error, which I think you are seeing.

Solutions

Two solutions: Blocking & Non-Blocking

Blocking Solution

One solution is to make the fetchApiData async call prior to the application start and wait for it to finish before proceeding. Done in a Bindings class. This will delay the loading of the page until the call finishes. If this is OK, this would be a potential solution:

// make main ↓ async
void main() async {
  await MyBlockingBindings().dependencies();
  // ↑ make API call prior to app start, wait for results
  runApp(MyApp());
}

class MyBlockingBindings extends Bindings {
  List<Tab> loadedTabs = [];

  @override
  Future<void> dependencies() async {
    // ↓ long-duration async call to load tab data
    await Future.delayed(Duration(seconds: 2),
            () => loadedTabs = [
              Tab(text: 'BlockedLeft'),
              Tab(text: 'BlockedRight')
            ]
    );

    // ↓ Register controller using fetched tab data
    Get.put(MyTabController(myTabs: loadedTabs));
  }
}

class MyTabController extends GetxController with SingleGetTickerProviderMixin {
  List<Tab> myTabs;

  // ↓ Constructor can now take myTabs argument
  MyTabController({this.myTabs});

  TabController controller;

  @override
  void onInit() {
    super.onInit();
    controller = TabController(vsync: this, length: myTabs.length);
  }

  @override
  void onClose() {
    controller.dispose();
    super.onClose();
  }
}

Since we did Get.put(MyTabController()) in MyBlockingBindings we can use Get.find() in our view widget.

class MyTabbedWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyTabController _tabx = Get.find();
    // ↑ controller already init in Bindings, just find it

    return Scaffold(
      appBar: AppBar(
        bottom: TabBar(
          controller: _tabx.controller,
          tabs: _tabx.myTabs,
        ),
      ),
      body: TabBarView(
        controller: _tabx.controller,
        children: _tabx.myTabs.map((Tab tab) {
          final String label = tab.text.toLowerCase();
          return Center(
            child: Text(
              'This is the $label tab',
              style: const TextStyle(fontSize: 36),
            ),
          );
        }).toList(),
      ),
    );
  }
}

The rest is unchanged from the example you followed.

Non-Blocking Solution

This solution loads immediately with placeholder tab data, then swaps placeholder data with tabs loaded from API call, after they've arrived.

The (fake) 2 sec. API call asyncLoadTabs() is done in MyTabController onInit(). Note that we're not using await here and onInit is not made async. We don't want to block processing. The async call will run when Flutter's event loop gets around to it.

In MyTabbedWidget we wrap everything in a GetBuilder<MyTabController> widget. When we call update() in MyTabController our GetBuilder will rebuild itself using the latest data.

API Calls on Tab Switch

TabBar onTap: calls controller's switchTab(index) which in turn calls asyncLoadTabs with the index of the tab selected, making another API call with the tab #.

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

class MyTabController extends GetxController with SingleGetTickerProviderMixin {
  List<Tab> myTabs = <Tab>[
    Tab(text: 'loading...'),
  ];

  // ↓ Constructor can now take myTabs argument
  MyTabController({myTabs}) {
    this.myTabs ??= myTabs;
  }

  TabController controller;

  @override
  void onInit() {
    super.onInit();
    controller = TabController(vsync: this, length: myTabs.length);
    asyncLoadTabs();
  }

  // Fake 2 sec. async call
  void asyncLoadTabs({int index = 0}) async {
    await Future.delayed(Duration(seconds: 2), () {
      myTabs = [
        Tab(text: 'LEFT $index'),
        Tab(text: 'RIGHT $index'),
      ];
      controller.dispose(); // release animation resources
      // recreate TabController as length is final/cannot change ↓
      controller = TabController(
          vsync: this,
          length: myTabs.length,
          initialIndex: index // to show a particular tab on create
      );
      update();
      // ↑ rebuilds GetBuilder widget with latest controller data
    });
  }

  void switchTab(int index) async {
    asyncLoadTabs(index: index);
  }

  @override
  void onClose() {
    controller.dispose();
    super.onClose();
  }
}

class MyTabbedWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {

    // ↓ use GetBuilder & rebuild using update()
    return GetBuilder<MyTabController>(
      init: MyTabController(),
      builder: (_tabx) => Scaffold(
        appBar: AppBar(
          bottom: TabBar(
            controller: _tabx.controller,
            tabs: _tabx.myTabs,
            onTap: _tabx.switchTab, // receives tab # on tab click
          ),
        ),
        body: TabBarView(
          controller: _tabx.controller,
          children: _tabx.myTabs.map((Tab tab) {
            final String label = tab.text.toLowerCase();
            return Center(
              child: Text(
                'This is the $label tab',
                style: const TextStyle(fontSize: 36),
              ),
            );
          }).toList(),
        ),
      ),
    );
  }
}
Baker
  • 24,730
  • 11
  • 100
  • 106
-1

Here's my Code, From my API I had all the categories and foods nested inside the categories. Api Response

{
  "data": {
    "getOneRestaurant": {
      "error": false,
      "msg": "Restaurant Get Successfully",
      "data": {
        "cover_img": "https://i.ibb.co/YNZ64QG/0-89399200-1551782137-fast1.jpg",
        "description": "",
        "address": {
          "address": "21 KDA Approach Rd, Khulna 9200, Bangladesh"
        },
        "food_categories": [
          {
            "_id": "5fa122713cf61557a65d0a12",
            "name": "Fast Food",
            "foods": [
              {
                "_id": "5fcc709678070b0098203a0f",
                "name": "Chicken reshmi kabab",
                "description": "",
                "dish_img": "https://i.ibb.co/kHGcn3v/Indian-chicken-kebab-Getty-Images-91279048-58eee4623df78cd3fcd0c6ca.jpg",
                "price": 320,
                "price_and_size": []
              },
              {
                "_id": "5fcc719178070b0098203a10",
                "name": "Kacchi biriyani",
                "description": "",
                "dish_img": "https://i.ibb.co/Zmp3yp5/47125153f54b972670697f49dac933cc.jpg",
                "price": 230,
                "price_and_size": []
              },
              {
                "_id": "5fcc722578070b0098203a11",
                "name": "Chicken tikka ",
                "description": "",
                "dish_img": "https://i.ibb.co/M2sLTqP/img-20161210-221320-largejpg.jpg",
                "price": 170,
                "price_and_size": []
              },
              {
                "_id": "5fcc72f478070b0098203a12",
                "name": "Chicken tandoori 1 pcs",
                "description": "",
                "dish_img": "https://i.ibb.co/LZw5Fp2/chicken-tandori-1526595014.jpg",
                "price": 170,
                "price_and_size": []
              },
              {
                "_id": "5fce042d78070b0098203b1b",
                "name": "Special thai soup for 4 person",
                "description": "",
                "dish_img": "https://i.ibb.co/YtmVwmm/download.jpg",
                "price": 300,
                "price_and_size": []
              },
              {
                "_id": "5fce048b78070b0098203b1c",
                "name": "Thai clear soup for 4 person",
                "description": "",
                "dish_img": "https://i.ibb.co/BjcRvNL/tomyum800-56a9498e5f9b58b7d0f9ea4f.jpg",
                "price": 250,
                "price_and_size": []
              },
              {
                "_id": "5fce04d078070b0098203b1d",
                "name": "Chicken vegetables soup four 4 person",
                "description": "",
                "dish_img": "https://i.ibb.co/ZN3Bxnk/chicken-vegetable-soup-9-1200.jpg",
                "price": 180,
                "price_and_size": []
              },
              {
                "_id": "5fce050678070b0098203b1e",
                "name": "Russian salad",
                "description": "",
                "dish_img": "https://i.ibb.co/vxh9qGZ/download.jpg",
                "price": 200,
                "price_and_size": []
              },
              {
                "_id": "5fce053378070b0098203b1f",
                "name": "Green salad",
                "description": "",
                "dish_img": "https://i.ibb.co/XpwwB8Y/green-salad-1200-1387.jpg",
                "price": 100,
                "price_and_size": []
              },
              {
                "_id": "5fce056878070b0098203b20",
                "name": "French fries",
                "description": "",
                "dish_img": "https://i.ibb.co/NCPsK6Y/Copycat-Mc-Donalds-French-Fries-500x500.jpg",
                "price": 60,
                "price_and_size": []
              },
              {
                "_id": "5fce059a78070b0098203b21",
                "name": "Chicken fry  4 pic",
                "description": "",
                "dish_img": "https://i.ibb.co/9hwPhgd/download-1.jpg",
                "price": 180,
                "price_and_size": []
              },
              {
                "_id": "5fce05dc78070b0098203b22",
                "name": "Chicken burger",
                "description": "",
                "dish_img": "https://i.ibb.co/HnJH38T/Butchies-2.jpg",
                "price": 80,
                "price_and_size": []
              },
              {
                "_id": "5fce060078070b0098203b23",
                "name": "Chicken pizza ",
                "description": "",
                "dish_img": "https://i.ibb.co/WWXzqdk/download.jpg",
                "price": 120,
                "price_and_size": []
              },
              {
                "_id": "5fce062a78070b0098203b24",
                "name": "Chicken naan",
                "description": "",
                "dish_img": "https://i.ibb.co/cgLg923/download-1.jpg",
                "price": 60,
                "price_and_size": []
              }
            ]
          }
        ]
      }
    }
  }
}

Tab's

TabBar(
        isScrollable: true,
        labelPadding: EdgeInsets.symmetric(horizontal: width * 20),
        controller: _tabController,
        labelColor: Color(0xffC8102E),
        unselectedLabelColor: Colors.black,
        labelStyle: TextStyle(
            fontWeight: FontWeight.bold
        ),
        unselectedLabelStyle: TextStyle(
            fontWeight: FontWeight.normal
        ),
        indicatorColor: Color(0xffC8102E),
        tabs: profile.foodCategories.map((e) => Tab(text: e.name)).toList(),
      )

Body

TabBarView(
      controller: _tabController,
      children: profile.foodCategories.map((RestaurantFoodCategories e) {
        return itemList(e.foods, e.id);
      }).toList(),
    )

ItemList

  Widget itemList(List<RestaurantFoods> items,String id) {
    return ListView.builder(
      primary: false,
      itemCount: items.length ?? 0,
      padding: EdgeInsets.zero,
      shrinkWrap: true,
      physics: AlwaysScrollableScrollPhysics(),
      itemBuilder: (context, index){
        RestaurantFoods item  = items[index];
        return itemCard(item, id);
      },
    );
  }
M.M.Hasibuzzaman
  • 1,015
  • 5
  • 10