用 Flutter、Socket.io 和 Node.js 构建一对一实时聊天 app(下)
用 Flutter、Socket.io 和 Node.js 构建一对一实时聊天 app(上)

Flutter(客户端)

现在,我们的后端已经启动并运行,是时候创建 flutter app 了。

创建一个新的 flutter 项目,在 pubspec.yaml 文件中添加以下依赖项:

  • scoped_model: ^1.0.1
  • flutter_socket_io: ^0.6.0

创建两个数据类:Message.dart 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);
}

然后,创建 ChatModel.dart,里面会保存所有的 socket 逻辑和数据。

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();
  }
}

ChatModel 类不仅包含全部用户、当前用户和该用户的朋友列表的虚拟数据,还包含了所有消息。ChatModel 类中有下列三个方法:

  • init() :用来初始化所有变量 和 socket 并向 socket 添加监听器。
  • sendMessage() :向服务器发送消息并将服务器添加到消息列表中。
  • getMessagesForChatID() :从消息列表中提取与当前对话相关的消息。

接下来,创建 AllChatsPage.dart,该页面会显示所有用户。

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(),
    );
  }
}

ChatModel 中的 init() 是从 AllChatsPageinitState() 中调用的,会在启动时初始化 app。

然后,我们需创建 ChatPage.dart 来显示消息。

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(),
        ],
      ),
    );
  }
}

最后,打开 main.dart,用下列代码替换原有代码:

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(),
      ),
    );
  }
}

如何检查 app 能否正常运行?

在一台设备上安装并运行 app,然后打开 ChatModel.dart,在 init() 方法中用另一个用户初始化 currentUser,示例如下:

currentUser=users[1]

然后,在另一台设备上重建并运行该 app,就可以与该用户聊天啦!

下一步是什么?
为 app 构建一个酷炫的 UI,删除虚拟用户列表,将用户存储在数据库中。另外,为特定用户显示在线状态。

快速提示:查看特定房间中的用户数量。

如果你在操作中遇到任何问题,可以查看我的 github repo

如果你喜欢这篇文章,别忘了点赞收藏。

如有任何疑问,欢迎在评论区留言。

感谢大家的阅读!



原文作者:Ibtesam Ansari
原文链接:https://medium.com/flutter-community/realtime-chat-app-one-to-one-using-flutter-socket-io-node-js-acd4152c6a00


推荐阅读
相关专栏
开发者实践
186 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。