20일차에서는 로컬에 데이터를 저장하는 방법을 학습하고 이를 사용해 기존에 제작했던 키오스크 앱을 업그레이드 했다.
학습한 내용
- shared_preferences
- 키오스크 앱 업그레이드
오늘은 패키지가 하나라서 링크 이외에 사용법도 간단히 정리하고자한다.
- shared_preferences
- 로컬에 앱 데이터를 저장하고 불러올 수 있다.
- 로컬 기반의 어플에 사용할 수 있다.
- 자동로그인, 장바구니 등의 기능을 구현할 수 있다.
pubspec.yaml
파일에 shared_preferences
플러그인을 추가한다.(플러그인 관련된 내용은 [Flutter] 스나이퍼팩토리 19일차를 참고)
dependencies:
flutter:
sdk: flutter
shared_preferences: "<newest version>"
데이터 저장
SharedPreferences
클래스가 제공하는 setter
메소드를 사용하여 데이터를 저장한다.
setter
메소드는 setInt
setBool
setString
등의 다양한 타입을 지원한다.
아래 코드처럼 SharedPreferences
객체를 초기화 하고 값을 저장하는데 setter
메소드에서 앞의 값은 키 값이고 뒤의 값은 저장할 밸류값이다.
// shared preferences 얻기
final prefs = await SharedPreferences.getInstance();
// 값 저장하기
prefs.setInt('counter', counter);
데이터 읽기
SharedPreferences
클래스가 제공하는 getter
메소드를 사용하여 데이터를 저장한다.
setter
와 마찬가지로 getInt
getBool
getString
등이 있다.
final prefs = await SharedPreferences.getInstance();
// counter 키에 해당하는 데이터 읽기를 시도합니다. 만약 존재하지 않는 다면 0을 반환합니다.
final counter = prefs.getInt('counter') ?? 0;
remove()
메소드를 사용해 데이터를 삭제할 수 있다.final prefs = await SharedPreferences.getInstance();
prefs.remove('counter');
하지만 shared_preferences
는 아래와 같이 두 가지 제약 사항이 존재한다.
shared_preferences의 제약 사항
- 오직 원시 타입만 사용 가능하다.(int, double, bool, string, stringList)
- 대용량 데이터 저장을 위해 설계되지 않았다.
위의 shared_preferences
는 Flutter 공식 문서를 참고하여 작성되었으며 더 자세한 내용은 아래의 링크를 참고
디스크에 키-값 데이터 저장하기 - Flutter
shared preferences 이외에도 로컬에 데이터를 저장하는 패키지 중 하나인 Hive가 있다.
Hive에 대한 기본적인 사용법과 설명은 [Flutter] 스나이퍼팩토리 4주차 도전하기의 추가 내용 정리에 작성했다.
해당 파트에서는 재미로 Hive의 성능에 대해서만 알아보고자 한다.
Hive에서 SQLite, shared preferences와 퍼포먼스릴 비교하여 올린 자료가 있는데 아래와 같다.
위의 결과를 보면 Hive는 다른 로컬 데이터베이스에 비해 높은 성능을 가진다.
자세한 Hive의 사용법이나 설명은 아래의 Hive 공식 문서를 참고
Hive Docs
- 키오스크 앱 업그레이드 (2)
처음 만들었던 키오스크 앱과 한 번 업그레이드를 진행했던 키오스크 앱에 관련된 내용은 아래 링크에서 확인할 수 있다.
- 키오스크 앱 기반
[Flutter] 스나이퍼팩토리 2주차 주간평가 : 키오스크 앱 기반 작성- 키오스크 앱 업그레이드 (1)
[Flutter] 스나이퍼팩토리 12일차 part1
코드는 전체 코드를 첨부하지만 설명은 기존에 만들었던 앱에서 추가된 사항만 작성했다.
dependencies:
dio: ^5.0.0
shared_preferences: ^2.0.17
pubspec.yaml
에 네트워크 통신을 위해 필요한 dio
를 설치하고, 로컬에 데이터를 저장하기 위해 shared_preferences
를 설치했다.
import 'package:flutter/material.dart';
import 'package:kiosk_app/pages/main_page.dart';
void main() {
runApp(const KioskApp());
}
class KioskApp extends StatelessWidget {
const KioskApp({super.key});
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp(
home: MainPage(),
);
}
}
main.dart
에서는 MainPage
위젯을 호출한다. 기존에 앱과 다르게 MainPage
를 다른 파일로 작성하여 호출했다.
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:kiosk_app/pages/admin_page.dart';
import 'package:shared_preferences/shared_preferences.dart';
class MainPage extends StatefulWidget {
const MainPage({super.key});
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
List<String> order = []; //주문 리스트
Dio dio = Dio(); //Dio 객체
Future? result; // 데이터 요청 결과
SharedPreferences? prefs; //SharedPreferences 객체
getData() async {
var url =
'http://52.79.115.43:8090/api/collections/options/records'; //데이터 요청 url
return await dio
.get(url)
.then((value) => value.data['items']); // 데이터 요청 및 반환
}
void initPreferences() async {
prefs = await SharedPreferences.getInstance(); //객체 초기화
if (prefs != null) {
order = prefs!.getStringList('order') ?? []; //저장된 주문 리스트 불러오기
setState(() {}); //화면 다시 그리기
}
}
void initState() {
super.initState();
initPreferences(); //주문 리스트 초기화
result = getData(); // 데이터 초기화
}
//페이지 이동 라우트 생성
Route _createRoute(Widget page) {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
var begin = Offset(1.0, 0.0); //오른쪽 위 시작 지점
var end = Offset.zero; //왼쪽 위 끝 지점
var curve = Curves.ease;
var tween =
Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
);
}
Widget build(BuildContext context) {
return Scaffold(
//앱바
appBar: AppBar(
title: GestureDetector(
//앱바 더블탭 이벤트 (어드민 페이지 이동)
onDoubleTap: () => Navigator.of(context).push(
_createRoute(AdminPage()),
),
child: Text('분식왕 이테디 주문하기'),
),
centerTitle: true, //타이틀 글씨 가운데 정렬
backgroundColor: Colors.transparent,
foregroundColor: Colors.black,
elevation: 0,
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
'주문 리스트',
),
//주문할 내역 출력(없을 경우 텍스트, 있을 경우 Chip)
if (order.isEmpty)
Center(child: Text('주문한 메뉴가 없습니다'))
else
Wrap(
children: List.generate(order.length, (index) {
return Chip(
label: Text(order[index]),
onDeleted: () => setState(() {
order.removeAt(index); // 주문 제거
if (prefs != null) {
prefs!
.setStringList('order', order); //로컬에 주문 리스트 저정
}
}));
})),
SizedBox(height: 8),
Text(
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
'음식',
),
Expanded(
child: FutureBuilder(
future: result,
builder: (context, snapshot) {
if (snapshot.hasData) {
// 음식 리스트 그리드 뷰
return GridView.count(
crossAxisCount: 3,
children: List.generate(snapshot.data.length, (index) {
return GestureDetector(
onTap: () => setState(() {
order.add(snapshot.data[index]['menu']);
if (prefs != null) {
prefs!.setStringList(
'order', order); //로컬에 주문 리스트 저정
}
}),
// 하나의 음식 요소
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Image(
image: NetworkImage(
snapshot.data[index]['imageUrl'],
),
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
fit: BoxFit.cover,
),
),
Text(
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
),
snapshot.data[index]['menu'].toString(),
),
Text(
style: TextStyle(
color: Colors.black,
),
'[담기]',
),
],
),
),
);
}),
);
} else {
return CircularProgressIndicator();
}
},
),
),
],
),
),
// 하단 버튼(주문 내역이 있을 경우에만 표시)
floatingActionButton: AnimatedOpacity(
opacity: order.isNotEmpty ? 1.0 : 0.0,
duration: const Duration(milliseconds: 500),
child: FloatingActionButton.extended(
onPressed: () {},
label: Text('결제하기'),
),
),
// 하단 버튼 위치
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
}
기존의 리스트 형태로 직접 변수를 만들어 데이터를 저장하던 코드를 지우고 네트워크에서 데이터를 불러오도록 수정했다.
네트워크 데이터 요청은 Dio
객체와 결과를 저장할 result
를 선언하고 데이터를 불러오는 getaData()
함수를 작성했다. 이 함수는 initState()
에서 초기화 했다.
음식 리스트를 출력하던 GridView
를 FutureBuilder
로 감싸 snapshot
으로 불러온 데이터를 출력했다.
다음으로 주문 리스트를 출력하던 리스트 뷰는 길어졌을 때 자동으로 줄 바꿈을 하도록 수정하기 위해 Wrap
위젯으로 변경했다.
마지막으로 로컬에 데이터를 저장하고 불러오는 기능을 작성했다. 우선 SharedPreferences
변수를 선언하고 이를 초기화하고 로컬에 있는 주문 리스트를 불러오는 initPreferences()
함수를 작성했다. 이번에는 FutureBuilder
를 사용하지 않고 setState()
를 호출해 화면을 다시 그려주는 방식을 사용해 보았다.
initPreferences()
역시 initState()
에서 호출해 초기화를 했다.
각 주문의 Chip
위젯이 삭제될 때와 음식 리스트의 Card
가 눌렸을 때, 각각 같은 방식으로 로컬에 데이터를 저장하는 기능을 추가했다.
import 'package:flutter/material.dart';
class AdminPage extends StatelessWidget {
const AdminPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Admin Page'),
centerTitle: true,
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text('메뉴 추가'),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text('메뉴 삭제'),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text('메뉴 수정'),
),
],
),
);
}
}
AdminPage
는 기존의 코드와 동일하게 특별한 기능을 수행하지 않는 화면이다.
내일 진행할 월간평가를 위해 오늘도 과제와 학습한 내용이 많지는 않다. (복습해야지...ㅠ) 오늘도 내용이 적어서 추가 내용 정리를 어떤걸 할까 고민하다가 어제 시리님이 알려주신 메소드 채널에 대해 정리하려고 했지만.........내용을 읽어봐도 아직 이해하지 못했다ㅎㅎ😅 이 내용은 나중에 더 찾아봐야 될 것 같다. 그래서 결국 오늘 사용한 shared_preferences를 조금 자세히 작성해 봤다.
일단 메소드 채널에 대해 학습 중인 내용은 아래 공식문서이다. (이해는 못했지만... ㅋㅋㅋㅋ)