본문 바로가기

Flutter

[Flutter] 채팅앱 만들기 #4

What to do?

ReceiptService, TypingEventService를 만들고, Test 코드 작성하기

채팅서비스에서 다음의 두가지 기능을 구현하기 위한 서비스 코드를 작성

상대방이 내가 보낸 메세지를 읽었는지 여부 

ⓑ 상대방이 현재 타이핑을 하고 있는 여부

이를 위해 Receipt, TypingEvent 관련된 모델과 서비스코드, 테스트코드까지 작성하였다.

이전에 작성한 Message service코드와 거의 대부분이 유사하다


Refence

 


Receipt Model

 

메세지를 상대방이 읽었는지를 나타내기 위한 Entity

  • recipeint : 메세지를 받은 유저의 id
  • messageId : 메세지 아이디
  • status : 메세지 상태를 enum으로 정의
    • delivered
    • read : 상대방이 메세지를 읽음

 

/model/receipt_model.dart

enum ReceiptStatus { delivered, read }

extension EnumParsing on ReceiptStatus {
  String value() {
    return toString().split(".").last;
  }

  static ReceiptStatus fromString(String text) {
    return ReceiptStatus.values.firstWhere((element) => element.value() == text);
  }
}


class Receipt {
  String _id;

  String get id => _id;

  final String recipient;
  final String messageId;
  final ReceiptStatus status;
  final DateTime timestamp;

  Receipt(
      {@required this.recipient,
      @required this.messageId,
      @required this.status,
      @required this.timestamp});

  Map<String, dynamic> toJson() => {
        'recipient': recipient,
        'message_id': messageId,
        'status': status.value(),
        'timestamp': timestamp
      };

  factory Receipt.fromJson(Map<String, dynamic> json) {
    var receipt = Receipt(
        recipient: json['recipient'],
        messageId: json['message_id'],
        status: EnumParsing.fromString(json['status']),
        timestamp: json['timestamp']);
    receipt._id = json['id'];
    return receipt;
  }
}

Typing Event Model

 

유저가 현재 채팅을 입력하고 있는지 여부를 나타나는 Entity

  • from : 타이핑을 하고 있는 유저의 id
  • to : 타이핑 여부를 전달받을 유저의 id
  • evnet : 타이핑 여부(start, stop)를 enum으로 정의

 

/model/typing_event_model.dart

enum Typing { start, stop }

extension TypingParser on Typing {
  String value() {
    return toString().split(".").last;
  }

  static Typing fromString(String event) {
    return Typing.values.firstWhere((element) => element.value() == event);
  }
}

class TypingEvent {
  String get id => _id;
  String _id;
  final String from;
  final String to;
  final Typing event;

  TypingEvent({@required this.from, @required this.to, @required this.event});

  // class → json
  Map<String, dynamic> toJson() => {
        'from': from,
        'to': to,
        'event': event,
      };

  // json → class
  factory TypingEvent.fromJson(Map<String, dynamic> json) {
    final event = TypingEvent(
      from: json['from'],
      to: json['to'],
      event: json['event'],
    );
    event._id = json['id'];
    return event;
  }
}

ReceiptService

 

기존에 작성한 message service 코드와 거의 유사

 

/service/receipt/receipient_service_interface.dart

abstract class IReceiptService {
  Future<bool> send(Receipt receipt);

  Stream<Receipt> receipts(User user);

  void dispose();
}

 

/service/receipt/receipient_service.dart

 

class ReceiptService implements IReceiptService {
  final Rethinkdb _db;
  final Connection _connection;
  final _logger = Logger();

  final _controller = StreamController<Receipt>.broadcast();
  StreamSubscription _changeFeed;

  ReceiptService(this._db, this._connection);

  @override
  dispose() {
    _changeFeed?.cancel();
    _controller?.close();
  }

  @override
  Stream<Receipt> receipts(User user) {
    _startReceivingReceipts(user);
    return _controller.stream;
  }

  @override
  Future<bool> send(Receipt receipt) async {
    Map record =
        await _db.table('receipts').insert(receipt.toJson()).run(_connection);
    return record['inserted'] == 1;
  }

  _startReceivingReceipts(User user) {
    _changeFeed = _db
        .table('receipts')
        .filter({'recipient': user.id})
        .changes({'include_initial': true})
        .run(_connection)
        .asStream()
        .cast<Feed>()
        .listen((event) {
          event
              .forEach((feedData) {
                if (feedData['new_val'] == null) return;

                final receipt = _receiptFromFeed(feedData);
                _controller.sink.add(receipt);
                _removeDeliveredReceipt(receipt);
              })
              .catchError((err) => _logger.e(err))
              .onError((err, stackTrace) => _logger.e(err));
        });
  }

  Receipt _receiptFromFeed(feedData) => Receipt.fromJson(feedData['new_val']);

  _removeDeliveredReceipt(Receipt receipt) {
    _db
        .table('receipts')
        .get(receipt.id)
        .delete({'return_changes': false}).run(_connection);
  }
}

 

 


TypingNotificationService

 

기존에 작성한 message service 코드와 거의 유사

 

/service/typing_notification/typing_notification_service_interface.dart

abstract class ITypingNotificationService {
  Future<bool> send({@required TypingEvent event});

  Stream<TypingEvent> subscribe(User user, List<String> userIds);

  void dispose();
}

 

/service/typing_notification/typing_notification_service.dart

class TypingNotificationService implements ITypingNotificationService {
  final _logger = Logger();
  final Rethinkdb _db;
  final Connection _connection;
  final _controller = StreamController<TypingEvent>.broadcast();
  StreamSubscription _changeFeed;

  TypingNotificationService(this._db, this._connection);

  @override
  Future<bool> send(
      {@required TypingEvent event, @required User to}) async {
    if (to.active) return false;
    Map record = await _db
        .table('typing_events')
        .insert(event.toJson(), {'conflict': 'update'}).run(_connection);
    return record['inserted'] == 1;
  }

  @override
  Stream<TypingEvent> subscribe(User user, List<String> userIds) {
    _startReceivingTypingEvent(user, userIds);
    return _controller.stream;
  }

  @override
  void dispose() {
    _changeFeed?.cancel();
    _controller?.close();
  }

  _startReceivingTypingEvent(User user, List<String> userIds) {
    _changeFeed = _db
        .table('typing_events')
        .filter((event) {
          return event('to')
              .eq(user.id)
              .and(_db.expr(userIds).contains(event('from')));
        })
        .changes({'include_initial': true})
        .run(_connection)
        .asStream()
        .cast<Feed>()
        .listen((event) {
          event
              .forEach((feedData) {
                if (feedData['new_val'] == null) return;

                final typingEvent = _typingEventFromFeed(feedData);
                _controller.sink.add(typingEvent);
                _removeDeliveredTypingEvent(typingEvent);
              })
              // logging error
              .catchError((err) => _logger.e(err))
              .onError((err, stackTrace) => _logger.e(err));
        });
  }

  TypingEvent _typingEventFromFeed(feedData) =>
      TypingEvent.fromJson(feedData['new_val']);

  _removeDeliveredTypingEvent(TypingEvent event) {
    _db
        .table('typing_events')
        .get(event.id)
        .delete({'return_changes': false}).run(_connection);
  }
}

테스트 코드

 

util_for_test.dart

 

(기작성했던 코드를 수정함)

String databaseName = 'test';
List<String> tablesNames = ["users", "messages", "receipts", "typing_events"];

Future<void> createDatabase(Rethinkdb db, Connection connection) async {
  await db.dbCreate(databaseName).run(connection).catchError(print);
  for (String tn in tablesNames) {
    await db.tableCreate(tn).run(connection).catchError(print);
  }
}

Future<void> cleanDatabase(Rethinkdb db, Connection connection) async {
  for (String tn in tablesNames) {
    await db.table(tn).delete().run(connection);
  }
}

 

test/receipt_service_test.dart

void main() {
  Rethinkdb db = Rethinkdb();
  Connection connection;
  ReceiptService sut;

  setUp(() async {
    connection = await db.connect(host: '127.0.0.1', port: 28015);
    await createDatabase(db, connection);
    sut = ReceiptService(db, connection);
  });

  tearDown(() async {
    sut.dispose();
    await cleanDatabase(db, connection);
  });

  final user =
      User.fromJson({'id': '2', 'active': true, 'lastSeen': DateTime.now()});

  test('if receipt sent successfully, then return true', () async {
    Receipt receipt = Receipt(
        recipient: '444',
        messageId: '12',
        status: ReceiptStatus.delivered,
        timestamp: DateTime.now());
    final result = await sut.send(receipt);
    expect(result, true);
  });

  test('sending receipt works successfully', () async {
    sut.receipts(user).listen(expectAsync1((receipt) {
          expect(receipt.recipient, user.id);
        }, count: 2));
    Receipt receipt1 = Receipt(
        recipient: user.id,
        messageId: "1234",
        status: ReceiptStatus.delivered,
        timestamp: DateTime.now());
    Receipt receipt2 = Receipt(
        recipient: user.id,
        messageId: "12345",
        status: ReceiptStatus.delivered,
        timestamp: DateTime.now());

    await sut.send(receipt1);
    await sut.send(receipt2);
  });
}

 

도커 컨테이너 실행

docker run -d -p 8080:8080 -p 28015:28015 rethinkdb

 

테스트 코드 실행 결과, 잘 수행됨

테스트 코드 성공


 

'Flutter' 카테고리의 다른 글

[Flutter] 채팅앱 만들기 #6  (0) 2023.03.05
[Flutter] 채팅앱 만들기 #5  (0) 2023.03.04
[Flutter] 채팅앱 만들기 #3  (0) 2023.03.01
[Flutter] 채팅앱 만들기 #2  (0) 2023.03.01
[Flutter] 채팅앱 만들기 #1  (0) 2023.02.27