flutter开发所遇问题总结-持续中

添加链接描述## 一、Exception in thread “main” java.net.ConnectException: Connection timed out: connect

flutter开发所遇问题总结-持续中_第1张图片

解决方案:下载gradle包到本地

flutter开发所遇问题总结-持续中_第2张图片

二、加载静态资源 Flutter 图片加载报错:Unable to load asset: xxx.png 解决

解决方案:重新运行flutter run 加载静态资源

三、flutter bottom overflowed by 50 PIXELS

解决方法:在Scaffold的子widget里面包裹一层SingleChildScrollView:
flutter开发所遇问题总结-持续中_第3张图片

四、GridView时出现:Vertical viewport was given unbounded height.

解决方案:添加属性 shrinkWrap:true
flutter开发所遇问题总结-持续中_第4张图片

五、手指滑动GridView,无法滑动

解决方案:flutter开发所遇问题总结-持续中_第5张图片

六、调试页面布局

1、ctrl+shift+P :dart:Open devtools
2、flutter开发所遇问题总结-持续中_第6张图片

flutter开发所遇问题总结-持续中_第7张图片

七、常用插件

http请求:dio
loading: flutter_easyloading
dialog:adaptive_dialog
状态管理:provider
数据持久化:share_preferences

八、获取全局的context

https://www.jianshu.com/p/897222522abb

九、dio的封装

https://blog.csdn.net/jiujiaopanduola/article/details/105573123

十、表单验证

https://blog.csdn.net/qq_36407748/article/details/109186749

十一、创建虚拟机

https://blog.csdn.net/weixin_44411857/article/details/88708740

十二、android studio 开发flutter快捷键

ctrl+shift+r 替换
Shift + Cmd + O 快速查找文件

cmd+n 可快速实现构造方法,get-set等方法,还有父类需要重写的方法等

1、快速从stateless 改变成stateFull
快捷键:Option + Enter
使用Option + Enter可以执行很多其他组件替换,按Option + Enter,就可以查看对该widget进行特定的操作
2、建一个新的Stateless 组件
输入:stless
3、创建一个 Stateful组件
输入:stful
4、快速选择整个小部件 Option + Up(↑)
5、格式化代码 Ctrl+Alt+L
6、其他快捷键
(一)查找/查看
格式化代码 Cmd + Option + L
清除无效包引用 Option + Control + O
删除词 option + Delete
大小写转换 Cmd + Shift + U
快捷生成结构体 Cmd + Option + T
文件方法结构 Cmd + F12
double shift 查找
Cmd + R 替换文件
Cmd +Shift +R 全工程替换
(二)控制操作
cmd + “-”,cmd + “+” :折叠/展开代码块
Cmd + Shift + “-”,Cmd + Shift+ “+” :折叠/展开全部代码块
ctr + tab :切换文件
Option + ctr + O :清除无效包引用
(三)代码重构
Option + cmd + M : 方法重构,方法抽离

抽离为局部变量 Option + cmd + V
抽离为成员变量 Option + cmd + F
代码快速补全:Cmd + Shift + Enter
快速修复存在问题的代码,导入包,自动修正:alt + enter

十三、flutter 颜色的使用

1、直接使用

color: Colors.grey

2、使用ARGB【可设置透明度】

0x后面是ARGB,A定义FF表示透明度,RGB代表三原色

 color: Color(0xFF5e12a9)

十四、flutter 的内置图标

flutter 的内置图标

十五、Flutter组件ListTile 使用说明

Flutter组件ListTile 使用说明

十六、wrap

Wrap({
    Key key,
    this.direction = Axis.horizontal, // 主轴方向
    this.alignment = WrapAlignment.start, //主轴方向上的对齐方式
    this.spacing = 0.0, // 主轴方向上children的间隔
    this.runAlignment = WrapAlignment.start, // children如何放置在交叉轴上
    this.runSpacing = 0.0, // 交叉轴上children的间隔
    this.crossAxisAlignment = WrapCrossAlignment.start, // children之间在交叉轴方向上的对齐方式
    this.textDirection, // 水平开始的方向
    this.verticalDirection = VerticalDirection.down, // 竖直的方向
    List<Widget> children = const <Widget>[],
  }) : super(key: key, children: children);

十七、忽略提示Prefer const with constant constructors

文件头添加 // ignore_for_file: prefer_const_constructors

// ignore_for_file: prefer_const_constructors

十八 InputDecoration属性描述

  const TextField({
    Key key,
    this.controller,    //编辑框的控制器,跟文本框的交互一般都通过该属性完成,如果不创建的话默认会自动创建
    this.focusNode,  //用于管理焦点
    this.decoration = const InputDecoration(),   //输入框的装饰器,用来修改外观
    TextInputType keyboardType,   //设置输入类型,不同的输入类型键盘不一样
    this.textInputAction,   //用于控制键盘动作(一般位于右下角,默认是完成)
    this.textCapitalization = TextCapitalization.none,
    this.style,    //输入的文本样式
    this.textAlign = TextAlign.start,   //输入的文本位置
    this.textDirection,    //输入的文字排列方向,一般不会修改这个属性
    this.autofocus = false,   //是否自动获取焦点
    this.obscureText = false,   //是否隐藏输入的文字,一般用在密码输入框中
    this.autocorrect = true,   //是否自动校验
    this.maxLines = 1,   //最大行
    this.maxLength,   //能输入的最大字符个数
    this.maxLengthEnforced = true,  //配合maxLength一起使用,在达到最大长度时是否阻止输入
    this.onChanged,  //输入文本发生变化时的回调
    this.onEditingComplete,   //点击键盘完成按钮时触发的回调,该回调没有参数,(){}
    this.onSubmitted,  //同样是点击键盘完成按钮时触发的回调,该回调有参数,参数即为当前输入框中的值。(String){}
    this.inputFormatters,   //对输入文本的校验
    this.enabled,    //输入框是否可用
    this.cursorWidth = 2.0,  //光标的宽度
    this.cursorRadius,  //光标的圆角
    this.cursorColor,  //光标的颜色
    this.keyboardAppearance,
    this.scrollPadding = const EdgeInsets.all(20.0),
    this.dragStartBehavior = DragStartBehavior.down,
    this.enableInteractiveSelection,
    this.onTap,    //点击输入框时的回调(){}
    this.buildCounter,
  })
InputDecoration({
    this.icon,    //位于装饰器外部和输入框前面的图片
    this.labelText,  //用于描述输入框,例如这个输入框是用来输入用户名还是密码的,当输入框获取焦点时默认会浮动到上方,
    this.labelStyle,  // 控制labelText的样式,接收一个TextStyle类型的值
    this.helperText, //辅助文本,位于输入框下方,如果errorText不为空的话,则helperText不会显示
    this.helperStyle, //helperText的样式
    this.hintText,  //提示文本,位于输入框内部
    this.hintStyle, //hintText的样式
    this.hintMaxLines, //提示信息最大行数
    this.errorText,  //错误信息提示
    this.errorStyle, //errorText的样式
    this.errorMaxLines,   //errorText最大行数
    this.hasFloatingPlaceholder = true,  //labelText是否浮动,默认为true,修改为false则labelText在输入框获取焦点时不会浮动且不显示
    this.isDense,   //改变输入框是否为密集型,默认为false,修改为true时,图标及间距会变小
    this.contentPadding, //内间距
    this.prefixIcon,  //位于输入框内部起始位置的图标。
    this.prefix,   //预先填充的Widget,跟prefixText同时只能出现一个
    this.prefixText,  //预填充的文本,例如手机号前面预先加上区号等
    this.prefixStyle,  //prefixText的样式
    this.suffixIcon, //位于输入框后面的图片,例如一般输入框后面会有个眼睛,控制输入内容是否明文
    this.suffix,  //位于输入框尾部的控件,同样的不能和suffixText同时使用
    this.suffixText,//位于尾部的填充文字
    this.suffixStyle,  //suffixText的样式
    this.counter,//位于输入框右下方的小控件,不能和counterText同时使用
    this.counterText,//位于右下方显示的文本,常用于显示输入的字符数量
    this.counterStyle, //counterText的样式
    this.filled,  //如果为true,则输入使用fillColor指定的颜色填充
    this.fillColor,  //相当于输入框的背景颜色
    this.errorBorder,   //errorText不为空,输入框没有焦点时要显示的边框
    this.focusedBorder,  //输入框有焦点时的边框,如果errorText不为空的话,该属性无效
    this.focusedErrorBorder,  //errorText不为空时,输入框有焦点时的边框
    this.disabledBorder,  //输入框禁用时显示的边框,如果errorText不为空的话,该属性无效
    this.enabledBorder,  //输入框可用时显示的边框,如果errorText不为空的话,该属性无效
    this.border, //正常情况下的border
    this.enabled = true,  //输入框是否可用
    this.semanticCounterText,  
    this.alignLabelWithHint,
  })


十九 dart

??

左边如果为空返回右边的值,否则不处理。
A??B
如果 A 等于 null,那么 A??B 为 B
如果 A 不等于 null,那么 A??B 为 A

.?

左边如果为空返回 null,否则返回右边的值。

A?.B
如果 A 等于 null,那么 A?.B 为 null
如果 A 不等于 null,那么 A?.B 等价于 A.B

二十 flutter 使用GestureDetector点击没有反应

原因:点击部分处于空白区域,没有接收到点击事件

属性 描述
deferToChild 只有当前容器中的child被点击时才会响应点击事件。
opaque 点击整个区域都会响应点击事件,但是点击事件不可穿透向下传递,注释翻译:阻止视觉上位于其后方的目标接收事件。
translucent 同样是点击整个区域都会响应点击事件,和opaque的区别是点击事件是否可以向下传递,注释翻译:半透明目标既可以在其范围内接受事件,也可以允许视觉上位于其后方的目标接收事件。
GestureDetector(
   behavior: HitTestBehavior.opaque,
   onTap: () {
   },
   child: Text("demo"),
),

二十一、Flutter_热重载和热重启

热重载(Hot Reload)
Flutter并不会重新执行 main 函数,只会根据原来的根节点重新创建控件树。
热重启(Hot Restart)
如果热重载不起作用的时候,我们也不需要进行漫长的重新编译加载等待,只要点击热重启(Hot Restart)按钮就可以以秒级的速度 进行代码重编译以及程序重启了
两者的区别:
热重启与热重载的区别只是 因为重启丢失了当前程序的运行状态而已,对实际调试也没什么影响。

二十二、Flutter jsonEncode 和 jsonDecode

json_decode( )    ---- json 转 对象/数组

通常网路请求后的数据用此方法 转为我们需要的定义的对象
当第二个参数为true返回 array ,默认是false返回object。

json_encode( )    ---- 对象/数组 转 json
成功返回 json 编码的 string ,失败返回 false 。
 

二十三、Flutter应用如何调试

Flutter应用如何调试

二十四、Flutter devTools netWork 不显示任何信息

试一试清除浏览器安装的插件

二十五、flutter 显示base64 图片

Image.memory(const Base64Decoder().convert(_captcha.split(",")[1]),fit: BoxFit.contain) ,

二十六、md5 加密

1、安装crypto
2、

md5.convert(Utf8Encoder().convert(_passwordController.text))

二十七、flutter 安装包的两种方式

1、直接添加

flutter pub add fluro

2、添加依赖,然后再添加

dependencies:
  fluro: ^2.0.3
flutter pub get

二十八、部件显示隐藏可用组件 Offstage

二十九、flutter的生命周期

【Flutter】Flutter 页面生命周期 ( 初始化期 | createState | initState | 更新期 | build | 销毁期 | dispose)

二十九、flutter StatelessWidget 传参

flutter开发所遇问题总结-持续中_第8张图片

使用: TitleWidget(
title: ‘工单材料’,
),

三十、 flutter StatefulWidget 传参

flutter开发所遇问题总结-持续中_第9张图片
使用: MaterialsWidget(pageType:“all”),

三十一、flutter Fluro路由使用(fluro 2.0.3)

1、添加依赖 Fluro

flutter pub add fluro

2、Router 静态化

application.dart


import 'package:fluro/fluro.dart';
 
class Application {
  static late final FluroRouter router;
}

2、路由配置
route_handles.dart

//工单详情页
var workOrderDetailHandler = Handler(
    handlerFunc: (BuildContext? context, Map<String, List<Object>> params) {
    // 获取路由带参数
  print(params["id"]![0]);
  return const WorkOrderDetail();
});

//工单处理页
var handleWorkOrderIndexHandler = Handler(
    handlerFunc: (BuildContext? context, Map<String, List<Object>> params) {
  return const HandleWorkOrderIndex();
});

3、路由配置
route.dart

class Routes {

  static void configureRoutes(FluroRouter router) {
    router.define("/", handler: workOrderIndexHandler);
    router.define("/login", handler: loginHandler);
    router.define("/detail/:id", handler: workOrderDetailHandler); //路由带参数
    router.define("/handleIndex", handler: handleWorkOrderIndexHandler);
    router.define("/writeWorkOrderInfo", handler: writeWorkOrderInfoHandler);
    router.define("/allMaterials", handler: allMaterialsHandler);
    router.define("/checkedMaterials", handler: checkedMaterialsHandler);
  }
}

4、创建主页面 main.dart

void main() {
  FluroRouter  router = FluroRouter();
  Application.router = router; //一定要先写这行
  Routes.configureRoutes(router);

  runApp(const MyApp());
}

5、路由返回多级
Flutter 一次退回多个页面
作用是一直把路由栈中的路由移除出栈,直到判定条件为true。

Navigator.push(
  context,
  MaterialPageRoute(
    settings: RouteSettings(name: "Foo"),
    builder: ...,
  ),
);
----------------
Navigator.popUntil(context, ModalRoute.withName("Foo"))
   Navigator.popUntil(
                            context, ModalRoute.withName("/handleIndex/${id}")); //这个参数为页面的path

5、使用

1、带参数

 Application.router.navigateTo(context, "/login");

2、不带参数

Application.router.navigateTo(context, "/detail/123456789");

3、返回

Application.router.pop(context);

4、navigateTo方法

Future navigateTo(
	BuildContext context,
	String path,
	{bool replace = false,
	bool clearStack = false,
	bool maintainState = true,
	bool rootNavigator = false,
	TransitionType? transition,
	Duration? transitionDuration,
	RouteTransitionsBuilder? transitionBuilder,
	RouteSettings? routeSettings}
)

三十二、生成model

在线生成工具:https://www.devio.org/io/tools/json-to-dart/

1、安装依赖

flutter pub add json_serializable
flutter pub add build_runner

2、打开网站
为了便利使用 json_serializable库

3、填入json数据
flutter开发所遇问题总结-持续中_第10张图片
4、复制到项目(这时候会报错),是正常的,因为还没有生成g.dart 文件
flutter开发所遇问题总结-持续中_第11张图片

5、终端运行

flutter packages pub run build_runner build

可能报错:
在这里插入图片描述
报上面错误,则运行

flutter packages pub run build_runner build --delete-conflicting-outputs

运行成功后,此时就生成了g.dart 文件
flutter开发所遇问题总结-持续中_第12张图片

6、运用

 //获取图片验证码
  static Future<VerifyCode> getVerificationCode() async {
    var response = await DioRequest.getInstance().dio.get('/blade-auth/oauth/captcha');
    var  dataMap= jsonDecode(response.toString());
    VerifyCode  verifyCode = VerifyCode.fromJson(dataMap);
    return verifyCode;
  }

三十三、当flutter devtools network 没有显示请求数据时,试一试清楚浏览器安装的插件,我是这样得行的

三十四、flutter 网络获取的list数据转model的方法

  //获取工单列表
  static Future<List<WorkOrder>?> getWorkOrderList({required Map<String,dynamic> params,required Pagination page}) async {
    params["size"] = page.size;
    params["current"] = page.current;
    var response = await DioRequest.getInstance().dio.post('/cqctsaascloud-work-order/workorder/page',data:params);
    var  resMap= jsonDecode(response.toString());
    var dataList = resMap["data"]["records"];
    List<WorkOrder> list = List<WorkOrder>.from(
        dataList.map((item) => WorkOrder.fromJson(item)).toList());
   return list;
  }

三十五、 android -studio 自动保存格式化代码

*Android-studio=》
flutter开发所遇问题总结-持续中_第13张图片

三十六、 安装flutter 代码提示片段插件 Flutter Snippets

Android-studio=》Preferences=》
plugins=>搜索Flutter Snippets

三十七、http 接口返回的数据转化为List

  static Future< List<Map<String, dynamic>>> getDicData({required Map<String,dynamic> params}) async {
    var response = await DioRequest.getInstance().dio.get('/blade-system/dict-user/dictionary',queryParameters:params);
    var  resMap= jsonDecode(response.toString())["data"];
    List<Map<String, dynamic>> listMap =
    List<Map<String, dynamic>>.from(resMap);
    return listMap;
  }

三十八、FutureBuilder


 ContainerWidget(widget:FutureBuilder(future:buildworkOrderCustomPropertysColumn(),builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
                  print("==============");
                  print(snapshot.connectionState);
                  switch (snapshot.connectionState) {
                    case ConnectionState.none:
                      return Text('Input a URL to start');
                    case ConnectionState.waiting:
                      return Center(child: CircularProgressIndicator());
                    case ConnectionState.active:
                      return Text('');
                    case ConnectionState.done:
                      if (snapshot.hasError) {
                        return Text(
                          '${snapshot.error}',
                          style: TextStyle(color: Colors.red),
                        );
                      } else {
                        return snapshot.data;
                      }
                  }
                },) ),
  Future<Widget> buildworkOrderCustomPropertysColumn() async {
    List<Widget> list = [];
    List<WorkOrderCustomPropertys> workOrderCustomPropertysList = context
        .read<WorkOrderProvider>()
        .workOrderDetailModel
        .workOrderCustomPropertys;
    for (int i = 0; i < workOrderCustomPropertysList.length; i++) {
      WorkOrderCustomPropertys item = workOrderCustomPropertysList[i];
      if (item.module == 3) {
        dynamic dataJson = jsonDecode(item.dataJson);
        String type = dataJson["type"] == "input" ? "input" : "select";
        List<Map<String, dynamic>> listMap = [];
        String hintText = type =="select" ? "请选择" :"请输入";
        String labelKey = "dictValue";
        String valueKey="dictKey";
        if (type == "select") {
          listMap = List<Map<String, dynamic>>.from(dataJson["dicData"] ?? []);
          //如果为空,则请求后端接口
          if(listMap.isEmpty){
            String dicUrl = dataJson["dicUrl"];
            print(dicUrl);
            listMap = await WorkOrderService.getDicDataByUrl(url: dicUrl.substring(4));

          }else{
            labelKey = "label";
            valueKey="value";
          }
        }

        list.add(buildInputRow(
            label: item.name,
            hintText: hintText+item.name,
            type: type,
            list: listMap,
            labelKey:labelKey,
            valueKey:valueKey,
            required: false));
        list.add(Divider(
          height: 0,
        ));
      }
    }
    return Column(children: list);
  }

三十九、provider 状态管理的例子,复制的官网,过程写的很详细

https://pub.flutter-io.cn/packages/provider/example

// ignore_for_file: public_member_api_docs, lines_longer_than_80_chars
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

/// This is a reimplementation of the default Flutter application using provider + [ChangeNotifier].

void main() {
  runApp(
    /// Providers are above [MyApp] instead of inside it, so that tests
    /// can use [MyApp] while mocking the providers
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => Counter()),
      ],
      child: const MyApp(),
    ),
  );
}

/// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool
// ignore: prefer_mixin
class Counter with ChangeNotifier, DiagnosticableTreeMixin {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }

  /// Makes `Counter` readable inside the devtools by listing all of its properties
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(IntProperty('count', count));
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Example'),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.center,
          children: const <Widget>[
            Text('You have pushed the button this many times:'),

            /// Extracted as a separate widget for performance optimization.
            /// As a separate widget, it will rebuild independently from [MyHomePage].
            ///
            /// This is totally optional (and rarely needed).
            /// Similarly, we could also use [Consumer] or [Selector].
            Count(),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        key: const Key('increment_floatingActionButton'),

        /// Calls `context.read` instead of `context.watch` so that it does not rebuild
        /// when [Counter] changes.
        onPressed: () => context.read<Counter>().increment(),
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Text(

        /// Calls `context.watch` to make [Count] rebuild when [Counter] changes.
        '${context.watch<Counter>().count}',
        key: const Key('counterState'),
        style: Theme.of(context).textTheme.headline4);
  }
}

四十、选择图片并上传的服务器

1、安装插件image_picker

flutter pub add image_picker

2、从相册选择文件并判断大小

  XFile? image = await _picker.pickImage(source: ImageSource.gallery);
  
  Map<String,dynamic> resMap = await FunUtils.uploadImage(image:image);

  Uint8List? size = await image?.readAsBytes();
  int? byte = size?.lengthInBytes;
   if (byte! / 1024 / 1024 > 20) {
     EasyLoading.showToast("资源大小应小于20M");
     throw Error();
   }

3、转化文件数据格式
await MultipartFile.fromFile(image.path) 。这里记得要加await,它是一个异步方法,不然服务器会报错:Required request part ‘file’ is not present

class FunUtils {
  static Future<Map<String, dynamic>> uploadImage({MultipartFile? multipartFile,XFile? image}) async {
    final prefs = await SharedPreferences.getInstance();
    String? userId = prefs.getString("userId");
    FormData formData = FormData.fromMap({
      "userId": userId,
      "attachType": 4,
      "file":multipartFile ?? await MultipartFile.fromFile(image?.path ?? ""),
    });
    var resMap = await WorkOrderService.putFileAttach(formData);
    print("上传成功");
    return resMap;
  }
}

4、上传到服务器

  //上传文件
  static Future<Map<String, dynamic>> putFileAttach(FormData formData) async {
    Options options = Options(headers: {'Content-Type': "multipart/form-data"});
    var response = await DioRequest.getInstance().dio.post(
        "/blade-resource/oss/endpoint/put-file-user-attach",
        data: formData,
        options: options);
    var resMap = jsonDecode(response.toString())["data"];
    return resMap;
  }

5、限制视频录制时长
:How can I limit video capturing via ImagePicker to 1 minutes maximum duration in Flutter?如何在 Flutter 中将通过 ImagePicker 捕获的视频限制为最长 1 分钟?

录制视频时,限制时间有效

 final PickedFile videoFile = await picker.getVideo( 
      source: ImageSource.camera,   
      maxDuration: const Duration(seconds: 60), //有效
  );

但是选择视频时,显示时间无效

XFile? tempImage =await _picker.pickVideo(
source: ImageSource.gallery,
maxDuration: const Duration(
    seconds: 10),
);

需要通过另外的插件进行计算
Flutter调用摄像头录像及获取视频信息

所需插件:image_picker、video_player

import 'package:image_picker/image_picker.dart';
import 'package:video_player/video_player.dart';

late VideoPlayerController _controller;

/// 录屏
Future _testRecordTheScreen() async {
  /// 打开摄像头录制视频,并限制时长5min
  PickedFile? image = await ImagePicker().getVideo(
    source: ImageSource.camera,
    maxDuration: Duration(minutes: 5)
  );
  
  if(image != null){
    /// 视频绝对路径地址
    String path = image.path;
    File f = File(path);
    /// 文件大小,单位:B
    int fileSize = 0;
    /// 视频时长,单位:秒
    int seconds = 0;
    _controller = VideoPlayerController.file(f);
    _controller.initialize().then((value) {
      _controller.setLooping(true);
      seconds = _controller.value.duration.inSeconds;
      fileSize=f.lengthSync();
    });
    /// 视频名称
    var name = path.substring(path.lastIndexOf("/") + 1, path.length);
  }
}

四十一、Signature 用户签名上传文件

注意:一定要传filename ,不然会得到错误:Required request part ‘file’ is not present

final Uint8List? data = await _controller.toPngBytes();
 MultipartFile multipartFile = MultipartFile.fromBytes(data!,filename: "用户签名.png",contentType: MediaType('image', "png"));//记得加后缀名,否则PC端不显示,MediaType需要pub add MediaType包
FunUtils.uploadImage(multipartFile:multipartFile);

四十三、Flutter 组件之间的通讯

1、回调通讯
2、InheritedWidget 数据共享
3、Global Key通信
4、ValueNotifier通信
5、第三方插件event_bus

Flutter 组件之间的通讯

四十四、could not read Username for ‘https://gitee.com’: Device not configured

ls -a #显示当前目录下的所有文件及文件夹包括隐藏的.和…等

1.找到项目所在的文件夹

2.打开隐藏目录

3.打开.git

4.修改config文件

原因使用https方式的时候 在git remote add origin 的https url 里面没有用户名和密码

修改为如下:

https://{username}:{password}@github.com/{username}/project.git
修改前:
flutter开发所遇问题总结-持续中_第14张图片
修改后:
flutter开发所遇问题总结-持续中_第15张图片

四十五、dio 的使用

1、封装dio

import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:shared_preferences/shared_preferences.dart';

import '../config/setting.dart';

class DioRequest {
  late Dio dio;
  static DioRequest? _instance;

  //构造函数
  DioRequest() {
    String authorization =
        "${ProjectConfig.clientId}:${ProjectConfig.clientSecret}";
    String base64Authorization = base64Encode(utf8.encode(authorization));
    Map extra = {};
    var options = BaseOptions(
        baseUrl: ProjectConfig.baseUrl,
        connectTimeout: 5000,
        headers: {
          "Authorization": "Basic $base64Authorization"
        },
        extra: {
          "isShowLoading": true,
          "showLoadingText": "请求中",
          "successShowText": "操作成功",
          "failShowText": "操作失败",
          "isShowSuccessText": false
        });
    dio = Dio(options);
    dio.interceptors
        .add(InterceptorsWrapper(onRequest: (options, handler) async {
      final prefs = await SharedPreferences.getInstance();
      String? accessToken = prefs.getString("accessToken");
      if (accessToken != null) {
        options.headers[ProjectConfig.tokenHeader] = "bearer $accessToken";
      }
      extra = options.extra;
      print(extra);
      if (extra["isShowLoading"] == true) {
        EasyLoading.show(status: extra["showLoadingText"]);
      }
      // Do something before request is sent
      return handler.next(options); //continue
      // 如果你想完成请求并返回一些自定义数据,你可以resolve一个Response对象 `handler.resolve(response)`。
      // 这样请求将会被终止,上层then会被调用,then中返回的数据将是你的自定义response.
      //
      // 如果你想终止请求并触发一个错误,你可以返回一个`DioError`对象,如`handler.reject(error)`,
      // 这样请求将被中止并触发异常,上层catchError会被调用。
    }, onResponse: (response, handler) {
      print("extra");
      print(extra);
      //关闭loading
      if (extra["isShowLoading"] == true) {
        EasyLoading.dismiss();
      }
      print("response");
      print(response);
      //根据不同的格式,返回不同的数据,此时
      // Do something with response data
      return handler.next(response); // continue
      // 如果你想终止请求并触发一个错误,你可以 reject 一个`DioError`对象,如`handler.reject(error)`,
      // 这样请求将被中止并触发异常,上层catchError会被调用。
    }, onError: (DioError e, handler) {
      // Do something with response error
      if (extra["isShowLoading"] == true) {
        EasyLoading.dismiss();
      }
      Response? response = e.response;
      Map dataMap = jsonDecode(response.toString());
      if (dataMap.containsKey("error_description")) {
        EasyLoading.showError(dataMap["error_description"]);
      } else {
        EasyLoading.showError("请求错误,请稍后再试");
      }

      return handler.next(e); //continue
      // 如果你想完成请求并返回一些自定义数据,可以resolve 一个`Response`,如`handler.resolve(response)`。
      // 这样请求将会被终止,上层then会被调用,then中返回的数据将是你的自定义response.
    }));
  }
  static DioRequest getInstance() {
    return _instance ??= DioRequest();
  }
}

2、使用

 //获取图片验证码
  static Future<VerifyCode> getVerificationCode() async {
    Options options = Options(extra: {
      'isShowLoading': false,
    });
    var response = await DioRequest.getInstance()
        .dio
        .get('/blade-auth/oauth/captcha', options: options);
    var dataMap = jsonDecode(response.toString());
    VerifyCode verifyCode = VerifyCode.fromJson(dataMap);
    return verifyCode;
  }

四十六、flutter Cannot run with sound null safety, because the following dependencies don’t support null safety

“Edit Configurations” (在run配置里) → “Additional run args” 添加 --no-sound-null-safety AndroidStudio前提下
flutter开发所遇问题总结-持续中_第16张图片
flutter开发所遇问题总结-持续中_第17张图片

四十七、Flutter BottomNavigationBar切换保留当前状态

Flutter BottomNavigationBar切换会刷新当前页面解决方案

1、要用PageView 构建页面,主页面

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:work_order/views/home/pendWorkOrder.dart';

import 'historyWorkOrder.dart';

class WorkOrderIndex extends StatefulWidget {
  const WorkOrderIndex({Key? key}) : super(key: key);

  @override
  State<WorkOrderIndex> createState() => _WorkOrderIndexState();
}

class _WorkOrderIndexState extends State<WorkOrderIndex> {
  int _currentIndex = 0;
  late List<Widget> _pages;
  @override
  void initState() {
    super.initState();
    _pages = [
      const PendWorkOrder(),
      const HistoryWorkOrder(),
    ];
  }

  void onTabTapped(int index) {
    _pageController.jumpToPage(index);
  }

  @override
  void dispose() {
    super.dispose();
    _pageController.dispose();
  }

  final _pageController = PageController();

  void _pageChanged(int index) {
    setState(() {
      if (_currentIndex != index) _currentIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: onTabTapped,
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: "待处理",
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: "处理历史",
          )
        ],
      ),
      body: PageView.builder(
          controller: _pageController,
          //physics: NeverScrollableScrollPhysics(),//禁止页面左右切换
          onPageChanged: _pageChanged,
          itemCount: _pages.length,
          itemBuilder: (context, index) => _pages[index]),
    );
  }
}

2、PendWorkOrder页面

关键点:
state 混合with AutomaticKeepAliveClientMixin

@override
bool get wantKeepAlive => true;

super.build(context);

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

import '../../customWidget/service/workOrderListWidget.dart';

// ignore_for_file: prefer_const_constructors
class PendWorkOrder extends StatefulWidget {
  const PendWorkOrder({Key? key}) : super(key: key);

  @override
  State<PendWorkOrder> createState() => _PendWorkOrderState();
}


class _PendWorkOrderState extends State<PendWorkOrder>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Scaffold(
        backgroundColor: Color(0xFFF7F7FB),
        appBar: AppBar(
          centerTitle: true,
          title: const Text('待处理工单'),
        ),
        body: WorkOrderListWidget(
          status: 3,
        ));
  }
}

3、HistoryWorkOrder页面

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

import '../../customWidget/service/workOrderListWidget.dart';

// ignore_for_file: prefer_const_constructors
class HistoryWorkOrder extends StatefulWidget {
  const HistoryWorkOrder({Key? key}) : super(key: key);

  @override
  State<HistoryWorkOrder> createState() => _HistoryWorkOrderState();
}

class _HistoryWorkOrderState extends State<HistoryWorkOrder>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Scaffold(
        backgroundColor: Color(0xFFF7F7FB),
        appBar: AppBar(
          centerTitle: true,
          title: const Text('历史工单'),
        ),
        body: WorkOrderListWidget(
          status: 6,
        ));
  }
}

四十八 flutter_easyrefresh 上拉加载,下拉刷新

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:shared_preferences/shared_preferences.dart';

import '../../route/application.dart';
import '../../service/workOrderService.dart';

class HistoryWorkOrderListWidget extends StatefulWidget {
  @override
  _HistoryWorkOrderListWidgetState createState() {
    return _HistoryWorkOrderListWidgetState();
  }
}

class _HistoryWorkOrderListWidgetState
    extends State<HistoryWorkOrderListWidget> {
  late EasyRefreshController _controller;

  int current = 1;
  List<Map<String, dynamic>> list = [];
  int total = 0;

  @override
  void initState() {
    super.initState();
    _controller = EasyRefreshController();
    _getWorkOrderListFun();
  }

  Future<void> _getWorkOrderListFun(
      {Map<String, dynamic>? params, int size = 10}) async {
    final prefs = await SharedPreferences.getInstance();

    Map<String, dynamic> queryParams = {
      "descs": "create_time",
      "current": current,
      "size": size,
      "createUser": prefs.getString("userId"),
    };
    queryParams.addAll(params ?? {});

    WorkOrderService.getHistoryWorkOrderList(params: queryParams).then((data) {
      setState(() {
        total = data["total"];
        List<Map<String, dynamic>> listMap =
            List<Map<String, dynamic>>.from(data["records"]);
        print("listMap");
        print(listMap);
        if (current > 1) {
          list.addAll(listMap);
        } else {
          list = listMap;
        }
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    print("build");
    return Column(
      children: [
        Row(
          children: [
            Expanded(
              child: TextField(
                onSubmitted: (value) {
                  _getWorkOrderListFun(params: {
                    "name": value,
                    "phone": value,
                    "payNumber": value,
                    "address": value,
                    "orderNumber": value
                  });
                },
                decoration: const InputDecoration(
                  hintText: "请输入工单编号、联系电话、客户名称、客户地址、户号",
                  filled: true,
                  fillColor: Colors.white,
                  enabledBorder: OutlineInputBorder(
                    borderSide: BorderSide(
                      color: Color(0xFFD6DBEE), // 边框颜色
                    ),
                  ),
                  border: OutlineInputBorder(),
                ),
              ),
            ),
          ],
        ),
        Expanded(
          child: EasyRefresh.custom(
            enableControlFinishRefresh: false,
            enableControlFinishLoad: true,
            controller: _controller,
            header: ClassicalHeader(
                refreshText: "下拉刷新",
                refreshReadyText: "准备刷新",
                refreshingText: "正在刷新",
                refreshedText: "刷新完成",
                refreshFailedText: "刷新失败",
                noMoreText: "没有更多",
                infoText: "更新时间"),
            footer: ClassicalFooter(
                loadText: "下拉加载",
                loadReadyText: "准备加载",
                loadingText: "正在加载",
                loadedText: "加载完成",
                loadFailedText: "加载失败",
                noMoreText: "没有更多",
                infoText: "加载时间"),
            onRefresh: () async {
              print('onRefresh');
              current = 1;
              await _getWorkOrderListFun();
              print(22);
              _controller.resetLoadState();
            },
            onLoad: () async {
              print('onLoad');
              current = current + 1;
              await _getWorkOrderListFun();
              print('33');
              _controller.finishLoad(noMore: list.length >= total);
            },
            slivers: <Widget>[
              SliverList(
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    return buildWorkOrderContainer(list[index]);
                  },
                  childCount: list.length,
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }

  GestureDetector buildWorkOrderContainer(Map<String, dynamic> workOrder) 					{
   
  }
}

四十九、flutter 应用生命周期

flutter 应用生命周期

import 'package:flutter/cupertino.dart';

//启动页
class Start extends StatefulWidget {
  const Start({Key? key}) : super(key: key);

  @override
  State<Start> createState() => _StartState();
}

class _StartState extends State<Start> with WidgetsBindingObserver {
  @override
  Widget build(BuildContext context) {
    return Container();
  }

  @override
  void initState() {
    super.initState();

    /// 如果想要监听应用生命周期 , 要先绑定观察者 ,
    /// 绑定完成后 , 如果应用生命周期发生了变化 ,
    /// 就会回调 didChangeAppLifecycleState 方法 ;
    WidgetsBinding.instance.addObserver(this);
  }

  /// 当应用生命周期发生变化时 , 会回调该方法
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);

    print("当前的应用生命周期状态 : ${state}");

    if (state == AppLifecycleState.paused) {
      print("应用进入后台 paused");
    } else if (state == AppLifecycleState.resumed) {
      print("应用进入前台 resumed");
    } else if (state == AppLifecycleState.inactive) {
      // 应用进入非活动状态 , 如来了个电话 , 电话应用进入前台
      // 本应用进入该状态
      print("应用进入非活动状态 inactive");
    } else if (state == AppLifecycleState.detached) {
      // 应用程序仍然在 Flutter 引擎上运行 , 但是与宿主 View 组件分离
      print("应用进入 detached 状态 detached");
    }
  }

  /// 移出组件中注册的观察者
  @override
  void dispose() {
    super.dispose();
    WidgetsBinding.instance.removeObserver(this);
  }
}

五十、Flutter获取全局context

Flutter:让你20秒搞懂BuildContext
Flutter获取全局context


void main() {
  runApp(MyApp());
}
 
final GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
 
class MyApp extends StatelessWidget {
  MyApp() {
  }
 
  // This widget is the view.common.root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: navigatorKey,
    );
  }

使用

//请求未授权,直接跳到登录页
        BuildContext? context = FunUtils.navigatorKey.currentContext;
        Application.router.navigateTo(context!, "/login");

五十一、定时器的使用

执行一次

 var updateSecond = Duration(seconds: 60);
Timer(timeout,(){
   //一分钟后回调

执行多次


Timer? _undateTimer;
var updateSecond = Duration(seconds: 60);
    _undateTimer = Timer.periodic(updateSecond, (timer) {
      //回调
    });
 
//使用完毕记得销毁
  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    _undateTimer?.cancel();
    _undateTimer = null;

}

五十二、下拉框表单的实现

//我实现的难点:如何表单向右靠qi
isExpanded: true,
alignment: AlignmentDirectional(1.0, 0),

//如何选中的文字向右靠齐
alignment: AlignmentDirectional(1.0, 0),

DropdownButtonFormField2(
                      decoration: InputDecoration(
                          focusedBorder: const OutlineInputBorder(
                            borderSide: BorderSide(
                              color: Colors.transparent, // 边框颜色
                            ),
                          ),
                          enabledBorder: const OutlineInputBorder(
                            borderSide: BorderSide(
                              color: Colors.transparent, // 边框颜色
                            ),
                          )),
                      isExpanded: true,
                      alignment: AlignmentDirectional(1.0, 0),
                      // customButton: Padding(
                      //   padding: const EdgeInsets.symmetric(vertical: 12),
                      //   child: Text(selectedValue ?? hintText,
                      //       textAlign: TextAlign.right),
                      // ),
                      hint: Text(
                        hintText,
                        style: TextStyle(
                          fontSize: 14,
                          color: Theme.of(context).hintColor,
                        ),
                      ),
                      items: getDropdownMenuItem(list ?? [],
                          label: labelKey,
                          value: valueKey,
                          valueIsLable: valueIsLable),
                      validator: (value) {
                        setPropValue(isCustomProperty, prop, value, label);
                        return;
                      },
                      onChanged: (value) {
                        setPropValue(isCustomProperty, prop, value, label);
                      },
                      // buttonHeight: 40,
                      // buttonWidth: 500,
                      //itemHeight: 40,
                    ),

五十三、Cleartext HTTP traffic not permitted

flutter开发所遇问题总结-持续中_第18张图片

停止程序运行,然后在命令行中执行 flutter clean 即可

五十四、动态加载下拉框的选项值,报错

The argument type ‘FutureBuilder’ can’t be assigned to the parameter type ‘List?’.

最开始,我把FutureBuilder 写在了这个位置
flutter开发所遇问题总结-持续中_第19张图片
后来FutureBuilder 改在了这个位置就正常了
flutter开发所遇问题总结-持续中_第20张图片

是不是 FutureBuilder 要写在widget的位置,不能写在属性上?

五十五、Couldn’t infer type parameter ‘T’

在做DropdownButtonFormField,属性validator,onChanged 抛出来了这样的错误
原因就是没有指定DropdownMenuItem的类型,后面指定一个String类型就可以了
flutter开发所遇问题总结-持续中_第21张图片

五十六、TextFormField initialValue 修改值无效

修改使用_disposeTimeController去修改值

 _disposeTimeController.text = value;

五十七、flutter There should be exactly one item with [DropdownButton]'s value:

原因:DropdownButton中value的值和DropdownMenuItem中value的值都不相同,无法显示选中状态
解决:修改DropdownButton中value的值和DropdownMenuItem中value的值有一个相同的值

但是,有些值我需要初始值,有些值不需要初始值
则通过判断,是否需要设置初始值属性
判断是否传value属性
flutter开发所遇问题总结-持续中_第22张图片

五十八、setState 会FutureBuilder重绘

flutter FutureBuilder的使用以及防止FutureBuilder不必要重绘的两种方法

解决方案 1 :Memoize the future

我们用AsyncMemoizer.runOnce包装我们的函数,;它只运行一次该函数,并在再次调用时返回缓存的Future。

final AsyncMemoizer _memoizer = AsyncMemoizer();
_gerData() {
    return _memoizer.runOnce(() async {
      return await HttpUtil()
          .get('http://api.douban.com/v2/movie/top250', data: {'count': 15});
    });
 }

解决方法2 :在构建函数之外调用Future

问题是每次发布重建时都会调用FutureBuilder状态的didUpdateWidget。此函数检查旧的future对象是否与新的对象不同,如果是,则重新启动FutureBuilder。为了解决这个问题,我们可以在构建函数之外的某个地方调用Future。例如,在initState中,将其保存在成员变量中,并将此变量传递给FutureBuilder。

var _futureBuilderFuture;
...

@override
void initState() { 
    ///用_futureBuilderFuture来保存_gerData()的结果,以避免不必要的ui重绘
    _futureBuilderFuture = _gerData();
  }
...

FutureBuilder(
  future: _futureBuilderFuture ,
  ....

五十九、dart深拷贝

map 深拷贝

  Map clonedObject = JSON.decode(JSON.encode(object));

实体类深拷贝

WorkOrderOrderMaterials.fromJson(item.toJson());

六十、flutter 添加静态资源作为页面背景图片

注意:1、images 在根目录下
2、只创建一个images图片的文件夹就可以了,不要再images之上再创建一个assets文件夹

flutter开发所遇问题总结-持续中_第23张图片

flutter开发所遇问题总结-持续中_第24张图片

六十一、Flutter Decoration背景设定(边框、圆角、阴影、形状、渐变、背景图像等)

Flutter Decoration背景设定(边框、圆角、阴影、形状、渐变、背景图像等)

decoration: new BoxDecoration(
  border: new Border.all(color: Color(0xFFFF0000),  0.5), // 边色与边宽度
  color: Color(0xFF9E9E9E), // 背景颜色
  //borderRadius: new BorderRadius.circular((20.0)), // 圆角度
  borderRadius: new BorderRadius.vertical(top: Radius.elliptical(20, 50)), // 也可控件一边圆角大小
  boxShadow: [BoxShadow(color: Color(0x99FFFF00), offset: Offset(5.0, 5.0), blurRadius: 10.0, spreadRadius: 2.0), BoxShadow(color: Color(0x9900FF00), offset: Offset(1.0, 1.0)), BoxShadow(color: Color(0xFF0000FF))],
  shape: BoxShape.rectangle, // 默认值也是矩形
  / 环形渲染
  gradient: RadialGradient(colors: [Color(0xFFFFFF00), Color(0xFF00FF00), Color(0xFF00FFFF)],radius: 1, tileMode: TileMode.mirror)
//扫描式渐变
//        gradient: SweepGradient(colors: [Color(0xFFFFFF00), Color(0xFF00FF00), Color(0xFF00FFFF)], startAngle: 0.0, endAngle: 1*3.14)
// 线性渐变
//        gradient: LinearGradient(colors: [Color(0xFFFFFF00), Color(0xFF00FF00), Color(0xFF00FFFF)], begin: FractionalOffset(1, 0), end: FractionalOffset(0, 1)),
 image: new DecorationImage(
  image: new NetworkImage('https://avatar.csdn.net/8/9/A/3_chenlove1.jpg'), // 网络图片
  // image: new AssetImage('graphics/background.png'), 本地图片
  fit: BoxFit.fill // 填满
  //          centerSlice: new Rect.fromLTRB(270.0, 180.0, 1360.0, 730.0),// 固定大小
  ),
  
),

六十二、Flutter 常用边框

几种Button的使用(OutlinedButton、ElevatedButton)

无色背景,有边框的按钮
style属性里可以通过OutlinedButton.styleFrom来设定边框的样式

OutlinedButton.icon(
              icon: const Icon(
                Icons.add,
                color: ColorsUtil.brand100Color3376FE,
              ),
              onPressed: () {
                _selectFile(item);
              },
              label: const Text(
                '上传附件',
                style: TextStyle(
                    fontSize: 14, color: ColorsUtil.brand100Color3376FE),
              ),
              style: OutlinedButton.styleFrom(
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(8),
                ),
                side: const BorderSide(
                    width: 1, color: ColorsUtil.brand100Color3376FE),
              ),
            ),


有背景色的Button

style属性里可以通过ElevatedButton.styleFrom来设定边框的样式

ElevatedButton(
                onPressed: () {
                  
                },
                child: const Text(
                  '保存并分析',
                  style: TextStyle(fontSize: 16),
                ),
                style: ButtonStyle(
                    shape: MaterialStateProperty.all(RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(8))),
                    padding: MaterialStateProperty.all(
                        const EdgeInsets.symmetric(vertical: 12)),
                    backgroundColor: MaterialStateProperty.all(
                        ColorsUtil.brand100Color3376FE)),
              ),
            )

六十三、主题的创建和获取

Flutter主题Theme的详解(创建&使用&获取)

创建主题

hemeData({
  Brightness brightness, //深色还是浅色
  MaterialColor primarySwatch, //主题颜色样本,见下面介绍
  Color primaryColor, //主色,决定导航栏颜色
  Color accentColor, //次级色,决定大多数Widget的颜色,如进度条、开关等。
  Color cardColor, //卡片颜色
  Color dividerColor, //分割线颜色
  ButtonThemeData buttonTheme, //按钮主题
  Color cursorColor, //输入框光标颜色
  Color dialogBackgroundColor,//对话框背景颜色
  String fontFamily, //文字字体
  TextTheme textTheme,// 字体主题,包括标题、body等文字样式
  IconThemeData iconTheme, // Icon的默认样式
  TargetPlatform platform, //指定平台,应用特定平台控件风格
  ...
})


主题的获取

new Container(
  color: Theme.of(context).accentColor,
  child: new Text(
    'Text with a background color',
    style: Theme.of(context).textTheme.title,
  ),
);

六十四、flutter的部署发布

构建和发布为 Android 应用
Flutter学习-打包和发布

一、android

1、添加启动图标、应用名称和应用权限
flutter开发所遇问题总结-持续中_第25张图片

2、为APP签名

Android系统在安装APK包的时候,首先会检验APK的签名,如果发现签名不存在或则校验签名失败,则会拒绝安装,所以应用程序在发布之前一定要进行签名

2.1 创建一个秘钥库

在 macOS 或者 Linux 系统上,执行下面的代码:

keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key


在 Windows 系统上,执行下述代码:

keytool -genkey -v -keystore c:/Users/USER_NAME/key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key


2.2 在app中引用秘钥库

创建一个名为 /android/key.properties 的文件,它包含了密钥库位置的定义:

storePassword=123456 //上一步骤中的密码
keyPassword=123456 //上一步骤中的密码
keyAlias=key
storeFile=/Users/cqct/key.jks //密钥库的位置

注意:这个文件一般不要提交到代码仓库

修改.gitignore文件

# Android ignore
/android/key.properties

2.3 在gradle中配置签名

flutter开发所遇问题总结-持续中_第26张图片

在 android 代码块之前将你 properties 文件的密钥库信息添加进去:

  def keystoreProperties = new Properties()
   def keystorePropertiesFile = rootProject.file('key.properties')
   if (keystorePropertiesFile.exists()) {
       keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
   }

   android {
         ...
   }

找到 buildTypes 代码块

  buildTypes {
       release {
           // TODO: Add your own signing config for the release build.
           // Signing with the debug keys for now,
           // so `flutter run --release` works.
           signingConfig signingConfigs.debug
       }
   }

将其替换为我们的配置内容:

signingConfigs {
       release {
           keyAlias keystoreProperties['keyAlias']
           keyPassword keystoreProperties['keyPassword']
           storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
           storePassword keystoreProperties['storePassword']
       }
   }
   buildTypes {
       release {
           signingConfig signingConfigs.release
       }
   }

现在我们 app 的发布版本就会被自动签名了。

当你更改 gradle 文件后,也许需要运行一下 flutter clean。这将防止缓存的版本影响签名过程。

3、 打包引用程序

APK文件

flutter build apk

AAB文件
Google推出的一种新的上传格式,某些应用市场不支持的
会根据用户打包的aab文件,动态生成用户设备需要的APK文件

flutter build appbundle

六十五、两次连续返回,则退出应用

import 'package:flutter/material.dart';

class WillPopScopeTestRoute extends StatefulWidget {
  @override
  WillPopScopeTestRouteState createState() {
    return WillPopScopeTestRouteState();
  }
}

class WillPopScopeTestRouteState extends State<WillPopScopeTestRoute> {
  DateTime? _lastPressedAt; //上次点击时间

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        if (_lastPressedAt == null ||
            DateTime.now().difference(_lastPressedAt!) > Duration(seconds: 1)) {
          //两次点击间隔超过1秒则重新计时
          _lastPressedAt = DateTime.now();
          return false;
        }
        return true;
      },
      child: Container(
        alignment: Alignment.center,
        child: Text("1秒内连续按两次返回键退出"),
      ),
    );
  }
}

六十五:flutter 沉浸式状态栏(电池栏和appbar的颜色一致)

沉浸式状态栏

return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        appBarTheme: const AppBarTheme(
            systemOverlayStyle:
                SystemUiOverlayStyle(statusBarColor: Colors.transparent)),
        primaryColor: ColorConfig.primaryColor,
        primarySwatch: ColorConfig.primarySwatchColor,
      ),

六十六:自定义 primarySwatch

import 'dart:ui';

import 'package:flutter/material.dart';

class ColorConfig {
  // 主题颜色
  // primaryColor的值是一个Color类型的,为所有的Widget 提供基础颜色;
  // primarySwatch的值是一个MaterialColor类型,而不是Color类型的,主要为Material 系列组件提供基础色。
  static const int _primaryColorValue = 0xFF447AD2;
  static const Color primaryColor = Color(_primaryColorValue);
  static const MaterialColor primarySwatchColor = MaterialColor(
    _primaryColorValue,
    <int, Color>{
      50: Color(0xFF447AD2),
      100: Color(0xFF447AD2),
      200: Color(0xFF447AD2),
      300: Color(0xFF447AD2),
      400: Color(0xFF447AD2),
      500: primaryColor,
      600: Color(0xFF447AD2),
      700: Color(0xFF447AD2),
      800: Color(0xFF447AD2),
      900: Color(0xFF447AD2),
    },
  );
}

六十七:bottomNavigationBar图标白色的解决办法

Flutter中,如果底部的Item超过三个,我们就需要为BottomNavigationBar设置一个type属性为:BottomNavigationBarType.fixed,否则图标就会变成白色,导致什么都看不
flutter开发所遇问题总结-持续中_第27张图片

六十七、Cannot run with sound null safety because dependencies don’t support null safety

Cannot run with sound null safety because dependencies don’t support null safety

运行

flutter run --no-sound-null-safety

六十八、RenderBox was not laid out: RenderRepaintBoundary#e8ffa relayoutBoundary=up9 NEEDS-PAINT

这样的报错一般是尺寸的问题

flutter开发所遇问题总结-持续中_第28张图片

解决
flutter开发所遇问题总结-持续中_第29张图片

六十九、Flutter编译卡在Running Gradle task ‘assembleDebug‘

Flutter编译卡在Running Gradle task ‘assembleDebug‘

七十、‘initialValue == null ||控制器 == null’:不正确。错误(Flutter - ‘initialValue == null || controller == null’: is not true. error)

不能同时使用initialValue 和controller

七十一、取值赋值不对

flutter开发所遇问题总结-持续中_第30张图片
赋值跟取值不能一样,,,不能写成discounttList = discountList

七十一、如何去除 Flutter 右上角的 debug 标识

如何去除 Flutter 右上角的 debug 标识

return MaterialApp(
  debugShowCheckedModeBanner: false,
);

七十二、Flutter 调用相机/相册,多图选择,图片视频文件压缩上传处理

还未实践

Flutter 调用相机/相册,多图选择,图片视频文件压缩上传处理

七十三、debug模式下,如何调整网速进行测试?

未测试,是经过询问网友得出的答案,觉得应该可行

1、开个本地wifi限速手机
2、抓包工具上有限速功能

七十三、flutter报错 Unhandled Exception: Looking up a deactivated widget‘s ancestor is unsafe.

解决:
上下文丢失了 把【context 改为 this.context 正确指代】

this
.context
.read<WorkOrderProvider>()
.setUploadLoading(false);

七十四、flutter getX的使用

1、getX的路由用法
Flutter 全能型选手GetX —— 路由管理

Flutter Getx 视频

Flutter Getx 实例文档

Getx - 如何使用路由管理页面

// 返回到指定页面
Get.until()
// GestureDetector(
//   child: Text("测试返回消除栈测试"),
 //   onTap: () {
 //     Get.until(
 //         (route) => (route as GetPageRoute).routeName == '/');
 //   },
 // ),
// 返回到指定页面,并创建指定页面
Get.offNamedUntil()

FunUtils.delayed(() => Get.offNamedUntil("/building/buildingListPage",
       ModalRoute.withName('/owner/addOwnerPage')));
// 替换当前页面,但没有返回上一个页面的选项(用于闪屏页,登录页面等)
Get.off()和Get.offNamed()
// 跳转到指定新页面,没有创建新的
// 注意:preventDuplicates参数true禁止页面重复
// false页面可以重复,但是要是用的Controller需要在StatelessWidget的build方法里面获取
Get.toNamed()
//带参数
//Get.toNamed("/second?name=river")
//print(Get.parameters['name']);

 //Get.toNamed("/workorder/WorkorderListPage", arguments: gridView)
 //menu = Get.arguments;

// 创建新页面,并关闭其他页面
Get.offAllNamed()和Get.offAllNamed()
// 关闭当前页面(等价Navigator.pop(context) )
Get.back()
//监听到页面带参数返回
onPressed: () async {
   var data = await Get.to(MinePage());
   // 上个页面返回后,立即拿到数据,456
   print(data);
 },
 // 返回携带参数
    Get.back(result: 456);
    
Get.to();

2、getX的依赖管理和状态管理

final name = ''.obs;
final isLogged = false.obs;
final count = 0.obs;
final balance = 0.0.obs;
final number = 0.obs;
final items = <String>[].obs;
final myMap = <String, int>{}.obs;

// 自定义类 - 可以是任何类
final user = User().obs;

AddOwnerPage.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_navigation/get_navigation.dart';
import 'package:iotdmcp_app/pages/owner/page/owner/controller/DeviceController.dart';

class AddOwnerPage extends StatelessWidget {
  AddOwnerPage({Key? key}) : super(key: key);
  final count = 0.obs;
  final map = <String, int>{"count": 0}.obs;
  final list = <String>[].obs;

  final deviceController = DeviceController();
  // final deviceController = Get.put(DeviceController());

  @override
  Widget build(BuildContext context) {
    print(map);
    return Scaffold(
      appBar: AppBar(
        title: const Text("添加业主"),
        centerTitle: true,
      ),
      body: Center(
        child: Column(
          children: [
            Obx(() => Text("状态管理obx测试-int" + count.toString())),
            ElevatedButton(
              onPressed: () {
                count.value++;
              },
              child: Text('Obx-add'),
            ),
            Divider(),
            Obx(() => Text("状态管理obx测试-map" + map['count'].toString())),
            ElevatedButton(
              onPressed: () {
                map['count'] = (map['count']! + 1);
              },
              child: Text('Obx-add-map'),
            ),
            Divider(),
            Obx(() => Text("状态管理obx测试-list" + list.length.toString())),
            ElevatedButton(
              onPressed: () {
                list.add('1');
              },
              child: Text('Obx-add-list'),
            ),
            Divider(),
            // GetX(
            //   init: deviceController,
            //   initState: (_) {},
            //   builder: (_) {
            //     return Text('状态管理obx测试getX -> ${_.count}');
            //   },
            // ),
            Obx(() =>
                Text("状态管理obx测试-int" + deviceController.count.toString())),
            ElevatedButton(
              onPressed: () {
                deviceController.add();
              },
              child: Text('状态管理obx测试getX++'),
            ),
            Divider(),
            GetX<DeviceController>(
              init: deviceController,
              initState: (_) {},
              builder: (_) {
                return Text('状态管理obx测试getX Map-> ${_.map.toString()}');
              },
            ),
            ElevatedButton(
              onPressed: () {
                deviceController.mapAdd();
              },
              child: Text('状态管理obx测试getX++Map'),
            ),
            Divider(),
            GetX<DeviceController>(
              init: deviceController,
              initState: (_) {},
              builder: (_) {
                return Text('状态管理obx测试getX List-> ${_.list.length.toString()}');
              },
            ),
            ElevatedButton(
              onPressed: () {
                deviceController.listAdd();
              },
              child: Text('状态管理obx测试getX++ List'),
            ),
            Divider(),
            ElevatedButton(
                onPressed: () {
                  Get.toNamed("/building/buildingListPage");
                },
                child: Text("选择建筑物,去建筑物列表")),
            ElevatedButton(
                onPressed: () {
                  Get.toNamed("/device/deviceListPage");
                },
                child: Text("选择设备,去选择设备")),
            ElevatedButton(
                onPressed: () {
                  Get.offAndToNamed("/owner/ownerListPage");
                },
                child: Text("确定添加,跳转业主列表")),
          ],
        ),
      ),
    );
  }
}

DeviceListPage

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:iotdmcp_app/pages/owner/page/owner/controller/DeviceController.dart';

class DeviceListPage extends StatelessWidget {
  DeviceListPage({Key? key}) : super(key: key);
  //final deviceController = new DeviceController();
  final deviceController = Get.find<DeviceController>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("设备列表"),
        centerTitle: true,
      ),
      body: Center(
        child: Column(
          children: [
            GetX<DeviceController>(
              init: deviceController,
              initState: (_) {},
              builder: (_) {
                return Text('状态管理obx测试getX -> ${_.count}');
              },
            ),
            ElevatedButton(
              onPressed: () {
                deviceController.add();
              },
              child: Text('状态管理obx测试getX++'),
            ),
            Divider(),
            ElevatedButton(
                onPressed: () {
                  Get.toNamed("/device/addDevicePage");
                },
                child: Text("去添加设备")),
            ElevatedButton(
                onPressed: () {
                  Get.back();
                },
                child: Text("已选择,,返回到列表")),
          ],
        ),
      ),
    );
  }
}

DeviceController

import 'package:get/get.dart';
import 'package:get/get_state_manager/get_state_manager.dart';

class DeviceController extends GetxController {
  final count = 0.obs;
  final map = <String, int>{"count": 0}.obs;
  final list = <String>[].obs;
  add() {
    count.value++;
    print(count.value);
  }

  @override
  void onInit() {
    super.onInit();
    print("onInit");
  }

  @override
  void onClose() {
    super.onClose();
    print("onClose");
  }

  mapAdd() {
    map["count"] = map['count']! + 1;
  }

  listAdd() => list.add('2');
}

3、 //刚开始显示加载中。。
change(null,status: RxStatus.loading());
change(null,status: RxStatus.error(‘Error’));
change(article,status: RxStatus.success());
4、
flutter开发所遇问题总结-持续中_第31张图片
解决方案
[Flutter]从Obx一个报错初探其原理

修改为

Obx(() => FormWidget(
                columns: columns,
                form: ownerController.ownerForm.value,
                changeCallback: (formValue) {
                  // ownerController.setOwnerForm(formValue);
                  //setState(() => {});
                })),

Flutter List’ is not a subtype of type 'List>

var dataMap =
        jsonDecode(response.toString())["data"];

解决方案:

var dataMap =
        jsonDecode(response.toString())["data"].cast<Map<String, dynamic>>();

报错原因:
无法直接用子类型去声明父类变量,或者无法直接用子集类型去声明父集类型;代码中 value 反编码后为的变量 list 的类型为 List ,而 Map 是 dynamic 的一种情况,即 dynamic 包含 Map

七十五、Flutter_downloader 在使用过程中出现的问题记录

1、callback is a top-level or static function
解决办法:回调要用静态方法

@override
  void initState() {
    FlutterDownloader.registerCallback(downloadCallback);
    super.initState();
  }

  //@pragma('vm:entry-point')
  static void downloadCallback(
    String id,
    DownloadTaskStatus status,
    int progress,
  ) {
    print(
      'Callback on background isolate: '
      'task ($id) is in status ($status) and process ($progress)',
    );
  }

2、java.io.IOException: Cleartext HTTP traffic to baoleiji.cq-ct.com not permitted
解决办法

flutter开发所遇问题总结-持续中_第32张图片
参考文档:

3、java.lang.NullPointerException: httpConn.contentType must not be null

现在版本是1.9.0 我把版本降到1.8.3就可以了

利用flutter_downloader插件在Flutter中实现文件下载

4、Unhandled Exception: ‘package:flutter_downloader/src/downloader.dart’: Failed assertion: line 304 pos 12: ‘_initialized’: plugin flutter_downloader is not initialized

在main.js 中已经初始化,但是提示还是未初始化
解决方案:下载是在后台下载,显示是在前端页面显示,所以显示是需要交互的。没有仔细看文档
UI is rendered on the main isolate, while download events come from the background isolate (in other words, code in callback is run in the background isolate), so you have to handle the communication between two isolates. For example:

ReceivePort _port = ReceivePort();

@override
void initState() {
  super.initState();

  IsolateNameServer.registerPortWithName(_port.sendPort, 'downloader_send_port');
  _port.listen((dynamic data) {
    String id = data[0];
    DownloadTaskStatus status = data[1];
    int progress = data[2];
    setState((){ });
  });

  FlutterDownloader.registerCallback(downloadCallback);
}

@override
void dispose() {
  IsolateNameServer.removePortNameMapping('downloader_send_port');
  super.dispose();
}

@pragma('vm:entry-point')
static void downloadCallback(String id, DownloadTaskStatus status, int progress) {
  final SendPort send = IsolateNameServer.lookupPortByName('downloader_send_port');
  send.send([id, status, progress]);
}

七十七、flutter App requires Multidex support

android {
        defaultConfig {
            multiDexEnabled true
        }
    }

七十八、map报错

form[“变量名”][“变量名”] 赋值 ,但是form[“变量名”][“变量名”] 取值的时候会报错

七十九、参数不匹配

原因是我回调时写了两个参数,参数接收时只写了一个导致的参数不匹配错误
flutter开发所遇问题总结-持续中_第33张图片
回调写了两个参数
在这里插入图片描述
最开始这里我只接收了一个参数
flutter开发所遇问题总结-持续中_第34张图片

八十、 flutter 真机调试卡在 Installing build\app\outputs\flutter-apk\app.apk…

应该是真机以前安装过此程序,且已经卸载,但是卸载有残留
adb 卸载 提示你不存在的包名

adb uninstall com.example.xxx

八十一、切换flutter的版本号

https://flutter.cn/docs/development/tools/sdk/releases?tab=macos

八十二、Manifest merger failed : uses-sdk:minSdkVersion 16 cannot be smaller than version 19 declared in libflutter开发所遇问题总结-持续中_第35张图片

找到 flutter/android/flutter/packages/flutter_tools/gradle/flutter.gradle 修改minSdkVersion为19就好了

八十三 Android异常篇 Manifest merger failed : Attribute application@label value=() from AndroidM

一般是项目中使用的第三方依赖库中的AndroidManifest.xml中跟当前App的AndroidManifest.xml中有重复的某些属性时AS会提示这个,按照提示添加就可以解决

flutter开发所遇问题总结-持续中_第36张图片

八十四、Multidex issue with Flutter

Open [project_folder]/app/build.gradle and add following lines.

defaultConfig {
    ...

    multiDexEnabled true
}

八十五、files found with path ‘lib/arm64-v8a/libc++_shared.so’ from inputs:

flutter开发所遇问题总结-持续中_第37张图片

你可能感兴趣的:(flutter)