Ở bài viết trước, mình đã hướng dẫn các bạn cách xây dựng màn hình login cho dự án chat realtime. Ở bài viết lần này mình sẽ hướng dẫn các bạn cách tạo quản lý và kết nối từ client tới server.
Để có thể làm được phần này, các bạn hãy tham khảo bài viết khởi tạo dự án server của thầy Dũng để xây dựng sever cho ứng dụng chatbot của chúng ta.

Yêu cầu

Trong folder “Model” của dự án, tạo một file “socket_proxy.dart” đây sẽ là nơi lưu trữ file để kết nối đến sever. Ở file này sẽ xử lý, quản lý nhiều khía cạnh khác nhau của kết nối, bao gồm: thiết lập, xử lý sự kiện, gửi/nhận tin nhắn.

Tạo thêm một file “globals.dart”, đây sẽ là nơi chứa các biến toàn cục, sử dụng để lưu trữ thông tin cho dự án. Sẽ gồm có các thông tin: lưu trữ thông tin về các liên hệ, lưu trữ tin nhắn, lưu trữ các các gợi ý người dùng khác và thêm alert_dialog (hộp thoại cảnh báo).

Tạo file

globals.dart

List contacts = [];
List messages = [];
List suggestions = [];
bool alert_dialog = false;

Giải thích:

  • contacts: Đây là một danh sách (list) lưu trữ thông tin về các liên hệ. Ban đầu, danh sách này rỗng.
  • messages: Đây là một danh sách lưu trữ các tin nhắn. Ban đầu, danh sách này cũng rỗng.
  • suggestions: Đây là một danh sách lưu trữ các gợi ý (có thể là danh sách các tên người dùng hoặc các từ khóa gợi ý). Ban đầu, danh sách này cũng rỗng.
  • alert_dialog: Đây là một biến kiểu bool (Boolean) được sử dụng để lưu trữ trạng thái của hộp thoại cảnh báo (alert dialog). Giá trị mặc định của nó là false, có nghĩa là hộp thoại cảnh báo ban đầu chưa được hiển thị.

socket_proxy.dart

const ZONE_NAME = "freechat";
const APP_NAME = "freechat";

class SocketProxy {
  bool settedUp = false;
  late String username;
  late String password;
  late EzyClient _client;
  late Function(String)? _greetCallback;
  late Function(String)? _secureChatCallback;
  late Function? _disconnectedCallback;
  late Function? _connectionCallback;
  late Function? _connectionFailedCallback;
  late Function? _requestCallback;
  late Function? _loginErrorCallback;
  late Function(String)? _chatBotResponseCallback; // Callback cho ChatBot

  static final SocketProxy _INSTANCE = SocketProxy._();

  SocketProxy._();

  static SocketProxy getInstance() {
    return _INSTANCE;
  }

  void sendMessage(Map<String, dynamic> message) {
    // Ensure you have a connected client before sending a message
    final client = EzyClients.getInstance().getDefaultClient();
    if (client != null && client.zone != null) {
      var app = client.zone!.appManager.getAppByName("freechat");
      if (app != null) {
        app.send("6", message);
      }
    }
  }

  void _setup() {
    EzyConfig config = EzyConfig();
    config.clientName = ZONE_NAME;
    config.enableSSL =
        false; // SSL is not active by default using freechat server
    config.ping.maxLostPingCount = 3;
    config.ping.pingPeriod = 1000;
    config.reconnect.maxReconnectCount = 3;
    config.reconnect.reconnectPeriod = 1000;
    EzyClients clients = EzyClients.getInstance();
    _client = clients.newDefaultClient(config);
    _client.setup.addEventHandler(EzyEventType.DISCONNECTION,
        _DisconnectionHandler(_disconnectedCallback!));
    _client.setup.addEventHandler(EzyEventType.CONNECTION_SUCCESS,
        _ConnectionHandler(_connectionCallback!, _client));
    _client.setup.addEventHandler(EzyEventType.CONNECTION_FAILURE,
        _ConnectionFailureHandler(_connectionFailedCallback!, _client));
    _client.setup.addDataHandler(EzyCommand.HANDSHAKE, _HandshakeHandler());
    _client.setup.addDataHandler(EzyCommand.LOGIN, _LoginSuccessHandler());
    _client.setup
        .addDataHandler(EzyCommand.APP_ACCESS, _AppAccessHandler(_client));
    _client.setup.addDataHandler(
        EzyCommand.APP_REQUEST, _RequestHandler(_requestCallback!, _client));
    _client.setup.addDataHandler(
        EzyCommand.LOGIN_ERROR, _LoginErrorHandler(_loginErrorCallback!));
    var appSetup = _client.setup.setupApp(APP_NAME);

    // Xử lý dữ liệu cho câu hỏi chatbot
    appSetup.addDataHandler("4", _ChatBotQuestionHandler((question) {
      _chatBotResponseCallback!(question);
    }));
  }

  void connectToServer(String username, String password) {
    if (!settedUp) {
      settedUp = true;
      _setup();
    }
    this.username = username;
    this.password = password;
    // _client.connect("10.0.2.2",
    //     3005); // Android emulator localhost-10.0.2.2 for ios it may be 127.0.0.1
    _client.connect(
        "192.168.31.86", 3005); // computer is server and use your real phone
  }

  void disconnect() {
    _client.disconnect();
  }

  void onGreet(Function(String) callback) {
    _greetCallback = callback;
  }

  void onSecureChat(Function(String) callback) {
    _secureChatCallback = callback;
  }

  void onDisconnected(Function callback) {
    _disconnectedCallback = callback;
  }

  void onConnection(Function callback) {
    _connectionCallback = callback;
  }

  void onConnectionFailed(Function callback) {
    _connectionFailedCallback = callback;
  }

  void onContacts(Function callback) {
    _connectionFailedCallback = callback;
  }

  void onData(Function callback) {
    _requestCallback = callback;
  }

  void onLoginError(Function callback) {
    _loginErrorCallback = callback;
  }

  void sendMessageToChatBot(String message) {
    // Đảm bảo bạn có một ứng dụng và có thể gửi tin nhắn
    var app = EzyClients.getInstance()
        .getDefaultClient()
        .zone
        ?.appManager
        .getAppByName("freechat");
    if (app != null) {
      app.send("4", {"message": message});
    }
  }

  void onChatBotResponse(Function(String) callback) {
    _chatBotResponseCallback = callback;
  }
}

class _ChatBotQuestionHandler extends EzyAppDataHandler<Map> {
  late Function(String) _callback;

  _ChatBotQuestionHandler(Function(String) callback) {
    _callback = callback;
  }

  @override
  void handle(EzyApp app, Map data) {
    String question = data["question"] ?? "Không có câu hỏi nào từ máy chủ.";
    _callback(question);
  }
}

class _HandshakeHandler extends EzyHandshakeHandler {
  @override
  List getLoginRequest() {
    var request = [];
    request.add(ZONE_NAME);
    request.add(SocketProxy.getInstance().username);
    request.add(SocketProxy.getInstance().password);
    request.add([]);
    return request;
  }
}

class _LoginSuccessHandler extends EzyLoginSuccessHandler {
  @override
  void handleLoginSuccess(responseData) {
    client.send(EzyCommand.APP_ACCESS, [APP_NAME]);
  }
}

class _AppAccessHandler extends EzyAppAccessHandler {
  late EzyClient _client;

  _AppAccessHandler(EzyClient client) {
    _client = client;
  }

  @override
  void postHandle(EzyApp app, List data) {
    var _data = {};
    _data["limit"] = 50;
    _data["skip"] = 0;
    app.send("5", _data);
  }
}

class _GreetResponseHandler extends EzyAppDataHandler<Map> {
  late Function(String) _callback;

  _GreetResponseHandler(Function(String) callback) {
    _callback = callback;
  }

  @override
  void handle(EzyApp app, Map data) {
    _callback(data["message"]);
    app.send("secureChat", {"who": "Young Monkey"}, true);
  }
}

class _SecureChatResponseHandler extends EzyAppDataHandler<Map> {
  late Function(String) _callback;

  _SecureChatResponseHandler(Function(String) callback) {
    _callback = callback;
  }

  @override
  void handle(EzyApp app, Map data) {
    _callback(data["secure-message"]);
  }
}

class _DisconnectionHandler extends EzyDisconnectionHandler {
  late Function _callback;

  _DisconnectionHandler(Function callback) {
    _callback = callback;
  }

  @override
  void postHandle(Map event) {
    _callback();
  }
}

class _ConnectionFailureHandler extends EzyConnectionFailureHandler {
  late Function _callback;
  late EzyClient _client;

  _ConnectionFailureHandler(Function callback, EzyClient client) {
    _callback = callback;
    _client = client;
  }

  @override
  void onConnectionFailed(Map event) {
    _callback();
  }
}

class _ConnectionHandler extends EzyConnectionSuccessHandler {
  late Function _callback;
  late EzyClient _client;

  _ConnectionHandler(Function callback, EzyClient client) {
    _callback = callback;
    _client = client;
  }

  @override
  void handle(Map event) {
    sendHandshakeRequest();
    postHandle();
    _callback();
  }
}

class _LoginErrorHandler extends EzyAbstractDataHandler {
  late Function _callback;

  _LoginErrorHandler(Function callback) {
    _callback = callback;
  }

  @override
  void handle(List data) {
    client.disconnect();
    _callback();
  }
}

class _RequestHandler extends EzyAbstractDataHandler {
  late Function _callback;
  late EzyClient _client;

  _RequestHandler(Function callback, EzyClient client) {
    _callback = callback;
    _client = client;
  }

  @override
  handle(List data) {
    print('data sucess `${data}`');
    // Handle requests
    if (data[1][0] == '4') {
      // Get contacts
      messages = messages +
          [
            {'from': data[1][1]['from'], 'message': data[1][1]['message']}
          ];
    } else if (data[1][0] == '5') {
      // Get contacts
      contacts = data[1][1] + contacts;
    } else if (data[1][0] == '2') {
      // Add contact
      contacts = data[1][1] + contacts;
    } else if (data[1][0] == '6') {
      // User message
      messages = messages +
          [
            {'from': data[1][1]['from'], 'message': data[1][1]['message']}
          ];
      EzyApp? app = _client.getApp(); // Sử dụng getApp để lấy ứng dụng
      app?.send("getChatBotQuestion", {"message": data[1][1]['message']});
    } else if (data[1][0] == '1') {
      // Suggest Contacts
      suggestions = [];
      for (var element in data[1][1]['users']) {
        suggestions = suggestions + [element['username'].toString()];
      }
    } else if (data[1][0] == '10') {
      // Suggest Contacts

      suggestions = [];
      for (var element in data[1][1]['users']) {
        suggestions = suggestions + [element['username'].toString()];
      }
    }
    _callback();
  }
}

Giải thích:

  • Singleton SocketProxy:

    • SocketProxy là một lớp singleton, nghĩa là chỉ có một đối tượng của nó có thể tồn tại. Điều này đảm bảo việc quản lý kết nối socket nhất quán trong toàn bộ ứng dụng.
    • Đối tượng singleton này được truy cập thông qua phương thức SocketProxy.getInstance().
  • Thiết Lập Client:

    • _setup() là phương thức cấu hình EzyClient. Nó thiết lập các chi tiết kết nối, trình xử lý sự kiện (như kết nối thành công, thất bại, và ngắt kết nối), và trình xử lý dữ liệu cho các lệnh cụ thể như HANDSHAKE, LOGIN, APP_ACCESS, v.v.
    • Phương thức này chỉ được gọi một lần trong lần đầu tiên kết nối để đảm bảo client được thiết lập đúng cách.
  • Quản Lý Kết Nối:

    connectsToServer

    • connectToServer() được sử dụng để khởi tạo kết nối tới máy chủ với username và password đã cho.
    • disconnect() ngắt kết nối khỏi máy chủ.
  • Gửi Tin Nhắn:

    sendMessages

    • sendMessage() gửi một tin nhắn tới máy chủ sau khi đảm bảo rằng client đã kết nối.
    • sendMessageToChatBot() gửi một tin nhắn cụ thể đến chatbot trên máy chủ.
  • Callbacks (Hàm Gọi Lại):

    • Lớp này định nghĩa các callback khác nhau (_greetCallback, _secureChatCallback, v.v.) được kích hoạt khi các sự kiện cụ thể xảy ra, chẳng hạn khi nhận được phản hồi từ chatbot, hoặc khi trạng thái kết nối thay đổi.
  • Trình Xử Lý (Handlers):

phanhoiChatbot

_ChatBotQuestionHandler để xử lý các câu hỏi liên quan đến chatbot.
_HandshakeHandler, _LoginSuccessHandler, và _AppAccessHandler để xử lý xác thực với máy chủ và truy cập ứng dụng.
_RequestHandler để xử lý các yêu cầu dữ liệu chung và cập nhật trạng thái ứng dụng với các tin nhắn, danh bạ hoặc gợi ý nhận được.

  • Quản Lý Dữ Liệu:
    • Phương thức handle() trong _RequestHandler được sử dụng để quản lý nhiều loại dữ liệu khác nhau nhận được từ máy chủ. Nó cập nhật các biến toàn cục như contacts, messages, và suggestions dựa trên lệnh nhận được từ máy chủ.
  • Biến Toàn Cục:
    • Đoạn mã có tham chiếu đến các biến toàn cục như contacts, messages, và suggestions, những biến này được quản lý bên ngoài lớp này, có thể là trong file globals.dart.

Tổng kết

Vừa rồi chúng ta đã cùng nhau xây dựng các file để quản lý và kết nối từ client tới sever. Ở bài học sau, mình sẽ hướng dẫn các bạn cách xây dựng màn hình chat.
link github dự án: https://github.com/nguyendao2101/freechat (truy cập branch “new_main” để tham khảo dự án)