Flutter - getx controller not updated when data changed

31,502

Solution 1

GetX doesn't know / can't see when database data has changed / been updated.

You need to tell GetX to rebuild when appropriate.

If you use GetX observables with GetX or Obx widgets, then you just assign a new value to your observable field. Rebuilds will happen when the obs value changes.

If you use GetX with GetBuilder<MyController>, then you need to call update() method inside MyController, to rebuild GetBuilder<MyController> widgets.


The solution below uses a GetX Controller (i.e. TabX) to:

  1. hold application state:

    1. list of all tabs (tabPages)
    2. which Tab is active (selectedIndex)
  2. expose a method to change the active/visible tab (onItemTapped())

OnItemTapped()

This method is inside TabX, the GetXController.

When called, it will:

  1. set which tab is visible
  2. save the viewed tab to the database (FakeDB)
  3. rebuild any GetBuilder widgets using update()
  void onItemTapped(int index) {
    selectedIndex = index;
    db.insertViewedPage(index); // simulate database update while tabs change
    update(); // ← rebuilds any GetBuilder<TabX> widget
  }

Complete Example

Copy/paste this entire code into a dart page in your app to see a working BottomNavigationBar page.

This tabbed / BottomNavigationBar example is taken from https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html but edited to use GetX.

import 'package:flutter/material.dart';
import 'package:get/get.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyTabHomePage(),
    );
  }
}

class FakeDB {
  List<int> viewedPages = [0];

  void insertViewedPage(int page) {
    viewedPages.add(page);
  }
}

/// BottomNavigationBar page converted to GetX. Original StatefulWidget version:
/// https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html
class TabX extends GetxController {

  TabX({this.db});

  final FakeDB db;
  int selectedIndex = 0;
  static const TextStyle optionStyle =
  TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
  List<Widget> tabPages;

  @override
  void onInit() {
    super.onInit();
    tabPages = <Widget>[
      ListViewTab(db),
      Text(
        'Index 1: Business',
        style: optionStyle,
      ),
      Text(
        'Index 2: School',
        style: optionStyle,
      ),
    ];
  }

  /// INTERESTING PART HERE ↓ ************************************
  void onItemTapped(int index) {
    selectedIndex = index;
    db.insertViewedPage(index); // simulate database update while tabs change
    update(); // ← rebuilds any GetBuilder<TabX> widget
    // ↑ update() is like setState() to anything inside a GetBuilder using *this*
    // controller, i.e. GetBuilder<TabX>
    // Other GetX controllers are not affected. e.g. GetBuilder<BlahX>, not affected
    // by this update()
    // Use async/await above if data writes are slow & must complete before updating widget. 
    // This example does not.
  }
}

/// REBUILT when Tab Page changes, rebuilt by GetBuilder in MyTabHomePage
class ListViewTab extends StatelessWidget {
  final FakeDB db;

  ListViewTab(this.db);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: db.viewedPages.length,
      itemBuilder: (context, index) =>
          ListTile(
            title: Text('Page Viewed: ${db.viewedPages[index]}'),
          ),
    );
  }
}


class MyTabHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Get.put(TabX(db: FakeDB()));

    return Scaffold(
      appBar: AppBar(
        title: const Text('BottomNavigationBar Sample'),
      ),
      body: Center(
        /// ↓ Tab Page currently visible - rebuilt by GetBuilder when 
        /// ↓ TabX.onItemTapped() called
        child: GetBuilder<TabX>(
            builder: (tx) => tx.tabPages.elementAt(tx.selectedIndex)
        ),
      ),
      /// ↓ BottomNavBar's highlighted/active item, rebuilt by GetBuilder when
      /// ↓ TabX.onItemTapped() called
      bottomNavigationBar: GetBuilder<TabX>(
        builder: (tx) => BottomNavigationBar(
          items: const <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              icon: Icon(Icons.home),
              label: 'Home',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.business),
              label: 'Business',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.school),
              label: 'School',
            ),
          ],
          currentIndex: tx.selectedIndex,
          selectedItemColor: Colors.amber[800],
          onTap: tx.onItemTapped,
        ),
      ),
    );
  }
}

Solution 2

thanks to @Baker for the right answer. However, if you have a list and in viewModel and want to update that list, just use the list.refresh() when the list updated

RxList<Models> myList = <Models>[].obs;

when add or insert data act like this:

myList.add(newItem);
myList.refresh();

Solution 3

 GetX< ExploreController >(builder: (controller) {
        if (controller.isLoading.value) {
          return Center(
            child: SpinKitChasingDots(
                color: Colors.deepPurple[600], size: 40),);
        }
        return ListView.separated(
            padding: EdgeInsets.all(12),
            itemCount: controller.articleList.length,
            separatorBuilder: (BuildContext context, int index) {});
      });

Solution 4

If you change the value in the database 'manually', you need a STREAM to listen to the change on the database. You can't do:

var articles = await ApiService.fetchArticles();

You need to do something like this:

var articles = await ApiService.listenToArticlesSnapshot();

The way you explained is like if you need the data to refresh after navigating to another page and clicking on a button, then navigating to first page (GetBuilder) OR automatically adds data from the within the first page (Obx). But your case is simple, just retrieve the articles SNAPSHOT, then in the controller onInit, subscribe to the snapshot with the bindStream method, and eventually use the function ever() to react to any change in the observable articleList. Something like this:

Share:
31,502
jancooth
Author by

jancooth

Updated on September 01, 2021

Comments

  • jancooth
    jancooth over 2 years

    I am developing an app that has a bottomnavitaionbar with five pages. I use getx. In first page, i am listing data. My problem is that, when i changed data(first page in bottomnavigationbar) manually from database and thn i pass over pages, came back to first page i could not see changes.

    Controller;

    class ExploreController extends GetxController {
      var isLoading = true.obs;
      var articleList = List<ExploreModel>().obs;
    
      @override
      void onInit() {
        fetchArticles();
        super.onInit();
      }
    
      void fetchArticles() async {
        try {
          isLoading(true);
          var articles = await ApiService.fetchArticles();
          if (articles != null) {
            //articleList.clear();
            articleList.assignAll(articles);
          }
        } finally {
          isLoading(false);
        }
        update();
      }
    }
    

    and my UI;

    body: SafeArea(
            child: Column(
            children: <Widget>[
              Header(),
              Expanded(
                child: GetX<ExploreController>(builder: (exploreController) {
                  if (exploreController.isLoading.value) {
                    return Center(
                      child: SpinKitChasingDots(
                          color: Colors.deepPurple[600], size: 40),
                    );
                  }
                  return ListView.separated(
                    padding: EdgeInsets.all(12),
                    itemCount: exploreController.articleList.length,
                    separatorBuilder: (BuildContext context, int index) {
    
  • jancooth
    jancooth about 3 years
    I tried this. I have my controller. I could not see new data, i must refresh page to see new data.
  • jancooth
    jancooth about 3 years
    Hello, i tried this, i updated my code. But nothing changed. I hope you understand my problem.
  • Rahul Singh
    Rahul Singh about 3 years
    make your data observable by adding .obs to it.
  • Rahul Singh
    Rahul Singh about 3 years
    make your data observable by adding .obs to it
  • jancooth
    jancooth about 3 years
    I have obs. var articleList = List<ExploreModel>().obs;
  • Loren.A
    Loren.A about 3 years
    Are you firing the fetch articles function from anywhere else besides the controller onInit? Because at least from what I can see, that would be the only time the data would update is when that function is fired. Also if you're having trouble seeing updated data on app start without manually refreshing, try await Get.put(ExploreController()) in your main method before running the app. Then your app will start with updated data displayed.
  • Simpler
    Simpler about 3 years
    why would you put the Get.put() inside the build function? Wouldn't it be better if it were outside of the build so that it's not called on each build?
  • Baker
    Baker about 3 years
    Get controller lifecycles are tied to routes. When a route is disposed, Get controllers registered to that route are also disposed. Registered as a field/member of a route (i.e. a widget), a controller is tied to the parent widget, not the current widget. And when the current widget is disposed (route popped), the Get controller lives on (and it shouldn't). Placing Get.put inside build(), avoids this lifecycle quirk (that the current widget cannot be registered with until build()). 1/2
  • Baker
    Baker about 3 years
    Here's Eduardo, one of the package maintainers, with an example: github.com/jonataslaw/getx/issues/818#issuecomment-733652172‌​. And more details on Get create/dispose lifecycle by me: stackoverflow.com/a/65117780/2301224 2/2
  • omarwaleed
    omarwaleed about 2 years
    you're a life saver