본문 바로가기
플러터(flutter)

플러터의 Sqlite를 riverpod의 Provider로 사용하기 - 1

by 기계공학 주인장 2024. 4. 23.
반응형

플러터의 Sqlite를 riverpod의 Provider를 사용해서 구현해보겠습니다.

 

필요한 라이브러리

  1. flutter_riverpod
  2. path
  3. sqflite

각각의 라이브러리를 설치하는 방법은 아래의 링크를 참조 하시길 바랍니다.

 

https://pub.dev/packages/path/install

 

path install | Dart package

A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web.

pub.dev

 

https://pub.dev/packages/sqflite/install

 

sqflite install | Flutter package

Flutter plugin for SQLite, a self-contained, high-reliability, embedded, SQL database engine.

pub.dev

 

https://riverpod.dev/docs/introduction/why_riverpod

 

Why Riverpod? | Riverpod

What is Riverpod?

riverpod.dev

 


플러터의 Sqlite를 riverpod의 Provider를 사용하여 구현하는 순서

코드는 다음과 같은 순서로 작성할 예정입니다.

 

  1. Sqlite에서 사용할 모델 구현
  2. Sqlite을 만들고 CRUD 코드 구현
  3. Repository 생성
  4. 해당 Repository를 사용하는 Provider 구현
  5. UI에서 사용

SQLite에서 사용할 모델 구현하기

메신저의 친구를 추가하는 기능을 만들어본다고 가정하겠습니다.

 

친구의 ID, 이름, 상태 메시지, 프로필 파일의 위치를 저장하는 Model을 생성합니다.

 

// friend.dart

class Friend {
  final String id;
  final String name;
  String message;
  String profileImgPath;

  Friend({
    required this.id,
    required this.name,
    this.message = "",
    this.profileImgPath = ""
  });

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'message': message,
      'profileImgPath': profileImgPath,
    };
  }

  factory Friend.fromMap(Map<String, dynamic> map) {
    return Friend(
      id: map['id'],
      name: map['name'],
      message: map['message'],
      profileImgPath: map['profileImgPath'],
    );
  }
}

 

메시지와 그림 파일의 위치의 같은 경우에 기본 값을 줘서 값을 항상 지정할 필요 없도록 정의합니다.


Database Class 생성하기

 

다음과 같이 Database가 될 class를 생성합니다.

 

class FriendDatabase {
  // 생성자를 private로 생성하기
  FriendDatabase._();

  static final FriendDatabase db = FriendDatabase._();
  static Database? _database;

  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDB();
    return _database!;
  }

  Future<Database> _initDB() async {
    final dbPath = await getDatabasesPath();
    final path = join(dbPath, 'friend_db.db');
    return await openDatabase(path, version: 1, onCreate: _createTables);
  }

  _createTables(Database db, int version) async {
    await db.execute('''
      CREATE TABLE friend_db (
        id TEXT PRIMARY KEY,
        name TEXT,
        message TEXT,
        profileImgPath TEXT
      )
    ''');
  }

  Future<List<Friend>> getFriends() async {
    final db = await database;
    final maps = await db.query('friend_db');
    return maps.map((map) => Friend.fromMap(map)).toList();
  }

  Future<void> insertFriend(Friend friend) async {
    Database db = await database;
    await db.insert('friend_db', friend.toMap());
  }

  Future updateFriend(Friend friend) async {
    Database db = await database;
    // 해당하는 id를 찾아서 update 한다.
    await db.update('friend_db', friend.toMap(),
        where: 'id = ?', whereArgs: [friend.id]);
  }

  Future deleteFriend(String id) async {
    Database db = await database;
    await db.delete('friend_db', where: 'id = ?', whereArgs: [id]);
  }
}

 

 

위 클래스는 싱글톤으로 사용되게 하기 위해 다음과 같은 동작으로 구현되었습니다.

  1. 생성자를 private로 선언
    1. private로 선언함으로써 외부에서 새로운 인스턴스를 만들 수 없도록 했습니다.
  2. 그리고 해당 클래스를 static final로 생성해서 하나의 인스턴스만 존재하도록 했습니다.

Database Class의 경우 여러 개의 인스턴스가 존재할 필요가 없기 때문에 싱글톤 처리를 했습니다.


Database에서 데이터를 가져오는 Repository 생성

MVVM 아키텍처를 사용하기 위해 Repository 클래스를 생성합니다.

 

구성은 단순히 위에서 정의한 함수 및 클래스를 가져와서 사용하는 방식입니다.

 

class FriendRepository {
  final _friendDb = FriendDatabase.db;

  Future<List<Friend>> getFriends() async {
    return _friendDb.getFriends();
  }

  Future<void> insertFriend(Friend friend) async {
    return _friendDb.insertFriend(friend);
  }

  Future<void> updateFriend(Friend friend) async{
    return _friendDb.updateFriend(friend);
  }

  Future<void> deleteFriend(String id) async {
    return _friendDb.deleteFriend(id);
  }
}

 

Database 클래스는 이미 선언되어 있는 인스턴스 = FriendDabase db를 가져와서 사용한다.


ViewModel 만들기 = Provider 클래스 생성

이제 UI 부분에서 사용할 viewModel을 생성합니다.

 

위에서 만든 Repository 클래스와 riverpod의 Provider를 사용해서 생성합니다.

 

final friendViewModelProvider = StateNotifierProvider<FriendViewModel, List<Friend>>((ref) => FriendViewModel(FriendRepository()));

class FriendViewModel extends StateNotifier<List<Friend>> {
  final FriendRepository _friendRepository;

  FriendViewModel(this._friendRepository) : super([]) {
    getFriend();
  }

  void getFriend() async {
    state = await _friendRepository.getFriends();
  }

  void insertFriend(Friend friend) {
    _friendRepository.insertFriend(friend).then((value) {
      state = [...state, friend];
    });
  }
}

 

위 코드를 보면 getFriend 부분에는 async 처리를 하지만, insertFriend 부분에는 async 처리를 하지 않은 것을 볼 수 있습니다.

 

Future를 반환하는 함수(friendRepository.getFriends() 등)를 사용할 때 async와 await를 사용하면 해당 처리를 기다리지만,

 

사용하지 않으면 백그라운드에서 동작합니다.

 

insertFriend 부분은 굳이 기다릴 필요가 없기 때문에 async를 생략했습니다.

 

또한 insertFriend는 확실히 Sql에 데이터를 넣은 후 state에 값을 추가해야하기 때문에 then을 사용했습니다.


View에서 ViewModel를 사용하여 UI 업데이트 하기

Provider를 UI에서 감시하고 싶으면 ConsumerStateful 위젯이나 Consumer 위젯을 사용해야합니다.

 

https://android-developer.tistory.com/entry/%ED%94%8C%EB%9F%AC%ED%84%B0%EC%9D%98-Riverpod-%EC%82%AC%EC%9A%A9-%EB%B0%A9%EB%B2%95-%EA%B8%B0%EC%B4%88%EB%B6%80%ED%84%B0-%EC%9E%90%EC%84%B8%ED%9E%88-%EC%84%A4%EB%AA%85-2

 

플러터의 Riverpod 사용 방법 기초부터 자세히 설명 - 2

1편에서는 Riverpod의 설치 방법과 기본적인 정의 및 사용 방법에 대해 알아봤습니다. 플러터의 Riverpod 사용 방법 기초부터 자세히 설명 - 1 플러터에서는 상태 관리를 위해 여러 가지 라이브러리를

android-developer.tistory.com

 

class FriendScreen extends ConsumerStatefulWidget {
  const FriendScreen({super.key});

  @override
  ConsumerState<ConsumerStatefulWidget> createState() => _FriendScreen();
}

class _FriendScreen extends ConsumerState<FriendScreen> {
  var uuid = const Uuid();

  @override
  Widget build(BuildContext context) {
    final viewModel = ref.watch(friendViewModelProvider.notifier);
    final friends = ref.watch(friendViewModelProvider);

    return Scaffold(
      body: Expanded(
          child: Column(
        children: [
          Container(
            decoration: BoxDecoration(
                shape: BoxShape.rectangle,
                border: Border.all(color: Colors.grey.shade300, width: 0.5)),
            width: double.infinity,
            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
            child: const Text(
              "친구 (하단의 친구 아이콘을 눌러서 추가하세요.)",
              style: TextStyle(fontSize: 10, color: Colors.blueGrey),
            ),
          ),
          Container(
            height: 0.5,
            margin: const EdgeInsets.symmetric(vertical: 5),
          ),
          ListView.separated(
            shrinkWrap: true,
            itemBuilder: (context, index) {
              return Container(
                padding:
                    const EdgeInsets.symmetric(vertical: 10, horizontal: 4),
                child: FriendItem(
                  friends: Friend(
                    id: friends[index].id,
                    name: friends[index].name,
                    message: friends[index].message,
                    profileImgPath: friends[index].profileImgPath
                  ),
                ),
              );
            },
            separatorBuilder: (context, index) => Divider(
              color: Colors.grey.withOpacity(0.5),
              thickness: 0.5,
            ),
            itemCount: friends.length,
          ),
          Container(
            height: 0.5,
            margin: const EdgeInsets.symmetric(vertical: 10),
            color: Colors.grey.withOpacity(0.5),
          )
        ],
      )),
      floatingActionButton: Container(
        margin: const EdgeInsets.only(bottom: 50, right: 35),
        child: FloatingActionButton(
          child: const Icon(Icons.person),
          onPressed: () {
            viewModel.insertFriend(Friend(
                id: uuid.v4(), name: "name", message: "abcd"));
          },
        ),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
    );
  }
}

 

UI 동작은 다음과 같습니다.

  1. 화면을 열면 Sql에서 데이터를 받아와서 데이터를 표시
  2. Floating Button을 클릭하면 insert 기능을 사용하여 Friend 데이터를 추가한다.

다음으로 중요한 곳은 다음 두 곳입니다.

final viewModel = ref.watch(friendViewModelProvider.notifier);
final friends = ref.watch(friendViewModelProvider);

 

viewModel은 viewModel 클래스에 있는 기능을 사용하기 위한 변수입니다.

 

friends는 FriendViewModel에서 생성자로 받고 있는 다음 부분을 의미합니다.

  FriendViewModel(this._friendRepository) : super([]) {
    getFriend();
  }

 

 


위에선 SQL의 GET, INSERT만 설명했는데요.

 

다음 포스팅에서 나머지 UPDATE, DELETE 구현도 해보겠습니다.

 

이상으로 "플러터의 Sqlite를 riverpod의 Provider로 사용하기" 였습니다.

반응형


"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."


댓글