DevBoi

[Flutter] SharedPreferences 사용하여 입/출력 정리 본문

[Mobile]/[Flutter]

[Flutter] SharedPreferences 사용하여 입/출력 정리

HiSmith 2023. 7. 25. 23:09
반응형

SharedPreferences에 대한걸 정리한다.

동기/비동기에 대한 처리를 제대로 하지않아, 오류가 발생했다.

 

목표 : SharedPreferences에 데이터를 넣고, 해당 데이터가 업데이트 될때마다, 리스트뷰에 추가해준다.

 

1. 저장 객체 선언

class Memo{
  int? id;
  String content='';
  String? completeYn;

  Memo(int id,String content,String completeYn){
    this.id = id;
    this.content = content;
    this.completeYn = completeYn;
  }


  Map<String, dynamic> toJson() {
    return {
      'id': this.id,
      'content': this.content,
      'completeYn': this.completeYn,
    };
  }

  factory Memo.fromJson(Map<String, dynamic> json) {
    return Memo(json['id'],json['content'],json['completeYn']);

  }
}

딱히 뭐가 없긴하다.

단순히, 객체를 선언하고, FromJson,toJson의 정형화된 로직을 작성했다.

Json에 대한 직렬화,역직렬화는 모두 여기서 처리하도록 한다.


2. SharedPreferences 처리 인터페이스 작성

import 'dart:convert';

import 'package:shared_preferences/shared_preferences.dart';

import 'memo.dart';

class MemoInterface{
  static late SharedPreferences prefs;
  final String prefix = "smithdata";

  Future init() async{
    prefs = await SharedPreferences.getInstance();
  }

  Future save(String text) async{
    var memos=  await prefs.getString(prefix);
    print(memos);
    //처음일때
    if(memos == null || memos.indexOf("null")==1 || memos.isEmpty){
      Memo memo = new Memo(0,text,"N");
      List<Memo> list = <Memo>[memo];
      prefs.setString(prefix, jsonEncode(list));
    }
    //추가할떄
    else{
      var jsonData = json.decode(memos!);
      List <Memo> list = List<Memo>.from(jsonData.map((x) => Memo.fromJson(x)));
      Memo memo = new Memo(list.length,text,"N");
      list.add(memo);
      prefs.setString(prefix, jsonEncode(list));

    }
    print(prefs.getString(prefix));
  }
    List<Memo> getList() {
    var memoList =  prefs.getString(prefix);
    if(memoList != null){
      var jsonData = json.decode(memoList!);
      return List<Memo>.from(jsonData.map((x) => Memo.fromJson(x)));
    }
    return List.empty();
  }
}

여기서 유의깊게 봐야하는 부분을 짤라서 정리하겠다.

 

1.선언부

static late SharedPreferences prefs;
  final String prefix = "smithdata";

우선 이렇게, SharedPreferences 와 저장 Prefix를 편의상 선언하다.

SharedPreferences는 비동기로 객체를 가져오고, 모든 데이터를 비동기로 처리하는 특성이 있어서,

해당 관련 처리를 할때는 이러한 처리에 신경을 써야한다.

 

2.초기화

Future init() async{
    prefs = await SharedPreferences.getInstance();
  }

해당 초기화 블록이다. 

사용하는 곳의 State에서 initsate에서 항상 해당 인터페이스를 초기화 한다고 약속해야한다.

안그러면, 콜백 지옥...이 되어서 오류가 팡팡 터진다.

 

3.저장

Future save(String text) async{
    var memos=  await prefs.getString(prefix);
    print(memos);
    //처음일때
    if(memos == null || memos.indexOf("null")==1 || memos.isEmpty){
      Memo memo = new Memo(0,text,"N");
      List<Memo> list = <Memo>[memo];
      prefs.setString(prefix, jsonEncode(list));
    }
    //추가할떄
    else{
      var jsonData = json.decode(memos!);
      List <Memo> list = List<Memo>.from(jsonData.map((x) => Memo.fromJson(x)));
      Memo memo = new Memo(list.length,text,"N");
      list.add(memo);
      prefs.setString(prefix, jsonEncode(list));

    }
  }

Sharedpreference의 입출력은 쉽다, 다만 해당 과정들에서 비동기 처리를 확실히 해야한다.

나는 리스트를 json으로 처리하고 이를 전체다 집어넣는다.

setStringList의 방법도 고민하긴했는데, 이게 더 간편할것같다.(직렬화 역직렬화가 생각보다 그렇게 리소스가 심하지않다)

무튼 아예 처음에 데이터를 넣을때는 리스트 초기화 과정까지 신경써주고, 이후 작업 부터는 그냥 add한 리스트를 넣어준다.

같은 prefix에 넣으면, 해당 값은 덮어씌워진다.

일부 초기데이터는 completeYn이 N으로 정해져서 들어간다.

 

4. 데이터 조회

List<Memo> getList() {
    var memoList =  prefs.getString(prefix);
    if(memoList != null){
      var jsonData = json.decode(memoList!);
      return List<Memo>.from(jsonData.map((x) => Memo.fromJson(x)));
    }
    return List.empty();
  }

단순히 조회 로직이다. Null처리를 위해서, 분기를 태운것이 전부이다.

값이 있으면 물론 그값을 리턴한다.

 

 

5. 사용 화면

사용 화면은 소스가 길어서, 먼저 짤라서 정리하고, 그 이후 전체 소스를 넣을 것이다.

 

5-1. 선언부

final MemoInterface memoInterface = MemoInterface();
late List<Memo> list = <Memo>[];

void main() {
  runApp(const MyApp());
}

void main() 상단에, 전역 변수를 지정해준다.

interface선언과, Memo List를 선언해준다.

 

 

5-2. 초기화

class _ExampleScreenState extends State<ExampleScreen> {

  @override
  void initState() {
    memoInterface.init().then((value) => getList());
    super.initState();
  }
  void getList(){
    list = memoInterface.getList();
    setState(() {
      
    });
  }

State를 상속 받으면, 내부적으로 제공하는 InitSate메소드를 재구현할 수 있다.

이를 재구현하여, 인터페이스를 초기화 한다.

그리고, 초기화가 되었다면, getList로, 선언한 리스트에 값을 넣어준다.

그리고 값이 바뀌었으니, setState()를 콜해준다.

 

중요한 것은, 비동기 메소드 뒤 then으로 처리하는 것,

그리고, setState로 해당 상태를 새로고침 하는 것.

 

 

5-3. 저장

 onPressed: () {
              memoInterface.save(_contentEditController.text);
              if(list.length ==0) {
              list = <Memo>[new Memo(list.length, _contentEditController.text, "N")];
              }
              else {
                list.add(
                    new Memo(list.length, _contentEditController.text, "N"));
              }
              Navigator.of(context).pop();
              context.widget.createElement();

저장 로직도 간단하다.

인터페이스에 세이브 해주고, 해당 리스트에 추가해준다.

pref와 직접 통신하면, 문제가 많다.

반영이 실제 메모리보다 느리기 때문에, 백업용으로 사용한다고 이해하면된다.

해당 과정을 거쳐서, 리스트에 정상적으로 노출됨을 확인한다.

 

아래는 전체 소스이다.

참고하자.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:memosmith/MemoInterfaceSmith.dart';
import 'package:transformable_list_view/transformable_list_view.dart';
import 'dart:async';
import 'package:memosmith/memo.dart';

final MemoInterface memoInterface = MemoInterface();
final  String prefix= "smithMemo";
late List<Memo> list = <Memo>[];

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {


  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Transformable List View Example',
      home: ExampleScreen(),
    );
  }
}

class ExampleScreen extends StatefulWidget {
  const ExampleScreen({Key? key}) : super(key: key);

  @override
  State<ExampleScreen> createState() => _ExampleScreenState();

}

class _ExampleScreenState extends State<ExampleScreen> {

  @override
  void initState() {
    memoInterface.init().then((value) => getList());
    super.initState();
  }
  void getList(){
    list = memoInterface.getList();
    setState(() {
      
    });
  }




  late final transformMatrices = {
    'Scale': getScaleDownMatrix,
  };

  late String currentMatrix = transformMatrices.entries.first.key;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body:
      Column(
        children: [
          SizedBox(height: 150.0,),
          Expanded(
            child: IndexedStack(
              index: transformMatrices.keys.toList().indexOf(currentMatrix),
              children: [
                for (final matrix in transformMatrices.values)
                  TransformableListView.builder(

                    controller: ScrollController(),
                    padding: EdgeInsets.zero,
                    getTransformMatrix: matrix,
                    itemBuilder: (context, index) {
                      return Container(
                        height: 80,
                        margin: const EdgeInsets.symmetric(
                          horizontal: 16,
                          vertical: 4,
                        ),
                        decoration: BoxDecoration(
                          color: list[index].completeYn=="N" ? Colors.blueAccent : Colors.grey,
                          borderRadius: BorderRadius.circular(20),
                        ),
                        alignment: Alignment.center,
                        child: Text(list[index].content),
                      );
                    },
                    itemCount: list.length
                  ),
              ],
            ),

          ),
          SizedBox(height: 150.0,),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _dialogBuilder(context).then((value) => refresh()),
        tooltip: 'Increment',
        backgroundColor: Colors.white,
        child: const Icon(Icons.add,color: Colors.black,),
      ), //
    );
  }

  refresh() {
    setState(() {

    });
  }
}
Future<void> _dialogBuilder(BuildContext context) {
  final _contentEditController = TextEditingController();
  return showDialog<void>(
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        title: const Text('메모 추가'),
        content: Row(children: <Widget>[

          Flexible(
            child: Container(
              margin: EdgeInsets.only(right: 20),
              child: TextField(
                controller: _contentEditController,
                style: TextStyle(color: Colors.black),
                decoration: InputDecoration(
                    border: InputBorder.none,
                    hintText: '메모를 입력해주세요',
                    hintStyle: TextStyle(color: Colors.grey[300])),
                cursorColor: Colors.blue,
              ),
            ),
          ),
        ]),
        actions: <Widget>[
          TextButton(
            style: TextButton.styleFrom(
              textStyle: Theme
                  .of(context)
                  .textTheme
                  .labelLarge,
            ),
            child: const Text('취소'),
            onPressed: () {
              Navigator.of(context).pop();
            },
          ),
          TextButton(
            style: TextButton.styleFrom(
              textStyle: Theme
                  .of(context)
                  .textTheme
                  .labelLarge,
            ),
            child: const Text('저장'),
            onPressed: () {
              memoInterface.save(_contentEditController.text);
              if(list.length ==0) {
              list = <Memo>[new Memo(list.length, _contentEditController.text, "N")];
              }
              else {
                list.add(
                    new Memo(list.length, _contentEditController.text, "N"));
              }
              Navigator.of(context).pop();
              context.widget.createElement();
              // settingList();
            },
          ),
        ],
      );
    },
  );
}
Matrix4 getScaleDownMatrix(TransformableListItem item) {
  /// final scale of child when the animation is completed
  const endScaleBound = 0.3;

  /// 0 when animation completed and [scale] == [endScaleBound]
  /// 1 when animation starts and [scale] == 1
  final animationProgress = item.visibleExtent / item.size.height;

  /// result matrix
  final paintTransform = Matrix4.identity();

  /// animate only if item is on edge
  if (item.position != TransformableListItemPosition.middle) {
    final scale = endScaleBound + ((1 - endScaleBound) * animationProgress);

    paintTransform
      ..translate(item.size.width / 2)
      ..scale(scale)
      ..translate(-item.size.width / 2);
  }

  return paintTransform;
}

 

다음번에는, 해당 메모별로 완료 및 삭제 기능을 추가할 예정이다.

반응형