티스토리 뷰
재즐보프님의 Flutter강의를 보고 배운 내용을 정리하는 게시물입니다.
www.youtube.com/watch?v=Yt-DjG5b4iA&list=PLnIaYcDMsScxP2Nl8pEbmI__wkF0YVu0a
먼저 파일 구조를 말씀드리자면, 재즐보프님 강의에서 메모앱을 만드실 때, 총 2개의 디렉토리(database, screens)와 총 5개의 dart파일(db.dart, memo.dart home.dart, edit.dart, main.dart)를 만드셨습니다.
이 포스트에서는 main.dart, home.dart, edit.dart의 레이아웃에 대하여 설명드리고, 다음 포스트에는 edit.dart와 연결되는 home.dart의 함수들까지 정리하고 마지막 포스트에는 db.dart와 memo.dart 등 4개의 dart 파일이 어떤 정보를 전달하고 받아오는 지에 대하여 말씀드리겠습니다.
import 'package:flutter/material.dart';
import 'screens/home.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.teal, primaryColor: Colors.white
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
먼저 main.dart의 모습입니다. 그냥 Flutter 프로젝트를 생성했을 때의 모습과 똑같습니다. 개인적으로 teal 색깔이 마음에 들어 primarySwatch를 teal로 주었습니다.
이제 이를 만들어주는, MyHomePage가 들어있는 home.dart를 보겠습니다.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'edit.dart';
import 'package:memomemo/database/memo.dart';
import 'package:memomemo/database/db.dart';
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String deleteId = "";
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Padding(
padding: EdgeInsets.only(left: 30, top: 40, bottom: 20),
child: Container(
child: Text('메모메모',
style: TextStyle(
fontSize: 36,
color: Colors.teal,
fontWeight: FontWeight.bold)),
alignment: Alignment.centerLeft,
),
),
Expanded(child: memoBuilder(context))
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) =>
EditPage())
).then((value) {
setState(() {});
});
},
tooltip: '메모를 추가하려면 클릭하세요',
label: Text('메모 추가'),
icon: Icon(Icons.add),
),
);
}
List<Widget> LoadMemo() {
List<Widget> memoList = [];
memoList.add(Container(
color: Colors.purpleAccent,
height: 100,
));
return memoList;
}
Future<List<Memo>> loadMemo() async {
DBHelper sd = DBHelper();
return await sd.memos();
}
Future<void> deleteMemo(String id) async {
DBHelper sd = DBHelper();
return sd.deleteMemo(id);
}
void showAlertDialog(BuildContext context) async {
String result = await showDialog(
context: context,
barrierDismissible: false, // user must tap button!
builder: (BuildContext context) {
return AlertDialog(
title: Text('삭제 경고'),
content: Text("정말 삭제하시겠습니까?\n 삭제된 메모는 복구되지 않습니다."),
actions: <Widget>[
FlatButton(
child: Text('삭제'),
onPressed: () {
Navigator.pop(context, "삭제");
setState(() {
deleteMemo(deleteId);
});
},
),
FlatButton(
child: Text('취소'),
onPressed: () {
deleteId = "";
Navigator.pop(context, "취소");
},
),
],
);
},
);
}
Widget memoBuilder(BuildContext parentContext) {
return FutureBuilder(
builder: (context, snap) {
if (snap.data == null || snap.data.isEmpty) {
return Container(
alignment: Alignment.center,
child: Text(
'지금 바로 "메모 추가" 버튼을 눌러\n새로운 메모를 추가해보세요!\n\n\n\n\n\n\n\n\n',
style: TextStyle(fontSize: 20, color: Colors.blueGrey),
textAlign: TextAlign.center,
),
);
}
return ListView.builder(
physics: BouncingScrollPhysics(),
padding: EdgeInsets.all(20),
itemCount: snap.data.length,
itemBuilder: (context, index) {
Memo memo = snap.data[index];
return InkWell(
onTap: (){},
onLongPress: (){
setState(() {
deleteId = memo.id;
showAlertDialog(parentContext);
});
},
child: Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 5),
padding: EdgeInsets.all(15),
alignment: Alignment.center,
height: 90,
decoration: BoxDecoration(
color: Color.fromRGBO(240, 240, 240, 1),
border: Border.all(
color: Colors.teal,
width: 1,
),
boxShadow: [BoxShadow(color: Colors.blueGrey, blurRadius: 3)],
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment
.stretch,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
memo.title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: Colors.teal),
),
Text(
memo.text,
style: TextStyle(fontSize: 15),
),
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"최종 수정 시간: " + memo.editTime.split('.')[0],
style: TextStyle(fontSize: 11),
textAlign: TextAlign.end,
),
],
)
],
),
),
);
},
);
},
future: loadMemo(),
);
}
}
상당히 길지만, 이제 여기서 조금씩 뜯어보고 강의에는 다 담겨있지 않지만 개인적으로 찾아본 것도 말씀드리겠습니다.
class _MyHomePageState extends State<MyHomePage> {
String deleteId = "";
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Padding(
padding: EdgeInsets.only(left: 30, top: 40, bottom: 20),
child: Container(
child: Text('메모메모',
style: TextStyle(
fontSize: 36,
color: Colors.teal,
fontWeight: FontWeight.bold)),
alignment: Alignment.centerLeft,
),
),
Expanded(child: memoBuilder(context))
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) =>
EditPage())
).then((value) {
setState(() {});
});
},
tooltip: '메모를 추가하려면 클릭하세요',
label: Text('메모 추가'),
icon: Icon(Icons.add),
),
);
}
먼저 기본 판이 되는 부분입니다. 동적으로 메모를 추가하면 추가한 메모가 떠야하므로 StatefulWidget으로 만들어져있습니다.
body에 Column을 넣어 내용물들을 넣어주게끔 했습니다.
이 때 Padding에 대하여 말씀드리겠습니다.
1. Padding 이란?
child Widget들에 대하여 padding을 갖게끔 하는 Widget입니다.
Flutter api 공식문서에도 다음과 같이 나와있습니다.
A widget that insets its child by the given padding.
너무나도 단순하죠?
Padding을 사용할 때, 위의 코드처럼 padding: EdgeInsets.only()를 사용하게 되면 괄호 안에 적어준 값만큼 padding을 주는 셈이 되는 거죠.
이와 같이 단어 그대로 Widget들에게 Padding을 주는 Padding은 현재 1 개의 Child를 갖고 있습니다.
child는 Container를 갖고, 그 안에 child를 넣어 Text를 넣어주었습니다.
2. Container란?
이번에도 Flutter 공식 문서에 기반하여 설명드리겠습니다.
위의 사진의 출처는 flutter.dev/docs/development/ui/layout입니다.
보시면 알 수 있듯이, Container 안에 하위 Widget을 넣으려면 Child혹은 Children(여러 개)를 넣어야함을 알 수 있습니다.
그 여러 Widget들을 하나로 모아서 Container 하나로 묶어두는 셈이죠.
Padding Widget에 하위 Widget을 넣기 위해 child를 사용했고, 그 child에는 Container로 묶어줄 것이며, 그 Container의 하위 Widget으로 Text를 넣어준 것이죠.
Text에는 '메모메모'라는 글자를 넣어주었고 이 Text에 대한 Style(CSS 같은 거라고 이해함)을 다음 속성에 넣어주었습니다.
TextStyle을 이용하면 많은 속성을 추가할 수 있는데 fontSize, color, fontWeight 등 이름만 봐도 직관적으로 알 수 있는 속성들을 넣어주었습니다.
그리고 Container 내부의 child들을 '배치'하기 위해 alignment를 사용합니다.
Alignment 외에도 mainAxisAlignment라는 속성이 존재합니다.
둘이 헷갈리기 쉽지도 않지만 mainAxisAlignment는 Row와 Column을 배치하는 속성이고 Alignment는 방금과 같이 내부 Widget들을 배치하는 속성입니다.
자, Column 하위에 Widget을 여러 개 넣을 것이기에 child가 아니라 children을 사용하였는데 하나는 Padding이었습니다. 다른 하나는 Expanded인데 이것은 무엇일까요?
Expanded란?
이 또한 Flutter 공식 문서를 먼저 살펴봤습니다.
A widget that expands a child of a Row, Column, or Flex so that the child fills the available space.
첫 줄만 읽어도 단순하게 이해되는 것이 Widget들을 배치할 때, extra space들을 children이 알아서 채우는 것으로 보입니다.
강의에서 재즐보프님이 작성하신 코드대로 한다면, memoBuilder(context)를 child로 가지는 놈이 extra space를 알아서 채우게끔 만들어지겠네요.
그리고 body에는 1개의 Column과 floatingActionButton이 들어있죠? 그 중에서 1개의 Column은 어플의 상당 왼쪽 끝에 제목을 달아주는 것이고 memoBuilder를 통해 남은 공간을 차지하게끔 만들어져 있었습니다.
그럼 나머지 floatingActionButton에 대하여 보면 되겠네요!
floaingActionButton은 안드로이드 스튜디오나 다른 라이브러리를 이용해서 어플을 개발해보신 적 있는 분이라면 바로 알아차리시겠지만 우측 하단에 터치할 수 있는 버튼을 만드는 것입니다.
FloatingActionButton이란?
Flutter 공식 문서에는 다음과 같은 사진과 함께 설명을 하고 있습니다.
A material design floating action button.
공식문서에 작성된 Constructor만 봐도 눈이 아플 정도로 넣어줄 수 있는 속성이 정말 많네요.
재즐보프님 강의에서는 이 버튼을 눌렀을 때 작동하는 함수를 추가하는 onPressed와 꾹~ 눌렀을 때 뜨는 tooltip, 이 button에 텍스트를 넣어줄 label, 그리고 이 버튼의 모양새(?)를 결정하는 icon을 내부 속성으로 넣어주셨습니다.
위의 생성자에 대한 설명을 살짝 읽어보니 extended를 붙혀주지 않으면 'creates a circular floating action button'이라고 하네요? 근데 저희는 한글을 넣어서 조금 길쭉한 floatingButton을 만들어줄 것이니까 extended를 붙혀줍니다.
사실상 onPressed의 내부 함수 구성만 제외하면 기본 페이지 구성은 다 만들어진 셈입니다.
그렇다면 재즐보프님의 강의에서처럼 하단 floatingButton을 클릭하여서 메모를 '추가'하는 페이지로 넘어가게끔 만들어야겠죠?
이 부분이 dart와 dart 파일 간의 연결, 페이지 넘김이라고 생각하면 될 것 같아요! 여러 페이지를 오가는 것이니 중요하겠죠?
onPressed: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) =>
EditPage())
).then((value) {
setState(() {});
});
},
onPressed부분만 따로 떼어내서 보겠습니다!
먼저 Navigator가 눈에 띄죠?
Navigator란?
당연히 Flutter 공식 문서를 참고해야겠죠?
A widget that manages a set of child widgets with a stack discipline.
Many apps have a navigator near the top of their widget hierarchy in order to display their logical history using an Overlay with the most recently visited pages visually on top of the older pages. Using this pattern lets the navigator visually transition from one page to another by moving the widgets around in the overlay. Similarly, the navigator can be used to show a dialog by positioning the dialog widget above the current page.
제가 생각할 때 중요한 부분만 강조해봤습니다.
안드로이드 스튜디오에서 다른 페이지로 넘어갈 때 Intent를 넘겨주는 것처럼 여기서는 Navigator를 이용하여 Page를 오간다고 생각하면 될 것 같습니다. (그리고 Flutter에서는 screen과 page를 'route'라고 부릅니다.)
그리고 이 Navigator는 stack 구조라고 나와있습니다. Stack 구조는 다들 아실 것 같지만 LIFO 구조로 마지막에 push된 것이 pop되는 구조지요?
한 마디로 10개의 route를 Navigator에 push하면 가장 최근에 방문한 route부터 차례로 pop시킬 수 있다는 겁니다.
이 Navigator의 생성자는 다음과 같습니다.
이에 따라 다음 route로 넘어가는 코드를 Flutter 공식 문서에는 다음과 같이 적어두었습니다.
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
}
기존 안드로이드 스튜디오의 Intent와 비슷한 구조를 이루는 것 같습니다.
첫 번째에는 현재 route의 context를 넣어주고, 두 번째에 넘어갈 route와 연결시킵니다.
그래서 재즐보프님이 강의에서 context를 첫 번째 인자로 넣어주었고, 이 인자를 가져오기 위해 Expanded에서 context를 인자로 담아 넘겨준 것입니다.
그런데 강의에서 사용하신 CupertinoPageRoute는 무엇일까요?
CupertinoPageRoute란?
Flutter 공식 문서를 찾아봅시다.
생성자는 Navigator에 비하면 한 없이 쉬워보이네요.
문서를 잘 읽어보니 iOS Design App으로 사용한다고 하네요?
디자인이 예뻐보여서 넣으신 것 같습니다(?)
그렇게 EditPage로 넘어가게 코딩을 해놓고 then과 setState를 이용해서 현재 State를 리로드하는 것 같습니다.
(※ 사실 이 부분을 정확하게 모르겠습니다. then이 무엇을 의미하는 지, value가 무엇을 의미하는지, 추후에 찾아보고 수정하겠습니다)
그렇다면, 메모를 추가하는 floatingActionButton을 클릭하면 메모 추가하는 route로 넘어가겠네요.
다음 route의 레이아웃을 보여주는 코드는 다음과 같습니다.
class EditPage extends StatelessWidget {
String title = "";
String text = "";
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(icon: const Icon(Icons.delete), onPressed: () {}),
IconButton(
icon: const Icon(Icons.save),
onPressed: saveDB,
)
],
),
body: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
TextField(
onChanged: (String title){ this. title = title; },
style: TextStyle(fontSize: 30, fontWeight: FontWeight.w600),
keyboardType: TextInputType.multiline,
maxLines: null,
// obscureText: true,.
decoration: InputDecoration(
// border: OutlineInputBorder(),
hintText: '제목을 적어주세요.',
),
),
Padding(padding: EdgeInsets.all(10)),
TextField(
onChanged: (String text){this.text = text; },
keyboardType: TextInputType.multiline,
maxLines: null,
// obscureText: true,
decoration: InputDecoration(
// border: OutlineInputBorder(),
hintText: '메모의 내용을 적어주세요.',
),
),
// TextField(),
],
),
)
);
}
...
}
사실 아래에 더 많은 코드가 있는데 현재 포스트는 레이아웃에 대하여 설명드리는 것이라 중략했습니다.
이제 많은 것들을 정리했기에 넘어갈 수 있는 게 많겠네요! 😋
appBar: AppBar(
actions: [
IconButton(icon: const Icon(Icons.delete), onPressed: () {}),
IconButton(
icon: const Icon(Icons.save),
onPressed: saveDB,
)
],
),
먼저 Scaffold에는 appBar를 기본적으로 넣어주었습니다.
저번 포스트에도 appBar에 대하여 간단하게 말씀드렸었습니다.
이번에는 Flutter 공식문서를 제대로 찾아서 내용을 파헤쳐보겠습니다.
appBar란?
공식문서에 생성자 부분을 찾아보면 다음과 같이 서술되어있습니다.
이전 포스트에서는 appBar로는 상단에 app이름을 띄워주는 정도로만 설명드렸는데 공식문서를 찾아보니 생각보다 더 많은 속성이 있었네요!
이 속성 중에 이번에 사용한 것은, actions입니다.
actions 속성을 이용하여 IconButton을 추가하고 아이콘을 클릭하여 저장 버튼을 만들 수 있습니다.
IconButton도 어쨌거나 Icon이고 Button이기 때문에 icon 속성과 onPressed 속성을 갖게 됩니다.
그리고 appBar는 자동적으로 뒤로 가기 기능을 자동으로 세팅해줍니다. (이게 너무나도 단순하면서도 좋은 것 같습니다 ㅎ)
이 다음 body 부분을 보겠습니다.
body: Padding(
padding: EdgeInsets.all(20),
child: Column(
children: [
TextField(
onChanged: (String title){ this. title = title; },
style: TextStyle(fontSize: 30, fontWeight: FontWeight.w600),
keyboardType: TextInputType.multiline,
maxLines: null,
// obscureText: true,
decoration: InputDecoration(
// border: OutlineInputBorder(),
hintText: '제목을 적어주세요.',
),
),
Padding(padding: EdgeInsets.all(10)),
TextField(
onChanged: (String text){this.text = text; },
keyboardType: TextInputType.multiline,
maxLines: null,
// obscureText: true,
decoration: InputDecoration(
// border: OutlineInputBorder(),
hintText: '메모의 내용을 적어주세요.',
),
),
],
),
)
body에 해당되는 부분은 모든 Padding을 주는 모습이네요.
그리고 body에는 Column을 주고 그 안에 다양한 Widget들을 넣어주기 위해 children으로 묶어주었네요.
그리고 이 route에서는 제목과 내용을 입력하는 두 개의 TextField가 필요하기 때문에 children으로 묶어줍니다.
TextField란?
항상 그랬듯 Flutter 공식 문서를 참고해봅시다.
A material design text field.
A text field lets the user enter text, either with hardware keyboard or with an onscreen keyboard.
The text field calls the onChanged callback whenever the user changes the text in the field. If the user indicates that they are done typing in the field (e.g., by pressing a button on the soft keyboard), the text field calls the onSubmitted callback.
이름 그대로 Text를 입력하는 Field이고 이 Field는 user가 입력할 때마다 onChanged 콜백을 호출하고 typing을 끝내면 onSubmitted 콜백을 호출하는 것을 알 수 있습니다.
위의 공식 문서에 대한 설명을 토대로 재즐보프님의 코드를 보면 두 TextField에는 onChanged 속성을 넣어주어 현재 title과 text를 사용자가 입력하는 것으로 바꿔주는 것을 알 수 있습니다.
TextField에 작성되는 부분에 해당하는 Text의 Style 속성을 넣어 바꿔주고, keyboardType 속성을 이용하여 TextField 칸을 넘어가는 Input을 받으면 여러 라인으로 나뉘어 적힐 수 있게끔 합니다.
코드에서 보면 'obscureText'가 주석처리 되어 있는 것을 알 수 있는데, 이는 비밀번호를 입력할 때처럼 보안상 보이지 않아야 하는 것들을 위한 속성입니다. 그런데 재즐보프님의 강의에서 만드는 앱은 메모 앱이기에 사용자가 입력한 글들이 보이지 않게 할 필요가 없지요?
마지막으로 decoration 속성입니다.
위의 생성자 사진을 보면 알 수 있듯이, TextField에는 다양한 속성을 적어줄 수 있는데, InputDecoration 또한 아래 사진에서 알 수 있듯이 생성자에 넣을 수 있는 속성이 드럽게 많습니다.
우리는 이 중에서 hintText만 사용할 것입니다. hintText는 TextField에 사용자가 입력하기 전에 희미하게 적혀있는 문자를 말합니다.
위와 같은 형태로 TextField를 2개 만들어 제목에 해당하는 부분과 내용에 해당하는 부분에 대한 Layout을 얼추 만들어줍니다.
재즐보프님 강의 내용 중에 메모앱을 만드는 부분에서 전체 Layout에 대한 코드와 추가 내용을 정리해보았습니다!
이 레이아웃들을 토대로 실제 사용자가 적은 텍스트를 저장하고 넘겨주는 부분부터는 다음 포스트에 정리하도록 하겠습니다! 👏
'Programming 📱 > Flutter' 카테고리의 다른 글
[재즐보프님 Flutter 강의] 플러터(Flutter)로 메모앱을 만들며(2) (0) | 2021.01.30 |
---|---|
[재즐보프님 Flutter 강의] 입문자를 위한 플러터(Flutter) 튜토리얼 (0) | 2021.01.20 |