[Flutter] web_socket_channel 채팅 구현

GH lee·2023년 7월 4일
2

Dart/Flutter

목록 보기
4/12
post-thumbnail

web_socket_channel

플러터에서 웹소켓을 사용하기 위한 패키지
https://pub.dev/packages/web_socket_channel
이 패키지와 Node.js로 구현한 웹소켓 서버를 이용해 간단한 채팅을 구현해보았다.

https://curryyou.tistory.com/348

이 글을 참고하여 소켓서버를 구현하였다.

그리고 플러터에서 소켓 서버에 연결하려고 하는데

channel = WebSocketChannel.connect(
      Uri.parse('ws://localhost:portNumber/ws'),
    );

이렇게 localhost로 연결하니 에러가 발생했다.

찾아보니 localhost가 아니라 ip로 연결을 해야한다고 해서 클라우드 서버에 소켓서버를 띄우고 서버ip로 연결하니 잘 되었다.

https://github.com/dart-lang/web_socket_channel/issues/216#issuecomment-1339600162

웹소켓 사용법

플러터에서 웹소켓을 사용하는 방법은 공식문서에도 잘 나와있다.

https://docs.flutter.dev/cookbook/networking/web-sockets

공식문서를 참고하여 소켓채널에서 데이터를 실시간으로 받아오도록 처리했다.

StreamBuilder(
	stream: channel.stream,
    builder: (context, snapshot) {
      if (snapshot.hasData) {
      	chatList.insert(0, snapshot.data);	//*****
      }
      if (scrollController.hasClients) {
          scrollController.animateTo(
              0,
              duration: const Duration(milliseconds: 100),
              curve: Curves.linear,
          );
      }
      return Expanded(
      	child: ListView.separated(
        	controller: scrollController,
        	reverse: true,	//*****
        	itemBuilder: (context, index) {
        		return ChatBox(text: chatList[index]);
        	},
        	separatorBuilder: (context, index) {
        		return const SizedBox(
        			height: 12,
        		);
        	},
        	itemCount: chatList.length,
        	),
		);
	},
),

그리고 새로운 채팅이 올라오면 스크롤이 아래로 내려가도록 하기위해 여러 방법을 시도해 보았는데 데이터를 리스트에 반대로 담고, 리스트뷰에 reverse를 true로 주면 원하는 대로 구현이 되었다.

그리고 listview 안에 container들은 정렬이 먹히지 않으니 container를 unconstainedBox로 감싸줘야 적용이 된다.

return Align(
      alignment: Alignment.centerRight,
      child: UnconstrainedBox(
        child: Container(
          width: 300,
          padding: const EdgeInsets.only(
            top: 14,
            left: 20,
            bottom: 14,
            right: 20,
          ),
          decoration: const BoxDecoration(
            color: Color.fromRGBO(80, 146, 78, 1),
            borderRadius: BorderRadius.only(
              bottomLeft: Radius.circular(13),
              bottomRight: Radius.circular(3),
              topLeft: Radius.circular(13),
              topRight: Radius.circular(13),
            ),
          ),
          child: Text(
            text,
            style: const TextStyle(
              color: Colors.white,
              fontSize: 14,
            ),
          ),
        ),
      ),
    );

위젯 빌드 후 스크롤 이동

채팅방에 이미 대화 내용이 많이 쌓여있을 경우 채팅방에 들어가면 스크롤을 가장 아래로 이동시키기 위해 SchedulerBinding.instance.addPostFrameCallback을 이용한다.

SchedulerBinding.instance.addPostFrameCallback((_) {
  scrollController.animateTo(
    0,
    duration: const Duration(milliseconds: 100),
    curve: Curves.linear,
  );
});

전체코드

class WebsocketPage extends StatefulWidget {
  const WebsocketPage({super.key});

  
  State<WebsocketPage> createState() => _WebsocketPageState();
}

class _WebsocketPageState extends State<WebsocketPage> {
  late final WebSocketChannel channel;
  final chatList = <String>[];
  final scrollController = ScrollController();
  final textController = TextEditingController();

  
  void initState() {
    super.initState();
    channel = WebSocketChannel.connect(
      Uri.parse('ws://your_server_ip:server_port/ws'),
    );
  }

  
  Widget build(BuildContext context) {
    SchedulerBinding.instance.addPostFrameCallback((_) {
      scrollController.animateTo(
        0,
        duration: const Duration(milliseconds: 100),
        curve: Curves.linear,
      );
    });
    return DefaultLayout(
      title: 'Websocket Test',
      body: GestureDetector(
        onTap: () {
          FocusManager.instance.primaryFocus?.unfocus();
        },
        child: Column(
          children: [
            StreamBuilder(
              stream: channel.stream,
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  chatList.insert(0, snapshot.data);
                }
                if (scrollController.hasClients) {
                  scrollController.animateTo(
                    0,
                    duration: const Duration(milliseconds: 100),
                    curve: Curves.linear,
                  );
                }
                return Expanded(
                  child: ListView.separated(
                    controller: scrollController,
                    reverse: true,
                    itemBuilder: (context, index) {
                      return ChatBox(text: chatList[index]);
                    },
                    separatorBuilder: (context, index) {
                      return const SizedBox(
                        height: 12,
                      );
                    },
                    itemCount: chatList.length,
                  ),
                );
              },
            ),
            Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: textController,
                    onSubmitted: (value) {
                      if (value.isNotEmpty) {
                        channel.sink.add(value);
                        textController.text = '';
                      }
                    },
                  ),
                ),
                IconButton(
                  onPressed: () {
                    if (textController.text.isNotEmpty) {
                      channel.sink.add(textController.text);

                      textController.text = '';
                    }
                  },
                  icon: const Icon(
                    Icons.send,
                    color: Color.fromRGBO(80, 146, 78, 1),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class ChatBox extends StatelessWidget {
  final String text;

  const ChatBox({
    super.key,
    required this.text,
  });

  
  Widget build(BuildContext context) {
    return Align(
      alignment: Alignment.centerRight,
      child: UnconstrainedBox(
        child: Container(
          width: 300,
          padding: const EdgeInsets.only(
            top: 14,
            left: 20,
            bottom: 14,
            right: 20,
          ),
          decoration: const BoxDecoration(
            color: Color.fromRGBO(80, 146, 78, 1),
            borderRadius: BorderRadius.only(
              bottomLeft: Radius.circular(13),
              bottomRight: Radius.circular(3),
              topLeft: Radius.circular(13),
              topRight: Radius.circular(13),
            ),
          ),
          child: Text(
            text,
            style: const TextStyle(
              color: Colors.white,
              fontSize: 14,
            ),
          ),
        ),
      ),
    );
  }
}
profile
Flutter Junior

0개의 댓글