2. 共享业务逻辑层与平台适配层拆分

为什么需要拆分共享逻辑与平台适配

跨平台项目的核心矛盾是:业务规则(登录校验、数据格式、错误码)在所有平台完全一致,而平台能力(安全存储、网络栈、文件路径、弹窗)各不相同。如果把这些混在一个文件里,Android 的改动会污染 iOS 与 Web。正确做法是依赖倒置:共享层只定义"我需要什么能力"的抽象接口,各平台提供最小实现。

共享业务逻辑层与平台适配层依赖关系 共享层 shared/(纯 Dart / 纯 JS,零平台依赖) AuthService login() / logout() 校验账号密码格式 Validator isEmail() / isPhone() 纯函数,无副作用 抽象接口(契约) TokenStorage HttpClient 依赖 平台适配层 adapters/(每个平台最小实现,仅实现接口) Android EncryptedSharedPrefs OkHttp Context.filesDir AndroidTokenStorage iOS Keychain URLSession NSDocumentDirectory IosTokenStorage Web localStorage fetch / XMLHttpRequest IndexedDB WebTokenStorage Desktop 文件加密存储 dart:io HttpClient path_provider DesktopTokenStorage 实现(implements)

共享层:定义契约与纯业务逻辑

共享层不引用任何平台 API。它包含两类内容:纯业务逻辑(可直接运行、可单元测试)和抽象接口(声明平台必须提供的能力)。以登录为例,共享层定义如下契约:

// shared/auth/token_storage.dart —— 抽象接口,无平台依赖
abstract class TokenStorage {
  Future<String?> read(String key);
  Future<void> write(String key, String value);
  Future<void> delete(String key);
}

// shared/auth/auth_service.dart —— 纯业务逻辑
class AuthService {
  final TokenStorage _storage;
  final HttpClient _http;

  AuthService(this._storage, this._http);

  Future<LoginResult> login(String account, String password) async {
    // 1. 本地校验(纯逻辑,不依赖平台)
    if (!Validator.isEmail(account) && !Validator.isPhone(account)) {
      return LoginResult.fail('账号格式不正确');
    }
    if (password.length < 8) {
      return LoginResult.fail('密码至少 8 位');
    }
    // 2. 调用网络(通过抽象接口)
    final resp = await _http.post('/api/login', {
      'account': account,
      'password': password,
    });
    if (resp.statusCode != 200) {
      return LoginResult.fail('登录失败:${resp.statusCode}');
    }
    // 3. 持久化 token(通过抽象接口)
    await _storage.write('token', resp.body['token']);
    return LoginResult.ok(resp.body['user']);
  }
}

// shared/validator.dart —— 纯函数,零依赖
class Validator {
  static final _emailRe = RegExp(r'^[\w.+-]+@[\w-]+\.[\w.]+$');
  static final _phoneRe = RegExp(r'^1[3-9]\d{9}$');
  static bool isEmail(String s) => _emailRe.hasMatch(s);
  static bool isPhone(String s) => _phoneRe.hasMatch(s);
}
共享层的关键约束:不 import 任何平台包(如 dart:io、dart:html、package:flutter)。这样它才能同时被移动端、Web 和桌面端复用,也才能在纯 Dart VM 中直接跑单元测试。

平台适配层:每个平台只写最小实现

适配层只做一件事:把抽象接口映射到平台原生能力。每个平台通常只需 20~50 行代码。以下展示 Android 与 Web 两个典型实现:

// adapters/android/token_storage_android.dart
import 'package:shared_preferences/shared_preferences.dart';
import '../../shared/auth/token_storage.dart';

class AndroidTokenStorage implements TokenStorage {
  @override
  Future<String?> read(String key) async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(key);
  }
  @override
  Future<void> write(String key, String value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(key, value);
  }
  @override
  Future<void> delete(String key) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(key);
  }
}

// adapters/web/token_storage_web.dart
import 'dart:html' as html;
import '../../shared/auth/token_storage.dart';

class WebTokenStorage implements TokenStorage {
  @override
  Future<String?> read(String key) async => html.window.localStorage[key];
  @override
  Future<void> write(String key, String value) async {
    html.window.localStorage[key] = value;
  }
  @override
  Future<void> delete(String key) async {
    html.window.localStorage.remove(key);
  }
}

组装:在入口处注入平台实现

共享层不关心谁实现了接口,由各平台入口负责"接线"。这是依赖注入最简单的形式——构造函数注入:

// main_android.dart
void main() {
  final auth = AuthService(AndroidTokenStorage(), AndroidHttpClient());
  runApp(App(auth: auth));
}

// main_web.dart
void main() {
  final auth = AuthService(WebTokenStorage(), WebHttpClient());
  runApp(App(auth: auth));
}

验证拆分是否成功

检查项通过标准验证方式
共享层零平台依赖shared/ 下无 dart:io、dart:html、flutter 导入grep 搜索 import 语句
业务逻辑可独立测试AuthService 用 Mock 实现即可跑通纯 Dart VM 运行单元测试
适配层足够薄每个平台实现类不超过 60 行代码行数统计
新增平台成本低只需新增一个 adapters/xxx/ 目录不改动 shared/ 任何文件
拆分后的收益:登录逻辑只写一次,Android 换用 EncryptedSharedPreferences 时只改 AndroidTokenStorage,iOS 和 Web 完全不受影响;新增桌面端时只需补一个 DesktopTokenStorage,共享层零改动。