Dandy Now!
  • [Flutter] "Do it! 플러터 앱 프로그래밍" - 내부 저장소 이용하기 | 공유 환경설정에 데이터 저장, 파일에 데이터 저장
    2022년 02월 17일 17시 31분 17초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    "조준수. (2021). Do it! 플러터 앱 프로그래밍. 이지스퍼블리싱", 9장을 실습하였다. 파일을 읽고 쓰는 방법에 대해 실습하였다. 파일을 읽고 쓰는 방법은 총 3가지가 있다. 그것은 공유 환경설정, 파일, DB에서 읽고 쓰는 방법이다. 이 장에서는 공유 환경설정과 파일에 읽고 쓰는 방법을 다룬다. 파일을 읽고 쓰는 기능을 활용하여 서버에서 이미지를 내려받아 인트로 화면을 변경하는 방법은 실무에서 유용해 보인다.

     

    Do it! 플러터 앱 프로그래밍

    플러터 기본 & 고급 위젯은 물론오픈 API와 파이어베이스를 이용한 앱 개발부터 배포까지!플러터 SDK 2.x 버전을 반영한 개정판!이 책은 플러터의 기초부터 고급 활용법까지 다루어 다양한 영역에

    book.naver.com

     

    공유 환경설정에 데이터 저장하기

    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),
          ),
        );
      }
    }

     

    [그림 1] 데모 앱을 종료한 후 다시 실행해도 기존 카운트 값이 로드된다.

     


     

    파일에 데이터 저장하기

    파일을 이용하면 공유 환경설정보다 더 복잡하고 다양한 데이터를 다룰 수 있다.

    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
    사과
    바나나
    포도
    배

     

    [그림 2] 파일 읽기

     

    좋아하는 과일 추가하기 - 파일 쓰기

    [그림 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);
      }
    }

     

    [그림 3] 파일 쓰기

     

    내려받은 이미지를 로고로 사용하기

    서버에서 이미지를 내려받아 인트로 화면을 변경하는 방법이다. 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');
      }
    }

     

    [그림 4] 내려받은 이미지로 로고 사용하기

     

    728x90
    반응형
    댓글