flutter项目初始化
-
- 0、flutter 官方插件网址
- 1、flutter项目环境配置
- 2、flutter数据本地存储(shared_preferences)
- 3、Dio 封装使用详解
- 4、EasyLoading的使用
- 5、缓存 keep-alive使用(除了Tabbar,其它的页面、组件都可以使用)
- 6、缓存 tabbar(补充6的缓存 - bottomNavigationBar)
- 7、tabbar的使用
- 8、判断组件是否被卸载掉
- 9、flutter中静态图片的配置
- 10、字体使用
- 11、GetX中的路由管理
- 12、GetX 中的数据响应式配合本地存储使用
- 13、GetX中controller、binding管理状态
- 14、GetX中 defaultDialog、snackbar、bottomSheet(使用Shape设置了圆角) 使用
- 15、GetX 国际化
- 16、系统自带的dialog框
- 17、滚动吸顶效果 CustormScrollView(Slivers) 和 NestedScrollView
- 18、监听滚动
- 19、计算自适应尺寸(以 iphone6 设计稿 屏幕的宽度 750px 为标准)推荐方式二
- 20、裁剪
- 21、ListView中的 shrinkWrap 的属性
- 22、wrap做图片九宫格布局
- 23-A、仿微信发送朋友圈,拖拽排序删除
- 23-B、image_picker 和 image_cropper 的使用
- 24、动画
- 25、TextField 、 LimitedBox(控制输入框的高度,局部滚动)
- 26、overlay 遮罩层使用
- 27 - A、获取元素在屏幕上的位置
- 27 - B、点击事件获取当前点击的位置
- 28、appBar 背景跟着页面滚动发生变化
- 29、文字溢出行数出现省略号,支持全文功能
- 30、dart 时间处理
- 31、键盘弹出底部跟着上走的元素写在 bottomNavigationBar 里面,也意味着 bottomNavigationBar 里面不一定只能写 底部导航
- 32、stream 流监听
- 33、flutter 监听页面跳转、应用进入后台
- 34、隐私协议 和 用户协议
- 35、退出 flutter APP 应用
- 36、provider 状态管理
- 37、本地通知 -- flutter_local_notifications
- 38、打包
0、flutter 官方插件网址
注意:使用 flutter 的时候一定要把数据放在父组件中(主要是指Dio数据请求),往子组件传展示
1、flutter项目环境配置
1-1、下载 flutter sdk 地址
1-2、flutter sdk 配置环境变量
1-3、在pub.dev下载插件配置镜像快速拉取
// 前面环境变量名字:后面是环境变量的值
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
PUB_HOSTED_URL: https://pub.flutter-io.cn
详细见 gitee地址
1-4、插件配置(vscode)
dart 、 flutter、Comment Translate、Awesome Flutter Snippets
1-5、 flutter doctor全部正确之后,创建好的项目还是报错 ,考虑是 gradle 下载太慢,可以 提前下载好 , 然后找到 android/gradle/wrapper/gradle-wrapper.properties 文件修改里面的 distributionUrl=file:///D:/BaiduNetdiskDownload/gradle-7.4-all.zip
android sdk 配置环境变量
环境变量配置完之后可能还会报错
上面的报错需要这样配置
继续报错
上面的报错有可能是java jdk版本8没有对应上
走提示的代码即可
2、flutter数据本地存储(shared_preferences)
1、dependencies:
shared_preferences: ^2.2.2
2、utils 包里面创建文件:
shared_preferences.dart
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
class JSpUtil {
JSpUtil._internal();
factory JSpUtil() => _instance;
static final JSpUtil _instance = JSpUtil._internal();
static late SharedPreferences _preferences;
static Future<JSpUtil> getInstance() async {
_preferences = await SharedPreferences.getInstance();
return _instance;
}
/// 通用设置持久化数据
static setLocalStorage<T>(String key, T value) {
String type = value.runtimeType.toString();
switch (type) {
case "String":
_preferences.setString(key, value as String);
break;
case "int":
_preferences.setInt(key, value as int);
break;
case "bool":
_preferences.setBool(key, value as bool);
break;
case "double":
_preferences.setDouble(key, value as double);
break;
case "List<String>":
_preferences.setStringList(key, value as List<String>);
break;
}
}
/// 获取持久化数据
static dynamic getLocalStorage<T>(String key) {
dynamic value = _preferences.get(key);
if (value.runtimeType.toString() == "String") {
if (_isJson(value)) {
return json.decode(value);
}
}
return value;
}
/// 获取持久化数据中所有存入的key
static Set<String> getKeys() {
return _preferences.getKeys();
}
/// 获取持久化数据中是否包含某个key
static bool containsKey(String key) {
return _preferences.containsKey(key);
}
/// 删除持久化数据中某个key
static Future<bool> remove(String key) async {
return await _preferences.remove(key);
}
/// 清除所有持久化数据
static Future<bool> clear() async {
return await _preferences.clear();
}
/// 重新加载所有数据,仅重载运行时
static Future<void> reload() async {
return await _preferences.reload();
}
/// 判断是否是json字符串
static _isJson(String value) {
try {
const JsonDecoder().convert(value);
return true;
} catch(e) {
return false;
}
}
}
3、入口函数加上两句话,不然报错
void main() async {
// 下面两句是持久化数据用的
WidgetsFlutterBinding.ensureInitialized();
await JSpUtil.getInstance();
runApp(const ApplicationApp());
}
4、使用
// 1、取值
JSpUtil.getLocalStorage('token');
// 2、存值
JSpUtil.setLocalStorage("token", "需要存的值");
3、Dio 封装使用详解
1、安装 Dio
dio: ^4.0.0
2、封装 Dio
// 在 utils 包里面创建 http 目录
// 然后创建四个文件
// 1、 http_request.dart 核心文件
import 'package:dio/dio.dart';
import 'package:hook_up_rant/utils/http/http_interceptor.dart';
import 'package:hook_up_rant/utils/http/http_options.dart';
// http 请求单例类
class HttpRequest {
// 工厂构造方法
factory HttpRequest() => _instance;
// 初始化一个单例实例
static final HttpRequest _instance = HttpRequest._internal();
// dio 实例
Dio? dio;
// 内部构造方法
HttpRequest._internal() {
if (dio == null) {
// BaseOptions、Options、RequestOptions 都可以配置参数,优先级别依次递增,且可以根据优先级别覆盖参数
BaseOptions baseOptions = BaseOptions(
baseUrl: HttpOptions.BASE_URL,
connectTimeout: HttpOptions.CONNECT_TIMEOUT,
receiveTimeout: HttpOptions.RECEIVE_TIMEOUT,
headers: {
},
);
// 没有实例 则创建之
dio = Dio(baseOptions);
// 添加拦截器
dio!.interceptors.add(HttpInterceptor());
// 打印日志
// dio!.interceptors.add(LogInterceptor(responseBody: true));
}
}
/// 初始化公共属性 如果需要覆盖原配置项 就调用它
///
/// [baseUrl] 地址前缀
/// [connectTimeout] 连接超时赶时间
/// [receiveTimeout] 接收超时赶时间
/// [headers] 请求头
/// [interceptors] 基础拦截器
// void init({ String? baseUrl, int? connectTimeout, int? receiveTimeout, Map<String, dynamic>? headers, List<Interceptor>? interceptors }) {
// print("许潇111 --- $baseUrl -- $connectTimeout --- $receiveTimeout --- $headers -- $interceptors");
// dio!.options.baseUrl = baseUrl ?? "";
// dio!.options.connectTimeout = connectTimeout ?? 10000;
// dio!.options.receiveTimeout = receiveTimeout ?? 10000;
// dio!.options.headers = headers;
// if (interceptors != null && interceptors.isNotEmpty) {
// dio!.interceptors.addAll(interceptors);
// }
// }
/// 设置请求头
void setHeaders(Map<String, dynamic> headers) {
dio!.options.headers.addAll(headers);
}
final CancelToken _cancelToken = CancelToken();
/*
* 取消请求
*
* 同一个cancel token 可以用于多个请求
* 当一个cancel token取消时,所有使用该cancel token的请求都会被取消。
* 所以参数可选
*/
void cancelRequests({
CancelToken? token}) {
token ?? _cancelToken.cancel("cancelled");
}
/// GET
Future get(String path, {
Map<String, dynamic>? params, Options? options, CancelToken? cancelToken, ProgressCallback? onReceiveProgress}) async {
Response response = await dio!.get(
path,
queryParameters: params,
options: options,
cancelToken: cancelToken ?? _cancelToken,
onReceiveProgress: onReceiveProgress
);
return response.data;
}
/// POST
Future post(String path, {
Map<String, dynamic>? params, Options? options, CancelToken? cancelToken, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress}) async {
Response response = await dio!.post(
path,
data: params,
options: options,
cancelToken: cancelToken ?? _cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress
);
return response.data;
}
/// 文件上传
Future postFormData(String path, {
Map<String, dynamic>? params, Options? options, CancelToken? cancelToken, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress}) async {
var option = Options(
// 上传文件设置请求头
contentType: "multipart/form-data",
);
Response response = await dio!.post(
path,
data: params,
options: option,
cancelToken: cancelToken ?? _cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress
);
return response.data;
}
}
// 2、http_options.dart
// 请求配置
class HttpOptions {
// 连接服务器超时时间,单位是毫秒 20S 响应超时
static const int CONNECT_TIMEOUT = 20000;
// 接收超时时间,单位是毫秒 20S 响应超时
static const int RECEIVE_TIMEOUT = 20000;
// 地址前缀
// static const String BASE_URL = 'http://dev.duuchin.com';
static const String BASE_URL = 'http://192.168.1.114:8888';
}
// 3、http_interceptor.dart 拦截器
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:hook_up_rant/store/index.dart';
import 'package:hook_up_rant/utils/http/http_exception.dart';
import 'package:hook_up_rant/utils/showLoading.dart';
// 自定义拦截器
class HttpInterceptor extends Interceptor {
// 请求拦截
void onRequest(RequestOptions options,RequestInterceptorHandler handler) {
super.onRequest(options, handler);
EasyLoading.show(status: "加载中...");
// 从登录的持久换缓存中取出来
String token = StoreIndex.token.value;
if(token != "null" && token != "") {
options.headers["token"] = token;
}
}
// 响应拦截
Future onResponse( Response response, ResponseInterceptorHandler handler) async {
var res = json.decode(response.toString());
// success不为 true 表示响应不成功,后台报错,就不 super 往父级传了
if(!res['success']) {
EasyLoadings.showToast(res['resultMessage']);
return;
}
EasyLoading.dismiss();
super.onResponse(response, handler);
}
// 异常拦截
Future onError( DioError err, ErrorInterceptorHandler handler) async {
// 覆盖异常为自定义的异常类
HttpException httpException = HttpException.create(err);
err.error = httpException;
EasyLoading.dismiss();
super.onError(err, handler);
}
}
// 4、http_exception.dart 异常处理文件
import 'package:dio/dio.dart';
import 'package:hook_up_rant/utils/showLoading.dart';
// 自定义 http 异常
class HttpException implements Exception {
final int code;
final String msg;
HttpException({
this.code = 500, this.msg = '未知异常,请联系管理员'});
String toString() {
return "HttpError [$code]: $msg";
}
factory HttpException.create(DioError error) {
// dio 异常
switch (error.type) {
case DioErrorType.cancel:
EasyLoadings.showError("请求取消");
return HttpException(code: -1, msg: '请求取消');
case DioErrorType.connectTimeout:
EasyLoadings.showError("连接超时");
return HttpException(code: -1, msg: '连接超时');
case DioErrorType.sendTimeout:
EasyLoadings.showError("请求超时");
return HttpException(code: -1, msg: '请求超时');
case DioErrorType.receiveTimeout:
EasyLoadings.showError("响应超时");
return HttpException(code: -1, msg: '响应超时');
case DioErrorType.response:
// 服务器异常
int statusCode = error.response!.statusCode ?? 0;
switch (statusCode) {
case 400:
EasyLoadings.showError("请求语法错误");
return HttpException(code: statusCode, msg: '请求语法错误');
case 401:
EasyLoadings.showError("没有权限");
return HttpException(code: statusCode, msg: '没有权限');
case 403:
EasyLoadings.showError("服务器拒绝执行");
return HttpException(code: statusCode, msg: '服务器拒绝执行');
case 404:
EasyLoadings.showError("无法连接服务器");
return HttpException(code: statusCode, msg: '无法连接服务器');
case 500:
EasyLoadings.showError("服务器内部错误");
return HttpException(code: statusCode, msg: '服务器内部错误');
case 502:
EasyLoadings.showError("无效的请求");
return HttpException(code: statusCode, msg: '无效的请求');
case 503:
EasyLoadings.showError("服务器挂了");
return HttpException(code: statusCode, msg: '服务器挂了');
case 505:
EasyLoadings.showError("不支持HTTP协议请求");
return HttpException(code: statusCode, msg: '不支持HTTP协议请求');
default:
EasyLoadings.showError("服务器异常");
return HttpException(
code: statusCode,
msg: error.response?.statusMessage ?? "服务器异常",
);
}
default:
EasyLoadings.showError("服务器异常");
return HttpException(code: 500, msg: error.message);
}
}
}
3、创建Api类,index.dart
import 'package:hook_up_rant/utils/http/http_request.dart';
class Api {
// 注册
static Future register(params) async {
return await HttpRequest().post("/hookUpRant/register", params: params);
}
// 登录
static Future login(params) async {
return await HttpRequest().post("/hookUpRant/login", params: params);
}
// 根据token获取用户信息
static Future userInfo() async {
return await HttpRequest().get("/hookUpRant/userInfoByUserName");
}
}
4、组件中使用
// UserInfo.fromJson(res['result']) 是通过网站 https://app.quicktype.io/ 快速生成的,将后台返回的json数据转成dart数据模型
_initData() async {
// 走上一步创建的接口
var res = await Api.userInfo({
name: '张三'});
setState(() {
userInfo = UserInfo.fromJson(res['result']);
});
}
5、文件上传(没有使用上面的统一封装)
/**
* 文件上传(单个文件)
*/
// 封装FormData
import 'package:dio/dio.dart' as form_data_name; // FormData 和其他的报名重复, 所以重新名了一个名字叫 form_data_name
form_data_name.FormData formData = form_data_name.FormData.fromMap({
// 其它的key value 值
"key": "value",
// 后端接的 key 是 file, 其中 File.path 是imagePicker插件获取到的文件路径 File 对象的 path 属性
'file': await form_data_name.MultipartFile.fromFile(File.path, filename: "image1.jpg"),
});
// 开始上传
try{
var result = await Dio().post("${
HttpOptions.BASE_URL}/hookUpRant/uploadPicture", data: formData, onSendProgress: (int progress, int total) {
print("当前进度是 $progress 总进度是 $total");
});
final resultMap = json.decode(result.toString());
if(resultMap['success']) {
UploadResult uploadResult = UploadResult.fromJson(resultMap['result']);
print(uploadResult.filepath);
}else{
EasyLoadings.showError("上传失败");
}
}catch(err) {
EasyLoadings.showError("服务器异常");
}
/**
* 多文件上传
*/
final formData = FormData.fromMap({
'files': [
MultipartFile.fromFileSync('path/to/upload1.txt', filename: 'upload1.txt'),
await MultipartFile.fromFile('path/to/upload2.txt', filename: 'upload2.txt'),
],
});
6、文件下载
//当前进度进度百分比 当前进度/总进度 从0-1
double currentProgress =0.0;
///下载文件的网络路径
String apkUrl ="https://www.baidu1.com/test.apk";
///使用dio 下载文件
/// 申请写文件权限
bool isPermiss = await checkPermissFunction(); // 获取权限放在见下面
if(isPermiss) {
///手机储存目录
String savePath = await getPhoneLocalPath();
String appName = "test.apk"; // 下载的文件名字
///创建DIO
Dio dio = new Dio();
///参数一 文件的网络储存URL
///参数二 下载的本地目录文件
///参数三 下载监听
Response response = await dio.download(
apkUrl,
"$savePath$appName",
onReceiveProgress: (received, total) {
if (total != -1) {
///当前下载的百分比例
print((received / total * 100).toStringAsFixed(0) + "%");
// CircularProgressIndicator(value: currentProgress,) 进度 0-1
currentProgress = received / total;
setState(() {
});
}
}
);
}else{
///提示用户请同意权限申请
}
文件下载需要用到手里目录的访权限
Android权限目前分为三种:正常权限、危险权限、特殊权限
正常权限 直接在AndroidManifest中配置即可获得的权限。大部分权限都归于此。 危险权限,Android 6.0之后将部分权限定义于此。 危险权限不仅需要需要在AndroidManifest中配置,还需要在使用前check是否真正拥有权限,以动态申请。
在ios中,使用xcode打开本目录
选中Xcode 工程中的 info.plist文件,右键选择Open As - Source Code,将权限配置的代码copy到里面即可,键值对中的内容可按项目需求相应修改。
ios的权限设置
// ios > Runner >Info.plist
<!-- 相册 -->
<key>NSPhotoLibraryUsageDescription</key>
<string>需要您的同意,APP才能访问相册</string>
<!-- 相机 -->
<key>NSCameraUsageDescription</key>
<string>需要您的同意,APP才能访问相机</string>
<!-- 麦克风 -->
<key>NSMicrophoneUsageDescription</key>
<string>需要您的同意,APP才能访问麦克风</string>
<!-- 位置 -->
<key>NSLocationUsageDescription</key>
<string>需要您的同意, APP才能访问位置</string>
<!-- 在使用期间访问位置 -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>App需要您的同意, APP才能在使用期间访问位置</string>
<!-- 始终访问位置 -->
<key>NSLocationAlwaysUsageDescription</key>
<string>App需要您的同意, APP才能始终访问位置</string>
<!-- 日历 -->
<key>NSCalendarsUsageDescription</key>
<string>App需要您的同意, APP才能访问日历</string>
<!-- 提醒事项 -->
<key>NSRemindersUsageDescription</key>
<string>需要您的同意, APP才能访问提醒事项</string>
<!-- 运动与健身 -->
<key>NSMotionUsageDescription</key>
<string>需要您的同意, APP才能访问运动与健身</string>
<!-- 健康更新 -->
<key>NSHealthUpdateUsageDescription</key>
<string>需要您的同意, APP才能访问健康更新 </string>
<!-- 健康分享 -->
<key>NSHealthShareUsageDescription</key>
<string>需要您的同意, APP才能访问健康分享</string>
<!-- 蓝牙 -->
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要您的同意, APP才能访问蓝牙</string>
<!-- 媒体资料库 -->
<key>NSAppleMusicUsageDescription</key>
<string>需要您的同意, APP才能访问媒体资料库</string>
安卓设置权限
//在这里使用的是 permission_handler 插件来申请权限的
//flutter sdk 3.16.9
permission_handler: ^10.2.0
// 申请权限代码如下
Future<bool> _checkPermission() async {
// 先对所在平台进行判断
if (Theme.of(context).platform == TargetPlatform.android) {
PermissionStatus permission = await Permission.storage.request();
if (permission != PermissionStatus.granted) {
// 没有获得权限
openAppSettings(); // 固定api,调起授权窗口让用户授权
} else {
return true;
}
} else {
return true;
}
return false;
}
// 获取路径,用于存储文件
Future<String> _findLocalPath() async {
// ignore: use_build_context_synchronously
final directory = Theme.of(context).platform == TargetPlatform.android ? await getExternalStorageDirectory() : await getApplicationSupportDirectory();
return directory!.path;
}
// 可以自己在上面获取的路径上在创建新的目录
Future<String> _getStoragePath() async {
// 获取存储路径
var localPath = '${
await _findLocalPath()}/Download';
final savedDir = Directory(localPath);
// 判断下载路径是否存在
bool hasExisted = await savedDir.exists();
// 不存在就新建路径
if (!hasExisted) {
savedDir.create();
}
return localPath;
}
到此,走上面第六步下载文件即可
4、EasyLoading的使用
1、 安装 flutter_easyloading: ^3.0.5
2、在 MaterialApp 配置
MaterialApp(
builder: EasyLoading.init(),
);
3、使用
// 显示指示器
EasyLoading.show(
status: '加载中', // 要显示的文字
);
// 延时2秒
Future.delayed(const Duration(seconds: 2), () {
// 关闭指示器
EasyLoading.dismiss();
});
4、其它的使用方式
// 1、showToast
EasyLoading.showToast("这是一个Toast");
// 2、showInfo
EasyLoading.showInfo("这是一个Info");
// 3、showError
EasyLoading.showError("这是一个Error");
// 4、showSuccess
EasyLoading.showSuccess("这是一个success");
5、修改样式
// 设置 EasyLoading 样式,由于 EasyLoading 是全局的, 所以在什么地方修改下面的样式都生效
void main() async {
// 设置 EasyLoading 样式
EasyLoading.instance
..indicatorType = EasyLoadingIndicatorType.circle // 加载进度的样式( 只适用于 EasyLoading.show() )
..userInteractions = true // 是否使用单例
..maskType = EasyLoadingMaskType.black // 遮罩层
..dismissOnTap = false;
runApp(const ApplicationApp());
}
详细其它的样式修改可查看 : https://pub-web.flutter-io.cn/packages/flutter_easyloading
5、缓存 keep-alive使用(除了Tabbar,其它的页面、组件都可以使用)
在utils包里面创建 keep-alive.dart 文件
import 'package:flutter/material.dart';
class