目的:

通过应用界面来设定网络访问信息,来解决直接写在代码中的问题。同时将配置信息存在手机上,以供下次打开应用是使用。

推荐方案了解

方案概览(简短)

  • 简单方案shared_preferences 存全部字段 + 用 Provider 或 ValueNotifier 暴露运行时设置。优点:实现最简单;缺点:Token 明文存储,安全性差。

  • 推荐混合方案shared_preferences 存 projectID/domain/perPageflutter_secure_storage 存 token;全局用 ChangeNotifier/Provider 或 Riverpod 管理。优点:平衡易用与安全。

  • 复杂/结构化方案hive(可选加密)或 drift(sqlite)用于复杂或大量配置,支持类型、模型和批量查询。优点:性能和结构好;缺点:稍复杂。

  • 只在内存 + 导入/导出: 不自动持久化,只提供导入/导出配置文件(适合安全需求极高,但每次需手动输入或导入的场景)。

如何在应用中结构化(推荐做法)

  • 模型: 建一个 Settings 数据类(包含 projectIDdomaintokenperPage 等),并实现 load()/save() 方法。

  • 全局状态: 使用 ChangeNotifier + Provider(或 StateNotifier + Riverpod)在 runApp 之前加载并注入,其他页面通过 Provider.of / Consumer 获取和修改。

  • 敏感数据token 使用 flutter_secure_storage 保存;非敏感的用 shared_preferences 以便快速读取。

  • 启动流程:

    1. 在 main() 调用 WidgetsFlutterBinding.ensureInitialized()

    2. 异步加载 Settings(从 shared_preferences / secure_storage)。

    3. 将加载好的 Settings 注入到 Provider,然后 runApp(...)

AI 修改要点

1. 我将修改 main.dart 添加 Settings、在 main() 中加载并注入 Provider,并更新 ProfilePage 为可编辑并保存设置;同时更新 pubspec.yaml 添加依赖。现在应用补丁。

2. 现在我将应用补丁:1) 更新 pubspec.yaml 添加依赖;2) 修改 main.dart 添加 Settings、在 main() 加载并注入 Provider,并更新 MirrorPage 与 ProfilePage

3. 我将修改 _MirrorPageState:延迟在首帧后加载数据,并用 Provider 中的 Settings 替代原来的常量。

4. 已完成实现:我在 main.dart 中添加了 Settings(使用 shared_preferences + flutter_secure_storage),在 main() 启动前加载并通过 Provider 注入;更新了 MirrorPage 以使用运行时设置并延迟首帧后加载;将 ProfilePage 改为可编辑并能保存/重置配置。也已更新 pubspec.yaml 添加依赖。

验证运行

发现在安卓平台没有问题(右边截屏),在鸿蒙模拟器上首页显示失败(左边截屏),如下:

鸿蒙运行问题分析

首先怀疑是否有新引进的插件是需要鸿蒙适配的,如下为pubspec.yaml的修改

diff --git a/pubspec.yaml b/pubspec.yaml
index 8bd8f06..b55fc16 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -37,6 +37,9 @@ dependencies:
   http: ^1.6.0
   pull_to_refresh: ^2.0.0
   infinite_scroll_pagination: ^5.1.1
+  shared_preferences: ^2.1.1^M
+  flutter_secure_storage: ^8.0.0^M
+  provider: ^6.0.5^M

 dev_dependencies:
   flutter_test:

对于新追加的三个依赖,这里默认都是使用的上游版本,

默认是没有对鸿蒙的适配的。然后在鸿蒙适配的一览表中,https://gitcode.com/openharmony-tpc/flutter_packages,可以看到有对这些依赖的适配:

尝试如下指定还是不行

  # shared_preferences: ^2.1.1
  shared_preferences:
    git:
      url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
      path: "packages/shared_preferences/shared_preferences"
  # flutter_secure_storage: ^8.0.0
  flutter_secure_storage_ohos:
    git:
      url: https://gitcode.com/openharmony-sig/fluttertpc_flutter_secure_storage.git
      path: flutter_secure_storage_ohos
  # provider: ^6.0.5
  path_provider:
    git:
      url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
      path: "packages/path_provider/path_provider"

如上指定时,Android和web编译会报如下编译错误:

PS D:\flutter\projects\pipeline\lib> flutter build apk    
Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source!
Changing current working directory to: D:\flutter\projects\pipeline
Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source!
Error: Couldn't resolve the package 'flutter_secure_storage' in 'package:flutter_secure_storage/flutter_secure_storage.dart'.
Error: Couldn't resolve the package 'provider' in 'package:provider/provider.dart'.
lib/main.dart:7:8: Error: Not found: 'package:flutter_secure_storage/flutter_secure_storage.dart'
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
       ^
lib/main.dart:8:8: Error: Not found: 'package:provider/provider.dart'
import 'package:provider/provider.dart';
       ^
lib/main.dart:30:9: Error: Type 'FlutterSecureStorage' not found.
  final FlutterSecureStorage _secure = FlutterSecureStorage();
        ^^^^^^^^^^^^^^^^^^^^
lib/main.dart:17:5: Error: Undefined name 'ChangeNotifierProvider'.
    ChangeNotifierProvider.value(
    ^^^^^^^^^^^^^^^^^^^^^^
lib/main.dart:30:9: Error: 'FlutterSecureStorage' isn't a type.
  final FlutterSecureStorage _secure = FlutterSecureStorage();
        ^^^^^^^^^^^^^^^^^^^^
lib/main.dart:30:40: Error: Method not found: 'FlutterSecureStorage'.
  final FlutterSecureStorage _secure = FlutterSecureStorage();
                                       ^^^^^^^^^^^^^^^^^^^^
lib/main.dart:241:24: Error: The getter 'Provider' isn't defined for the type '_MirrorPageState'.
 - '_MirrorPageState' is from 'package:pipeline/main.dart' ('lib/main.dart').
Try correcting the name to the name of an existing getter, or defining a getter or field named 'Provider'.     
      final settings = Provider.of<Settings>(context, listen: false);
                       ^^^^^^^^
lib/main.dart:272:24: Error: The getter 'Provider' isn't defined for the type '_MirrorPageState'.
 - '_MirrorPageState' is from 'package:pipeline/main.dart' ('lib/main.dart').
Try correcting the name to the name of an existing getter, or defining a getter or field named 'Provider'.     
      final settings = Provider.of<Settings>(context, listen: false);
                       ^^^^^^^^
lib/main.dart:454:24: Error: The getter 'Provider' isn't defined for the type '_ProfilePageState'.
 - '_ProfilePageState' is from 'package:pipeline/main.dart' ('lib/main.dart').
Try correcting the name to the name of an existing getter, or defining a getter or field named 'Provider'.     
      final settings = Provider.of<Settings>(context, listen: false);
                       ^^^^^^^^
lib/main.dart:474:22: Error: The getter 'Provider' isn't defined for the type '_ProfilePageState'.
 - '_ProfilePageState' is from 'package:pipeline/main.dart' ('lib/main.dart').
Try correcting the name to the name of an existing getter, or defining a getter or field named 'Provider'.     
    final settings = Provider.of<Settings>(context);
                     ^^^^^^^^
Unhandled exception:
FileSystemException(uri=org-dartlang-untranslatable-uri:package%3Aflutter_secure_storage%2Fflutter_secure_storage.dart; message=StandardFileSystem only supports file:* and data:* URIs)
#0      StandardFileSystem.entityForUri (package:front_end/src/api_prototype/standard_file_system.dart:45)     
#1      asFileUri (package:vm/kernel_front_end.dart:1002)
#2      writeDepfile (package:vm/kernel_front_end.dart:1165)
<asynchronous suspension>
#3      FrontendCompiler.compile (package:frontend_server/frontend_server.dart:729)
<asynchronous suspension>
#4      starter (package:frontend_server/starter.dart:102)
<asynchronous suspension>
#5      main (file:///D:/Code/gitcode/flutter3.35/engine/src/flutter/third_party/dart/pkg/frontend_server/bin/frontend_server_starter.dart:13)
<asynchronous suspension>

Target kernel_snapshot_program failed: Exception


FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:compileFlutterBuildRelease'.
> Process 'command 'D:\flutter\sdks\flutter_flutter\bin\flutter.bat'' finished with non-zero exit value 1      

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 19s
Running Gradle task 'assembleRelease'...                           19.7s
Gradle task assembleRelease failed with exit code 1
PS D:\flutter\projects\pipeline\lib>

不知道该如何修改了,后面继续调查该如何使用适配鸿蒙的插件吧

代码修改(main.dart)

import 包

import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:provider/provider.dart';

main()

初始化,加载settings, 并通过ChangeNotifierProvider将配置信息传递给runApp

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final settings = Settings();
  await settings.load();
  runApp(
    ChangeNotifierProvider.value(
      value: settings,
      child: const MyApp(),
    ),
  );
}

继承于ChangeNotifier的Settings

通过SharedPreferences和FlutterSecureStorage实现对配置信息的加载和保存


class Settings extends ChangeNotifier {
  String projectID = '28894006';
  String domain = 'gitlab.com';
  String token = '';
  int perPage = 20;

  final FlutterSecureStorage _secure = FlutterSecureStorage();

  Future<void> load() async {
    final prefs = await SharedPreferences.getInstance();
    projectID = prefs.getString('projectID') ?? projectID;
    domain = prefs.getString('domain') ?? domain;
    perPage = prefs.getInt('perPage') ?? perPage;
    token = await _secure.read(key: 'token') ?? token;
    notifyListeners();
  }

  Future<void> save() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('projectID', projectID);
    await prefs.setString('domain', domain);
    await prefs.setInt('perPage', perPage);
    await _secure.write(key: 'token', value: token);
    notifyListeners();
  }
}

_MirrorPageState

更新为通过Settings来获取配置信息。

class _MirrorPageState extends State<MirrorPage> {
  ...
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _loadFirstPage();
    });
  }
  ...
  Future<void> _loadFirstPage() async {
    ...
    final settings = Provider.of<Settings>(context, listen: false);
    final perPage = settings.perPage;
    _pageSize = perPage;
    final newItems = await getPipelinesPage(settings.projectID, settings.domain, settings.token, 1, perPage);
 
    ...
  }
  Future<void> _loadMore() async {
    ...
    final settings = Provider.of<Settings>(context, listen: false);
    final newItems = await getPipelinesPage(settings.projectID, settings.domain, settings.token, _page, _pageSize);
    ...
  }
  ...
}

ProfilePage

主要使用了TextEditingController,Provider,ListView,Expanded,TextField,SizedBox,InputDecoration,ElevatedButton,OutlinedButton等类和组件实现了配置页面的显示生成和配置信息的保存及提取。

// 我的页面:编辑并保存 Settings
class ProfilePage extends StatefulWidget {
  ProfilePage({super.key});

  @override
  State<ProfilePage> createState() => _ProfilePageState();
}

class _ProfilePageState extends State<ProfilePage> {
  late TextEditingController _projectController;
  late TextEditingController _domainController;
  late TextEditingController _tokenController;
  late TextEditingController _perPageController;
  bool _inited = false;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (!_inited) {
      final settings = Provider.of<Settings>(context, listen: false);
      _projectController = TextEditingController(text: settings.projectID);
      _domainController = TextEditingController(text: settings.domain);
      _tokenController = TextEditingController(text: settings.token);
      _perPageController = TextEditingController(text: settings.perPage.toString());
      _inited = true;
    }
  }

  @override
  void dispose() {
    _projectController.dispose();
    _domainController.dispose();
    _tokenController.dispose();
    _perPageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final settings = Provider.of<Settings>(context);
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: ListView(
        children: [
          const SizedBox(height: 8),
          TextField(
            controller: _projectController,
            decoration: const InputDecoration(labelText: 'Project ID'),
          ),
          const SizedBox(height: 12),
          TextField(
            controller: _domainController,
            decoration: const InputDecoration(labelText: 'Domain'),
          ),
          const SizedBox(height: 12),
          TextField(
            controller: _tokenController,
            decoration: const InputDecoration(labelText: 'Private Token'),
            obscureText: true,
          ),
          const SizedBox(height: 12),
          TextField(
            controller: _perPageController,
            decoration: const InputDecoration(labelText: 'Per Page'),
            keyboardType: TextInputType.number,
          ),
          const SizedBox(height: 20),
          Row(
            children: [
              Expanded(
                child: ElevatedButton(
                  onPressed: () async {
                    settings.projectID = _projectController.text.trim();
                    settings.domain = _domainController.text.trim();
                    settings.token = _tokenController.text;
                    settings.perPage = int.tryParse(_perPageController.text) ?? settings.perPage;
                    await settings.save();
                    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已保存')));
                  },
                  child: const Text('保存'),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: OutlinedButton(
                  onPressed: () {
                    _projectController.text = settings.projectID;
                    _domainController.text = settings.domain;
                    _tokenController.text = settings.token;
                    _perPageController.text = settings.perPage.toString();
                  },
                  child: const Text('重置'),
                ),
              ),
            ],
          ),
          const SizedBox(height: 20),
          Text('当前配置: project=${settings.projectID}, domain=${settings.domain}, perPage=${settings.perPage}'),
        ],
      ),
    );
  }
}

总结

鸿蒙的适配还是有待完善。

  1. 适配的代码还没有贡献到上游。虽然做了很多努力,也已经做了很多适配,但是还停留在单独维护的代码库里。为开发者的使用增加了一些麻烦。
  2. 代码库的适配方法还不是很明确。对于初学者比较难于发现怎么去使用已经做了鸿蒙适配的代码库。可能有的适配的代码库里已经有使用说明文档关于如何使用,不过如果有一个文档能够将各种情况的使用方法都罗列在一起,应该会给初学者带来很大方便。这里可能也涉及到一些术语概念,容易给初学者造成混乱。比如:
    1. 有些包是在flutter_package中的, 比如 flutter_packages - AtomGit | GitCode
    2. 有些包是单独的库,比如fluttertpc_flutter_secure_storage - AtomGit | GitCode
    3. 如何做到其他平台使用上游库,鸿蒙使用适配库?

Logo

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

更多推荐