개요
애플리케이션을 채팅 형태의 UI를 구성하고,
사용자와 상호작용하여 기능을 수행할 수 있도록 기본 뼈대를 만드는 것을 목표로 한다.
[ 시리즈 포스팅 내용 보기 ]
이번 포스팅에서는 소켓을 다루지 않는다.
하지만 채팅 앱을 구성하려면 사용자와 상호작용하는 UI가 필수적으로 필요하다.
때문에 소켓 서버에서의 송수신되는 메시지를 임의의 내부 리스트 변수로 가정해두고,
앱의 UI 레이아웃 구성과 사용자와의 기본적인 상호작용 기능 구현에 대해서 다루도록 하겠다.
포스팅에서 설명하는 프로젝트는 깃허브에서 다운로드할 수 있다.
GitHub - luvris2/flutter_chatting_app
Contribute to luvris2/flutter_chatting_app development by creating an account on GitHub.
github.com
필요한 기능
필수 기능
채팅 앱에 필수적으로 필요한 기능을 요약하면 다음과 같다.
1) 메시지 내용을 입력할 필드
- 내가 전달할 메시지를 작성할 수 있도록 상호작용할 수 있어야 한다.
2) 입력한 메시지 내용을 전송할 버튼
- 입력한 메시지를 다른 사용자에게 전달할 수 있어야 한다.
3) 메시지의 내용을 표시할 메시지 목록
- 본인 혹은 다른 사용자의 입력한 메시지를 확인 가능하여야 한다.
4) 메시지 목록 내 최신 메시지 확인
- 메시지 목록에서 스크롤이 생길 경우, 최신 메시지의 내용으로 스크롤이 이동되어야 한다.
모바일에서 필요한 기능
1) 키보드 없애기
- 사용자가 직접 뒤로가기를 누르지 않고 다른 영역을 터치하면 가상 키보드가 없어져야 한다.
UI 레이아웃 설계
레이아웃은 두 영역으로 나뉜다.
- 메시지를 표시할 영역
- 메시지를 입력하고 전송하는 영역
[메시지 표시 영역]
- ListView : 여러 개의 메시지를 표시하기 위한 메시지 리스트를 보여줄 위젯
- Stack : 메시지와 다른 정보를 함께 표시하기 위해 사용할 위젯, 선호도에 따라 다른 위젯 선택 가능
- Text : 사용자의 정보를 표시하기 위한 위젯
- Card > Text : 사용자의 메시지를 표시하기 위한 위젯
- Stack : 메시지와 다른 정보를 함께 표시하기 위해 사용할 위젯, 선호도에 따라 다른 위젯 선택 가능
[메시지 입력 및 전송 영역]
- Row : 사용자의 메시지 입력 폼과 전송 버튼을 수평으로 나란히 보여주기 위한 위젯
- TextField : 사용자가 메시지를 작성할 수 있도록 입력 폼을 제공하는 위젯
- IconButton : 사용자가 작성한 메시지를 서버에 전송할 수 있도록 상호작용하는 버튼 위젯
구조 설계
[ 파일 구조 ]
앱 실행을 위한 메인 함수와 UI 및 기능을 제공하는 세 개의 클래스로 구성한다.
- 메인 함수 : MaterialApp 하위의 Scaffold 로 구성
- ChatPage Class : 메인함수의 Scaffold의 body 속성에 매칭될 클래스, 사실상 메인 페이지를 의미
- ChatArea Class : ChatPage 하위 자식, 메시지를 표시할 리스트뷰를 배치할 클래스
- InputTextArea Class : ChatPage 하위 자식, 메시지 작성을 위한 폼과 전송 버튼을 배치할 클래스
- ChatPage Class : 메인함수의 Scaffold의 body 속성에 매칭될 클래스, 사실상 메인 페이지를 의미
[ 상태관리 참고사항 ]
간단한 앱이기 때문에 상태 관리 패키지를 따로 사용하지 않고 setState를 이용하여 구성한다.
[ 메인 실행 함수 ]
터치를 감지하여 메시지 입력을 위한 가상 키보드를 없앨 수 있도록 GestureDetector 를 최상위로 감싼다.
- 가상 키보드는 입력 폼에 포커스가 존재하면 생기므로, 포커스를 제거하면 된다.
- 포커스는 FocusManager를 사용하여 포커스를 제어한다.
- 포커스 제거 코드 : FocusManager.instance.primaryFocus?.unfucous();
- 포커스가 있다면 포커스 제거 수행
기본적으로 MeterialApp > Scaffold 순으로 위젯을 감싸 메인 페이지를 body 속성에 넣는다.
[ 메인 페이지 (ChatPage) ]
리스트 타입 변수
- 메시지 내용 저장을 목적으로 함
- 각 하위 자식 클래스에 값(메시지)을 공유하기 위함
- 소켓 서버에서 송수신할 데이터(메시지)를 저장할 변수
상태관리를 위한 함수
- 상태관리 패키지 대신 setState를 구성하여 상태를 관리할 함수
- 리스트 타입 변수의 값을 업데이트 하기 위함
- 즉, 메시지가 추가되면 메시지 리스트를 업데이트 함
역할
- 앱 화면에 보여질 레이아웃
- 하위 자식 클래스를 구성하여 각각의 레이아웃을 배치 할 수 있도록 함
- 메시지(리스트 타입 변수)의 값을 각 하위 자식의 클래스와 공유
- 값의 변화가 있을 때 상태 업데이트 수행
[ 메시지 표시 영역 (ChatArea) ]
생성자 필요 파라미터
- 리스트 타입 변수 : 메시지 내용을 전달받기 위함
컨트롤러
- 스크롤 컨트롤러 : 메시지가 추가 될 경우, 제일 아래의 메시지 내용 확인을 위해 스크롤을 가장 아래로 이동하기 위함
역할
- 생성자로 전달 받은 파라미터의 값을 표시하는 영역
- 리스트 타입 변수의 값 (메시지 내용)
- 생성자로 전달 받은 파라미터 값은 다른 클래스와 상태를 공유하기 때문에 변경된 값 확인 및 리렌더링 수행
[ 메시지 입력 및 전송 영역 (InputTextArea) ]
생성자 필요 파라미터
- 값 업데이트(setState) 함수 : 리스트 타입 변수(메시지 내용)의 값을 업데이트하기 위함
컨트롤러
- 텍스트 에디팅 컨트롤러 : 사용자가 입력한 텍스트를 제어하기 위함
역할
- 사용자가 메시지를 작성하고 서버로 메시지를 전송하기 위한 영역
- 텍스트필드에 사용자가 입력한 텍스트를 컨트롤러를 이용하여 입력 내용 확인
- 버튼을 이용하여 입력한 텍스트를 값 업데이트 함수를 통해 리스트 타입 변수의 값 업데이트 요청
- 값 업데이트는 메인 페이지인 ChatPage에서 수행하며, 업데이트된 값을 다른 클래스와 공유
코딩
- 메인 함수
void main() async {
runApp(GestureDetector(
onTap: () {
FocusManager.instance.primaryFocus?.unfocus();
},
child: const MaterialApp(
home: Scaffold(
body: ChatPage(),
),
),
));
}
- 메인 함수와 연결될 ChatPage 클래스
class ChatPage extends StatefulWidget {
const ChatPage({super.key});
@override
State<ChatPage> createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
// 메시지 내용을 저장할 리스트
List messageList = [];
// 메시지 내용 추가
void setStateMessage(message) {
setState(() => messageList.add(message));
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
// 메시지 표시 영역
ChatArea(
messageList: messageList,
),
// 메시지 입력 영역
InputTextArea(
updateMessag: setStateMessage,
)
],
);
}
}
- ChatPage의 레이아웃을 구성할 메시지 표시 영역 (ChatArea Class)
class ChatArea extends StatefulWidget {
final List messageList;
const ChatArea({super.key, required this.messageList});
@override
State<ChatArea> createState() => _ChatAreaState();
}
class _ChatAreaState extends State<ChatArea> {
// 스크롤을 제어하기 위한 스크롤 컨트롤러
ScrollController scrollController = ScrollController();
@override
Widget build(BuildContext context) {
// 리스트뷰에 메시지가 추가되면 스크롤을 가장 아래로 이동
WidgetsBinding.instance.addPostFrameCallback((_) {
scrollController.animateTo(scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 500), curve: Curves.ease);
});
return Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.builder(
controller: scrollController,
itemCount: widget.messageList.length,
itemBuilder: (BuildContext context, int index) {
return Stack(
children: [
// 유저 아이디
const Text('tester'),
Card(
margin: const EdgeInsets.fromLTRB(0, 20, 0, 10),
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
// 메시지 내용
child: Text(widget.messageList[index]),
),
),
],
);
},
),
),
);
}
}
- ChatPage의 레이아웃을 구성할 메시지 작성 및 전송 영역 (InputTextArea Class)
class InputTextArea extends StatefulWidget {
final Function updateMessag;
const InputTextArea({super.key, required this.updateMessag});
@override
State<InputTextArea> createState() => _InputTextAreaState();
}
class _InputTextAreaState extends State<InputTextArea> {
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
Expanded(
// 메시지 입력 필드
child: TextField(
controller: _controller,
decoration:
const InputDecoration(labelText: 'Enter your message'),
),
),
// 입력 메시지 전송 버튼
IconButton(
icon: const Icon(Icons.send),
onPressed: () {
if (_controller.text.isNotEmpty) {
widget.updateMessag(_controller.text);
_controller.clear();
}
},
),
],
),
);
}
}