Wai Yan Hein
Wai Yan Hein

Reputation: 14831

Flutter Bloc is not updating the state/ not working

I am developing a mobile application using Flutter. I am using the flutter bloc package, https://pub.dev/packages/flutter_bloc for managing and setting up the bloc. But when the state change it is not updating the widgets or views.

I have a bloc class file called home_bloc.dart with the following implementation.

class HomeEvent {
  static const int FETCH_ARTICLES = 1;
  static const int TOGGLE_IS_FILTERING = 2;
  int _event = 0;
  String _filterKeyword = "";

  int get event => _event;

  void set event(int event) {
    this._event = event;
  }

  String get filterKeyword => _filterKeyword;

  void set filterKeyword(String filterKeyword) {
    this._filterKeyword = filterKeyword;
  }
}

class HomeBloc extends Bloc<HomeEvent, HomeState> {
  Repository _repository = Repository();
  HomeState state = HomeState();

  @override
  HomeState get initialState => state;

  @override
  Stream<HomeState> mapEventToState(HomeEvent event) async* {
    switch (event.event) {
      case HomeEvent.FETCH_ARTICLES:
        {
          List<dynamic> articles = List<dynamic>();
          fetchArticles(filter: event.filterKeyword).listen((dynamic article) {
            articles.add(article);
          });
          state = state.copyWith(articles: articles);
          break;
        }
      case HomeEvent.TOGGLE_IS_FILTERING:
        {
          state.isFiltering = ! state.isFiltering;
          state = state.copyWith();
          break;
        }
      default:
        {
          state = state.initial();
          break;
        }
    }

    yield state;
  }

  Stream<dynamic> fetchArticles({String filter = ""}) async* {
    List<dynamic> list = (this.state.articles.length > 0)
        ? this.state.articles
        : await _repository.getArticles();
    if (filter.isNotEmpty) {
      for (var article in list) {
        if (article is String) {
          yield article;
        } else if (article.title.contains(filter)) {
          yield article;
        }
      }
    } else {
      for (var article in list) {
        yield article;
      }
    }
  }
}

class HomeState {
  bool _isFiltering = false;
  List<dynamic> _articles = List<dynamic>();

  bool get isFiltering => _isFiltering;

  void set isFiltering(bool isFiltering) {
    this._isFiltering = isFiltering;
  }

  List<dynamic> get articles => _articles;

  void set articles(List<dynamic> list) {
    this._articles = list;
  }

  HomeState initial() {
    HomeState state = HomeState();
    state.isFiltering = false;
    state.articles = List<dynamic>();

    return state;
  }

  HomeState copyWith({ bool isFiltering, List<dynamic> articles }) {
    HomeState state = HomeState();
    state.isFiltering = isFiltering != null? isFiltering: this._isFiltering;
    state.articles = articles!=null && articles.length > 0? articles: this._articles;

    return state;
  }
}

This is my repository class returning dummy data.

class Repository {
  Future<List<dynamic>> getArticles() async {
    List<dynamic> list = List<dynamic>();
    list.add("A");

    Article article1 = Article();
    article1.id = 1;
    article1.title = "A start is born";
    list.add(article1);

    Article article2 = Article();
    article2.id = 2;
    article2.title = "Asking for help";
    list.add(article2);

    Article article3 = Article();
    article3.id = 3;
    article3.title = "Angel is comming";
    list.add(article3);

    list.add("B");

    Article article4 = Article();
    article4.id = 4;
    article4.title = "Baby Boss";
    list.add(article4);

    Article article5 = Article();
    article5.id = 5;
    article5.title = "Beginner guide to Staying at Home";
    list.add(article5);

    list.add("C");

    Article article6 = Article();
    article6.id = 6;
    article6.title = "Care each other";
    list.add(article6);

    Article article7 = Article();
    article7.id = 7;
    article7.title = "Controlling the world";
    list.add(article7);

    Article article8 = Article();
    article8.id = 8;
    article8.title = "Chasing the dream";
    list.add(article8);

    return list;
  }
}

This is my HomePage widget

class HomePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _HomePageState();
  }
}

class _HomePageState extends State<HomePage> {
  IconData _searchIcon = Icons.search;
  Widget _appBarTitle;
  HomeBloc _homeBloc;

  @override
  void initState() {
    super.initState();
    this._homeBloc = BlocProvider.of(context);
    WidgetsBinding.instance.addPostFrameCallback((_) => this.fetchArticles());
    WidgetsBinding.instance
        .addPostFrameCallback((_) => this.buildAppBarTitle());
  }

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<HomeBloc, HomeState>(
      builder: (context, state) {
        return Scaffold(
          appBar: AppBar(title: Text("Home"),),
          body: Container(
            child: buildListView(context, state),
          ),
        );
      },
    );
  }

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

  void buildAppBarTitle() {
    this.setState(() {
      if (_searchIcon == Icons.search) {
        this._appBarTitle = Text("Home");
      } else {
        this._appBarTitle = TextField(
          onChanged: (String inputValue) {
            debugPrint("Search term has changed $inputValue");
            //homeBloc.fetchArticles(filter: inputValue);
          },
          style: TextStyle(
            color: Colors.white,
          ),
          decoration: InputDecoration(
            hintText: "Search",
          ),
        );
      }
    });
  }

  Widget buildAppBarSearchIcon() {
    return IconButton(
        icon: Icon(
          _searchIcon,
          color: Colors.white,
        ),
        onPressed: () {
          if (this._searchIcon == Icons.search) {
            //display the search text field and close icons

            this.setState(() {
              this._searchIcon = Icons.close;
              this.buildAppBarTitle();
              //homeBloc.toggleFiltering();
            });
          } else {
            this.fetchArticles();
            this.setState(() {
              this._searchIcon = Icons.search;
              this.buildAppBarTitle();
              //homeBloc.toggleFiltering();
            });
          }
        });
  }

  Widget buildListView(
      BuildContext context, HomeState state) {
    if (state.articles.length > 0) {
      var listView = ListView.builder(
          itemCount: state.articles.length,
          itemBuilder: (context, index) {
            var item = state.articles[index];

            if (item is String) {
              return buildListFirstInitialView(item);
            }

            Article article = item as Article;

            return buildListArticleView(article);
          });

      return listView;
    } else {
      return Center(
        child: Text("No resources found."),
      );
    }
  }

  Widget buildListFirstInitialView(String initial) {
    return ListTile(
      title: Text(initial),
    );
  }

  Widget buildListArticleView(Article article) {
    return ListTile(
      title: Text(article.title),
    );
  }

  Widget buildBottomNavigationBar() {
    return BottomNavigationBar(
        currentIndex: 0,
        onTap: (int position) {},
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            title: Text('Home'),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            title: Text('Settings'),
          ),
        ]);
  }

  void fetchArticles({String filter = ""}) {
    HomeEvent event = HomeEvent();
    event.event = HomeEvent.FETCH_ARTICLES;
    _homeBloc.add(event);
  }
}

As you can see this is my HomePage widget is doing. It will fetch the articles after the widget is built. Then the list view will be updated with the dummy data.

My main.dart file.

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: BlocProvider(
        create: (context) => HomeBloc(),
        child: HomePage(),
      ),
    );
  }
}

When I run my app, it is not updating the list view with the dummy data. Instead, it is always showing the message for no records found.

enter image description here

Why is it not working?

Upvotes: 11

Views: 35319

Answers (5)

Henrik
Henrik

Reputation: 9945

For Equatable users mixing in Iterable

It will default to ONLY comparing the contents of the Iterable<>, not the othe fields, despite these fields being in the props array: so override operator == and hashCode manually in this case, or remove the iterable mixin.

Upvotes: 0

Matthew Trent
Matthew Trent

Reputation: 3274

For Equatable users...

If you use Equatable for your state classes (this package), ensure that you have overridden the props method on each of your state classes to return the specific fields on your object you want to be considered when checking object equality.

For example, we consider Results (one of my states) which extends UrlState (parent state class the Cubit/Bloc deals with) which in turn extends Equatable.

class Results extends UrlState {
  final String dataEntry;
  final List<String> jsonStats;
  Results(this.dataEntry, this.jsonStats);

  // NOT HAVING THIS METHOD OVERRIDDEN MEANS 2 `RESULT` OBJECTS
  // THAT HAVE DIFFERENT FIELD VALUES ARE CONSIDERED "THE SAME".
  //
  // THIS WILL CAUSE A LOT OF UNEXPECTED BEHAVIOUR WITH BLOC/CUBIT.
  @override
  List<Object?> get props => [dataEntry, jsonStats]; // We only consider two results to be the same if both these fields are the same for the 2 objects.
}

Upvotes: 1

Ricardo Mu&#241;oz
Ricardo Mu&#241;oz

Reputation: 51

I had the same issue and solved this problem changing:

yield status; 

to

yield status.toList();

At the end of my mapEventToState() method.

And probably you had to make for all yield that is passing a List.

If it worked for you let me know

Upvotes: 5

Dewey
Dewey

Reputation: 776

your state class is not equatable ... Dart can't tell when the state has changed

You must use: import 'package:equatable/equatable.dart';

I also don't think you should SET the state property on your bloc class. You should only yield it and let the bloc update it...

please check the docs as I could be wrong

Upvotes: 7

Tokenyet
Tokenyet

Reputation: 4291

Bloc will never change If the state didn't change, you might be confused that you assigned a state, but the fact is that Bloc is a Stream, you'll need to yield a state instead of assigning It directly.

Hopefully, you had implemented a copyWith, so you could do It as below:

yield state.copyWith(articles: articles)

For keeping your structure, you could still use:

// state = state.copyWith(articles: articles);
newState = state.copyWith(articles: articles);
...
yield newState;

Because state variable is used by Bloc, you must use another variable to prevent Bloc from comparing the same variable (state == state will always true, so the state never changed.)

Upvotes: 6

Related Questions