제공되는 패키지 secrets_cat_sdk를 활용하여 다음의 기대 결과물을 따라 만드세요.
supported font formats
.ttc
.ttf
.otf
fonts:
- family: Neo
fonts:
- asset: assets/fonts/neo.ttf
style: italic
weight: 700
return MaterialApp(
theme: ThemeData(fontFamily: 'Neo'),
home: const MyHomePage(),
);
child: Text(
'Roboto Mono sample',
style: TextStyle(fontFamily: 'RobotoMono'),
),
제일 처음 뜨는 페이지.
페이지 이동하는 버튼이 세 개 있어서 빌더 위젯을 따로 분리해 메인 페이지에서 렌더링하도록 했다.
import 'package:assignment2/builder/NavBtns.dart';
import 'package:assignment2/widgets/Background.dart';
import 'package:flutter/material.dart';
class MainPage extends StatefulWidget {
const MainPage({super.key});
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
Widget build(BuildContext context) {
return Background(
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text(
'Shit.. 비밀이야',
style: TextStyle(
fontSize: 36,
),
),
Image.asset(
'assets/imgs/fashion.png',
width: 300,
),
const SizedBox(
height: 200,
child: NavBtns(), //버튼 렌더링하는 부분.
),
],
// button
),
),
);
}
}
페이지에 대한 정보를 따로 리스트로 만들었다. 그리고 그 리스트의 label, page를 버튼 아이템에 전달함.
import 'package:assignment2/variables/pages.dart';
import 'package:assignment2/widgets/NavBtn.dart';
import 'package:flutter/material.dart';
class NavBtns extends StatelessWidget {
const NavBtns({super.key});
Widget build(BuildContext context) {
return PageView.builder(
controller: PageController(viewportFraction: 0.8),
scrollDirection: Axis.horizontal,
itemCount: navList.length,
itemBuilder: (context, idx) {
return NavBtn(
label: navList[idx]["label"],
page: navList[idx]["page"],
);
},
);
}
}
여기서 역시나 또 약간의 고생을 했는데, 페이지를 어떻게 전달하느냐가 문제였다. 실행시켜야 하는건지, 그냥 전달만 해야하는건지...
final List<Map<String, dynamic>> navList = [
{
"name": 'author',
"image": null,
"label": '누구?',
"page": AuthorPage(),
},
{
"name": 'secret',
"image": 'assets/imgs/sleep.png',
"label": '어떤?',
"page": SecretPage(),
},
{
"name": 'upload',
"image": 'assets/imgs/cat.png',
"label": '나도..',
"page": UploadPage(),
},
];
class AuthorPage extends StatefulWidget {
const AuthorPage({super.key});
State<AuthorPage> createState() => _AuthorPageState();
}
class _AuthorPageState extends State<AuthorPage> {
var authors;
Widget build(BuildContext context) {
return Scaffold(
body: Background(
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CustomAppbar(
title: '비밀스러운 사람들..',
),
FutureBuilder(
future: SecretCatApi.fetchAuthors(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
authors = snapshot.data;
return Container(
alignment: Alignment.center,
margin: const EdgeInsets.symmetric(
vertical: 32,
),
child: GridView.builder(
padding: const EdgeInsets.all(16),
itemCount: authors.length,
shrinkWrap: true,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3),
itemBuilder: (context, idx) {
return AuthorItem(
name: authors[idx].name,
avatar: authors[idx].avatar,
);
},
),
);
}
return Container();
},
),
],
),
),
),
);
}
}
비눗방울에 작성자들이 들어가있는 느낌을 내고 싶었다. 그래서 ShakeY에 from 값을 랜덤으로 생성해서 주도록 함.final randomFrom = random.nextDouble() * 15.0
class AuthorItem extends StatelessWidget {
const AuthorItem({
super.key,
required this.avatar,
required this.name,
});
final String? avatar;
final String name;
Widget build(BuildContext context) {
final random = Random();
final randomFrom = random.nextDouble() * 15.0;
return Column(
children: [
Expanded(
child: ShakeY(
from: randomFrom, //0~15 사이 숫자 랜덤으로
duration: const Duration(seconds: 5),
infinite: true,
child: Bubble(
child: avatar != null
? Image.network(
avatar!,
)
: Image.asset('assets/imgs/origami.png'),
),
),
),
Text(
name,
style: const TextStyle(
fontSize: 24,
),
),
],
);
}
}
비밀이 잠자는 유니콘 위를 둥둥 떠다니도록 하고 싶었다.
화면을 비밀 비눗방울 1: 유니콘 1 로 하려고 Expanded로 감싸줌.
비눗방울이 자연스럽게 떠다니도록 하고싶은데, 좀더 연구해봐야할 것 같다.
원래는 비눗방울이 비밀 크기만큼만 적당한 크기를 갖도록 하고 나타나는 방향도 각자 다르게 하고싶었는데 이게 한계였다...
class SecretPage extends StatefulWidget {
const SecretPage({super.key});
State<SecretPage> createState() => _SecretPageState();
}
class _SecretPageState extends State<SecretPage> {
var secrets;
// secret, author{name, username, avatar}
Widget build(BuildContext context) {
return Scaffold(
body: Background(
child: SafeArea(
child: Column(
children: [
const CustomAppbar(title: 'shit..'),
FutureBuilder(
future: SecretCatApi.fetchSecrets(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
secrets = snapshot.data;
return Expanded(
child: PageView.builder(
controller: PageController(
viewportFraction: 0.8,
),
itemCount: secrets.length,
itemBuilder: (context, index) {
if (secrets[index].secret != null) {
return Secret(secret: secrets[index].secret);
}
return null;
},
),
);
}
return const Secret(
secret: '비밀은 누구나 있어..',
);
},
),
Expanded(
child: ShakeY(
from: 10,
duration: const Duration(seconds: 5),
infinite: true,
child: Image.asset(
'assets/imgs/sleep.png',
width: 300,
),
),
),
],
),
),
),
);
}
}
buble이 Expanded때문인가 동그랗지않고 화면을 다 차지했다. 그래서 secret에서 antiAlias를 줌. 근데 뭐 생각보다 나쁘지 않아서 그냥 뒀다. 위아래로 흔들리면서 오버플로우되는 부분이 가려지는데 그게 제법 흔들리면서 모양이 흩트러지는 비눗방울같지 않은가? ㅎㅎ
class Secret extends StatelessWidget {
const Secret({
super.key,
required this.secret,
});
final String secret;
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 40),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(200),
),
child: ShakeY(
from: 5,
duration: const Duration(seconds: 5),
infinite: true,
child: Bubble(
child: Center(
child: Expanded(
child: Text(
maxLines: 3,
overflow: TextOverflow.ellipsis,
secret != '' ? secret : 'null',
style: const TextStyle(fontSize: 18),
),
),
),
),
),
);
}
}
값을 받아올 때 보니까 빈 문자열을 누군가가 서버에 보내놓은 것 같았다. 처음에 아무것도 안떠서 당황;
그래서 필자는 아예 빈 문자열이 아닐 경우에만 서버에 요청을 하도록 했다. if (secret != '')
submitSecret이라는 불리언 값을 만들어서 데이터 전송시 true로 바꿔 종이학을 보내는 등의 액션이 발생하도록 했다. 한번 true가 되면 개발자가 다시 false로 값을 바꾸지 않는 한 다시 텍스트필드가 화면에 나올 수 없기 때문에 Future.delayed
를 통해 5초 뒤에 다시 이 값이 false가 되어 텍스트필드가 다시 나오도록 했다.
class UploadPage extends StatefulWidget {
const UploadPage({super.key});
State<UploadPage> createState() => _UploadPageState();
}
class _UploadPageState extends State<UploadPage> {
var controller = TextEditingController();
bool submitSecret = false;
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Background(
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomAppbar(title: submitSecret ? '비밀 접수중' : '내 비밀은'),
submitSecret
? Container()
: Column(
children: [
CustomTextField(controller: controller),
CustomBtn(
onPressed: () {
var secret = controller.text;
if (secret != '') {
try {
SecretCatApi.addSecret(secret);
submitSecret = true;
secret = '';
controller.clear();
setState(() {});
Future.delayed(const Duration(seconds: 5), () {
setState(() {
submitSecret = false;
});
});
} catch (err) {
alert(
context,
message: '에러가 발생했어요.',
);
}
} else {
alert(
context,
message: '아무 비밀도 듣지 못했어요!',
);
}
},
),
],
),
submitSecret
? ShakeY(
from: 20,
duration: const Duration(seconds: 2),
child: FadeOutLeftBig(
animate: true,
duration: const Duration(seconds: 5),
child: Image.asset(
'assets/imgs/origami.png',
),
),
)
: Bounce(
child: Image.asset(
'assets/imgs/cat.png',
width: 300,
),
),
],
),
),
),
);
}
}
class CustomTextField extends StatelessWidget {
const CustomTextField({
super.key,
required this.controller,
});
final TextEditingController controller;
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(30.0),
child: TextField(
minLines: 5,
maxLines: 5,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Colors.transparent,
),
borderRadius: BorderRadius.circular(20),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(
color: Color(0xffC56CB7),
),
borderRadius: BorderRadius.circular(20),
),
contentPadding: const EdgeInsets.all(16),
),
style: const TextStyle(
fontSize: 20,
),
controller: controller,
),
);
}
}
업로드 페이지에서 보면 에러상황 등에서 alert을 발생시키는 것을 볼 수 있다. 이건 따로 위젯이 있는 게 아니고 필자가 따로 만든 알림창이다.
showDialog를 사용했다. 유저에게 보여줄 메시지를 두번재 인자로 보내야한다. 뭐 확인버튼 외에도 취소나 처음으로 돌아가기 같은 버튼이 있는 게 더 좋겠지만 시간이 모자라서.
사실... stateless 위젯이라 생각했는데 그게 아니고Future<viod>
라서 유틸함수인 격인가? 싶다. 정확히는 아직 잘 모르겠음. 위젯으로 생성하려니까 안되더라.
Future<void> alert(BuildContext context, {required message}) async {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Error'),
content: Text(message),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('확인'),
),
],
);
},
);
}
개발할때 아이폰 에뮬레이터로 했다. 블로깅을 하기 위해 안드로이드로 빌드를 해보니 새로운 에러가 발생했다.
텍스트필드를 active 시켰을 때 bottom에서 키보드가 올라오면서 오버플로우가 발생한다.
찾아보니 키보드가 켜지면서 리사이즈를 하는 듯.
Scaffold 위젯에 resizeToAvoidBottomInset
라는 옵션이 있다. 이 옵션에 false값을 주어 해결.
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
... 생략...
의외로 이게 안되더라. 당연히 되는건줄;
본 후기는 유데미-스나이퍼팩토리 9주 완성 프로젝트캠프 학습 일지 후기로 작성 되었습니다.