Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
Tags
- multiprovider
- Swift
- ListView.builder
- consumer
- transform
- changenotifierprovider
- controller
- Camera
- provider
- setstate
- runTransaction
- signout
- reference
- swift 문법
- Snapshot
- datetime
- 문법
- Firebase
- Navigator
- platformexception
- switch
- Stream
- globalkey
- snackbar
- borderRadius
- enum
- user
- permission
- divider
- changenotifier
Archives
- Today
- Total
코딩하는 제리
[Flutter/MiniProject] 채팅앱 UI 본문
소스코드 및 pubspec.yaml
// components/filled_outline_button.dart
import 'package:flutter/material.dart';
import '../constants.dart';
class FillOutlineButton extends StatelessWidget {
const FillOutlineButton({
Key key,
this.isFilled = true,
@required this.press,
@required this.text,
}) : super(key: key);
final bool isFilled;
final VoidCallback press;
final String text;
@override
Widget build(BuildContext context) {
return MaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
side: BorderSide(color: Colors.white),
),
elevation: isFilled ? 2 : 0,
color: isFilled ? Colors.white : Colors.transparent,
onPressed: press,
child: Text(
text,
style: TextStyle(
color: isFilled ? kContentColorLightTheme : Colors.white,
fontSize: 12,
),
),
);
}
}
// components/primary_button.dart
import 'package:flutter/material.dart';
import '../constants.dart';
class PrimaryButton extends StatelessWidget {
const PrimaryButton({
Key key,
@required this.text,
@required this.press,
this.color = kPrimaryColor,
this.padding = const EdgeInsets.all(kDefaultPadding * 0.75),
}) : super(key: key);
final String text;
final VoidCallback press;
final color;
final EdgeInsets padding;
@override
Widget build(BuildContext context) {
return MaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(40)),
),
padding: padding,
color: color,
minWidth: double.infinity,
onPressed: press,
child: Text(
text,
style: TextStyle(color: Colors.white),
),
);
}
}
// models/Chat.dart
class Chat {
final String name, lastMessage, image, time;
final bool isActive;
Chat({
this.name,
this.lastMessage,
this.image,
this.time,
this.isActive,
});
}
List chatsData = [
Chat(
name: "Jenny Wilson",
lastMessage: "Hope you are doing well...",
image: "assets/images/user.png",
time: "3m ago",
isActive: false,
),
Chat(
name: "Esther Howard",
lastMessage: "Hello Abdullah! I am...",
image: "assets/images/user_2.png",
time: "8m ago",
isActive: true,
),
Chat(
name: "Ralph Edwards",
lastMessage: "Do you have update...",
image: "assets/images/user_3.png",
time: "5d ago",
isActive: false,
),
Chat(
name: "Jacob Jones",
lastMessage: "You’re welcome :)",
image: "assets/images/user_4.png",
time: "5d ago",
isActive: true,
),
Chat(
name: "Albert Flores",
lastMessage: "Thanks",
image: "assets/images/user_5.png",
time: "6d ago",
isActive: false,
),
Chat(
name: "Jenny Wilson",
lastMessage: "Hope you are doing well...",
image: "assets/images/user.png",
time: "3m ago",
isActive: false,
),
Chat(
name: "Esther Howard",
lastMessage: "Hello Abdullah! I am...",
image: "assets/images/user_2.png",
time: "8m ago",
isActive: true,
),
Chat(
name: "Ralph Edwards",
lastMessage: "Do you have update...",
image: "assets/images/user_3.png",
time: "5d ago",
isActive: false,
),
];
// models/ChatMessage.dart
import 'package:flutter/material.dart';
enum ChatMessageType { text, audio, image, video }
enum MessageStatus { not_sent, not_view, viewed }
class ChatMessage {
final String text;
final ChatMessageType messageType;
final MessageStatus messageStatus;
final bool isSender;
ChatMessage({
this.text,
@required this.messageType,
@required this.messageStatus,
@required this.isSender,
});
}
List demeChatMessages = [
ChatMessage(
text: "Hi Sajol,",
messageType: ChatMessageType.text,
messageStatus: MessageStatus.viewed,
isSender: false,
),
ChatMessage(
text: "Hello, How are you?",
messageType: ChatMessageType.text,
messageStatus: MessageStatus.viewed,
isSender: true,
),
ChatMessage(
text: "",
messageType: ChatMessageType.audio,
messageStatus: MessageStatus.viewed,
isSender: false,
),
ChatMessage(
text: "",
messageType: ChatMessageType.video,
messageStatus: MessageStatus.viewed,
isSender: true,
),
ChatMessage(
text: "Error happend",
messageType: ChatMessageType.text,
messageStatus: MessageStatus.not_sent,
isSender: true,
),
ChatMessage(
text: "This looks great man!!",
messageType: ChatMessageType.text,
messageStatus: MessageStatus.viewed,
isSender: false,
),
ChatMessage(
text: "Glad you like it",
messageType: ChatMessageType.text,
messageStatus: MessageStatus.not_view,
isSender: true,
),
];
// main.dart
import 'package:flutter/material.dart';
import 'package:messaging_app_jerry/screens/welcome/welcome_screen.dart';
import 'package:messaging_app_jerry/theme.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: lightThemeData(context),
darkTheme: darkThemeData(context),
home: WelcomeScreen(),
);
}
}
// theme.dart
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'constants.dart';
ThemeData lightThemeData(BuildContext context) {
return ThemeData.light().copyWith(
primaryColor: kPrimaryColor,
scaffoldBackgroundColor: Colors.white,
appBarTheme: appBarTheme,
iconTheme: IconThemeData(color: kContentColorLightTheme),
textTheme: GoogleFonts.interTextTheme(Theme.of(context).textTheme)
.apply(bodyColor: kContentColorLightTheme),
colorScheme: ColorScheme.light(
primary: kPrimaryColor,
secondary: kSecondaryColor,
error: kErrorColor,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: Colors.white,
selectedItemColor: kContentColorLightTheme.withOpacity(0.7),
unselectedItemColor: kContentColorLightTheme.withOpacity(0.32),
selectedIconTheme: IconThemeData(color: kPrimaryColor),
),
);
}
ThemeData darkThemeData(BuildContext context) {
return ThemeData.dark().copyWith(
primaryColor: kPrimaryColor,
scaffoldBackgroundColor: kContentColorLightTheme,
appBarTheme: appBarTheme,
iconTheme: IconThemeData(color: kContentColorDarkTheme),
textTheme: GoogleFonts.interTextTheme(Theme.of(context).textTheme)
.apply(bodyColor: kContentColorDarkTheme),
colorScheme: ColorScheme.dark().copyWith(
primary: kPrimaryColor,
secondary: kSecondaryColor,
error: kErrorColor,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: kContentColorLightTheme,
selectedItemColor: Colors.white70,
unselectedItemColor: kContentColorDarkTheme.withOpacity(0.32),
selectedIconTheme: IconThemeData(color: kPrimaryColor),
),
);
}
final appBarTheme = AppBarTheme(centerTitle: false, elevation: 0);
// lib/screens/messages/components/audio_messge.dart
import 'package:flutter/material.dart';
import 'package:messaging_app_jerry/models/ChatMessage.dart';
import '../../../constants.dart';
class AudioMessage extends StatelessWidget {
final ChatMessage message;
const AudioMessage({Key key, this.message}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width * 0.55,
padding: EdgeInsets.symmetric(
horizontal: kDefaultPadding * 0.75,
vertical: kDefaultPadding / 2.5,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: kPrimaryColor.withOpacity(message.isSender ? 1 : 0.1),
),
child: Row(
children: [
Icon(
Icons.play_arrow,
color: message.isSender ? Colors.white : kPrimaryColor,
),
Expanded(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: kDefaultPadding / 2),
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Container(
width: double.infinity,
height: 2,
color: message.isSender
? Colors.white
: kPrimaryColor.withOpacity(0.4),
),
Positioned(
left: 0,
child: Container(
height: 8,
width: 8,
decoration: BoxDecoration(
color: message.isSender ? Colors.white : kPrimaryColor,
shape: BoxShape.circle,
),
),
)
],
),
),
),
Text(
'0.37',
style: TextStyle(
fontSize: 12,
color: message.isSender ? Colors.white : null,
),
)
],
),
);
}
}
// lib/screens/messages/components/body.dart
import 'package:flutter/material.dart';
import 'package:messaging_app_jerry/constants.dart';
import 'package:messaging_app_jerry/models/ChatMessage.dart';
import 'chat_input_field.dart';
import 'message.dart';
class Body extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: kDefaultPadding),
child: ListView.builder(
itemCount: demeChatMessages.length,
itemBuilder: (context, index) => Message(
message: demeChatMessages[index],
),
),
),
),
ChatInputField(),
],
);
}
}
// lib/screens/messages/components/chat_input_field.dart
import 'package:flutter/material.dart';
import '../../../constants.dart';
class ChatInputField extends StatelessWidget {
const ChatInputField({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(
horizontal: kDefaultPadding,
vertical: kDefaultPadding / 2,
),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
boxShadow: [
BoxShadow(
offset: Offset(0, 4),
blurRadius: 32,
color: Colors.green.withOpacity(0.08),
),
],
),
child: SafeArea(
child: Row(
children: [
Icon(Icons.mic, color: kPrimaryColor),
SizedBox(width: kDefaultPadding),
Expanded(
child: Container(
padding:
EdgeInsets.symmetric(horizontal: kDefaultPadding * 0.75),
height: 50,
decoration: BoxDecoration(
color: kPrimaryColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(40),
),
child: Row(
children: [
Icon(
Icons.sentiment_satisfied_outlined,
color: Theme.of(context)
.textTheme
.bodyText1
.color
.withOpacity(0.64),
),
SizedBox(width: kDefaultPadding / 4),
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Type message',
border: InputBorder.none,
),
),
),
Icon(
Icons.attach_file,
color: Theme.of(context)
.textTheme
.bodyText1
.color
.withOpacity(0.64),
),
SizedBox(width: kDefaultPadding / 4),
Icon(
Icons.camera_alt_outlined,
color: Theme.of(context)
.textTheme
.bodyText1
.color
.withOpacity(0.64),
),
],
),
),
),
],
),
),
);
}
}
// lib/screens/messages/components/message.dart
import 'package:flutter/material.dart';
import 'package:messaging_app_jerry/models/Chat.dart';
import 'package:messaging_app_jerry/models/ChatMessage.dart';
import '../../../constants.dart';
import 'audio_messge.dart';
import 'text_message.dart';
import 'video_message.dart';
class Message extends StatelessWidget {
final ChatMessage message;
const Message({
Key key,
@required this.message,
}) : super(key: key);
@override
Widget build(BuildContext context) {
Widget messageContaint(ChatMessage message) {
switch (message.messageType) {
case ChatMessageType.text:
return TextMessage(message: message);
break;
case ChatMessageType.audio:
return AudioMessage(message: message);
break;
case ChatMessageType.video:
return VideoMessage();
break;
default:
return SizedBox();
}
}
return Padding(
padding: const EdgeInsets.only(top: kDefaultPadding),
child: Row(
mainAxisAlignment:
message.isSender ? MainAxisAlignment.end : MainAxisAlignment.start,
children: [
if (!message.isSender) ...[
CircleAvatar(
radius: 12,
backgroundImage: AssetImage('assets/images/user_2.png'),
),
SizedBox(width: kDefaultPadding / 2),
],
messageContaint(message),
if (message.isSender)
MessageStatusDot(
status: message.messageStatus,
)
],
),
);
}
}
class MessageStatusDot extends StatelessWidget {
final MessageStatus status;
const MessageStatusDot({Key key, this.status}) : super(key: key);
@override
Widget build(BuildContext context) {
Color dotColor(MessageStatus status) {
switch(status) {
case MessageStatus.not_sent:
return kErrorColor;
break;
case MessageStatus.not_view:
return Theme.of(context).textTheme.bodyText1.color.withOpacity(0.1);
break;
case MessageStatus.viewed:
return kPrimaryColor;
break;
default:
return Colors.transparent;
}
}
return Container(
margin: EdgeInsets.only(left: kDefaultPadding / 2),
height: 12,
width: 12,
decoration: BoxDecoration(
color: dotColor(status),
shape: BoxShape.circle,
),
child: Icon(
status == MessageStatus.not_sent ? Icons.close : Icons.done,
size: 8,
color: Theme.of(context).scaffoldBackgroundColor,
),
);
}
}
// lib/screens/messages/components/text_message.dart
import 'package:flutter/material.dart';
import 'package:messaging_app_jerry/models/ChatMessage.dart';
import '../../../constants.dart';
class TextMessage extends StatelessWidget {
final ChatMessage message;
const TextMessage({
Key key,
this.message,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(
horizontal: kDefaultPadding * 0.75,
vertical: kDefaultPadding / 2,
),
decoration: BoxDecoration(
color: kPrimaryColor.withOpacity(message.isSender ? 1 : 0.1),
borderRadius: BorderRadius.circular(30),
),
child: Text(
message.text,
style: TextStyle(
color: message.isSender
? Colors.white
: Theme.of(context).textTheme.bodyText1.color),
),
);
}
}
// lib/screens/messages/components/video_message.dart
import 'package:flutter/material.dart';
import '../../../constants.dart';
class VideoMessage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.of(context).size.width * 0.45,
child: AspectRatio(
aspectRatio: 1.6,
child: Stack(
alignment: Alignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset('assets/images/Video Place Here.png'),
),
Container(
height: 25,
width: 25,
decoration: BoxDecoration(
color: kPrimaryColor,
shape: BoxShape.circle,
),
child: Icon(
Icons.play_arrow,
size: 16,
color: Colors.white,
),
),
],
),
),
);
}
}
// constants.dart
import 'package:flutter/material.dart';
const kPrimaryColor = Color(0xFF00BF6D);
const kSecondaryColor = Color(0xFFFE9901);
const kContentColorLightTheme = Color(0xFF1D1D35);
const kContentColorDarkTheme = Color(0xFFF5FCF9);
const kWarninngColor = Color(0xFFF3BB1C);
const kErrorColor = Color(0xFFF03738);
const kDefaultPadding = 20.0;
// welcome/welcome_screen.dart
import 'package:flutter/material.dart';
import 'package:messaging_app_jerry/constants.dart';
import 'package:messaging_app_jerry/screens/signinOrSignUp/signin_or_signup_screen.dart';
class WelcomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
Spacer(flex: 2),
Image.asset('assets/images/welcome_image.png'),
Spacer(flex: 3),
Text(
'Welcome to our freedom \nmessaging app',
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.headline5
.copyWith(fontWeight: FontWeight.bold),
),
Spacer(),
Text(
'Freedom talk any person of your \nmother language.',
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context)
.textTheme
.bodyText1
.color
.withOpacity(0.64),
),
),
Spacer(flex: 3),
FittedBox(
child: TextButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SigninOrSignupScreen(),
),
),
child: Row(
children: [
Text(
'Skip',
style: Theme.of(context).textTheme.bodyText1.copyWith(
color: Theme.of(context)
.textTheme
.bodyText1
.color
.withOpacity(0.8)),
),
SizedBox(width: kDefaultPadding / 4),
Icon(Icons.arrow_forward_ios,
size: 16,
color: Theme.of(context)
.textTheme
.bodyText1
.color
.withOpacity(0.8)),
],
),
),
),
],
),
),
);
}
}
// signInOrSignUp/signin_or_signup_screen.dart
import 'package:flutter/material.dart';
import 'package:messaging_app_jerry/components/primary_button.dart';
import 'package:messaging_app_jerry/constants.dart';
import 'package:messaging_app_jerry/screens/chat/chat_screen.dart';
class SignInOrSignUpScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: kDefaultPadding),
child: Column(
children: [
Spacer(flex: 2),
Image.asset(
MediaQuery.of(context).platformBrightness == Brightness.light
? 'assets/images/Logo_light.png'
: 'assets/images/Logo_dark.png',
height: 146,
),
Spacer(),
PrimaryButton(
text: 'Sing In',
press: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => ChatScreen()));
},
),
SizedBox(height: kDefaultPadding * 1.5),
PrimaryButton(
color: Theme.of(context).colorScheme.secondary,
text: 'Sing Up',
press: () {},
),
Spacer(flex: 3),
],
),
),
),
);
}
}
// screens/chat/chat_screen.dart
import 'package:flutter/material.dart';
import 'package:messaging_app_jerry/constants.dart';
import 'components/body.dart';
class ChatScreen extends StatefulWidget {
@override
_ChatScreenState createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: buildAppBar(),
body: Body(),
floatingActionButton: FloatingActionButton(
onPressed: () {},
backgroundColor: kPrimaryColor,
child: Icon(
Icons.person_add_alt_1,
color: Colors.white,
),
),
bottomNavigationBar: buildBottomNavigationBar(),
);
}
BottomNavigationBar buildBottomNavigationBar() {
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: _selectedIndex,
onTap: (value) {
setState(() {
_selectedIndex = value;
});
},
items: [
BottomNavigationBarItem(icon: Icon(Icons.messenger), label: 'Chats'),
BottomNavigationBarItem(icon: Icon(Icons.people), label: 'People'),
BottomNavigationBarItem(icon: Icon(Icons.call), label: 'Calls'),
BottomNavigationBarItem(
icon: CircleAvatar(
radius: 14,
backgroundImage: AssetImage('assets/images/user_2.png'),
),
label: 'Profile'),
],
);
}
AppBar buildAppBar() {
return AppBar(
title: Text('Chats'),
automaticallyImplyLeading: false,
actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {},
),
],
);
}
}
// screens/chat/components/body.dart
import 'package:flutter/material.dart';
import 'package:messaging_app_jerry/components/filled_outline_button.dart';
import 'package:messaging_app_jerry/constants.dart';
import 'package:messaging_app_jerry/models/Chat.dart';
import 'package:messaging_app_jerry/screens/messages/message_screen.dart';
import 'chat_card.dart';
class Body extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
padding: EdgeInsets.fromLTRB(
kDefaultPadding, 0, kDefaultPadding, kDefaultPadding),
color: kPrimaryColor,
child: Row(
children: [
FillOutlineButton(press: () {}, text: 'Recent Message'),
SizedBox(width: kDefaultPadding),
FillOutlineButton(
press: () {},
text: 'Active',
isFilled: false,
),
],
),
),
Expanded(
child: ListView.builder(
itemCount: chatsData.length,
itemBuilder: (context, index) => ChatCard(
chat: chatsData[index],
press: () => Navigator.push(context,
MaterialPageRoute(builder: (context) => MessagesScreen())),
),
),
),
],
);
}
}
// screens/chat/components/chat_cart.dart
import 'package:flutter/material.dart';
import 'package:messaging_app_jerry/models/Chat.dart';
import '../../../constants.dart';
class ChatCard extends StatelessWidget {
final Chat chat;
final VoidCallback press;
const ChatCard({
Key key,
@required this.chat,
@required this.press,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
onTap: press,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: kDefaultPadding,
vertical: kDefaultPadding * 0.75,
),
child: Row(
children: [
Stack(
children: [
CircleAvatar(
radius: 24,
backgroundImage: AssetImage(chat.image),
),
if (chat.isActive)
Positioned(
right: 0,
bottom: 0,
child: Container(
height: 16,
width: 16,
decoration: BoxDecoration(
color: kPrimaryColor,
shape: BoxShape.circle,
border: Border.all(
color: Theme.of(context).scaffoldBackgroundColor,
width: 3,
),
),
),
),
],
),
Expanded(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: kDefaultPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
chat.name,
style:
TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
SizedBox(height: 8),
Opacity(
opacity: 0.64,
child: Text(
chat.lastMessage,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
Opacity(
opacity: 0.64,
child: Text(chat.time),
),
],
),
),
);
}
}
// lib/screens/messages/message_screen.dart
import 'package:flutter/material.dart';
import 'package:messaging_app_jerry/constants.dart';
import 'components/body.dart';
class MessagesScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: buildAppBar(),
body: Body(),
);
}
AppBar buildAppBar() {
return AppBar(
automaticallyImplyLeading: false,
title: Row(
children: [
BackButton(),
CircleAvatar(
backgroundImage: AssetImage('assets/images/user_2.png'),
),
SizedBox(width: kDefaultPadding * 0.75),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Kristin Watson',
style: TextStyle(fontSize: 16),
),
Text(
'Active 3m ago',
style: TextStyle(fontSize: 12),
),
],
)
],
),
actions: [
IconButton(icon: Icon(Icons.local_phone), onPressed: () {}),
IconButton(icon: Icon(Icons.videocam), onPressed: () {}),
SizedBox(width: kDefaultPadding / 2)
],
);
}
}
'Flutter > MiniProject' 카테고리의 다른 글
[Flutter/MiniProject] 간단한 가계부 앱 (0) | 2021.03.26 |
---|---|
[Flutter/MiniProject] 퀴즈앱 UI (0) | 2021.03.23 |
[Flutter/MiniProject] 간단한 채팅앱 UI (0) | 2021.03.17 |
[Flutter/MiniProject] (구) Netflix Clone (0) | 2021.03.17 |
[Flutter/MiniProject] AuthenticationPage (0) | 2021.03.12 |
Comments