ㄹ이제 사진을 올린다던가 로그인 혹은 로그아웃 또는 댓글을 다는 형식의 백엔드적인 (백엔드라고 할 수 있을까...) 형태의 작업은 모두 끝났다. 이제 스크린을 구성해보자. 사실 스크린은 너무나도 간단한 부분이라서 어떠한 스크린들이 있고 이게 어떻게 연결이 되어 있는지 정도만 적어주고 넘어가도록 하겠다.
(나중에 내가 다시 보고 이해를 해야하니까...)
lib/screens/ 내에 생성되어야할 파일은 다음과 같다.
- add_post_screen.dart
- comment_card.dart
- comments_screen.dart
- feed_screen.dart
- profile_screen.dart
- search_screen.dart
- login_screen.dart
- signup_screen.dart
이전에 이미 login_screen.dart와 signup_screen.dart는 만들어 두었으니 그 외 나머지 부분에 대해서 이제 작성을 해보자.
처음 봤던 것 처럼 해당 인스타그램 클론은 아래 버튼이 총 5개로 구분이 되어있다.
첫번째는 홈
두번째는 서치
세번째는 업로드
네번째는 좋아요 저장 (기능 구현 예정)
다섯번째는 My
이다.
그럼 각각의 파일들을 매칭하자면,
홈 - feed_screen.dart /comment_card.dart / comments_screen.dart
서치 - search_screen.dart
업로드 - add_post_screen.dart
좋아요 저장 -
My - profile_screen.dart
이다.
홈에는 comment_card와 comments_screen이 있는데, 이는 댓글 기능 클릭시에 댓글을 보고 입력할 수 있는 별도의 스크린을 만들기 위한 것이다.
자 차례대로, 코드를 보자면 다음과 같다.
홈 - feed_screen.dart /comment_card.dart / comments_screen.dart
feed_screen.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:instagram_flutter/utils/colors.dart';
import 'package:instagram_flutter/utils/global_variables.dart';
import 'package:instagram_flutter/widgets/post_card.dart';
class FeedScreen extends StatefulWidget {
const FeedScreen({super.key});
@override
State<FeedScreen> createState() => _FeedScreenState();
}
class _FeedScreenState extends State<FeedScreen> {
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return Scaffold(
appBar: width > webScreenSize
? null
: AppBar(
backgroundColor: width > webScreenSize
? webBackgroundColor
: mobileBackgroundColor,
centerTitle: false,
title: SvgPicture.asset(
'assets/ic_instagram.svg',
color: primaryColor,
height: 32,
),
actions: [
IconButton(
onPressed: () {},
icon: const Icon(
Icons.messenger_outline,
),
),
],
),
body: StreamBuilder(
stream: FirebaseFirestore.instance.collection('posts').snapshots(),
builder: (context,
AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) => Container(
margin: EdgeInsets.symmetric(
horizontal: width > webScreenSize ? width * 0.3 : 0,
vertical: width > webScreenSize ? 15 : 0,
),
child: PostCard(
snap: snapshot.data!.docs[index].data(),
),
),
);
},
),
);
}
}
comment_card.dart : 각 댓글별 작성인 및 내용 확인용
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class CommentCard extends StatefulWidget {
final snap;
CommentCard({Key? key, required this.snap}) : super(key: key);
@override
State<CommentCard> createState() => _CommentCardState();
}
class _CommentCardState extends State<CommentCard> {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
vertical: 18,
horizontal: 16,
),
child: Row(
children: [
CircleAvatar(
backgroundImage: NetworkImage(
widget.snap['profilePic'],
),
radius: 18,
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: 16,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
RichText(
text: TextSpan(
children: [
TextSpan(
text: widget.snap['name'],
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: ' ${widget.snap['text']}',
),
],
),
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
DateFormat.yMMMd()
.format(widget.snap['datePublished'].toDate()),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
),
],
),
),
),
Container(
padding: const EdgeInsets.all(8),
child: const Icon(Icons.favorite, size: 16),
)
],
),
);
}
}
comments_screen.dart: 댓글 페이지용
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:instagram_flutter/models/user.dart';
import 'package:instagram_flutter/providers/user_provider.dart';
import 'package:instagram_flutter/resources/firestore_methods.dart';
import 'package:instagram_flutter/screens/comment_card.dart';
import 'package:instagram_flutter/utils/colors.dart';
import 'package:provider/provider.dart';
class CommentsScreen extends StatefulWidget {
final snap;
const CommentsScreen({Key? key, required this.snap}) : super(key: key);
@override
State<CommentsScreen> createState() => _CommentsScreenState();
}
class _CommentsScreenState extends State<CommentsScreen> {
final TextEditingController _commentController = TextEditingController();
@override
void dispose() {
// TODO: implement dispose
super.dispose();
_commentController.dispose();
}
@override
Widget build(BuildContext context) {
final User user = Provider.of<UserProvider>(context).getUser;
return Scaffold(
appBar: AppBar(
backgroundColor: mobileBackgroundColor,
title: const Text('comments'),
centerTitle: false,
),
body: StreamBuilder(
stream: FirebaseFirestore.instance
.collection('posts')
.doc(widget.snap['postId'])
.collection('comments')
.orderBy(
'datePublished',
descending: true,
)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: (snapshot.data! as dynamic).docs.length,
itemBuilder: (context, index) => CommentCard(
snap: (snapshot.data! as dynamic).docs[index].data(),
),
);
},
),
bottomNavigationBar: SafeArea(
child: Container(
height: kToolbarHeight,
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
padding: const EdgeInsets.only(left: 16, right: 8),
child: Row(
children: [
CircleAvatar(
backgroundImage: NetworkImage(
user.photoUrl,
),
radius: 18,
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 8),
child: TextField(
controller: _commentController,
decoration: InputDecoration(
hintText: 'Comment as ${user.username}',
border: InputBorder.none,
),
),
),
),
InkWell(
onTap: () async {
await FirestoreMethods().postComment(
widget.snap['postId'],
_commentController.text,
user.uid,
user.username,
user.photoUrl,
);
setState(() {
_commentController.text = "";
});
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: const Text(
'Post',
style: TextStyle(
color: blueColor,
),
),
),
)
],
),
),
),
);
}
}
자, 다음은 검색용
서치 - search_screen.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:instagram_flutter/screens/profile_screen.dart';
import 'package:instagram_flutter/utils/colors.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:instagram_flutter/utils/global_variables.dart';
class SearchScreen extends StatefulWidget {
const SearchScreen({Key? key}) : super(key: key);
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
final TextEditingController searchController = TextEditingController();
bool isShowUsers = false;
@override
void dispose() {
// TODO: implement dispose
super.dispose();
searchController.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: mobileBackgroundColor,
title: TextFormField(
controller: searchController,
decoration: const InputDecoration(
labelText: 'Search for a user',
),
onFieldSubmitted: (String _) {
// // print(_);
// print(searchController.text);
setState(() {
isShowUsers = true;
});
},
),
),
body: isShowUsers
? FutureBuilder(
future: FirebaseFirestore.instance
.collection('users')
.where(
'username',
isGreaterThanOrEqualTo: searchController.text,
)
.get(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: (snapshot.data! as dynamic).docs.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ProfileScreen(
uid: (snapshot.data! as dynamic).docs[index]['uid'],
),
),
),
child: ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(
(snapshot.data! as dynamic).docs[index]['photoUrl'],
),
),
title: Text(
(snapshot.data! as dynamic).docs[index]['username'],
),
),
);
},
);
},
)
: FutureBuilder(
future: FirebaseFirestore.instance.collection('posts').get(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
return StaggeredGridView.countBuilder(
crossAxisCount: 3,
itemCount: (snapshot.data! as dynamic).docs.length,
itemBuilder: (context, index) => Image.network(
(snapshot.data! as dynamic).docs[index]['postUrl']),
staggeredTileBuilder: (index) => MediaQuery.of(context)
.size
.width >
webScreenSize
? StaggeredTile.count(
(index % 7 == 0) ? 1 : 1, (index % 7 == 0) ? 1 : 1)
: StaggeredTile.count(
(index % 7 == 0) ? 2 : 1, (index % 7 == 0) ? 2 : 1),
mainAxisSpacing: 8,
crossAxisSpacing: 8,
);
},
),
);
}
}
다음은 업로드
업로드 - add_post_screen.dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:instagram_flutter/models/user.dart';
import 'package:instagram_flutter/providers/user_provider.dart';
import 'package:instagram_flutter/resources/firestore_methods.dart';
import 'package:instagram_flutter/utils/colors.dart';
import 'package:instagram_flutter/utils/utils.dart';
import 'package:provider/provider.dart';
class AddPostScreen extends StatefulWidget {
const AddPostScreen({super.key});
@override
State<AddPostScreen> createState() => _AddPostScreenState();
}
class _AddPostScreenState extends State<AddPostScreen> {
Uint8List? _file;
final TextEditingController _descriptionController = TextEditingController();
bool _isLoading = false;
void postImage(
String uid,
String username,
String profImage,
) async {
setState(() {
_isLoading = true;
});
try {
String res = await FirestoreMethods().uploadPost(
_descriptionController.text,
_file!,
uid,
username,
profImage,
);
if (res == "success") {
setState(() {
_isLoading = false;
});
showSnackBar('Posted', context);
clearImage();
} else {
setState(() {
_isLoading = false;
});
showSnackBar(res, context);
}
} catch (e) {
showSnackBar(e.toString(), context);
}
}
_selectImage(BuildContext context) async {
return showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: const Text('Create a Post'),
children: [
SimpleDialogOption(
padding: const EdgeInsets.all(20),
child: const Text('Take a photo'),
onPressed: () async {
Navigator.of(context).pop();
Uint8List file = await pickImage(
ImageSource.camera,
);
setState(() {
_file = file;
});
},
),
SimpleDialogOption(
padding: const EdgeInsets.all(20),
child: const Text('Choose from gallery'),
onPressed: () async {
Navigator.of(context).pop();
Uint8List file = await pickImage(
ImageSource.gallery,
);
setState(() {
_file = file;
});
},
),
SimpleDialogOption(
padding: const EdgeInsets.all(20),
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
)
],
);
});
}
void clearImage() {
setState(() {
_file = null;
});
}
@override
void displose() {
super.dispose();
_descriptionController.dispose();
}
@override
Widget build(BuildContext context) {
final User user = Provider.of<UserProvider>(context).getUser;
return _file == null
? Center(
child: IconButton(
icon: const Icon(Icons.upload),
onPressed: () => _selectImage(context),
),
)
: Scaffold(
appBar: AppBar(
backgroundColor: mobileBackgroundColor,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: clearImage,
),
title: const Text('Post to'),
centerTitle: false,
actions: [
TextButton(
onPressed: () => postImage(
user.uid,
user.username,
user.photoUrl,
),
child: const Text(
'Post',
style: TextStyle(
color: Colors.blueAccent,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
)
],
),
body: Column(
children: [
_isLoading
? const LinearProgressIndicator()
: Padding(
padding: EdgeInsets.only(top: 0),
),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
backgroundImage: NetworkImage(user.photoUrl),
),
SizedBox(
width: MediaQuery.of(context).size.width * 0.45,
child: TextField(
controller: _descriptionController,
decoration: const InputDecoration(
hintText: 'Write a caption',
border: InputBorder.none,
),
),
),
SizedBox(
height: 45,
width: 45,
child: AspectRatio(
aspectRatio: 487 / 451,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: MemoryImage(_file!),
fit: BoxFit.fill,
alignment: FractionalOffset.topCenter,
),
),
),
),
),
const Divider(),
],
)
],
),
);
}
}
마지막으로 내 정보창
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:instagram_flutter/resources/auth_methods.dart';
import 'package:instagram_flutter/resources/firestore_methods.dart';
import 'package:instagram_flutter/screens/login_screen.dart';
import 'package:instagram_flutter/utils/colors.dart';
import 'package:instagram_flutter/utils/utils.dart';
import 'package:instagram_flutter/widgets/follow_button.dart';
class ProfileScreen extends StatefulWidget {
final String uid;
const ProfileScreen({Key? key, required this.uid}) : super(key: key);
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
var userData = {};
int postLen = 0;
int followers = 0;
int following = 0;
bool isFollowing = false;
bool isLoading = false;
@override
void initState() {
// TODO: implement initState
super.initState();
getData();
}
getData() async {
setState(() {
isLoading = true;
});
try {
var userSnap = await FirebaseFirestore.instance
.collection('users')
.doc(widget.uid)
.get();
// get post LENGTH
var postSnap = await FirebaseFirestore.instance
.collection('posts')
.where('uid', isEqualTo: FirebaseAuth.instance.currentUser!.uid)
.get();
postLen = postSnap.docs.length;
userData = userSnap.data()!;
followers = userSnap.data()!['followers'].length;
following = userSnap.data()!['following'].length;
isFollowing = userSnap
.data()!['followers']
.contains(FirebaseAuth.instance.currentUser!.uid);
setState(() {});
} catch (e) {
showSnackBar(
e.toString(),
context,
);
}
setState(() {
isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return isLoading
? const Center(
child: CircularProgressIndicator(),
)
: Scaffold(
appBar: AppBar(
backgroundColor: mobileBackgroundColor,
title: Text(
userData['username'],
),
centerTitle: false,
),
body: ListView(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
CircleAvatar(
backgroundColor: Colors.grey,
backgroundImage: NetworkImage(
userData['photoUrl'],
),
radius: 40,
),
Expanded(
flex: 1,
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
buildStatColumn(postLen, "posts"),
buildStatColumn(followers, "followers"),
buildStatColumn(following, "following"),
],
),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
FirebaseAuth.instance.currentUser!.uid ==
widget.uid
? FollowButton(
text: 'Sign Out',
backgroundColor:
mobileBackgroundColor,
textColor: primaryColor,
borderColor: Colors.grey,
function: () async {
await AuthMethods().signOut();
Navigator.of(context)
.pushReplacement(
MaterialPageRoute(
builder: (context) =>
const LoginScreen(),
),
);
},
)
: isFollowing
? FollowButton(
text: 'Unfollow',
backgroundColor: Colors.white,
textColor: Colors.black,
borderColor: Colors.grey,
function: () async {
await FirestoreMethods()
.followUser(
FirebaseAuth.instance
.currentUser!.uid,
userData['uid'],
);
setState(() {
isFollowing = false;
followers--;
});
},
)
: FollowButton(
text: 'Follow',
backgroundColor: Colors.blue,
textColor: Colors.white,
borderColor: Colors.blue,
function: () async {
await FirestoreMethods()
.followUser(
FirebaseAuth.instance
.currentUser!.uid,
userData['uid'],
);
setState(() {
isFollowing = true;
followers++;
});
},
)
],
),
],
),
),
],
),
Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(
top: 15,
),
child: Text(
userData['username'],
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(
top: 1,
),
child: Text(
userData['bio'],
),
),
],
),
),
const Divider(),
FutureBuilder(
future: FirebaseFirestore.instance
.collection('posts')
.where('uid', isEqualTo: widget.uid)
.get(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
return GridView.builder(
shrinkWrap: true,
itemCount: (snapshot.data! as dynamic).docs.length,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 5,
mainAxisSpacing: 1.5,
childAspectRatio: 1,
),
itemBuilder: (context, index) {
DocumentSnapshot snap =
(snapshot.data! as dynamic).docs[index];
return Container(
child: Image(
image: NetworkImage(
snap['postUrl'],
),
fit: BoxFit.cover,
),
);
},
);
},
)
],
),
);
}
Column buildStatColumn(int num, String label) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
num.toString(),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Container(
margin: EdgeInsets.only(
top: 4,
),
child: Text(
label,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w400,
color: Colors.grey,
),
),
),
],
);
}
}
해당 스크린의 구성과 백엔드 등은 다음의 유튜브의 내용을 참고하면서 스스로 변형도 해보고 여러가지로 바꾸어보기도 해보았다. 물론 그러면서 실패를 하였던것이 많기에 되도록이면 여기서는 해당 프로그래머 (Rivaan Ranawat)님의 코드를 되도록 변경하지 않으면서 올려놓았다.
https://www.youtube.com/watch?v=BBccK1zTgxw
해당 영상을 참고 하면서 공부를 해보는 것도 좋다.
근데 지금까지 적고 보니, 너무 코드만 주루룩 적어놓은거 같아, 추가적으로 comment의 기능과 like의 기능등에 대해서 추가적인 설명을 하고 난 다음에 그 다음 기능 구현을 위한 코드진행으로 넘어가는게 좋을 것 같다.
또한, 해당 미현된 스크랩 기능이라던가 채팅기능 또한 한번 구현을 해보아야겠다. 이건 시간이 조금 더 걸릴 수도 있다.
여튼 졸린 와중에 글을 쓰다보니 글이 너무 주저리 주저리가 되었다.
다들 굿밤!
'Flutter' 카테고리의 다른 글
[Firebase] FCM을 알아보자 (0) | 2023.08.22 |
---|---|
[Flutter] Textfield를 futurebuilder로 감싸면? (0) | 2023.08.08 |
[Flutter] Instagram Clone Coding (인스타그램 클론코딩) 8 (main.dart) (0) | 2023.08.03 |
[Flutter] Instagram Clone Coding (인스타그램 클론코딩) 7 (image_picker, widgets) (0) | 2023.08.03 |
[Flutter] Instagram Clone Coding (인스타그램 클론코딩) 6 (resources) (0) | 2023.08.01 |