Skip to content

学习Flutter,准备基于所学搭建一个脚手架,不断的去更新维护这个项目。

项目准备

首先创建一个Flutter项目,因为主要还是面向移动端开发,所以先聚焦于Android(没有IOS设备) 修改gradle为本地的,免得去网上下半天 image.png build文件下的下载源也换一下

java
// 阿里云云效仓库:https://maven.aliyun.com/mvn/guide
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/google' }
// 华为开源镜像:https://mirrors.huaweicloud.com
maven { url 'https://repo.huaweicloud.com/repository/maven' }
// JitPack 远程仓库:https://jitpack.io
maven { url 'https://jitpack.io' }

image.png 然后运行起来,确定项目是能跑的 image.png 确定一下Android项目使用的sdk,以及flutter和dart的版本 image.pngimage.png

项目配置

修改项目的应用名称配置和图标配置 Android: image.png 配置项目的配置文件 image.png

基础工具类封装

吐司工具

使用框架:

java
fluttertoast: ^8.0.8

定义好吐司显示位置,和背景颜色,使用者只需要传入显示的信息即可

java
class ToastUtils {
  static void showToast(String msg) {
    //如果已经显示,则取消
    Fluttertoast.cancel();
    //显示
    Fluttertoast.showToast(
        msg: msg,
        backgroundColor: Colors.black54,
        gravity: ToastGravity.CENTER);
  }
}

日志工具

控制是否为debug模式,打印日志,以及打印的格式

java
class LogUtils {
  ///打印log日志
  static const String _defaultLogTag = "flutter_log";

  ///是否是debug模式,true:log不输出
  static bool _debugMode = false;

  ///log日志的长度
  static int _maxLogLength = 130;

  ///当前的logTag的值
  static String _tagValue = _defaultLogTag;

  static void init({
    String tag = _defaultLogTag,
    bool isDebug = false,
    int maxLen = 130,
  }) {
    _tagValue = tag;
    _debugMode = isDebug;
    _maxLogLength = maxLen;
  }

  static void e(Object object, {required String tag}) {
    if (_debugMode) {
      _printLog(tag, 'e', object);
    }
  }

  static void _printLog(String tag, String s, Object object) {
    String da = object.toString();
    tag = tag ?? _tagValue; //tag为空则返回_tagValue的值
    if (da.length <= _maxLogLength) {
      debugPrint("$tag$tag $da");
      return;
    }
    debugPrint(
        '$tag$s — — — — — — — — — — — — — — — — st — — — — — — — — — — — — — — — —');
    while (da.isNotEmpty) {
      if (da.length > _maxLogLength) {
        debugPrint("$tag$s | ${da.substring(0, _maxLogLength)}");
        da = da.substring(_maxLogLength, da.length);
      } else {
        debugPrint("$tag$s | $da");
        da = "";
      }
    }
    debugPrint(
        '$tag$s — — — — — — — — — — — — — — — — ed — — — — — — — — — — — — — — — —');
  }
}

路由框架

路由的关闭和打开

java
class NavigatorUtils {
  ///关闭当前页面
  ///[context]当前页面的context
  ///[parameters]需要回传到上一个页面的参数
  static pop(BuildContext context, {parameters}) {
    if (Navigator.canPop((context))) {
      Navigator.of(context).pop(parameters);
    } else {
      //最后一个页面不可以pop
    }
  }

  ///动态路由方法封装
  ///[context]当前页面的Context
  ///[routeName]目标页面的路由名称
  ///[parameters]向目标页面传的参数
  ///[callback]目标页面关闭时的回调函数
  ///[isReplace]是否替换当前的路由
  static pushPage(
    BuildContext context,
    Widget page, {
    required String routeName,
    parameters,
    required Function callback,
    bool isReplace = false,
  }) {
    PageRoute pageRoute;
    //ios平台
    if (Platform.isIOS) {
      pageRoute = CupertinoPageRoute(
        builder: (_) {
          return page;
        },
        settings: RouteSettings(name: routeName, arguments: parameters),
      );
    } else {
      //android等其他平台使用Material风格
      pageRoute = MaterialPageRoute(
        builder: (_) {
          return page;
        },
        settings: RouteSettings(name: routeName, arguments: parameters),
      );
    }

    //替换当前的路由
    //目标页面关闭时回调函数与回传参数
    if (isReplace) {
      Navigator.of(context)
          .pushReplacement(pageRoute)
          .then((value) => {callback(value)});
    }
    //压栈
    //目标页面关闭时回调函数与回传参数
    Navigator.of(context).push(pageRoute).then((value) => {callback(value)});
  }
}

SP本地数据缓存

我们在使用shared_preferences时每次都需要去获取它的实例,如果多个地方用到,那么每次都要实例化一次。这样代码的可读性差,后期的维护成本也变得很高,而且还不支持存储Map类型,所以接下来我们对shared_preferences来封装一个通用而且使用更简单的库。 因为我们获取的都是同一个实例,所以采用单例模式来进行封装最好,而且获取实例是异步的,所以我们在应用程序启动时先初始化,这样使用起来更加的方便。 参考文章:https://juejin.cn/post/7012840579964862471#heading-35

java
class SPUtils {
  SPUtils._internal();

  factory SPUtils() => _instance;
  static final SPUtils _instance = SPUtils._internal();
  static late SharedPreferences _sharedPreferences;

  static Future<SPUtils> getInstance() async {
    _sharedPreferences = await SharedPreferences.getInstance();
    return _instance;
  }

  //清除数据
  static void remove(String key) async {
    if (_sharedPreferences.containsKey(key)) {
      _sharedPreferences.remove(key);
    }
  }

  // 异步保存基本数据类型
  static Future save(String key, dynamic value) async {
    if (value is String) {
      _sharedPreferences.setString(key, value);
    } else if (value is bool) {
      _sharedPreferences.setBool(key, value);
    } else if (value is double) {
      _sharedPreferences.setDouble(key, value);
    } else if (value is int) {
      _sharedPreferences.setInt(key, value);
    } else if (value is List<String>) {
      _sharedPreferences.setStringList(key, value);
    }
  }

  // 异步读取
  static Future<String?> getString(String key) async {
    return _sharedPreferences.getString(key);
  }

  static Future<int?> getInt(String key) async {
    return _sharedPreferences.getInt(key);
  }

  static Future<bool?> getBool(String key) async {
    return _sharedPreferences.getBool(key);
  }

  static Future<double?> getDouble(String key) async {
    return _sharedPreferences.getDouble(key);
  }

  ///保存自定义对象
  static Future saveObject(String key, dynamic value) async {
    ///通过 json 将Object对象编译成String类型保存
    _sharedPreferences.setString(key, json.encode(value));
  }

  ///获取自定义对象
  ///返回的是 Map<String,dynamic> 类型数据
  static dynamic getObject(String key) {
    String? data = _sharedPreferences.getString(key);
    return (data == null || data.isEmpty) ? null : json.decode(data);
  }

  ///保存列表数据
  static Future<bool> putObjectList(String key, List<Object> list) {
    ///将Object的数据类型转换为String类型
    List<String>? dataList = list.map((value) {
      return json.encode(value);
    }).toList();
    return _sharedPreferences.setStringList(key, dataList);
  }

  ///获取对象集合数据
  ///返回的是List<Map<String,dynamic>>类型
  static List<Map>? getObjectList(String key) {
    List<String>? dataLis = _sharedPreferences.getStringList(key);
    return dataLis?.map((value) {
      Map dataMap = json.decode(value);
      return dataMap;
    }).toList();
  }
}

除此之外我们还会创建一个单独的文件夹来保存sp的key值,统一管理,sp_key

java
String spUserIsFirstKey ="sp_user_isfirst";
///是否同意隐私协议
String spUserProtocolKey ="sp_user_protocol";
///主题保存KEY
String spUserThemeKey="sp_user_theme";

///用户信息保存KEY
String spUserBeanKey ="sp_user_bean";

用户信息配置

一般我们要保存用户的个性化设置在本地,所以也会通过一个类来统一管理

  1. 构造单例对象
  2. 获取用户登录信息
  3. 获取用户是否统一隐私与用户协议
  4. 加载用户缓存信息
  5. 退出登录,清楚缓存信息
java
class UserHelper {
  // 私有构造函数
  UserHelper._() {
    // 具体初始化代码
  }

  ///获取单例对象
  static UserHelper getInstance = UserHelper._();

  //用户基本信息模型
  UserBean? _userBean;

  //获取 UserBean
  UserBean? get userBean => _userBean;

  //userBean的设置方法
  set userBean(UserBean? bean) {
    _userBean = bean;
    //缓存用户信息
    SPUtils.saveObject(spUserBeanKey, bean);
  }
  ///判断用户是否登录的便捷方法
  bool get userIsLogin =>userBean==null?false:true;

  ///是否同同意隐私与用户协议
  bool? _userProtocol = false;

  bool? get userProtocol => _userProtocol;

  set userProtocol(bool? flag) {
    _userProtocol = flag;

    //保存同意的标识
    SPUtils.save(spUserProtocolKey, flag);
  }

  //判断用户是否同意用户协议便捷方法
  bool? get isUserProtocol => _userProtocol ?? false;

  ///用来初始化用户信息的缓存数据
  Future<bool> init() async {
    ///加载缓存数据
    Map<String, dynamic> map = await SPUtils.getObject(spUserBeanKey);

    ///解析缓存数据
    _userBean = UserBean.fromJson(map);
    return Future.value(true);
  }

  //退出登录 清除数据
  void exitLogin() {
    _userBean = null;
    SPUtils.remove(spUserBeanKey);
  }
}

网络封装

HttpHelper封装网络请求所有接口的URL地址

java
/// 网络请求所有接口的URL地址
class HttpHelper{
  static const String BASE_HOST="";
  //获取用户的基本信息
  static const String USER_INFO="";
  //用户登录
  static const String USER_LOGIN="";
}

LoadingStatus封装网络请求的状态

java
/// 网络请求的状态

enum LoadingStatus{
  success,//加载成功有数据
  noData,//加载成功没数据
  fail,//加载失败
  none,//默认无状态
  loading,//加载中
}

LogInterceptor是日志拦截器,监控到网络的请求以及响应等过程状态

java
/// 日志拦截器
class LogInterceptor extends InterceptorsWrapper {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    print("\n================== 请求数据 ==========================");
    print("|请求url:${options.path}");
    print('|请求头: ' + options.headers.toString());
    print('|请求参数: ' + options.queryParameters.toString());
    print('|请求方法: ' + options.method);
    print("|contentType = ${options.contentType}");
    print('|请求时间: ' + DateTime.now().toString());
    if (options.data != null) {
      print('|请求数据: ' + options.data.toString());
    }
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    print("\n|================== 响应数据 ==========================");
    if (response != null) {
      print("|url = ${response.realUri}");
      print("|code = ${response.statusCode}");
      print("|data = ${response.data}");
      print('|返回时间: ' + DateTime.now().toString());
      print("\n");
    } else {
      print("|data = 请求错误 E409");
      print('|返回时间: ' + DateTime.now().toString());
      print("\n");
    }
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    print("\n================== 错误响应数据 ======================");
    print("|url = ${err.response?.realUri}");
    print("|type = ${err.type}");
    print("|message = ${err.message}");

    print('|response = ${err.response}');
    print("\n");
  }
}

最终就是封装Dio中的get请求和post请求,同时也需要对异常做处理 在封装Dio类时,使用了factory关键字,实现单例,保证实例唯一Dart中的factory关键字

java
/// dio网络请求封装
class DioUtils {
  static late final Dio _dio;

  //factory单例模式
  static final DioUtils _instance = DioUtils._internal();

  factory DioUtils() => _instance;

  //配置代理标识 false 标识不配置
  bool isProxy = false;

  //网络代理地址
  String proxyIp = "";

  //网络代理端口
  String proxyPort = "";

  DioUtils._internal() {
    BaseOptions options = BaseOptions();
    //请求超时时间
    options.connectTimeout = 20000;
    options.receiveTimeout = 2 * 60 * 1000;
    options.sendTimeout = 2 * 60 * 1000;
    //初始化
    _dio = Dio(options);
    //当App运行在Release环境时,inProduction为true;
    // 当App运行在Debug和Profile环境时,inProduction为false。
    bool inProduction = const bool.fromEnvironment("dart.vm.product");
    if (!inProduction) {
      debugFunction();
    }
  }

  void debugFunction() {
    //添加log
    _dio.interceptors.add(LogInterceptor());
    //配置代理
    if (isProxy) {
      _setupProxy();
    }
  }

  //配置代理
  void _setupProxy() {
    (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
        (HttpClient client) {
      client.findProxy = (uri) {
        //proxyIp 地址  proxyPort 端口
        return 'PROXY $proxyIp : $proxyPort';
      };
      client.badCertificateCallback =
          (X509Certificate cert, String host, int port) {
        //忽略证书
        return true;
      };
      return null;
    };
  }
  /// get 请求
  ///[url]请求链接
  ///[queryParameters]请求参数
  ///[cancelTag] 取消网络请求的标识
  Future<ResponseInfo> getRequest({required String url,
    required Map<String, dynamic> queryParameters,
    required CancelToken cancelTag}) async {
    //发起get请求
    try{
      Response response=await _dio.get(url,
      queryParameters: queryParameters,cancelToken: cancelTag);
      //响应数据
      dynamic responseData = response.data;
      //数据解析
      if(responseData is Map<String,dynamic>){
        //转换
        Map<String,dynamic>responseMap=responseData;
        int code=responseMap["code"];
        if(code==200){
          //业务代码处理正常
          dynamic data=responseMap["data"];
          return ResponseInfo(data: data);
        }else{
          return ResponseInfo.error(message: "数据解析异常");
        }
      }else{
        //业务代码异常
        return ResponseInfo.error(code:responseData["code"]);
      }
    }catch(e,s){
      //异常
      return errorController(e,s);
    }
  }


  ///
  ///
  ///
  Future<ResponseInfo> postRequest(
  {required String url,
  required Map<String,dynamic>formDataMap,
  required Map<String,dynamic>jsonMap,
  required CancelToken cancelTag}) async{
    FormData form=FormData.fromMap(formDataMap);
    //发起post请求
    try{
      Response response=await _dio.post(url,data: form,cancelToken: cancelTag);
      //响应数据
      dynamic responseData=response.data;
      if (responseData is Map<String, dynamic>) {
        Map<String, dynamic> responseMap = responseData;
        int code = responseMap["code"];
        if (code == 200) {
          //业务代码处理正常
          //获取数据
          dynamic data = responseMap["data"];
          return ResponseInfo(data: data);
        } else {
          //业务代码异常
          return ResponseInfo.error(
              code: responseMap["code"],
              message:responseMap["message"]);
        }
      }else{
        return ResponseInfo.error(
            message:"数据解析异常");
      }
    }catch(e,s){
      return errorController(e, s);
    }
  }
  Future<ResponseInfo> errorController(e, StackTrace s) {
    ResponseInfo responseInfo=ResponseInfo();
    responseInfo.success=false;
    //网络错误处理
    if(e is DioError){
      DioError dioError = e;
      switch(dioError.type){
        case DioErrorType.connectTimeout:
          responseInfo.message = "连接超时";
          break;
        case DioErrorType.sendTimeout:
          responseInfo.message = "请求超时";
          break;
        case DioErrorType.receiveTimeout:
          responseInfo.message = "响应超时";
          break;
        case DioErrorType.response:
        // 响应错误
          responseInfo.message = "响应错误";
          break;
        case DioErrorType.cancel:
        // 取消操作
          responseInfo.message = "已取消";
          break;
        case DioErrorType.other:
        // 默认自定义其他异常
          responseInfo.message = dioError.message;
          break;
      }
    }else{
      //其他错误
      responseInfo.message="未知错误";
    }
    return Future.value(responseInfo);
  }
}

class ResponseInfo {
  late bool success;
  late int code;
  late String message;
  dynamic data;

  ResponseInfo(
      {this.success = true, this.code = 200, this.data, this.message = "请求成功"});

  ResponseInfo.error({
    this.success = false,
    this.code = 201,
    this.message = "请求异常",
  });
}

配置闪屏页面

在Android目录下的launch_backgraound.xml文件下可以配置闪屏页面的背景颜色和需要加载的图片 image.png

启动初始化页面

我们需要在默认的main.dart中配置启动的根视图以及flutter项目运行app报错捕捉UI显示的功能 主要完成了两件事情:

  1. 配置根页面
  2. 自定义报错页面
java
///
/// 1.根页面
/// 2.报错页面
void main() {
  //启动根目录
  runApp(AppRootPage());
  //自定义报错页面
  ErrorWidget.builder = (FlutterErrorDetails details) {
    //debug模式下输出日志
    debugPrint(details.toString());
    return Scaffold(
      body: Container(
        width: double.infinity,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text("App错误,快去返回给作者!${details.exception}",
              maxLines: 4,),
          ],
        ),
      ),
    );
  };
}

在根页面中需要

  1. 配置主题色,语言环境
  2. 默认显示界面IndexPage
java
/// 1.配置主题色,语言环境
/// 2.配置默认显示页面
class AppRootPage extends StatefulWidget {
  const AppRootPage({super.key});

  @override
  State<StatefulWidget> createState() => _AppRootPageState();
}

class _AppRootPageState extends State<AppRootPage> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      //应用的主题
      theme: ThemeData(
        //主背景色
        primaryColor: Colors.blue,
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      //应用程序默认显示的页面
      home: IndexPage(),
      //debug模式下不显示debug标签
      debugShowCheckedModeBanner: false,
      //国际化语言环境
      localizationsDelegates: [],
      //配置程序语言环境
      locale: const Locale('zh', 'CN'),
    );
  }

  @override
  void dispose() {
    //执行一些注销订阅的操作
    super.dispose();
  }
}

IndexPage是默认显示界面,也是我们启动App后的启动页面 在这个页面中主要完成,一些第三方功能的初始化,以及获取用户偏好配置。 并且根据用户是否第一次启动App决策进入首页面还是引导页面

  1. 初始化sp
  2. 初始化日志工具
  3. 获取用户是否第一次登录
  4. 获取用户的隐私协议
  5. 初始化用户的登录信息
  6. 判断用户隐私协议
  7. 根据用户是否第一次登录决定进入欢迎界面还是引导页面

引导页面

用户第一次登录会进入引导页面

欢迎界面

倒计时的功能,播放广告