# 数据持久化-GetStorage

有几个常见的flutter侧的数据持久化方案:

  • shared_preferences
  • localstorage
  • get_storage(推荐)
  • Fluttter_secure_storage -> 安全性好,但是性能会差一点

其中的get_storage的性能最好:

image-20220122154609883

都是用于存储key-value的形式的数据。

# get_storage介绍

# 安装依赖

dependencies:
  get_storage: ^2.0.3

使用flutter pub get安装依赖。

# 具体使用

调整main.dart,需要初始化GetStorage:

main() async {
  await GetStorage.init();
  runApp(App());
}

创建一个storage.dart的文件,用于存放常用的比如token、userInfo等方法:

import 'package:get_storage/get_storage.dart';
import 'package:my_app/entity/user_info.dart';

enum StoreKeys { token, refreshToken, userInfo }

class Storage {
  static Storage _storage = Storage._internal();
  final GetStorage _box = GetStorage();

  GetStorage get box => _box;

  Storage._internal();

  factory Storage() => _storage;

  // setToken, getToken
  setToken(String token) => _box.write(StoreKeys.token.toString(), token);
  String? getToken() => _box.read<String>(StoreKeys.token.toString());

  // setRefreshToken, getRefreshToken
  setRefreshToken(String refreshToken) =>
      _box.write(StoreKeys.refreshToken.toString(), refreshToken);
  String? getRefreshToken() =>
      _box.read<String>(StoreKeys.refreshToken.toString());

  // setUserInfo, getUserInfo
  setUserInfo(UserInfo userInfo) =>
      _box.write(StoreKeys.userInfo.toString(), userInfo);

  UserInfo? getUserInfo() => _box.read<UserInfo>(StoreKeys.userInfo.toString());
}

GetStorage高性能的原因:

As soon as you declare "write" the file is immediately written in memory and can now be accessed immediately with box.read(). You can also wait for the callback that it was written to disk using await box.write().

read与write方法不需要使用async/await,它会先记录到内存中,再记录到文件,是自动异步。

在文件中使用,可以见下文。

# 完善登录逻辑

登录部分现在还缺少:

  • 登录请求
  • 登录成功之后处理
  • 存储token等有用信息 -> 持久化
  • 登录失败的错误提示

对用户手机号进行正则判断,可以考虑把所有的正则写在一个文件中,方便维护:

class RegParttern {
  static const mobileParttern = r'^1[3-9]\d{9}$';
  static const codeParttern = r'\d{6}$';
}

修改lib/services/user_service.dart文件,新增方法login

Future login(String mobile, String code) async {
  var params = {'mobile': mobile, 'code': code};
  var res = await DioHttp().post('/login/loginByPhone', params);
  if (res.statusCode == 200) {
    // 请求成功
    Map<String, dynamic>? data = res.data;
    HttpResponse httpResponse = HttpResponse.fromJson(data!);
    if (httpResponse.code == 200) {
      UserInfo? userInfo = UserInfo.fromJson(httpResponse.data);
      Storage().setToken(httpResponse.token!);
      Storage().setRefreshToken(httpResponse.refreshToken!);
      Storage().setUserInfo(userInfo);
      // 返回给前端一个数据 -> 路由导航
      httpResponse.data = userInfo;
      // _userInfo = httpResponse.data as UserInfo
    } else {
      // 请求异常
      CommonToast.showToast(httpResponse.message ?? '登录失败,请重试');
    }
    return httpResponse;
  }
}

调整login_form.dart的逻辑:

Padding(
  padding: const EdgeInsets.only(top: 10.0),
  child: SizedBox(
    width: 120,
    child: ElevatedButton(
      child: Text('登录'),
      onPressed: () async {
        String mobile = controller.text;
        String code = codeController.text;
        if (RegExp(RegParttern.mobileParttern).hasMatch(mobile) &&
            RegExp(RegParttern.codeParttern).hasMatch(code)) {
          await userService.login(mobile, code);
        } else {
          CommonToast.showToast("手机号或验证码格式不正确");
        }
      },
    ),
  ),
)

# 整个login_form.dart文件

import 'package:flutter/material.dart';
import 'package:my_app/services/user_service.dart';
import 'package:my_app/setup_get_it.dart';
import 'package:my_app/widgets/base/button/counter_button.dart';
import 'package:my_app/widgets/base/fonts/my_icons_icons.dart';
import 'package:my_app/widgets/base/toast/common_toast.dart';
import 'package:my_app/widgets/const/reg_parttern.dart';

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

  
  _LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  bool _active = false;
  TextEditingController controller = TextEditingController();
  TextEditingController codeController = TextEditingController();
  FocusNode currentFocusNode = FocusNode();
  FocusNode focusNode = FocusNode();
  UserService userService = getIt<UserService>();

  _changeFocus(FocusNode focusNodeNext) {
    // 判断一下,current是否被聚焦,取消聚焦 -> 聚焦新的输入框
    FocusNode currentFocus = FocusScope.of(context);
    if (!currentFocus.hasPrimaryFocus) {
      currentFocus.unfocus();
    }
    focusNodeNext.requestFocus();
  }

  
  Widget build(BuildContext context) {
    return Container(
      height: 300,
      margin: const EdgeInsets.symmetric(
        vertical: 20.0,
        horizontal: 15.0,
      ), // padding(20px, 15px)
      padding: const EdgeInsets.only(
        top: 30.0,
        right: 30.0,
        bottom: 20.0,
        left: 30.0,
      ),
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Colors.black26,
            blurRadius: 5.0,
          ),
        ],
        // BorderRadius.circular(10),
        borderRadius: BorderRadius.all(Radius.circular(10)),
      ),
      // 1. padding
      // padding: const EdgeInsets.only(top: 110),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        // crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          TextField(
              focusNode: currentFocusNode,
              controller: controller,
              // keyboardAppearance: Brightness.light,
              decoration: InputDecoration(
                counterText: "",
                prefixIcon: Icon(
                  MyIcons.person,
                  color: Colors.black54,
                  size: 26,
                ),
              ),
              maxLength: 11,
              keyboardType: TextInputType.phone,
              onChanged: (val) {
                RegExp exp = RegExp(r'^1[3-9]\d{9}$');
                if (exp.hasMatch(val)) {
                  setState(() {
                    _active = true;
                  });
                } else {
                  setState(() {
                    _active = false;
                  });
                }
                // print('val is 👉 $val');
              }),
          // Container, SizedBox, Padding, Divider
          Padding(
            padding: const EdgeInsets.all(15.0),
          ),
          // stack
          Stack(
            children: [
              TextField(
                controller: codeController,
                focusNode: focusNode,
                decoration: InputDecoration(
                  counterText: "",
                  prefixIcon: Icon(
                    Icons.lock,
                    color: Colors.black54,
                  ),
                ),
                maxLength: 6,
                keyboardType: TextInputType.number,
              ),
              // Positioned
              // 1.页面按钮状态 -> 有状态组件如何设置页面状态
              // 2.倒计时逻辑 -> stream
              Positioned(
                right: 10.0,
                child: CounterButton(
                  active: _active,
                  onPressed: () async {
                    _changeFocus(focusNode);
                    userService.sendCode(controller.text);
                  },
                ),
              )
            ],
          ),
          // method1: Padding -> child
          // methods2: Padding
          Padding(
            padding: const EdgeInsets.only(top: 10.0),
            child: SizedBox(
              width: 120,
              child: ElevatedButton(
                child: Text('登录'),
                onPressed: () async {
                  String mobile = controller.text;
                  String code = codeController.text;
                  if (RegExp(RegParttern.mobileParttern).hasMatch(mobile) &&
                      RegExp(RegParttern.codeParttern).hasMatch(code)) {
                    await userService.login(mobile, code);
                  } else {
                    CommonToast.showToast("手机号或验证码格式不正确");
                  }
                },
              ),
            ),
          )
        ],
      ),
    );
  }
}