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

前言

在跨平台开发领域,Flutter 凭借高效渲染与多端适配能力成为主流;鸿蒙化三方库大幅降低开发成本;开源鸿蒙(OpenHarmony)提供稳定的国产系统运行环境。

本文将带你零基础开发一款课程表App,使用已完成鸿蒙适配的稳定三方库,实现添加课程、周视图展示、本地存储等功能,并完成鸿蒙平台移植,全程可一步步跟着操作。


一、项目功能

  • 课程表一周视图展示
  • 添加课程(名称、教室、星期、节次)
  • 本地持久化存储(重启不丢失)
  • 界面简洁、适配鸿蒙
  • 无白屏、无崩溃、可正常运行

二、技术栈(全部鸿蒙化,无违规库)

库名 功能 鸿蒙适配
flutter 主框架
provider 状态管理 ✅ 鸿蒙化
shared_preferences 本地存储 ✅ 鸿蒙化
intl 时间工具 ✅ 鸿蒙化

三、开发环境

  • Flutter 3.10+
  • VS Code / Android Studio
  • DevEco Studio(鸿蒙运行)
  • 项目路径纯英文、无中文、无空格

四、第一步:创建项目

flutter create timetable
cd timetable

五、第二步:添加依赖(pubspec.yaml)

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.1.2
  shared_preferences: ^2.2.3
  intl: ^0.19.0

执行安装:

flutter pub get

六、第三步:编写数据模型(lib/course_model.dart)

class Course {
  final String id;
  final String name; // 课程名
  final String room; // 教室
  final int weekDay; // 星期 1-7
  final int timeIndex; // 节次 1、2、3...

  Course({
    required this.id,
    required this.name,
    required this.room,
    required this.weekDay,
    required this.timeIndex,
  });

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'room': room,
      'weekDay': weekDay,
      'timeIndex': timeIndex,
    };
  }

  static Course fromJson(Map<String, dynamic> json) {
    return Course(
      id: json['id'],
      name: json['name'],
      room: json['room'],
      weekDay: json['weekDay'],
      timeIndex: json['timeIndex'],
    );
  }
}

七、第四步:状态管理 + 存储(lib/course_provider.dart)

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import 'course_model.dart';

class CourseProvider extends ChangeNotifier {
  List<Course> _courses = [];
  SharedPreferences? _prefs;

  List<Course> get courses => _courses;

  Future<void> initProvider() async {
    try {
      _prefs = await SharedPreferences.getInstance();
      _loadCourses();
    } catch (e) {
      print("存储初始化异常:$e");
    }
  }

  void _loadCourses() {
    if (_prefs == null) return;
    final data = _prefs!.getString("courses");
    if (data != null) {
      try {
        final list = jsonDecode(data) as List;
        _courses = list.map((e) => Course.fromJson(e)).toList();
      } catch (e) {
        _courses = [];
      }
    }
    notifyListeners();
  }

  Future<void> _save() async {
    if (_prefs == null) return;
    final jsonList = _courses.map((e) => e.toJson()).toList();
    await _prefs!.setString("courses", jsonEncode(jsonList));
    notifyListeners();
  }

  Future<void> addCourse(Course course) async {
    _courses.add(course);
    await _save();
  }

  Future<void> deleteCourse(String id) async {
    _courses.removeWhere((c) => c.id == id);
    await _save();
  }

  List<Course> getCoursesByWeekDay(int weekday) {
    return _courses.where((c) => c.weekDay == weekday).toList();
  }
}

八、第五步:主页面 + 完整UI(lib/main.dart)

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'course_model.dart';
import 'course_provider.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (ctx) => CourseProvider(),
      child: MaterialApp(
        title: "课程表",
        debugShowCheckedModeBanner: false,
        theme: ThemeData(primarySwatch: Colors.blue),
        home: const TimetablePage(),
      ),
    );
  }
}

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

  
  State<TimetablePage> createState() => _TimetablePageState();
}

class _TimetablePageState extends State<TimetablePage> {
  bool _loading = true;
  final _nameController = TextEditingController();
  final _roomController = TextEditingController();
  int _selectedWeekday = 1;
  int _selectedTime = 1;

  
  void initState() {
    super.initState();
    _init();
  }

  Future<void> _init() async {
    final provider = Provider.of<CourseProvider>(context, listen: false);
    await provider.initProvider();
    if (mounted) {
      setState(() => _loading = false);
    }
  }

  void _showAddDialog() {
    _nameController.clear();
    _roomController.clear();
    _selectedWeekday = 1;
    _selectedTime = 1;

    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text("添加课程"),
        content: SingleChildScrollView(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextField(
                controller: _nameController,
                decoration: const InputDecoration(labelText: "课程名称"),
              ),
              TextField(
                controller: _roomController,
                decoration: const InputDecoration(labelText: "教室"),
              ),
              const SizedBox(height: 10),
              DropdownButton<int>(
                value: _selectedWeekday,
                items: List.generate(7, (i) => i + 1)
                    .map((e) => DropdownMenuItem(
                          value: e,
                          child: Text("星期$e"),
                        ))
                    .toList(),
                onChanged: (v) {
                  if (v != null) setState(() => _selectedWeekday = v);
                },
              ),
              DropdownButton<int>(
                value: _selectedTime,
                items: List.generate(8, (i) => i + 1)
                    .map((e) => DropdownMenuItem(
                          value: e,
                          child: Text("第$e节"),
                        ))
                    .toList(),
                onChanged: (v) {
                  if (v != null) setState(() => _selectedTime = v);
                },
              ),
            ],
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx),
            child: const Text("取消"),
          ),
          TextButton(
            onPressed: () async {
              final provider = Provider.of<CourseProvider>(context, listen: false);
              if (_nameController.text.isNotEmpty) {
                await provider.addCourse(Course(
                  id: DateTime.now().millisecondsSinceEpoch.toString(),
                  name: _nameController.text,
                  room: _roomController.text,
                  weekDay: _selectedWeekday,
                  timeIndex: _selectedTime,
                ));
              }
              Navigator.pop(ctx);
            },
            child: const Text("确定"),
          ),
        ],
      ),
    );
  }

  
  Widget build(BuildContext context) {
    final provider = Provider.of<CourseProvider>(context);

    return Scaffold(
      appBar: AppBar(
        title: const Text("课程表"),
        actions: [
          IconButton(
            icon: const Icon(Icons.add),
            onPressed: _showAddDialog,
          ),
        ],
      ),
      body: _loading
          ? const Center(child: CircularProgressIndicator())
          : ListView(
              padding: const EdgeInsets.all(8),
              children: List.generate(7, (weekIndex) {
                final day = weekIndex + 1;
                final list = provider.getCoursesByWeekDay(day);
                return Card(
                  margin: const EdgeInsets.only(bottom: 10),
                  child: Padding(
                    padding: const EdgeInsets.all(12),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          "星期$day",
                          style: const TextStyle(
                              fontSize: 18, fontWeight: FontWeight.bold),
                        ),
                        const Divider(),
                        list.isEmpty
                            ? const Text("  无课程")
                            : Column(
                                children: list.map((c) {
                                  return ListTile(
                                    title: Text(c.name),
                                    subtitle: Text("教室:${c.room} | 第${c.timeIndex}节"),
                                    trailing: IconButton(
                                      icon: const Icon(Icons.delete, color: Colors.red),
                                      onPressed: () async {
                                        await provider.deleteCourse(c.id);
                                      },
                                    ),
                                  );
                                }).toList(),
                              ),
                      ],
                    ),
                  ),
                );
              }),
            ),
    );
  }
}

九、第六步:运行

flutter clean
flutter pub get
flutter run

功能正常:

  • 打开不白屏
  • 点击右上角 + 添加课程
  • 选择星期、节次、教室
  • 课程自动展示
  • 长按删除(点击垃圾桶)
  • 重启App数据不丢失

十、第七步:鸿蒙移植(解决 hvigor 报错)

1. 确保路径纯英文

2. 执行

flutter create --platforms=harmony .
flutter run

3. 鸿蒙运行成功

所有三方库均已鸿蒙化,无编译错误。
在这里插入图片描述
在这里插入图片描述


十一、常见问题

1. 白屏

解决:异步初始化放到页面加载完成后,使用 Loading 状态。

2. 添加课程没反应

解决:按钮必须使用 await,provider 必须刷新。

3. 鸿蒙编译报错 hvigor

解决:路径纯英文 + 清理缓存 + 重新生成鸿蒙工程。


十二、总结

本文通过 Flutter + 鸿蒙化三方库 完成了课程表App开发,实现了多端兼容与鸿蒙平台运行,展示了跨平台开发在鸿蒙生态中的高效实践。

欢迎大家加入开源鸿蒙跨平台开发者社区


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐