在软件设计中,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 代码。