Flutter架构设计插件simple_architecture的使用
Flutter架构设计插件simple_architecture的使用
简介
simple_architecture
是一个用于简化 Flutter 应用架构设计的库。通过该库,你可以轻松实现可重用性、功能单元化、配置管理、状态管理和异常处理等功能。以下是详细的介绍和示例。
目标
可重用性
某些应用部分在多个应用中都是通用的,例如认证。这些部分仅需一些特定设置(如 clientId
或 redirectUri
),因此无需为不同的应用编写相同的代码。
通过使用存储库(repository)概念,可以实现这部分代码的重用。虽然存储库定义通常与数据库相关,但同样的概念可以应用于任何进行输入输出操作的部分,例如读写文件、远程调用等。
功能单元
单一职责原则(Single Responsibility Principle)要求每个模块只负责一个任务。为了确保这一点,应将每个功能分解成独立的模块。每个模块应具有独立的实现,并且可以在不同时间开发和测试。
配置
当系统部分是可重用时,通常配置项会有所不同。对于认证系统,可能需要配置 Google Client Id
、Apple Service Id
和 Apple Redirect Uri
等。这些配置可以通过依赖注入系统进行管理。
状态管理
状态管理在 Flutter 中是一个常见的问题。虽然有许多复杂的框架(如 BLoC 和 Riverpod),但状态管理实际上非常简单。通过 ValueNotifier<T>
,我们可以轻松地管理全局和局部状态。
监控
有时我们需要监控某些功能的执行情况,例如异常管理、性能测量和审计日志。这些可以通过 PipelineBehavior
实现,它们会在每次系统调用时添加这些特性。
SOLID 原则
SOLID 原则是许多项目中常见的原则。这些原则包括单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖反转原则。这些原则有助于编写更易于维护和扩展的代码。
实际示例
我们将实现一个完整的认证系统,使用该库及其所有现有概念。
规格
- 认证将仅通过 OAuth 使用 Google 或 Apple 进行。
- 包选择为
sign_in_with_apple
、google_sign_in
和firebase_auth
,但这些包应作为插件实现。 - 业务规则必须可重用于其他未来的应用。
- 认证必须在本地数据库中持久化,记录用户登录的时间和使用的数据(如用户 ID 和认证方法)。
- 认证过程可能需要很长时间,UI 必须报告每个阶段的状态(等待 OAuth 提供商、等待 Firebase、等待数据库等)。
项目结构
├── features
│ ├── AUTH
│ │ ├── domain
│ │ │ ├── AuthServiceCredential.dart
│ │ │ ├── Principal.dart
│ │ │ └── SignInAuthStageNotification.dart
│ │ ├── infrastructure
│ │ │ ├── IAuthService.dart
│ │ │ ├── IAuthRepository.dart
│ │ │ ├── IGoogleOAuthService.dart
│ │ │ ├── IAppleOAuthService.dart
│ │ │ └── AuthRepository.dart
│ │ ├── presentation
│ │ │ ├── LoginPage.dart
│ │ │ └── AuthPage.dart
│ │ ├── settings
│ │ │ └── AuthSettings.dart
│ │ └── states
│ │ └── AuthState.dart
└── infrastructure
├── google_sign_in_service.dart
├── firebase_auth_service.dart
初始化
启动应用的代码如下:
Future<void> main() async {
_registerSettings();
_registerServices();
_registerStates();
_registerHandlers();
_registerPipelines();
await $initializeAsync();
runApp(const App());
}
void _registerSettings() {
$settings.add(
AuthSettings(
googleClientId: DefaultFirebaseOptions.ios.androidClientId!,
appleServiceId: "TODO:",
appleRedirectUri: Uri.parse("https://somewhere"),
isGame: true,
),
);
}
void _registerServices() {
$services.registerBootableSingleton(
(get) => const FirebaseApp(),
);
$services.registerTransient<IAuthService>(
(get) => const FirebaseAuthService(),
);
$services.registerTransient<IAuthRepository>(
(get) => const IsarAuthRepository(),
);
$services.registerTransient<IGoogleOAuthService>(
(get) => GoogleSignInService(authSettings: get<AuthSettings>()),
);
$services.registerTransient<IAppleOAuthService>(
(get) => AppleSignInService(authSettings: get<AuthSettings>()),
);
}
void _registerStates() {
$states.registerState(
(get) => AuthState(authService: get<IAuthService>()),
);
}
void _registerHandlers() {
$mediator.registerRequestHandler(
(get) => SignInRequestHandler(
authService: get<IAuthService>(),
googleOAuthService: get<IGoogleOAuthService>(),
appleOAuthService: get<IAppleOAuthService>(),
authRepository: get<IAuthRepository>(),
),
);
$mediator.registerRequestHandler(
(get) => SignOutRequestHandler(
authService: get<IAuthService>(),
googleOAuthService: get<IGoogleOAuthService>(),
appleOAuthService: get<IAppleOAuthService>(),
authRepository: get<IAuthRepository>(),
),
);
}
void _registerPipelines() {
$mediator.registerPipelineBehavior(
0,
(get) => const ErrorMonitoringPipelineBehavior(),
registerAsTransient: false,
);
$mediator.registerPipelineBehavior(
1000,
(get) => const PerformancePipelineBehavior(),
);
}
登录
这是登录功能的完整代码:
@MappableEnum()
enum SignInFailure {
unknown,
cancelledByUser,
userDisabled,
networkRequestFailed,
notSupported,
}
typedef SignInResponse = Response<Principal?, SignInFailure>;
final class SignInRequest implements IRequest<SignInResponse> {
const SignInRequest(this.authProvider);
final AuthProvider authProvider;
}
final class SignInRequestHandler
implements IRequestHandler<SignInResponse, SignInRequest> {
const SignInRequestHandler({
required IAuthService authService,
required IGoogleOAuthService googleOAuthService,
required IAppleOAuthService appleOAuthService,
required IAuthRepository authRepository,
}) : _authService = authService,
_googleOAuthService = googleOAuthService,
_appleOAuthService = appleOAuthService,
_authRepository = authRepository;
final IAuthService _authService;
final IGoogleOAuthService _googleOAuthService;
final IAppleOAuthService _appleOAuthService;
final IAuthRepository _authRepository;
static const _logger = Logger<SignInRequestHandler>();
[@override](/user/override)
Future<SignInResponse> handle(SignInRequest request) async {
final IOAuthService oAuthService =
request.authProvider == AuthProvider.apple
? _appleOAuthService
: _googleOAuthService;
$mediator.publish(
SignInAuthStageNotification(
request.authProvider == AuthProvider.apple
? AuthStage.signingInWithApple
: AuthStage.signingInWithGoogle,
),
);
_logger.info("Signing in with ${request.authProvider}");
final oAuthResponse = await oAuthService.signIn();
if (oAuthResponse.isFailure) {
const SignInAuthStageNotification(AuthStage.idle);
return SignInResponse.fromFailure(oAuthResponse);
}
$mediator.publish(
const SignInAuthStageNotification(AuthStage.authorizing),
);
_logger.info("Authorizing");
final authResponse = await _authService.signIn(oAuthResponse.value);
if (authResponse.isFailure) {
const SignInAuthStageNotification(AuthStage.idle);
return authResponse;
}
$mediator.publish(
const SignInAuthStageNotification(AuthStage.registering),
);
_logger.info("Persisting");
final repoResponse = await _authRepository.signIn(authResponse.value!);
if (repoResponse.isFailure) {
const SignInAuthStageNotification(AuthStage.idle);
return repoResponse;
}
$mediator.publish(
const SignInAuthStageNotification(AuthStage.idle),
);
Future<void>.delayed(const Duration(milliseconds: 500))
.then(
(_) => $mediator.publish(
const SignInAuthStageNotification(AuthStage.idle),
),
)
.ignore();
if (repoResponse.isSuccess) {
$states.get<AuthState>().change(repoResponse.value);
}
return repoResponse;
}
}
登录页面
这是登录页面的代码:
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
Future<void> _signIn(BuildContext context, AuthProvider authProvider) async {
final response = await $mediator.send(SignInRequest(authProvider));
if (response.isSuccess) {
return;
}
switch (response.failure) {
case SignInFailure.cancelledByUser:
break;
case SignInFailure.networkRequestFailed:
await context.showOKDialog(
title: "No internet connection",
message: "There were a failure while trying to reach the "
"authentication service.\n\nPlease, check your internet connection.",
);
case SignInFailure.userDisabled:
await context.showOKDialog(
title: "User is disabled",
message: "Your user is disabled, please, contact support.",
);
default:
await context.showOKDialog(
title: "Oops",
message: "An unknown error has occurred!\n\n"
"(Details: ${response.exception})",
);
}
}
[@override](/user/override)
Widget build(BuildContext context) {
final theme = Theme.of(context);
return StreamBuilder(
stream: $mediator.getChannel<SignInAuthStageNotification>(),
initialData: const SignInAuthStageNotification(AuthStage.idle),
builder: (context, snapshot) {
final currentAuthStage = snapshot.data?.stage ?? AuthStage.idle;
final authMessage = switch (currentAuthStage) {
AuthStage.idle => "Sign in with",
AuthStage.signingInWithApple => "Awaiting Apple...",
AuthStage.signingInWithGoogle => "Awaiting Google...",
AuthStage.authorizing => "Authorizing...",
AuthStage.registering => "Registering...",
};
final isBusy = currentAuthStage != AuthStage.idle;
return Scaffold(
body: SafeArea(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Spacer(),
const AppLogo(dimension: 200),
const SizedBox.square(dimension: 16),
Text(
"App Name",
style: theme.textTheme.headlineMedium,
),
const Spacer(),
Text(
authMessage,
style: theme.textTheme.labelMedium,
),
const SizedBox.square(dimension: 8),
isBusy
? const Center(
child: SizedBox.square(
dimension: 48,
child: Center(
child: CircularProgressIndicator.adaptive(),
),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_AuthProviderButton(
onPressed: () => _signIn(context, AuthProvider.google),
icon: const GoogleLogo(dimension: 16),
),
Transform.translate(
offset: const Offset(0, -2),
child: Text(
" or ",
style: theme.textTheme.labelMedium,
),
),
_AuthProviderButton(
onPressed: () => _signIn(context, AuthProvider.apple),
icon: const AppleLogo(
dimension: 16,
color: Colors.black,
),
),
],
),
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: isBusy ? null : () {},
child: Text(
"PRIVACY POLICY",
style: theme.textTheme.labelSmall,
),
),
TextButton(
onPressed: isBusy ? null : () {},
child: Text(
"ABOUT",
style: theme.textTheme.labelSmall,
),
),
TextButton(
onPressed: isBusy ? null : () {},
child: Text(
"TERMS OF USE",
style: theme.textTheme.labelSmall,
),
)
],
),
),
],
),
),
),
);
},
);
}
}
final class _AuthProviderButton extends StatelessWidget {
const _AuthProviderButton({
required this.onPressed,
required this.icon,
});
final void Function() onPressed;
final Widget icon;
[@override](/user/override)
Widget build(BuildContext context) {
return IconButton(
onPressed: onPressed,
isSelected: true,
icon: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(22),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 2,
)
],
),
child: icon,
),
);
}
}
更多关于Flutter架构设计插件simple_architecture的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter架构设计插件simple_architecture的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是一个关于如何在Flutter项目中使用simple_architecture
插件的示例代码案例。simple_architecture
插件是一个用于简化Flutter应用架构的库,它通常基于MVVM(Model-View-ViewModel)或类似模式来组织代码。
首先,确保你已经在pubspec.yaml
文件中添加了simple_architecture
依赖:
dependencies:
flutter:
sdk: flutter
simple_architecture: ^x.y.z # 替换为最新版本号
然后运行flutter pub get
来安装依赖。
示例代码
1. 创建Model
Model
通常代表应用中的数据。这里我们创建一个简单的User
模型。
// models/user.dart
class User {
final String name;
final int age;
User({required this.name, required this.age});
// 可以添加toJson和fromJson方法用于序列化/反序列化
Map<String, dynamic> toJson() => {
'name': name,
'age': age,
};
factory User.fromJson(Map<String, dynamic> json) => User(
name: json['name'] as String,
age: json['age'] as int,
);
}
2. 创建ViewModel
ViewModel
负责处理业务逻辑和状态管理。
// viewmodels/user_view_model.dart
import 'package:flutter/material.dart';
import 'package:simple_architecture/simple_architecture.dart';
import 'package:your_app/models/user.dart';
class UserViewModel extends BaseViewModel {
User? _user;
User? get user => _user;
void fetchUser() async {
// 模拟从API获取用户数据
await Future.delayed(Duration(seconds: 2));
_user = User(name: 'John Doe', age: 30);
notifyListeners(); // 更新UI
}
}
3. 创建View (Widget)
View
负责显示UI并与ViewModel
交互。
// views/user_view.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app/viewmodels/user_view_model.dart';
class UserView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final UserViewModel model = Provider.of<UserViewModel>(context);
return Scaffold(
appBar: AppBar(title: Text('User Info')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: model.fetchUser,
child: Text('Fetch User'),
),
SizedBox(height: 20),
if (model.user != null)
Text('Name: ${model.user!.name}'),
if (model.user != null)
Text('Age: ${model.user!.age}'),
],
),
),
);
}
}
4. 在Main中提供ViewModel
在主文件中提供ViewModel
并使用MultiProvider
来管理依赖。
// main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:your_app/viewmodels/user_view_model.dart';
import 'package:your_app/views/user_view.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => UserViewModel()),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: UserView(),
);
}
}
总结
以上代码展示了如何使用simple_architecture
插件在Flutter应用中实现一个基本的MVVM架构。通过分离关注点(数据模型、业务逻辑和UI),代码变得更加模块化和易于维护。你可以根据实际需求进一步扩展和自定义这个架构。