Flutter移动功能增强插件mobility_features的使用

发布于 1周前 作者 ionicwang 来自 Flutter

Flutter移动功能增强插件mobility_features的使用

Mobility Features 插件简介

Mobility Features 是一个用于基于手机位置跟踪实时计算移动特征的Flutter插件。该插件可以收集以下位置特征:

  • places(地点)
  • stops(停留点)
  • moves(移动)

从这些基础特征中,插件会计算出一系列衍生特征,包括但不限于:

  • 重要地点数量
  • 家居停留时间
  • 位置熵
  • 归一化位置熵
  • 行驶距离

更多关于这些移动特征的理论背景,请参见理论背景部分。

Setup 配置

添加依赖

在您的 pubspec.yaml 文件中添加 mobility_features 依赖,并在代码中导入:

import 'package:mobility_features/mobility_features.dart';

MobilityFeatures 类是一个单例模式类,可以通过 MobilityFeatures() 在代码中访问。

Step 1 - 参数配置

根据需求调整参数以影响特征算法:

  • 停留半径(stop radius):建议保持较低值(5-20米)
  • 地点半径(place radius):稍高一点(25-50米)
  • 停留时长(stop duration):通常应小于3分钟

示例配置如下:

void initState() {
  ...
  MobilityFeatures().stopDuration = Duration(seconds: 20);
  MobilityFeatures().placeRadius = 50;
  MobilityFeatures().stopRadius = 5.0;
}

Step 2 - 设置位置流

由于此包不直接支持位置数据收集,因此需要使用其他位置插件,如 carp_background_locationlocation 来实现位置流。以下是使用 carp_background_location 插件的一个例子:

void streamInit() async {
  locationStream = LocationManager().locationStream;

  // 启动位置服务
  await LocationManager().start();

  // 将 [LocationDto] 映射为 [LocationSample]
  Stream<LocationSample> locationSampleStream = locationStream.map(
      (location) => LocationSample(
          GeoLocation(location.latitude, location.longitude),
          DateTime.now()));

  // 提供 [MobilityFeatures] 实例与位置样本流
  MobilityFeatures().startListening(locationSampleStream);

  // 订阅 [MobilityContext] 更新
  mobilitySubscription =
      MobilityFeatures().contextStream.listen(onMobilityContext);
}

注意:必须处理好位置权限问题,确保应用能持续在后台获取位置信息。

Step 3 - 监听移动特征

每当有新的 MobilityContext 对象生成时,回调方法 onMobilityContext 将被调用:

void onMobilityContext(MobilityContext context) {
  print('Context received: ${context.toJson()}');
}

通过 toJson() 方法,您可以将 MobilityContext 对象序列化为JSON格式,便于查看或进一步处理。

示例代码

下面是一个完整的示例应用程序,它展示了如何使用 mobility_features 插件来收集并展示移动特征。

library mobility_app;

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:carp_background_location/carp_background_location.dart';
import 'package:mobility_features/mobility_features.dart';

part 'stops_page.dart';
part 'moves_page.dart';
part 'places_page.dart';

// ... 省略了一些辅助函数 ...

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Mobility Features Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(title: 'Mobility Features Example'),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key, required this.title});
  final String title;

  @override
  HomePageState createState() => HomePageState();
}

class HomePageState extends State<HomePage> {
  AppState _state = AppState.NO_FEATURES;
  int _currentIndex = 0;

  late Stream<LocationDto> locationStream;
  late StreamSubscription<LocationDto> locationSubscription;
  late StreamSubscription<MobilityContext> mobilitySubscription;
  late MobilityContext _mobilityContext;

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

    MobilityFeatures().stopDuration = const Duration(seconds: 20);
    MobilityFeatures().placeRadius = 50.0;
    MobilityFeatures().stopRadius = 5.0;

    LocationManager().distanceFilter = 0;
    LocationManager().interval = 1;
    LocationManager().notificationTitle = 'Mobility Features';
    LocationManager().notificationMsg = 'Your geo-location is being tracked';
    streamInit();
  }

  Future<void> streamInit() async {
    await requestNotificationPermission();

    if (!await isLocationAlwaysGranted()) {
      await requestLocationPermission();
      await askForLocationAlwaysPermission();
    }

    locationStream = LocationManager().locationStream;

    await LocationManager().start();

    Stream<LocationSample> locationSampleStream = locationStream.map(
        (location) => LocationSample(
            GeoLocation(location.latitude, location.longitude),
            DateTime.now()));

    await MobilityFeatures().startListening(locationSampleStream);

    mobilitySubscription =
        MobilityFeatures().contextStream.listen(onMobilityContext);
  }

  Future<bool> isLocationAlwaysGranted() async {
    bool granted = false;
    try {
      granted = await Permission.locationAlways.isGranted;
    } catch (e) {
      print(e);
    }
    return granted;
  }

  Future<bool> askForLocationAlwaysPermission() async {
    bool granted = false;
    try {
      granted = await Permission.locationAlways.isGranted;
    } catch (e) {
      print(e);
    }

    if (!granted) {
      granted =
          await Permission.locationAlways.request() == PermissionStatus.granted;
    }

    return granted;
  }

  Future<void> requestLocationPermission() async {
    final result = await Permission.location.request();

    if (result == PermissionStatus.granted) {
      print('GRANTED'); 
    } else {
      print('NOT GRANTED'); 
    }
  }

  Future<void> requestNotificationPermission() async {
    final result = await Permission.notification.request();

    if (result == PermissionStatus.granted) {
      print('NOTIFICATION GRANTED');
    } else {
      print('NOTIFICATION NOT GRANTED');
    }
  }

  void onMobilityContext(MobilityContext context) {
    print('Context received: ${context.toJson()}');
    setState(() {
      _state = AppState.FEATURES_READY;
      _mobilityContext = context;
    });
  }

  @override
  void dispose() {
    mobilitySubscription.cancel();
    locationSubscription.cancel();
    super.dispose();
  }

  Widget get featuresOverview {
    return ListView(
      children: <Widget>[
        entry("Stops", "${_mobilityContext.stops?.length}", stopIcon),
        entry("Moves", "${_mobilityContext.moves?.length}", moveIcon),
        entry("Significant Places",
            "${_mobilityContext.numberOfSignificantPlaces}", placeIcon),
        entry(
            "Home Stay",
            _mobilityContext.homeStay == null || _mobilityContext.homeStay! < 0
                ? "?"
                : "${(_mobilityContext.homeStay! * 100).toStringAsFixed(1)}%",
            homeStayIcon),
        entry(
            "Distance Traveled",
            "${(_mobilityContext.distanceTraveled! / 1000).toStringAsFixed(2)} km",
            distanceTraveledIcon),
        entry(
            "Normalized Entropy",
            "${_mobilityContext.normalizedEntropy?.toStringAsFixed(2)}",
            entropyIcon),
        entry(
            "Location Variance",
            "${(111.133 * _mobilityContext.locationVariance!).toStringAsFixed(5)} km",
            varianceIcon),
      ],
    );
  }

  List<Widget> get contentNoFeatures {
    return [
      Container(
          margin: const EdgeInsets.all(25),
          child: const Text(
            'Move around to start generating features',
            style: TextStyle(fontSize: 20),
          ))
    ];
  }

  List<Widget> get contentFeaturesReady {
    return [
      Container(
          margin: const EdgeInsets.all(25),
          child: Column(children: [
            const Text(
              'Statistics for today,',
              style: TextStyle(fontSize: 20),
            ),
            Text(
              formatDate(_mobilityContext.date!),
              style: const TextStyle(fontSize: 20, color: Colors.blue),
            ),
          ])),
      Expanded(child: featuresOverview),
    ];
  }

  Widget get content {
    List<Widget> children;
    if (_state == AppState.FEATURES_READY) {
      children = contentFeaturesReady;
    } else {
      children = contentNoFeatures;
    }
    return Column(children: children);
  }

  void onTabTapped(int index) {
    setState(() {
      _currentIndex = index;
    });
  }

  Widget get navBar => BottomNavigationBar(
        onTap: onTabTapped,
        currentIndex: _currentIndex,
        type: BottomNavigationBarType.fixed,
        items: const [
          BottomNavigationBarItem(icon: featuresIcon, label: 'Features'),
          BottomNavigationBarItem(icon: stopIcon, label: 'Stops'),
          BottomNavigationBarItem(icon: placeIcon, label: 'Places'),
          BottomNavigationBarItem(icon: moveIcon, label: 'Moves')
        ],
      );

  @override
  Widget build(BuildContext context) {
    List<Stop> stops = [];
    List<Move> moves = [];
    List<Place> places = [];

    if (_state == AppState.FEATURES_READY) {
      for (var x in _mobilityContext.stops!) {
        print(x);
      }
      for (var x in _mobilityContext.moves!) {
        print(x);
        print('${x.stopFrom} --> ${x.stopTo}');
      }
      stops = _mobilityContext.stops ?? [];
      moves = _mobilityContext.moves ?? [];
      places = _mobilityContext.places ?? [];
    }

    List<Widget> pages = [
      content,
      StopsPage(stops),
      PlacesPage(places),
      MovesPage(moves),
    ];

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.teal,
        title: Text(widget.title),
      ),
      body: pages[_currentIndex],
      bottomNavigationBar: navBar,
    );
  }
}

此示例程序包含了一个主界面,用于显示当前收集到的所有移动特征统计信息;以及三个子页面,分别用于详细展示停留点、地点和移动路径的具体信息。用户可以通过底部导航栏在这四个页面之间切换浏览。


更多关于Flutter移动功能增强插件mobility_features的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter移动功能增强插件mobility_features的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,我可以为你提供一个关于如何使用Flutter的mobility_features插件的代码案例。mobility_features插件主要用于增强Flutter应用在移动设备上的功能,比如电池状态监测、设备方向管理等。以下是一个简单的示例,展示了如何使用该插件来获取设备的电池状态信息。

首先,确保你已经在pubspec.yaml文件中添加了mobility_features依赖:

dependencies:
  flutter:
    sdk: flutter
  mobility_features: ^x.y.z  # 替换为最新版本号

然后,运行flutter pub get来安装依赖。

接下来,编写Flutter代码来获取电池状态信息。下面是一个完整的示例:

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Mobility Features Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: BatteryStatusScreen(),
    );
  }
}

class BatteryStatusScreen extends StatefulWidget {
  @override
  _BatteryStatusScreenState createState() => _BatteryStatusScreenState();
}

class _BatteryStatusScreenState extends State<BatteryStatusScreen> {
  BatteryStatus? _batteryStatus;

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

  Future<void> _checkBatteryStatus() async {
    try {
      final batteryInfo = await Battery.instance.batteryState;
      setState(() {
        _batteryStatus = batteryInfo;
      });
    } catch (e) {
      print('Error checking battery status: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Battery Status Demo'),
      ),
      body: Center(
        child: _batteryStatus == null
            ? CircularProgressIndicator()
            : Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Battery Level: ${_batteryStatus!.level * 100}%'),
                  Text('Is Charging: ${_batteryStatus!.isCharging ? 'Yes' : 'No'}'),
                ],
              ),
      ),
    );
  }
}

在这个示例中,我们做了以下几件事:

  1. 引入必要的包:引入了flutter/material.dartmobility_features/mobility_features.dart
  2. 创建主应用:使用MaterialApp作为应用的主框架。
  3. 创建电池状态屏幕:创建了一个有状态的BatteryStatusScreen小部件,用于显示电池状态信息。
  4. 初始化电池状态检查:在initState方法中调用_checkBatteryStatus方法来获取电池状态。
  5. 显示电池状态:根据获取到的电池状态信息,更新UI显示电池电量和充电状态。

请注意,mobility_features插件的功能可能会随着版本的更新而有所变化,因此建议查阅最新的官方文档以获取最新的使用方法和API。此外,在实际应用中,你可能还需要处理更多的边界情况和错误处理逻辑。

回到顶部