[왕초보] 플러터(Flutter)로 시작하는 앱개발 3주차 - 스파르타코딩클럽
https://pub.dev/packages/google_fonts
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 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 패키지 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();
}
}
'4차산업혁명의 일꾼 > 앱' 카테고리의 다른 글
[왕초보] 플러터(Flutter)로 시작하는 앱개발 5주차 - 스파르타코딩클럽 (0) | 2023.04.11 |
---|---|
[왕초보] 플러터(Flutter)로 시작하는 앱개발 4주차 - 스파르타코딩클럽 (0) | 2023.04.11 |
[왕초보] 플러터(Flutter)로 시작하는 앱개발 2주차 - 스파르타코딩클럽 (0) | 2023.04.11 |
[왕초보] 플러터(Flutter)로 시작하는 앱개발 1주차 - 스파르타코딩클럽 (0) | 2023.04.11 |
Expo + React 로 앱만들기(vscode) (0) | 2022.07.20 |