个人Flutter实战记录,不断完善中...

593 阅读7分钟

1. 新建flutter工程

学习编写Dart推荐repl.it ,可以在线新建 main.dart 文件

2. 引用三方库

在pubspec.yaml文件Pub get 需要用到的三方库、插件

  • 类似iOS的cocopods;
  • dependencies为代码运行时所需要的包,会打包到线上生产环境;devDependencies为开发环境所需要的包,不会打包至线上
  • 三方库如需指定版本在:后^+版本号
  • cupertino_icons图标库、 css_colors CSS颜色定义颜色常量 、 dio网络请求、logger日志

common_utils常用工具类库、flutter_swiper_null_safety轮播图、flutter_easyrefresh下拉刷新、fluttertoast提示弹框、cached_network_image图片加载、flutter_easyloading进度加载等等,后续可根据自己项目需求加入新的三方库

3. 设计工程目录

4. 引入头文件、设置页面路由

注意:创建dart文件官方推荐下划线创建如app_routes.dart,但是我用驼峰创建appRoutes.dart然后敲代码会报黄色波浪线警告,可能是我下载的白狐版的android studio有问题

解决方式:1.用_创建文件;

2.实在习惯驼峰创建那就右键选择删除analysis_options.yaml文件,然后点击do refactor

5. tabbar主页面设置

  1. main.dart设置默认加载路由

    import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:xpx/routes/app_pages.dart'; import 'package:xpx/routes/app_routes.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart';

    /// 应用入口 /// void main() => runApp(MyApp()); void main() async { runApp(MyApp()); }

    class MyApp extends StatelessWidget { MyApp({Key? key}) : super(key: key);

    @override Widget build(BuildContext context) { return GetMaterialApp( debugShowCheckedModeBanner: false, title: "Flutter Base", initialRoute: Routes.Initial, defaultTransition: Transition.fade, getPages: AppPages.pages, unknownRoute: AppPages.pages[1], theme: ThemeData( primarySwatch: Colors.blue, brightness: Brightness.light, ), builder: EasyLoading.init(), ); } }

  2. main_page配置,相当于我们的tabber配置

    import 'package:flutter/material.dart'; import 'package:xpx/pages/home/home_page.dart';

    /// 主界面 class MainPage extends StatefulWidget { @override State createState() { return _MainPageState(); } }

    class _MainPageState extends State { /// 当前位置索引 int _currentIndex = 0;

    /// 页面集合 List _pageList = [ HomePage(), HomePage() ];

    /// 底部Bar数据 final Map _bottomMap = { "首页": Icon(Icons.home), "我的": Icon(Icons.category), };

    @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( _bottomMap.keys.toList()[_currentIndex], ), ), body: IndexedStack( index: _currentIndex, children: _pageList, ), bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, items: _generateBarItems(), type: BottomNavigationBarType.fixed, onTap: (int index) { setState(() { _currentIndex = index; }); }, ), ); }

    /// 生成底部NavigationBarItem List _generateBarItems() { var items = []; _bottomMap.forEach((key, value) { items.add( BottomNavigationBarItem( icon: value, label: key, ), ); }); return items; } }

  3. 体验热重载,页面展示

6. 网络请求封装

网络请求主要是用官方推荐的dio库进行二次封装,并拆分了几个模块

  1. 域名、接口地址、代理设置、超时时间等配置

将域名、代理设置、超时时间放在http_config.dart,接口地址单独抽成了url_path_config.dart,是为了配置的时候看着更清晰

  • http_config.dart

    /// 配置管理 class HttpConfig { /// 超时时间 static const CONNECT_TIMEOUT = 30000; static const RECEIVE_TIMEOUT = 30000;

    /// 默认域名 static const String BASE_URL = "api-crm.test.51xpx.com";

    /// 是否启用代理 static const PROXY_ENABLE = false;

    /// 代理服务IP static const PROXY_IP = '172.16.43.74';

    /// 代理服务端口 static const PROXY_PORT = 8888; }

  • url_path_config.dart

    /// URL管理 class UrlPath { /// 获取验证码 static const String SEND_MESSAGE_PATH = "/user/sendSms"; }

  1. 错误码、客户端异常、服务端异常解析
  • 客户端请求错误bad_request_analysis.dart

    import 'package:xpx/network/analysis/http_analysis.dart';

    /// 客户端请求错误 class BadRequestException extends HttpException { BadRequestException([int? code, String? message]) : super(code, message); }

  • 服务端相应错误bad_response_analysis.dart

    import 'package:xpx/network/analysis/http_analysis.dart';

    /// 服务端响应错误 class BadResponseException extends HttpException { BadResponseException([int? code, String? message]) : super(code, message); }

  • 自定义异常、错误码处理http_analysis.dart

    import 'package:dio/dio.dart'; import 'package:xpx/network/analysis/bad_request_analysis.dart'; import 'package:xpx/network/analysis/bad_response_analysis.dart';

    /// 自定义异常 class HttpException implements Exception { final int? _code; final String? _message;

    HttpException([this._code, this._message]);

    int get code => _code ?? -1;

    String get message => _message ?? this.runtimeType.toString();

    String toString() { return "code==_code==_message"; }

    factory HttpException.create(DioError error) { switch (error.type) { case DioErrorType.cancel: return BadRequestException(-1, "请求取消"); case DioErrorType.connectTimeout: return BadRequestException(-1, "连接超时"); case DioErrorType.sendTimeout: return BadRequestException(-1, "请求超时"); case DioErrorType.receiveTimeout: return BadRequestException(-1, "响应超时"); case DioErrorType.response: return _convertHttpException(error); default: return HttpException(-1, error.message); } }

    /// code转换成对应异常 static HttpException convertHttpException(DioError error) { try { int errCode = error.response!.statusCode!; switch (errCode) { case 400: return BadRequestException(errCode, "请求语法错误"); case 401: return BadRequestException(errCode, "没有权限"); case 403: return BadRequestException(errCode, "服务器拒绝执行"); case 404: return BadRequestException(errCode, "无法连接服务器"); case 405: return BadRequestException(errCode, "请求方法被禁止"); case 500: return BadResponseException(errCode, "服务器内部错误"); case 502: return BadResponseException(errCode, "无效的请求"); case 503: return BadResponseException(errCode, "服务器挂了"); case 505: return BadResponseException(errCode, "不支持HTTP协议请求"); default: return HttpException(errCode, error.response!.statusMessage!); } } on Exception catch () { return HttpException(-1, "未知错误"); } } }

  1. 日志拦截、错误处理拦截器
  • 日志拦截log_interceptor.dart

    import 'package:dio/dio.dart'; import 'package:xpx/utils/extension/mapList_to_string.dart'; import 'package:xpx/utils/log_utils.dart';

    /// 日志拦截器 class DioLogInterceptor extends Interceptor { /// 请求 @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { String requestStr = "request url: options.baseUrl+options.path\n";requestStr+="requestmethod:{options.baseUrl + options.path}\n"; requestStr += "request method: {options.method}\n"; requestStr += "request header:\noptions.headers.mapToJsonString()\n";requestStr+="requestparams:\n{options.headers.mapToJsonString()}\n"; requestStr += "request params:\n{options.queryParameters.mapToJsonString()}\n";

    final data = options.data;
    if (data != null) {
      if (data is Map) {
        requestStr += "request body:\n${data.mapToJsonString()}\n";
      } else if (data is FormData) {
        final formDataMap = Map()
          ..addEntries(data.fields)
          ..addEntries(data.files);
        requestStr += "request body:\n${formDataMap.mapToJsonString()}\n";
      } else
        requestStr += "request body:\n${data.toString()}\n";
    }
    
    LogHelper.e("DioLogInterceptor onRequest", requestStr);
    super.onRequest(options, handler);
    

    }

    /// 出错 @override void onError(DioError err, ErrorInterceptorHandler handler) { String errorStr = "error url: err.requestOptions.baseUrl+err.requestOptions.path\n";errorStr+="errormethod:{err.requestOptions.baseUrl + err.requestOptions.path}\n"; errorStr += "error method: {err.requestOptions.method}\n"; errorStr += "error header:\n{err.response?.headers.map.mapToJsonString()}\n"; if (err.response != null && err.response?.data != null) { errorStr += "error body:\n{_parseResponse(err.response!)}\n"; } LogHelper.e("DioLogInterceptor onError", errorStr); super.onError(err, handler); }

    /// 响应 @override void onResponse(Response response, ResponseInterceptorHandler handler) { String responseStr = "response url: response.requestOptions.uri\n";responseStr+="responsestatus:{response.requestOptions.uri}\n"; responseStr += "response status: {response.statusCode}\n"; responseStr += "response header:\n{"; response.headers.forEach((key, list) => responseStr += "\n " + ""key\" : \"list","); responseStr += "\n}\n"; if (response.data != null) { responseStr += "response body:\n ${_parseResponse(response)}"; } LogHelper.e("DioLogInterceptor onResponse", responseStr); super.onResponse(response, handler); } /// 解析响应 String _parseResponse(Response response) { String responseStr = ""; var data = response.data; if (data is Map) { responseStr += data.mapToJsonString(); } else if (data is List) { responseStr += data.listToJsonString(); } else { responseStr += response.data.toString(); } return responseStr; } }

  • 错误处理拦截器error_interceptor.dart

    import 'package:dio/dio.dart'; import 'package:xpx/network/analysis/http_analysis.dart'; import 'package:xpx/utils/log_utils.dart';

    /// 错误处理拦截器 class ErrorInterceptor extends Interceptor { @override void onError(DioError err, ErrorInterceptorHandler handler) { // error统一处理 HttpException httpException = HttpException.create(err); err.error = httpException; _handlerHttpException(httpException); super.onError(err, handler); } /// 处理异常 _handlerHttpException(HttpException httpException) { LogHelper.e("ErrorInterceptor", httpException.toString()); } }

  1. 网络请求封装,初始化全局单例
  • 网络请求初始化,post&get及取消请求管理http_sington.dart

    import 'dart:io'; import 'package:dio/dio.dart'; import 'package:dio/adapter.dart'; import 'package:xpx/network/config/http_config.dart'; import 'package:xpx/network/interceptor/log_interceptor.dart';

    /// Http管理 class Http { static Http _instance = Http._internal(); Dio _dio = Dio(); CancelToken _cancelToken = new CancelToken();

    factory Http() => _instance;

    /// 通用全局单例,第一次使用时初始化 Http._internal() { _dio.options = BaseOptions( connectTimeout: HttpConfig.CONNECT_TIMEOUT, receiveTimeout: HttpConfig.RECEIVE_TIMEOUT, ); _dio.interceptors.add(DioLogInterceptor());

    // 在调试模式下需要抓包调试,所以我们使用代理,并禁用HTTPS证书校验
    if (HttpConfig.PROXY_ENABLE) {
      (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
        client.findProxy = (uri) {
          return "PROXY ${HttpConfig.PROXY_IP}:${HttpConfig.PROXY_PORT}";
        };
        //代理工具会提供一个抓包的自签名证书,会通不过证书校验,所以我们禁用证书校验
        client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
      };
    }
    

    }

    ///初始化公共属性 /// [baseUrl] 地址前缀 /// [connectTimeout] 连接超时赶时间 /// [receiveTimeout] 接收超时赶时间 /// [interceptors] 基础拦截器 void init({ String baseUrl = "", int? connectTimeout, int? receiveTimeout, List? interceptors, }) { _dio.options = _dio.options.copyWith( baseUrl: baseUrl, connectTimeout: connectTimeout, receiveTimeout: receiveTimeout, ); // 添加拦截器 if (interceptors != null && interceptors.isNotEmpty) { _dio.interceptors.addAll(interceptors); } }

    /// Get请求 Future get( String path, { Map<String, dynamic>? params, Options? options, }) async { var response = await _dio.get( path, queryParameters: params, options: options, ); return response.data; }

    /// Post请求 Future post( String path, { Map<String, dynamic>? params, Options? options, }) async { var response = await _dio.post( path, queryParameters: params, options: options, ); return response.data; }

    /// 取消请求 /// 同一个cancel token 可以用于多个请求,当一个cancel token取消时,所有使用该cancel token的请求都会被取消。 void cancelRequests({CancelToken? token}) { token ?? _cancelToken.cancel("cancelled"); } }

  • 封装对外的网络请求工具类http_utils.dart

    import 'package:dio/dio.dart'; import 'package:xpx/utils/loading_utils.dart'; import 'package:xpx/network/utils/http_sington.dart';

    /// Http工具类 class HttpUtils { HttpUtils._internal();

    /// 初始化 static void init({ String baseUrl = "", int? connectTimeout, int? receiveTimeout, List? interceptors, }) { Http().init( baseUrl: baseUrl, connectTimeout: connectTimeout, receiveTimeout: receiveTimeout, interceptors: interceptors, ); }

    /// Get请求 static Future get( String path, { Map<String, dynamic>? params, Options? options, bool isShowLoading = true, }) async { LoadingUtils.show(isShowLoading); var response = await Http().get( path, params: params, options: options, ); LoadingUtils.dismiss(); return response; }

    /// Post请求 static Future post( String path, { Map<String, dynamic>? params, Options? options, bool isShowLoading = true, }) async { LoadingUtils.show(isShowLoading); var response = await Http().post( path, params: params, options: options, ); LoadingUtils.dismiss(); return response; } }

Q:使用浏览器进行网络请求会出现跨域问题

A: 如果使用的是Chrome浏览器,去下载 ‘Allow-Control-Allow-Origin: *’ ,开启后刷新界面即可。下载网址:chrome.google.com/webstore/de… 估计需要翻墙才能下载。

7. 常用工具类的封装(待完善)

  1. loading加载框,在flutter_easyloading基础上进行简单的二次封装loading_utils.dart

    import 'package:flutter_easyloading/flutter_easyloading.dart';

    /// Loading管理 class LoadingUtils { /// 显示Loading static show(bool isShowLoading) { if (isShowLoading) { EasyLoading.show(); } }

    /// 隐藏Loading static dismiss() { if (EasyLoading.isShow) { EasyLoading.dismiss(); } } }

  2. 日志打印管理log_utils.dart

    import 'package:xpx/utils/extension/mapList_to_string.dart'; import 'package:logger/logger.dart';

    /// 日志管理 class LogHelper { /// 日志 static Logger _logger = Logger( printer: PrettyPrinter(), );

    LogHelper._internal();

    static e(String tag, dynamic message) { _logger.e(message, tag); }

    static v(String tag, dynamic message) { _logger.v(message, tag); }

    static d(String tag, dynamic message) { _logger.d(message, tag); }

    static map(String tag, Map<dynamic, dynamic> map) { _logger.e(map.mapToJsonString(), tag); } }

  3. map&&list数据转字符串mapList_to_list.dart

    /// Map拓展,MAp转字符串输出 extension Map2StringEx on Map { String mapToJsonString({int indentation = 2}) { String result = ""; String indentationStr = " " * indentation; if (true) { result += "{"; this.forEach((key, value) { if (value is Map) { var temp = value.mapToJsonString(indentation: indentation + 2); result += "\nindentationStr" + "\"key" : temp,"; } else if (value is List) { result += "\nindentationStr" + ""key\" : {value.listToJsonString(indentation: indentation + 2)},"; } else { result += "\nindentationStr" + "\"key" : "value\","; } }); result = result.substring(0, result.length - 1); result += indentation == 2 ? "\n}" : "\n{" " * (indentation - 1)}}"; }

    return result;
    

    } }

    /// List拓展,List转字符串输出 extension List2StringEx on List { String listToJsonString({int indentation = 2}) { String result = ""; String indentationStr = " " * indentation; if (true) { result += "indentationStr["; this.forEach((value) { if (value is Map) { var temp = value.mapToJsonString(indentation: indentation + 2); result += "\nindentationStr" + ""temp\","; } else if (value is List) { result += value.listToJsonString(indentation: indentation + 2); } else { result += "\nindentationStr" + ""value\","; } }); result = result.substring(0, result.length - 1); result += "\nindentationStr]"; }

    return result;
    

    } }

  4. 布局生成工具类layout_utils.dart(待维护)

    import 'package:flutter/material.dart'; import 'package:get/get.dart';

    /// 布局生成工具类 class LayoutUtils { LayoutUtils._internal();

    static Widget getApp(String text, Widget body) { return MaterialApp( title: "Flutter Demo", home: Scaffold( appBar: AppBar( title: Text( text, ), leading: IconButton( icon: Icon( Icons.arrow_back, ), onPressed: () { Get.back(); }, ), ), body: body, ), // debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, ), ); }

    static Widget generateButton(String url, String text) { return ElevatedButton( onPressed: () { Get.toNamed(url); }, child: Text( text, ), ); } }

  5. 宏定义集合,如屏幕宽高、颜色值等macroDefinition_utils.dart

    import 'package:flutter/cupertino.dart'; import 'dart:ui' as ui show window;

    class DefinitionUtil {

    // 屏幕宽 static final screenWidth = MediaQueryData.fromWindow(ui.window).size.width; // 屏幕高 static final screenHeight = MediaQueryData.fromWindow(ui.window).size.height; // 底部导航栏高 static final bottomBarHeight = MediaQueryData.fromWindow(ui.window).padding.bottom; // 主题色 Color mainColor = Color.fromRGBO(34, 212, 174, 1);

    }

8. 登陆页面

  1. 封装基础文本组件base_text.dart

  1. 封装基础输入框组件base_textField.dart
  • 定义控件基础参数

  • 设置textField属性

  1. 登陆页面绘制
  • column布局控件

  • 页面展示

  1. 基础公共弹框封装

  • 自定义提示弹框