学习Flutter,准备基于所学搭建一个脚手架,不断的去更新维护这个项目。
项目准备
首先创建一个Flutter项目,因为主要还是面向移动端开发,所以先聚焦于Android(没有IOS设备) 修改gradle为本地的,免得去网上下半天 build文件下的下载源也换一下
// 阿里云云效仓库: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' }
然后运行起来,确定项目是能跑的 确定一下Android项目使用的sdk,以及flutter和dart的版本
项目配置
修改项目的应用名称配置和图标配置 Android: 配置项目的配置文件
基础工具类封装
吐司工具
使用框架:
fluttertoast: ^8.0.8
定义好吐司显示位置,和背景颜色,使用者只需要传入显示的信息即可
class ToastUtils {
static void showToast(String msg) {
//如果已经显示,则取消
Fluttertoast.cancel();
//显示
Fluttertoast.showToast(
msg: msg,
backgroundColor: Colors.black54,
gravity: ToastGravity.CENTER);
}
}
日志工具
控制是否为debug模式,打印日志,以及打印的格式
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 — — — — — — — — — — — — — — — —');
}
}
路由框架
路由的关闭和打开
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
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
String spUserIsFirstKey ="sp_user_isfirst";
///是否同意隐私协议
String spUserProtocolKey ="sp_user_protocol";
///主题保存KEY
String spUserThemeKey="sp_user_theme";
///用户信息保存KEY
String spUserBeanKey ="sp_user_bean";
用户信息配置
一般我们要保存用户的个性化设置在本地,所以也会通过一个类来统一管理
- 构造单例对象
- 获取用户登录信息
- 获取用户是否统一隐私与用户协议
- 加载用户缓存信息
- 退出登录,清楚缓存信息
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地址
/// 网络请求所有接口的URL地址
class HttpHelper{
static const String BASE_HOST="";
//获取用户的基本信息
static const String USER_INFO="";
//用户登录
static const String USER_LOGIN="";
}
LoadingStatus封装网络请求的状态
/// 网络请求的状态
enum LoadingStatus{
success,//加载成功有数据
noData,//加载成功没数据
fail,//加载失败
none,//默认无状态
loading,//加载中
}
LogInterceptor是日志拦截器,监控到网络的请求以及响应等过程状态
/// 日志拦截器
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关键字
/// 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文件下可以配置闪屏页面的背景颜色和需要加载的图片
启动初始化页面
我们需要在默认的main.dart中配置启动的根视图以及flutter项目运行app报错捕捉UI显示的功能 主要完成了两件事情:
- 配置根页面
- 自定义报错页面
///
/// 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,),
],
),
),
);
};
}
在根页面中需要
- 配置主题色,语言环境
- 默认显示界面IndexPage
/// 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决策进入首页面还是引导页面
- 初始化sp
- 初始化日志工具
- 获取用户是否第一次登录
- 获取用户的隐私协议
- 初始化用户的登录信息
- 判断用户隐私协议
- 根据用户是否第一次登录决定进入欢迎界面还是引导页面
引导页面
用户第一次登录会进入引导页面
欢迎界面
倒计时的功能,播放广告