Search code examples
flutterblocflutter-blocflutter-streambuildertwilio-conversations

Flutter bloc chat is not refreshing after sending message


I'm new to bloc and trying to implement a chat with flutter_bloc package. The service for my messaging is twilio conversations api. My function works perfectly fine, Im simply not able to refresh my list of messages. Could somebody show me what I'm missing here? If I access the chatpage I can see all the messages, it only doesnt refresh if we have a new one.

I updated my code since I have a small success. Whenever User A or User B joins the chat, all messages are displayed. If I'm sending a message as User A, it will be visible in the UI for User A now and it is part of the conversation, BUT User B doesnt receive the new message which was added to the conversation without reloading. Which step is missing here so that the other User also receives the message? I just need help converting my code so I have a stream where the other participants of the chat can listen to so their conversation is refreshing too.

my chat_event.dart

  abstract class ChatEvent extends Equatable{
  const ChatEvent();

  @override
  List<Object> get props => [];
}

class InitialChatEvent extends ChatEvent {}

class AddMessage extends ChatEvent {
  final String messageToPost;

  AddMessage(this.messageToPost);
}

my chat_state.dart

   class ChatState extends Equatable {
  final Messages messages;

  const ChatState({required this.messages});

  factory ChatState.initial() =>  ChatState(messages: Messages(messages: []));

  @override
  List<Object> get props => [messages];

  @override
  bool get stringify => true;

  ChatState copyWith({
    List<Messages>? messages,
  }) {
    return ChatState(
      messages: this.messages,
    );
  }
}

part of chatpage

...
Expanded(
              child: BlocBuilder<ChatBloc, ChatState>(
                builder: (context, state) {
                  print('chatpage builder: ' + state.messages.toString());
                  return ListView.builder(
                      itemCount: state.messages.messages.length,
                      scrollDirection: Axis.vertical,
                      itemBuilder: (context, i) {
                        return ListTile(
                       tileColor: state.messages.messages[i].author.toString() == username ? Colors.amber : Colors.amber.shade100,
                      title: Text(
                        state.messages.messages[i].body.toString(),
                        style: TextStyle(color: Colors.black),
                      ),
                    );
                      });
                },
              ),
            ),
            ...
                  Container(
                      height: 50,
                      padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
                      child: RaisedButton(
                        textColor: Colors.white,
                        color: Colors.red,
                        child: Text('Button'),
                        onPressed: () async {
                          // print(chatMessage.text);
                          context.read<ChatBloc>().add(AddMessage(chatMessage.text));
                        },
                      )),
                ],
...

chat_bloc.dart

class ChatBloc extends Bloc<ChatEvent, ChatState> {
 
  ChatBloc() : super(ChatState.initial()) {
    //  print('wird ausgeführt');
    on<InitialChatEvent>((event, emit) async {
      final chatFeed = await HttpService().getMessages();
      emit(ChatState(messages: chatFeed));
    });
    
    on<AddMessage>((event, emit) async {
      final newConversation = await HttpService().postMessage(event.messageToPost);
      final chatFeed = await HttpService().getMessages();
      emit(ChatState(messages: chatFeed));
    });
  }
}

main.dart if needed

...

void main() => runApp(MultiBlocProvider(
        providers: [
          BlocProvider(create: (context) => ColorBloc()),
         BlocProvider(create: (context) => ChatBloc()),
        ],
        child: MaterialApp(
          title: "App",
          home: MyApp(),
        )));

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  // This widget is the root of your application.
  TextEditingController nameController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          ...
              child: RaisedButton(
                textColor: Colors.white,
                color: Colors.red,
                child: Text('Button'),
                onPressed: () {
              print(nameController.text);
              context.read<ChatBloc>().add(InitialChatEvent());
              Navigator.of(context).push(
                MaterialPageRoute(
                  builder: (context) => ChatPage(userText: nameController.text)
                ),
              );
            },
              )),
        ],
      ),
    );
  }
}

http_service.dart

  Future postMessage(String messageToPost) async {
    Map<String, String> _messageToPost = {
      'Author': 'User A',
      'Body': messageToPost,
    };

    try {
      // print(messageToPost);

      var response = await dio.post(
          "https://conversations.twilio.com/v1/Conversations/$sid/Messages",
          data: _messageToPost,
          options: Options(
            contentType: Headers.formUrlEncodedContentType,
            headers: <String, String>{'authorization': basicAuth},
          ));

      return messageToPost;
    } catch (e) {
      print('exception: ' + e.toString());
      Future.error(e.toString());
    }
  }

  Future getMessages() async {
    try {
      final response = await dio.get(
          "https://conversations.twilio.com/v1/Conversations/$sid/Messages",
          // data: _messageToPost,
          options: Options(
            // contentType: Headers.formUrlEncodedContentType,
            headers: <String, String>{'authorization': basicAuth},
          ));
print(response.data);
      final messageList = Messages.fromJson(response.data);
      print(messageList);
      return messageList;
    } catch (e) {
      print('exception: ' + e.toString());
      return Future.error(e.toString());
    }
  }
}

Solution

  • There is no short answer to this question, and if I have to write the full process here it's going to be too lengthy and redundant. Others have already written full tutorials on this like this fantastic one: https://blog.codemagic.io/flutter-ui-socket/. However, this might miss the BLoC part that you are interested in. So, below I will describe how you can use the example provided in this link and improve it with BLoC.

    You will need to create a ChatBloc and a Socket. I prefer to create a Socket Singletone to make sure I have only a single socket connection at all times throughout the lifecycle of my apps :D.

    Side Note
    I acknowledge that there might be other ways of doing this. Here I am explaining my way ;) I have revolved the below example based on the written tutorial linked above for simplicity ;)

    Chat BLoc

    Will contains the basic message events:

    • SendMessage
    • GetMessage
    • LoadHistory
    • UpdateConnectedUsers

    Here I will focus on the most sensitive one (i.e. Your concern). That is, GetMessage. The secret here is to call this event, not from the UI, but rather from the Socket Singletone. In other words, your socket will listen for incoming messages, and whenever it receives one, it triggers the 'GetMessage' event of the ChatBloc which, in turn, will update the UI. Here is an implementation attempt:

    Socket Singletone

    class SocketApi {
      IO.Socket _socket;
      //You need to inject an instance of the ChatBloc here so that you can 
      //call the 'GetMessage' event --- see comment below ;)
      ChatBloc chatBloc; 
    
      /// We are creating a singleton here to make sure we have only one instance 
      /// of Io.io at all time 
      static final SocketApi _socketApi = SocketApi._internal();
      SocketApi._internal() {
        _socket = IO.io(<url_to_your_socket_Server>, <String, dynamic>{
          'transports': ['websocket'],
          'autoConnect': false,
        });
    
        // This is where the magic happens... 
        // you listen of incoming messages 
        // on the socket and then trigger the chatBloc event 'GetMessage'. 
        _socket.on('message', (data) {
          // I am converting here to a chatMessage object... 
          // on your side you might have something like 'fromJson',
          // etc..
          ChatMessage chatMessage = chatMessageFromMap(data);
          chatBloc.add(GetMessage(chatMessage));
        });
      }
      //...
    }
    

    Now all that is left is to add a BlocBuilder or BlocSelector where ever you need your messages to be matched in your widget tree.

    Final Important Note If you are really serious about using BLoC, I will recommend you check on the Freezed package that goes hand-in-hand with the bloc library. It helps you build your models quickly and efficiently. I have nothing to do with the developers of this package and I gain nothing by advertising. It just makes my life easier and I absolutely love the way it improves my code quality and wanted to share <3

    Cheers and good luck!