Flutter架构设计插件simple_architecture的使用

发布于 1周前 作者 yibo5220 来自 Flutter

Flutter架构设计插件simple_architecture的使用

简介

simple_architecture 是一个用于简化 Flutter 应用架构设计的库。通过该库,你可以轻松实现可重用性、功能单元化、配置管理、状态管理和异常处理等功能。以下是详细的介绍和示例。

目标

可重用性

某些应用部分在多个应用中都是通用的,例如认证。这些部分仅需一些特定设置(如 clientIdredirectUri),因此无需为不同的应用编写相同的代码。

通过使用存储库(repository)概念,可以实现这部分代码的重用。虽然存储库定义通常与数据库相关,但同样的概念可以应用于任何进行输入输出操作的部分,例如读写文件、远程调用等。

功能单元

单一职责原则(Single Responsibility Principle)要求每个模块只负责一个任务。为了确保这一点,应将每个功能分解成独立的模块。每个模块应具有独立的实现,并且可以在不同时间开发和测试。

配置

当系统部分是可重用时,通常配置项会有所不同。对于认证系统,可能需要配置 Google Client IdApple Service IdApple Redirect Uri 等。这些配置可以通过依赖注入系统进行管理。

状态管理

状态管理在 Flutter 中是一个常见的问题。虽然有许多复杂的框架(如 BLoC 和 Riverpod),但状态管理实际上非常简单。通过 ValueNotifier<T>,我们可以轻松地管理全局和局部状态。

监控

有时我们需要监控某些功能的执行情况,例如异常管理、性能测量和审计日志。这些可以通过 PipelineBehavior 实现,它们会在每次系统调用时添加这些特性。

SOLID 原则

SOLID 原则是许多项目中常见的原则。这些原则包括单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖反转原则。这些原则有助于编写更易于维护和扩展的代码。

实际示例

我们将实现一个完整的认证系统,使用该库及其所有现有概念。

规格

  • 认证将仅通过 OAuth 使用 Google 或 Apple 进行。
  • 包选择为 sign_in_with_applegoogle_sign_infirebase_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

1 回复

更多关于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),代码变得更加模块化和易于维护。你可以根据实际需求进一步扩展和自定义这个架构。

回到顶部