[왕초보] 플러터(Flutter)로 시작하는 앱개발 3주차 - 스파르타코딩클럽
Dart packages
Pub is the package manager for the Dart programming language, containing reusable libraries & packages for Flutter and general Dart programs.
pub.dev
https://pub.dev/packages/google_fonts
google_fonts | Flutter Package
A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling.
pub.dev

analysis_options.yaml
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
// 홈 페이지
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("mymemo"),
),
body: Center(child: Text("메모를 작성해주세요")),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailPage()),
);
},
),
);
}
}
// 메모 생성 및 수정 페이지
class DetailPage extends StatelessWidget {
DetailPage({super.key});
TextEditingController contentController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
// 삭제 버튼 클릭시
},
icon: Icon(Icons.delete),
)
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: "메모를 입력하세요",
border: InputBorder.none,
),
autofocus: true,
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
onChanged: (value) {
// 텍스트필드 안의 값이 변할 때
},
),
),
);
}
}
body: memoList.isEmpty
? Center(child: Text("메모를 작성해 주세요"))
: Center(child: Text('메모가 존재합니다!')),
[ListTile 위젯을 활용]
https://api.flutter.dev/flutter/material/ListTile-class.html
ListTile class - material library - Dart API
A single fixed-height row that typically contains some text as well as a leading or trailing icon. A list tile contains one to three lines of text optionally flanked by icons or other widgets, such as check boxes. The icons (or other widgets) for the tile
api.flutter.dev
[ ListTile onTap / DetailPage 로 이동]
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailPage()),
);
[[코드스니펫] ListTile onTap / DetailPage 로 이동]
final List<String> memoList; final int index;
String memo = ''; // 빈 메모 내용 추가 Navigator.push( context, MaterialPageRoute( builder: (_) => DetailPage( index: memoList.indexOf(memo), memoList: memoList, ), ), );
contentController.text = memoList[index];
setState(() { memoList.add(memo); });
memoList[index] = value;
actions: [
// 취소 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("취소"),
),
// 확인 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(
"확인",
style: TextStyle(color: Colors.pink),
),
),
],

[HomePage / delete memo]
memoList.removeAt(index); // index에 해당하는 항목 삭제
Navigator.pop(context); // 팝업 닫기
Navigator.pop(context); // HomePage 로 가기
[main.dart 완성 코드]
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
// 홈 페이지
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<String> memoList = ['장보기 목록: 사과, 양파', '새 메모']; // 전체 메모 목록
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("mymemo"),
),
body: memoList.isEmpty
? Center(child: Text("메모를 작성해 주세요"))
: ListView.builder(
itemCount: memoList.length, // memoList 개수 만큼 보여주기
itemBuilder: (context, index) {
String memo = memoList[index]; // index에 해당하는 memo 가져오기
return ListTile(
// 메모 고정 아이콘
leading: IconButton(
icon: Icon(CupertinoIcons.pin),
onPressed: () {
print('$memo : pin 클릭 됨');
},
),
// 메모 내용 (최대 3줄까지만 보여주도록)
title: Text(
memo,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
// 아이템 클릭시
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: index,
memoList: memoList,
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
String memo = ''; // 빈 메모 내용 추가
setState(() {
memoList.add(memo);
});
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoList.indexOf(memo),
memoList: memoList,
),
),
);
},
),
);
}
}
// 메모 생성 및 수정 페이지
class DetailPage extends StatelessWidget {
DetailPage({super.key, required this.memoList, required this.index});
final List<String> memoList;
final int index;
TextEditingController contentController = TextEditingController();
@override
Widget build(BuildContext context) {
contentController.text = memoList[index];
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
// 삭제 버튼 클릭시
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("정말로 삭제하시겠습니까?"),
actions: [
// 취소 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("취소"),
),
// 확인 버튼
TextButton(
onPressed: () {
memoList.removeAt(index); // index에 해당하는 항목 삭제
Navigator.pop(context); // 팝업 닫기
Navigator.pop(context); // HomePage 로 가기
},
child: Text(
"확인",
style: TextStyle(color: Colors.pink),
),
),
],
);
},
);
},
icon: Icon(Icons.delete),
)
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: "메모를 입력하세요",
border: InputBorder.none,
),
autofocus: true,
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
onChanged: (value) {
// 텍스트필드 안의 값이 변할 때
memoList[index] = value;
},
),
),
);
}
}

대부분의 상태 관리 패키지들은 위 사진과 같이 중앙 집중식으로 데이터를 한 곳에 모아서 관리합니다. 앞으로 데이터를 담당하는 클래스를 서비스(Service)라고 부르도록 하겠습니다. 이렇게 되면 각 페이지에서는 데이터에 대한 CRUD는 모두 서비스에게 요청하는 방식으로 구현되고, 이를 통해 각 화면 간 데이터를 주고받는 문제를 해결할 수 있습니다.
https://pub.dev/packages/provider/install
provider | Flutter Package
A wrapper around InheritedWidget to make them easier to use and more reusable.
pub.dev
Provider 패키지 Install 탭이 아래와 같이 나오면, flutter pub add provider 명령어 옆에 아이콘을 눌러 복사해 주세요.
View → Terminal
=> flutter pub add provider
pubspec.yaml
import 'package:flutter/material.dart';
import 'main.dart';
// Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다.
class Memo {
Memo({
required this.content,
});
String content;
}
// Memo 데이터는 모두 여기서 관리
class MemoService extends ChangeNotifier {
List<Memo> memoList = [
Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터
Memo(content: '새 메모'), // 더미(dummy) 데이터
];
}
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => MemoService()),
],
child: const MyApp(),
),
);
Consumer<MemoService>(
builder: (context, memoService, child) {
// memoService 라는 변수에 MemoService 를 담아서 쓸 수 있습니다.
return 위젯;
}
)
// memoService로 부터 memoList 가져오기
List<Memo> memoList = memoService.memoList;
DetailPage({super.key, required this.index});
MemoService memoService = context.read<MemoService>();
Memo memo = memoService.memoList[index];
contentController.text = memo.content;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: index,
),
),
);
createMemo({required String content}) {
Memo memo = Memo(content: content);
memoList.add(memo);
}
memoService.createMemo(content: '');
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoService.memoList.length - 1,
),
),
);
notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
updateMemo({required int index, required String content}) {
Memo memo = memoList[index];
memo.content = content;
notifyListeners();
}
memoService.updateMemo(index: index, content: value);
deleteMemo({required int index}) {
memoList.removeAt(index);
notifyListeners();
}
memoService.deleteMemo(index: index);
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'memo_service.dart';
late SharedPreferences prefs;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
prefs = await SharedPreferences.getInstance();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => MemoService()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
// 홈 페이지
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Consumer<MemoService>(
builder: (context, memoService, child) {
// memoService로 부터 memoList 가져오기
List<Memo> memoList = memoService.memoList;
return Scaffold(
appBar: AppBar(
title: Text("mymemo"),
),
body: memoList.isEmpty
? Center(child: Text("메모를 작성해 주세요"))
: ListView.builder(
itemCount: memoList.length, // memoList 개수 만큼 보여주기
itemBuilder: (context, index) {
Memo memo = memoList[index]; // index에 해당하는 memo 가져오기
return ListTile(
// 메모 고정 아이콘
leading: IconButton(
icon: Icon(memo.isPinned
? CupertinoIcons.pin_fill
: CupertinoIcons.pin),
onPressed: () {
memoService.updatePinMemo(index: index);
},
),
// 메모 내용 (최대 3줄까지만 보여주도록)
title: Text(
memo.content,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
onTap: () {
// 아이템 클릭시
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: index,
),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// + 버튼 클릭시 메모 생성 및 수정 페이지로 이동
memoService.createMemo(content: '');
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(
index: memoService.memoList.length - 1,
),
),
);
},
),
);
},
);
}
}
// 메모 생성 및 수정 페이지
class DetailPage extends StatelessWidget {
DetailPage({super.key, required this.index});
final int index;
TextEditingController contentController = TextEditingController();
@override
Widget build(BuildContext context) {
MemoService memoService = context.read<MemoService>();
Memo memo = memoService.memoList[index];
contentController.text = memo.content;
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
onPressed: () {
// 삭제 버튼 클릭시
showDeleteDialog(context, memoService);
},
icon: Icon(Icons.delete),
)
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: contentController,
decoration: InputDecoration(
hintText: "메모를 입력하세요",
border: InputBorder.none,
),
autofocus: true,
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
onChanged: (value) {
// 텍스트필드 안의 값이 변할 때
memoService.updateMemo(index: index, content: value);
},
),
),
);
}
void showDeleteDialog(BuildContext context, MemoService memoService) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("정말로 삭제하시겠습니까?"),
actions: [
// 취소 버튼
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("취소"),
),
// 확인 버튼
TextButton(
onPressed: () {
memoService.deleteMemo(index: index);
Navigator.pop(context); // 팝업 닫기
Navigator.pop(context); // HomePage 로 가기
},
child: Text(
"확인",
style: TextStyle(color: Colors.pink),
),
),
],
);
},
);
}
}
import 'dart:convert';
import 'package:flutter/material.dart';
import 'main.dart';
// Memo 데이터의 형식을 정해줍니다. 추후 isPinned, updatedAt 등의 정보도 저장할 수 있습니다.
class Memo {
Memo({
required this.content,
this.isPinned = false,
});
String content;
bool isPinned;
Map toJson() {
return {
'content': content,
'isPinned': isPinned,
};
}
factory Memo.fromJson(json) {
return Memo(
content: json['content'],
isPinned: json['isPinned'] ?? false,
);
}
}
// Memo 데이터는 모두 여기서 관리
class MemoService extends ChangeNotifier {
MemoService() {
loadMemoList();
}
List<Memo> memoList = [
Memo(content: '장보기 목록: 사과, 양파'), // 더미(dummy) 데이터
Memo(content: '새 메모'), // 더미(dummy) 데이터
];
createMemo({required String content}) {
Memo memo = Memo(content: content);
memoList.add(memo);
notifyListeners(); // Consumer<MemoService>의 builder 부분을 호출해서 화면 새로고침
saveMemoList();
}
updateMemo({required int index, required String content}) {
Memo memo = memoList[index];
memo.content = content;
notifyListeners();
saveMemoList();
}
updatePinMemo({required int index}) {
Memo memo = memoList[index];
memo.isPinned = !memo.isPinned;
memoList = [
...memoList.where((element) => element.isPinned),
...memoList.where((element) => !element.isPinned)
];
notifyListeners();
saveMemoList();
}
deleteMemo({required int index}) {
memoList.removeAt(index);
notifyListeners();
saveMemoList();
}
saveMemoList() {
List memoJsonList = memoList.map((memo) => memo.toJson()).toList();
// [{"content": "1"}, {"content": "2"}]
String jsonString = jsonEncode(memoJsonList);
// '[{"content": "1"}, {"content": "2"}]'
prefs.setString('memoList', jsonString);
}
loadMemoList() {
String? jsonString = prefs.getString('memoList');
// '[{"content": "1"}, {"content": "2"}]'
if (jsonString == null) return; // null 이면 로드하지 않음
List memoJsonList = jsonDecode(jsonString);
// [{"content": "1"}, {"content": "2"}]
memoList = memoJsonList.map((json) => Memo.fromJson(json)).toList();
}
}
'Spring & Backend' 카테고리의 다른 글
| [왕초보] 플러터(Flutter)로 시작하는 앱개발 5주차 - 스파르타코딩클럽 (0) | 2023.04.11 |
|---|---|
| [왕초보] 플러터(Flutter)로 시작하는 앱개발 4주차 - 스파르타코딩클럽 (0) | 2023.04.11 |
| [왕초보] 플러터(Flutter)로 시작하는 앱개발 2주차 - 스파르타코딩클럽 (0) | 2023.04.11 |
| [왕초보] 플러터(Flutter)로 시작하는 앱개발 1주차 - 스파르타코딩클럽 (0) | 2023.04.11 |
| [왕초보] 마케터, 기획자를 위한 실전 데이터 분석 5주차[파이썬,python] -스파르타코딩클럽 (0) | 2023.04.11 |
