Flutter

[Flutter] 채팅앱 만들기 #10

상도동 카르마 2023. 3. 11. 19:54

What to do?

홈화면 UI 구현

데모영상

홈화면

Reference

 


구현해야 할 내용

 

1. 상단 탭바

 

2. 채팅방 List

 

3. 활동중인 유저


State 관리

 

상단 탭바에서 활동중인 유저 숫자를 보여줘야 함.

또한 활동중 유저 탭을 누르면 보여줄 유저 목록이 필요함.

이를 위해 홈화면에서 사용할 State를 정의

 

  • State
abstract class HomeState extends Equatable {}

class HomeInitial extends HomeState {
  @override
  List<Object> get props => [];
}

class HomeLoading extends HomeState {
  @override
  List<Object> get props => [];
}

class HomeSuccess extends HomeState {
  final List<User> activeUsers;

  HomeSuccess(this.activeUsers);

  @override
  List<Object> get props => [activeUsers];
}

 

  • Cubit
class HomeCubit extends Cubit<HomeState> {
  final IUserService _userService;

  HomeCubit(this._userService) : super(HomeInitial());

  Future<void> activeUsers() async {
    // satte를 로딩중으로 변경하기
    emit(HomeLoading());
    
    // 활동중인 users 가져오기
    final users = await _userService.online();
        
    // state를 성공으로 변경하기
    emit(HomeSuccess(users));
  }
}

 

  • compositonRoot
    • Home 위젯에서 위에서 정의한 Cubit을 사용할 수 있도록 함
class CompositionRoot {
  
  ...

  static Widget composeHomeUi() {
    HomeCubit homeCubit = HomeCubit(_userService);
    return MultiBlocProvider(
        providers: [BlocProvider(create: (BuildContext context) => homeCubit)],
        child: const Home());
  }
}

 

  • main.dart
    • 이전 코드에서는 홈화면을 회원가입화면으로 만들었는데, 이를 홈화면으로 수정함
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await CompositionRoot.configure();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: "Chat App",
        debugShowCheckedModeBanner: false,
        theme: lightTheme(context),
        darkTheme: darkTheme(context),
        
        // 기존코드 : home: CompositionRoot.composeOnBoardingUi());
       // 수정한 코드 ▽
        home: CompositionRoot.composeHomeUi());
  }

  const MyApp({key}) : super(key: key);
}

Widgets

 

홈화면에서 상단 탭바를 누르면 보여줄 ⓐ 채팅방 목록 ⓑ 활동중 유저 목록을 각각 위젯으로 만듬

 

  • 채팅방 List
    • 실제 데이터 베이스에서 채팅방 정보를 가져오는 로직은 작성하지 않음
    • 일단 3개의 가짜 채팅방 목록만 보이도록 함
class Chats extends StatefulWidget {
  const Chats();

  @override
  State<Chats> createState() => _ChatsState();
}

class _ChatsState extends State<Chats> {
  @override
  Widget build(BuildContext context) {
    return ListView.separated(
        itemBuilder: (_, idx) => _chatItem(),
        separatorBuilder: (_, __) => Divider(),
        // TODO : item 개수
        itemCount: 3);
  }

  _chatItem() => ListTile(
        /// profile image
        leading: const ProfileImage(
          // TODO : 프로필 이미지 가져오는 경로
          imageUrl: "https://picsum.photos/seed/picsum/200/300",
          isInternetConnected: true,
        ),

        /// 유저명
        title: Text(
          "유저명을 넣을 곳",
          style: Theme.of(context).textTheme.bodyMedium.copyWith(
              fontWeight: FontWeight.bold,
              color: isLightTheme(context) ? Colors.black : Colors.white),
        ),

        /// message
        subtitle: Text(
          "메세지를 넣을 곳",
          overflow: TextOverflow.ellipsis,
          softWrap: true,
          style: Theme.of(context).textTheme.bodySmall.copyWith(
              color: isLightTheme(context) ? Colors.black54 : Colors.white70),
        ),

        trailing: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.end,
            children: [
              /// 메세지 보낸 시간
              Text(
                "메세지 보낸 시간을 넣을 곳",
                style: Theme.of(context).textTheme.labelSmall.copyWith(
                    color: isLightTheme(context)
                        ? Colors.black54
                        : Colors.white70),
              ),

              /// 읽지 않은 메세지 수
              ClipRRect(
                borderRadius: BorderRadius.circular(50.0),
                child: Container(
                  height: 15.0,
                  width: 15.0,
                  color: kPrimary,
                  alignment: Alignment.center,
                  child: Text(
                    "3+",
                    style: Theme.of(context).textTheme.labelSmall.copyWith(
                        fontWeight: FontWeight.bold, color: Colors.white70),
                  ),
                ),
              )
            ],
          ),
        ),
      );
}

 

  • 활동중인 유저 List
    • state = HomeLoading → 로딩중 화면
    • state = HomeSuccess → 활동중인 유저 목록를 ListView로 렌더링
class ActiveUsers extends StatefulWidget {
  const ActiveUsers({Key key}) : super(key: key);

  @override
  State<ActiveUsers> createState() => _ActiveUsersState();
}

class _ActiveUsersState extends State<ActiveUsers> {
  @override
  Widget build(BuildContext context) => BlocBuilder<HomeCubit, HomeState>(
        builder: (_, state) {
          if (state is HomeLoading) return _loading();
          if (state is HomeSuccess) return _activeUserList(state.activeUsers);
          return Container();
        },
      );

  ListTile _activeUserItem(User user) => ListTile(
        leading: ProfileImage(
          imageUrl: user.photoUrl,
          isInternetConnected: true,
        ),
        title: Text(
          user.username,
          style: Theme.of(context)
              .textTheme
              .bodyMedium
              .copyWith(fontSize: 14.0, fontWeight: FontWeight.bold),
        ),
      );

  _activeUserList(List<User> users) => ListView.separated(
      itemBuilder: (BuildContext context, idx) => _activeUserItem(users[idx]),
      separatorBuilder: (_, __) => const Divider(),
      itemCount: users.length);

  Widget _loading() => const Center(
        child: CircularProgressIndicator(),
      );
}

홈화면

 

class Home extends StatefulWidget {
  const Home();

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  @override
  void initState() {
    super.initState();   
    /// 처음에 활동중 유저 목록 가져오기
    context.read<HomeCubit>().activeUsers();
  }

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
        length: 2,
        child: Scaffold(
          appBar: AppBar(
            /// 로그인 유저 프로필
            title: _loginUserProfile(),

            /// 탭바
            bottom: TabBar(
              indicatorPadding: const EdgeInsets.only(top: 10, bottom: 10),
              tabs: [
                _messageTab(),
                _activeUserTab(),
              ],
            ),
          ),
          
          /// 홈화면에서 보여줄 화면 (채팅방 목록, 활동 중 유저 목록)
          body: const TabBarView(
            children: [Chats(), ActiveUsers()],
          ),
        ));
  }

  /// 로그인한 유저의 프로필
  Widget _loginUserProfile() => Container(
        width: double.maxFinite,
        child: Row(
          children: [
            // TODO : 썸네일 이미지 주소
            const ProfileImage(
                imageUrl: "https://picsum.photos/seed/picsum/200/300"),
            Column(
              children: [
                Padding(
                  padding: const EdgeInsets.only(left: 12.0),
                  child: Text(
                    "TEST",
                    style: Theme.of(context)
                        .textTheme
                        .caption
                        .copyWith(fontSize: 14.0, fontWeight: FontWeight.bold),
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.only(left: 12.0),
                  child:
                      Text("TEST", style: Theme.of(context).textTheme.caption),
                ),
              ],
            )
          ],
        ),
      );

  /// 메세지 탭
  Tab _messageTab() => Tab(
        child: Container(
          decoration: BoxDecoration(borderRadius: BorderRadius.circular(50)),
          child: Align(
            alignment: Alignment.center,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Icon(Icons.message_rounded),
                Container(
                    margin: EdgeInsets.only(left: 10.0), child: Text("메세지"))
              ],
            ),
          ),
        ),
      );

  /// 활동중 유저 탭
  Tab _activeUserTab() => Tab(
        child: Container(
          decoration: BoxDecoration(borderRadius: BorderRadius.circular(50)),
          child: Align(
              alignment: Alignment.center,
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  Icon(Icons.people_outline_rounded),
                  BlocBuilder<HomeCubit, HomeState>(
                      builder: (_, state) => state is HomeSuccess
                          ? Text('활동 중 유저(${state.activeUsers.length})')
                          : Text('활동 중 유저(0)')),
                ],
              )),
        ),
      );
}