本文将开发一个轻量记事本应用,采用“Flutter负责UI与业务逻辑,鸿蒙原生负责本地存储”的架构,完整覆盖从工程搭建、跨端通信到功能调试的全流程。最终实现“新建笔记、查看列表、编辑笔记、删除笔记”核心功能,适配鸿蒙设备特性。

前置环境要求:已配置Flutter for OpenHarmony环境(含专属SDK)、DevEco Studio 4.0+(API Version 10)、鸿蒙模拟器/真机(API 9+)

Flutter + DevEco Studio 记事本开发实战大纲

开发环境准备
  • Flutter SDK 安装与环境变量配置
  • DevEco Studio 安装与鸿蒙开发工具链配置
  • 跨平台兼容性检查:确保 Flutter 支持鸿蒙设备
项目初始化与配置
  • 使用 flutter create 创建项目
  • 在 DevEco Studio 中导入 Flutter 项目
  • 配置鸿蒙应用的 config.json 文件(如权限声明)
UI 界面设计
  • 使用 Flutter Widget 构建记事本主界面
    • ListView 展示笔记列表
    • FloatingActionButton 实现添加功能
  • 适配鸿蒙系统 UI 规范(如字体、间距)
  • 暗黑模式与多主题支持
数据存储方案
  • 使用 sqflite 插件实现本地数据库存储
  • 鸿蒙端数据持久化兼容性处理
  • 数据模型定义与 CRUD 操作封装
功能模块实现
  • 笔记编辑页面:TextFormField 与富文本支持
  • 时间戳与排序功能
  • 搜索功能:基于标题或内容的模糊查询
鸿蒙特性集成
  • 调用鸿蒙原生能力(如系统通知提醒)
  • 使用 channel 实现 Flutter 与鸿蒙原生代码通信
  • 鸿蒙分布式能力探索(可选)
调试与测试
  • Flutter 热重载在 DevEco Studio 中的应用
  • 鸿蒙设备/模拟器上的运行测试
  • 跨平台行为一致性验证
性能优化与发布
  • 应用包体积分析及优化策略
  • 鸿蒙应用签名与上架流程
  • Flutter 产物集成到鸿蒙项目的最终打包
扩展方向
  • 云同步功能(结合鸿蒙帐号体系)
  • 多端协同场景下的记事本共享
  • 鸿蒙原子化服务探索

一、工程架构设计与环境准备

1. 核心架构逻辑

采用“Flutter前端+鸿蒙原生存储”的混合架构,优势在于:Flutter实现跨端统一UI,鸿蒙原生API确保本地存储稳定性(利用鸿蒙分布式数据管理能力)。核心交互流程:

  1. Flutter通过MethodChannel调用鸿蒙原生接口;

  2. 鸿蒙端实现笔记的增删改查(基于Preferences存储轻量数据);

  3. 原生端将数据结果通过通道返回给Flutter,驱动UI更新。

2. 工程创建步骤

需分别创建鸿蒙工程与Flutter模块,再建立关联,确保包名一致(避免通信失败)。

步骤1:创建鸿蒙Stage工程(DevEco Studio)
  1. 打开DevEco Studio,选择“Create Project”,模板选“Empty Ability”;

  2. 配置工程信息: Project Name:OHOSNotepad

  3. Bundle Name:com.example.ohos_notepad(核心,需与Flutter一致)

  4. Save Location:自定义路径

  5. Compile SDK Version:API 10

  6. Model:Stage

  7. 点击“Finish”,等待工程初始化完成。

步骤2:创建Flutter模块(VS Code/Android Studio)
  1. 打开终端,执行命令创建Flutter模块(路径建议与鸿蒙工程同级): # 注意flutter_ohos是适配鸿蒙的命令,需安装专属SDK flutter_ohos create -t module flutter_notepad_module

  2. 进入Flutter模块目录,修改pubspec.yaml,确保包名一致:

        name: ohos_notepad
    description: A Flutter Notepad for OpenHarmony
    environment:
      sdk: '>=3.0.0 <4.0.0'
    dependencies:
      flutter:
        sdk: flutter
      # 用于日期格式化
      intl: ^0.18.1

  3. 执行flutter pub get安装依赖。

步骤3:关联Flutter模块与鸿蒙工程

  1. 在DevEco Studio中,右键鸿蒙工程根目录 → New → Module,选择“Flutter Module”;

  2. 在弹出的配置窗口中,“Flutter Module Path”选择步骤2创建的`flutter_notepad_module`路径,点击“Finish”;

  3. DevEco Studio会自动在鸿蒙工程的`oh-package.json5`中添加Flutter依赖,检查配置是否生效:

     // 鸿蒙工程oh-package.json5
    {
      "dependencies": {
        "flutter_notepad_module": "file:../flutter_notepad_module" // 自动生成的依赖
      }
    }

  4. 执行“Sync Project”同步依赖,确保无报错(若提示“Flutter SDK not found”,需在DevEco Studio → Settings → Flutter中配置专属SDK路径)。

步骤4:配置Flutter页面入口(鸿蒙端)

需将Flutter页面嵌入鸿蒙的Ability中,作为应用入口:

3. 常见问题排查

问题现象

解决方案

Flutter页面加载空白

1. 检查Flutter模块依赖是否同步;2. 确认`FlutterPage`的pageName与Flutter路由一致;3. 执行`flutter clean`清除缓存

存储失败,提示“权限拒绝”

1. 检查`module.json5`权限配置是否正确;2. 在设备“设置→应用→记事本→权限”中手动开启存储权限

通信失败,提示“Method not found”

1. 核对Flutter与鸿蒙端的通道名称、方法名是否完全一致;2. 确认NoteChannel已在Ability中初始化

四、功能扩展与打包上架

1. 功能扩展建议

2. 打包鸿蒙应用

总结

本实战通过“Flutter UI+鸿蒙原生存储”的架构,实现了记事本核心功能,核心亮点在于:

通过本案例可掌握Flutter与鸿蒙融合开发的核心流程,后续可基于此架构扩展更复杂的应用场景。若在调试中遇到具体问题,可针对场景补充日志或代码细节进一步排查。

  1. 打开鸿蒙工程`entry/src/main/ets/pages/Index.ets`,替换为以下代码:

      import router from '@ohos.router';
    import { FlutterPage } from '@flutter.notepad_module'; // 引入Flutter模块
    
    @Entry
    @Component
    struct Index {
      build() {
        // 鸿蒙页面仅作为入口,直接加载Flutter页面
        FlutterPage({
          bundleName: 'com.example.ohos_notepad',
          moduleName: 'flutter_notepad_module',
          pageName: 'notepad_main' // 对应Flutter中定义的路由名称
        })
      }
    }

    在鸿蒙工程`entry/src/main/ets/ability/EntryAbility.ets`中,初始化Flutter引擎:

      import Ability from '@ohos.application.Ability';
    import { FlutterAbility } from '@flutter.notepad_module';
    
    export default class EntryAbility extends Ability {
      onCreate(want, launchParam) {
        // 初始化Flutter能力
        FlutterAbility.init(this.context);
        super.onCreate(want, launchParam);
      }
    
      onDestroy() {
        // 销毁Flutter引擎,避免内存泄漏
        FlutterAbility.destroy();
        super.onDestroy();
      }
    }

    二、核心功能实现:跨端通信与数据交互

    1. 定义通信协议(统一两端交互规则)

    提前约定MethodChannel通道名称、方法名与参数格式,避免通信混乱:

    通道名称

    方法名

    参数格式

    返回值

    com.example.notepad/channel

    addNote

    {title: string, content: string, time: string}

    bool(是否成功)

    getAllNotes

    无参数

    List<Map>(笔记列表)

    updateNote

    {index: int, title: string, content: string, time: string}

    bool(是否成功)

    deleteNote

    {index: int}

    bool(是否成功)

    2. 鸿蒙端:实现本地存储与通信接口

    基于鸿蒙Preferences实现轻量数据存储,封装通信监听逻辑:

    步骤1:创建笔记存储服务(NoteStorage.ets)
    import dataPreferences from '@ohos.data.preferences';
    
    // 单例模式:确保存储实例唯一
    export class NoteStorage {
      private static instance: NoteStorage;
      private preferences: dataPreferences.Preferences | null = null;
    
      // 初始化存储(应用启动时调用)
      async init() {
        this.preferences = await dataPreferences.getPreferences(this.context, 'note_storage');
      }
    
      // 添加笔记
      async addNote(note: {title: string, content: string, time: string}): Promise<boolean> {
        if (!this.preferences) return false;
        // 获取现有笔记列表
        let notes = await this.getNotes();
        notes.push(note);
        // 存储到Preferences
        await this.preferences.put('notes', JSON.stringify(notes));
        await this.preferences.flush();
        return true;
      }
    
      // 获取所有笔记
      async getNotes(): Promise<Array<{title: string, content: string, time: string}>> {
        if (!this.preferences) return [];
        const notesStr = await this.preferences.get('notes', '[]');
        return JSON.parse(notesStr as string);
      }
    
      // 更新笔记
      async updateNote(index: number, note: {title: string, content: string, time: string}): Promise<boolean> {
        if (!this.preferences) return false;
        let notes = await this.getNotes();
        if (index < 0 || index >= notes.length) return false;
        notes[index] = note;
        await this.preferences.put('notes', JSON.stringify(notes));
        await this.preferences.flush();
        return true;
      }
    
      // 删除笔记
      async deleteNote(index: number): Promise<boolean> {
        if (!this.preferences) return false;
        let notes = await this.getNotes();
        if (index < 0 || index >= notes.length) return false;
        notes.splice(index, 1);
        await this.preferences.put('notes', JSON.stringify(notes));
        await this.preferences.flush();
        return true;
      }
    
      // 单例获取方法
      static getInstance(context): NoteStorage {
        if (!this.instance) {
          this.instance = new NoteStorage();
          this.instance.context = context;
        }
        return this.instance;
      }
    }
    步骤2:创建通信监听服务(NoteChannel.ets)
    import { FlutterMethodChannel } from '@hmscore.flutter-ohos-adapter';
    import { NoteStorage } from './NoteStorage';
    
    export class NoteChannel {
      private static channel: FlutterMethodChannel;
      private static noteStorage: NoteStorage;
    
      // 初始化通道与存储
      static init(ability) {
        // 1. 初始化通信通道(与Flutter端名称一致)
        this.channel = new FlutterMethodChannel('com.example.notepad/channel');
        // 2. 初始化存储服务
        this.noteStorage = NoteStorage.getInstance(ability.context);
        this.noteStorage.init();
        // 3. 监听Flutter方法调用
        this.setMethodHandler();
      }
    
      // 方法调用处理逻辑
      private static setMethodHandler() {
        this.channel.setMethodCallHandler(async (method, params) => {
          switch (method) {
            case 'addNote':
              return this.noteStorage.addNote(params);
            case 'getAllNotes':
              return this.noteStorage.getNotes();
            case 'updateNote':
              return this.noteStorage.updateNote(params.index, params.note);
            case 'deleteNote':
              return this.noteStorage.deleteNote(params.index);
            default:
              return Promise.reject('未实现的方法:' + method);
          }
        });
      }
    }
    步骤3:注册服务到Ability

    修改`EntryAbility.ets`,添加通信服务初始化:

    import Ability from '@ohos.application.Ability';
    import { FlutterAbility } from '@flutter.notepad_module';
    import { NoteChannel } from '../pages/NoteChannel'; // 引入通信服务
    
    export default class EntryAbility extends Ability {
      onCreate(want, launchParam) {
        FlutterAbility.init(this.context);
        NoteChannel.init(this); // 初始化笔记通信服务
        super.onCreate(want, launchParam);
      }
    
      onDestroy() {
        FlutterAbility.destroy();
        super.onDestroy();
      }
    }

    3. Flutter端:实现UI与通信调用

    基于Flutter实现笔记列表、编辑页面,通过MethodChannel调用鸿蒙原生接口。

    步骤1:封装通信工具类(note_channel.dart)
    import 'package:flutter/services.dart';
    
    // 笔记数据模型
    class Note {
      final String title;
      final String content;
      final String time;
    
      Note({
        required this.title,
        required this.content,
        required this.time,
      });
    
      // 转换为Map,用于传递给鸿蒙端
      Map<String, dynamic> toMap() {
        return {
          'title': title,
          'content': content,
          'time': time,
        };
      }
    
      // 从Map解析为Note对象
      static Note fromMap(Map<dynamic, dynamic> map) {
        return Note(
          title: map['title'] ?? '',
          content: map['content'] ?? '',
          time: map['time'] ?? '',
        );
      }
    }
    
    // 通信工具类
    class NoteChannel {
      // 与鸿蒙端一致的通道名称
      static const _channel = MethodChannel('com.example.notepad/channel');
    
      // 单例模式
      static final NoteChannel _instance = NoteChannel._internal();
      factory NoteChannel() => _instance;
      NoteChannel._internal();
    
      // 1. 添加笔记
      Future<bool> addNote(Note note) async {
        try {
          return await _channel.invokeMethod(
            'addNote',
            note.toMap(),
          ) as bool;
        } on PlatformException catch (e) {
          print('添加笔记失败:${e.message}');
          return false;
        }
      }
    
      // 2. 获取所有笔记
      Future<List<Note>> getAllNotes() async {
        try {
          final List<dynamic> result = await _channel.invokeMethod('getAllNotes');
          return result.map((e) => Note.fromMap(e)).toList();
        } on PlatformException catch (e) {
          print('获取笔记失败:${e.message}');
          return [];
        }
      }
    
      // 3. 更新笔记
      Future<bool> updateNote(int index, Note note) async {
        try {
          return await _channel.invokeMethod(
            'updateNote',
            {'index': index, 'note': note.toMap()},
          ) as bool;
        } on PlatformException catch (e) {
          print('更新笔记失败:${e.message}');
          return false;
        }
      }
    
      // 4. 删除笔记
      Future<bool> deleteNote(int index) async {
        try {
          return await _channel.invokeMethod(
            'deleteNote',
            {'index': index},
          ) as bool;
        } on PlatformException catch (e) {
          print('删除笔记失败:${e.message}');
          return false;
        }
      }
    }
    步骤2:实现笔记列表页面(main.dart)
    import 'package:flutter/material.dart';
    import 'package:intl/intl.dart';
    import 'note_channel.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: '鸿蒙记事本',
          theme: ThemeData(primarySwatch: Colors.blue),
          routes: {
            '/': (context) => const NoteListPage(),
            '/edit': (context) => const NoteEditPage(),
          },
          initialRoute: '/',
        );
      }
    }
    
    // 笔记列表页面
    class NoteListPage extends StatefulWidget {
      const NoteListPage({super.key});
    
      @override
      State<NoteListPage> createState() => _NoteListPageState();
    }
    
    class _NoteListPageState extends State<NoteListPage> {
      final NoteChannel _noteChannel = NoteChannel();
      List<Note> _notes = [];
      bool _isLoading = true;
    
      // 加载笔记列表
      Future<void> _loadNotes() async {
        setState(() => _isLoading = true);
        final notes = await _noteChannel.getAllNotes();
        setState(() {
          _notes = notes;
          _isLoading = false;
        });
      }
    
      // 跳转到编辑页面(新建/修改)
      void _gotoEditPage({Note? note, int? index}) {
        Navigator.pushNamed(
          context,
          '/edit',
          arguments: {'note': note, 'index': index},
        ).then((value) {
          // 编辑完成后刷新列表
          if (value == true) _loadNotes();
        });
      }
    
      // 删除笔记
      Future<void> _deleteNote(int index) async {
        final success = await _noteChannel.deleteNote(index);
        if (success) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('删除成功')),
          );
          _loadNotes();
        } else {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('删除失败')),
          );
        }
      }
    
      @override
      void initState() {
        super.initState();
        _loadNotes(); // 页面初始化时加载笔记
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('我的笔记')),
          body: _buildBody(),
          floatingActionButton: FloatingActionButton(
            onPressed: () => _gotoEditPage(), // 新建笔记
            child: const Icon(Icons.add),
          ),
        );
      }
    
      // 构建列表内容
      Widget _buildBody() {
        if (_isLoading) {
          return const Center(child: CircularProgressIndicator());
        }
        if (_notes.isEmpty) {
          return const Center(child: Text('暂无笔记,点击右下角添加'));
        }
        return ListView.builder(
          itemCount: _notes.length,
          itemBuilder: (context, index) {
            final note = _notes[index];
            return ListTile(
              title: Text(
                note.title,
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                style: const TextStyle(fontSize: 18),
              ),
              subtitle: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    note.content,
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                  Text(
                    note.time,
                    style: const TextStyle(color: Colors.grey, fontSize: 12),
                  ),
                ],
              ),
              onTap: () => _gotoEditPage(note: note, index: index), // 编辑笔记
              onLongPress: () => _deleteNote(index), // 长按删除
            );
          },
        );
      }
    }
    步骤3:实现笔记编辑页面(note_edit_page.dart)
    import 'package:flutter/material.dart';
    import 'package:intl/intl.dart';
    import 'note_channel.dart';
    
    class NoteEditPage extends StatefulWidget {
      const NoteEditPage({super.key});
    
      @override
      State<NoteEditPage> createState() => _NoteEditPageState();
    }
    
    class _NoteEditPageState extends State<NoteEditPage> {
      final NoteChannel _noteChannel = NoteChannel();
      final TextEditingController _titleController = TextEditingController();
      final TextEditingController _contentController = TextEditingController();
      Note? _editNote;
      int? _editIndex;
    
      // 保存笔记(新建/更新)
      Future<void> _saveNote() async {
        final title = _titleController.text.trim();
        final content = _contentController.text.trim();
        if (title.isEmpty) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('标题不能为空')),
          );
          return;
        }
        // 获取当前时间(格式化)
        final time = DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now());
        final note = Note(title: title, content: content, time: time);
    
        bool success;
        if (_editNote != null && _editIndex != null) {
          // 更新现有笔记
          success = await _noteChannel.updateNote(_editIndex!, note);
        } else {
          // 新建笔记
          success = await _noteChannel.addNote(note);
        }
    
        if (success) {
          Navigator.pop(context, true); // 返回列表页并刷新
        } else {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('保存失败,请重试')),
          );
        }
      }
    
      // 初始化编辑数据
      void _initEditData() {
        final args = ModalRoute.of(context)?.settings.arguments as Map?;
        if (args != null) {
          _editNote = args['note'] as Note?;
          _editIndex = args['index'] as int?;
          if (_editNote != null) {
            _titleController.text = _editNote!.title;
            _contentController.text = _editNote!.content;
          }
        }
      }
    
      @override
      void initState() {
        super.initState();
        _initEditData();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(_editNote == null ? '新建笔记' : '编辑笔记'),
            actions: [
              TextButton(
                onPressed: _saveNote,
                child: const Text(
                  '保存',
                  style: TextStyle(color: Colors.white, fontSize: 16),
                ),
              )
            ],
          ),
          body: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                // 标题输入框
                TextField(
                  controller: _titleController,
                  decoration: const InputDecoration(
                    hintText: '请输入笔记标题',
                    border: InputBorder.none,
                    hintStyle: TextStyle(fontSize: 20),
                  ),
                  style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
                  maxLines: 1,
                ),
                const Divider(height: 1),
                // 内容输入框
                Expanded(
                  child: TextField(
                    controller: _contentController,
                    decoration: const InputDecoration(
                      hintText: '请输入笔记内容',
                      border: InputBorder.none,
                      hintStyle: TextStyle(color: Colors.grey),
                    ),
                    style: const TextStyle(fontSize: 16),
                    maxLines: null,
                    expands: true,
                  ),
                ),
              ],
            ),
          ),
        );
      }
    
      @override
      void dispose() {
        // 释放资源
        _titleController.dispose();
        _contentController.dispose();
        super.dispose();
      }
    }

    三、权限配置与调试运行

    1. 配置鸿蒙应用权限

    Preferences存储需要“文件访问权限”,在鸿蒙工程`module.json5`中添加配置:

    // entry/src/main/module.json5
    {
      "module": {
        "abilities": [
          {
            "name": ".EntryAbility",
            "permissions": [
              "ohos.permission.READ_USER_STORAGE",
              "ohos.permission.WRITE_USER_STORAGE"
            ],
            "skills": [
              {
                "entities": ["entity.system.home"],
                "actions": ["action.system.home"]
              }
            ]
          }
        ],
        "minAPIVersion": 9,
        "targetAPIVersion": 10
      }
    }

    2. 调试运行步骤

  2. 在DevEco Studio中,连接鸿蒙模拟器或真机(确保已开启开发者模式与USB调试);

  3. 选择“entry”模块作为启动模块,点击“Run”按钮(或快捷键Shift+F10);

  4. 首次运行会自动编译Flutter代码与鸿蒙工程,若提示“权限申请”,在设备上点击“允许”;

  5. 调试技巧: Flutter代码调试:在VS Code中打开Flutter模块,通过“Flutter Attach”连接运行中的应用;

  6. 鸿蒙代码调试:在DevEco Studio中设置断点,通过“Debug”模式运行,查看日志面板;

  7. 通信问题排查:统一使用`print`(Flutter)和`console.log`(鸿蒙)打印日志,在DevEco Studio日志面板查看。

  8. 添加笔记搜索:Flutter端实现搜索框,鸿蒙端添加按标题筛选的接口;

  9. 分布式同步:基于鸿蒙分布式数据库,实现多设备笔记同步;

  10. 在DevEco Studio中,选择“Build→Build APP(s)”;

  11. 选择“Release”模式,配置签名证书(需在华为开发者联盟申请);

  12. 等待编译完成,在`entry/build/outputs/default`目录下获取`.app`安装包;

  13. 上架华为应用市场:按要求填写应用信息,上传`.app`包提交审核。

  14. 跨端通信规范化:统一通道与参数格式,避免交互混乱;

  15. 数据存储本地化:利用鸿蒙Preferences确保数据稳定性;

  16. 工程结构清晰:Flutter与鸿蒙职责分离,便于维护扩展。

  17. 富文本编辑:集成Flutter富文本插件(如`flutter_quill`),支持字体样式、图片插入;

  18. 深色模式:通过MediaQuery获取鸿蒙系统主题,实现明暗主题自动切换。

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐