Never give up

Flutter - Multi image picker with SQFLite 본문

해왔던 삽질..

Flutter - Multi image picker with SQFLite

대기만성 개발자 2021. 4. 17. 03:41
반응형

Multi image picker를 사용할 경우 애매한 상황이 생깁니다

(해당 예제는 4.8.0 버전을 사용했습니다)

 

불러오는 데이터 타입이 Asset인데 내부를 보면

(링크 참고 : pub.dev/documentation/multi_image_picker/latest/multi_image_picker/Asset-class.html)

  Asset(
    this._identifier,
    this._name,
    this._originalWidth,
    this._originalHeight,
  );
  //너는 뭐냐...

Image picker는 file path를 가져와서 File형태로 사용할 수 있고

 

byte array가 필요한 경우 readAsByte같은 메소드를 사용하면 됩니다

 

근데 이 Asset이란 녀석(?)은 기기에서 이미지를 가져와서 뿌려주는작업 까지는 크게 어려울게 없는데

 

문제는 저장과 불러오기를 할 때 많이 까다로운거 같습니다

 

위 값을 그냥 저장하는건 문제없지만 이미지 형태 로컬이나 서버에 저장할 때

 

그리고 불러온 데이터를 다시 화면에 이미지로 그려줄 때 문제가 생깁니다

 

다른 개발자분들은 어떻게 처리했는지 모르겠지만 필자는 byte array(Uint8List)를 적극 활용했습니다

 

서론이 길었는데 바로 코드와 설명 들어갑니다

 

Splash 화면

class FetchImageData extends StatefulWidget {
  @override
  _FetchImageDataState createState() => _FetchImageDataState();
}

class _FetchImageDataState extends State<FetchImageData> {
  @override
  void initState() {
    super.initState();
    initData();
  }

  void initData() async {
    await Provider.of<MultiImageProvider>(context, listen: false).initData();

    WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
      Navigator.push(
          context,
          MaterialPageRoute(
              builder: (_) =>
                  MultiImageExample('Multi image picker store example')));
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('Now loading', style: TextStyle(fontSize: 30)),
      ),
    );
  }
}

main 화면

class MultiImageExample extends StatelessWidget {
  final String title;

  MultiImageExample(this.title);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Consumer<MultiImageProvider>(builder: (_, items, __) {
        return Center(
          child: SingleChildScrollView(
            child: Column(
              children: [
                GridView.builder(
                    shrinkWrap: true,
                    physics: NeverScrollableScrollPhysics(),
                    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                        crossAxisCount: 2),
                    itemCount: items.images.length,
                    itemBuilder: (_, index) {
                      Uint8List image = items.images[index];
                      return AspectRatio(
                          aspectRatio: 1.0, child: Image.memory(image));
                    }),
                ElevatedButton(
                    onPressed: () async {
                      DataUtility utility = await items.getImages();

                      if (utility.isFailed) {
                        ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(content: Text(utility.errorStack)));
                      }
                    },
                    child: Text('Get images')),
              ],
            ),
          ),
        );
      }),
    );
  }
}

provider

class MultiImageProvider extends ChangeNotifier {
  List<Uint8List> images = [];
  late DBHelper _helper;

  Future<void> initData()async{
    _helper = DBHelper();

    images = await _helper.getImage();
  }

  Future<DataUtility> getImages() async {
    DataUtility utility = DataUtility();

    List<Asset> list = [];

    try {
      list = await MultiImagePicker.pickImages(maxImages: 4);
    } catch (e) {
      utility.hasError();
      utility.setError(e.toString());
    }
    if (list.isNotEmpty) {
      for(int i = 0 ; i < list.length ; i++){
        Uint8List image = utility.getImageFromByteData(await list[i].getByteData());
        images.add(image);

        await _helper.saveImage(image);
      }

      notifyListeners();
    }

    return utility;
  }
}

Utility

class DataUtility{
  bool isFailed;
  String errorStack;

  DataUtility({this.isFailed = false, this.errorStack = ''});

  void hasError(){
    isFailed = true;
  }

  void setError(String text){
    errorStack = text;
  }

  Uint8List getImageFromByteData(ByteData data){
    return data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
  }
}

DBHelper

class DBHelper {
  final String dbName = 'test.db';
  final String tableName = 'image_table';
  late String dbPath;
  Database? _database;

  Future<void> getDBPath() async {
    String path = await getDatabasesPath();
    dbPath = p.join(path, dbName);
  }

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

  Future<Database> _initDB() async {
    await getDBPath();
    return await openDatabase(dbPath, version: 1, onCreate: _onCreate);
  }

  FutureOr<void> _onCreate(Database database, int index) async {
    await database.execute('CREATE TABLE $tableName(image BLOB)');
  }

  Future<int> saveImage(Uint8List image) async {
    var db = await _openDB();
    int insert =
        await db.rawInsert('INSERT INTO $tableName(image) VALUES (?)', [image]);

    return insert;
  }

  Future<List<Uint8List>> getImage() async {
    var db = await _openDB();
    List<Map> rawList = await db.rawQuery('SELECT * FROM $tableName');

    if (rawList.isEmpty) {
      return [];
    } else {
      return List.generate(rawList.length, (index) => rawList[index]['image']);
    }
  }
}

 

SQFLite 생성과 데이터를 가져오는 부분은 공식문서와 크게 다른점이 없으니 링크로 대체합니다

(링크 : flutter.dev/docs/cookbook/persistence/sqlite)

 

유틸리티는 에러 처리부분과 ByteData를 Uint8List로 변환해주는 작업을 하고있고

 

Provider에서 init에서 로컬 db에서 이미지를 가져오는 작업을 하고

 

getImages부분에서 multi image picker를 이용해서 List<Asset>데이터를 가져옵니다

 

가져온 데이터를 utility를 이용해서 Uint8List로 변환을 해주고

 

class의 images parameter에 넣어주는것과 DB에 저장을 해줍니다

 

원래 필자는 foreach 사용하는것을 선호하는데 비동기 처리과정에서 문제가 발생해서

 

구식(?)을 사용했고, 에러처리방식은 필자가 선호하는 방식으로 해봤습니다

 

그리고 메인화면에서 Consumer를 이용해서 이미지를 가져오는것과 보여주는것을 합니다

 

마지막으로 Splash부분에서 db에 저장했던 Blob형태의 이미지를 가져와서

 

Main에서 보여줍니다

 

 

 

-- 이후 필자의 헛소리가 시작될 예정이니 예제만 확인하실 분은 넘어가주세요

 

사실.. 애초에 path만 가져오면 편할텐데 굳이 Asset으로 만들어서

 

번거로운 작업을 하게되는 상황이 생기니 썩 좋지많은 않더군요..

< 사실 식빵을 많이 찾았습니... >

File api문서를 확인하던중 byte array를 FIle로 만들어주는게 있어서 사용해봤는데

File.fromRawPath(Uint8List)

 

어마어마한 에러를 뿌려주면서 거부를 하더군요

 

대략 보여드리자면..

The following FileSystemException was thrown resolving an image codec:
Y  �  
[        XYZ       ��     �-mluc          enUS        G o o g l e   I n c .   2 0 1 6�� C �� C�� g�" ��           	
�� �  
 !1AQa��q��#3����"CSc	$%5
24EsBDRUbe�����&'67Tgtu����VWdr�����������(G�������FHXfw����������            	�� P   !1aAQ��q�������$b�"#2r��%34BCDERT�&5d���6S���   ? ��2��]{���D�&g�x����KV=�Tޭ{G�0%i��7?c��􋧹]�Ε�鳮^헦g���޺�$����g�jL�i>]|��x���٢�
ƽ����<�%륪�y�l�3=&�^�^J�T�|�a���n��h�,��]Z��M>�2����p���ؕ��t%h[zV�뙦���0��D��ߵ�^�:ݻF���E��g��q�۷S��V�N#��<i���W���Y2����J–�&Sτ|G���}+��M3=�Æ�/���ύ�=�I����ҽt�o
//이하 생략..
//무려 9만자의 외계문자가 출력이 나옵니다..

다행이도 ByteData는 잘 출력이 돼서 byte array로는 처리할 수 있었는데

 

다른 개발자분들은 어떻게 처리했는지 궁금하더군요

 

혹시라도 괜히 삽질하지는 않았나해서 말이죠

 

맨날 삽질만해서 이제 뭐 그러려니 하지만요..

반응형
Comments