Last active
June 29, 2023 11:24
-
-
Save jinyongp/beb656ed3cd6059afac59277a1d6ade8 to your computer and use it in GitHub Desktop.
Flutter MyMemo App (CRUD)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ignore_for_file: prefer_const_constructors, must_be_immutable | |
import 'dart:convert'; | |
import 'package:flutter/foundation.dart' show kIsWeb; | |
import 'package:flutter/cupertino.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:intl/intl.dart'; | |
import 'package:provider/provider.dart'; | |
import 'package:shared_preferences/shared_preferences.dart'; | |
void main() async { | |
WidgetsFlutterBinding.ensureInitialized(); | |
late SharedPreferences pref; | |
if (!kIsWeb) { | |
pref = await SharedPreferences.getInstance(); | |
} | |
runApp(MultiProvider( | |
providers: [ | |
ChangeNotifierProvider( | |
create: (_) => MemoService( | |
save: (String payload) async => await pref.setString("memo", payload), | |
load: () async => pref.getString("memo"), | |
), | |
), | |
], | |
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) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text("My Memo"), | |
), | |
body: memoService.isEmpty | |
? Center(child: Text("메모를 작성해주세요")) | |
: ListView.builder( | |
itemCount: memoService.count, | |
itemBuilder: (context, index) { | |
return memoListItem(index, () { | |
Navigator.push( | |
context, | |
MaterialPageRoute( | |
builder: (_) => DetailPage(index: index)), | |
); | |
}); | |
}, | |
), | |
floatingActionButton: FloatingActionButton( | |
child: Icon(Icons.add), | |
onPressed: () { | |
Navigator.push( | |
context, | |
MaterialPageRoute(builder: (_) => DetailPage()), | |
); | |
}, | |
), | |
); | |
}); | |
} | |
Widget memoListItem(int index, void Function()? onTap) { | |
MemoService memoService = context.read<MemoService>(); | |
Memo memo = memoService.memo(index); | |
DateTime updatedAt = DateTime.fromMillisecondsSinceEpoch(memo.updatedAt); | |
String formattedUpdatedAt = | |
DateFormat("yy년 MM월 dd일\nHH시 mm분 ss초").format(updatedAt); | |
return ListTile( | |
leading: IconButton( | |
icon: Icon(memo.pinned ? CupertinoIcons.pin_fill : CupertinoIcons.pin), | |
onPressed: () { | |
if (memo.pinned) { | |
memoService.unpin(index); | |
} else { | |
memoService.pin(index); | |
} | |
}, | |
), | |
title: Text( | |
memo.content, | |
maxLines: 3, | |
overflow: TextOverflow.ellipsis, | |
), | |
onTap: onTap, | |
trailing: Text( | |
formattedUpdatedAt, | |
style: TextStyle(fontSize: 12), | |
textAlign: TextAlign.right, | |
), | |
); | |
} | |
} | |
// 메모 생성 및 수정 페이지 | |
class DetailPage extends StatelessWidget { | |
DetailPage({super.key, this.index}); | |
final int? index; | |
TextEditingController contentController = TextEditingController(); | |
@override | |
Widget build(BuildContext context) { | |
final bool isEditMode = index != null; | |
MemoService memoService = context.read<MemoService>(); | |
contentController.text = isEditMode ? memoService.memo(index!).content : ""; | |
return Scaffold( | |
appBar: AppBar( | |
leading: IconButton( | |
onPressed: () { | |
if (contentController.text.isEmpty) { | |
if (isEditMode) { | |
memoService.delete(index!); | |
} | |
} else { | |
if (isEditMode) { | |
memoService.update(index!, contentController.text); | |
} else { | |
memoService.create(contentController.text); | |
} | |
} | |
Navigator.pop(context); | |
}, | |
icon: Icon(CupertinoIcons.back), | |
), | |
actions: [ | |
IconButton( | |
onPressed: () { | |
showDeleteDialog(context, index ?? -1); | |
}, | |
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, | |
), | |
), | |
); | |
} | |
void showDeleteDialog(BuildContext context, int index) { | |
MemoService memoService = context.read<MemoService>(); | |
showDialog( | |
context: context, | |
builder: (context) { | |
return CupertinoAlertDialog( | |
title: Text("메모 삭제"), | |
content: Text("정말로 삭제하시겠습니까?"), | |
actions: [ | |
CupertinoDialogAction( | |
child: Text("취소"), | |
onPressed: () { | |
Navigator.pop(context); | |
}, | |
), | |
CupertinoDialogAction( | |
child: Text("삭제"), | |
onPressed: () { | |
try { | |
memoService.delete(index); | |
} finally { | |
Navigator.pop(context); | |
Navigator.pop(context); | |
} | |
}, | |
), | |
], | |
); | |
}, | |
); | |
} | |
} | |
class Memo { | |
String _content; | |
int? _pinnedAt; | |
int _createdAt = 0; | |
int _updatedAt = 0; | |
Memo({ | |
required String content, | |
int? pinnedAt, | |
int? createdAt, | |
int? updatedAt, | |
}) : _content = content, | |
_pinnedAt = pinnedAt { | |
int ts = DateTime.now().millisecondsSinceEpoch; | |
_createdAt = createdAt ?? ts; | |
_updatedAt = updatedAt ?? ts; | |
} | |
factory Memo.create(String content) { | |
return Memo(content: content); | |
} | |
String get content { | |
return _content; | |
} | |
get pinned { | |
return _pinnedAt != null; | |
} | |
int get updatedAt { | |
return _updatedAt; | |
} | |
void update(String content) { | |
_content = content; | |
_updatedAt = DateTime.now().millisecondsSinceEpoch; | |
} | |
void pin() { | |
_pinnedAt = DateTime.now().millisecondsSinceEpoch; | |
} | |
void unpin() { | |
_pinnedAt = null; | |
} | |
int compareTo(Memo other) { | |
if (pinned && other.pinned) { | |
return _pinnedAt! - other._pinnedAt!; | |
} else if (pinned) { | |
return -1; | |
} else if (other.pinned) { | |
return 1; | |
} | |
return other.updatedAt - updatedAt; | |
} | |
Map<String, dynamic> toJson() { | |
return { | |
'content': _content, | |
'pinnedAt': _pinnedAt, | |
'createdAt': _createdAt, | |
'updatedAt': _updatedAt, | |
}; | |
} | |
factory Memo.fromJson(Map<String, dynamic> json) { | |
return Memo( | |
content: json['content'], | |
pinnedAt: json['pinnedAt'], | |
createdAt: json['createdAt'], | |
updatedAt: json['updatedAt'], | |
); | |
} | |
} | |
class MemoService extends ChangeNotifier { | |
final List<Memo> _memos = []; | |
final Future<void> Function(String payload)? save; | |
final Future<String?> Function()? load; | |
MemoService({ | |
this.save, | |
this.load, | |
}) { | |
try { | |
_load(); | |
} catch (error) { | |
// ignore | |
} | |
} | |
Memo get last => _memos.last; | |
bool get isEmpty => _memos.isEmpty; | |
int get count => _memos.length; | |
Memo memo(int index) => _memos.elementAt(index); | |
void create(String content) { | |
_memos.add(Memo.create(content)); | |
postprocess(); | |
} | |
void update(int index, String content) { | |
memo(index).update(content); | |
postprocess(); | |
} | |
void delete(int index) { | |
_memos.remove(memo(index)); | |
postprocess(); | |
} | |
void pin(int index) { | |
memo(index).pin(); | |
postprocess(); | |
} | |
void unpin(int index) { | |
memo(index).unpin(); | |
postprocess(); | |
} | |
void postprocess() { | |
_memos.sort((a, b) => a.compareTo(b)); | |
notifyListeners(); | |
_save(); | |
} | |
Future<void> _save() { | |
if (save == null) return Future.value(); | |
String payload = jsonEncode(_memos.map((m) => m.toJson()).toList()); | |
return save!(payload); | |
} | |
Future<void> _load() async { | |
if (load == null) return; | |
String? payload = await load!(); | |
if (payload == null) return; | |
_memos.clear(); | |
_memos.addAll(jsonDecode(payload).map((e) => Memo.fromJson(e))); | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment