Flutter实战笔记

路由和导航

路由可以在函数入口链接中声明一个MAP:
image.png
通过Navigator跳转:
image.png
或者直接使用PUSH方法:
image.png
获取跳转后页面的返回值,相当于Android中的startActivityForresult方法:
image.png
跳转到其他APP,可以使用 url_launcher插件;
在Flutter中,Navigator类提供了多种API用于页面导航和管理。以下是一些常用的Navigator API用法:

  1. push:将新页面推入导航栈中,并显示在当前页面之上。
    Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => NewPage()),
    );

  2. pushNamed:通过路由名称将新页面推入导航栈中,并显示在当前页面之上。
    Navigator.pushNamed(context, ‘/newPage’);

  3. pushReplacement:将新页面推入导航栈中,并替换当前页面。
    Navigator.pushReplacement(
    context,
    MaterialPageRoute(builder: (context) => NewPage()),
    );

  4. pushNamedAndRemoveUntil:将新页面推入导航栈中,并移除指定条件之前的所有页面。
    Navigator.pushNamedAndRemoveUntil(
    context,
    ‘/newPage’,
    (route) => false, // 移除所有页面历史记录
    );

  5. pop:从导航栈中移除当前页面,并返回到上一个页面。
    Navigator.pop(context);

  6. popUntil:从导航栈中移除当前页面及指定条件之前的所有页面,并返回到指定条件的页面。
    Navigator.popUntil(context, ModalRoute.withName(‘/home’));

手势检测和触摸事件

1.如何添加点击事件

如果控件本身支持事件监测,直接传递一个函数
image.png
如果控件不支持则在外面包裹一个GestureDetector,重写onTab.
image.png

监听其他的手势操作

主要是GestureDetector中的一些方法使用
image.png
image.png

如何给APP设置主题

image.png

表单的输入和富文本

image.png
显示提示文字
image.png
错误信息可以通过errorText;
富文本如何显示:
image.png

如何调用硬件和第三方服务

正常都是通过集成对应的插件,下面是一些常用的:
image.png
image.png

图片展示

image.png

image.png
加载网络图片
image.png
静态图片:
image.png

本地图片
image.png
加载相对路径图片
image.png
placeholder设置:
image.png
image.png
配置图片缓存
image.png
ICON
image.png

动画

动画的分类:
image.png
image.png
image.png

网络请求

网络请求的库有很多,这里记录常规的使用流程,使用HTTP插件
image.png
image.png
image.png
将response转换成对象,有在线的生成工具
image.png
image.png

JSON解析与复杂模型转化

序列化
image.png
image.png
image.png
image.png

在线工具
image.png

设置网络代理

(dio?.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate =
          (client) {
        client.findProxy = (uri) {
          return 'PROXY 192.168.124.34:8888';
        };
        client.badCertificateCallback =
            (X509Certificate cert, String host, int port) => true;
      };

本地存储SP A

https://pub.dev/packages/shared_preferences/example

异步

关于Future代表依次异步操作,他具有状态执行中、执行完成,也有一些操作符,需要注意的是下面的情况

Future<String> tesy1() async {
  return "sssss";
}

Future<String> tesy2() async {
  return Future.value("sssss");
}

这两种是等价的,在Dart中,当你在一个函数声明中使用async关键字时,这个函数会自动返回一个Future。你可以在这个函数中使用return语句来返回一个值,这个值会被自动包装在Future中。
image.png
timeout可以设置超时。
FutureBuilder

Flutter插件和Flutter包

在Flutter中,插件和包都是一种代码复用的方式,但它们的用途和功能有所不同。
Flutter包(Package):包是Dart代码的集合,可以被其他应用程序复用。包可以包含函数、类、变量等,以及一些静态资源(如图片、样式表等)。包可以帮助你组织和共享代码,你可以将一些通用的功能封装到包中,然后在多个项目中使用这个包。
Flutter插件(Plugin):插件是一种特殊的包,它包含了一些可以调用平台特定的API的代码。插件通常包含Dart代码和对应平台(如Android或iOS)的原生代码。插件可以让你在Flutter应用中使用一些平台特定的功能,如访问设备的相机、传感器等。
总的来说,如果你想要复用和共享Dart代码,你可以创建一个包。如果你想要在Flutter应用中使用平台特定的API,你可以创建一个插件

通过Android studio可视化面板创建。
发布,第一步检查包是否正常可发布:
image.png
发布命令
image.png
也存在依赖冲突,但解决的方式貌似比较简单,只需要在项目里声明版本,优先级最高。

与原生通信

MethodChannel的使用
在DART中使用

import 'package:flutter/services.dart';

void main() async {
  const channel = MethodChannel('samples.flutter.dev/battery');

  try {
    final int batteryLevel = await channel.invokeMethod('getBatteryLevel');
    print('Battery level: $batteryLevel%');
  } on PlatformException catch (e) {
    print('Failed to get battery level: ${e.message}');
  }
}

在Android端的实现

import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;

public class BatteryPlugin implements FlutterPlugin, MethodCallHandler {
  private MethodChannel channel;

  @Override
  public void onAttachedToEngine(FlutterPluginBinding binding) {
    channel = new MethodChannel(binding.getBinaryMessenger(), "samples.flutter.dev/battery");
    channel.setMethodCallHandler(this);
  }

  @Override
  public void onMethodCall(MethodCall call, Result result) {
    if (call.method.equals("getBatteryLevel")) {
      int batteryLevel = getBatteryLevel();

      if (batteryLevel != -1) {
        result.success(batteryLevel);
      } else {
        result.error("UNAVAILABLE", "Battery level not available.", null);
      }
    } else {
      result.notImplemented();
    }
  }

  private int getBatteryLevel() {
    // 实现获取电池电量的代码
  }

  // ...
}

注册插件

import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;

public class MainActivity extends FlutterActivity {
  @Override
  public void configureFlutterEngine(FlutterEngine flutterEngine) {
    flutterEngine.getPlugins().add(new BatteryPlugin());
  }
}
import 'package:flutter_blue/flutter_blue.dart';
import 'dart:async';

class BleManager {
   static BleManager get instance => _getInstance();
   static BleManager? _instance;
   static const  TAG  = "BleManager test: ";

   static const  serverUUid = "0000FFF0-0000-1000-8000-00805F9B34FB";
   static const  writeUuid = "0000FFF2-0000-1000-8000-00805F9B34FB";
   static const  noticeUuid = "0000FFF1-0000-1000-8000-00805F9B34FB";

   FlutterBlue? _flutterBlue;

   BluetoothDevice? _currentBleDevice;

   BleManager._internal() {
      _flutterBlue = FlutterBlue.instance;
   }

   static BleManager _getInstance(){
      if(_instance == null){
         _instance = BleManager._internal();
      }
      return _instance!;
   }

    startScan(void Function(List<ScanResult>) callback){
       List<ScanResult> scanFilters = [];
       _flutterBlue?.startScan(timeout: Duration(seconds: 10));
       _flutterBlue?.scanResults.listen((scanResult){
         print("scanResults : ${scanResult.length}");
         callback(scanResult);
       },onDone:(){
         print("startScan onDone");
       });
    }


    stopScan(){
      _flutterBlue?.stopScan();
    }
    
    connetDevice(BluetoothDevice device,void Function() callback){
      print("start connect" );
      device.connect(timeout: Duration(seconds: 20),autoConnect: true)
        .then((value) {
           print("connect success " );
           callback();
           _currentBleDevice = device;
          return registerNotityValue();
        })
        .then((BluetoothCharacteristic characteristic){
           print(TAG + "模拟开始写入数据");
           
        })
        .onError((error, stackTrace) {
           print(TAG +  "connect error " + error.toString()); 

        });
    }

  

  Future<BluetoothCharacteristic>  registerNotityValue() async {
        List<BluetoothService> services = await _currentBleDevice!.discoverServices(); 
        //此处完成涉及异步嵌套有必要控制回调时机  相当于rxjava的发射器了.....
        Completer<BluetoothCharacteristic> completer = Completer<BluetoothCharacteristic>();
        services.forEach((service) {
          print(service.uuid);
          if(service.uuid.toString() == serverUUid.toLowerCase()){
              print("service.uuid: ${service.uuid.toString()}");
               service.characteristics.forEach((element) {
                if(element.uuid.toString() == noticeUuid.toLowerCase()){
                  print("element.uuid: ${element.uuid.toString()}");
                  element.setNotifyValue(true).then((value){
                  print("setNotifyValue success~");
                  element.value.listen((data){
                    print("reveiveValue: ${data}");
               });
               completer.complete(element);
          }).onError((error, stackTrace){
             completer.completeError(error!, stackTrace);
          });
          }
        });
    }
   });
    return completer.future;
}

  disconnect(BluetoothDevice device,void Function() callback){
      device.disconnect().then((value){
           print("disconnect success " );
           callback();
      }).onError((error, stackTrace){
        print("connect error " + error.toString()); 
      });
    }


    Future<bool> bleIsOpen() async{
      var  result = await _flutterBlue!.isAvailable;
      return result;
    }
}

简述通信原理

阅读:Flutter原理篇:硬核-从Platform到Dart通信原理分析之Android篇

  • BasicMessageChannel:用于传递字符串和半结构化的信息。
  • MethodChannel:用于传递方法调用(method invocation)。
  • EventChannel: 用于数据流(event streams)的通信。

简单的总结:通信的双方做的工作就是通知、接收、和处理。
处理数据,编码首先通过JsonCodec.encode进行编码,然后再通过StringCodec 编码成ByteData;解码部分首先通过StringCodec解码成字符串再通过JsonCodec.decode解码Json字符串。
通知,比如dart发送通知消息给android端,大概是这样的

以项目中使用的MethodChannel为例,追寻一下源码的调用情况:
dart端调用

  var result = await channel.invokeMethod("compressData",{'data': sourceData});

image.png
方法详情,

Future<T?> _invokeMethod<T>(String method, { required bool missingOk, dynamic arguments }) async {
    //使用MethodCodec将参数解析成二进制数据
    final ByteData input = codec.encodeMethodCall(MethodCall(method, arguments));
    final ByteData? result =
      !kReleaseMode && debugProfilePlatformChannels ?
        await (binaryMessenger as _ProfiledBinaryMessenger).sendWithPostfix(name, '#$method', input) :
        //发送
        await binaryMessenger.send(name, input);
    if (result == null) {
      if (missingOk) {
        return null;
      }
      throw MissingPluginException('No implementation found for method $method on channel $name');
    }
    return codec.decodeEnvelope(result) as T?;
  }

BinaryMessenger是一个抽象类,看下实现类BackgroundIsolateBinaryMessenger的send方法:

 Future<ByteData?>? send(String channel, ByteData? message) {
    final Completer<ByteData?> completer = Completer<ByteData?>();
    _messageCount += 1;
    final int messageIdentifier = _messageCount;
    _completers[messageIdentifier] = completer;
    ui.PlatformDispatcher.instance.sendPortPlatformMessage(
      channel,
      message,
      messageIdentifier,
      _receivePort.sendPort,
    );
    return completer.future;
  }

调用sendPortPlatformMessage方法。

void sendPortPlatformMessage(
    String name,
    ByteData? data,
    int identifier,
    SendPort port) {
    final String? error =
        _sendPortPlatformMessage(name, identifier, port.nativePort, data);
    if (error != null) {
      throw Exception(error);
    }
  }

  String? _sendPortPlatformMessage(String name, int identifier, int port, ByteData? data) =>
      __sendPortPlatformMessage(name, identifier, port, data);

  @Native<Handle Function(Handle, Handle, Handle, Handle)>(symbol: 'PlatformConfigurationNativeApi::SendPortPlatformMessage')
  external static String? __sendPortPlatformMessage(String name, int identifier, int port, ByteData? data);

这里调用了native原生的方法,(使用了Dart的FFI(Foreign Function Interface)功能来调用一个原生(C/C++等)API。具体来说,这是在Flutter或Dart项目中调用平台(如iOS或Android)原生代码的一种方式),

数据共享和通知

为了满足业务开发的需求,这是一个绕不开的问题,在原生的android中可以通过很多方式解决,通常是使用共有生命周期的VM,而在flutter中也应该有相似的解决方案。
可以使用eventbus,这种方式可以共享同步数据,也可以作为同步信号使用,作为通知他的场景是可以的,但是作为共享状态和数据的手段缺点明显,为了防止内存问题需要显式的注册和反注册,在项目中我也大量的使用了这种方法去实现数据共享,最麻烦的还是每个业务界面都需要去执行重复的注册反注册操作,相同一个界面内可能会有很多个eventbus的事件,事件难以管理,看上去比较呆板。
官方推荐的Provider是一种更好的方式,使用起来也比较简单,在全局的任何地方都可以使用。实战中对其介绍比较详细。
所以应该避免通过eventbus的方式去实现全局的共享数据。
使用provider也可以实现MVVM的基础业务架构,但是否有必要还是看业务而定。

滑动控件嵌套问题

场景:这里使用listview嵌套了gridview,

 child: ListView(
           children: [
                getTopbarView(),
                getBanner(),
                getCenterTab(),
                getBottomListView(),
             ],
             ),

于是一直报错:
image.png
都有滚动,就是滑动冲突了,还一个就是高度的问题,当设定gridview高度时就可以正常显示了,
所以在gridview中添加如下代码:

shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),


- shrinkWrap: true:这个属性告诉ListView组件根据其内容的大小来调整自身的大小。当shrinkWrap设置为true时,ListView会根据其子组件的大小来确定自身的高度,而不是占据整个可用空间。这样,ListView就不会尝试扩展到剩余空间的大小,从而避免了报错。

  • physics: NeverScrollableScrollPhysics():这个属性指定了ListView的滚动行为。NeverScrollableScrollPhysics表示禁用滚动功能,即ListView不会响应滚动手势。通过禁用滚动,我们可以确保ListView的高度只取决于其内容的大小,而不会尝试扩展到剩余空间的大小。

单选按钮

 RadioListTile(value: 0, groupValue: "map",title: Text(""), onChanged:(value){

                  }), 

父亲控件和子控件如何通信

  1. 回调函数:父控件可以通过回调函数将一个函数传递给子控件,在子控件中调用该函数来传递数据或用户选择的结果。例如:
class ParentWidget extends StatelessWidget {
  void _handleResult(String result) {
    // 处理子控件传递的结果
    print('用户选择的结果:$result');
  }

  
  Widget build(BuildContext context) {
    return ChildWidget(onResult: _handleResult);
  }
}

class ChildWidget extends StatelessWidget {
  final Function(String) onResult;

  ChildWidget({required this.onResult});

  void _handleButtonPressed() {
    String result = '用户选择的结果';
    // 调用回调函数将结果传递给父控件
    onResult(result);
  }

  
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: _handleButtonPressed,
      child: Text('按钮'),
    );
  }
}
  1. 构造函数参数:父控件可以通过构造函数参数将数据传递给子控件,并在子控件中使用这些数据。例如:
class ParentWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    String data = '父控件传递的数据';
    return ChildWidget(data: data);
  }
}

class ChildWidget extends StatelessWidget {
  final String data;

  ChildWidget({required this.data});

  
  Widget build(BuildContext context) {
    return Text(data);
  }
}
  1. 全局状态管理:使用状态管理库(如Provider、GetX、Riverpod等),父控件和子控件可以共享同一个状态,并在其中读取或修改数据。这样,父控件和子控件之间可以实时共享数据。例如使用Provider库:
final dataProvider = Provider<String>((ref) => '共享的数据');

class ParentWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, watch, child) {
        String data = watch(dataProvider);
        return ChildWidget(data: data);
      },
    );
  }
}

class ChildWidget extends StatelessWidget {
  final String data;

  ChildWidget({required this.data});

  
  Widget build(BuildContext context) {
    return Text(data);
  }
}

弹框:如何写一个自定义内容的弹框?弹框高度填充了屏幕如何处理?

1.高度问题包裹Wrap即可

showDialog(
      context: context, 
      builder: (BuildContext context){
        return Dialog(
          child: AddNewDeviceDialog(),
        );
      });
import 'package:flutter/material.dart';

class AddNewDeviceDialog extends StatefulWidget {
  const AddNewDeviceDialog({super.key});

  
  State<AddNewDeviceDialog> createState() => _AddNewDeviceDialogState();
}

class _AddNewDeviceDialogState extends State<AddNewDeviceDialog> {
 
  TextEditingController _nameController = TextEditingController();

  
  Widget build(BuildContext context) {
    return Wrap(
      children: [
        Container(
      padding: EdgeInsets.only(top: 20, left: 12, right: 12,bottom: 20),
      child: Wrap(
        children: [
          Column(
        children: [
          Text("自定义设备名称",
              style: TextStyle(fontSize: 20, color: Color(0xff1b294b))),
          Container(
            height: 48,
            margin: EdgeInsets.only(right: 4,top: 14),
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(5),
              color: Colors.grey[200],
            ),
            child: TextField(
              maxLines: 1,
              controller: _nameController,
              decoration: InputDecoration(
                  border: InputBorder.none,
                  hintText: "请输入设备名称(建议包含数字序号)",
                  hintStyle: TextStyle(
                    fontSize: 14,
                    color: Color(0xFF9FA5B2),
                  ),
                  contentPadding: EdgeInsets.only(left: 20)),
            ),
          ),
          Container(
            height: 48,
            margin: EdgeInsets.only(right: 4,top: 14),
             decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(5),
              color: Colors.grey[200],
            ),
            child: Row(
               children: [
                 Container(
                  margin: EdgeInsets.only(left: 12),
                  child: Text("默认分组",style: TextStyle(fontSize: 14, color: Color(0xff1e2145))),
                 )  
               ],
            ),
          ) 
        ],
      ),
        ],
      )
    ),
      ], 
      );
  }
}

2.宽度发现太宽了
如果我们嫌Loading框太宽,想自定义对话框宽度,这时只使用SizedBox或ConstrainedBox是不行的,原因是showDialog中已经给对话框设置了最小宽度约束,根据我们在第五章“尺寸限制类容器”一节中所述,我们可以使用UnconstrainedBox先抵消showDialog对宽度的约束,然后再使用SizedBox指定宽度,代码如下:

UnconstrainedBox(
  constrainedAxis: Axis.vertical,
  child: SizedBox(
    width: 280,
    child: AlertDialog(
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          CircularProgressIndicator(value: .8,),
          Padding(
            padding: const EdgeInsets.only(top: 26.0),
            child: Text("正在加载,请稍后..."),
          )
        ],
      ),
    ),
  ),
);

3.如何写一个自定义内容样式的底部弹框

showBottomGroupDialog() {
    showModalBottomSheet(
        context: context,
        builder: (BuildContext context) {
          return SelectBottomGroupDialog(
            selectResultCallBack: (group) {
              setState(() {
                selectGroup = group;
              });
            },
          );
        });
  }

showModalBottomSheet的弹框高度默认为屏幕高度的一半,可以通过内容内容去设定。

GestureDetector点击空白处不响应点击事件

getItemGroupView(Group group, int index) {
    return GestureDetector(
      onTap: (){
        setState(() {
            selcetIndex = index;
        });
      },
      child: Container(
        height: 40,
        margin: EdgeInsets.only(top: 5, bottom: 5, left: 40),
        child: Row(
          children: [
            Text(
              group.name ?? "",
              style: TextStyle(color: Color(0xff1b294b), fontSize: 15,fontWeight: FontWeight.bold),
            ),
            Expanded(
              child: Align(
                alignment: Alignment.centerRight,
                child: RoundCheckBox(
                    size: 20,
                    checkedWidget: const Icon(
                      Icons.check,
                      color: Colors.white,
                      size: 15,
                    ),
                    checkedColor: Color(0xFF3C78FF),
                    uncheckedColor: Color(0x003C78FF),
                    border: Border.all(color: Color(0xFFD1D1D1), width: 1),
                    isChecked: selcetIndex == index,
                    onTap: (selected) {
                      setState(() {
                        selcetIndex = index;
                      });
                    }),
              ),
            )
          ],
        )),
    );
  }

上述的布局中点击空白没反应,通过添加下面的属性可以

behavior: HitTestBehavior.opaque,

配置响应的区域
当behavior选择deferToChild时,只有当前容器中的child被点击时才会响应点击事件;
当behavior选择opaque时,点击整个区域都会响应点击事件,但是点击事件不可穿透向下传递,注释翻译:阻止视觉上位于其后方的目标接收事件,所以我需要的这种效果直接将behavior设置为HitTestBehavior.opaque就可以了;
当behavior选择translucent时,同样是点击整个区域都会响应点击事件,和opaque的区别是点击事件是否可以向下传递,注释翻译:半透明目标既可以在其范围内接受事件,也可以允许视觉上位于其后方的目标接收事件。

旋转

场景是这样的,在一个可展开和收起的列表头部中有一个状态图标,展开时我希望旋转90,收起时我希望旋转0度
Android中的实现貌似很简单:

if (entity.isExpand) {
    ivState.rotation = 90f
} else {
    ivState.rotation = 0f
}

�Flutter中的实现:

Transform.rotate(
            angle: isExpanded ? 90 * math.pi / 180 : 0,
            child: Image.asset('images/group_head_flag.png'),
),

BuildContext使用错误导致弹框不消失

场景:

SlidableAction(
            onPressed: (context) {
              DialogUtils.showSureDialog(context,
              '确定删除此部门吗?',
               '确定删除${dep.deptName}吗?删除后不可恢复,请谨慎操作',
               '取消',
               "确定",(){
                 _deleteDep(dep.deptId?.toString() ?? '');
               });
            },

showSureDialog方法的实现:

Dialogs.materialDialog(
        msg: content,
        title: title,
        color: Colors.white,
        context: context,
        actions: [
          IconsOutlineButton(
            onPressed: () {
              Navigator.of(context).pop();
            },
            text: cancel,
            // iconData: Icons.cancel_outlined,
            textStyle: TextStyle(color: Colors.grey),
            iconColor: Colors.grey,
          ),

点击取消弹框会消失,但这里发现并未消失,原因就是showSureDialog传入的context并不是当前页面的context而是组件中回调的context,如果想要正常显示要修改回调context的名称,避免和当前页面的context名称相同即可。

StreamBuild

Flutter中的key

大概是个什么?是不是Android中的tag?

在Flutter中,Key是一个用于标识Widget的对象。它在某些情况下非常有用,例如当你需要改变Widget的集合(如ListView,GridView等)中的Widget的顺序或数量时。

Key的主要作用是帮助Flutter框架区分在Widget树重建过程中的Widget,这样框架就可以正确地同步UI和状态。如果没有Key,那么Flutter框架会根据Widget的类型和位置来匹配旧的Widget和新的Widget。

例如,当你有一个Widget列表,并且你想要删除其中的一个Widget,如果你没有给每个Widget一个唯一的Key,那么Flutter可能会删除错误的Widget。因为在没有Key的情况下,Flutter只能通过Widget的位置来确定哪个Widget被删除了。

所以,你可以把Key看作是一个标签,它可以帮助你和Flutter框架识别Widget。但是,它并不完全等同于Android中的tag。在Android中,tag可以用来存储一些数据,而在Flutter中,Key主要用于标识Widget。

总的来说,如果你不需要改变Widget的顺序或数量,或者你不需要保持Widget的状态,那么你可能不需要使用Key。但是,如果你在做这些操作,那么使用Key可能会很有帮助。

常用的格式代码:

跳转页面
Navigator.push(context,
                MaterialPageRoute(builder: (context) => SelectDevicePage()));

圆角背景
decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(5),
        color: Colors.white,
 )

多种文字颜色格式
RichText(
              text: TextSpan(children: [
            TextSpan(
              text: "已选择",
              style: TextStyle(color: Color(0xff1e2145), fontSize: 14),
            ),
            TextSpan(
              text: " ${selectDeviceCount} ",
              style: TextStyle(
                  color: Color(0xFF526EFD),
                  fontSize: 14,
                  fontWeight: FontWeight.bold),
            ),
            TextSpan(
              text: "个设备",
              style: TextStyle(color: Color(0xff1e2145), fontSize: 14),
            ),
          ])),

集合的一些操作

添加依赖
collection: ^1.17.2

groupby

将集合按照条件转换成map对象

var group = groupBy(deviceList.device!, (p0) => p0.groupName);

every:判断是否存在不符合某种条件的子项
 _checkGroupIsAllSelect(int sectionIndex) {
    bool isAllGroupSelect = _listDeviceSection[sectionIndex]
        .items
        .every((element) => element.isSelect ?? false);
    _listDeviceSection[sectionIndex].isAllSelect = isAllGroupSelect;
  }

where:过滤符合条件的子项返回一个新集合
actionSure() {
    List selectDevices = []; 
    _listDeviceSection.forEach((element) {
       selectDevices.addAll(element.items.where((element) => element.isSelect==true));
    });
    Navigator.pop(context,selectDevices); 
  }

any :根据某一个条件判断对象在另一个集合中是否存在

对象的引用不同,但是主键相同的情况,想判断另一个集合中是否存在了

staffs.forEach((item) {
        item.isSelect = BleManager.instance.listTempSelectAPeople.any((element) => element.staffId == item.staffId);
       });

removeWhere:按某一条件从集合中移除符合条件的对象
 BleManager.instance.listTempSelectAPeople.removeWhere((obj) => obj.staffId == staff.staffId);

cast 集合类型转换

在确保集合中子项类型兼容的前提下可以调用cast,下面的场景就是接受跳转目标页面返回的值:
如不显示的转换会报错,Unhandled Exception: type ‘List’ is not a subtype of type ‘List’ in type cast

goSelectDevicePage() async{
     final result = await Navigator.push(context, MaterialPageRoute(builder: (context) => SelectDevicePage()));
     if(result != null && (result as List).isNotEmpty){
         selectDevice.clear();
         selectDevice.addAll(result.cast<Device>());
         setState(() {});
     }
  }

IOS打包和调试

数据库的使用

使用插件:sqflite: ^2.3.0
数据库使用的关键:创建使用和升级,创建使用包括打开数据库、创建数据表、建立数据模型、编写增删改查的语句等,flutter中可以使用组件完成,相对于android来说差不多,下面是使用sqflite组件实现存储设备信息的场景:
创建一个数据库单列类:

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DatabaseHelper {
  static final _databaseName = "MyDatabase.db";
  static final _databaseVersion = 1;

  static final table = 'deviceInfo';
  static final deviceMac = 'mac';
  static final deviceName = 'name';

  DatabaseHelper._privateConstructor();
  static final DatabaseHelper instance = DatabaseHelper._privateConstructor();

  // 数据库引用
  static Database? _database;
  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }

  // 打开数据库
  _initDatabase() async {
    String path = join(await getDatabasesPath(), _databaseName);
    return await openDatabase(path,
        version: _databaseVersion, onCreate: _onCreate);
  }

  // 创建数据表
  Future _onCreate(Database db, int version) async {
    await db.execute('CREATE TABLE $tableName ($deviceId INTEGER PRIMARY KEY AUTOINCREMENT,$deviceMac TEXT NOT NULL, $deviceName TEXT NOT NULL)');
  }

  // 插入数据
  Future<int> insert(Map<String, dynamic> row) async {
    Database db = await instance.database;
    return await db.insert(table, row);
  }

  // 查询所有数据
  Future<List<Map<String, dynamic>>> queryAllRows() async {
    Database db = await instance.database;
    return await db.query(table);
  }

  // 更新数据
  Future<int> update(Map<String, dynamic> row) async {
    Database db = await instance.database;
    int id = row[deviceMac];
    return await db.update(table, row, where: '$deviceMac = ?', whereArgs: [id]);
  }

  // 删除数据
  Future<int> delete(int id) async {
    Database db = await instance.database;
    return await db.delete(table, where: '$deviceMac = ?', whereArgs: [id]);
  }
}

关于升级:

class DatabaseHelper {
  // ...

  Future<Database> get database async {
    if (_database != null) return _database!;

    _database = await _initDatabase();
    return _database!;
  }

  Future<Database> _initDatabase() async {
    String path = join(await getDatabasesPath(), _databaseName);
    return await openDatabase(path, version: 2, onCreate: _onCreate, onUpgrade: _onUpgrade);
  }

  Future _onCreate(Database db, int version) async {
    await db.execute('''
      CREATE TABLE $_tableName (
        $_columnId INTEGER PRIMARY KEY AUTOINCREMENT,
        $_columnDeviceId TEXT NOT NULL,
        $_columnDeviceName TEXT NOT NULL
      )
      ''');
  }

  Future _onUpgrade(Database db, int oldVersion, int newVersion) async {
    if (newVersion > oldVersion) {
      // Add new fields to your table
      await db.execute("ALTER TABLE $_tableName ADD COLUMN new_field TEXT");
    }
  }

  // ...
}

vscode运行安装不了apk

执行flutter run命令,发现一直卡死在Installing
image.png

使用adb卸载安装包

APK卸载不干净,解决的方法就是用adb卸载一下
adb devices 查看当前连接的手机设备列表
adb shell 进入手机的shell
pm list packages 显示所有应用包名
找到包名,执行卸载命令
image.png
success成功以后再次执行flutter run 就可以了

CocoaPods not installed or not in valid state

踩坑CocoaPods重装
xcode突然报错,进入项目运行pod install,pod update也不行。
–突然出现的问题,先尝试重启IDE,flutter clean - flutter pub get – flutter run ,应该会好。
– 这里我直接pod update 发现无效,各种网上的操作发现都没用。
最后:选择重新安装cocoapods,进入项目目录执行pod install
—又报错
image.png
这个是开VPN的原因。

软件盘适配

在输入edtxt的时候会出现一些问题
1.超出布局范围:
image.png
2.软件盘弹起的时候遮挡了控件
image.pngimage.png
解决方式也很常规,网上也可以查到通用的方法,第一是设置 Scaffold 的 resizeToAvoidBottomInset ,然后是嵌套滚动布局。
上面问题二的页面比较复杂,涉及了一些分离的子控件使用上述的两种方式并没有办法解决,如下是实现该页面的布局代码:

return Scaffold(
      backgroundColor: Color(0xFFF9F9F9),
      body: Column(
        children: [
          NavigationTopBar(
            child: CommonTopBarView(
              title: "设备详情",
            ),
          ),
          Expanded(
            child: SingleChildScrollView(
                padding: EdgeInsets.only(top: 0),
                // shrinkWrap: true,
                child: Container(
                  child: Column(
                  children: [
                    getTopInfoView(),
                    getGroupTopView(),
                    ModelPanelView(
                      deviceInfoModel: _deviceInfoModel,
                      changeMapWayNotice: (value) {
                        isAbSame = value;
                        BleManager.instance.isAbSame = value;
                      },
                    ),
                  ],
                )
                ),
                ),
          ),
          getMapButtonView()
        ],
      ),
    );

也是参考网上的解决思路,监听软件盘弹起,然后动态填充一个键盘高度,让外面的滑动控件滑动相应的距离,修改布局

return Scaffold(
      resizeToAvoidBottomInset: false,
      backgroundColor: Color(0xFFF9F9F9),
      body: Column(
        children: [
          NavigationTopBar(
            child: CommonTopBarView(
              title: "设备详情",
            ),
          ),
          Expanded(
            child: SingleChildScrollView(
                controller: _scrollController, 
                padding: EdgeInsets.only(top: 0),
                // shrinkWrap: true,
                child: Container(
                  child: Column(
                  children: [
                    getTopInfoView(),
                    getGroupTopView(),
                    ModelPanelView(
                      key: _targetWidgetKey,
                      deviceInfoModel: _deviceInfoModel,
                      changeMapWayNotice: (value) {
                        isAbSame = value;
                        BleManager.instance.isAbSame = value;
                      },
                    ),
                    SizedBox(height: _keyboardHeight)
                  ],
                )
                ),
                ),
          ),
          getMapButtonView()
        ],
      ),
    );

添加一个SizedBox和一个controller控制滑动
如何监听软件盘弹起?
通过实现WidgetsBindingObserver接口,这个接口用于观察应用程序生命周期和窗口指标的变化,通过重写方法didChangeMetrics,关于didChangeMetrics:
会在以下情况下被调用:
1. 当应用程序的窗口大小发生变化时,比如屏幕旋转或键盘弹出/隐藏时。
2. 当设备的像素密度(density)发生变化时,比如在高DPI屏幕上。
主要的适配逻辑也在这个回调方法中实现:


  void didChangeMetrics() {
   super.didChangeMetrics();
   var pageHeight = MediaQuery.of(context).size.height;
    if (pageHeight <= 0) {
      return;
    }
    //窗口顶部 - 底部 = 键盘距离顶部
    final keyboardTopPixels = View.of(context).physicalSize.height - View.of(context).viewInsets.bottom;
    final keyboardTopPoints = keyboardTopPixels / View.of(context).devicePixelRatio;
    //键盘高度
    final keyboardHeight = pageHeight - keyboardTopPoints;
    setState(() {
      _keyboardHeight = keyboardHeight;
    });
    if (keyboardHeight <= 0) {
      return;
    }

    RenderBox? renderBox =
        _targetWidgetKey.currentContext?.findRenderObject() as RenderBox?;
    if (renderBox == null) {
      return;
    }
    // 转换为全局坐标
    final bottomOffset =
        renderBox.localToGlobal(Offset(0, renderBox.size.height));
    final targetDy = bottomOffset.dy;
    // 获取要滚动的距离
    // 即被软键盘挡住的那段距离 加上 _scrollController.offset 已经滑动过的距离
    final offsetY =
        keyboardHeight - (pageHeight - targetDy) + _scrollController.offset;
    // 滑动到指定位置
    if (offsetY > 0) {
      _scrollController.animateTo(
        offsetY,
        duration: kTabScrollDuration,
        curve: Curves.ease,
      );
    }
  }

剽窃了网上现成的代码,但确实解决了问题,而且效果很丝滑堪比原生。
image.png
此处的SizeBox和设置padding: EdgeInsets.only(top: 0,bottom: _keyboardHeight)是一样的效果,这种思路也可以解决弹框中软件盘的适配问题。

卡顿问题:为什么使用了async依然会让UI卡顿呢?

Flutter的渲染原理

Flutter 的 UI Task Runner 负责执行 Dart 代码,而 Flutter 的渲染管线也是在 UI Task Runner 中运行的。每次 Flutter App 的界面需要更新时,Framework 会通过 ui.window.scheduleFrame 通知 Engine。然后 Engine 会注册一个 Vsync 信号的回调,在下一个 VSync 信号到来之际,Engine 会通过 ui.window.onBeginFrame 和 ui.window.onDrawFrame 回调给 Framework 来驱动 Flutter 渲染管线,渲染管线中的 Build、Layout、Paint 一一被执行,生成了最新的 Layer Tree。最后 Layer Tree 通过 ui.window.render 发送到了 Engine 端,交给 GPU Task Runner 做光栅化与上屏。
这段原理简述和android原生的渲染机制有很大相同。

卡顿是如何产生的
  • Flutter 的目标是提供 60 帧每秒 (fps) 的性能,或者是在可以达到 120 Hz 的设备上提供 120 fps 的性能。
  • 对于 60 fps 来说,需要在约每 16 ms 的时候渲染一帧。
  • 当 UI 渲染不流畅的时候,卡顿就随之产生了。举例来说,如果一帧花了 10 倍的时间来渲染,这帧就会被丢弃,动画看起来就会卡。

android原生中的16ms机制,而大多数的卡顿也同理是由于阻塞导致的。阻塞的原因有很多,在android中长时间的阻塞可以导致ANR的触发,而大部分场景都是执行了耗时操作。

如何设计采集方案

定义一个卡顿阈值,在 ui.window.onBeginFrame 开始计时,在 ui.window.onDrawFrame 做好卡口,如果渲染管线的执行时间过长,大于卡顿阈值,那么我们就可以判断发生了卡顿。
定义的卡顿阈值为 100ms,然后每隔 5ms 采集一次卡顿堆栈,假设 ui.window.onBeginFrame 开始到 ui.window.onDrawFrame 结束总共耗时 200ms,其中 foo 方法耗时 160ms,bar 方法耗时 30ms,其余方法耗时 10ms。那么在这段时间,我们一共能采集到 40 个堆栈,其中有 32 个堆栈的栈顶为 foo 方法,6 个堆栈的栈顶为 bar 方法。
这也是android一些卡顿检测工具的设计思路。

不同语言对于耗时操作的处理
  • 多线程。比如 Java、C++,就是开启一个新的线程,将耗时操作放在新的线程里面处理,再通过线程间通信的方式,将拿到的数据传给主线程处理。
  • 单线程+事件循环。比如 JavaScript、Dart 都是基于单线程加事件循环来完成耗时操作的处理。

dart是一种单线程语言,android中存在UI线程的说法,貌似在flutter中并类似但说法不同。
所有的 Dart 代码均运行在一个 isolate 的上下文环境中,该 isolate 中拥有对应 Dart 代码片段运行所需的所有内存。Dart 代码执行时不允许其它的代码片段在与之相同的 isolate 中执行。它更像是一个进程,内存不共享。
一个线程,一个循环,两个队列

  • event queue:负责处理I/O事件、绘制事件、手势事件、接收其他 isolate 消息等外部事件。
  • microtask queue:可以自己向 isolate 内部添加事件,事件的优先级比 event queue高。
while (eventQueue.waitForEvent()) {
  eventQueue.processNextEvent();
}

async的工作原理

async和await和kotlin中的_协程_挂起的机制似乎相同。async和await也是协程的一种。
协程在执行时,执行到async则表示进入一个协程,会同步执行async的代码块。async的代码块本质上也相当于一个函数,并且有自己的上下文环境。当执行到await时,则表示有任务需要等待,CPU 则去调度执行其他 IO,也就是后面的代码或其他协程代码。过一段时间 CPU 就会轮循一次,看某个协程是否任务已经处理完成,有返回结果可以被继续执行,如果可以被继续执行的话,则会沿着上次离开时指针指向的位置继续执行,也就是await标志的位置。
—什么使用了async依然会卡顿?
在一个页面中做耗时比较大的运算时,就算用了 async、await 异步处理,UI页面的动画还是会卡顿,因为还是在这个UI线程中做运算,异步只是你可以先做其他,等我这边有结果再返回,但是,我们的计算仍旧是在这个UI线程,仍会阻塞UI的刷新,异步只是在同一个线程的并发操作。所以这个时候就需要创建新的线程来执行耗时操作解决这个问题。

Flutter中如何进行性能调试

Flutter 性能分析
https://juejin.cn/post/6922660841514860557
真机才能进行调试,修改launch.json中的flutterMode,运行命令flutter run --profile
image.png
点击此处的连接,进入观望台
image.png
image.png
不同颜色代表这每一帧渲染情况,红色的话代表当前帧的渲染和绘制都很耗时。
image.png

修改版本号无效问题

image.png
修改ideneity中的配置一直无效,打包出来的版本号还是以前的
还必须修改info中的配置项,应该是flutter中的配置字段覆盖了这个设置

屏幕适配

通常就是尺寸和字体大小的适配了,使用组件: flutter_screenutil: ^5.9.0
进行初始化,配置设计稿的尺寸大小,适配的思路和原生思路一致。

 return ScreenUtilInit(
      designSize: const Size(375, 812),
      minTextAdapt: true,
      builder: (context, child){
        return MaterialApp(
          // navigatorKey: NavigatorProvider.navigatorKey,
          debugShowCheckedModeBanner: false,
          // theme: ThemeData(
          //     primaryColor:Color(0x526EFD)
          // ),
          home:IndexPage(),
          routes: {
             '/indexPage': (context) => IndexPage(),
             Routers.login :(context)=> LoginPage(),
          },
          builder: EasyLoading.init(),
      );
      },
    );

使用:

 return Container(
      height: bannerHeight.h,
      child: _banner(),
    );

Text("查看全部",style: TextStyle(color: Color(0xFF90939C), fontSize: 12.sp)),

上架和总结

经过一系列的折腾还是成功上架了第一款IOS应用,
image.png
从证书的配置到打包,再经过审核信息的填写,包括预览图、描述、常规的审核信息,由于这款APP比较轻量级,最重要的是没有继承任何第三方的SDK,不涉及太多的危险权限,个人账号也比较新比较干净,在审核上比较顺利,只被拒了一次,随后对核心的蓝牙功能进行了录屏和文档说明,再次审核就上架成功了。
这个项目比较常规,涉及的难点就是蓝牙部分和图传功能,要分别借助原生去实现,在处理数据的时候使用到了C库,对于android端来说要借助JNI,IOS的相对容易,可以直接调用C库的方法,这部分花费了很多的时间,但并不是很难。
总的来说上架流程并不繁琐,和国内应用当前的上架流程比相对容易太多,项目需要优化的地方也有很多,包括功能和代码。
优化:路由配置,环境配置,业务架构,全局通信等等。
优化点可参考:
Flutter如何创建一个企业级项目(一) - 掘金
Flutter项目规范 - 掘金
flutter状态库到底用哪个! - 掘金
基于Getx的Flutter应用架构 - 掘金
GitHub - TheAlphamerc/flutter_twitter_clone: Fully functional Twitter clone built in flutter framework using Firebase realtime database and storage

  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值