Build app chat realtime với Flutter, Socket.io & NodeJS


1059

Bạn sẽ học được gì?

Sử dụng socket.io, NodeJS để tạo kết nối realtime giữa hai thiết bị

Khái niệm về room(phòng) và cách sử dụng chúng với socket.io, NodeJS

Một chút về thư viện model scope😅

Điều kiện tiên quyết

Phần sau sẽ được cài đặt và chạy trong PC của bạn.


Room app NodeJS

Room là cơ chế nhóm hợp lý có thể được sử dụng để chỉ nhắm mục tiêu đến socket cụ thể chứ không phải tất cả chúng.

Hãy hiểu nó trong những con số đơn giản:

Trước đây, một người dùng đang gửi một tin nhắn và máy chủ đã phát nó cho tất cả những người dùng khác.

Image for post
Before using room

Sau khi sử dụng room, máy chủ chỉ gửi tin nhắn đến room cụ thể.

Image for post
After using room

Bằng cách này, chúng ta có thể trò chuyện 1-1 bằng cách sử dụng phòng. Hãy bắt đầu nào.

NodeJS (Server-Side)

Tạo một dự án NodeJS mới và cài đặt các phụ thuộc sau:

  • express
  • http
  • nodemon
  • socket.io

Truy cập package.json và thêm script dev:

Image for post
dev script trong package.json

Tiếp theo trong tệp index.js, hãy thêm code sau:

const app = require('express')()
const http = require('http').createServer(app)
const io = require('socket.io')(http);
app.get('/', (req, res) => {
    res.send("Node Server is running. Yay!!")
})
io.on('connection', socket => {
    //Get the chatID of the user and join in a room of the same chatID
    chatID = socket.handshake.query.chatID
    socket.join(chatID)
    //Leave the room if the user closes the socket
    socket.on('disconnect', () => {
        socket.leave(chatID)
    })
    //Send message to only a particular user
    socket.on('send_message', message => {
        receiverChatID = message.receiverChatID
        senderChatID = message.senderChatID
        content = message.content
        //Send message to only that particular room
        socket.in(receiverChatID).emit('receive_message', {
            'content': content,
            'senderChatID': senderChatID,
            'receiverChatID':receiverChatID,
        })
    })
});
http.listen(process.env.PORT)

Mỗi người dùng được kết nối với máy chủ của chúng ta đều có một chatID cụ thể và client đó tham gia vào phòng với cùng một chatID. Vì vậy, nếu bất kỳ ai muốn gửi tin nhắn cho một người dùng cụ thể, anh ta sẽ nhắm mục tiêu chatID của họ.

Bây giờ code phía máy chủ của chúng ta đã hoàn thành, hãy deploy nó lên Heroku.

Tham khảo thêm Heroku tại đây Deploy app NodeJS với Heroku và Docker

Tạo một tệp mới trong thư mục gốc có tên Procfile và thêm dòng sau

web: node index.js

Tiếp theo, tạo một tệp có tên .gitignore và thêm dòng sau

/node_modules

Sau khi khởi tạo git và commit tất cả các code. Tiếp theo, tạo một ứng dụng heroku mới và đẩy code vào nhánh master. Nếu bạn gặp bất kỳ vấn đề nào, bạn có thể tham khảo tại đây. Sau khi deploy thành công, chúng ta sẽ nhận được url của máy chủ NodeJS

Flutter (Client-Side)

Bây giờ chương trình backend đã bắt đầu và đang chạy, đã đến lúc tạo ứng dụng flutter. Tạo một dự án mới và thêm các phụ thuộc sau vào tệp pubspec.yaml:

scoped_model: ^1.0.1
flutter_socket_io: ^0.6.0

Hãy tạo hai class dữ liệu Message.dart và User.dart.

class Message{
  final String text;
  final String senderID;
  final String receiverID;
  Message(this.text,this.senderID,this.receiverID);
}
class User{
  String name;
  String chatID;
  User(this.name,this.chatID);
}

Tiếp theo, tạo ChatModel.dart, nơi tất cả logic cho socket và tất cả dữ liệu sẽ được lưu trữ

import 'package:scoped_model/scoped_model.dart';
import 'package:flutter_socket_io/flutter_socket_io.dart';
import 'package:flutter_socket_io/socket_io_manager.dart';
import 'dart:convert';
import './User.dart';
import './Message.dart';
class ChatModel extends Model {
  List<User> users = [
    User('IronMan', '111'),
    User('Captain America', '222'),
    User('Antman', '333'),
    User('Hulk', '444'),
    User('Thor', '555'),
  ];
  User currentUser;
  List<User> friendList = List<User>();
  List<Message> messages = List<Message>();
  SocketIO socketIO;
  void init() {
    currentUser = users[0];
    friendList =
        users.where((user) => user.chatID != currentUser.chatID).toList();
    socketIO = SocketIOManager().createSocketIO(
        '<ENTER_YOUR_SERVER_URL_HERE>', '/',
        query: 'chatID=${currentUser.chatID}');
    socketIO.init();
    socketIO.subscribe('receive_message', (jsonData) {
      Map<String, dynamic> data = json.decode(jsonData);
      messages.add(Message(
          data['content'], data['senderChatID'], data['receiverChatID']));
      notifyListeners();
    });
    socketIO.connect();
  }
  void sendMessage(String text, String receiverChatID) {
    messages.add(Message(text, currentUser.chatID, receiverChatID));
    socketIO.sendMessage(
      'send_message',
      json.encode({
        'receiverChatID': receiverChatID,
        'senderChatID': currentUser.chatID,
        'content': text,
      }),
    );
    notifyListeners();
  }
  List<Message> getMessagesForChatID(String chatID) {
    return messages
        .where((msg) => msg.senderID == chatID || msg.receiverID == chatID)
        .toList();
  }
}

Class ChatModel chứa dữ liệu giả của tất cả người dùng, người dùng hiện tại và danh sách bạn bè của người dùng đó (là tất cả người dùng ngoại trừ người dùng hiện tại). Nó cũng chứa tất cả các tin nhắn. Có những method trong đó.

  • init() :Để khởi tạo tất cả các biến và cũng khởi tạo socket và thêm listeners vào socket.
  • sendMessage() : Gửi tin nhắn đến máy chủ và thêm nó vào danh sách tin nhắn.
  • getMessagesForChatID(): Trích xuất các tin nhắn từ danh sách tin nhắn có liên quan đến cuộc trò chuyện hiện tại.

Tiếp theo, hãy tạo AllChatsPage.dart. Đây sẽ là trang mà tất cả người dùng sẽ được hiển thị.

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import './ChatPage.dart';
import './User.dart';
import './ChatModel.dart';
class AllChatsPage extends StatefulWidget {
  @override
  _AllChatsPageState createState() => _AllChatsPageState();
}
class _AllChatsPageState extends State<AllChatsPage> {
  @override
  void initState() {
    super.initState();
    ScopedModel.of<ChatModel>(context, rebuildOnChange: false).init();
  }
  void friendClicked(User friend) {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (BuildContext context) {
          return ChatPage(friend);
        },
      ),
    );
  }
  Widget buildAllChatList() {
    return ScopedModelDescendant<ChatModel>(
      builder: (context, child, model) {
        return ListView.builder(
          itemCount: model.friendList.length,
          itemBuilder: (BuildContext context, int index) {
            User friend = model.friendList[index];
            return ListTile(
              title: Text(friend.name),
              onTap: () => friendClicked(friend),
            );
          },
        );
      },
    );
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('All Chats'),
      ),
      body: buildAllChatList(),
    );
  }
}

Init () trong ChatModel được gọi từ initState () của AllChatsPage, do đó khởi chạy ứng dụng NodeJS của chúng ta khi bắt đầu.

Tạo ChatPage.dart nơi các thông báo sẽ được hiển thị.

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import './User.dart';
import './Message.dart';
import './ChatModel.dart';
class ChatPage extends StatefulWidget {
  final User friend;
  ChatPage(this.friend);
  @override
  _ChatPageState createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
  final TextEditingController textEditingController = TextEditingController();
  Widget buildSingleMessage(Message message) {
    return Container(
      alignment: message.senderID == widget.friend.chatID
          ? Alignment.centerLeft
          : Alignment.centerRight,
      padding: EdgeInsets.all(10.0),
      margin: EdgeInsets.all(10.0),
      child: Text(message.text),
    );
  }
  Widget buildChatList() {
    return ScopedModelDescendant<ChatModel>(
      builder: (context, child, model) {
        List<Message> messages =
            model.getMessagesForChatID(widget.friend.chatID);
        return Container(
          height: MediaQuery.of(context).size.height * 0.75,
          child: ListView.builder(
            itemCount: messages.length,
            itemBuilder: (BuildContext context, int index) {
              return buildSingleMessage(messages[index]);
            },
          ),
        );
      },
    );
  }
  Widget buildChatArea() {
    return ScopedModelDescendant<ChatModel>(
      builder: (context, child, model) {
        return Container(
          child: Row(
            children: <Widget>[
              Container(
                width: MediaQuery.of(context).size.width * 0.8,
                child: TextField(
                  controller: textEditingController,
                ),
              ),
              SizedBox(width: 10.0),
              FloatingActionButton(
                onPressed: () {
                  model.sendMessage(
                      textEditingController.text, widget.friend.chatID);
                  textEditingController.text = '';
                },
                elevation: 0,
                child: Icon(Icons.send),
              ),
            ],
          ),
        );
      },
    );
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.friend.name),
      ),
      body: ListView(
        children: <Widget>[
          buildChatList(),
          buildChatArea(),
        ],
      ),
    );
  }
}

Cuối cùng mở main.dart và thay thế code hiện có bằng code sau:

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import './AllChatsPage.dart';
import './ChatModel.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModel(
      model: ChatModel(),
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        home: AllChatsPage(),
      ),
    );
  }
}

Làm thế nào để kiểm tra nó hoạt động?

Build và chạy ứng dụng trên một thiết bị. Sau đó, mở ChatModel.dart và trong methodinit (), khởi tạo currentUser với người khác. Ví dụ:

currentUser=users[1]

Rebuild và chạy ứng dụng trên một thiết bị khác. Bây giờ bạn có thể trò chuyện với người kia. 😄

Kết luận

Qua bài viết bạn đã có thể tạo app chat realtime với Socket.io, Futter và NodeJS. Hãy cùng theo dõi để xem thêm bài viết chất lượng nhé .

Tham khảo thêm về NodeJS tại đây: 10 cách để viết restful api NodeJS chuẩn cơm mẹ nấu luôn

Tham khảo thêm về React tại đây: Xây dựng app to-do-list đơn giản bằng React


Like it? Share with your friends!

1059