728x90
SQLite 로컬DB사용

1. 패키지 2개 설치
flutter pub add sqflite path
2. DB 헬퍼 클래스 만들기 (싱글톤 패턴)
// db_helper.dart
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DBHelper {
// 1. C++ 스타일의 싱글톤 인스턴스 선언
static final DBHelper _instance = DBHelper._internal();
factory DBHelper() => _instance;
DBHelper._internal();
static Database? _database;
// 2. DB 객체 가져오기 (Lazy Initialization)
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB();
return _database!;
}
// 3. DB 파일 생성 및 오픈
Future<Database> _initDB() async {
// 기기 내부의 안전한 가상 문서 경로를 찾아 줍니다.
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'pos_system.db');
// DB 오픈 (파일이 없으면 onCreate를 실행하여 테이블을 만듭니다)
return await openDatabase(
path,
version: 1,
onCreate: (db, version) async {
// 전통적인 SQL 쿼리로 테이블을 생성합니다.
await db.execute('''
CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
amount INTEGER,
date TEXT
)
''');
},
);
}
}
3. CRUD 비즈니스 로직 작성 (Provider 결합)
// local_order_provider.dart
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
import 'db_helper.dart'; // 방금 만든 DB 헬퍼
class LocalOrderProvider extends ChangeNotifier {
List<Map<String, dynamic>> _localOrders = [];
List<Map<String, dynamic>> get localOrders => _localOrders;
// [Read] 데이터 조회 (SELECT)
Future<void> loadOrders() async {
final db = await DBHelper().database;
// SQL 쿼리를 실행하여 결과를 List<Map> 형태로 받습니다.
// Delphi의 Query.Open 후 Record셋을 받는 것과 같습니다.
_localOrders = await db.query('orders', orderBy: 'id DESC');
notifyListeners(); // 화면 갱신 방송
}
// [Create] 데이터 추가 (INSERT)
Future<void> addOrder(String title, int amount) async {
final db = await DBHelper().database;
// 파라미터 바인딩을 통해 SQL 인젝션을 방어하며 데이터를 집어넣습니다.
await db.insert(
'orders',
{
'title': title,
'amount': amount,
'date': DateTime.now().toString().substring(0, 19), // 현재 시간
},
conflictAlgorithm: ConflictAlgorithm.replace, // 충돌 시 덮어쓰기
);
// 데이터를 추가했으니 리스트를 새로고침합니다.
await loadOrders();
}
}
4단계: UI 화면 구성 및 앱 구동 시 초기화
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:http/http.dart' as http;
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
// 💡 [중요] 이미 만들어두신 로컬 DB 프로바이더 파일을 임포트합니다.
// 파일명이 다르면 실제 파일명에 맞게 수정해주세요.
import 'local_order_provider.dart';
// ==========================================================================
// 1. REST API용 모델 및 프로바이더 (임시로 main에 유지)
// ==========================================================================
class Order {
final int id;
final String title;
final int amount;
Order({required this.id, required this.title, required this.amount});
}
class OrderProvider extends ChangeNotifier {
List<Order> _orders = [];
bool _isLoading = false;
List<Order> get orders => _orders;
bool get isLoading => _isLoading;
Future<void> fetchOrders() async {
_isLoading = true;
notifyListeners();
try {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
if (response.statusCode == 200) {
final List<dynamic> jsonData = jsonDecode(response.body);
_orders = jsonData.take(5).map((item) => Order(
id: item['id'],
title: item['title'].toString().substring(0, 10),
amount: 15000 + (item['id'] as int) * 1000,
)).toList();
}
} catch (e) {
debugPrint('API 에러: $e');
} finally {
_isLoading = false;
notifyListeners();
}
}
}
// ==========================================================================
// 2. 앱 진입점 및 의존성 주입 (Windows FFI 초기화 포함)
// ==========================================================================
void main() {
WidgetsFlutterBinding.ensureInitialized();
// Windows 데스크톱 환경을 위한 SQLite 팩토리 초기화
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
}
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => OrderProvider()),
// 💡 외부에서 임포트한 LocalOrderProvider를 이곳에 주입합니다.
ChangeNotifierProvider(create: (context) => LocalOrderProvider()),
],
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '통합 테스트 앱',
theme: ThemeData(primarySwatch: Colors.blue),
home: const MainNavigationPage(),
);
}
}
// ==========================================================================
// 3. UI 화면 구성 (탭 네비게이션)
// ==========================================================================
class MainNavigationPage extends StatefulWidget {
const MainNavigationPage({super.key});
@override
State<MainNavigationPage> createState() => _MainNavigationPageState();
}
class _MainNavigationPageState extends State<MainNavigationPage> {
int _selectedIndex = 0;
final List<Widget> _pages = [
const ApiOrderPage(),
const LocalDbPage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _pages[_selectedIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) => setState(() => _selectedIndex = index),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.cloud), label: 'REST API'),
BottomNavigationBarItem(icon: Icon(Icons.storage), label: '로컬 SQLite'),
],
),
);
}
}
// [탭 1] REST API 화면
class ApiOrderPage extends StatelessWidget {
const ApiOrderPage({super.key});
@override
Widget build(BuildContext context) {
final provider = context.watch<OrderProvider>();
return Scaffold(
appBar: AppBar(title: const Text('REST API 연동')),
body: Center(
child: _buildBody(provider),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<OrderProvider>().fetchOrders(),
child: const Icon(Icons.download),
),
);
}
Widget _buildBody(OrderProvider provider) {
if (provider.isLoading) return const CircularProgressIndicator();
if (provider.orders.isEmpty) return const Text('하단 다운로드 버튼을 눌러주세요.');
return ListView.builder(
itemCount: provider.orders.length,
itemBuilder: (context, index) {
final order = provider.orders[index];
return ListTile(
title: Text(order.title),
trailing: Text('${order.amount}원'),
);
},
);
}
}
// [탭 2] 로컬 DB 화면 (임포트한 LocalOrderProvider 사용)
class LocalDbPage extends StatefulWidget {
const LocalDbPage({super.key});
@override
State<LocalDbPage> createState() => _MyLocalDbPageState();
}
class _MyLocalDbPageState extends State<LocalDbPage> {
@override
void initState() {
super.initState();
// 화면 로드 시 분리된 프로바이더의 조회 함수를 호출합니다.
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<LocalOrderProvider>().loadOrders();
});
}
@override
Widget build(BuildContext context) {
// 임포트된 프로바이더의 상태를 관찰합니다.
final provider = context.watch<LocalOrderProvider>();
return Scaffold(
appBar: AppBar(title: const Text('로컬 SQLite DB')),
body: provider.localOrders.isEmpty
? const Center(child: Text('우측 하단 버튼을 눌러 데이터를 추가하세요.'))
: ListView.builder(
itemCount: provider.localOrders.length,
itemBuilder: (context, index) {
final order = provider.localOrders[index];
return ListTile(
leading: Text('${order['id']}'),
title: Text(order['title']),
subtitle: Text(order['date']),
trailing: Text('${order['amount']}원'),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<LocalOrderProvider>().addOrder('로컬 주문 결제', 18000),
child: const Icon(Icons.add),
),
);
}
}
DB로 insert 후 다시 로드하고 리스너에게 갱신..
메인에서는 MultiProvider로 LocalOrderProvider를 생성해놔서 받아서 갱신
728x90
'운동하는 개발자 > Flutter' 카테고리의 다른 글
| 13년차 응용프로그래머, Gemini에게 Flutter 배우기 - 7 MVVM (2) (0) | 2026.06.06 |
|---|---|
| 13년차 응용프로그래머, Gemini에게 Flutter 배우기 - 6 MVVM (1) (0) | 2026.06.03 |
| 13년차 응용프로그래머, Gemini에게 Flutter 배우기 - 4 RestAPI (0) | 2026.05.27 |
| 13년차 응용프로그래머, Gemini에게 Flutter 배우기 - 3 (0) | 2026.05.27 |
| 13년차 응용프로그래머, Gemini에게 Flutter 배우기 - 2 (0) | 2026.05.27 |