코딩강의/favorite_places(플러터-유데미)

~262. Using a FutureBuilder for Loading Data

김마드 2023. 11. 22. 16:28

1. 모바일 기기 SQL Database에 데이터를 저장해보고, 화면에 띄워보자.

 

- user.places.dart

import 'dart:io';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path_provider/path_provider.dart' as syspaths;
import 'package:path/path.dart' as path;
import 'package:sqflite/sqflite.dart' as sql;
import 'package:sqflite/sqlite_api.dart';

import 'package:favorite_places/models/place.dart';

Future<Database> _getDatabase() async {
  final dbPath = await sql.getDatabasesPath();
  final db = await sql.openDatabase(
    path.join(dbPath, 'places.db'),
    onCreate: (db, version) {
      return db.execute(
          'CREATE TABLE user_places(id TEXT PRIMARY KEY, title TEXT, image TEXT, lat REAL, lng REAL, address TEXT)');
    },
    version: 1,
  );
  return db;
}

class UserPlacesNotifier extends StateNotifier<List<Place>> {
  UserPlacesNotifier() : super(const []);

  Future<void> loadPlaces() async {
    final db = await _getDatabase();
    final data = await db.query('user_places');
    final places = data
        .map(
          (row) => Place(
            id: row['id'] as String,
            title: row['title'] as String,
            image: File(row['image'] as String),
            location: PlaceLocation(
              latitude: row['lat'] as double,
              longitude: row['lng'] as double,
              address: row['address'] as String,
            ),
          ),
        )
        .toList();

    state = places;
  }

  void addPlace(String title, File image, PlaceLocation location) async {
    final appDir = await syspaths.getApplicationDocumentsDirectory();
    final filename = path.basename(image.path);
    final copiedImage = await image.copy('${appDir.path}/$filename');

    final newPlace =
        Place(title: title, image: copiedImage, location: location);

    final db = await _getDatabase();
    db.insert('user_places', {
      'id': newPlace.id,
      'title': newPlace.title,
      'image': newPlace.image.path,
      'lat': newPlace.location.latitude,
      'lng': newPlace.location.longitude,
      'address': newPlace.location.address,
    });

    state = [newPlace, ...state];
  }
}

final userPlacesProvider =
    StateNotifierProvider<UserPlacesNotifier, List<Place>>(
  (ref) => UserPlacesNotifier(),
);

 

1) place상태를 관리하는 프로바이더에서 SQL 관리를 하고 있다.

2) 아래 내용을 보면, 초기 DB세팅 및 불러오는 로직이다. 

Future<Database> _getDatabase() async {
  final dbPath = await sql.getDatabasesPath();
  final db = await sql.openDatabase(
    path.join(dbPath, 'places.db'),
    onCreate: (db, version) {
      return db.execute(
          'CREATE TABLE user_places(id TEXT PRIMARY KEY, title TEXT, image TEXT, lat REAL, lng REAL, address TEXT)');
    },
    version: 1,
  );
  return db;
}

 

3) 위에서 불러온 db에 새로 추가된 place값들을 하나씩 넣어준다. 

    final newPlace =
        Place(title: title, image: copiedImage, location: location);

    final db = await _getDatabase();
    db.insert('user_places', {
      'id': newPlace.id,
      'title': newPlace.title,
      'image': newPlace.image.path,
      'lat': newPlace.location.latitude,
      'lng': newPlace.location.longitude,
      'address': newPlace.location.address,
    });

 

4) 그리고, 아래는 저장된 db에서 data를 추출하고, 그 data를 다시 Place 객체에 하나씩 넣어준 후, 리스트화 한다. 그리고 해당 리스트를 다시 state값에 넣어준다. 여기서 id 값을 넣어 주는 이유는, 기존 id 값을 그대로 유지하기 위해서이다. 만약 id 값이 없으면 작동하기는 하는데, id값이 바뀔 것이다.(uuid에 의해)

 Future<void> loadPlaces() async {
    final db = await _getDatabase();
    final data = await db.query('user_places');
    final places = data
        .map(
          (row) => Place(
            id: row['id'] as String,
            title: row['title'] as String,
            image: File(row['image'] as String),
            location: PlaceLocation(
              latitude: row['lat'] as double,
              longitude: row['lng'] as double,
              address: row['address'] as String,
            ),
          ),
        )
        .toList();

    state = places;
  }

 

참고로 데이터는 아래와 같은 값을 반환한다.

 

 [{id: ddea5a71-a665-44ed-a688-489c021397a4, title: rryy, image: /data/user/0/com.example.favorite_places/app_flutter/scaled_377ab4e4-c019-4523-acf2-298420755f59611531850.jpg, lat: 37.42243487079162, lng: -122.08302285522223, address: 1598 Amphitheatre Pkwy, Mountain View, CA 94043, USA}, ~~~~]

 

- place.dart

import 'dart:io';

import 'package:uuid/uuid.dart';

const uuid = Uuid();

class PlaceLocation {
  const PlaceLocation({
    required this.latitude,
    required this.longitude,
    required this.address,
  });

  final double latitude;
  final double longitude;
  final String address;
}

class Place {
  Place({
    required this.title,
    required this.image,
    required this.location,
    String? id,
  }) : id = id ?? uuid.v4();

  final String id;
  final String title;
  final File image;
  final PlaceLocation location;
}

1) Place값의 id가 없는 경우는 uuid를 통해 생성되고, id 값이 있는경우 (데이터를 SQL DB에서 가지고온 경우)는 해당 id값을 사용 한다.

 

- places.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'package:favorite_places/screens/add_place.dart';
import 'package:favorite_places/widgets/places_list.dart';
import 'package:favorite_places/providers/user_places.dart';

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

  @override
  ConsumerState<PlacesScreen> createState() {
    return _PlacesScreenState();
  }
}

class _PlacesScreenState extends ConsumerState<PlacesScreen> {
  late Future<void> _placesFuture;

  @override
  void initState() {
    super.initState();
    _placesFuture = ref.read(userPlacesProvider.notifier).loadPlaces();
  }

  @override
  Widget build(BuildContext context) {
    final userPlaces = ref.watch(userPlacesProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Your Places'),
        actions: [
          IconButton(
            icon: const Icon(Icons.add),
            onPressed: () {
              Navigator.of(context).push(
                MaterialPageRoute(
                  builder: (ctx) => const AddPlaceScreen(),
                ),
              );
            },
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: FutureBuilder(
          future: _placesFuture,
          builder: (context, snapshot) =>
              snapshot.connectionState == ConnectionState.waiting
                  ? const Center(child: CircularProgressIndicator())
                  : PlacesList(
                      places: userPlaces,
                    ),
        ),
      ),
    );
  }
}

1) initState를 사용해야 하기 때문에, stateful로 바꿔주고, initState에 DB에서 값을 가져오는것을 추가한다. 그리고 해당 함수가 완료 되면 _placesFuture에 담긴다.

 

2) FutureBuilder를 통해, 로딩이 끝나면 값을 보여준다. 여기서 future값이 리스트 값이 아니고 void인 이유, 그리고 PlaceList에 보내주는 리스트가 final userPlaces = ref.watch(userPlacesProvider);인 이유는

 

만약 해당 화면이 다른화면에서 가지고오는 값이 아니고 해당 화면에서만 다루는 것이라면 db에서 가지고 온 값을 바로 보여줘도 되지만, 위와 같이한 이유는 add_place화면에서 pop을 통해 값을 가져오면 init이 발동되는것이 아니라 실시간으로 화면이 업데이트 되어야 하기 때문에 위와 같이 세팅하였다.

 

추가적으로 add_place에서 값을 저장하면 해당 값은 맨 위에 위치하는데, init을 통해 db에서 값을 가져오면 해당 값은 맨 아래에 위치 한다. 이러한 문제가 있으니 참고. (순서는 내가 원하는대로 바꾸면 됨)