Search

Handling Image File with Dart:IO

Created
2020/09/06
tag
Flutter
Image File
Directory
Asset
multi_image_picker
path_provider
image_picker
프로젝트를 진행하면서 FileDirectory를 다루는 부분이 생각보다 헷갈렸는지 꽤 많이 찾아본 것 중 하나였다. 그래도 해보면서 느낀 건, Node.js에서의 File, Directory 생성보다는 그래도 좀 편하지 않았나 했다.
아래 3가지 실습에서 저장한 모든 File들은 확장자를 명시하지 않은 상태로 저장했기 때문에, 저장된 File을 열려고 하면 안 열릴 것이다. File을 생성할 때 경로의 끝에 확장자 명을 명시하면 해결된다.

1. Directory 생성과 경로 취득 그리고 File 생성과 읽고 쓰기

어플 개발을 하면서 포스팅 글에 이미지를 함께 업로드를 했을 때, 이 포스팅 글을 수정하기 위해서 수정 페이지로 돌아오면 이미지를 일시적으로 File로 유지를 하기 위해 FileDirectory를 사용 했었다. 그리고 Logger를 두면서 기록되는 로그들을 사용자 핸드폰에 저장되도록 하면서 만일 문제가 생겨 개발자에게 로그 보내기를 클릭하면 해당 로그 파일들을 불러올 수 있도록 FileDirectory를 이용하기도 했다.
이 과정에서 Directory를 어떻게 생성하고 File을 어떻게 생성하는지 모르고 헷갈렸던 적이 잦아 그만큼 자주 찾아봤다. 실습을 통해서 해당 내용을 다시 정리해보려 한다.
우선 pub.dev로부터 path_provider버전을 확인하고 해당 라이브러리를 pubspec.yamldependencies에 추가한다. path_provider가 제공해주는 여러 Method를 이용할 수 있는데, 플랫폼 별로 여러 경로들을 얻을 수 있다.
이 때 이용하는 클래스는 Directory 클래스이며, 객체 생성 방법은 Directory를 선언한 뒤 해당 타입을 까서 확인해보자. 일반적으로는 Directory의 생성자 함수 인자로 경로 문자열을 주면 된다. (FileDirectory 클래스를 이용하기 위해선 dart:io 라이브러리의 import가 필요하다.)
실습으로 해볼 것은 Android 에뮬레이터의 External StorageDirectory를 생성하고, 그 안에 File을 생성하여 데이터를 써보는 것이다. 코드는 다음과 같다.
iOS 에뮬레이터로도 할 수 있지만, 내가 못하는 건지 에뮬레이터의 파일 시스템에는 작업한 File이 정상적으로 보이지가 않는다. 다만 정상적으로 수행이 되는 것이라고 생각은 되는 것이 File 생성을 하면 macOSiOS 에뮬레이터 Directory에 정상적으로 작업을 수행한 결과를 볼 수 있기 때문이다.
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: FlatButton( child: Text('Click Here To Create'), onPressed: () async { Directory motherDirectory = await getExternalStorageDirectory(); Directory dummyDirectory = await Directory('${motherDirectory.path}/dummy') .create(recursive: true); File dummyFile = File( '${dummyDirectory.path}/${DateTime.now()}', ); dummyFile.writeAsStringSync(DateTime.now().toString()); }, ), ), ), ); } }
Dart
path_providergetExternalStorageDirectory 함수를 통해 외부 저장 장치 Directory에 쉽게 접근할 수 있다. 또한 Directory 생성 시 create 함수로는 아래 4가지를 사용할 수 있다.
1. Future<Directory>create()
2. Future<Directory>createTemp()
3. void createSync()
4. void createTempSync()
이 중에서 1번과 3번을 골라서 사용하여, Directory를 생성하면 되는데, 이 2가지 함수는 함수의 인자로 bool 타입 인자를 받는다. recursive라는 인자는 기본적으로 false 값을 갖지만, true를 할당하면 Directory로 지정한 경로까지 없는 Directory를 모두 생성하게 된다.
현재 정확히 필요한 기능이다.
위 코드를 실행하면 에뮬레이터에는 빈 화면에 버튼만 생성되고, 해당 버튼을 누르면 DirectoryFile의 생성을 수행하게 된다. (Directory의 경로는 Directory 객체의 path를 이용하면 되고, File 역시 File의 경로를 얻기 위해서는 File 객체의 path를 이용하면 된다. File의 생성도 Directory 객체 생성과 비슷하게 경로를 String으로 주면 생성할 수 있다. 실제 File을 생성하기 위해선 File 객체의 write 관련 함수를 사용하면 된다. 실습의 케이스에서는 StringFile에 쓰는 동기적으로 작동하는 함수를 이용했다.)
버튼을 클릭하기 전에는 Directory도 존재하지 않다가, 버튼 클릭 후에는 Directory 뿐 아니라 File도 생성된 것을 확인할 수 있다.

2. multi_image_picker 라이브러리의 Asset을 File로 변환

앨범에서 사진을 여러 장 선택을 하거나, 카메라로 부터 사진을 찍고 해당 이미지를 이용하기 위해서 image_picker 혹은 multi_image_picker를 이용하는 경우가 많다.
image_picker 라이브러리의 경우 과거에는 선택한 이미지File 타입을 갖고 있었지만, 최근에는 PickedFile이라는 타입을 갖는 것으로 알고 있다. PickedFile의 경우 크게 어려움이 없는 것이, PickedFile 객체의 path를 통해, 고른 이미지의 경로를 취득할 수 있고 이를 File 객체 생성에 이용할 수 있기 때문이다.
하지만 multi_image_picker이미지들은 모두 Asset 타입을 갖고 있다. 물론 Asset 타입을 Byte로 처리를 할 수도 있겠지만, File로 처리를 해야하는 경우도 있을 것이다. 이번 카테고리의 실습은 앨범에서 고른 사진 1장의 Asset 타입을 File 타입으로 변환하여 이용해보는 것이다. File의 저장 경로는 첫 번째 실습의 경로와 동일하다.
우선 pub.dev에서 multi_image_pickerimport한다. 새로운 dependency를 잘 이용하기 위해선 에뮬레이터를 종료하고 flutter clean을 한 번 해준뒤, iOSAndroid에 맞는 권한 설정을 해준다. (권한에 대해서는 pub.devmulti_image_picker 라이브러리를 참고하자.)
방법은 이미지를 Byte로 처리하여 Backend에 업로드 했던 것과 비슷하게 Byte들을 File에 쓰는 것이다. 코드는 아래와 같다.
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:multi_image_picker/multi_image_picker.dart'; import 'package:path_provider/path_provider.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: FlatButton( child: Text('Click Here To Create'), onPressed: () async { List<Asset> pickedAsset = []; pickedAsset = await MultiImagePicker.pickImages( maxImages: 1, ); Directory motherDirectory = await getExternalStorageDirectory(); Directory dummyDirectory = await Directory('${motherDirectory.path}/dummy') .create(recursive: true); File convertedFile = File( '${dummyDirectory.path}/${DateTime.now()}', ); var bytes = await pickedAsset[0].getByteData(); await convertedFile.writeAsBytes(bytes.buffer.asUint8List()); }, ), ), ), ); } }
Dart
첫 번째 실습과 마찬가지로 버튼을 클릭하면 앨범에서 사진을 고를 수 있고, 사진을 고르게 되면 dummy Directory에 사진이 파일로 저장된다.

3. Image를 URL을 통해 다운로드 받기 (Showing이 아닌 다운로드)

온라인 상에 존재하는 이미지File로 다운로드 받기 위해서 HTTP의 도움이 필요하다. 내 경우에는 HTTP가 아니라 dio를 이용하여 HTTP 통신을 하였다. pub.dev에서 dio 패키지를 다운로드 받아서 사용하면 된다.
dio 패키지에는 downloadUri라는 함수가 존재하고, 함수의 인자로 Uri와 저장 경로를 주면 Uri의 데이터를 다운로드하여 저장경로에 저장하게 된다.
다운로드 받을 이미지Lorem Picsum이라는 Random Image URL을 제공해주는 사이트의 이미지이다.
코드는 아래와 같다.
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; import 'package:path_provider/path_provider.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: FlatButton( child: Text('Click Here To Create'), onPressed: () async { Dio dio = Dio(); Directory motherDirectory = await getExternalStorageDirectory(); Directory dummyDirectory = await Directory('${motherDirectory.path}/dummy') .create(recursive: true); await dio.downloadUri( Uri.http('picsum.photos', '/200'), '${dummyDirectory.path}/${DateTime.now()}', ); }, ), ), ), ); } }
Dart
위의 여러 실습과 마찬가지로 버튼을 누르면 정상적으로 이미지 다운로드가 완료되는 것을 볼 수 있다.

4. Reference