Flutter for OpenHarmony 跨平台开发:计算器功能实战指南

Flutter for OpenHarmony 跨平台开发:计算器功能实战指南

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


一、引言

计算器是移动设备中最基础且常用的应用之一,其开发涉及用户交互、状态管理、数学运算等多个技术领域。随着鸿蒙生态的快速发展,如何高效地实现跨平台计算器应用,成为开发者关注的技术要点。

Flutter作为Google推出的开源UI框架,凭借其跨平台能力和丰富的组件生态,为计算器功能的实现提供了便捷的技术方案。Flutter for OpenHarmony的出现,使得Flutter开发者能够将应用部署到鸿蒙设备,进一步拓展了跨平台开发的应用范围。

本文将以计算器功能为例,详细介绍如何使用Flutter for OpenHarmony实现标准计算和科学计算功能,包括状态管理、表达式处理、数学运算等核心技术,为开发者提供完整的技术参考。


二、技术背景

2.1 Flutter for OpenHarmony概述

Flutter是Google于2017年发布的开源UI框架,采用Dart语言进行开发。Flutter通过Skia渲染引擎实现自绘,不依赖平台原生组件,从而保证了不同平台上UI的一致性。

OpenHarmony是由开放原子开源基金会孵化的开源操作系统项目,旨在构建万物智联的操作系统生态。Flutter for OpenHarmony是Flutter在OpenHarmony平台上的适配实现,使Flutter开发者能够将应用无缝部署到鸿蒙设备。

2.2 计算器的技术架构

实现计算器功能涉及以下核心技术:

状态管理:管理显示屏内容、当前操作符、操作数等状态,确保计算逻辑正确执行。

表达式处理:处理用户输入的数字和操作符,构建并解析计算表达式。

数学运算:实现基本四则运算和科学计算函数。

UI布局:使用GridView等组件构建按钮网格布局。

2.3 Flutter与原生鸿蒙开发的对比

对比维度 Flutter for OpenHarmony 原生鸿蒙开发(ArkTS)
编程语言 Dart ArkTS
数学库 dart:math功能完善 需要手动实现
UI布局 GridView组件便捷 需要手动布局
跨平台能力 支持多平台 仅限鸿蒙平台
开发效率 热重载支持 需要重新编译

三、功能设计

3.1 需求分析

计算器功能的核心需求包括:

  1. 标准模式:支持加、减、乘、除四则运算,百分比计算,正负号切换
  2. 科学模式:支持三角函数、对数、幂运算、阶乘等高级运算
  3. 历史记录:保存并显示最近的计算记录
  4. 表达式显示:实时显示当前计算表达式
  5. 输入控制:支持退格删除、清除重置等操作

3.2 架构设计

本功能采用Flutter的状态管理模式进行架构设计:

  • 数据层:使用String存储显示内容,double存储操作数,List存储历史记录
  • 逻辑层:实现数字输入、运算符处理、计算执行等业务逻辑
  • 视图层:构建显示区域、历史记录、按钮网格等UI组件

3.3 界面设计

界面分为两种展示模式:

标准模式:

  • 显示区域:表达式 + 计算结果
  • 历史记录:横向滚动列表
  • 模式切换:标准/科学切换按钮
  • 按钮区域:4×5网格布局

科学模式:

  • 科学按钮区:5×3网格布局
  • 标准按钮区:4×5网格布局
  • 支持更多数学函数

四、核心实现

4.1 状态变量设计

使用以下状态变量管理计算器状态:

dart 复制代码
// 显示屏内容
String _display = '0';

// 当前表达式
String _expression = '';

// 第一个操作数
double? _firstOperand;

// 当前操作符
String? _operator;

// 是否等待第二个操作数
bool _waitingForSecondOperand = false;

// 是否科学模式
bool _isScientific = false;

// 计算历史记录
final List<String> _history = [];

4.2 数字输入处理

当用户按下数字按钮时的处理逻辑:

dart 复制代码
void _onNumberPressed(String number) {
  setState(() {
    if (_waitingForSecondOperand) {
      // 等待第二个操作数时,替换当前显示
      _display = number;
      _waitingForSecondOperand = false;
    } else {
      // 追加数字到当前显示
      _display = _display == '0' ? number : _display + number;
    }
  });
}

4.3 运算符处理

当用户按下运算符时的处理逻辑:

dart 复制代码
void _onOperatorPressed(String op) {
  setState(() {
    // 如果已有操作符且正在输入第二个操作数,先执行计算
    if (_operator != null && !_waitingForSecondOperand) {
      _calculate();
    }
    // 保存第一个操作数和操作符
    _firstOperand = double.parse(_display);
    _operator = op;
    _expression = '$_display $op';
    _waitingForSecondOperand = true;
  });
}

4.4 计算逻辑实现

核心计算逻辑的实现:

dart 复制代码
void _calculate() {
  if (_firstOperand == null || _operator == null) return;
  
  double secondOperand = double.parse(_display);
  double result = 0;
  
  switch (_operator!) {
    case '+':
      result = _firstOperand! + secondOperand;
      break;
    case '-':
      result = _firstOperand! - secondOperand;
      break;
    case '×':
      result = _firstOperand! * secondOperand;
      break;
    case '÷':
      result = secondOperand != 0 
          ? _firstOperand! / secondOperand 
          : double.infinity;
      break;
    case '%':
      result = _firstOperand! % secondOperand;
      break;
    case '^':
      result = math.pow(_firstOperand!, secondOperand).toDouble();
      break;
  }
  
  // 添加到历史记录
  String historyItem = '$_firstOperand $_operator $secondOperand = $result';
  
  setState(() {
    _display = _formatResult(result);
    _expression = '';
    _operator = null;
    _firstOperand = null;
    _history.insert(0, historyItem);
    if (_history.length > 10) _history.removeLast();
  });
}

4.5 科学计算函数

科学计算功能的实现,使用dart:math库:

dart 复制代码
void _onScientificOp(String op) {
  double value = double.parse(_display);
  double result = 0;
  
  switch (op) {
    case 'sin':
      result = math.sin(value * math.pi / 180); // 角度转弧度
      break;
    case 'cos':
      result = math.cos(value * math.pi / 180);
      break;
    case 'tan':
      result = math.tan(value * math.pi / 180);
      break;
    case 'log':
      result = math.log(value); // 自然对数
      break;
    case 'ln':
      result = math.log(value);
      break;
    case '√':
      result = math.sqrt(value);
      break;
    case 'x²':
      result = value * value;
      break;
    case '1/x':
      result = 1 / value;
      break;
    case '!':
      result = _factorial(value.toInt()).toDouble();
      break;
    case 'π':
      result = math.pi;
      break;
    case 'e':
      result = math.e;
      break;
  }
  
  setState(() {
    _display = _formatResult(result);
  });
}

4.6 结果格式化

计算结果的格式化处理:

dart 复制代码
String _formatResult(double result) {
  // 如果是整数,显示整数形式
  if (result == result.toInt()) {
    return result.toInt().toString();
  }
  // 否则显示小数,去除尾部多余的0
  return result
      .toStringAsFixed(8)
      .replaceAll(RegExp(r'0+$'), '')
      .replaceAll(RegExp(r'\.$'), '');
}

五、完整代码实现

dart 复制代码
import 'package:flutter/material.dart';
import 'dart:math' as math;

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

  @override
  State<CalculatorFeature> createState() => _CalculatorFeatureState();
}

class _CalculatorFeatureState extends State<CalculatorFeature> {
  String _display = '0';
  String _expression = '';
  double? _firstOperand;
  String? _operator;
  bool _waitingForSecondOperand = false;
  bool _isScientific = false;
  final List<String> _history = [];

  void _onNumberPressed(String number) {
    setState(() {
      if (_waitingForSecondOperand) {
        _display = number;
        _waitingForSecondOperand = false;
      } else {
        _display = _display == '0' ? number : _display + number;
      }
    });
  }

  void _onOperatorPressed(String op) {
    setState(() {
      if (_operator != null && !_waitingForSecondOperand) {
        _calculate();
      }
      _firstOperand = double.parse(_display);
      _operator = op;
      _expression = '$_display $op';
      _waitingForSecondOperand = true;
    });
  }

  void _calculate() {
    if (_firstOperand == null || _operator == null) return;
    double secondOperand = double.parse(_display);
    double result = 0;
    String op = _operator!;

    switch (op) {
      case '+':
        result = _firstOperand! + secondOperand;
        break;
      case '-':
        result = _firstOperand! - secondOperand;
        break;
      case '×':
        result = _firstOperand! * secondOperand;
        break;
      case '÷':
        result = secondOperand != 0 ? _firstOperand! / secondOperand : double.infinity;
        break;
      case '%':
        result = _firstOperand! % secondOperand;
        break;
      case '^':
        result = math.pow(_firstOperand!, secondOperand).toDouble();
        break;
    }

    String historyItem = '$_firstOperand $op $secondOperand = $result';

    setState(() {
      _display = _formatResult(result);
      _expression = '';
      _operator = null;
      _firstOperand = null;
      _history.insert(0, historyItem);
      if (_history.length > 10) _history.removeLast();
    });
  }

  String _formatResult(double result) {
    if (result == result.toInt()) {
      return result.toInt().toString();
    }
    return result.toStringAsFixed(8).replaceAll(RegExp(r'0+$'), '').replaceAll(RegExp(r'\.$'), '');
  }

  void _onScientificOp(String op) {
    double value = double.parse(_display);
    double result = 0;

    switch (op) {
      case 'sin':
        result = math.sin(value * math.pi / 180);
        break;
      case 'cos':
        result = math.cos(value * math.pi / 180);
        break;
      case 'tan':
        result = math.tan(value * math.pi / 180);
        break;
      case 'log':
        result = math.log(value);
        break;
      case 'ln':
        result = math.log(value);
        break;
      case '√':
        result = math.sqrt(value);
        break;
      case 'x²':
        result = value * value;
        break;
      case '1/x':
        result = 1 / value;
        break;
      case '!':
        result = _factorial(value.toInt()).toDouble();
        break;
      case 'π':
        result = math.pi;
        break;
      case 'e':
        result = math.e;
        break;
    }

    setState(() {
      _display = _formatResult(result);
    });
  }

  int _factorial(int n) {
    if (n <= 1) return 1;
    return n * _factorial(n - 1);
  }

  void _onClear() {
    setState(() {
      _display = '0';
      _expression = '';
      _firstOperand = null;
      _operator = null;
      _waitingForSecondOperand = false;
    });
  }

  void _onBackspace() {
    setState(() {
      if (_display.length > 1) {
        _display = _display.substring(0, _display.length - 1);
      } else {
        _display = '0';
      }
    });
  }

  void _onDecimal() {
    if (_waitingForSecondOperand) {
      setState(() {
        _display = '0.';
        _waitingForSecondOperand = false;
      });
    } else if (!_display.contains('.')) {
      setState(() => _display += '.');
    }
  }

  void _onPlusMinus() {
    setState(() {
      if (_display != '0') {
        if (_display.startsWith('-')) {
          _display = _display.substring(1);
        } else {
          _display = '-$_display';
        }
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _buildDisplay(),
        if (_history.isNotEmpty) _buildHistory(),
        const Divider(height: 1),
        _buildModeSwitch(),
        Expanded(
          child: _isScientific ? _buildScientificPad() : _buildBasicPad(),
        ),
      ],
    );
  }

  Widget _buildDisplay() {
    return Container(
      padding: const EdgeInsets.all(20),
      alignment: Alignment.centerRight,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          Text(
            _expression,
            style: const TextStyle(fontSize: 20, color: Colors.grey),
          ),
          const SizedBox(height: 8),
          SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            child: Text(
              _display,
              style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildHistory() {
    return Container(
      height: 60,
      padding: const EdgeInsets.symmetric(horizontal: 12),
      child: ListView.builder(
        scrollDirection: Axis.horizontal,
        itemCount: _history.length,
        itemBuilder: (context, index) {
          return Container(
            margin: const EdgeInsets.only(right: 8),
            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
            decoration: BoxDecoration(
              color: Colors.grey.shade100,
              borderRadius: BorderRadius.circular(8),
            ),
            child: Center(
              child: Text(_history[index], style: const TextStyle(fontSize: 12)),
            ),
          );
        },
      ),
    );
  }

  Widget _buildModeSwitch() {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
      child: Row(
        children: [
          ChoiceChip(
            label: const Text('标准'),
            selected: !_isScientific,
            onSelected: (selected) => setState(() => _isScientific = false),
          ),
          const SizedBox(width: 8),
          ChoiceChip(
            label: const Text('科学'),
            selected: _isScientific,
            onSelected: (selected) => setState(() => _isScientific = true),
          ),
        ],
      ),
    );
  }

  Widget _buildBasicPad() {
    return GridView.count(
      crossAxisCount: 4,
      children: [
        _buildButton('C', Colors.grey.shade400, _onClear),
        _buildButton('⌫', Colors.grey.shade400, _onBackspace),
        _buildButton('%', Colors.grey.shade400, () => _onOperatorPressed('%')),
        _buildButton('÷', Colors.orange, () => _onOperatorPressed('÷')),
        _buildButton('7', Colors.white, () => _onNumberPressed('7')),
        _buildButton('8', Colors.white, () => _onNumberPressed('8')),
        _buildButton('9', Colors.white, () => _onNumberPressed('9')),
        _buildButton('×', Colors.orange, () => _onOperatorPressed('×')),
        _buildButton('4', Colors.white, () => _onNumberPressed('4')),
        _buildButton('5', Colors.white, () => _onNumberPressed('5')),
        _buildButton('6', Colors.white, () => _onNumberPressed('6')),
        _buildButton('-', Colors.orange, () => _onOperatorPressed('-')),
        _buildButton('1', Colors.white, () => _onNumberPressed('1')),
        _buildButton('2', Colors.white, () => _onNumberPressed('2')),
        _buildButton('3', Colors.white, () => _onNumberPressed('3')),
        _buildButton('+', Colors.orange, () => _onOperatorPressed('+')),
        _buildButton('±', Colors.white, _onPlusMinus),
        _buildButton('0', Colors.white, () => _onNumberPressed('0')),
        _buildButton('.', Colors.white, _onDecimal),
        _buildButton('=', Colors.orange, _calculate),
      ],
    );
  }

  Widget _buildScientificPad() {
    return Column(
      children: [
        Expanded(
          child: GridView.count(
            crossAxisCount: 5,
            children: [
              _buildSciButton('sin'),
              _buildSciButton('cos'),
              _buildSciButton('tan'),
              _buildSciButton('log'),
              _buildSciButton('ln'),
              _buildSciButton('√'),
              _buildSciButton('x²'),
              _buildSciButton('^'),
              _buildSciButton('1/x'),
              _buildSciButton('!'),
              _buildSciButton('π'),
              _buildSciButton('e'),
              _buildSciButton('('),
              _buildSciButton(')'),
              _buildSciButton('C', isControl: true),
            ],
          ),
        ),
        Container(
          height: 200,
          child: _buildBasicPad(),
        ),
      ],
    );
  }

  Widget _buildSciButton(String text, {bool isControl = false}) {
    return Container(
      margin: const EdgeInsets.all(2),
      child: ElevatedButton(
        style: ElevatedButton.styleFrom(
          backgroundColor: isControl ? Colors.grey.shade400 : Colors.blue.shade100,
          foregroundColor: isControl ? Colors.black : Colors.blue,
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
        ),
        onPressed: () => _onScientificOp(text),
        child: Text(text, style: const TextStyle(fontSize: 14)),
      ),
    );
  }

  Widget _buildButton(String text, Color color, VoidCallback onPressed) {
    return Container(
      margin: const EdgeInsets.all(4),
      child: ElevatedButton(
        style: ElevatedButton.styleFrom(
          backgroundColor: color,
          foregroundColor: color == Colors.white ? Colors.black : Colors.white,
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
        ),
        onPressed: onPressed,
        child: Text(text, style: const TextStyle(fontSize: 24)),
      ),
    );
  }
}

六、运行效果


七、关键技术点解析

7.1 GridView构建按钮网格

Flutter的GridView组件非常适合构建计算器按钮网格:

dart 复制代码
GridView.count(
  crossAxisCount: 4,  // 4列布局
  children: [
    _buildButton('C', Colors.grey.shade400, _onClear),
    _buildButton('⌫', Colors.grey.shade400, _onBackspace),
    // ... 更多按钮
  ],
)

使用crossAxisCount参数设置列数,GridView会自动计算行数并进行布局,无需手动计算位置。

7.2 dart:math科学计算

Flutter内置的dart:math库提供了丰富的数学函数:

dart 复制代码
import 'dart:math' as math;

// 三角函数(注意:参数为弧度,需将角度转换为弧度)
math.sin(value * math.pi / 180)
math.cos(value * math.pi / 180)
math.tan(value * math.pi / 180)

// 其他数学函数
math.sqrt(value)      // 平方根
math.pow(base, exp)   // 幂运算
math.log(value)       // 自然对数

// 数学常量
math.pi  // 圆周率 π
math.e   // 自然对数底 e

需要注意的是,dart:math中的三角函数参数为弧度值,如果用户输入的是角度,需要进行转换。

7.3 ChoiceChip模式切换

ChoiceChip是Material Design的选择芯片组件,适用于单选场景:

dart 复制代码
ChoiceChip(
  label: const Text('标准'),
  selected: !_isScientific,
  onSelected: (selected) => setState(() => _isScientific = false),
)

通过selected属性控制选中状态,onSelected回调处理选择事件,实现简洁的模式切换功能。

7.4 OpenHarmony平台适配要点

在OpenHarmony设备上运行Flutter应用,需要注意以下配置:

  1. 签名配置:需要在DevEco Studio中配置应用签名
  2. 数学运算:dart:math库在鸿蒙平台完全兼容,无需额外适配
  3. 触摸交互:使用ElevatedButton处理触摸事件,确保交互响应正常

八、总结与展望

本文详细介绍了使用Flutter for OpenHarmony开发计算器功能的完整过程。通过合理的状态管理、清晰的计算逻辑、规范的UI组件构建,实现了一个功能完善、交互友好的计算器模块。

技术要点回顾:

  • 使用GridView构建按钮网格布局
  • 使用dart:math库实现科学计算功能
  • 使用ChoiceChip实现模式切换
  • 使用List存储历史记录
  • 实现结果格式化处理

扩展方向:

  • 表达式解析:支持复杂表达式如(1+2)*3
  • 主题切换:支持明暗主题切换
  • 语音播报:计算结果语音播报功能
  • 单位换算:集成单位换算功能
相关推荐
2601_949970691 天前
HumanizeDuration 跨端三方库仓颉适配全流程
前端·javascript·flutter
传奇开心果编程1 天前
【Flutter踩坑排雷】第3课:const 误用坑|const 错误使用引发 UI 异常与性能损耗
flutter·移动开发·跨平台·dart·编程语言理论·编程框架·声明式ui
程序员老刘1 天前
你的Flutter项目要不要紧急升级3.47.8?
flutter·客户端
Talyra421 天前
不想开会员,我写了一个记账 App
flutter
2601_949575861 天前
JSON5 跨端三方库仓颉适配全流程
前端·javascript·flutter
法的空间1 天前
AI 编程时代,为什么 Dart 分析会拖慢大型项目的开发
android·flutter·ios
恋猫de小郭1 天前
Flutter 紧急发布 3.47.8 安全升级版本,建议都升级
android·前端·flutter
好的佩奇2 天前
Dart 之模式
android·flutter·dart
淡淡的香烟2 天前
Flutter实现多语言切换
android·flutter·1024程序员节
事圆则缓2 天前
从零搭建一套 Flutter 架构:先跑通数据流
flutter