[Flutter翻译]用Very Good CLI和Supabase构建Flutter应用

793 阅读11分钟

本文由 简悦SimpRead 转码,原文地址 verygood.ventures

学习如何使用Supabase和VGV工具(如Very Good CLI、Flutter Bloc、l......)制作Flutter应用程序。

在本教程中,我们将采用Supabase提供的Flutter示例,并使用VGV工具,如Very Good CLIFlutter Bloc、分层架构,以及最重要的一点,100%的测试覆盖率🧪来重写它。

本教程是基于Supabase网站上的Quickstart: Flutter教程编写的。它将针对Android、iOS和Web。

让我们开始吧! 🙌

但首先,Supabase是什么?

Supabase是一个开源的Firebase替代品。简单地说,Supabase为你提供了一堆服务,比如用不同的提供者(email, Apple, Azure, Discord, GitHub, GitLab, Bitbucket, Slack 等等)进行认证,数据库(PostgreSQL),存储,函数等等。

如果你想拥有比典型的认证提供者(email/password, Twitter, Facebook, Google, and Apple)更多的认证提供者,或者是一个SQL数据库而不是NoSQL数据库,你可以考虑为你的项目使用Supabase。要了解更多关于Supabase的信息,请访问他们的官方网站这里

💡概述

这些是我们在本教程中要介绍的内容。

  • 使用深度链接用电子邮件登录。
  • 处理用户状态( authenticatedunauthenticated),以了解我们需要使用Flow Builder导航的地方。
  • 用_数据库SQL编辑器_控制台的脚本创建一个简单的数据库。
  • 向数据库添加规则。
  • 更新数据库中的用户信息。

数据库的初始配置

创建一个新的项目

第一步是访问Supabase网站并创建一个登录。当我们可以访问仪表板时,我们可以创建一个新的组织和项目。

  • 点击 新项目,选择你的组织(如果你没有组织,你应该点击 新组织 ,创建一个新的组织)。下一步是填写表格,创建一个新的项目。

  • 现在我们可以在仪表板上看到新项目。

用SQL编辑器创建一个数据库

为了在我们的项目中创建一个新的数据库,我们应该导航到SQL编辑器,在那里我们可以创建一个脚本来创建表和规则来应用在这个数据库中。

在这种情况下,我们要创建一个名为 account 的表。我们需要修改 User Management Started 脚本,并添加以下代码。

-- Create a table for Public Account
create table account (
  id uuid references auth.users not null,
  username text unique,
  companyName text,
  primary key (id),
  unique(username),
  constraint username_length check (char_length(username) >= 3)
);
alter table account
  enable row level security;
create policy "Public account are viewable by everyone." on account
  for select using (true);
create policy "Users can insert their own account." on account
  for insert with check (auth.uid() = id);
create policy "Users can update their own account." on account
  for update using (auth.uid() = id);
-- Set up Realtime!
begin;
  drop publication if exists supabase_realtime;
  create publication supabase_realtime;
commit;
alter publication supabase_realtime
  add table account;

然后,我们需要点击右下角的RUN。最后我们的新数据库就创建好了。

数据库表编辑器*

一旦数据库被创建,我们就可以在整个仪表板上导航,查看表和规则。

在左边的菜单中,我们可以导航到_表编辑器_来查看账户表。它目前是空的,但你可以看到该表有三列 idusernamecompanyname

如果你想检查我们用脚本创建的规则,你可以点击 RLS enabled 来查看它们。

认证配置

正如我在一开始提到的,我们在本教程中要使用的认证方法是电子邮件深度链接。

我们需要在我们的认证仪表板中添加一个重定向URL(这只适用于你的应用程序是在网络上的情况)。

在这种情况下,我们要使用Supabase在Flutter例子中提供的重定向URL。如果你正在建立一个真正的项目,你应该添加提供者的重定向URL。

io.supabase.flutterquickstart://login-callback/

现在,我们已经设置好了一切,下一步是创建我们的Flutter项目 🙌

Flutter项目和包

在本节中,我们将使用Very Good CLI创建Flutter项目和不同的包。

第一步是激活Very Good CLI。

dart pub global activate very_good_cli

一旦我们激活了Very Good CLI,我们就可以创建Flutter项目了。我们只需在终端运行以下一行。

very_good create very_good_supabase --desc "Example of a Flutter application using Very Good CLI and Supabase"

我们这个项目需要哪些包? 🤔

我已经在 package 文件夹下创建了几个(你应该在你的应用程序根目录下创建它们)。

  • Supabase auth client:这个包负责Supabase的 sign-insign-out 方法。
  • Supabase数据库客户端:这个包负责从数据库中获取用户信息和更新用户信息。
  • 用户资源库:这个资源库将有能力使用 SupabaseAuthClient 来调用 sign-insign-out 方法,使用 SupabaseDatabaseClient获取用户资料 信息,并 更新 Supabase上的用户。
  • 表单输入:这个包承载了在视图上使用的不同表单输入。
  • 电子邮件启动器:在Android和iOS上打开一个默认的电子邮件应用程序。

你可以访问GitHub仓库来查看 表单输入电子邮件启动器 包的代码。在这里,我只打算介绍与Supabase有关的包。

让我们来创建它们吧! 😀

Supabase auth client

首先,我们需要运行以下命令。

very_good create supabase_auth_client -t flutter_pkg --desc "Supabase auth client"

然后我们应该修改pubspec.yaml文件,添加必要的依赖关系。

name: supabase_auth_client
description: Supabase auth client
publish_to: none

environment:
  sdk: ">=2.17.0 <3.0.0"

dependencies:
  flutter_test:
    sdk: flutter
  supabase_flutter: ^0.3.1

dev_dependencies:
  flutter:
    sdk: flutter
  mocktail: ^0.3.0
  very_good_analysis: ^3.0.1

现在我们可以为这个包添加逻辑。在这一部分中,我们将专注于签入和签出方法。

/// {@template supabase_auth_client}
/// Supabase auth client
/// {@endtemplate}
class SupabaseAuthClient {
  /// {@macro supabase_auth_client}
  SupabaseAuthClient({
    required GoTrueClient auth,
  }) : _auth = auth;
  final GoTrueClient _auth;
  /// Method to do sign in on Supabase.
  Future<void> signIn({
    required String email,
    required bool isWeb,
  }) async {
    try {
      await _auth.signIn(
        email: email,
        options: AuthOptions(
          redirectTo:
              isWeb ? null : 'io.supabase.flutterquickstart://login-callback/',
        ),
      );
    } catch (error, stackTrace) {
      Error.throwWithStackTrace(SupabaseSignInFailure(error), stackTrace);
    }
  }
  /// Method to do sign out on Supabase.
  Future<void> signOut() async {
    try {
      await _auth.signOut();
    } catch (error, stackTrace) {
      Error.throwWithStackTrace(SupabaseSignOutFailure(error), stackTrace);
    }
  }
}

Supabase数据库客户端

首先你需要创建这个包。

very_good create supabase_database_client -t flutter_pkg --desc "Supabase database client"

然后修改pubspec.yaml文件。

name: supabase_database_client
description: Supabase database client
publish_to: none

environment:
  sdk: ">=2.17.0 <3.0.0"

dependencies:
  equatable: ^2.0.3
  flutter:
    sdk: flutter
  json_annotation: ^4.5.0
  json_serializable: ^6.2.0
  mocktail: ^0.3.0
  supabase_flutter: ^0.3.1

dev_dependencies:
  build_runner: ^2.1.11
  flutter_test:
    sdk: flutter
  very_good_analysis: ^3.0.1

下一步是向这个包添加逻辑。在本例中,这个包将负责从账户表中检索用户信息,同时也负责更新账户表中的数据。

首先,我们需要创建一个 SupabaseUser 模型。要做到这一点,我们可以在 src 里面创建一个新的文件夹,叫做 models 。然后在其中添加新的supabase_user.dart文件。对于这个模型我使用了Json Serializable包。

/// {@template supabase_user}
/// Supabase user model
/// {@endtemplate}
@JsonSerializable()
class SupabaseUser extends Equatable {
  /// {@macro supabase_user}
  const SupabaseUser({
    String? id,
    required this.userName,
    required this.companyName,
  }) : id = id ?? '';

  /// Connect the generated [_$SupabaseUserFromJson] function to the `fromJson`
  /// factory.
  factory SupabaseUser.fromJson(Map<String, dynamic> json) =>
      _$SupabaseUserFromJson(json);

  /// Id of the user.
  final String id;

  /// Name of the supabase user.
  @JsonKey(name: 'username')
  final String userName;

  /// Company name of the supabase user.
  @JsonKey(name: 'companyname')
  final String companyName;

  @override
  List<Object> get props => [id, userName, companyName];

  /// Empty Supabase object.
  static const empty = SupabaseUser(
    userName: '',
    companyName: '',
  );

  /// Connect the generated [_$SupabaseUserToJson]
  /// function to the `toJson` method.
  Map<String, dynamic> toJson() => _$SupabaseUserToJson(this);
}

下一步是在supabase_database_client.dart文件中添加逻辑。

/// {@template supabase_database_client}
/// Supabase database client
/// {@endtemplate}
class SupabaseDatabaseClient {
  /// {@macro supabase_database_client}
  const SupabaseDatabaseClient({
    required SupabaseClient supabaseClient,
  }) : _supabaseClient = supabaseClient;

  final SupabaseClient _supabaseClient;

  /// Method to get the user information by id
  /// from the profiles database on Supabase.
  Future<SupabaseUser> getUserProfile() async {
    try {
      final response = await _supabaseClient
          .from('account')
          .select()
          .eq('id', _supabaseClient.auth.currentUser?.id)
          .single()
          .execute();

      final data = response.data as Map<String, dynamic<;
      return SupabaseUser.fromJson(data);
    } catch (error, stackTrace) {
      Error.throwWithStackTrace(
        SupabaseUserInformationFailure(error),
        stackTrace,
      );
    }
  }

  /// Method to update the user information on the profiles database.
  Future<void> updateUser({required SupabaseUser user}) async {
    try {
      final supabaseUser = SupabaseUser(
        id: _supabaseClient.auth.currentUser?.id,
        userName: user.userName,
        companyName: user.companyName,
      );

      await _supabaseClient
          .from('account')
          .upsert(supabaseUser.toJson())
          .execute();
    } catch (error, stackTrace) {
      Error.throwWithStackTrace(
        SupabaseUpdateUserFailure(error),
        stackTrace,
      );
    }
  }
}

用户库

第一步是创建包。

very_good create user_repository -t flutter_pkg --desc "A package which manages the user domain."

第二步是修改pubspec.yaml文件,添加依赖关系。

name: user_repository
description: A package which manages the user domain
publish_to: none

environment:
  sdk: ">=2.17.0 <3.0.0"

dependencies:
  equatable: ^2.0.3
  flutter:
    sdk: flutter
  mocktail: ^0.3.0
  supabase_auth_client:
    path: ../supabase_auth_client
  supabase_database_client:
    path: ../supabase_database_client
  supabase_flutter: ^0.3.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  very_good_analysis: ^3.0.1

现在我们可以为这个包添加逻辑。在这个资源库中,我们可以使用 SupabaseAuthClientSupabaseDatabaseClient ,能够使用每个类里面的方法。通过这个资源库,我们可以做以下事情。

  • 签入
  • 签出
  • 获取用户信息
  • 更新用户

我们已经完成了软件包的配置。现在我们可以跳到业务逻辑和视图。

/// {@template user_repository}
/// A package which manages the user domain.
/// {@endtemplate}
class UserRepository {
  /// {@macro user_repository}
  UserRepository({
    required SupabaseAuthClient authClient,
    required SupabaseDatabaseClient databaseClient,
  })  : _authClient = authClient,
        _databaseClient = databaseClient;

  final SupabaseAuthClient _authClient;
  final SupabaseDatabaseClient _databaseClient;

  /// Method to access the current user.
  Future<User> getUser() async {
    final supabaseUser = await _databaseClient.getUserProfile();
    return supabaseUser.toUser();
  }

  /// Method to update user information on profiles database.
  Future<void> updateUser({required User user}) {
    return _databaseClient.updateUser(user: user.toSupabaseUser());
  }

  /// Method to do signIn.
  Future<void> signIn({required String email, required bool isWeb}) async {
    return _authClient.signIn(email: email, isWeb: isWeb);
  }

  /// Method to do signOut.
  Future<void> signOut() async => _authClient.signOut();
}

extension on SupabaseUser {
  User toUser() {
    return User(
      id: id,
      userName: userName,
      companyName: companyName,
    );
  }
}

extension on User {
  SupabaseUser toSupabaseUser() {
    return SupabaseUser(
      id: id,
      userName: userName,
      companyName: companyName,
    );
  }
}

注意:这些包需要不同的工作流程来配合GitHub的工作流程。我们可以在示例仓库.github 文件夹中找到这些文件。

Flutter中的Supabase

原生配置

第一步是在我们的项目中配置Supabase。首先我们需要在Android和iOS上添加一些本地配置。

  • Android:在AndroidManifest.xml文件中添加这些行。
<!-- Add this intent-filter for Deep Links -->
<intent-filter>
     <action android:name="android.intent.action.VIEW" />
     <category android:name="android.intent.category.DEFAULT" />
     <category android:name="android.intent.category.BROWSABLE" />
     <!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
     <data
         android:scheme="io.supabase.flutterquickstart"
         android:host="login-callback" />
</intent-filter>
  • iOS:将这些行添加到Info.plist文件中。
<!-- Add this array for Deep Links -->
  <key>CFBundleURLTypes</key>
  <array>
    <dict>
      <key>CFBundleTypeRole</key>
      <string>Editor</string>
      <key>CFBundleURLSchemes</key>
      <array>
        <string>io.supabase.flutterquickstart</string>
      </array>
    </dict>
  </array>

准备Flutter项目以使用Supabase

这里我们需要做几件事。首先,我们需要更新我们项目中的pubspec.yaml文件,以便能够使用我们刚刚创建的所有软件包,以及Supabase依赖。

name: very_good_supabase
description: Example of a flutter application to use Very Good CLI and Supabase
version: 1.0.0+1
publish_to: none

environment:
  sdk: ">=2.17.0 <3.0.0"

dependencies:
  bloc: ^8.0.3
  email_launcher:
    path: packages/email_launcher
  equatable: ^2.0.3
  flow_builder: ^0.0.8
  flutter:
    sdk: flutter
  flutter_bloc: ^8.0.1
  flutter_dotenv: ^5.0.2
  flutter_localizations:
    sdk: flutter
  form_inputs:
    path: packages/form_inputs
  intl: ^0.17.0
  mockingjay: ^0.3.0
  mocktail_image_network: ^0.3.1
  supabase_auth_client:
    path: packages/supabase_auth_client
  supabase_database_client:
    path: packages/supabase_database_client
  supabase_flutter: ^0.3.0
  user_repository:
    path: packages/user_repository

dev_dependencies:
  bloc_test: ^9.0.3
  flutter_test:
    sdk: flutter
  mocktail: ^0.3.0
  very_good_analysis: ^3.0.1

flutter:
  uses-material-design: true
  generate: true
  assets:
    - assets/images/
    - assets/.env

第二步是准备main_development.dart文件,以使用必要的包并初始化Supabase。

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await dotenv.load(fileName: 'assets/.env');
  await Supabase.initialize(
    url: dotenv.get('SUPABASE_URL'),
    anonKey: dotenv.get('ANON_KEY'),
  );
  await bootstrap(() {
    final authClient = SupabaseAuthClient(
      auth: Supabase.instance.client.auth,
    );
    final databaseClient = SupabaseDatabaseClient(
      supabaseClient: Supabase.instance.client,
    );
    final userRepository = UserRepository(
      authClient: authClient,
      databaseClient: databaseClient,
    );
    return App(userRepository: userRepository);
  });
}

你可能已经注意到,为了初始化Supabase,我们需要提供 supabase urlanon key ,这可以在项目设置中找到。

创建功能

在这个项目中,我们将有四个功能。

  • Auth States Supabase:在这里我们将添加两个类,它们将告诉我们一个用户是否被认证,以及是否需要认证。这些类也将在我们的小部件中使用。
  • App:它将负责监听用户状态(已认证或未认证),以决定是否导航到登录页面或账户页面。
  • 登录:这个功能将承载这个项目的所有登录逻辑。
  • 账户:该功能将获得用户信息并将其显示在一个文本字段中。它还将更新Supabase数据库中的信息并处理签出。

Supabase状态

  • AuthStateSupabase
class AuthStateSupabase<T extends StatefulWidget> extends SupabaseAuthState<T> {
  @override
  void onUnauthenticated() {
    if (mounted) {
      context.read<AppBloc>().add(AppUnauthenticated());
    }
  }

  @override
  void onAuthenticated(Session session) {
    if (mounted) {
      context.read<AppBloc>().add(const AppAuthenticated());
    }
  }

  @override
  void onPasswordRecovery(Session session) {}

  @override
  void onErrorAuthenticating(String message) {}
}
  • AuthStateSupabaseRequired
class AuthRequiredState<T extends StatefulWidget>
    extends SupabaseAuthRequiredState<T> {
  @override
  void onUnauthenticated() {
   if (mounted) {
      context.read<AppBloc>().add(AppUnauthenticated());
    }
  }
}

应用

该功能负责恢复Supabase会话,以了解用户在前一个会话中是否得到认证。有了这些信息,这个功能将决定应用程序应该在哪里导航。

我们将使用Flow Builder来处理这个导航。这里我们有以下结构。

├── lib
|   ├── app
│   │   ├── bloc
│   │   │   └── app_bloc.dart
|   |   |   └── app_event.dart
|   |   |   └── app_state.dart
│   │   └── view
│   │   |   ├── app.dart
│   │   |   └── app_view.dart
|   |   |   └── view.dart
|   |   └── routes
|   |   |   └── routes.dart
│   ├── app.dart
  • AppView:这里我们用一个_AuthStateSupabase_而不是一个_StatefulWidget_来扩展这个类。这对于了解用户是否被认证是很重要的。
class AppView extends StatefulWidget {
  const AppView({super.key});

  @override
  State<AppView> createState() => _AppViewState();
}

class _AppViewState extends AuthStateSupabase<AppView> {
  @override
  void initState() {
    super.initState();
    recoverSupabaseSession(); // <--- Here we recover the supabase session
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Very Good Supabase',
      theme: ThemeData(
        appBarTheme: const AppBarTheme(color: Colors.teal),
        colorScheme: const ColorScheme.light().copyWith(
          primary: Colors.teal,
        ),
      ),
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
      ],
      supportedLocales: AppLocalizations.supportedLocales,
      home: FlowBuilder<AppStatus>(
        state: context.select((AppBloc bloc) => bloc.state.status),
        onGeneratePages: onGenerateAppViewPages,
      ),
    );
  }
}
  • AppBloc

  • 路由:这个方法 onGenerateAppViewPages 在我们的 AppView 中使用。

List<Page> onGenerateAppViewPages(AppStatus state, List<Page<dynamic>> pages) {
  switch (state) {
    case AppStatus.unauthenticated:
      return [LoginPage.page()];
    case AppStatus.authenticated:
      return [AccountPage.page()];
  }
}

登录

这个功能负责签到和使用我们创建的表单输入包检查电子邮件文本字段。

其结构是。

├── lib
|   ├── login
│   │   ├── bloc
│   │   │   └── login_bloc.dart
|   |   |   └── login_event.dart
|   |   |   └── login_state.dart
│   │   └── view
│   │   |   ├── login_page.dart
│   │   |   └── login_view.dart
|   |   |   └── view.dart
│   ├── login.dart
  • LoginView: 在这里,我们将拥有登录我们的应用程序所需的所有小部件。另外,我们将使用一个 AuthStateSupabase 类来了解用户当前是否已经登录。此外,我们将使用输入表单包来检查电子邮件是否正确。
class LoginView extends StatefulWidget {
  const LoginView({super.key});

  @override
  State<LoginView> createState() => _LoginViewState();
}

class _LoginViewState extends AuthStateSupabase<LoginView> {
  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.symmetric(
        vertical: 50,
        horizontal: 32,
      ),
      children: [
        const _Header(),
        const SizedBox(height: 18),
        const _EmailInput(),
        const SizedBox(height: 28),
        const _SendEmailButton(),
        const SizedBox(height: 28),
        if (!kIsWeb) OpenEmailButton()
      ],
    );
  }
}

class _Header extends StatelessWidget {
  const _Header();

  @override
  Widget build(BuildContext context) {
    return Column(
      key: const Key('loginView_header'),
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            SizedBox.square(
              dimension: 100,
              child: Assets.images.supabase.image(),
            ),
            SizedBox(
              width: 200,
              height: 100,
              child: Assets.images.vgv.image(),
            ),
          ],
        ),
        Padding(
          padding: const EdgeInsets.only(top: 100),
          child: Text(
            'Sign in via the magic link',
            style: Theme.of(context).textTheme.headline5,
          ),
        ),
      ],
    );
  }
}

class _EmailInput extends StatelessWidget {
  const _EmailInput();

  @override
  Widget build(BuildContext context) {
    final isInProgress = context.select(
      (LoginBloc bloc) => bloc.state.status == FormzSubmissionStatus.inProgress,
    );
    return TextFormField(
      key: const Key('loginView_emailInput_textField'),
      readOnly: isInProgress,
      onChanged: (email) {
        context.read<LoginBloc>().add(LoginEmailChanged(email));
      },
      decoration: const InputDecoration(labelText: 'Email'),
    );
  }
}

class _SendEmailButton extends StatelessWidget {
  const _SendEmailButton();

  @override
  Widget build(BuildContext context) {
    final state = context.watch<LoginBloc>().state;
    return ElevatedButton(
      key: const Key('loginView_sendEmail_button'),
      onPressed: state.status.isInProgress || !state.valid
          ? null
          : () => context.read<LoginBloc>().add(
                LoginSubmitted(
                  email: state.email.value,
                  isWeb: kIsWeb,
                ),
              ),
      child: Text(
        state.status.isInProgress ? 'Loading' : 'Send Magic Link',
      ),
    );
  }
}

class OpenEmailButton extends StatelessWidget {
  OpenEmailButton({
    EmailLauncher? emailLauncher,
    super.key,
  }) : _emailLauncher = emailLauncher ?? EmailLauncher();

  final EmailLauncher _emailLauncher;

  @override
  Widget build(BuildContext context) {
    final state = context.watch<LoginBloc>().state;
    return OutlinedButton(
      key: const Key('loginView_openEmail_button'),
      onPressed: state.status.isInProgress || !state.valid
          ? null
          : _emailLauncher.launchEmailApp,
      child: const Text('Open Email App'),
    );
  }
}
  • LoginBloc: 这个类的主要目的是与 UserRepository 通信以进行登录。另外,在这里我们要处理当用户改变数值时,电子邮件文本字段的所有可能状态。
class LoginBloc extends Bloc<LoginEvent, LoginState> {
  LoginBloc(this._userRepository) : super(const LoginState()) {
    on<LoginSubmitted>(_onSignIn);
    on<LoginEmailChanged>(_onEmailChanged);
  }

  final UserRepository _userRepository;

  void _onEmailChanged(LoginEmailChanged event, Emitter<LoginState> emit) {
    final email = Email.dirty(event.email);
    emit(
      state.copyWith(
        email: email,
        valid: Formz.validate([email]),
      ),
    );
  }

  Future<void> _onSignIn(
    LoginSubmitted event,
    Emitter<LoginState> emit,
  ) async {
    try {
      emit(state.copyWith(status: FormzSubmissionStatus.inProgress));
      await _userRepository.signIn(
        email: event.email,
        isWeb: event.isWeb,
      );
      emit(state.copyWith(status: FormzSubmissionStatus.success));
    } catch (error) {
      emit(state.copyWith(status: FormzSubmissionStatus.failure));
      addError(error);
    }
  }
}

账户

这个功能负责多项工作。

  • 从数据库中检索信息。
  • 更新数据库中的用户信息。
  • 签出。
  • 检查 userNamecompanyName 文本字段的输入。其结构是
├── lib
|   ├── account
│   │   ├── bloc
│   │   │   └── account_bloc.dart
|   |   |   └── account_event.dart
|   |   |   └── account_state.dart
│   │   └── view
│   │   |   ├── account_page.dart
│   │   |   └── account_view.dart
|   |   |   └── view.dart
│   ├── account.dart
  • AccountView: 这个类要扩展到 AuthRequiredState ,它决定了用户是否登录了。这是必要的,因为我们要显示用户的账户信息。
class AccountView extends StatefulWidget {
  const AccountView({super.key});

  @override
  AccountViewState createState() => AccountViewState();
}

class AccountViewState extends AuthRequiredState<AccountView> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Account')),
      body: BlocConsumer<AccountBloc, AccountState>(
        listener: (context, state) {
          if (state.status.isUpdate) {
            context.showSnackBar(message: 'Updated!');
          }
        },
        buildWhen: (previous, current) =>
            current.status.isSuccess ||
            current.status.isUpdate ||
            current.status.isEditing,
        builder: (context, state) {
          return ListView(
            padding: const EdgeInsets.all(28),
            children: const [
              _Header(),
              _UserNameTextField(),
              _UserCompanyNameTextField(),
              SizedBox(height: 50),
              _UpdateUserButton(),
              SizedBox(height: 18),
              _SignOutButton(),
            ],
          );
        },
      ),
    );
  }
}

class _Header extends StatelessWidget {
  const _Header();

  @override
  Widget build(BuildContext context) {
    return Column(
      key: const Key('account_header'),
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            SizedBox.square(
              dimension: 100,
              child: Assets.images.supabase.image(),
            ),
            SizedBox(
              width: 200,
              height: 100,
              child: Assets.images.vgv.image(),
            ),
          ],
        ),
        Padding(
          padding: const EdgeInsets.only(top: 100),
          child: Text(
            'Update your information 🦄',
            style: Theme.of(context).textTheme.headline5,
          ),
        ),
      ],
    );
  }
}

class _UserNameTextFieldState extends State<_UserNameTextField> {
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return BlocConsumer<AccountBloc, AccountState>(
      listener: (context, state) {
        if (state.status.isSuccess) {
          _controller.text = state.userName.value;
        }
      },
      builder: (context, state) {
        return Padding(
          padding: const EdgeInsets.only(top: 18),
          child: TextFormField(
            controller: _controller,
            key: const Key('accountView_userName_textField'),
            readOnly: state.status.isLoading,
            textInputAction: TextInputAction.next,
            onChanged: (userName) => context
                .read<AccountBloc>()
                .add(AccountUserNameChanged(userName)),
            decoration: const InputDecoration(labelText: 'User Name'),
          ),
        );
      },
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

class _UserCompanyNameTextField extends StatefulWidget {
  const _UserCompanyNameTextField();

  @override
  State<_UserCompanyNameTextField> createState() =>
      _UserCompanyNameTextFieldState();
}

class _UserCompanyNameTextFieldState extends State<_UserCompanyNameTextField> {
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return BlocConsumer<AccountBloc, AccountState>(
      listener: (context, state) {
        if (state.status.isSuccess) {
          _controller.text = state.companyName.value;
        }
      },
      builder: (context, state) {
        return Padding(
          padding: const EdgeInsets.only(top: 18),
          child: TextFormField(
            controller: _controller,
            key: const Key('accountView_companyName_textField'),
            readOnly: state.status.isLoading,
            onChanged: (companyName) => context.read<AccountBloc>().add(
                  AccountCompanyNameChanged(companyName),
                ),
            decoration: const InputDecoration(
              labelText: 'Company Name',
            ),
          ),
        );
      },
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

class _UpdateUserButton extends StatelessWidget {
  const _UpdateUserButton();

  @override
  Widget build(BuildContext context) {
    final state = context.watch<AccountBloc>().state;
    return ElevatedButton(
      key: const Key('accountView_update_button'),
      onPressed: state.status.isLoading || !state.valid
          ? null
          : () => context.read<AccountBloc>().add(
                AccountUserUpdated(
                  user: User(
                    id: state.user.id,
                    userName: state.userName.value,
                    companyName: state.companyName.value,
                  ),
                ),
              ),
      child: Text(state.status.isLoading ? 'Saving...' : 'Update'),
    );
  }
}

class _SignOutButton extends StatelessWidget {
  const _SignOutButton();

  @override
  Widget build(BuildContext context) {
    final isLoading = context.select(
      (AccountBloc bloc) => bloc.state.status == AccountStatus.loading,
    );
    return OutlinedButton(
      key: const Key('accountView_signOut_button'),
      onPressed: isLoading
          ? null
          : () => context.read<AccountBloc>().add(AccountSignedOut()),
      child: const Text('Sign Out'),
    );
  }
}
  • AccountBloc:这个类负责多个事件的处理。这里我们需要使用 UserRepository 来检索用户信息,更新数据库,并签出。另外,我们还有其他事件来处理用户与 userNamecompanyName 文本字段的互动。
class AccountBloc extends Bloc<AccountEvent, AccountState> {
  AccountBloc(this._userRepository) : super(const AccountState()) {
    on<AccountUserInformationFetched>(_onGetUserInformation);
    on<AccountUserUpdated>(_onUpdateUser);
    on<AccountSignedOut>(_onSignOut);
    on<AccountUserNameChanged>(_onUserNameChanged);
    on<AccountCompanyNameChanged>(_onCompanyNameChanged);
  }

  final UserRepository _userRepository;

  Future<void> _onGetUserInformation(
    AccountUserInformationFetched event,
    Emitter<AccountState> emit,
  ) async {
    try {
      emit(state.copyWith(status: AccountStatus.loading));
      final user = await _userRepository.getUser();
      emit(
        state.copyWith(
          status: AccountStatus.success,
          user: user,
          userName: UserName.dirty(user.userName),
          companyName: CompanyName.dirty(user.companyName),
        ),
      );
    } catch (error) {
      emit(state.copyWith(status: AccountStatus.error));
      addError(error);
    }
  }

  Future<void> _onUpdateUser(
    AccountUserUpdated event,
    Emitter<AccountState> emit,
  ) async {
    try {
      emit(state.copyWith(status: AccountStatus.loading));
      await _userRepository.updateUser(user: event.user);
      emit(state.copyWith(status: AccountStatus.update, valid: false));
    } catch (error) {
      emit(state.copyWith(status: AccountStatus.error));
      addError(error);
    }
  }

  Future<void> _onSignOut(
    AccountSignedOut event,
    Emitter<AccountState> emit,
  ) async {
    try {
      emit(state.copyWith(status: AccountStatus.loading));
      await _userRepository.signOut();
      emit(state.copyWith(status: AccountStatus.success));
    } catch (error) {
      emit(state.copyWith(status: AccountStatus.error));
      addError(error);
    }
  }

  Future<void> _onUserNameChanged(
    AccountUserNameChanged event,
    Emitter<AccountState> emit,
  ) async {
    final userName = UserName.dirty(event.userName);
    emit(
      state.copyWith(
        status: AccountStatus.edit,
        userName: userName,
        valid: Formz.validate([userName, state.companyName]),
      ),
    );
  }

  Future<void> _onCompanyNameChanged(
    AccountCompanyNameChanged event,
    Emitter<AccountState> emit,
  ) async {
    final companyName = CompanyName.dirty(event.companyName);
    emit(
      state.copyWith(
        status: AccountStatus.edit,
        companyName: companyName,
        valid: Formz.validate([companyName, state.userName]),
      ),
    );
  }
}

演示

现在是时候看到所有的东西都在一起工作了! 祈祷吧 🤞

github.com/VGVentures/…

它正在工作! 🎉

额外的。个性化你的邮件正文内容

你可以个性化你在签到后发送的电子邮件的正文。使用Supabase很容易做到这一点。在你的Supabase控制台中进入 项目设置 ,然后按照步骤操作。

摘要

如果你正在寻找典型的登录选项,如 email/passwordTwitterFacebookGoogleApple ,Supabase是一个伟大的工具。你可以看到这里Supabase可以提供的所有认证供应商。

另外,如果你喜欢在SQL数据库中管理你的数据,这是一个很好的选择,因为Supabase使用PostgreSQL数据库,与Firebase相反。你可以在官方文档中找到更多信息。

我强烈建议你检查Supabase提供的所有选项,因为有大量的选项和大量的文档。这里有几个参考链接。

希望你喜欢它! 如果你有兴趣测试这个应用程序,你可以在GitHub仓库中找到所有的测试工作。

谢谢你的时间! 😃 编码愉快!


www.deepl.com 翻译