# 강의 제목 : 누적다운로드 120만+ 1인 개발자와 함께하는 앱 개발 입문 Online
# 강의 목표 : 기초부터 운영까지 앱 개발의 전체 프로세스를 이해한다.
내 이름으로 된 앱을 최대 10개까지 만들어 출시할 수 있다.
앱 개발자로 성장할 수 있는 초석을 다진다.
반응 얻는 앱의 특징과 노하우를 알아간다.
향후 강의 없이도 나만의 앱을 개발할수 있는 실력을 가진다.
# 강의 요약 : 프로그램 설치부터 기본 문법, 광고 다는 법, 클론코딩을 진행하며 필수 지식을 학습한다.
총 10개의 다른 주제로 실제 사용화 가능한 수준의 앱을 만들어본다.
나의 앱을 세상에 선보이기 위한 개발자 등록 및 배포를 진행한다.
강사님의 리뷰/클레임 대응사례 등 앱 성공 포인트를 참고해 1인 개발자로서의 입지를 다진다.
# 강의 목차 : Flutter 실전 앱 제작
- 앱 기능 및 디자인 설계 및 초기 구조 만들기
- TODO 화면 만들기
- TODO 화면 만들기
- TODO 화면 만들기(10일차)
- TODO 기록 화면 만들기 (내부 DB 저장, 수정, 삭제)
- TODO 기록 화면 만들기 (이전 할일 기록 리스트, 카테고리 별 리스트)
- 앱 기능 및 디자인 설계 및 초기 구조 만들기 (일기앱)
- 일기 작성 화면 만들기
- 일기 작성 화면 만들기 (이모티콘 추가)
- 달력 화면 만들기
- 통계, 더보기 화면 만들기
- Splash 스크린 추가하기
# 강의 화면 :
# 강의 내용 : Flutter 실전 앱 제작 (TODO 화면 만들기)
# main.dart
import 'package:flutter/material.dart';
import 'data/todo.dart';
import 'data/util.dart';
import 'write.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Todo> todos = [
Todo(
title: "온라인교육듣기1",
memo: "앱개발 강의 듣기",
color: Colors.redAccent.value,
done: 0,
category: "공부",
date: 20210709
),
Todo(
title: "온라인교육듣기2",
memo: "앱개발 강의 듣기",
color: Colors.blue.value,
done: 1,
category: "공부",
date: 20210709
),
];
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(child: AppBar(),
preferredSize: Size.fromHeight(0),
),
// + 버튼 추가 하기
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add, color: Colors.white,),
onPressed: () async {
// 화면 이동
Todo todo = await Navigator.of(context).push(
MaterialPageRoute(builder: (ctx) => TodoWritePage(todo: Todo(
title: "",
color: 0,
memo: "",
done: 0,
category: "",
date: Utils.getFormatTime(DateTime.now())
))));
setState(() {
todos.add(todo);
});
},
),
body: ListView.builder(
itemBuilder: (ctx, idx){
if(idx == 0){
return Container(
child: Text("오늘하루", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),),
margin: EdgeInsets.symmetric(vertical: 12, horizontal: 20),
);
}else if(idx == 1){
List<Todo> undone = todos.where((t){
return t.done == 0;
}).toList();
return Container(
child: Column(
children: List.generate(undone.length, (_idx){
Todo t = undone[_idx];
return Container(
decoration: BoxDecoration(
color: Color(t.color),
borderRadius: BorderRadius.circular(16)
),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
margin: EdgeInsets.symmetric(vertical: 8, horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(t.title, style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold),),
Text(t.done == 0 ? "미완료" : "완료", style: TextStyle(color: Colors.white),)
],
),
Container(height: 8),
Text(t.memo)
]
)
);
}),
),
);
}
else if(idx == 2){
return Container(
child: Text("완료된 하루", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),),
margin: EdgeInsets.symmetric(vertical: 12, horizontal: 20),
);
}else if(idx == 3){
List<Todo> done = todos.where((t){
return t.done == 1;
}).toList();
return Container(
child: Column(
children: List.generate(done.length, (_idx){
Todo t = done[_idx];
return Container(
decoration: BoxDecoration(
color: Color(t.color),
borderRadius: BorderRadius.circular(16)
),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
margin: EdgeInsets.symmetric(vertical: 8, horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(t.title, style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold),),
Text(t.done == 0 ? "미완료" : "완료", style: TextStyle(color: Colors.white),)
],
),
Container(height: 8),
Text(t.memo)
]
)
);
}),
),
);
}
return Container();
},
itemCount: 4,
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.today_outlined),
label: "오늘"
),
BottomNavigationBarItem(
icon: Icon(Icons.assignment_ind_outlined),
label: "기록"
),
BottomNavigationBarItem(
icon: Icon(Icons.more_horiz),
label: "더보기"
),
],
),
);
}
}
# write.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'data/todo.dart';
class TodoWritePage extends StatefulWidget {
final Todo todo;
TodoWritePage({Key key, this.todo}): super(key: key);
@override
State<StatefulWidget> createState(){
return _TodoWritePageState();
}
}
class _TodoWritePageState extends State<TodoWritePage> {
TextEditingController nameController = TextEditingController();
TextEditingController memoController = TextEditingController();
int colorIndex = 0;
int ctIndex = 0;
@override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(
actions: [
TextButton(
child: Text("저장", style: TextStyle(color:Colors.white),),
onPressed: (){
// 페이지 저장시 사용
widget.todo.title = nameController.text;
widget.todo.memo = memoController.text;
Navigator.of(context).pop(widget.todo);
},
)
],
),
body: ListView.builder(
itemBuilder: (ctx, idx) {
if (idx == 0) {
return Container(
child: Text("제목", style: TextStyle(fontSize: 20),),
margin: EdgeInsets.symmetric(horizontal: 16),
);
} else if (idx == 1) {
return Container(
child: TextField(
controller: nameController,
),
margin: EdgeInsets.symmetric(vertical: 12, horizontal: 10),
);
}
else if (idx == 2) {
return InkWell(child: Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("색상", style: TextStyle(fontSize: 20),),
Container(
width: 20,
height: 20,
color: Color(widget.todo.color),
)
],
),
),
onTap: (){
List<Color> colors = [
Color(0xFF80d3f4),
Color(0xFFa794fa),
Color(0xFFfb91d1),
Color(0xFFfb8a94),
Color(0xFFfebd9a),
Color(0xFF51e29d),
Color(0xFFFFFFFF),
];
widget.todo.color = colors[colorIndex].value;
colorIndex++;
setState(() {
colorIndex = colorIndex % colors.length;
});
},
);
}
else if(idx==3){
return InkWell(child: Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('카테고리', style: TextStyle(fontSize: 20),),
Text(widget.todo.category)
],
),
),
onTap: (){
List<String> category = ["공부", "운동", "게임"];
widget.todo.category = category[ctIndex];
ctIndex++;
setState(() {
ctIndex = ctIndex % category.length;
});
},
);
}
else if (idx ==4){
return Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text("메모", style: TextStyle(fontSize: 20),),
);
}else if (idx == 5){
return Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 1),
child: TextField(
controller: memoController,
maxLines: 10,
minLines: 10,
decoration: InputDecoration(
border: OutlineInputBorder(borderSide: BorderSide(color: Colors.black))
),
),
);
}
return Container();
},
itemCount: 6,
)
);
}
}
# 실행결과
# 완료된 하루 미완료 클릭 하기
# main.dart
import 'package:flutter/material.dart';
import 'data/todo.dart';
import 'data/util.dart';
import 'write.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Todo> todos = [
Todo(
title: "온라인교육듣기1",
memo: "앱개발 강의 듣기",
color: Colors.redAccent.value,
done: 0,
category: "공부",
date: 20210709
),
Todo(
title: "온라인교육듣기2",
memo: "앱개발 강의 듣기",
color: Colors.blue.value,
done: 1,
category: "공부",
date: 20210709
),
];
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(child: AppBar(),
preferredSize: Size.fromHeight(0),
),
// + 버튼 추가 하기
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add, color: Colors.white,),
onPressed: () async {
// 화면 이동
Todo todo = await Navigator.of(context).push(
MaterialPageRoute(builder: (ctx) => TodoWritePage(todo: Todo(
title: "",
color: 0,
memo: "",
done: 0,
category: "",
date: Utils.getFormatTime(DateTime.now())
))));
setState(() {
todos.add(todo);
});
},
),
body: ListView.builder(
itemBuilder: (ctx, idx){
if(idx == 0){
return Container(
child: Text("오늘하루", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),),
margin: EdgeInsets.symmetric(vertical: 12, horizontal: 20),
);
}else if(idx == 1){
List<Todo> undone = todos.where((t){
return t.done == 0;
}).toList();
return Container(
child: Column(
children: List.generate(undone.length, (_idx){
Todo t = undone[_idx];
return InkWell(child: TodoCardWidget(t: t),
onTap: (){
setState(() {
if(t.done == 0){
t.done = 1;
}else{
t.done = 0;
}
});
},
);
}),
),
);
}
else if(idx == 2){
return Container(
child: Text("완료된 하루", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),),
margin: EdgeInsets.symmetric(vertical: 12, horizontal: 20),
);
}else if(idx == 3) {
List<Todo> done = todos.where((t) {
return t.done == 1;
}).toList();
return Container(
child: Column(
children: List.generate(done.length, (_idx) {
Todo t = done[_idx];
return InkWell(child: TodoCardWidget(t: t),
onTap: () {
setState(() {
if (t.done == 0) {
t.done = 1;
} else {
t.done = 0;
}
});
},
);
}),
),
);
}
return Container();
},
itemCount: 4,
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.today_outlined),
label: "오늘"
),
BottomNavigationBarItem(
icon: Icon(Icons.assignment_ind_outlined),
label: "기록"
),
BottomNavigationBarItem(
icon: Icon(Icons.more_horiz),
label: "더보기"
),
],
),
);
}
}
class TodoCardWidget extends StatelessWidget {
final Todo t;
TodoCardWidget({Key key, this.t}) :super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Color(t.color),
borderRadius: BorderRadius.circular(16)
),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
margin: EdgeInsets.symmetric(vertical: 8, horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(t.title, style: TextStyle(fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold),),
Text(t.done == 0 ? "미완료" : "완료",
style: TextStyle(color: Colors.white),)
],
),
Container(height: 8),
Text(t.memo, style: TextStyle(color: Colors.white))
],
),
);
}
}
# 실행결과
# long 클릭 시 수정 하기
# main.dart
import 'package:flutter/material.dart';
import 'data/todo.dart';
import 'data/util.dart';
import 'write.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Todo> todos = [
Todo(
title: "온라인교육듣기1",
memo: "앱개발 강의 듣기",
color: Colors.redAccent.value,
done: 0,
category: "공부",
date: 20210709
),
Todo(
title: "온라인교육듣기2",
memo: "앱개발 강의 듣기",
color: Colors.blue.value,
done: 1,
category: "공부",
date: 20210709
),
];
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(child: AppBar(),
preferredSize: Size.fromHeight(0),
),
// + 버튼 추가 하기
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add, color: Colors.white,),
onPressed: () async {
// 화면 이동
Todo todo = await Navigator.of(context).push(
MaterialPageRoute(builder: (ctx) => TodoWritePage(todo: Todo(
title: "",
color: 0,
memo: "",
done: 0,
category: "",
date: Utils.getFormatTime(DateTime.now())
))));
setState(() {
todos.add(todo);
});
},
),
body: ListView.builder(
itemBuilder: (ctx, idx){
if(idx == 0){
return Container(
child: Text("오늘하루", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),),
margin: EdgeInsets.symmetric(vertical: 12, horizontal: 20),
);
}else if(idx == 1){
List<Todo> undone = todos.where((t){
return t.done == 0;
}).toList();
return Container(
child: Column(
children: List.generate(undone.length, (_idx){
Todo t = undone[_idx];
return InkWell(child: TodoCardWidget(t: t),
onTap: (){
setState(() {
if(t.done == 0){
t.done = 1;
}else{
t.done = 0;
}
});
},
onLongPress: () async {
Todo todo = await Navigator.of(context).push(
MaterialPageRoute(builder: (ctx) => TodoWritePage(todo: t)));
setState(() {});
},
);
}),
),
);
}
else if(idx == 2){
return Container(
child: Text("완료된 하루", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),),
margin: EdgeInsets.symmetric(vertical: 12, horizontal: 20),
);
}else if(idx == 3) {
List<Todo> done = todos.where((t) {
return t.done == 1;
}).toList();
return Container(
child: Column(
children: List.generate(done.length, (_idx) {
Todo t = done[_idx];
return InkWell(child: TodoCardWidget(t: t),
onTap: () {
setState(() {
if (t.done == 0) {
t.done = 1;
} else {
t.done = 0;
}
});
},
onLongPress: () async {
Todo todo = await Navigator.of(context).push(
MaterialPageRoute(builder: (ctx) => TodoWritePage(todo: t)));
setState(() {});
},
);
}),
),
);
}
return Container();
},
itemCount: 4,
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.today_outlined),
label: "오늘"
),
BottomNavigationBarItem(
icon: Icon(Icons.assignment_ind_outlined),
label: "기록"
),
BottomNavigationBarItem(
icon: Icon(Icons.more_horiz),
label: "더보기"
),
],
),
);
}
}
class TodoCardWidget extends StatelessWidget {
final Todo t;
TodoCardWidget({Key key, this.t}) :super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Color(t.color),
borderRadius: BorderRadius.circular(16)
),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
margin: EdgeInsets.symmetric(vertical: 8, horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(t.title, style: TextStyle(fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.bold),),
Text(t.done == 0 ? "미완료" : "완료",
style: TextStyle(color: Colors.white),)
],
),
Container(height: 8),
Text(t.memo, style: TextStyle(color: Colors.white))
],
),
);
}
}
# write.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'data/todo.dart';
class TodoWritePage extends StatefulWidget {
final Todo todo;
TodoWritePage({Key key, this.todo}): super(key: key);
@override
State<StatefulWidget> createState(){
return _TodoWritePageState();
}
}
class _TodoWritePageState extends State<TodoWritePage> {
TextEditingController nameController = TextEditingController();
TextEditingController memoController = TextEditingController();
int colorIndex = 0;
int ctIndex = 0;
@override
void initState() {
// TODO: implement initState
super.initState();
nameController.text = widget.todo.title;
memoController.text = widget.todo.memo;
}
@override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(
actions: [
TextButton(
child: Text("저장", style: TextStyle(color:Colors.white),),
onPressed: (){
// 페이지 저장시 사용
widget.todo.title = nameController.text;
widget.todo.memo = memoController.text;
Navigator.of(context).pop(widget.todo);
},
)
],
),
body: ListView.builder(
itemBuilder: (ctx, idx) {
if (idx == 0) {
return Container(
child: Text("제목", style: TextStyle(fontSize: 20),),
margin: EdgeInsets.symmetric(horizontal: 16),
);
} else if (idx == 1) {
return Container(
child: TextField(
controller: nameController,
),
margin: EdgeInsets.symmetric(vertical: 12, horizontal: 10),
);
}
else if (idx == 2) {
return InkWell(child: Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("색상", style: TextStyle(fontSize: 20),),
Container(
width: 20,
height: 20,
color: Color(widget.todo.color),
)
],
),
),
onTap: (){
List<Color> colors = [
Color(0xFF80d3f4),
Color(0xFFa794fa),
Color(0xFFfb91d1),
Color(0xFFfb8a94),
Color(0xFFfebd9a),
Color(0xFF51e29d),
Color(0xFFFFFFFF),
];
widget.todo.color = colors[colorIndex].value;
colorIndex++;
setState(() {
colorIndex = colorIndex % colors.length;
});
},
);
}
else if(idx==3){
return InkWell(child: Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('카테고리', style: TextStyle(fontSize: 20),),
Text(widget.todo.category)
],
),
),
onTap: (){
List<String> category = ["공부", "운동", "게임"];
widget.todo.category = category[ctIndex];
ctIndex++;
setState(() {
ctIndex = ctIndex % category.length;
});
},
);
}
else if (idx ==4){
return Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text("메모", style: TextStyle(fontSize: 20),),
);
}else if (idx == 5){
return Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 1),
child: TextField(
controller: memoController,
maxLines: 10,
minLines: 10,
decoration: InputDecoration(
border: OutlineInputBorder(borderSide: BorderSide(color: Colors.black))
),
),
);
}
return Container();
},
itemCount: 6,
)
);
}
}
#실행결과
# 교육 소감
오늘은 tolist 추가 페이지에서 기록하고 이전페이지 기록까지 확인하는 페이지를 만들었다. 수정하거나 완료/미완료로 변경하거나 하는 기능도 추가했는데, main.dart 에서 카드를 클릭 가능하도록 linkwell 을 사용하였다. t.done을 통해서 0일때 미완료일때는 완료로 변경하고, t.done이 1일때 완료일때는 미완료로 변경하도록 하였다. 이때는 실제 state 가 변경될수 있도록 setSate 함수로 감싸줘야 한다. 또한 stateless widget 을 사용하여 card 그리는 위젯을 작성하였다. 왜냐하면 상태를 변경하는건 페이지가 stateful 을 사용하면 되고, 카드들은 todo 정보들만 변경하면 되기 때문이다. card 를 렌더링 하기 위해선 기존에 만들어놓은 컨테이너를 사용하였다. 다만 클래스에 t라는 변수가 없기 때문에 final Todo t라는 변수를 사용하여, todo card widget을 생성할 때 매개변수로 받아 처리하도록 하였다. todo card를 추가하면, 위에 생성한 컨테이너를 생략 가능하게 하여 코드를 간결하게 만들어준다. write.dart 에서 값을 받을수 있게 하기 위해서 initstate 페이지가 생성될 때 처음 해야하는 일들을 정의 해줬다. 제목을 위한 name controller와 memo controller를 통해 새로운 값으로 덮어씌우는 구조를 만들수 있었다.
# 본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
'코딩 알로하 :: two > 하이브리드앱' 카테고리의 다른 글
패스트캠퍼스 챌린지 12일차 (0) | 2021.11.12 |
---|---|
패스트캠퍼스 챌린지 11일차 (0) | 2021.11.11 |
패스트캠퍼스 챌린지 9일차 (0) | 2021.11.09 |
패스트캠퍼스 챌린지 8일차 (0) | 2021.11.08 |
패스트캠퍼스 챌린지 7일차 (0) | 2021.11.07 |
댓글