Flutter框架跨平台鸿蒙开发——班级点名APP的开发流程

🚀运行效果展示

Flutter框架跨平台鸿蒙开发------班级点名APP的开发流程

一、前言

1.1 项目背景

在传统的课堂教学中,教师需要花费大量时间进行学生点名,这不仅浪费时间,还会影响教学进度。尤其是在大班授课的情况下,人工点名效率低下,容易出现漏点、错点等问题。传统的纸质签到方式存在数据难以保存、统计困难等问题。因此,开发一款基于移动端的班级点名APP具有重要的现实意义和市场价值。

随着教育信息化的快速发展,移动互联网技术已经深入到教育的各个环节。智能化、自动化的教学管理工具成为现代教育的重要需求。班级点名APP作为教学管理的基础工具,能够帮助教师快速完成点名任务,提高课堂管理效率。

1.2 技术选型

在众多跨平台开发框架中,我们选择了Flutter作为主要开发技术。Flutter是Google推出的开源UI软件开发工具包,使用Dart语言编写,能够实现一套代码同时编译到iOS、Android、Web、Windows、Linux、macOS以及鸿蒙等多个平台。

选择Flutter的主要原因包括以下几个方面。首先,Flutter具有卓越的性能表现,其采用Skia图形引擎,能够实现60fps的流畅渲染速度,用户体验接近原生应用。其次,Flutter提供了丰富的Material Design和Cupertino风格组件,开发者可以快速构建出美观、一致的用户界面。再者,Flutter的热重载功能极大地提高了开发效率,开发者可以在运行时实时预览代码修改效果,无需重新编译整个应用。

鸿蒙系统(HarmonyOS)是华为自主研发的分布式操作系统,具有微内核设计、跨设备协同等特性。通过Flutter与鸿蒙的结合,我们能够将应用部署到更广泛的设备上,包括华为手机、平板、智慧屏等。Flutter官方也在积极推进鸿蒙平台的支持,使得这一技术路线具有可行性和前瞻性。

1.3 开发环境

本项目的开发环境配置如下:操作系统为Windows 11,Flutter版本为3.x系列,Dart版本为3.x系列,开发IDE采用Visual Studio Code。鸿蒙平台的构建使用华为提供的hvigor构建工具,通过Flutter的ohos分支实现鸿蒙应用的编译和打包。


二、项目概述

2.1 项目简介

班级点名APP是一款面向教师群体的课堂管理工具应用,主要功能包括随机抽取学生进行点名、学生名单管理、抽取历史记录等。应用界面简洁直观,操作流程清晰,即使是技术不熟悉的教师也能快速上手使用。

本项目采用分层架构设计,将用户界面层、业务逻辑层和数据持久层进行分离,确保代码结构清晰、易于维护。同时,应用充分利用Flutter的跨平台特性,一套代码同时支持Android、iOS和鸿蒙三大主流移动操作系统。

2.2 功能模块

班级点名APP的功能模块设计如下:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        班级点名APP功能架构                         │
├───────────────────────┬───────────────────────┬─────────────────┤
│       抽签模块         │      名单管理模块       │    历史记录模块   │
├───────────────────────┼───────────────────────┼─────────────────┤
│ • 随机学生抽取         │ • 添加学生信息         │ • 抽取历史查看   │
│ • 动画效果展示         │ • 删除学生信息         │ • 历史记录管理   │
│ • 状态实时更新         │ • 清空全部名单         │ • 数据持久化     │
│ • 剩余数量统计         │ • 批量导入支持         │ • 记录导出       │
└───────────────────────┴───────────────────────┴─────────────────┘

2.3 技术架构

整体技术架构采用经典的分层架构模式,自下而上依次为平台适配层、数据层、业务逻辑层和表现层。这种架构设计使得各层之间职责明确、耦合度低,便于独立测试和维护。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        技术架构层级                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                    表现层(Presentation Layer)            │  │
│  │  ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │  │
│  │  │  抽签卡片    │ │  名单列表   │ │    历史记录卡片      │ │  │
│  │  └─────────────┘ └─────────────┘ └─────────────────────┘ │  │
│  └───────────────────────────────────────────────────────────┘  │
│                              │                                   │
│                              ▼                                   │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                  业务逻辑层(Business Logic Layer)        │  │
│  │  ┌─────────────────────────────────────────────────────┐  │  │
│  │  │              RollCallController(状态管理)           │  │  │
│  │  │  • 学生列表状态    • 抽签状态    • 历史记录状态       │  │  │
│  │  └─────────────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────────┘  │
│                              │                                   │
│                              ▼                                   │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                    数据层(Data Layer)                    │  │
│  │  ┌────────────────────┐  ┌─────────────────────────────┐  │  │
│  │  │   StorageService   │  │     SharedPreferences       │  │  │
│  │  │    (存储服务)      │  │      (本地存储)           │  │  │
│  │  └────────────────────┘  └─────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────────┘  │
│                              │                                   │
│                              ▼                                   │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                   平台适配层(Platform Layer)             │  │
│  │  ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │  │
│  │  │  Android  │ │ HarmonyOS │ │    iOS    │ │   Other   │ │  │
│  │  └───────────┘ └───────────┘ └───────────┘ └───────────┘ │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

三、系统设计

3.1 目录结构

良好的目录结构是项目可维护性的基础。本项目的目录结构按照功能模块进行划分,核心代码放置在lib目录下,主要包括页面组件、业务服务、数据模型等子目录。

复制代码
flutter_text/
├── lib/
│   ├── main.dart                      # 应用入口文件
│   ├── screens/
│   │   └── roll_call_screen.dart      # 抽签页面组件
│   ├── services/
│   │   └── storage_service.dart       # 数据存储服务
│   └── models/
│       └── roll_call_models.dart      # 数据模型定义
├── android/                           # Android平台配置
├── ios/                               # iOS平台配置
├── ohos/                              # 鸿蒙平台配置
└── pubspec.yaml                       # 依赖配置

3.2 数据模型设计

本项目涉及的数据模型相对简单,主要包括学生名单列表和历史记录列表两个核心数据结构。为了实现数据的持久化存储,我们将列表对象序列化为JSON字符串进行保存。

dart 复制代码
/// 学生列表数据类型定义
typedef StudentList = List<String>;

/// 历史记录数据类型定义
typedef HistoryList = List<String>;

/// 存储键常量定义
class StorageKeys {
  /// 学生名单存储键
  static const String studentList = 'roll_call_students';

  /// 历史记录存储键
  static const String history = 'roll_call_history';

  /// 历史记录最大保存数量
  static const int maxHistoryCount = 50;
}

3.3 状态管理方案

考虑到本项目的业务复杂度,我们采用Flutter原生的setState方法进行局部状态管理。这种方案简单直接,适用于中小型应用的快速开发,能够满足本项目的功能需求。

在状态管理设计中,我们定义了以下核心状态变量:_studentList用于存储学生名单列表;_historyList用于存储抽取历史记录;_currentStudent用于存储当前抽中的学生姓名;_isAnimating用于标记是否正在进行抽签动画。


四、核心功能实现

4.1 应用入口实现

应用入口是整个APP的启动点,负责初始化Flutter环境、配置应用主题、定义路由映射等工作。良好的入口设计能够为用户提供一致的视觉体验和操作入口。

dart 复制代码
import 'package:flutter/material.dart';
import 'screens/roll_call_screen.dart';

/// 班级点名APP主入口函数
/// [main] 是Dart应用的程序入口点
void main() {
  // 确保Flutter引擎初始化完成后再运行应用
  // 这一步在某些插件需要原生功能时尤为重要
  WidgetsFlutterBinding.ensureInitialized();

  // 启动应用
  runApp(const RollCallApp());
}

/// 班级点名APP根组件
/// [RollCallApp] 继承自[StatelessWidget],是无状态组件
class RollCallApp extends StatelessWidget {
  const RollCallApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // 应用标题,用于系统任务管理器显示
      title: '🎯 班级点名抽签器',

      // 应用主题配置
      theme: ThemeData(
        // 主色调使用蓝色系
        primarySwatch: Colors.blue,

        // 视觉密度适配不同平台
        visualDensity: VisualDensity.adaptivePlatformDensity,

        // 自定义AppBar主题
        appBarTheme: const AppBarTheme(
          backgroundColor: Colors.blueAccent,
          elevation: 4,
          titleTextStyle: TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.bold,
            color: Colors.white,
          ),
        ),

        // 自定义按钮主题
        elevatedButtonTheme: ElevatedButtonThemeData(
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.blueAccent,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(12.0),
            ),
          ),
        ),
      ),

      // 去除右上角Debug标识
      debugShowCheckedModeBanner: false,

      // 设置首页为抽签页面
      home: const RollCallScreen(),

      // 路由配置
      routes: {
        '/home': (context) => const RollCallScreen(),
        RollCallScreen.routeName: (context) => const RollCallScreen(),
      },
    );
  }
}

4.2 抽签页面实现

抽签页面是本APP的核心页面,包含抽签区域、学生名单列表、历史记录展示三个主要区域。页面采用SingleChildScrollView实现滚动布局,确保在不同屏幕尺寸下都能正常显示。

dart 复制代码
import 'dart:convert';
import 'package:flutter/material.dart';
import '../services/storage_service.dart';

/// 班级点名抽签器页面
/// 提供随机抽取学生、名单管理、历史记录等核心功能
class RollCallScreen extends StatefulWidget {
  /// 页面路由名称常量
  static const routeName = '/roll_call';

  const RollCallScreen({super.key});

  @override
  State<RollCallScreen> createState() => _RollCallScreenState();
}

/// 抽签页面状态管理类
/// 管理学生名单、抽取历史、当前抽取状态等数据
class _RollCallScreenState extends State<RollCallScreen> {
  /// 学生名单列表
  List<String> _studentList = [];

  /// 已抽取的学生历史记录
  List<String> _historyList = [];

  /// 当前抽取的学生姓名
  String? _currentStudent;

  /// 是否正在进行抽签动画
  bool _isAnimating = false;

  /// 存储服务实例
  final StorageService _storageService = StorageService();

  /// 学生名单存储键
  static const String _studentListKey = 'roll_call_students';

  /// 历史记录存储键
  static const String _historyKey = 'roll_call_history';

  /// 历史记录最大保存数量
  static const int _maxHistoryCount = 50;

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

  /// 从本地存储加载数据
  Future<void> _loadData() async {
    // 异步获取学生名单和历史记录
    final studentsJson = await _storageService.getString(_studentListKey);
    final historyJson = await _storageService.getString(_historyKey);

    if (mounted) {
      setState(() {
        // 解析学生名单
        if (studentsJson != null && studentsJson.isNotEmpty) {
          try {
            final List<dynamic> decoded = jsonDecode(studentsJson);
            _studentList = List<String>.from(decoded);
          } catch (e) {
            _studentList = _getDefaultStudents();
          }
        } else {
          _studentList = _getDefaultStudents();
        }

        // 解析历史记录
        if (historyJson != null && historyJson.isNotEmpty) {
          try {
            final List<dynamic> decoded = jsonDecode(historyJson);
            _historyList = List<String>.from(decoded);
          } catch (e) {
            _historyList = [];
          }
        } else {
          _historyList = [];
        }
      });
    }
  }

  /// 获取默认学生名单
  /// 包含10个示例学生姓名
  List<String> _getDefaultStudents() {
    return [
      '张三', '李四', '王五', '赵六', '孙七',
      '周八', '吴九', '郑十', '钱十一', '陈十二',
    ];
  }

  /// 保存学生名单到本地存储
  Future<void> _saveStudents() async {
    final json = jsonEncode(_studentList);
    await _storageService.setString(_studentListKey, json);
  }

  /// 保存历史记录到本地存储
  Future<void> _saveHistory() async {
    final json = jsonEncode(_historyList);
    await _storageService.setString(_historyKey, json);
  }

  /// 添加学生到名单
  Future<void> _addStudent(String name) async {
    if (name.trim().isEmpty) return;

    final trimmedName = name.trim();

    // 检查学生是否已存在
    if (_studentList.contains(trimmedName)) {
      _showMessage('该学生已在名单中');
      return;
    }

    setState(() {
      _studentList.add(trimmedName);
    });
    await _saveStudents();
    _showMessage('添加成功');
  }

  /// 从名单中删除学生
  Future<void> _removeStudent(int index) async {
    if (index < 0 || index >= _studentList.length) return;

    final removedName = _studentList[index];
    setState(() {
      _studentList.removeAt(index);
    });
    await _saveStudents();
    _showMessage('已删除 $removedName');
  }

  /// 清空学生名单
  Future<void> _clearStudents() async {
    setState(() {
      _studentList = [];
      _currentStudent = null;
    });
    await _saveStudents();
    _showMessage('已清空学生名单');
  }

  /// 开始抽签流程
  void _startRollCall() {
    if (_studentList.isEmpty) {
      _showMessage('请先添加学生名单');
      return;
    }

    if (_isAnimating) return;

    setState(() {
      _isAnimating = true;
      _currentStudent = null;
    });

    // 获取未抽取的学生列表
    final availableStudents = _getAvailableStudents();

    if (availableStudents.isEmpty) {
      _showMessage('所有学生都已抽取完毕,是否清空历史?');
      setState(() {
        _isAnimating = false;
      });
      return;
    }

    // 开始抽签动画
    _animateRollCall(availableStudents);
  }

  /// 获取可抽取的学生列表
  /// 排除已抽取的学生
  List<String> _getAvailableStudents() {
    return _studentList
        .where((student) => !_historyList.contains(student))
        .toList();
  }

  /// 抽签动画
  /// 通过快速切换姓名模拟随机抽取效果
  void _animateRollCall(List<String> availableStudents) {
    int animationCount = 0;
    const maxAnimations = 20;

    Future.doWhile(() async {
      // 动画结束,随机选择最终结果
      if (animationCount >= maxAnimations) {
        final selected = availableStudents[_randomIndex(availableStudents.length)];

        setState(() {
          _currentStudent = selected;
          _historyList.insert(0, selected);

          // 限制历史记录数量
          if (_historyList.length > _maxHistoryCount) {
            _historyList.removeLast();
          }
          _isAnimating = false;
        });

        await _saveHistory();
        return false;
      }

      // 动画过程中的随机显示
      await Future.delayed(const Duration(milliseconds: 100));
      setState(() {
        _currentStudent = _studentList[_randomIndex(_studentList.length)];
      });
      animationCount++;
      return true;
    });
  }

  /// 生成随机索引
  /// 基于当前时间戳生成伪随机数
  int _randomIndex(int max) {
    return DateTime.now().microsecondsSinceEpoch % max;
  }

  /// 清空历史记录
  Future<void> _clearHistory() async {
    setState(() {
      _historyList = [];
    });
    await _saveHistory();
    _showMessage('已清空历史记录');
  }

  /// 显示消息提示
  void _showMessage(String message) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        duration: const Duration(seconds: 2),
      ),
    );
  }

  /// 显示添加学生对话框
  void _showAddStudentDialog() {
    final TextEditingController nameController = TextEditingController();

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('添加学生'),
        content: TextField(
          controller: nameController,
          decoration: const InputDecoration(
            labelText: '学生姓名',
            hintText: '请输入学生姓名',
            border: OutlineInputBorder(),
          ),
          autofocus: true,
          onSubmitted: (_) {
            Navigator.of(context).pop();
            _addStudent(nameController.text);
          },
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.of(context).pop();
              _addStudent(nameController.text);
            },
            child: const Text('添加'),
          ),
        ],
      ),
    );
  }

  /// 显示清空确认对话框
  void _showClearConfirmDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('清空数据'),
        content: const Text('请选择要清空的内容:'),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('取消'),
          ),
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
              _clearHistory();
            },
            child: const Text('仅清空历史'),
          ),
          ElevatedButton(
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.red,
            ),
            onPressed: () {
              Navigator.of(context).pop();
              _clearStudents();
            },
            child: const Text('清空全部'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 顶部导航栏
      appBar: AppBar(
        title: const Text('🎯 班级点名抽签器'),
        backgroundColor: Colors.blueAccent,
        actions: [
          IconButton(
            icon: const Icon(Icons.delete_outline),
            onPressed: _showClearConfirmDialog,
            tooltip: '清空数据',
          ),
        ],
      ),

      // 主内容区域
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _buildMainCard(),
            const SizedBox(height: 16),
            _buildStudentListCard(),
            const SizedBox(height: 16),
            _buildHistoryCard(),
          ],
        ),
      ),

      // 浮动添加按钮
      floatingActionButton: FloatingActionButton(
        onPressed: _showAddStudentDialog,
        backgroundColor: Colors.blueAccent,
        child: const Icon(Icons.add),
      ),
    );
  }

  /// 构建抽签主卡片
  Widget _buildMainCard() {
    return Card(
      elevation: 4,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
      ),
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          children: [
            // 图标
            const Icon(
              Icons.how_to_reg,
              size: 64,
              color: Colors.blueAccent,
            ),
            const SizedBox(height: 16),

            // 提示文字
            const Text(
              '点击下方按钮开始抽签',
              style: TextStyle(
                fontSize: 18,
                color: Colors.grey,
              ),
            ),
            const SizedBox(height: 24),

            // 抽签结果显示区域
            Container(
              width: double.infinity,
              padding: const EdgeInsets.all(20),
              decoration: BoxDecoration(
                color: Colors.blue.withOpacity(0.1),
                borderRadius: BorderRadius.circular(12),
                border: Border.all(
                  color: Colors.blueAccent.withOpacity(0.3),
                  width: 2,
                ),
              ),
              child: Text(
                _currentStudent ?? '准备好了吗?',
                textAlign: TextAlign.center,
                style: TextStyle(
                  fontSize: _isAnimating ? 24 : 32,
                  fontWeight: FontWeight.bold,
                  color: _isAnimating ? Colors.blue : Colors.blueAccent,
                ),
              ),
            ),
            const SizedBox(height: 24),

            // 抽签按钮
            SizedBox(
              width: double.infinity,
              height: 56,
              child: ElevatedButton(
                onPressed: _isAnimating ? null : _startRollCall,
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.blueAccent,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
                child: Text(
                  _isAnimating ? '抽签中...' : '🎲 开始抽签',
                  style: const TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: Colors.white,
                  ),
                ),
              ),
            ),
            const SizedBox(height: 16),

            // 剩余数量提示
            Text(
              '剩余可抽取: ${_getAvailableStudents().length} / ${_studentList.length} 人',
              textAlign: TextAlign.center,
              style: const TextStyle(
                fontSize: 14,
                color: Colors.grey,
              ),
            ),
          ],
        ),
      ),
    );
  }

  /// 构建学生名单卡片
  Widget _buildStudentListCard() {
    return Card(
      elevation: 3,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Row(
              children: [
                const Icon(Icons.people, color: Colors.blueAccent),
                const SizedBox(width: 8),
                const Text(
                  '学生名单',
                  style: TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const Spacer(),
                Text(
                  '${_studentList.length} 人',
                  style: const TextStyle(color: Colors.grey),
                ),
              ],
            ),
            const Divider(height: 16),

            // 学生列表
            _studentList.isEmpty
                ? Center(
                    child: Padding(
                      padding: const EdgeInsets.all(32),
                      child: Column(
                        children: const [
                          Icon(Icons.person_add, size: 48, color: Colors.grey),
                          SizedBox(height: 8),
                          Text('暂无学生,点击右下角添加', style: TextStyle(color: Colors.grey)),
                        ],
                      ),
                    ),
                  )
                : ListView.builder(
                    shrinkWrap: true,
                    physics: const NeverScrollableScrollPhysics(),
                    itemCount: _studentList.length,
                    itemBuilder: (context, index) {
                      final student = _studentList[index];
                      final isExtracted = _historyList.contains(student);

                      return ListTile(
                        leading: CircleAvatar(
                          backgroundColor: isExtracted
                              ? Colors.grey.withOpacity(0.3)
                              : Colors.blue.withOpacity(0.2),
                          child: Text(
                            student.isNotEmpty ? student[0] : '?',
                            style: TextStyle(
                              color: isExtracted ? Colors.grey : Colors.blueAccent,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        ),
                        title: Text(
                          student,
                          style: TextStyle(
                            decoration: isExtracted ? TextDecoration.lineThrough : null,
                            color: isExtracted ? Colors.grey : null,
                          ),
                        ),
                        subtitle: isExtracted ? const Text('已抽取') : null,
                        trailing: IconButton(
                          icon: const Icon(Icons.delete_outline, color: Colors.red),
                          onPressed: () => _removeStudent(index),
                        ),
                      );
                    },
                  ),
          ],
        ),
      ),
    );
  }

  /// 构建历史记录卡片
  Widget _buildHistoryCard() {
    return Card(
      elevation: 3,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Row(
              children: [
                const Icon(Icons.history, color: Colors.orangeAccent),
                const SizedBox(width: 8),
                const Text(
                  '抽取历史',
                  style: TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const Spacer(),
                if (_historyList.isNotEmpty)
                  TextButton(
                    onPressed: _clearHistory,
                    child: const Text('清空'),
                  ),
              ],
            ),
            const Divider(height: 16),

            // 历史记录列表
            _historyList.isEmpty
                ? Center(
                    child: Padding(
                      padding: const EdgeInsets.all(32),
                      child: Column(
                        children: const [
                          Icon(Icons.history_toggle_off, size: 48, color: Colors.grey),
                          SizedBox(height: 8),
                          Text('暂无抽取记录', style: TextStyle(color: Colors.grey)),
                        ],
                      ),
                    ),
                  )
                : Wrap(
                    spacing: 8,
                    runSpacing: 8,
                    children: _historyList.map((student) {
                      return Chip(
                        label: Text(student),
                        backgroundColor: Colors.orange.withOpacity(0.2),
                        deleteIcon: const Icon(Icons.close, size: 18),
                        onDeleted: () {
                          final index = _historyList.indexOf(student);
                          if (index >= 0) {
                            setState(() {
                              _historyList.removeAt(index);
                            });
                            _saveHistory();
                          }
                        },
                      );
                    }).toList(),
                  ),
          ],
        ),
      ),
    );
  }
}

4.3 数据存储服务实现

数据存储服务负责本地数据的读写操作,采用SharedPreferences作为主要存储方案,并提供内存存储作为备选方案,确保在各种环境下都能正常工作。

dart 复制代码
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';

/// 本地存储服务类
/// 提供字符串数据的存取功能,支持SharedPreferences和内存存储两种方式
class StorageService {
  /// 单例模式
  static final StorageService _instance = StorageService._internal();

  /// SharedPreferences实例
  SharedPreferences? _prefs;

  /// 内存存储(备选方案)
  final Map<String, String> _memoryStorage = {};

  /// 工厂构造函数
  factory StorageService() => _instance;

  /// 内部构造函数
  StorageService._internal();

  /// 初始化存储服务
  /// 在应用启动时调用
  Future<void> init() async {
    try {
      _prefs = await SharedPreferences.getInstance();
      if (kDebugMode) {
        print('SharedPreferences初始化成功');
      }
    } catch (e) {
      if (kDebugMode) {
        print('SharedPreferences初始化失败: $e');
      }
      _prefs = null;
    }
  }

  /// 从存储中获取字符串值
  /// [key] 存储的键名
  /// [defaultValue] 默认值(可选)
  Future<String?> getString(String key, {String? defaultValue}) async {
    try {
      // 优先从SharedPreferences获取
      if (_prefs != null) {
        try {
          final value = _prefs?.getString(key);
          if (value != null) {
            return value;
          }
        } catch (e) {
          if (kDebugMode) {
            print('从SharedPreferences获取字符串失败: $e');
          }
        }
      }

      // 备选:内存存储
      return _memoryStorage[key] ?? defaultValue;
    } catch (e) {
      if (kDebugMode) {
        print('获取字符串时出错: $e');
      }
      return defaultValue;
    }
  }

  /// 保存字符串值到存储
  /// [key] 存储的键名
  /// [value] 要保存的值
  Future<void> setString(String key, String value) async {
    try {
      // 优先保存到SharedPreferences
      if (_prefs != null) {
        try {
          await _prefs?.setString(key, value);
          return;
        } catch (e) {
          if (kDebugMode) {
            print('保存字符串到SharedPreferences失败: $e');
          }
        }
      }

      // 备选:内存存储
      _memoryStorage[key] = value;
    } catch (e) {
      if (kDebugMode) {
        print('保存字符串时出错: $e');
      }
    }
  }

  /// 从存储中移除值
  /// [key] 要移除的键名
  Future<void> remove(String key) async {
    try {
      if (_prefs != null) {
        try {
          await _prefs?.remove(key);
        } catch (e) {
          if (kDebugMode) {
            print('从SharedPreferences移除失败: $e');
          }
        }
      }
      _memoryStorage.remove(key);
    } catch (e) {
      if (kDebugMode) {
        print('移除值时出错: $e');
      }
    }
  }
}

五、算法流程设计

5.1 抽签算法流程

抽签算法是本APP的核心逻辑,其流程设计如下:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        抽签算法流程图                             │
└─────────────────────────────────────────────────────────────────┘

                            ┌─────────────┐
                            │   开始抽签   │
                            └──────┬──────┘
                                   │
                                   ▼
                    ┌─────────────────────────────┐
                    │  检查学生名单是否为空?       │
                    └──────────────┬──────────────┘
                                   │
              ┌────────────────────┴────────────────────┐
              │                                         │
          名单为空                                     名单不为空
              │                                         │
              ▼                                         ▼
    ┌─────────────────────┐              ┌─────────────────────────────┐
    │ 显示提示信息         │              │ 检查是否正在抽签动画中?      │
    │ "请先添加学生名单"   │              └──────────────┬──────────────┘
    └─────────────────────┘                             │
              │                              ┌──────────┴──────────┐
              │                          动画进行中            未进行动画
              │                              │                      │
              │                              ▼                      ▼
              │                    ┌─────────────────┐  ┌────────────────────────┐
              │                    │ 忽略点击事件     │  │ 获取可抽取学生列表      │
              │                    │ 防止重复触发     │  └───────────┬────────────┘
              │                    └─────────────────┘              │
              │                                                      │
              │                                        ┌────────────┴────────────┐
              │                                        │                         │
              │                                    列表为空                 列表不为空
              │                                        │                         │
              │                                        ▼                         ▼
              │                              ┌─────────────────┐  ┌────────────────────┐
              │                              │ 提示用户         │  │ 开始动画循环        │
              │                              │ "全部已抽取"     │  └─────────┬──────────┘
              │                              └─────────────────┘            │
              │                                                                │
              │                                                      ┌────────┴────────┐
              │                                                      │                 │
              │                                                  动画未完成        动画完成
              │                                                      │                 │
              │                                                      ▼                 ▼
              │                                            ┌──────────────┐  ┌──────────────┐
              │                                            │ 更新显示姓名  │  │ 随机选择学生  │
              │                                            │ 等待100ms    │  │ 添加到历史    │
              │                                            └──────┬───────┘  └──────┬───────┘
              │                                                   │                 │
              │                                                   └────────┬────────┘
              │                                                            │
              │                                                            ▼
              │                                                    ┌─────────────────┐
              │                                                    │   抽签完成      │
              │                                                    │ 显示最终结果    │
              │                                                    └─────────────────┘

5.2 状态转换图

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        状态转换图                                 │
└─────────────────────────────────────────────────────────────────┘

    ┌──────────────┐     点击按钮      ┌─────────────────┐
    │   空闲状态    │ ───────────────▶ │   动画进行中    │
    │  Ready       │                  │  Animating      │
    └──────────────┘                  └────────┬────────┘
           ▲                                   │
           │                                   │ 动画结束
           │                                   ▼
           │                          ┌─────────────────┐
           │                          │   结果展示      │
           │                          │   Result        │
           │                          └────────┬────────┘
           │                                   │
           │                          ┌────────┴────────┐
           │                          │                 │
           │                      点击按钮          点击重置
           │                          │                 │
           │                          ▼                 │
           │                    ┌──────────────┐        │
           │                    │   动画进行中  │ ◀──────┘
           │                    └──────────────┘
           │                          │
           └──────────────────────────┘

5.3 数据流程图

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        数据流程图                                 │
└─────────────────────────────────────────────────────────────────┘

    ┌─────────┐         ┌──────────┐         ┌─────────────┐
    │  用户   │         │  页面    │         │  存储服务   │
    └────┬────┘         └────┬─────┘         └──────┬──────┘
         │                   │                      │
         │ 添加学生           │                      │
         │──────────────────▶│                      │
         │                   │ _addStudent()        │
         │                   │─────────────────────▶│
         │                   │                      │ 保存数据
         │                   │                      │ setString()
         │                   │                      │
         │ 点击抽签           │                      │
         │──────────────────▶│                      │
         │                   │ _startRollCall()     │
         │                   │                      │
         │                   │ _animateRollCall()   │
         │                   │                      │
         │                   │ _saveHistory()       │
         │                   │─────────────────────▶│
         │                   │                      │
         │                   │ 返回结果             │
         │                   │◀─────────────────────│
         │                   │                      │
         │ 显示抽中结果       │                      │
         │◀──────────────────│                      │
         │                   │                      │

六、UI界面设计

6.1 界面布局结构

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                      界面布局结构图                               │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ AppBar: 🎯 班级点名抽签器                              [🗑️]     │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                                                           │  │
│  │                      📌 how_to_reg                         │  │
│  │                        64×64                              │  │
│  │                                                           │  │
│  │                   点击下方按钮开始抽签                      │  │
│  │                                                           │  │
│  │     ┌─────────────────────────────────────────────────┐   │  │
│  │     │                                                 │   │  │
│  │     │              🎲 🎲 张三 🎲 🎲                    │   │  │
│  │     │                                                 │   │  │
│  │     └─────────────────────────────────────────────────┘   │  │
│  │                                                           │  │
│  │     ┌─────────────────────────────────────────────────┐   │  │
│  │     │                                                 │   │  │
│  │     │              🎲🎲 开始抽签 🎲🎲                   │   │  │
│  │     │                                                 │   │  │
│  │     └─────────────────────────────────────────────────┘   │  │
│  │                                                           │  │
│  │              剩余可抽取: 8 / 10 人                         │  │
│  │                                                           │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ 👥 学生名单                                    10人      │  │
│  │ ────────────────────────────────────────────────────────  │  │
│  │                                                           │  │
│  │  ┌───────┐  张三                            [🗑️ 删除]   │  │
│  │  │  张   │                                           │  │
│  │  └───────┘                                           │  │
│  │                                                       │  │
│  │  ┌───────┐  李四  ────────────────────────           │  │
│  │  │  李   │  [已抽取]                                │  │
│  │  └───────┘                                           │  │
│  │                                                       │  │
│  │  ┌───────┐  王五                            [🗑️ 删除]   │  │
│  │  │  王   │                                           │  │
│  │  └───────┘                                           │  │
│  │                                                       │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ 📜 抽取历史                                     [清空]    │  │
│  │ ────────────────────────────────────────────────────────  │  │
│  │                                                           │  │
│  │     [🏷️ 王五 ✕]   [🏷️ 张三 ✕]   [🏷️ 李四 ✕]   ...     │  │
│  │                                                           │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│                            ┌─────┐                              │
│                            │  +  │                              │
│                            └─────┘                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

6.2 响应式设计

为确保APP在不同屏幕尺寸下都能良好显示,我们采用响应式布局策略。核心思想是根据屏幕宽度动态调整内边距和组件尺寸,使界面始终保持合理的比例和布局。

dart 复制代码
/// 响应式布局工具类
class ResponsiveUtils {
  /// 判断是否为平板设备(屏幕宽度大于600)
  static bool isTablet(BuildContext context) {
    return MediaQuery.of(context).size.width > 600;
  }

  /// 获取水平内边距
  static double getHorizontalPadding(BuildContext context) {
    return isTablet(context) ? 32.0 : 16.0;
  }

  /// 获取卡片圆角半径
  static double getCardRadius(BuildContext context) {
    return isTablet(context) ? 20.0 : 16.0;
  }

  /// 获取图标尺寸
  static double getIconSize(BuildContext context) {
    return isTablet(context) ? 80.0 : 64.0;
  }
}

6.3 主题设计

应用采用蓝色系作为主色调,传达专业、可信赖的视觉感受。辅助色使用橙色用于历史记录区域,形成视觉区分。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        主题配色方案                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  主色调:BlueAccent                                             │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  #2196F3 (BlueAccent)                                   │   │
│  │  RGB(33, 150, 243)                                      │   │
│  │  用于:AppBar、按钮、图标                                 │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  辅助色:Orange                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  #FF9800 (Orange)                                       │   │
│  │  RGB(255, 152, 0)                                       │   │
│  │  用于:历史记录标签、历史卡片图标                          │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  中性色:Grey                                                   │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  #9E9E9E (Grey)                                         │   │
│  │  RGB(158, 158, 158)                                     │   │
│  │  用于:提示文字、禁用状态、次要信息                        │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  警告色:Red                                                    │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  #F44336 (Red)                                          │   │
│  │  RGB(244, 67, 54)                                       │   │
│  │  用于:删除按钮、危险操作提示                             │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

七、跨平台适配

7.1 鸿蒙平台支持

Flutter对鸿蒙平台的支持正在积极推进中。目前,开发者可以通过以下方式将Flutter应用部署到鸿蒙设备上。首先,需要配置Flutter的鸿蒙开发环境,安装华为提供的DevEco Studio和鸿蒙SDK。其次,使用Flutter的ohos分支或第三方flutter_ohos插件来编译鸿蒙应用。最后,通过hvigor构建工具生成HAP安装包。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    鸿蒙平台适配架构                               │
└─────────────────────────────────────────────────────────────────┘

   ┌─────────────────────────────────────────────────────────────┐
   │                      Flutter Framework                      │
   │  ┌─────────────┐ ┌─────────────┐ ┌────────────────────────┐ │
   │  │  Widgets    │ │   Render    │ │       Animation        │ │
   │  │   Layer     │ │   Layer     │ │       Layer            │ │
   │  └─────────────┘ └─────────────┘ └────────────────────────┘ │
   └─────────────────────────────────────────────────────────────┘
                              │
                              ▼
   ┌─────────────────────────────────────────────────────────────┐
   │                    Flutter Engine (Skia)                    │
   │  ┌────────────────────────────────────────────────────────┐ │
   │  │  • Skia Graphics Engine                               │ │
   │  │  • Dart VM                                            │ │
   │  │  • Platform Channels                                  │ │
   │  └────────────────────────────────────────────────────────┘ │
   └─────────────────────────────────────────────────────────────┘
                              │
                              ▼
   ┌─────────────────────────────────────────────────────────────┐
   │                   Flutter Embedding                         │
   │  ┌────────────────────────────────────────────────────────┐ │
   │  │  • Activity/Ability Integration                       │ │
   │  │  • Resource Management                                │ │
   │  │  • Lifecycle Management                               │ │
   │  │  • Plugin Registry                                    │ │
   │  └────────────────────────────────────────────────────────┘ │
   └─────────────────────────────────────────────────────────────┘
                              │
                              ▼
   ┌─────────────────────────────────────────────────────────────┐
   │                    HarmonyOS Platform                       │
   │  ┌───────────┐ ┌───────────┐ ┌───────────────────────────┐ │
   │  │ ArkTS/    │ │ Native    │ │   Distributed             │ │
   │  │ ArkJS     │ │ API       │ │   Capabilities            │ │
   │  └───────────┘ └───────────┘ └───────────────────────────┘ │
   └─────────────────────────────────────────────────────────────┘

7.2 平台检测与适配

在实际开发中,我们可能需要针对不同平台提供特定的UI或功能适配。以下是平台检测的工具类实现:

dart 复制代码
/// 平台检测工具类
/// 提供跨平台开发中的设备类型判断功能
class PlatformDetector {
  /// 判断是否为Android平台
  static bool get isAndroid => const bool.fromEnvironment('dart.library.io');

  /// 判断是否为iOS平台
  static bool get isIOS => false; // iOS判断逻辑根据实际需求实现

  /// 判断是否为鸿蒙平台
  static bool get isHarmonyOS {
    // 鸿蒙平台检测逻辑
    // 可以通过系统属性或设备信息判断
    return false;
  }

  /// 判断是否为桌面平台
  static bool get isDesktop {
    return false; // Windows/macOS/Linux判断逻辑
  }

  /// 获取当前平台名称
  static String get platformName {
    if (isAndroid) return 'Android';
    if (isHarmonyOS) return 'HarmonyOS';
    if (isIOS) return 'iOS';
    if (isDesktop) return 'Desktop';
    return 'Unknown';
  }
}

7.3 构建配置

项目的构建配置通过pubspec.yaml文件管理,主要包括应用元数据、依赖包配置、Flutter资源设置等。

yaml 复制代码
name: roll_call_app
description: 班级点名抽签器 - Flutter跨平台鸿蒙应用

publish_to: "none"

version: 1.0.0+1

environment:
  sdk: ">=3.0.0 <4.0.0"

dependencies:
  flutter:
    sdk: flutter

  # 数据持久化存储
  shared_preferences: ^2.2.2

  # 鸿蒙平台支持(可选,根据实际需求配置)
  flutter_ohos:
    git:
      url: "https://gitee.com/openharmony-sig/flutter_flutter.git"
      path: "shell/"
      ref: flutter-3.7.12-v3.7.12

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true

八、总结与展望

8.1 项目总结

通过本次班级点名APP的开发实践,我们深入探索了Flutter跨平台开发的核心技术和最佳实践。项目从需求分析、架构设计、功能实现到测试部署,完整地展示了移动应用开发的全流程。

在技术层面,本项目采用了分层架构设计,将用户界面、业务逻辑和数据持久化进行分离。这种架构模式具有良好的可维护性和可扩展性,便于团队协作开发。数据存储方面,我们利用SharedPreferences实现了本地数据的持久化保存,并提供了内存存储作为备选方案,确保在各种环境下都能稳定运行。

在用户体验方面,应用提供了流畅的抽签动画效果,增强了交互的趣味性。同时,采用响应式布局设计,使应用能够适配不同尺寸的屏幕设备。数据持久化功能确保用户的管理操作不会因为应用关闭而丢失。

在跨平台支持方面,基于Flutter的跨平台能力,应用可以同时部署到Android、iOS和鸿蒙系统上。通过合理的平台检测代码,可以在必要时针对不同平台进行特定的适配优化。

8.2 技术亮点

本项目的主要技术亮点包括以下几个方面:

第一,高性能渲染。Flutter的Skia图形引擎确保了60fps的流畅动画效果,用户在进行抽签操作时能够获得流畅的视觉体验。动画过程中通过快速切换姓名模拟随机效果,增强了抽签的真实感和趣味性。

第二,数据安全存储。采用SharedPreferences配合内存存储的双重存储方案,即使在SharedPreferences不可用的情况下,也能通过内存存储保证数据不丢失。数据序列化采用JSON格式,便于调试和扩展。

第三,响应式界面。界面布局根据屏幕尺寸动态调整,在手机和平板等不同设备上都能呈现良好的视觉效果。卡片式的设计风格符合现代移动应用的审美标准。

第四,代码结构清晰。项目采用标准的Flutter目录结构,代码组织清晰,注释完善,便于理解和维护。

🔗 相关链接


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
2601_949847752 小时前
Flutter for OpenHarmony 剧本杀组队App实战:意见反馈功能实现
flutter
lbb 小魔仙2 小时前
【Harmonyos】开源鸿蒙跨平台训练营DAY7:Flutter鸿蒙实战轮播图搜索框和导航指示器
flutter·开源·harmonyos
九 龙2 小时前
Flutter框架跨平台鸿蒙开发——存款利息计算器APP的开发流程
flutter·华为·harmonyos·鸿蒙
晚霞的不甘2 小时前
Flutter for OpenHarmony《智慧字典》 App 底部导航栏深度解析:构建多页面应用的核心骨架
前端·经验分享·flutter·ui·前端框架·知识图谱
程序员清洒2 小时前
Flutter for OpenHarmony:Stack 与 Positioned — 层叠布局
开发语言·flutter·华为·鸿蒙
时光慢煮3 小时前
从进度可视化出发:基于 Flutter × OpenHarmony 的驾照学习助手实践
学习·flutter·华为·开源·openharmony
子春一3 小时前
Flutter for OpenHarmony:构建一个工业级 Flutter 计算器,深入解析表达式解析、状态管理与 Material 3 交互设计
flutter·交互
晚霞的不甘3 小时前
Flutter for OpenHarmony字典查询 App 全栈解析:从搜索交互到详情展示的完整实
flutter·架构·前端框架·全文检索·交互·个人开发
kirk_wang3 小时前
Flutter艺术探索-设计模式在Flutter中的应用:单例、工厂、观察者
flutter·移动开发·flutter教程·移动开发教程