Flutter 框架跨平台鸿蒙开发 - 打字练习应用开发教程

Flutter打字练习应用开发教程

项目简介

打字练习是一款专业的打字训练应用,帮助用户提高打字速度和准确率。应用支持英文、中文、数字、符号等多种练习模式,提供实时反馈和详细的统计分析功能。
运行效果图



核心特性

  • 多种模式:英文、中文、数字、符号、混合练习
  • 三档难度:简单、中等、困难
  • 实时反馈:彩色标注正确/错误字符
  • 速度测试:WPM(每分钟字数)计算
  • 准确率统计:实时显示打字准确率
  • 练习记录:保存每次练习的详细数据
  • 进步曲线:可视化展示学习进度
  • 数据持久化:本地保存练习记录

技术栈

  • Flutter 3.x
  • Material Design 3
  • SharedPreferences(数据持久化)
  • Timer(计时功能)
  • RichText(文本高亮显示)

数据模型设计

练习模式枚举

dart 复制代码
enum PracticeMode {
  english,    // 英文
  chinese,    // 中文
  number,     // 数字
  symbol,     // 符号
  mixed,      // 混合
}

难度级别枚举

dart 复制代码
enum DifficultyLevel {
  easy,       // 简单
  medium,     // 中等
  hard,       // 困难
}

打字记录模型

dart 复制代码
class TypingRecord {
  final DateTime date;                // 练习日期
  final PracticeMode mode;            // 练习模式
  final DifficultyLevel difficulty;   // 难度级别
  final int totalChars;               // 总字符数
  final int correctChars;             // 正确字符数
  final int timeSpent;                // 用时(秒)
  final double wpm;                   // 每分钟字数
  
  double get accuracy => totalChars > 0 
      ? (correctChars / totalChars * 100) 
      : 0;
}

核心功能实现

1. 文本生成

根据模式和难度生成练习文本:

英文文本生成
dart 复制代码
String _generateEnglishText() {
  final words = [
    'the', 'be', 'to', 'of', 'and', 'a', 'in', 'that', 'have', 'I',
    // ... 更多常用单词
  ];

  final random = Random();
  final length = widget.difficulty == DifficultyLevel.easy
      ? 10
      : widget.difficulty == DifficultyLevel.medium
          ? 20
          : 30;

  final selectedWords = List.generate(
    length,
    (_) => words[random.nextInt(words.length)],
  );

  return selectedWords.join(' ');
}
中文文本生成
dart 复制代码
String _generateChineseText() {
  final texts = {
    DifficultyLevel.easy: [
      '春眠不觉晓,处处闻啼鸟。',
      '床前明月光,疑是地上霜。',
      // ... 更多简单诗句
    ],
    DifficultyLevel.medium: [
      '人生得意须尽欢,莫使金樽空对月。',
      // ... 更多中等难度文本
    ],
    DifficultyLevel.hard: [
      '明月几时有?把酒问青天。不知天上宫阙,今夕是何年。',
      // ... 更多困难文本
    ],
  };

  final textList = texts[widget.difficulty]!;
  return textList[Random().nextInt(textList.length)];
}
数字和符号生成
dart 复制代码
String _generateNumberText() {
  final random = Random();
  final length = widget.difficulty == DifficultyLevel.easy ? 20 : 40;
  return List.generate(length, (_) => random.nextInt(10).toString()).join(' ');
}

String _generateSymbolText() {
  final symbols = ['!', '@', '#', '\$', '%', '^', '&', '*', '(', ')'];
  final random = Random();
  final length = widget.difficulty == DifficultyLevel.easy ? 15 : 25;
  return List.generate(length, (_) => symbols[random.nextInt(symbols.length)]).join(' ');
}

2. 实时输入监听

监听用户输入并实时反馈:

dart 复制代码
void _onInputChanged() {
  if (!isStarted && _inputController.text.isNotEmpty) {
    setState(() {
      isStarted = true;
    });
    _startTimer();
  }

  setState(() {
    currentInput = _inputController.text;
    currentIndex = currentInput.length;
    
    // 计算正确字符数
    correctChars = 0;
    for (int i = 0; i < currentInput.length && i < targetText.length; i++) {
      if (currentInput[i] == targetText[i]) {
        correctChars++;
      }
    }
    
    totalTypedChars = currentInput.length;
    
    // 检查是否完成
    if (currentInput.length >= targetText.length) {
      _finishPractice();
    }
  });
}

3. 文本高亮显示

使用RichText实现彩色标注:

dart 复制代码
List<TextSpan> _buildTextSpans() {
  final spans = <TextSpan>[];
  
  for (int i = 0; i < targetText.length; i++) {
    Color color;
    FontWeight weight = FontWeight.normal;
    
    if (i < currentInput.length) {
      if (currentInput[i] == targetText[i]) {
        color = Colors.green;  // 正确:绿色
        weight = FontWeight.bold;
      } else {
        color = Colors.red;    // 错误:红色
        weight = FontWeight.bold;
      }
    } else if (i == currentInput.length) {
      color = Colors.blue;     // 当前位置:蓝色
      weight = FontWeight.bold;
    } else {
      color = Colors.grey.shade600;  // 未输入:灰色
    }
    
    spans.add(TextSpan(
      text: targetText[i],
      style: TextStyle(
        color: color,
        fontWeight: weight,
        backgroundColor: i == currentInput.length ? Colors.blue.shade50 : null,
      ),
    ));
  }
  
  return spans;
}

4. WPM计算

计算每分钟字数(Words Per Minute):

dart 复制代码
void _finishPractice() {
  _timer?.cancel();
  
  // 计算WPM
  final minutes = elapsedSeconds / 60;
  final words = targetText.split(' ').length;
  final wpm = minutes > 0 ? (words / minutes).toDouble() : 0.0;

  final record = TypingRecord(
    date: DateTime.now(),
    mode: widget.mode,
    difficulty: widget.difficulty,
    totalChars: targetText.length,
    correctChars: correctChars,
    timeSpent: elapsedSeconds,
    wpm: wpm,
  );
}

5. 数据持久化

使用SharedPreferences保存记录:

dart 复制代码
Future<void> _loadRecords() async {
  final prefs = await SharedPreferences.getInstance();
  final recordsData = prefs.getStringList('typing_records') ?? [];
  setState(() {
    records = recordsData
        .map((json) => TypingRecord.fromJson(jsonDecode(json)))
        .toList();
  });
}

Future<void> _saveRecords() async {
  final prefs = await SharedPreferences.getInstance();
  final recordsData = records.map((r) => jsonEncode(r.toJson())).toList();
  await prefs.setStringList('typing_records', recordsData);
}

UI组件设计

1. 统计卡片

显示学习概览:

dart 复制代码
Widget _buildStatsCard() {
  final totalPractices = records.length;
  final avgWpm = records.isEmpty
      ? 0.0
      : records.fold<double>(0, (sum, r) => sum + r.wpm) / records.length;

  return Card(
    child: Container(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [Colors.purple.shade400, Colors.purple.shade600],
        ),
      ),
      child: Column(
        children: [
          const Text('学习统计'),
          Row(
            children: [
              _buildStatItem('练习次数', '$totalPractices'),
              _buildStatItem('平均速度', '${avgWpm.toStringAsFixed(0)} WPM'),
            ],
          ),
        ],
      ),
    ),
  );
}

2. 练习页面

实时显示打字进度:

dart 复制代码
Widget build(BuildContext context) {
  return Scaffold(
    body: Column(
      children: [
        // 进度显示
        Row(
          children: [
            _buildProgressItem('进度', '$currentIndex / ${targetText.length}'),
            _buildProgressItem('速度', '${_calculateCurrentWpm()} WPM'),
            _buildProgressItem('准确率', '${_calculateAccuracy()}%'),
          ],
        ),
        
        // 进度条
        LinearProgressIndicator(
          value: currentIndex / targetText.length,
        ),
        
        // 目标文本(彩色标注)
        RichText(
          text: TextSpan(
            style: TextStyle(fontSize: 24),
            children: _buildTextSpans(),
          ),
        ),
        
        // 输入框
        TextField(
          controller: _inputController,
          focusNode: _focusNode,
          enabled: !isFinished,
        ),
      ],
    ),
  );
}

3. 进步曲线

可视化展示学习进度:

dart 复制代码
Widget _buildProgressChart() {
  final recentRecords = records.take(10).toList().reversed.toList();

  return SizedBox(
    height: 200,
    child: ListView.builder(
      scrollDirection: Axis.horizontal,
      itemCount: recentRecords.length,
      itemBuilder: (context, index) {
        final record = recentRecords[index];
        final maxWpm = recentRecords.map((r) => r.wpm).reduce((a, b) => a > b ? a : b);
        final height = (record.wpm / maxWpm * 150).clamp(20.0, 150.0);
        
        return Column(
          children: [
            Text('${record.wpm.toStringAsFixed(0)}'),
            Container(
              width: 30,
              height: height,
              decoration: BoxDecoration(
                color: Colors.purple,
                borderRadius: BorderRadius.circular(4),
              ),
            ),
            Text('${index + 1}'),
          ],
        );
      },
    ),
  );
}

应用架构

页面结构

TypingHomePage

主页面
练习页

HomePage
记录页

RecordsPage
统计页

StatisticsPage
模式选择
难度选择
TypingPracticePage

练习页面
ResultPage

结果页面
练习记录列表
总体统计
模式统计
进步曲线

数据流

正确
错误


用户选择模式和难度
生成练习文本
开始计时
用户输入
实时比对
字符匹配?
绿色标注
红色标注
完成?
停止计时
计算WPM和准确率
生成记录
保存到本地
显示结果

功能扩展建议

1. 盲打模式

隐藏输入内容,训练盲打:

dart 复制代码
class BlindTypingMode extends StatefulWidget {
  @override
  State<BlindTypingMode> createState() => _BlindTypingModeState();
}

class _BlindTypingModeState extends State<BlindTypingMode> {
  bool showInput = false;
  
  @override
  Widget build(BuildContext context) {
    return TextField(
      obscureText: !showInput,  // 隐藏输入
      decoration: InputDecoration(
        suffixIcon: IconButton(
          icon: Icon(showInput ? Icons.visibility : Icons.visibility_off),
          onPressed: () {
            setState(() {
              showInput = !showInput;
            });
          },
        ),
      ),
    );
  }
}

2. 键盘热力图

显示按键使用频率:

dart 复制代码
class KeyboardHeatmap extends StatelessWidget {
  final Map<String, int> keyPressCount;
  
  @override
  Widget build(BuildContext context) {
    final maxCount = keyPressCount.values.isEmpty 
        ? 1 
        : keyPressCount.values.reduce((a, b) => a > b ? a : b);
    
    return GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 10,
      ),
      itemCount: 26,
      itemBuilder: (context, index) {
        final key = String.fromCharCode(65 + index);  // A-Z
        final count = keyPressCount[key] ?? 0;
        final intensity = count / maxCount;
        
        return Container(
          margin: EdgeInsets.all(2),
          decoration: BoxDecoration(
            color: Colors.red.withOpacity(intensity),
            borderRadius: BorderRadius.circular(4),
          ),
          child: Center(child: Text(key)),
        );
      },
    );
  }
}

3. 自定义文本

允许用户输入自定义练习文本:

dart 复制代码
Future<void> _showCustomTextDialog() async {
  final controller = TextEditingController();
  
  await showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('自定义文本'),
      content: TextField(
        controller: controller,
        maxLines: 5,
        decoration: InputDecoration(
          hintText: '输入要练习的文本',
          border: OutlineInputBorder(),
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () {
            if (controller.text.isNotEmpty) {
              Navigator.pop(context);
              _startCustomPractice(controller.text);
            }
          },
          child: const Text('开始'),
        ),
      ],
    ),
  );
}

4. 文章导入

从文件导入练习文本:

dart 复制代码
// 使用 file_picker 包
import 'package:file_picker/file_picker.dart';

Future<void> _importTextFile() async {
  final result = await FilePicker.platform.pickFiles(
    type: FileType.custom,
    allowedExtensions: ['txt'],
  );
  
  if (result != null) {
    final file = File(result.files.single.path!);
    final content = await file.readAsString();
    
    setState(() {
      targetText = content;
    });
  }
}

5. 多人竞赛

添加多人对战模式:

dart 复制代码
class MultiplayerMode extends StatefulWidget {
  @override
  State<MultiplayerMode> createState() => _MultiplayerModeState();
}

class _MultiplayerModeState extends State<MultiplayerMode> {
  Map<String, PlayerProgress> players = {};
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 玩家进度列表
        ...players.entries.map((entry) {
          return ListTile(
            leading: CircleAvatar(child: Text(entry.key[0])),
            title: Text(entry.key),
            trailing: Text('${entry.value.progress}%'),
            subtitle: LinearProgressIndicator(
              value: entry.value.progress / 100,
            ),
          );
        }),
      ],
    );
  }
}

class PlayerProgress {
  final int progress;
  final int wpm;
  final double accuracy;
  
  PlayerProgress({
    required this.progress,
    required this.wpm,
    required this.accuracy,
  });
}

6. 声音反馈

添加按键音效:

dart 复制代码
// 使用 audioplayers 包
import 'package:audioplayers/audioplayers.dart';

class SoundFeedback {
  final AudioPlayer _player = AudioPlayer();
  
  Future<void> playKeySound(bool isCorrect) async {
    final sound = isCorrect ? 'correct.mp3' : 'wrong.mp3';
    await _player.play(AssetSource(sound));
  }
  
  Future<void> playFinishSound() async {
    await _player.play(AssetSource('finish.mp3'));
  }
}

7. 成就系统

添加打字成就:

dart 复制代码
enum TypingAchievement {
  firstPractice,      // 首次练习
  speed50,            // 速度达到50 WPM
  speed100,           // 速度达到100 WPM
  accuracy95,         // 准确率95%
  accuracy100,        // 准确率100%
  practice10,         // 练习10次
  practice100,        // 练习100次
  marathon,           // 连续练习30分钟
}

void _checkAchievements(TypingRecord record) {
  if (record.wpm >= 50) {
    _unlockAchievement(TypingAchievement.speed50);
  }
  if (record.wpm >= 100) {
    _unlockAchievement(TypingAchievement.speed100);
  }
  if (record.accuracy >= 95) {
    _unlockAchievement(TypingAchievement.accuracy95);
  }
  if (record.accuracy == 100) {
    _unlockAchievement(TypingAchievement.accuracy100);
  }
}

8. 每日挑战

每日更新挑战文本:

dart 复制代码
class DailyChallenge {
  static String getDailyText() {
    final today = DateTime.now();
    final seed = today.year * 10000 + today.month * 100 + today.day;
    final random = Random(seed);
    
    final texts = [
      '今日挑战文本1',
      '今日挑战文本2',
      // ... 更多文本
    ];
    
    return texts[random.nextInt(texts.length)];
  }
  
  static bool hasCompletedToday(List<TypingRecord> records) {
    final today = DateTime.now();
    return records.any((r) =>
      r.date.year == today.year &&
      r.date.month == today.month &&
      r.date.day == today.day
    );
  }
}

9. 错误分析

分析常见错误:

dart 复制代码
class ErrorAnalysis {
  Map<String, int> errorCount = {};
  
  void recordError(String expected, String actual) {
    final key = '$expected->$actual';
    errorCount[key] = (errorCount[key] ?? 0) + 1;
  }
  
  List<MapEntry<String, int>> getTopErrors(int count) {
    final sorted = errorCount.entries.toList()
      ..sort((a, b) => b.value.compareTo(a.value));
    return sorted.take(count).toList();
  }
  
  Widget buildErrorReport() {
    final topErrors = getTopErrors(10);
    
    return Card(
      child: Column(
        children: [
          const Text('常见错误', style: TextStyle(fontSize: 18)),
          ...topErrors.map((entry) {
            final parts = entry.key.split('->');
            return ListTile(
              title: Text('${parts[0]} 误输入为 ${parts[1]}'),
              trailing: Text('${entry.value} 次'),
            );
          }),
        ],
      ),
    );
  }
}

10. 导出报告

生成PDF练习报告:

dart 复制代码
// 使用 pdf 包
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';

Future<void> exportReport(List<TypingRecord> records) async {
  final pdf = pw.Document();
  
  pdf.addPage(
    pw.Page(
      build: (context) {
        return pw.Column(
          crossAxisAlignment: pw.CrossAxisAlignment.start,
          children: [
            pw.Text('打字练习报告', style: pw.TextStyle(fontSize: 24)),
            pw.SizedBox(height: 20),
            pw.Text('总练习次数: ${records.length}'),
            pw.Text('平均速度: ${_calculateAvgWpm(records)} WPM'),
            pw.Text('平均准确率: ${_calculateAvgAccuracy(records)}%'),
            pw.SizedBox(height: 20),
            pw.Text('练习记录:', style: pw.TextStyle(fontSize: 18)),
            ...records.map((r) {
              return pw.Text(
                '${_formatDate(r.date)} - ${r.wpm.toStringAsFixed(1)} WPM - ${r.accuracy.toStringAsFixed(1)}%'
              );
            }),
          ],
        );
      },
    ),
  );
  
  await Printing.layoutPdf(
    onLayout: (format) async => pdf.save(),
  );
}

性能优化建议

1. 文本渲染优化

使用CustomPainter优化大文本渲染:

dart 复制代码
class TextHighlightPainter extends CustomPainter {
  final String text;
  final String input;
  
  TextHighlightPainter({required this.text, required this.input});
  
  @override
  void paint(Canvas canvas, Size size) {
    final textPainter = TextPainter(
      textDirection: TextDirection.ltr,
    );
    
    for (int i = 0; i < text.length; i++) {
      final color = i < input.length
          ? (input[i] == text[i] ? Colors.green : Colors.red)
          : Colors.grey;
      
      textPainter.text = TextSpan(
        text: text[i],
        style: TextStyle(color: color, fontSize: 24),
      );
      
      textPainter.layout();
      textPainter.paint(canvas, Offset(i * 15.0, 0));
    }
  }
  
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

2. 输入防抖

避免频繁更新UI:

dart 复制代码
Timer? _debounceTimer;

void _onInputChangedDebounced() {
  _debounceTimer?.cancel();
  _debounceTimer = Timer(const Duration(milliseconds: 50), () {
    _onInputChanged();
  });
}

3. 使用Isolate处理统计

将复杂计算移到后台线程:

dart 复制代码
Future<Map<String, dynamic>> _calculateStatsInBackground(
  List<TypingRecord> records,
) async {
  return await compute(_calculateStats, records);
}

Map<String, dynamic> _calculateStats(List<TypingRecord> records) {
  // 复杂的统计计算
  return {
    'avgWpm': records.fold<double>(0, (sum, r) => sum + r.wpm) / records.length,
    'avgAccuracy': records.fold<double>(0, (sum, r) => sum + r.accuracy) / records.length,
    // ... 更多统计
  };
}

测试建议

1. 单元测试

测试WPM计算:

dart 复制代码
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('WPM Calculation Tests', () {
    test('Calculate WPM correctly', () {
      final words = 20;
      final seconds = 60;
      final wpm = (words / (seconds / 60)).toDouble();
      
      expect(wpm, 20.0);
    });
    
    test('Accuracy calculation', () {
      final total = 100;
      final correct = 95;
      final accuracy = (correct / total * 100);
      
      expect(accuracy, 95.0);
    });
  });
}

2. Widget测试

测试输入监听:

dart 复制代码
testWidgets('Input listener works', (WidgetTester tester) async {
  await tester.pumpWidget(
    MaterialApp(
      home: TypingPracticePage(
        mode: PracticeMode.english,
        difficulty: DifficultyLevel.easy,
        onComplete: (_) {},
      ),
    ),
  );
  
  final textField = find.byType(TextField);
  expect(textField, findsOneWidget);
  
  await tester.enterText(textField, 'test');
  await tester.pump();
  
  // 验证输入被正确处理
});

部署发布

1. 应用图标

yaml 复制代码
dev_dependencies:
  flutter_launcher_icons: ^0.13.1

flutter_launcher_icons:
  android: true
  ios: true
  image_path: "assets/icon.png"

2. 版本管理

yaml 复制代码
version: 1.0.0+1

3. 打包

bash 复制代码
# Android
flutter build apk --release
flutter build appbundle --release

# iOS
flutter build ios --release

项目总结

通过开发这个打字练习应用,你将掌握:

  • Flutter文本处理和渲染
  • 实时输入监听和反馈
  • RichText高级用法
  • 性能优化技巧
  • 数据统计和可视化
  • 用户体验设计

这个应用不仅能帮助用户提高打字技能,还展示了Flutter在教育类应用开发中的强大能力。继续扩展功能,让打字练习变得更有趣!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
猛扇赵四那边好嘴.2 小时前
Flutter 框架跨平台鸿蒙开发 - 录音工具应用开发教程
flutter·华为·harmonyos
世人万千丶2 小时前
鸿蒙跨端框架 Flutter 学习 iverpod 实战:超越 Provider 的响应式状态管理
学习·flutter·华为·交互·harmonyos·鸿蒙
猛扇赵四那边好嘴.2 小时前
Flutter 框架跨平台鸿蒙开发 - 学习打卡助手应用开发教程
学习·flutter·华为·harmonyos
哈哈你是真的厉害2 小时前
基础入门 React Native 鸿蒙跨平台开发:TextInput 输入框
react native·react.js·harmonyos
晚霞的不甘2 小时前
Flutter for OpenHarmony 实战:[开发环境搭建与项目编译指南]
git·flutter·react native·react.js·elasticsearch·visual studio code
熊猫钓鱼>_>2 小时前
【开源鸿蒙跨平台开发先锋训练营】[Day 3] React Native for OpenHarmony 实战:网络请求集成与高健壮性列表构建
大数据·人工智能·react native·华为·开源·harmonyos·智能体
小白阿龙3 小时前
鸿蒙+flutter 跨平台开发——自定义单词速记APP开发实战
flutter·华为·harmonyos·鸿蒙
2401_zq136y033 小时前
Flutter for OpenHarmony:从零搭建今日资讯App(二十三)数据模型设计的艺术
flutter
前端不太难3 小时前
Flutter / RN / iOS,在状态重构容忍度上的本质差异
flutter·ios·重构