본문 바로가기

아카이브/Flutter

[Local Storage] SQFLite, Bloc Pattern활용

Flutter version 2.2.3
Dart version 2.13.4

0. 패키지 설치

VSCode의 확장프로그램, Pubspec Assist

  • Flutter 프로젝트를 진행할 때, 여러모로 편리하다.

Command + Shift + P
패키지명 입력

  • Flutter 프로젝트의 pubspec.yaml에서 command + shift + P에서 "add"를 입력한 다음, 패키지 명을 입력하면 패키지의 최신버전을 불러와준다.
이 프로젝트에서 사용한 패키지들 null-safety가 적용된 버전이다.
get: ^4.1.4 // https://pub.dev/packages/get
intl: ^0.17.0 // https://pub.dev/packages/intl
path_provider: ^2.0.2 // https://pub.dev/packages/path_provider
sqflite: ^2.0.0+3 // https://pub.dev/packages/sqflite

 

1. database_helper.dart

final String todoTable = 'todo';
final String todoMemoTable = 'todoMemo';

class DBHelper {
  DBHelper() : super();
  
  // 싱글톤 클래스
  DBHelper._(); 
  static final DBHelper db = DBHelper._();
  static Database? _database;
  
  Future<Database> get database async {
    if (_database != null) return _database as Database;
    _database = await initDB();
    return _database as Database;
  }

  initDB() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, 'myDb.db');
    return await openDatabase(
      path, // .db 파일 경로
      version: 1,
      onCreate: (Database db, int version) async {
        // INTEGER -> int,
        // REAL -> num, int + double
        // TEXT -> String,
        // BLOB -> Uint8List,
        // sqflite는 boolean을 지원 안함 -> INTEGER, {false: 0, true: 1}로 저장
        await db.execute(
            'CREATE TABLE $todoTable (pk INTEGER PRIMARY KEY AUTOINCREMENT, todo TEXT, type TEXT, complete INTEGER)'); // [1]
        await db.execute(
            'CREATE TABLE $todoMemoTable (pk INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, todoPk INTEGER, createdAt INTEGER, FOREIGN KEY(todoPk) REFERENCES $todoTable(pk) ON DELETE CASCADE ON UPDATE NO ACTION)'); // [2]
            // 여러 테이블을 생성하고싶으면 이처럼 여러번 선언을 하던지 for 문을 사용해도 된다.
      },
    );
  }

  deleteDB() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, 'myDb.db');
    return await deleteDatabase(path); // .db 삭제
  }

  // Todo
  // Create Todo
  createTodo(Todo todo) async {
    final db = await database;
    var res = await db.insert(todoTable, todo.toJson());
    return res;
  }

  // Read Todo
  getTodo(int pk) async {
    final db = await database;
    var res = await db.query(todoTable, where: 'pk = ?', whereArgs: [pk]); // [3]
    return res.isNotEmpty ? Todo.fromJson(res.first) : Null;
  }

  // Read All Todos
  getAllTodos() async {
    final db = await database;
    var res = await db.query(todoTable);
    List<Todo> list =
        res.isNotEmpty ? res.map((c) => Todo.fromJson(c)).toList() : [];
    return list;
  }

  // Update Todo
  updateTodo(Todo todo) async {
    final db = await database;
    var res = await db.update(todoTable, todo.toJson(),
        where: 'pk = ?', whereArgs: [todo.pk]);
    return res;
  }

  // Delete Todo
  deleteTodo(int pk) async {
    final db = await database;
    db.delete(todoTable, where: 'pk = ?', whereArgs: [pk]);
  }

  // Delete All Todos
  deleteAllTodos() async {
    final db = await database;
    db.rawDelete('Delete from $todoTable');
  }

  // TodoMemo
  // Create TodoMemo
  createTodoMemo(TodoMemo todoMemo) async {
    final db = await database;
    var res = await db.insert(todoMemoTable, todoMemo.toJson());
    return res;
  }

  // Read TodoMemo
  getTodoMemo(int pk) async {
    final db = await database;
    var res = await db.query(todoMemoTable, where: 'pk = ?', whereArgs: [pk]);
    return res.isNotEmpty ? TodoMemo.fromJson(res.first) : Null;
  }

  // Read All TodoMemos
  getAllTodoMemosByTodoPk(int todoPk) async {
    final db = await database;
    var res =
        await db.query(todoMemoTable, where: 'todoPk = ?', whereArgs: [todoPk]);
    List<TodoMemo> list =
        res.isNotEmpty ? res.map((c) => TodoMemo.fromJson(c)).toList() : [];
    return list;
  }

  // Update TodoMemo
  updateTodoMemo(TodoMemo todo) async {
    final db = await database;
    var res = await db.update(todoMemoTable, todo.toJson(),
        where: 'pk = ?', whereArgs: [todo.pk]);
    return res;
  }

  // Delete TodoMemo
  deleteTodoMemo(int pk) async {
    final db = await database;
    db.delete(todoMemoTable, where: 'pk = ?', whereArgs: [pk]);
  }

  // Delete All TodoMemos
  deleteAllTodoMemos() async {
    final db = await database;
    db.rawDelete('Delete from $todoMemoTable');
  }
}
  1. 'complete INTEGER'
    • SQFlite는 boolean이 없다. true면 1, false면 0 Integer 활용
  2. 'createdAt INTEGER'
    • DateTime 형식도 없기 때문에 Integer나 String으로 변환
    • 2.2 todo_memo_model.dart 의 createdAt 부분 참조
  3. 'where: 'pk = ?', whereArgs = [pk]'
    •  ?에 순차적으로 whereArgs의 value가 들어감
    • Example
      • 'where: "col1 LIKE ? and col2 = ? and col3 = ?", whereArgs: ['$exLikeVal%', exStrVal, exIntVal],'

 

2.1 todo_model.dart

class Todo {
  int? pk;
  String? todo;
  String type;
  int? complete;

  Todo({
    this.pk,
    this.todo,
    this.type = "기타",
    this.complete = 0,
  });

  bool get getCompleteAsBool { // [1]
    return (complete == 1);
  }

  void toggleComplete() {
    if (getComplete) {
      complete = 0;
    } else {
      complete = 1;
    }
  }

  factory Todo.fromJson(Map<String, dynamic> json) => new Todo(
        pk: json["pk"],
        todo: json["todo"],
        type: json["type"],
        complete: json["complete"],
      );

  Map<String, dynamic> toJson() => {
        "pk": pk,
        "todo": todo,
        "type": type,
        "complete": complete,
      };
}
  1. bool get getCompleteAsBool
    • Integer로 저장되는 complete를 boolean으로 변환하는 getter

 

2.2 todo_memo_model.dart

class TodoMemo {
  int? pk;
  String? content;
  int? todoPk;
  int createdAt;

  TodoMemo({
    this.pk,
    this.content,
    this.todoPk,
    createdAt,
  }) : createdAt = DateTime.now().millisecondsSinceEpoch; // [1]

  DateTime getCreatedAtDateTime() {
    return DateTime.fromMillisecondsSinceEpoch(createdAt);
  }

  factory TodoMemo.fromJson(Map<String, dynamic> json) => new TodoMemo(
        pk: json["pk"],
        content: json["content"],
        todoPk: json["todoPk"],
        createdAt: json["createdAt"],
      );

  Map<String, dynamic> toJson() => {
        "pk": pk,
        "content": content,
        "todoPk": todoPk,
        "createdAt": createdAt,
      };
}

 

  1.  ': createdAt = DateTime.now().millisecondsSinceEpoch'
    • 현재시간을 밀리초로 환산하여 Integer로 반환

 

3.1 todo_bloc.dart

class TodoBloc {
  TodoBloc() {
    getTodos();
  }
  
  final _todosController = StreamController<List<Todo>>.broadcast(); // [1]
  
  get todos => _todosController.stream;
  
  dispose() {
    _todosController.close();
  }

  getTodos() async {
    _todosController.sink.add(await DBHelper().getAllTodos());
  }

  addTodos(Todo todo) async {
    await DBHelper().createTodo(todo);
    getTodos();
  }

  deleteTodo(int pk) async {
    await DBHelper().deleteTodo(pk);
    getTodos();
  }

  deleteAll() async {
    await DBHelper().deleteAllTodos();
    getTodos();
  }

  updateTodo(Todo todo) async {
    await DBHelper().updateTodo(todo);
    getTodos();
  }
}
  1. 'StreamController.broadcast()'
    • Single Subscription인 스트림은 한군데에서 리슨할 수 있지만, 이를 여러 군데에서 리슨할 수 있도록 변경
    • 같은 데이터를 다른 뷰에서 처리할 수 있게된다.

 

3.2 todo_memo_bloc.dart

class TodoMemoBloc {
  int todoPk;

  TodoMemoBloc(this.todoPk) {
    getTodoMemos(todoPk);
  }

  final _todoMemosController = StreamController<List<TodoMemo>>.broadcast();
  
  get todoMemos => _todoMemosController.stream;

  dispose() {
    _todoMemosController.close();
  }

  getTodoMemos(int todoPk) async {
    _todoMemosController.sink
        .add(await DBHelper().getAllTodoMemosByTodoPk(todoPk));
  }

  addTodos(TodoMemo todoMemo) async {
    await DBHelper().createTodoMemo(todoMemo);
    getTodoMemos(todoPk);
  }

  deleteTodo(int pk) async {
    await DBHelper().deleteTodoMemo(pk);
    getTodoMemos(todoPk);
  }

  deleteAll() async {
    await DBHelper().deleteAllTodoMemos();
    getTodoMemos(todoPk);
  }

  updateTodo(TodoMemo todoMemo) async {
    await DBHelper().updateTodoMemo(todoMemo);
    getTodoMemos(todoPk);
  }
}

 

4. main.dart

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp( // [1]
      title: 'Todo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        accentColor: Color(0xFF6F35A5),
        canvasColor: const Color(0xFFfafafa),
        scaffoldBackgroundColor: Colors.white,
        primaryColor: Color(0xFFF1E6FF),
        textTheme:
            Theme.of(context).textTheme.apply(bodyColor: Color(0xFF3C4046)),
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      initialRoute: '/',
      getPages: [
        GetPage(
            name: '/', page: () => TodoList(), transition: Transition.fadeIn),
        GetPage(
            name: '/memos/:todoPk',
            page: () => TodoMemoList(),
            transition: Transition.fade),
      ],
    );
  }
}
  1. 'GetMaterialApp'
    • GetX의 routes, snackbars, i18n, bottomSheets, dialogs, 고수준 api들을 컨텍스트 없이 사용하기 위해 필요하다.

 

5. todo_list.dart

class TodoList extends StatefulWidget {
  TodoList({Key? key}) : super(key: key);

  @override
  _TodoListState createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  List<Todo> todosDatas = [
    Todo(todo: '메모 내용이양', type: '했어!', complete: 1),
    Todo(todo: '메모 내용이에용', type: '해야되!', complete: 0),
    Todo(todo: '메모 내용!', type: '해야될까?', complete: 0),
  ];

  final TodoBloc todoBloc = TodoBloc();
  
  @override
  void dispose() {
    super.dispose();
    todoBloc.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Todo'),
        actions: [
          IconButton(
              onPressed: () {
                DBHelper().deleteDB();
              },
              icon: Icon(Icons.delete))
        ],
      ),
      body: StreamBuilder(
        stream: todoBloc.todos,
        builder: (BuildContext context, AsyncSnapshot<List<Todo>> snapshot) {
          return snapshot.hasData
              ? ListView.builder(
                  itemCount: snapshot.data?.length,
                  itemBuilder: (BuildContext context, int index) {
                    Todo item = snapshot.data![index];
                    return Dismissible(
                      key: UniqueKey(),
                      onDismissed: (direction) {
                        todoBloc.deleteTodo(item.pk as int);
                      },
                      child: ListTile(
                        onTap: () {
                          Get.toNamed("/memos/${item.pk}");
                        },
                        leading: Text(
                          item.pk.toString(),
                        ),
                        title: Text(item.todo as String),
                        subtitle: Text(item.type),
                        trailing: Checkbox(
                          onChanged: (bool? value) {
                            item.toggleComplete();
                            todoBloc.updateTodo(item);
                          },
                          value: item.complete == 1 ? true : false,
                        ),
                      ),
                    );
                  },
                )
              : Center(
                  child: Center(
                    child: Text('아무것도 엄써 ㅇㅅㅇ!'),
                  ),
                );
        },
      ),
      floatingActionButton: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          FloatingActionButton(
            heroTag: null,
            child: Icon(Icons.remove),
            onPressed: () {
              todoBloc.deleteAll();
            },
          ),
          SizedBox(
            height: 16.0,
          ),
          FloatingActionButton(
            heroTag: null,
            child: Icon(Icons.add),
            onPressed: () {
              Todo newTodo = todosDatas[Random().nextInt(todosDatas.length)];
              todoBloc.addTodos(newTodo);
            },
          ),
        ],
      ),
    );
  }
}

todo_list.dart

 

6. todo_memo_list.dart

class TodoMemoList extends StatefulWidget {
  const TodoMemoList({Key? key}) : super(key: key);

  @override
  _TodoMemoListState createState() => _TodoMemoListState();
}

class _TodoMemoListState extends State<TodoMemoList> {

  final TodoMemoBloc todoMemoBloc =
      TodoMemoBloc(int.parse(Get.parameters['todoPk']!));
      
  int? todoPk;
  
  @override
  void initState() {
    super.initState();
    todoPk = int.parse(Get.parameters['todoPk']!);
  }

  @override
  void dispose() {
    super.dispose();
    todoMemoBloc.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Todo Memos $todoPk'),
        actions: [
          IconButton(
            icon: Icon(Icons.add),
            onPressed: () {
              TodoMemo todoMemo = TodoMemo(
                content: '메모 내용입니다.',
                todoPk: todoPk,
              );
              todoMemoBloc.addTodos(todoMemo);
            },
          )
        ],
      ),
      body: StreamBuilder(
        stream: todoMemoBloc.todoMemos,
        builder:
            (BuildContext context, AsyncSnapshot<List<TodoMemo>> snapshot) {
          return snapshot.hasData
              ? ListView.builder(
                  itemCount: snapshot.data?.length,
                  itemBuilder: (BuildContext context, int index) {
                    TodoMemo item = snapshot.data![index];
                    return Dismissible(
                      key: UniqueKey(),
                      onDismissed: (direction) {
                        todoMemoBloc.deleteTodo(item.pk as int);
                      },
                      child: ListTile(
                        leading: Text(
                          item.pk.toString(),
                        ),
                        title: Text(item.content as String),
                        subtitle: Text(
                          DateFormat('yyyy년 MM월 dd일 – kk시 mm분')
                              .format(item.getCreatedAtDateTime()),
                        ),
                      ),
                    );
                  },
                )
              : Center(
                  child: Center(
                    child: Text('아무것도 엄써 ㅇㅅㅇ~'),
                  ),
                );
        },
      ),
    );
  }
}

todo_memo_list.dart

 

https://alexband.tistory.com/55
 

Flutter Local Storage (SQLite) 사용하기 with BLoC Pattern

Flutter 에서 sqflite 를 사용하여 로컬 에서 데이터를 관리 해 봅시다. 참고로 sqflite 는 flutter 에서 sqlite 의 사용을 도와주는 패키지 이름 입니다. 기본적인 개발환경은 구성이 되어있다는 전제하에

alexband.tistory.com

https://devmemory.tistory.com/13
 

Flutter - When you store data in SQFLlite

기본적으로 SQLite에서는 NULL, INTEGER, REAL, TEXT, BLOB형태로 저장이 됩니다 NULL은 데이터 값이 비어있을때 INTEGER는 정수형 (int value, DateTime, bool 등) DateTime의 경우 INSERT할 때..

devmemory.tistory.com

https://software-creator.tistory.com/9
 

Flutter - 스트림. 다트에서 비동기 프로그래밍

Flutter - 스트림. 다트에서 비동기 프로그래밍 목차 스트림이란? 스트림 간단한 예제 스트림 다양하게 처리하기 스트림 내부 구조 서브스크립션 브로드 캐스트 스트림 컨트롤러 1. Stream이란? 스트

software-creator.tistory.com