felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.77k stars 3.39k forks source link

BlocProvider bloc dispatches not showing up in transitions #131

Closed seklum closed 5 years ago

seklum commented 5 years ago

When using the bloc provider to give a bloc to a child widget any dispatch called on it does not show up in transitions or update the bloc builder in the parent widget.

It does function correctly in the background but I need it to automatically update the BlocBuilder in the parent widget when each event occurs.

felangel commented 5 years ago

Hi @seklum 👋

Thanks for opening an issue. Can you please provide an example app with the problem you're having? It's hard for me to give any suggestions without a concrete example. Thanks!

seklum commented 5 years ago

This code loads up your contacts to a list and the idea is to hold the state of which contacts are selected while you scroll through and tap names.

If you tap a name and then scroll so the item is rebuilt it shows that it is selected, proving the code is running without triggering the onTransition or BlocBuilder updating.

pubspec.yaml

dependencies:
  contacts_service: ^0.2.1
  permission_handler: ^2.2.0
  bloc: ^0.9.5
  flutter_bloc: ^0.7.1
  equatable: ^0.2.2

main.dart

import 'dart:math';

import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:contacts_service/contacts_service.dart';
import 'package:test_app/bloc/ContactBloc.dart';
import 'package:test_app/bloc/contact_event.dart';
import 'package:test_app/bloc/contact_state.dart';

class SimpleBlocDelegate extends BlocDelegate {
  @override
  void onTransition(Transition transition) {
    print(transition);
  }
}
void main() {
  BlocSupervisor().delegate = SimpleBlocDelegate();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      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: PeopleScreen(),
    );
  }
}

class ContactItem {
  ContactItem(this.cont, this.selected);

  Contact cont;
  bool selected = false;
}

class PeopleScreen extends StatefulWidget {
  PeopleScreen({Key key}) : super(key: key);

  @override
  _PeopleScreenState createState() => _PeopleScreenState();
}

class _PeopleScreenState extends State<PeopleScreen> {
  final ContactBloc _contactBloc = ContactBloc();

  Widget getContacts() {
    return BlocBuilder(
      bloc: _contactBloc,
      builder: (BuildContext context, ContactItemState state) {
        if (state is Uninitialized) {
          _contactBloc.dispatch(InitializeEvent());
          return Text('Loading');
        }
        if (state is ContactItemLoaded) {
          List<Widget> contactWidgets = List<Widget>();
          state.posts
              .forEach((cont) => contactWidgets.add(BlocProvider<ContactBloc>(
            bloc: _contactBloc,
            child: ContactWidget(cont),
          )));
          return ListView(
            shrinkWrap: true,
            padding: const EdgeInsets.all(20.0),
            children: contactWidgets,
          );
        }
      },
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      initialIndex: 0,
      child: Scaffold(
        appBar: AppBar(
          title: Text('Invite'),
          bottom: new TabBar(
            tabs: <Widget>[
              new Tab(
                text: "Contacts",
              ),
              new Tab(
                text: "Friends",
              ),
              new Tab(
                text: "Recents",
              ),
            ],
          ),
        ),
        body: new TabBarView(
          children: <Widget>[
            getContacts(),
            new Container(
              color: Colors.deepOrangeAccent,
              child: new Center(
                child: new Text(
                  "Second",
//                  style: textStyle(),
                ),
              ),
            ),
            new Container(
              color: Colors.deepOrangeAccent,
              child: new Center(
                child: new Text(
                  "Third",
//                  style: textStyle(),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class ContactWidget extends StatefulWidget {
  ContactWidget(this.contact, {Key key}) : super(key: key);
  final ContactItem contact;

  @override
  _ContactWidgetState createState() => _ContactWidgetState();
}

class _ContactWidgetState extends State<ContactWidget> {

  List<Color> colors = [
    Colors.green,
    Colors.amber,
    Colors.blue,
    Colors.amberAccent,
    Colors.blueAccent,
    Colors.deepOrange,
    Colors.greenAccent,
    Colors.orange,
    Colors.purpleAccent,
    Colors.yellow
  ];

  @override
  Widget build(BuildContext context) {
    ContactBloc _contactBloc = BlocProvider.of<ContactBloc>(context);

    Random random = Random();
    Color color = colors[random.nextInt(colors.length)];

    if (widget.contact.selected)
      return Container(
        decoration: BoxDecoration(
          border: Border.all(width: 1.0, color: Colors.green),
        ),
        child: ListTile(
          leading: Container(
            margin: const EdgeInsets.only(right: 16.0),
            child: CircleAvatar(
              backgroundColor: color,
              foregroundColor: Colors.white,
              child: new Text(widget.contact.cont.displayName[0]),
            ),
          ),
          title: Text(widget.contact.cont.displayName),
          onTap: () {
            _contactBloc.dispatch(SelectEvent(widget.contact.cont.displayName));
          },
          enabled: true,
        ),
      );
    return ListTile(
      leading: Container(
        margin: const EdgeInsets.only(right: 16.0),
        child: CircleAvatar(
          backgroundColor: color,
          foregroundColor: Colors.white,
          child: new Text(widget.contact.cont.displayName[0]),
        ),
      ),
      title: Text(widget.contact.cont.displayName),
      onTap: () {
        _contactBloc.dispatch(SelectEvent(widget.contact.cont.displayName));
      },
      enabled: true,
    );
  }
}

contact_event.dart

import 'package:equatable/equatable.dart';

abstract class ContactEvent extends Equatable {}

class InitializeEvent extends ContactEvent {
  @override
  String toString() => 'Initialize Event';
}

class SelectEvent extends ContactEvent {
  String displayName;

  SelectEvent(this.displayName);

  @override
  String toString() => 'Selected: $displayName';
}

contact_state.dart

import 'package:equatable/equatable.dart';
import 'package:test_app/main.dart';

abstract class ContactItemState extends Equatable {
  ContactItemState([List props = const []]) : super(props);
}

class Uninitialized extends ContactItemState {
  @override
  String toString() =>
      'Uninitialized';
}

class ContactItemLoaded extends ContactItemState {
  final List<ContactItem> posts;

  ContactItemLoaded({
    this.posts,
  }) : super(posts);

  ContactItemLoaded copyWith({
    List<ContactItem> posts,
  }) {
    return ContactItemLoaded(
      posts: posts ?? this.posts,
    );
  }

  @override
  String toString() =>
      'ContactItemLoaded { contacts: ${posts.length} }';
}

ContactBloc.dart

import 'package:bloc/bloc.dart';
import 'package:contacts_service/contacts_service.dart';

import 'package:permission_handler/permission_handler.dart';
import 'package:test_app/bloc/contact_event.dart';
import 'package:test_app/bloc/contact_state.dart';
import 'package:test_app/main.dart';

class ContactBloc extends Bloc<ContactEvent, ContactItemState> {
  @override
  // TODO: implement initialState
  ContactItemState get initialState => Uninitialized();

  @override
  Stream<ContactItemState> mapEventToState(
    ContactItemState currentState,
    ContactEvent event,
  ) async* {
    if(event is SelectEvent && currentState is ContactItemLoaded) {
      ContactItem item = currentState.posts.firstWhere((e) => e.cont.displayName == event.displayName);
      item.selected = !item.selected;
      yield currentState;
    }
    else if(currentState is Uninitialized){
      yield ContactItemLoaded(posts: await getContacts());
    }
    else
      yield ContactItemLoaded(posts: await getContacts());
  }

  Future<Iterable<Contact>> loadContacts() async {
    PermissionStatus permission = await PermissionHandler()
        .checkPermissionStatus(PermissionGroup.contacts);

    if (permission != PermissionStatus.granted) {
      Map<PermissionGroup, PermissionStatus> permissions =
      await PermissionHandler()
          .requestPermissions([PermissionGroup.contacts]);
      assert(permissions[PermissionGroup.contacts] == PermissionStatus.granted);
    }
    Iterable<Contact> cont = await ContactsService.getContacts();
    return cont;
  }

  Future<List<ContactItem>> getContacts() async {
    List<ContactItem> list = List<ContactItem>();
    Iterable<Contact> contacts = await loadContacts();
    contacts.toList().forEach((Contact cont) {
      if(cont.phones.length > 0)
        list.add(ContactItem(cont, false));
    });
    list.sort((contA, contB) => contA.cont.displayName.compareTo(contB.cont.displayName));

    return list;
  }
}
felangel commented 5 years ago

@seklum thanks for the code! 👍 I'll have a look in the next few hours.

felangel commented 5 years ago

@seklum There were several problems with the code you sent me. I have corrected them and you can find a working version of this here.

The main problem was in your bloc you were mutating currentState and yielding it. You cannot mutate your state; instead, you must always yield a new state otherwise bloc thinks nothing has changed and no transition will occur. Let me know if you have any other questions! 😄

leenorshn commented 4 years ago

Hello, I'm working on a firebase auth, when I try to login it pass successfully but in the console the transition show me that currentState uninitialized. this is my bloc`class AuthBloc extends Bloc<AuthEvent, AuthState> { final AuthRepository _authRepository = AuthRepository(auth: FirebaseAuth.instance, googleSignin: GoogleSignIn()); StreamSubscription _todosSubscription;

@override AuthState get initialState => Uninitialized();

@override Stream mapEventToState( AuthEvent event, ) async { if (event is AppStarted) { yield _mapAuthCurrentState();

}
if (event is AuthChanged) {
  if(event.user!=null){
    //yield Unauthenticated();
  //}else{
    yield*  _mapAuthChangedState(event);
  }

}

if (event is LoggedOut) {
  yield Unauthenticated();
  _authRepository.logout();
}

}

Stream _mapAuthChangedState(AuthChanged event) async* { yield Authenticated(event.user); }

Stream _mapAuthCurrentState() async* { _todosSubscription?.cancel(); _todosSubscription = _authRepository.onAuthStateChanged().listen( (auth) => add( AuthChanged(auth), ), ); }

@override Future close() { _todosSubscription?.cancel(); return super.close(); } } `

I would like to have currentState as Authenticated after login. please help me