- [Flutter] "Do it! 플러터 앱 프로그래밍" - 내부 저장소 이용하기 | 공유 환경설정에 데이터 저장, 파일에 데이터 저장2022년 02월 17일 17시 31분 17초에 업로드 된 글입니다.작성자: DandyNow728x90반응형
"조준수. (2021). Do it! 플러터 앱 프로그래밍. 이지스퍼블리싱", 9장을 실습하였다. 파일을 읽고 쓰는 방법에 대해 실습하였다. 파일을 읽고 쓰는 방법은 총 3가지가 있다. 그것은 공유 환경설정, 파일, DB에서 읽고 쓰는 방법이다. 이 장에서는 공유 환경설정과 파일에 읽고 쓰는 방법을 다룬다. 파일을 읽고 쓰는 기능을 활용하여 서버에서 이미지를 내려받아 인트로 화면을 변경하는 방법은 실무에서 유용해 보인다.
공유 환경설정에 데이터 저장하기
Shared Preferences 패키지 설치
pub.dev에서 Shared Preferences 패키지를 찾아 설치한다(https://pub.dev/packages/shared_preferences/install). Shared Preferences 패키지는 키-값 쌍으로 구성된 공유 환경설정 파일을 가리키며 이 파일에 데이터를 읽거나 쓰는 함수를 제공한다. cmd 창을 열고 Shared Preferences 패키지를 설치한다.
flutter pub add shared_preferences
데모 앱의 카운트 값 저장하기
데모 앱을 종료하더라도 카운트 값이 초기화되지 않고 종료 시점의 값을 공유 환경설정에 저장된 값으로부터 불러오도록 한다. 공유 환경설정을 이용하는 것도 결국 내부 저장소에 파일 형태로 데이터를 저장하는 것이지만, 공유 환경설정 파일은 앱에서만 접근할 수 있는 특수한 목적의 데이터 저장소라는 점에서 차이가 있다.
// main.dart import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; // Shared Preferences 패키지를 가져오는 코드 void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; // 데이터를 저장하는 함수 void _setData(int value) async { var key = 'count'; SharedPreferences pref = await SharedPreferences.getInstance(); pref.setInt(key, value); } // 데이터를 가져오는 함수 void _loadData() async { var key = 'count'; SharedPreferences pref = await SharedPreferences.getInstance(); setState(() { var value = pref.getInt(key); if (value == null) { _counter = 0; } else { _counter = value; } }); } // 데이터를 가져오는 함수 호출 @override void initState() { super.initState(); _loadData(); } void _incrementCounter() { setState(() { _counter++; _setData(_counter); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), ); } }
파일에 데이터 저장하기
파일을 이용하면 공유 환경설정보다 더 복잡하고 다양한 데이터를 다룰 수 있다.
path_provider 패키지 설치
pub.dev에서 path_provider 패키지를 찾아 설치한다(https://pub.dev/packages/path_provider/install).
flutter pub add path_provider
데모 앱을 이용한 파일 입출력 연습
// main.dart import 'package:flutter/material.dart'; import 'fileApp.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: FileApp(), ); } }
// fileApp.dart import 'package:file/memory.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'dart:io'; import 'package:path_provider/path_provider.dart'; class FileApp extends StatefulWidget { @override State<StatefulWidget> createState() => _FileApp(); } class _FileApp extends State<FileApp> { int _count = 0; @override void initState() { super.initState(); readCountFile(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('File Example'), ), body: Container( child: Center( child: Text( '$_count', style: TextStyle(fontSize: 40), ), ), ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { _count++; }); writeCountFile(_count); // 증가한 카운트 값 파일에 저장 }, child: Icon(Icons.add), ), ); } // 파일 쓰기 void writeCountFile(int count) async { // 내부 저장소의 경로를 가져올 때 getTemporaryDirectory() 함수를 이용할 수 있으나, 임시 디렉터리는 캐시를 이용하므로 앱이 종료되고 일정 시간이 지나면 사라질 수 있다. var dir = await getApplicationDocumentsDirectory(); File(dir.path + '/count.txt').writeAsStringSync(count.toString()); } // 파일 읽기 void readCountFile() async { try { var dir = await getApplicationDocumentsDirectory(); var file = await File(dir.path + '/count.txt').readAsString(); print(file); setState(() { _count = int.parse(file); }); } catch (e) { print(e.toString()); } } }
좋아하는 과일 표시하기 - 파일 읽기
// main.dart import 'package:flutter/material.dart'; import 'fileApp.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: FileApp(), ); } }
// fileApp.dart import 'package:flutter/material.dart'; import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; // 공유 환경설정 패키지 import class FileApp extends StatefulWidget { @override State<StatefulWidget> createState() => _FileApp(); } class _FileApp extends State<FileApp> { int _count = 0; List<String> itemList = new List.empty(growable: true); TextEditingController controller = new TextEditingController(); @override void initState() { super.initState(); initData(); } // initState 함수는 async 키워드를 사용할 수 없으므로 별도의 iniData 함수를 만든다. void initData() async { var result = await readListFile(); setState( () { itemList.addAll(result); }, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('File Example'), ), body: Container( child: Center( child: Column( children: <Widget>[ TextField( controller: controller, keyboardType: TextInputType.text, ), // Expanaded 위젯은 남은 공간을 모두 사용한다. 텍스트필드 이외 나머지 부분은 모두 ListView로 사용하겠다는 의미이다. Expanded( child: ListView.builder( itemBuilder: (context, index) { return Card( child: Center( child: Text( itemList[index], style: TextStyle(fontSize: 30), ), ), ); }, itemCount: itemList.length, ), ), ], ), ), ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { _count++; }); }, child: Icon(Icons.add), ), ); } // fruit.txt 파일에서 데이터를 읽어와 내부 저장소인 공유 환경설정에 저장한 다음 리스트를 만드는 함수 Future<List<String>> readListFile() async { List<String> itemList = new List.empty(growable: true); var key = 'first'; SharedPreferences pref = await SharedPreferences.getInstance(); bool? firstCheck = pref.getBool(key); // 파일을 처음 열었는지 확인하는 용도 var dir = await getApplicationDocumentsDirectory(); bool fileExist = await File(dir.path + '/fruit.txt').exists(); // 파일이 있는 확인하는 용도 // 파일을 처음 열었거나 없는 경우 if (firstCheck == null || firstCheck == false || fileExist == false) { pref.setBool(key, true); var file = await DefaultAssetBundle.of(context).loadString('repo/fruit.txt'); File(dir.path + '/fruit.txt').writeAsString(file); var array = file.split('\n'); for (var item in array) { print(item); itemList.add(item); } return itemList; // 파일을 처음 열지 않은 경우 } else { var file = await File(dir.path + '/fruit.txt').readAsString(); var array = file.split('\n'); for (var item in array) { print(item); itemList.add(item); } return itemList; } } }
// repo/fruit.txt 사과 바나나 포도 배
좋아하는 과일 추가하기 - 파일 쓰기
[그림 3]은 파일 쓰기가 가능해진 상태이며, "딸기"를 추가해 보았다.
// fileApp.dart import 'package:flutter/material.dart'; import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; // 공유 환경설정 패키지 import class FileApp extends StatefulWidget { @override State<StatefulWidget> createState() => _FileApp(); } class _FileApp extends State<FileApp> { int _count = 0; List<String> itemList = new List.empty(growable: true); TextEditingController controller = new TextEditingController(); @override void initState() { super.initState(); initData(); } // initState 함수는 async 키워드를 사용할 수 없으므로 별도의 iniData 함수를 만든다. void initData() async { var result = await readListFile(); setState( () { itemList.addAll(result); }, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('File Example'), ), body: Container( child: Center( child: Column( children: <Widget>[ TextField( controller: controller, keyboardType: TextInputType.text, ), // Expanaded 위젯은 남은 공간을 모두 사용한다. 텍스트필드 이외 나머지 부분은 모두 ListView로 사용하겠다는 의미이다. Expanded( child: ListView.builder( itemBuilder: (context, index) { return Card( child: Center( child: Text( itemList[index], style: TextStyle(fontSize: 30), ), ), ); }, itemCount: itemList.length, ), ), ], ), ), ), floatingActionButton: FloatingActionButton( onPressed: () { writeFruit(controller.value.text); setState(() { itemList.add(controller.value.text); }); }, child: Icon(Icons.add), ), ); } // fruit.txt 파일에서 데이터를 읽어와 내부 저장소인 공유 환경설정에 저장한 다음 리스트를 만드는 함수 Future<List<String>> readListFile() async { List<String> itemList = new List.empty(growable: true); var key = 'first'; SharedPreferences pref = await SharedPreferences.getInstance(); bool? firstCheck = pref.getBool(key); // 파일을 처음 열었는지 확인하는 용도 var dir = await getApplicationDocumentsDirectory(); bool fileExist = await File(dir.path + '/fruit.txt').exists(); // 파일이 있는 확인하는 용도 // 파일을 처음 열었거나 없는 경우 if (firstCheck == null || firstCheck == false || fileExist == false) { pref.setBool(key, true); var file = await DefaultAssetBundle.of(context).loadString('repo/fruit.txt'); File(dir.path + '/fruit.txt').writeAsString(file); var array = file.split('\n'); for (var item in array) { print(item); itemList.add(item); } return itemList; // 파일을 처음 열지 않은 경우 } else { var file = await File(dir.path + '/fruit.txt').readAsString(); var array = file.split('\n'); for (var item in array) { print(item); itemList.add(item); } return itemList; } } // 사용자가 등록한 과일 이름을 내부 저장소에 있는 fruit.txt 파일에 추가하는 함수 void writeFruit(String fruit) async { var dir = await getApplicationDocumentsDirectory(); var file = await File(dir.path + '/fruit.txt').readAsString(); file = file + '\n' + fruit; File(dir.path + '/fruit.txt').writeAsStringSync(file); } }
내려받은 이미지를 로고로 사용하기
서버에서 이미지를 내려받아 인트로 화면을 변경하는 방법이다. largeFileMain.dart는 7장에서 작성한 파일을 가져온 것이다.
// main.dart import 'package:flutter/material.dart'; import 'largeFileMain.dart'; import 'introPage.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: IntroPage(), // inrtoPage.dart 파일 실행 ); } } class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('로고 바꾸기'), actions: <Widget>[ TextButton( onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => LargeFileMain())); }, child: Text( '로고 바꾸기', style: TextStyle(color: Colors.white), ), ), ], ), body: Container(), ); } }
// introPage.dart import 'dart:io'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'main.dart'; class IntroPage extends StatefulWidget { @override State<StatefulWidget> createState() => _IntroPage(); } class _IntroPage extends State<IntroPage> { Widget logo = Icon( Icons.info, size: 50, ); @override void initState() { super.initState(); initData(); // initData 함수 호출 } @override Widget build(BuildContext context) { return Scaffold( body: Container( child: Center( child: Column( children: [ logo, ElevatedButton( onPressed: () { Navigator.of(context) .pushReplacement(MaterialPageRoute(builder: (context) { return MyHomePage(title: ''); // main.dart의 MyHomePage 클래스 })); }, child: Text('다음으로 가기'), ) ], mainAxisAlignment: MainAxisAlignment.center, ), ), ), ); } // 파일이 있는지 확인하고 있으면 logo 위젯에 이미지를 넣는다. void initData() async { var dir = await getApplicationDocumentsDirectory(); bool fileExist = await File(dir.path + '/myimage.jpg').exists(); if (fileExist) { setState(() { logo = Image.file( File(dir.path + '/myimage.jpg'), height: 200, width: 200, fit: BoxFit.contain, ); }); } } }
// largeFileMain.dart import 'package:file/memory.dart'; import 'package:dio/dio.dart'; // 터미널 창에서 flutter pub add dio import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; // 터미널 창에서 flutter pub add path_provider import 'dart:io'; // 파일 입출력을 돕는 io 패키지 class LargeFileMain extends StatefulWidget { @override State<StatefulWidget> createState() => _LargeFileMain(); } class _LargeFileMain extends State<LargeFileMain> { // 내려받을 이미지 주소 // final imgUrl = // 'https://images.pexels.com/photos/5019409/pexels-photo-5019409.jpeg' // '?auto=compress'; bool downloading = false; // 지금 내려받는 중인지 확인하는 변수 var progressString = ""; // 현재 얼마나 내려받았는지 표시하는 변수 String file = ""; // 내려받은 파일] TextEditingController? _editingController; // URL 직접 입력해 내려받기 // URL 직접 입력해 내려받기 @override void initState() { super.initState(); _editingController = new TextEditingController( text: 'https://images.pexels.com/photos/5019409/pexels-photo-5019409.jpeg' '?auto=compress'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( // title: Text('Large File Example'), // URL 직접 입력해 내려받기 TextField title: TextField( controller: _editingController, style: TextStyle(color: Colors.white), keyboardType: TextInputType.text, decoration: InputDecoration(hintText: 'url 입력하세요'), ), ), body: Center( child: downloading ? Container( height: 120.0, width: 200.0, child: Card( color: Colors.black, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ CircularProgressIndicator(), SizedBox( height: 20.0, ), Text( 'Downloading File: $progressString', style: TextStyle( color: Colors.white, ), ), ], ), ), ) : FutureBuilder( builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState .none: // FutureBuilder.future가 null일 때 print('none'); return Text('데이터 없음'); case ConnectionState .waiting: // 연결되기 전(FutureBuilder.future에서 데이터를 반환받지 않았을 때) print('waiting'); return CircularProgressIndicator(); case ConnectionState.active: // 하나 이상의 데이터를 반환받을 때 print('active'); return CircularProgressIndicator(); case ConnectionState.done: // 모든 데이터를 받아서 연결이 끝날 때 print('done'); if (snapshot.hasData) { return snapshot.data as Widget; } } print('end process'); return Text('데이터 없음'); }, future: downloadWidget(file), )), floatingActionButton: FloatingActionButton( onPressed: () { downloadFile(); }, child: Icon(Icons.file_download), ), ); } // downloadWidget은 이미지 파일이 있는지 확인해서 있으면 이미지를 화면에 보여주는 위젯을 반환하고, 없으면 'No Data'라는 텍스트를 출력한다. Future<Widget> downloadWidget(String filePath) async { File file = File(filePath); bool exist = await file.exists(); new FileImage(file).evict(); // 캐시 초기화하기, evict()로 캐시를 비우면 같은 이름이라도 갱신한다. if (exist) { return Center( child: Column( children: <Widget>[Image.file(File(filePath))], ), ); } else { return Text('No Data'); } } Future<void> downloadFile() async { // dio를 선언한 후 내부 디렉터리를 가져온다. Dio dio = Dio(); try { // getApplicationDocumentsDirectory는 path_provider 패키지가 제공하며 플러터 앱의 내부 디렉터리를 가져오는 역할을 한다. var dir = await getApplicationDocumentsDirectory(); // dio.download를 이용해 url에 담긴 주소의 파일 내려 받는다. // dio.download() 함수의 첫 번째 인자로 사용자가 입력한 주소(_editingController)를 전달 await dio .download(_editingController!.value.text, '${dir.path}/myimage.jpg', onReceiveProgress: (rec, total) { // onReceiveProgress함수를 실행해 진행 상황을 표시한다. print('Rec: $rec, Total:$total'); file = '${dir.path}/myimage.jpg'; setState(() { downloading = true; // 내려받기 시작 progressString = ((rec / total) * 100).toStringAsFixed(0) + '%'; // 얼마나 내려 받았는지 계산 }); }); } catch (e) { print(e); } setState(() { downloading = false; // 내려받기 끝 progressString = 'Completed'; }); print('Download completed'); } }
728x90반응형'언어·프레임워크 > Flutter' 카테고리의 다른 글
다음글이 없습니다.이전글이 없습니다.댓글