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主页面设置
-
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(), ); } }
-
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; } }
-
体验热重载,页面展示
6. 网络请求封装
网络请求主要是用官方推荐的dio库进行二次封装,并拆分了几个模块
-
域名、接口地址、代理设置、超时时间等配置
将域名、代理设置、超时时间放在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"; }
-
错误码、客户端异常、服务端异常解析
-
客户端请求错误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 "_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, "未知错误"); } } }
-
日志拦截、错误处理拦截器
-
日志拦截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.method}\n"; requestStr += "request header:\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.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.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()); } }
-
网络请求封装,初始化全局单例
-
网络请求初始化,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. 常用工具类的封装(待完善)
-
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(); } } }
-
日志打印管理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); } }
-
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;} }
-
布局生成工具类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, ), ); } }
-
宏定义集合,如屏幕宽高、颜色值等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. 登陆页面
-
封装基础文本组件base_text.dart
-
封装基础输入框组件base_textField.dart
- 定义控件基础参数
- 设置textField属性
-
登陆页面绘制
- column布局控件
- 页面展示
-
基础公共弹框封装
- 自定义提示弹框