18일차에서는 스나이퍼 팩토리의 비밀듣는 고양이라는 백엔드 서버를 사용하여 비밀듣는 고양이 앱을 제작했다.
학습한 내용
- 비밀듣는 고양이 앱 제작
먼저 사용할 폰트를 다운로드하고, 프로젝트의 최상위 폴더에 assets > fonts
폴더를 만들어 파일을 저장한다.
다음으로 pubspec.yaml
파일에서 font:
라고 작성된 부분에 아래와 같이 작성한다.
fonts:
- family: 폰트명
fonts:
- asset: assets/fonts/폰트명-이탈릭.ttf
weight: 100
style: italic
해당 파일에서 weight
와 style
도 작성할 수 있다.
폰트는 family
속성에 작성한 이름으로 적용시킬 수 있다.
전체 앱에 적용시키고 싶다면 아래와 같이 MaterialApp
의 속성으로 작성하면 된다.
class MyApp extends StatelessWidget{
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
home: MyHome(),
theme: ThemeData(fontFamily: '적용할 폰트 이름'),
themeMode: ThemeMode.system,
);
}
}
별도로 지정하고 싶다면 위젯의 TextStyle
에서 fontFamily
속성에 설정할 수 있다.
스낵바는 화면 하단에 간단하게 메세지를 띄우는 것이다.
스낵바를 구현하기 위새서는 ScaffoldMessenger.of(context)
함수와 그 뒤에 ShowSnackBar()
를 사용하면 된다. 이 메소드는 반드시 Scaffold
의 위치를 참조한 뒤에 사용해야 한다.
- ScaffoldMessenger.of(context)의 의미
주어진 context에서 위로 올라가면서 가장 가까운 scaffold를 찾아서 반환
아래는 스낵바의 예시 코드이다.
TextButton(
child: Text(
'show me'
),
onPressed () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
context: Text('This is a SnackBar!')
)
);
},
),
- 비밀듣는 고양이 앱 제작
- 추가 사항
dependencies:
secret_cat_sdk: ^0.0.5+2
animate_do: ^3.0.2
assets:
- assets/images/piggy-bank.png
- assets/images/background_image.jpg
fonts:
- family: Neo
fonts:
- asset: assets/fonts/neo.ttf
pubspec.yaml
에 사용할 패키지 secret_cat_sdk
와 animate_do
를 설치하고 이미지 파일을 등록했다. 그리고 Neo 폰트를 사용하기 위해 설정해 주었다.
import 'package:flutter/material.dart';
import 'package:secret_app/page/home_page.dart';
void main() {
runApp(const SecretApp());
}
class SecretApp extends StatelessWidget {
const SecretApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(fontFamily: 'Neo'),
home: HomePage(),
);
}
}
main.dart
에서는 HomePage
위젯을 호출한다.
import 'package:flutter/material.dart';
import 'package:secret_app/page/author_page.dart';
import 'package:secret_app/page/secret_page.dart';
import 'package:secret_app/page/upload_page.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
//페이지 이동 라우트 생성
Route _createRoute(Widget page) {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
var begin = Offset(1.0, 0.0); //오른쪽 위 시작 지점
var end = Offset.zero; //왼쪽 위 끝 지점
var curve = Curves.easeIn;
var tween =
Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
);
}
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.green,
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.greenAccent,
shape: BoxShape.circle,
),
clipBehavior: Clip.antiAlias,
child: Image.asset('assets/images/piggy-bank.png'),
),
SizedBox(height: 8),
Text(
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 40,
color: Colors.white,
),
'비밀 저금통',
),
SizedBox(height: 48),
ListTile(
tileColor: Colors.white70,
title: Text('비밀 보기'),
subtitle: Text('놀러가기'),
trailing: Image.asset('assets/images/piggy-bank.png'),
onTap: () =>
Navigator.of(context).push(_createRoute(SecretPage())),
),
SizedBox(height: 24),
ListTile(
tileColor: Colors.white70,
title: Text('작성자들 보기'),
subtitle: Text('놀러가기'),
trailing: Image.asset('assets/images/piggy-bank.png'),
onTap: () =>
Navigator.of(context).push(_createRoute(AuthorPage())),
),
SizedBox(height: 24),
ListTile(
tileColor: Colors.white70,
title: Text('비밀 공유'),
subtitle: Text('놀러가기'),
trailing: Image.asset('assets/images/piggy-bank.png'),
onTap: () =>
Navigator.of(context).push(_createRoute(UploadPage())),
),
],
),
),
);
}
}
우선 HomePage
에서는 페이지 이동의 라우트를 생성할 _createRoute()
함수를 작성했는데 페이지 이동 시 페이지가 오른쪽에서 왼쪽으로 슬라이딩 되도록 애니메이션을 적용했다.
본문에서는 Column
으로 둥근 이미지와 제목 텍스트, 세개의 리스트 타일을 넣어 각 리스트 타일을 눌렀을때 맞는 페이지로 이동하도록 onTap
이벤트를 작성했다.
import 'package:flutter/material.dart';
import 'package:secret_cat_sdk/api/api.dart';
import 'package:animate_do/animate_do.dart';
class SecretPage extends StatefulWidget {
const SecretPage({super.key});
State<SecretPage> createState() => _SecretPageState();
}
class _SecretPageState extends State<SecretPage> {
late Future result; // 데이터 가져오기 결과
var pageController = PageController();
void initState() {
super.initState();
result = SecretCatApi.fetchSecrets();
}
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
title: Text('뒤로가기'),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Stack(
children: [
Image.asset(
height: double.infinity,
fit: BoxFit.cover,
'assets/images/background_image.jpg',
),
FutureBuilder(
future: result,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return PageView.builder(
controller: pageController,
itemCount: snapshot.data.length,
itemBuilder: ((context, index) {
return Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FadeInRight(
child: Image.asset(
width: 80,
height: 80,
'assets/images/piggy-bank.png',
),
),
SizedBox(height: 16),
SlideInRight(
child: Text(
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
snapshot.data[index].secret.toString(),
),
),
SizedBox(height: 16),
if (snapshot.data[index].author != null)
ElasticInRight(
child: Text(
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
snapshot.data[index].author.username
.toString()),
),
if (snapshot.data[index].author == null)
ElasticInRight(
child: Text(
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
'익명',
),
),
],
),
);
}),
);
}
return Center(child: CircularProgressIndicator());
},
),
],
),
);
}
}
SecretPage
는 비밀을 볼 수 있는 페이지로 initState()
에서 데이터를 가져와 result
에 저장했다.
Stack
위젯으로 본문에 배경 이미지를 넣고, 그 위에 비밀들을 출력할 수 있도록 FutureBuilder
를 사용했다. FutureBuider
의 future
에는 앞서 불러왔던 데이터인 result
를 연결 시키고, 각 비밀을 PageView
로 출력했다.
내부의 이미지와 글씨들은 animate_do
패키지를 사용하여 각각 다른 애니메이션을 적용해 보았다.
import 'package:animate_do/animate_do.dart';
import 'package:flutter/material.dart';
import 'package:secret_cat_sdk/api/api.dart';
class AuthorPage extends StatefulWidget {
const AuthorPage({super.key});
State<AuthorPage> createState() => _AuthorPageState();
}
class _AuthorPageState extends State<AuthorPage> {
late Future result;
void initState() {
super.initState();
result = SecretCatApi.fetchAuthors();
}
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
title: Text('뒤로가기'),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Stack(
children: [
Image.asset(
height: double.infinity,
fit: BoxFit.cover,
'assets/images/background_image.jpg',
),
FutureBuilder(
future: result,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemCount: snapshot.data.length,
itemBuilder: ((context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Expanded(
child: ZoomIn(
child: Image.network(snapshot.data[index].avatar),
),
),
SizedBox(height: 8),
Pulse(
child: Text(
style: TextStyle(color: Colors.white),
snapshot.data[index].name.toString(),
),
),
],
),
);
}),
);
}
return Center(child: CircularProgressIndicator());
},
),
],
),
);
}
}
해당 페이지는 글을 쓴 사람들을 볼 수 있는 페이지로 initState()
에서 데이터를 가져와 result
에 넣었다.
이번에도 Stack
위젯을 사용하여 배경 이미지를 넣고 그 위에 FutureBuilder
를 사용해 데이터를 출력해주었다.
글을 쓴 사람들은 GridView.builder
를 사용해 만들었다. 각 요소에는 animate_do
패키지의 다른 애니메이션을 적용해 보았다.
import 'package:flutter/material.dart';
import 'package:secret_cat_sdk/api/api.dart';
class UploadPage extends StatelessWidget {
UploadPage({super.key});
var textController = TextEditingController();
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
title: Text('뒤로가기'),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Stack(
children: [
Image.asset(
height: double.infinity,
fit: BoxFit.cover,
'assets/images/background_image.jpg',
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
TextField(
controller: textController,
maxLines: 6,
decoration: InputDecoration(
hintText: '비밀을 입력하세요',
fillColor: Colors.white12,
filled: true,
border: InputBorder.none,
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.green,
width: 2,
),
),
),
),
SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
onPressed: () async {
await SecretCatApi.addSecret(textController.text);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('비밀 공유 완료!'),
),
);
},
child: Text('비밀공유'),
),
),
],
),
),
],
),
);
}
}
업로드 페이지에서도 같은 방식으로 이미지 배경을 넣었고, 그 위에 TextField
와 ElevatedButton
을 만들었다.
TextField
에는 TextEditingController()
를 생성하여 연결해 주었다.
ElevatedButton
에는 onPressed
이벤트에 서textController.text
에 적힌 내용을 업데이트 시키는 작업을 수행했고, 작업이 끝난 뒤 비밀 공유 완료를 표시하는 스낵바를 띄워주었다.
과제로 개발을 진행하고, 강의를 수강한 뒤 아쉬웠던 점을 정리하고자 한다.
[Flutter] 스나이퍼팩토리 16일차에서 네트워크 데이터를 initState
에서 불러오는 이유를 작성한 적이 있는데 그 이유는 build
가 자주 불리게 되기 때문에 필요없는 API 요청을 막기 위해서 였다.
하지만 화면을 다시 그릴 필요 없는 Stateless
위젯은 build
를 다시 호출하지 않기 때문에 굳이 Stateful
위젯의 initState
를 사용하지 않고 FutureBuilder
의 future
에 바로 바인딩 해도 된다.
즉, 앞의 코드에서 SecretPage
와 AuthorPage
는 화면을 다시 그릴 필요가 없기 때문에 성능이 더 좋은 Stateless
로 만드는 것이 더 좋았을 것이다.
직접 작성한 코드에서는 Statk
위젯을 사용해 Image
로 배경 이미지를 넣었다. 하지만 결과물 예시를 자세히 보면 배경 이미지가 약간 어둡게 처리된 것을 알 수 있다.
Container
의 colorFilter
속성을 사용하면 배경을 어둡게 처리할 수 있다. 아래는 예시 코드이다.
Container(
alignment: Alignment.center,
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(backgroundImg),
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
Colors.black54,
BlendMode.darken,
),
),
),
...
animate_do
의 delay
속성을 사용하면 애니메이션이 적용되는 딜레이를 설정할 수 있다. 결과물 예시에서 AuthorPage
의 글쓴이 정보들은 하나씩 딜레이를 가지고 출력된다. 하지만 코드를 작성할 때 해결 방법을 찾지 못해 동시에 애니메이션을 적용했다. 여기에 delay
속성을 사용하면 되는데 각 정보들이 index
를 가지고 있으므로 이를 사용하여 딜레이를 다르게 적용할 수 있다. 아래의 코드는 예시 코드이다.
...
return GridView.builder(
itemCount: snapshot.data?.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemBuilder: (context, index) {
return ZoomIn(
delay: Duration(milliseconds: 200 * index),
child: Column(
children: [
CircleAvatar(
radius: 48,
backgroundImage:
NetworkImage(snapshot.data![index].avatar!),
),
SizedBox(
height: 8,
),
Text(
snapshot.data![index].username,
style: TextStyle(color: Colors.white),
),
],
),
);
},
);
...
해당 코드는 FutureBuilder
내에서 GridView
를 사용한 것인데 ZoomIn
애니메이션을 사용할 때 delay
를 Duration(millisecons: 200 *index)
로 설정해 각 요소에 딜레이를 다르게 설정한 것을 알 수 있다.
UploadPage
에서 데이터를 ElevatedButton
으로 업로드 할 때 TextEditingController
의 null
을 체크하지 않고 업로드를 하도록 작성했었다. 아래의 코드처럼 if문을 사용해 null
이 아닐 경우에만 업로드를 하도록 작성하는 것이 더 좋다.
...
ElevatedButton(
onPressed: () async {
if (controller.text != '') {
var secret = await SecretCatApi.addSecret(controller.text);
if (secret != null) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("비밀공유성공! ${secret.secret}")));
}
}
},
child: Text("업로드하기"),
)
...
또한 위의 코드에서 업로드를 한 뒤에 정상적으로 업로드가 되었으면 Navigator.pop(context)
를 사용해 뒤로 자동으로 이동하도록 기능을 추가하는 것도 좋은 방법이다.
이번 과제는 2시까지 제출을 해야해서 시간이 조금 모자라다...ㅠ 최대한 만들 수 있는 부분까지 해 봤는데 시간이 없어서 완벽하게 만들지는 못한 것 같다. 다음에 따로 좀 더 수정을 해야겠다. 그리고 일단은 2시까지 제출을 해야해서 추가 내용 정리는 없습니다. (나중에 추가 내용 정리를 작성한다면 플러터에 폰트 적용하는 법을 적을까 생각중...)