Flutter开发一个Wifi信号测量应用

在之前的一篇文章中我介绍了如何用Jetpack compose来开发一个Android的Wifi信号测量应用,使得可以根据室内不同地点的Wifi信号来生成指纹,用于室内导航,详情可见Jetpack Compose开发一个Android WiFi信号测量应用-CSDN博客。但是Jetpack compose只能用于开发Android应用,如果我们要开发ios应用,需要用其他的框架来重写代码。

Flutter是一个Google推出的一个跨平台的UI框架,可以快速在iOS和Android上构建高质量的原生用户界面,实现一套代码同时适配Android, ios, macos, window, linux等多个系统。为此我决定用Flutter来重构之前写的应用,实现一个跨平台的Wifi信号测量应用。

应用架构

这个应用比较简单,包括了两个页面,第一个页面是让用户输入当前室内位置的编号,同时会显示当前手机的朝向角度。用户可以点击一个按钮来对当前位置和朝向进行Wifi信号检测。

第二个页面显示信号检测的列表,用户可以点击按钮把检测结果上传到远端服务器。

我采用Android studio来新建一个Flutter项目。

页面主题设计

Flutter和Jetpack Compose一样,都可以用Material Design来设计应用的主题。具体可见我之前另一篇博客Material Design设计和美化APP应用-CSDN博客的介绍,在Material design的theme builder设计好主色调之后,导出为Flutter项目需要的文件,放置到lib目录。例如我以#825500作为主色,生成的color_schemes.g.dart文件内容如下:

import 'package:flutter/material.dart';const lightColorScheme = ColorScheme(brightness: Brightness.light,primary: Color(0xFF825500),onPrimary: Color(0xFFFFFFFF),primaryContainer: Color(0xFFFFDDB3),onPrimaryContainer: Color(0xFF291800),secondary: Color(0xFF6F5B40),onSecondary: Color(0xFFFFFFFF),secondaryContainer: Color(0xFFFBDEBC),onSecondaryContainer: Color(0xFF271904),tertiary: Color(0xFF51643F),onTertiary: Color(0xFFFFFFFF),tertiaryContainer: Color(0xFFD4EABB),onTertiaryContainer: Color(0xFF102004),error: Color(0xFFBA1A1A),errorContainer: Color(0xFFFFDAD6),onError: Color(0xFFFFFFFF),onErrorContainer: Color(0xFF410002),background: Color(0xFFFFFBFF),onBackground: Color(0xFF1F1B16),surface: Color(0xFFFFFBFF),onSurface: Color(0xFF1F1B16),surfaceVariant: Color(0xFFF0E0CF),onSurfaceVariant: Color(0xFF4F4539),outline: Color(0xFF817567),onInverseSurface: Color(0xFFF9EFE7),inverseSurface: Color(0xFF34302A),inversePrimary: Color(0xFFFFB951),shadow: Color(0xFF000000),surfaceTint: Color(0xFF825500),outlineVariant: Color(0xFFD3C4B4),scrim: Color(0xFF000000),
);const darkColorScheme = ColorScheme(brightness: Brightness.dark,primary: Color(0xFFFFB951),onPrimary: Color(0xFF452B00),primaryContainer: Color(0xFF633F00),onPrimaryContainer: Color(0xFFFFDDB3),secondary: Color(0xFFDDC2A1),onSecondary: Color(0xFF3E2D16),secondaryContainer: Color(0xFF56442A),onSecondaryContainer: Color(0xFFFBDEBC),tertiary: Color(0xFFB8CEA1),onTertiary: Color(0xFF243515),tertiaryContainer: Color(0xFF3A4C2A),onTertiaryContainer: Color(0xFFD4EABB),error: Color(0xFFFFB4AB),errorContainer: Color(0xFF93000A),onError: Color(0xFF690005),onErrorContainer: Color(0xFFFFDAD6),background: Color(0xFF1F1B16),onBackground: Color(0xFFEAE1D9),surface: Color(0xFF1F1B16),onSurface: Color(0xFFEAE1D9),surfaceVariant: Color(0xFF4F4539),onSurfaceVariant: Color(0xFFD3C4B4),outline: Color(0xFF9C8F80),onInverseSurface: Color(0xFF1F1B16),inverseSurface: Color(0xFFEAE1D9),inversePrimary: Color(0xFF825500),shadow: Color(0xFF000000),surfaceTint: Color(0xFFFFB951),outlineVariant: Color(0xFF4F4539),scrim: Color(0xFF000000),
);

主页面设计

确定了架构之后,我们开始设计主页面。在Lib目录新建一个dart文件,例如MyHomePage。其代码如下:

class MyHomePage extends StatefulWidget {const MyHomePage({super.key, required this.title});final String title;@overrideState<MyHomePage> createState() => _MyHomePageState();
}class _MyHomePageState extends State<MyHomePage> {static const eventChannel = EventChannel('roygao.cn/orientationEvent');final positionNameController = TextEditingController();final orientationController = TextEditingController();Stream<String> streamOrientationFromNative() {return eventChannel.receiveBroadcastStream().map((event) => event.toString());}@overridevoid initState() {eventChannel.receiveBroadcastStream().listen((message) {// Handle incoming messagesetState(() {orientationController.text = message;});});super.initState();}@overrideWidget build(BuildContext context) {var appState = context.watch<MyAppState>();final theme = Theme.of(context);return Scaffold(backgroundColor: theme.colorScheme.surfaceVariant,appBar: AppBar(backgroundColor: theme.colorScheme.inversePrimary,title: Text(widget.title),),body: Center(child: Column(children: <Widget>[const Image(image: AssetImage('images/wifi_location.jpg')),Padding(padding: const EdgeInsets.all(20.0),child: Column(children: <Widget>[TextField(controller: positionNameController,obscureText: false,decoration: const InputDecoration(border: OutlineInputBorder(),labelText: 'Indoor position name',),),const SizedBox(height: 15.0,),TextField(controller: orientationController,obscureText: false,enabled: false,decoration: const InputDecoration(border: OutlineInputBorder(),labelText: 'Orientation in degrees',),),const SizedBox(height: 15.0,),ElevatedButton(style: ElevatedButton.styleFrom(primary: theme.colorScheme.primary,elevation: 0,),onPressed: () {appState.updatePositionOrientation(positionNameController.text, orientationController.text);appState.updateWifiScanResults();Navigator.of(context).push(MaterialPageRoute(builder: (context) => MeasureReport(title: widget.title,//positionName: positionNameController.text,//orientation: orientationController.text,)));},child: Text("Measure", style: theme.textTheme.bodyLarge!.copyWith(color: theme.colorScheme.onPrimary,),),),])),],),),);}@overridevoid dispose() {// Clean up the controller when the widget is disposed.positionNameController.dispose();orientationController.dispose();super.dispose();}
}

解释一下以上的代码,这个类是继承了StatefulWidget,因为从传感器接收的朝向角度信息我想保存在这个Widget中。在_MyHomePageState中定义了一个EventChannel,用于调用Android的原生方法来获取加速传感器和磁传感器的信息,计算手机朝向之后通过EventChannel来传送到Flutter widget。在initState函数中需要监听EventChannel获取到的数据,并调用SetState来更新显示朝向数据的那个TextField的controller。在build函数中,定义了一个应用级别的state,这个state可以用于保存用户输入的位置信息和手机朝向数据,并给到我们之后的测试报告的widget来获取。另外,通过Scaffold layout来组织这个UI界面,显示相应的组件。在Measure这个button的onPressed中,更新appState的位置和朝向数据,并调用updateWifiScanResult方法来测量Wifi信号,然后通过Navigator组建来跳转到测量报告页面。

调用Android原生方法

刚才提到了用EventChannel的方式来调用Android的原生方法来计算手机朝向。其实在Flutter里面也有一个sensor plugin可以获取到手机的传感器数据,不需要调用原生方法。但是这个plugin获取到的只是磁传感器和加速度传感器的数值,还需要通过一些计算来获得手机的朝向。在Android的原生方法已经提供了方法可以直接计算,因此这里我还是采用EventChannel的方式来做。在Android Studio中打开这个Flutter项目的Android文件夹,编辑MainActivity文件。以下是代码:

class MainActivity: FlutterActivity(), SensorEventListener {private val eventChannel = "roygao.cn/orientationEvent"private lateinit var sensorManager : SensorManagerprivate var accelerometerReading = FloatArray(3)private var magnetometerReading = FloatArray(3)private var rotationMatrix = FloatArray(9)private var orientationAngles = FloatArray(3)override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {super.configureFlutterEngine(flutterEngine)EventChannel(flutterEngine.dartExecutor.binaryMessenger, eventChannel).setStreamHandler(MyEventChannel)}override fun onSensorChanged(event: SensorEvent) {if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {System.arraycopy(event.values, 0, accelerometerReading, 0, accelerometerReading.size)} else if (event.sensor.type == Sensor.TYPE_MAGNETIC_FIELD) {System.arraycopy(event.values, 0, magnetometerReading, 0, magnetometerReading.size)}SensorManager.getRotationMatrix(rotationMatrix,null,accelerometerReading,magnetometerReading)SensorManager.getOrientation(rotationMatrix, orientationAngles)var degree = if (orientationAngles[0] >= 0) {(180 * orientationAngles[0]/PI).toInt()} else {(180 * (2+orientationAngles[0]/PI)).toInt()}MyEventChannel.sendEvent(degree.toString())}override fun onAccuracyChanged(p0: Sensor?, p1: Int) {}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManagerval accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)val magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_UI)sensorManager.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_UI)}object MyEventChannel: EventChannel.StreamHandler {private var eventSink: EventChannel.EventSink? = nulloverride fun onListen(arguments: Any?, events: EventChannel.EventSink?) {eventSink = events;}override fun onCancel(arguments: Any?) {eventSink = null;}fun sendEvent(message: String) {eventSink?.success(message)}}
}

这个代码比较简单,在configureFlutterEngine里面定义EventChannel的handler,提供一个sendEvent的方法来传输数据。同时实现了SensorEventListener的接口,当收到SensorEvent的时候通过两个传感器的信息计算出手机的朝向角度,并调用sendEvent方法。

页面入口设计

在main.dart文件中,修改如下:

void main() {runApp(const MyApp());
}class MyApp extends StatelessWidget {const MyApp({super.key});// This widget is the root of your application.@overrideWidget build(BuildContext context) {return ChangeNotifierProvider(create: (context) => MyAppState(),child: MaterialApp(title: 'Wifi Measurement',theme: ThemeData(useMaterial3: true, colorScheme: lightColorScheme),darkTheme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme),home: const MyHomePage(title: 'Wifi Measurement'),),);}
}class MyAppState extends ChangeNotifier {var positionName;var orientation;List<WiFiAccessPoint> accessPoints = [];void updatePositionOrientation(String name, String degree) {positionName = name;orientation = degree;}updateWifiScanResults() async {final can = await WiFiScan.instance.canGetScannedResults(askPermissions: true);switch (can) {case CanGetScannedResults.yes:// get scanned resultsaccessPoints = await WiFiScan.instance.getScannedResults();break;default:break;}}
}

这里定义了一个MyAppState用于保存应用级别的状态数据。这个类扩展了ChangeNotifier,使得其他Widget可以通过观察这个类的对象来获取到状态的更新。在这个类里面定义了一个updateWifiScanResults的方法,这个是采用了wifi scan的plugin来实现的。在pubspec.yaml里面的dependencies增加wifi_scan: ^0.4.1

测量报告页面

最后是设计一个页面显示测量报告,当在主页面点击测量按钮,跳转到这个页面显示测量结果。代码如下:

class MeasureReport extends StatelessWidget {const MeasureReport({super.key, required this.title});final String title;@overrideState<MeasureReport> createState() => _MeasureReportState();@overrideWidget build(BuildContext context) {var appState = context.watch<MyAppState>();final theme = Theme.of(context);return Scaffold(backgroundColor: theme.colorScheme.surfaceVariant,appBar: AppBar(backgroundColor: theme.colorScheme.inversePrimary,title: const Text("Wifi Measurement"),),body: Column(children: <Widget>[Padding(padding: const EdgeInsets.all(20.0),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: <Widget>[Text("Position: ${appState.positionName}", style: theme.textTheme.titleLarge,),const SizedBox(height: 5.0,),Text("Orientation: ${appState.orientation}", style: theme.textTheme.titleLarge,),const SizedBox(height: 5.0,),Text("Scan Results:", style: theme.textTheme.titleLarge,),const SizedBox(height: 5.0,),ListView.builder(itemCount: appState.accessPoints.length,itemBuilder: (context, index) {var rowColor = index%2==0?theme.colorScheme.primaryContainer:theme.colorScheme.surfaceVariant;if (index==0) {return Column(children: [Container(child: Row(children: [Expanded(child: Text("BSSID", style: theme.textTheme.bodyLarge,)),Expanded(child: Text("Level", style: theme.textTheme.bodyLarge,)),],),),Container(color: rowColor,child: Row(children: [Expanded(child: Text(appState.accessPoints[index].bssid, style: theme.textTheme.bodyLarge,)),Expanded(child: Text(appState.accessPoints[index].level.toString(), style: theme.textTheme.bodyLarge,)),],),)]);}return Container(color: rowColor,child: Row(children: <Widget>[Expanded(child: Text(appState.accessPoints[index].bssid, style: theme.textTheme.bodyLarge,)),Expanded(child: Text(appState.accessPoints[index].level.toString(), style: theme.textTheme.bodyLarge,)),],));},scrollDirection: Axis.vertical,shrinkWrap: true,),],),)]),);}
}

这个代码比较简单,就不用解释了。

运行效果

最终的应用运行效果如下:

wifi measure flutter app

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/578036.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Hadoop】ZooKeeper数据模型Znode

ZooKeeper 数据模型ZnodeZooKeeper 中的时间ZooKeeper 节点属性 ZooKeeper 数据模型Znode 前面提过&#xff0c;Zookeeper相当于文件系统通知机制。既然是文件系统&#xff0c;那就涉及数据模型。 ZooKeeper 的数据模型在结构上和Unix标准文件系统非常相似&#xff0c;都是采用…

分类预测 | Matlab实现SCSO-SVM基于沙猫群优化算法优化支持向量机的多变量分类预测【23年新算法】

分类预测 | Matlab实现SCSO-SVM基于沙猫群优化算法优化支持向量机的多变量分类预测【23年新算法】 目录 分类预测 | Matlab实现SCSO-SVM基于沙猫群优化算法优化支持向量机的多变量分类预测【23年新算法】分类效果基本描述程序设计参考资料 分类效果 基本描述 1.Matlab实现SCSO-…

C# WPF上位机开发(windows pad上的应用)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 大部分同学可能都认为c# wpf只能用在pc端。其实这是一种误解。c# wpf固然暂时只能运行在windows平台上面&#xff0c;但是windows平台不仅仅是电脑…

听GPT 讲Rust源代码--src/tools(27)

File: rust/src/tools/clippy/clippy_lints/src/methods/suspicious_to_owned.rs 文件rust/src/tools/clippy/clippy_lints/src/methods/suspicious_to_owned.rs的作用是实施Clippy lint规则&#xff0c;检测产生潜在性能问题的字符转换代码&#xff0c;并给出相关建议。 在Rus…

概率论中的 50 个具有挑战性的问题 [第 6 部分]:Chuck-a-Luck

一、说明 我最近对与概率有关的问题产生了兴趣。我偶然读到了弗雷德里克莫斯特勒&#xff08;Frederick Mosteller&#xff09;的《概率论中的五十个具有挑战性的问题与解决方案》&#xff09;一书。我认为创建一个系列来讨论这些可能作为面试问题出现的迷人问题会很有趣。每篇…

详解ibm_t60(945)的板子的保护隔离和ec的待机供电

1.,首先看ec待机条件: 待机供电&#xff0c;32k时钟&#xff0c;复位&#xff0c;适配器检测&#xff0c;开关信号。但是视频居然是找适配器的接口&#xff0c;跟着视频走&#xff0c;所以我先找打了适配器接口j24。vint20为公共点&#xff0c;我查了vint20的所有接线发现没有小…

js右击自定义菜单

功能点&#xff1a;右击阻止默认事件 根据js提供方法window.getSelection()判断当前有值则出现菜单 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-w…

现代 NLP:详细概述,第 1 部分:transformer

阿比吉特罗伊 一、说明 近五年来&#xff0c;随着 BERT 和 GPT 等思想的引入&#xff0c;我们在自然语言处理领域取得了巨大的成就。在本文中&#xff0c;我们的目标是逐步深入研究改进的细节&#xff0c;并了解它们带来的演变。 二、关注就是你所需要的 2017 年&#xff0c;来…

CSRF(Pikachu)

CSRF&#xff08;get&#xff09; 首先我们先登录账号 admin 密码是&#xff1b;123456 点击修改个人信息 用F12或者BP 抓包看看我们的url 那么构成的CSRF攻击payload为http://pikachu.shifa23.com/pikachu/vul/csrf/csrfget/csrf_get_edit.php?sexboy&phonenum”手机…

美团开店宝集成策略:API连接助力用户运营与客服系统

无代码开发与美团开店宝API的协同作用 随着电子商务的快速发展&#xff0c;商家们迫切需要一种既能提高效率又能降低技术门槛的电商管理方案。美团开店宝API提供了这样一种解决方案&#xff0c;特别是当与无代码开发结合时&#xff0c;它的优势更加明显。无代码开发免去了商家…

44.常用shell之 export - 设置或显示环境变量 的用法及衍生用法

export 是一个在类 Unix 系统的 shell 中常用的命令&#xff0c;用于设置或显示环境变量。环境变量是在操作系统层面定义的&#xff0c;用于控制程序的行为和访问系统资源。以下是 export 命令的基本用法和一些衍生用法的示例&#xff1a; 基本用法 设置环境变量: export VARv…

Django-REST-Framework 如何快速生成Swagger, ReDoc格式的 REST API 文档

1、API 接口文档的几种规范格式 前后端分离项目中&#xff0c;使用规范、便捷的API接口文档工具&#xff0c;可以有效提高团队工作效率。 标准化的API文档的益处&#xff1a; 允许开发人员以交互式的方式查看、测试API接口&#xff0c;以方便使用将所有可暴露的API接口进行分…

普冉PY32系列(十四) 从XL2400迁移到XL2400P

目录 普冉PY32系列(一) PY32F0系列32位Cortex M0 MCU简介普冉PY32系列(二) Ubuntu GCC Toolchain和VSCode开发环境普冉PY32系列(三) PY32F002A资源实测 - 这个型号不简单普冉PY32系列(四) PY32F002A/003/030的时钟设置普冉PY32系列(五) 使用JLink RTT代替串口输出日志普冉PY32…

React快速入门之组件

组件 文件&#xff1a;Profile.js export default function Profile({isPacked true&#xff0c;head,stlyeTmp,src,size 80}) {if (isPacked) {head head " ✔";}return (<div><h1>{head}</h1><imgsrc{src}alt"Katherine Johnson&q…

cmake 入门笔记

以下内容为本人的著作&#xff0c;如需要转载&#xff0c;请声明原文链接 微信公众号「englyf」https://mp.weixin.qq.com/s/dUmsmiwzULQKmjfFILDdag 1. cmake 是什么&#xff1f; 这些年大型 C/C 工程都纷纷转到了 cmake 环境下&#xff0c;那么这个工具到底有什么魅力吸引着…

电商数据分析-02-电商业务介绍及表结构

参考 电商业务简介 大数据项目之电商数仓、电商业务简介、电商业务流程、电商常识、业务数据介绍、电商业务表、后台管理系统 举个例子:&#x1f330; 1.1 电商业务流程 电商的业务流程可以以一个普通用户的浏览足迹为例进行说明&#xff0c;用户点开电商首页开始浏览&…

Guava的Joiner的日常使用

具体使用参考官方文档&#xff1a;https://github.com/google/guava/wiki/StringsExplained#joiner//1 处理&#xff0c;为null的值&#xff0c;替换 String join Joiner.on("_").useForNull("*").join("1", "2", 90, 100,110,109,20…

shell 编程中内置的变量(冷门又好用)

简介 分别盘点一下 shell 中的内置变量&#xff0c;真的巨好用&#xff01;&#xff01;&#xff01;包括&#xff1a;环境变量类、shell 变量类、终端设置类和其他一些变量。 常用的内置变量目录如下 1. 环境变量类 $MACHTYPE&#xff1a;机器类型 $OSTYPE&#xff1a;操作…

JVM初识-----01章

一.虚拟机与java虚拟机的区别以及共同点 1.虚拟机&#xff08;Virtual Machine&#xff0c;简称VM&#xff09; 是一种能够在物理计算机上模拟一台完整的计算机系统的软件。它运行在宿主操作系统之上&#xff0c;可以提供一个独立的运行环境&#xff0c;使得在不同的操作系统上…

【深度学习-目标检测】03 - Faster R-CNN 论文学习与总结

论文地址&#xff1a;Faster R-CNN: Towards Real-Time ObjectDetection with Region Proposal Networks 论文学习 1. 摘要与引言 研究背景与挑战&#xff1a;当前最先进的目标检测网络依赖于 区域提议&#xff08;Region Proposals&#xff09;来假设目标的位置&#xff0c…