• Flutter如何实现app端多环境切换功能?
  • 发布于 2个月前
  • 130 热度
    0 评论
一、需求来源
工作中遇到某些偶现或者问题流程比较长的 bug,需要反复进行切换环境或者配合接口同学进行流程操作的场景,就特别耗费团队时间,就想在app端支持 api 环境切换的功能。再考虑到后端人员变动的原因,再 dev 环境可以进行域名输入的功能之后,此功能趋于完美,遂分享给大家。
最终实现效果图:

二、使用示例
NOriginSheet(),
三、实现源码
1. CacheService 源码
import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter_templet_project/network/RequestConfig.dart';
import 'package:shared_preferences/shared_preferences.dart';

/// 请求环境信息缓存
const String CACHE_REQUEST_ENV = "CACHE_REQUEST_ENV";

const String CACHE_REQUEST_ENV_DEV_ORIGIN = "CACHE_REQUEST_ENV_DEV_ORIGIN";


class CacheService {

  CacheService._() {
    init();
  }

  static final CacheService _instance = CacheService._();

  factory CacheService() => _instance;

  static CacheService get shard => _instance;

  SharedPreferences? prefs;

  init() async {
    prefs ??= await SharedPreferences.getInstance();
    debugPrint("init prefs: $prefs");
  }

  /// 清除数据
  Future<bool>? remove(String key) {
    return prefs?.remove(key);
  }


  setString(String key, String? value) {
    if (value == null) {
      return;
    }
    prefs?.setString(key, value);
  }

  String? getString(String key) {
    final result = prefs?.getString(key);
    return result;
  }

  ......
}


extension CacheServiceExt on CacheService {

  /// 清除登录环境
  clearEnv() {
    CacheService().remove(CACHE_REQUEST_ENV);
  }

  /// 设置登录环境
  set env(APPEnvironment? val) {
    if (val == null) {
      return;
    }
    final result = val.toString();
    CacheService().setString(CACHE_REQUEST_ENV, result);
  }

  /// 获取登录环境
  APPEnvironment? get env {
    final result = CacheService().getString(CACHE_REQUEST_ENV);
    final val = APPEnvironment.fromString(result);
    return val;
  }

  /// 设置登录环境 dev 下的域名
  set devOrigin(String? val) {
    if (val == null || val.isEmpty) {
      return;
    }
    CacheService().setString(CACHE_REQUEST_ENV_DEV_ORIGIN, val);
  }
  /// 堆代码 duidaima.com
  /// 获取登录环境 dev 下的域名
  String? get devOrigin {
    return CacheService().getString(CACHE_REQUEST_ENV_DEV_ORIGIN);
  }

  ......
}
2. RequestConfig 源码
//
//  RequestConfig.dart
//  flutter_templet_project
//
//  Created by shang on 2024/1/6 10:58.
//  Copyright © 2024/1/6 shang. All rights reserved.
//
import 'package:flutter_templet_project/cache/CacheService.dart';
/// 当前 api 环境
enum APPEnvironment{
  /// 开发环境
  dev('https://*.cn'),
  /// 预测试环境
  beta('https://*.cn'),
  /// 测试环境
  test('https://*.cn'),
  /// 预发布环境
  pre('https://*.cn'),
  /// 生产环境
  prod('https://*.cn');
  const APPEnvironment(this.origin,);
  /// 当前枚举对应的域名
  final String origin;


  /// 字符串转类型
  static APPEnvironment? fromString(String? val) {
    if (val == null || !val.contains(",")) {
      return null;
    }

    final list = val.split(",");
    if (list.length != 2) {
      return null;
    }
    final first = list[0];
    final isEnumType = APPEnvironment.values.map((e) => e.name).contains(first);
    if (!isEnumType) {
      return null;
    }
    return APPEnvironment.values.firstWhere((e) => e.name == first);
  }

  @override
  String toString(){
    if (this == APPEnvironment.dev) {
      return "$name,${CacheService().devOrigin ?? origin}";
    }
    return "$name,$origin";
  }
}
///request config
class RequestConfig {
  static APPEnvironment current = APPEnvironment.dev;
  /// 网络请求域名
  static String get baseUrl {
    final env = CacheService().env;
    if (env != null) {
      current = env;
      if (env == APPEnvironment.dev) {
        return CacheService().devOrigin ?? current.origin;
      }
    }
    return current.origin;
  }


}

class RequestMsg {
  static String networkSucessMsg = "操作成功";
  static String networkErrorMsg = "网络连接失败,请稍后重试";
  static String networkErrorSeverMsg = '服务器响应超时,请稍后再试!';

  static Map<String, String> statusCodeMap = <String, String>{
    '401': '验票失败!',
    '403': '无权限访问!',
    '404': '404未找到!',
    '500': '服务器内部错误!',
    '502': '服务器内部错误!',
  };

}
3. NOriginSheet 源码
//  NOriginSheet.dart
//  flutter_templet_project
//
//  Created by shang on 2024/1/6 11:54.
//  Copyright © 2024/1/6 shang. All rights reserved.
//


import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_templet_project/basicWidget/n_textfield.dart';
import 'package:flutter_templet_project/cache/CacheService.dart';
import 'package:flutter_templet_project/extension/widget_ext.dart';
import 'package:flutter_templet_project/network/RequestConfig.dart';
import 'package:flutter_templet_project/util/color_util.dart';
import 'package:flutter_templet_project/util/debug_log.dart';
import 'package:flutter_templet_project/vendor/easy_toast.dart';


/// 域名选择器
class NOriginSheet extends StatefulWidget {

  NOriginSheet({
    super.key,
    this.onChanged,
  });

  /// 改变回调
  final void Function(APPEnvironment env, String origin)? onChanged;


  @override
  State<NOriginSheet> createState() => _NOriginSheetState();
}

class _NOriginSheetState extends State<NOriginSheet> {

  final textController = TextEditingController();

  APPEnvironment get currentEnv{
    final env = CacheService().env;
    final result = env ?? RequestConfig.current;
    return result;
  }

  @override
  Widget build(BuildContext context) {
    return buildOriginSheet();
  }

  /// 域名选择
  Widget buildOriginSheet() {
    if (currentEnv == APPEnvironment.prod) {
      return const SizedBox();
    }

    const list = APPEnvironment.values;

    // final currentWidget = Column(
    //   children: [
    //     // Text(RequestChannel.baseUrl,),
    //     Text("当前域名: ${currentEnv.name}",),
    //     Text("当前域名: ${currentEnv.origin}",),
    //   ],
    // );


    final currentWidget = Column(
      children: "${currentEnv}".split(",").map((e) {
        return Text(e,);
      }).toList(),
    );

    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          TextButton(
            style: TextButton.styleFrom(
              foregroundColor: Colors.red,
              padding: EdgeInsets.zero,
              tapTargetSize: MaterialTapTargetSize.shrinkWrap,
            ),
            onPressed: () {
              showAlertSheet(
                message: currentWidget,
                actions: list.map((e) {

                  return ListTile(
                    dense: true,
                    onTap: (){
                      Navigator.of(context).pop();

                      onUpdate(env: e, origin: e.origin);
                    },
                    title: Text(e.name.toString()),
                    subtitle: Text(e.origin),
                  );
                }).toList(),
              );
            },
            child: currentWidget,
          ),
          const SizedBox(width: 4,),
          Opacity(
            opacity: currentEnv == APPEnvironment.dev ? 1 : 0,
            child: InkWell(
              onTap: (){
                // YLog.d("edit");

                showAlertTextField(
                  onChanged: (String value) {
                    DebugLog.d("showAlertTextField $value");
                    onUpdate(env: APPEnvironment.dev, origin: value);
                  }
                );
              },
              child: Icon(Icons.edit, color: Colors.red,)
            ),
          ),
        ],
      ),
    );
  }

  void showAlertSheet({
    Widget title = const Text("请选择"),
    Widget? message,
    required List<Widget> actions,
  }) {
    CupertinoActionSheet(
      title: title,
      message: message,
      actions: actions,
      cancelButton: CupertinoActionSheetAction(
        isDestructiveAction: true,
        onPressed: () {
          Navigator.pop(context);
        },
        child: const Text('取消'),
      ),
    ).toShowCupertinoModalPopup(context: context);
  }


  void showAlertTextField({
    Widget? title = const Text("请选择"),
    Widget? message,
    required ValueChanged<String> onChanged,
  }) {
    textController.text = RequestConfig.baseUrl;

    CupertinoAlertDialog(
      title: title ?? const Padding(
        padding: EdgeInsets.only(bottom: 12),
        child: Text("请输入",
          style: TextStyle(
            fontWeight: FontWeight.w500,
          ),
        ),
      ),
      content: message ?? NTextField(
        controller: textController,
        style: const TextStyle(
          fontSize: 14,
          fontWeight: FontWeight.w400,
          color: fontColor,
        ),
        isCollapsed: true,
        contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
        onChanged: (String value) {
          // YLog.d("onChanged $value");
        },
        onSubmitted: (String value) {
          // YLog.d("onSubmitted $value");
        },
      ),
      actions: ["取消", "确定"].map((e) => TextButton(
        style: TextButton.styleFrom(
          padding: const EdgeInsets.symmetric(vertical: 12),
        ),
        onPressed: () {
          if (e == "确定") {
            final val = textController.text.trim();
            if (!val.startsWith("http")) {
              EasyToast.showToast("必须以 http 开头");
              return;
            }
            onChanged(val);
          }
          Navigator.pop(context);
        },
        child: Text(e),
      )).toList(),
    ).toShowCupertinoModalPopup(context: context);
  }

  onUpdate({
    required APPEnvironment env,
    required String origin,
  }) {
    RequestConfig.current = env;
    CacheService().env = env;
    if (env == APPEnvironment.dev) {
      CacheService().devOrigin = origin;
    }
    setState(() {});

    widget.onChanged?.call(env, origin);
  }
}
四、总结
1、此功能实现之后,后端上线新环境,如果 app 代码没有改动,测试同学直接切换环境即可测试和验证问题;
2、某些偶现或者问题流程比较长的 bug,需要反复进行切换环境或者配合接口同学进行流程操作的场景问题得到彻底解决。
3、对于开发者来说,创造力是第一竞争力。
用户评论