Flutter 中的 SOLID 原则实用指南:S - 单一职责原则(SRP)篇

185 阅读4分钟

在软件设计中,SOLID 是五大面向对象设计原则的缩写,其中的 "S" 代表 单一职责原则(Single Responsibility Principle,SRP)。在实际 Flutter 开发中,如何落地 SRP,往往是新手迈向架构进阶的第一步。

本文将通过违反与遵循 SRP 的真实案例,逐步优化,并结合 Riverpod,展示如何在实际项目中应用 SRP 原则。

什么是 SRP?

单一职责原则:一个类应该仅有一个引起它变化的原因。

换句话说,一个类只负责一个功能,如果该功能发生变化,只应影响这个类。

🧨 违反 SRP 的示例

class LoginPage extends StatelessWidget {
  final emailController = TextEditingController();
  final passwordController = TextEditingController();

  Future<void> _login(BuildContext context) async {
    final email = emailController.text;
    final password = passwordController.text;

    if (email.isEmpty || !email.contains('@')) {
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('邮箱格式错误')));
      return;
    }

    if (password.length < 6) {
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('密码至少6位')));
      return;
    }

    final response = await http.post(
      Uri.parse('https://api.example.com/login'),
      body: {'email': email, 'password': password},
    );

    if (response.statusCode == 200) {
      Navigator.pushReplacementNamed(context, '/home');
    } else {
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('登录失败')));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(controller: emailController),
        TextField(controller: passwordController),
        ElevatedButton(onPressed: () => _login(context), child: Text('登录')),
      ],
    );
  }
}

😵 问题点

  • UI 控制器集成了:UI 展示、输入验证、网络请求、导航跳转,职责混乱。
  • 难以复用、测试困难、修改任何功能都可能牵一发动全身。

✅ 初步重构:不使用 Riverpod,仅做职责分离

我们首先把验证与网络逻辑从 UI 中拆分出去。

login_validator.dart

class LoginValidator {
  String? validate(String email, String password) {
    if (email.isEmpty || !email.contains('@')) return '邮箱格式错误';
    if (password.length < 6) return '密码至少6位';
    return null;
  }
}

auth_service.dart

class AuthService {
  Future<bool> login(String email, String password) async {
    final response = await http.post(
      Uri.parse('https://api.example.com/login'),
      body: {'email': email, 'password': password},
    );
    return response.statusCode == 200;
  }
}

login_page.dart

class LoginPage extends StatefulWidget {
  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final emailController = TextEditingController();
  final passwordController = TextEditingController();
  final validator = LoginValidator();
  final authService = AuthService();
  bool isLoading = false;

  Future<void> _login() async {
    final error = validator.validate(
      emailController.text,
      passwordController.text,
    );
    if (error != null) {
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error)));
      return;
    }

    setState(() => isLoading = true);
    final success = await authService.login(
      emailController.text,
      passwordController.text,
    );
    setState(() => isLoading = false);

    if (success) {
      Navigator.pushReplacementNamed(context, '/home');
    } else {
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('登录失败')));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(controller: emailController),
        TextField(controller: passwordController, obscureText: true),
        ElevatedButton(
          onPressed: isLoading ? null : _login,
          child: isLoading ? CircularProgressIndicator() : Text('登录'),
        ),
      ],
    );
  }
}

✅ 优点

  • UI 仅负责展示,验证和请求逻辑被分离,结构更清晰。
  • 依旧没有使用状态管理工具,适合中小页面。

🚀 更进一步:结合 Riverpod,彻底实现 SRP + 响应式状态管理

我们将项目结构优化如下:

lib/
├── features/login/
│   ├── presentation/      // UI 层
│   ├── application/       // 状态/控制器层
│   ├── domain/            // 验证/模型层
│   └── infrastructure/    // 网络服务层

1. domain/login_result.dart

sealed class LoginResult {
  const LoginResult();
}

class LoginSuccess extends LoginResult {
  final String token;
  const LoginSuccess(this.token);
}

class LoginError extends LoginResult {
  final String message;
  const LoginError(this.message);
}

2. application/login_controller.dart

final loginControllerProvider =
    StateNotifierProvider<LoginController, AsyncValue<LoginResult?>>(
  (ref) => LoginController(
    validator: LoginValidator(),
    authService: AuthService(),
  ),
);

class LoginController extends StateNotifier<AsyncValue<LoginResult?>> {
  final LoginValidator validator;
  final AuthService authService;

  LoginController({required this.validator, required this.authService})
      : super(const AsyncValue.data(null));

  Future<void> login(String email, String password) async {
    final error = validator.validate(email, password);
    if (error != null) {
      state = AsyncValue.data(LoginError(error));
      return;
    }

    state = const AsyncValue.loading();
    try {
      final token = await authService.login(email, password);
      state = AsyncValue.data(LoginSuccess(token));
    } catch (_) {
      state = AsyncValue.data(LoginError('登录失败,请稍后再试'));
    }
  }
}

3. presentation/login_page.dart

class LoginPage extends ConsumerStatefulWidget {
  @override
  ConsumerState<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends ConsumerState<LoginPage> {
  final emailController = TextEditingController();
  final passwordController = TextEditingController();

  void _onLogin() {
    ref.read(loginControllerProvider.notifier).login(
          emailController.text,
          passwordController.text,
        );
  }

  @override
  Widget build(BuildContext context) {
    final loginState = ref.watch(loginControllerProvider);
    final result = loginState.value;
    final isLoading = loginState.isLoading;

    return Column(
      children: [
        TextField(controller: emailController),
        TextField(controller: passwordController, obscureText: true),
        if (result is LoginError)
          Text(result.message, style: TextStyle(color: Colors.red)),
        ElevatedButton(
          onPressed: isLoading ? null : _onLogin,
          child: isLoading ? CircularProgressIndicator() : Text('登录'),
        ),
      ],
    );
  }
}

✅ SRP + Riverpod 的优势总结

优点描述
职责单一每层专注于一个目标:验证、请求、UI、状态管理分别负责
可测试性高各层可独立 mock 和单元测试
易维护与扩展新增需求时不会牵一发动全身
状态响应式Riverpod 自动响应状态变化,简洁清晰

💡 思考题:设计可扩展的登录流程

假设未来你需要扩展如下功能:

  • 第三方登录(Google、Apple)
  • 登录成功后拉取用户资料
  • 登录失败上报日志或打点统计

🤔 你会将这些逻辑分别放在哪一层?

  • 都放在 Controller 会不会导致职责再次变得臃肿?
  • AuthService 是否需要切换为抽象接口以支持更多登录方式?

📌 总结

在 Flutter 项目中,应用 SRP 原则能够极大地提升代码的可维护性和扩展性。我们先通过基本的逻辑拆分做出初步优化,再结合 Riverpod 实现响应式、职责清晰的架构。

下一次,当你在一个 Widget 中写出 200 行处理表单、验证、网络、跳转的逻辑时,不妨停下来问问自己:

"这个类是不是做太多事了?"

如果你觉得本文对你有帮助,欢迎分享、点赞支持,让更多开发者写出优雅、可维护的 Flutter 代码。