快速入门|使用MemFire Cloud构建Flutter应用程序

MemFireDB,带你体验不一样的云端飞翔。

MemFire Cloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专注于编写前端应用程序代码,加速WEB或APP应用开发。

快速入门|使用MemFire Cloud构建Flutter应用程序

此示例提供了使用 MemFire Cloud 和 Flutter构建简单用户管理应用程序的步骤。这包括:

  • MemFire Cloud数据库:用于存储用户数据的 MemFireDB数据库。
  • MemFire Cloud用户验证:用户可以使用邮箱密码登录。
  • MemFire Cloud存储:用户可以上传照片。
  • 行级安全策略:数据受到保护,因此个人只能访问自己的数据。
  • 即时API:创建数据库表时会自动生成 API。

在本指南结束时,您将拥有一个允许用户登录和更新个人基本资料的应用程序:

快速入门|使用MemFire Cloud构建Flutter应用程序_第1张图片

创建应用

目的:我们的应用就是通过在这里创建的应用来获得数据库、云存储等一系列资源,并将获得该应用专属的API访问链接和访问密钥,用户可以轻松的与以上资源进行交互。

登录 链接: https://cloud.memfiredb.com 创建应用

快速入门|使用MemFire Cloud构建Flutter应用程序_第2张图片

创建数据表

点击应用,视图化创建数据表

  1. 创建profiles表;

在数据表页面,点击“新建数据表”,页面配置如下:

快速入门|使用MemFire Cloud构建Flutter应用程序_第3张图片

其中profiles表字段id和auth.users表中的uuid外键关联。

  1. 开启Profiles的RLS数据安全访问规则;
    选中创建的Profiles表,点击表权限栏,如下图所示,点击"启用RLS"按钮

快速入门|使用MemFire Cloud构建Flutter应用程序_第4张图片

  1. 允许每个用户可以查看公共的个人信息资料;
    点击"新规则"按钮,在弹出弹框中,选择"为所有用户启用访问权限",输入策略名称,选择"SELECT(查询)"操作,点击“创建策略”按钮,如下图。

快速入门|使用MemFire Cloud构建Flutter应用程序_第5张图片

  1. 仅允许用户增删改查本人的个人资料信息;
    点击"新规则"按钮,在弹出弹框中,选择"根据用户ID为用户启用访问权限",输入策略名称,选择"ALL(所有)"操作,点击“创建策略”按钮,如下图。
    快速入门|使用MemFire Cloud构建Flutter应用程序_第6张图片

创建avatars存储桶

创建云存储的存储桶,用来存储用户的头像图片,涉及操作包括:

  1. 创建一个存储桶avatars
    在该应用的云存储导航栏,点击“新建Bucket”按钮,创建存储桶avatars。

快速入门|使用MemFire Cloud构建Flutter应用程序_第7张图片

  1. 允许每个用户可以查看存储桶avatars
    选中存储桶avatars,切换到权限设置栏,点击“新规则”按钮,弹出策略编辑弹框,选择“自定义”,如下图所示:

快速入门|使用MemFire Cloud构建Flutter应用程序_第8张图片

选择SELECT操作,输入策略名称,点击“生成策略”按钮,如下图所示。

快速入门|使用MemFire Cloud构建Flutter应用程序_第9张图片

  1. 允许用户上传存储桶avatars;
    选中存储桶avatars,切换到权限设置栏,点击“新规则”按钮,弹出策略编辑弹框,选择“自定义”,如下图所示:

快速入门|使用MemFire Cloud构建Flutter应用程序_第10张图片
选择INSERT操作,输入策略名称,点击“生成策略”按钮,如下图所示。

快速入门|使用MemFire Cloud构建Flutter应用程序_第11张图片
查看结果

快速入门|使用MemFire Cloud构建Flutter应用程序_第12张图片

所有数据表及RLS的sql(策略名称用英文代替)

-- Create a table for public "profiles"
create table profiles (
  id uuid references auth.users not null,
  updated_at timestamp with time zone,
  username text unique,
  avatar_url text,
  website text,

  primary key (id),
  unique(username),
);

alter table profiles enable row level security;

create policy "Public profiles are viewable by everyone."
  on profiles for select
  using ( true );

create policy "Users can insert their own profile."
  on profiles for insert
  with check ( auth.uid() = id );

create policy "Users can update own profile."
  on profiles for update
  using ( auth.uid() = id );
-- Set up Storage!
insert into storage.buckets (id, name)
values ('avatars', 'avatars');

create policy "Avatar images are publicly accessible."
  on storage.objects for select
  using ( bucket_id = 'avatars' );

create policy "Anyone can upload an avatar."
  on storage.objects for insert
  with check ( bucket_id = 'avatars' );

获取 API密钥

现在您已经创建了一些数据库表,您可以使用自动生成的 API 插入数据。我们只需要从API设置中获取URL和anon的密钥。

在应用->概括页面,获取服务地址以及token信息。

快速入门|使用MemFire Cloud构建Flutter应用程序_第13张图片

Anon(公开)密钥是客户端API密钥。它允许“匿名访问”您的数据库,直到用户登录。登录后,密钥将切换到用户自己的登录令牌。这将为数据启用行级安全性。

注意:service_role(秘密)密钥可以绕过任何安全策略完全访问您的数据。这些密钥必须保密,并且要在服务器环境中使用,决不能在客户端或浏览器上使用。在后续示例代码中,需要提供supabaseUrl和supabaseKey。

构建应用程序

让我们从头开始构建 Flutter应用程序。

初始化一个 Flutter应用程序

我们可以flutter create用来初始化一个名为memfiredb_quickstart:

flutter pub add supabase_flutter

再运行以下命令安装依赖项。

flutter pub get

设置深层链接

现在我们已经安装了依赖项,让我们设置深度链接,以便通过魔术链接或 OAuth 登录的用户可以返回应用程序。

快速入门|使用MemFire Cloud构建Flutter应用程序_第14张图片

com.memfire.flutterquickstart://login-callback

这就是配置Memfire Cloud的结尾,其余的是平台特定的设置

对于 Android,添加一个意图过滤器以启用深链接:

android/app/src/main/AndroidManifest.xml

<manifest ...>
  <!-- ... 其他的标签 -->
  <application ...>
    <activity ...>
      <!-- ... 其他的标签 -->

      <!-- 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="com.memfire.flutterquickstart"
          android:host="login-callback" />
      </intent-filter>

    </activity>
  </application>
</manifest>

对于 iOS,添加 CFBundleURLTypes 以启用深链接:

ios/Runner/Info.plist

<!-- ... 其他的标签 -->
<plist>
<dict>
  <!-- ... 其他的标签 -->

  <!-- Add this array for Deep Links -->
    <key>CFBundleURLTypes</key>
  <array>
    <dict>
      <key>CFBundleTypeRole</key>
      <string>Editor</string>
      <key>CFBundleURLSchemes</key>
      <array>
        <string>com.memfire.flutterquickstart</string>
      </array>
    </dict>
  </array>
  <!-- ... 其他的标签 -->
</dict>
</plist>

主要功能

我们需要创建一个可以访问我们应用程序数据的客户端,我们使用了Supabase 客户端,使用他生态里提供的功能(登录、注册、增删改查等)去进行交互。创建一个可以访问我们应用程序数据的客户端需要接口的地址(URL)和一个数据权限的令牌(ANON_KEY),我们需要去应用的概览里面去获取这两个参数然后配置到main.dart里面去。

lib/main.dart

import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:memfiredb_quickstart/pages/account_page.dart';
import 'package:memfiredb_quickstart/pages/login_page.dart';
import 'package:memfiredb_quickstart/pages/splash_page.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Supabase.initialize(
    // TODO: Replace credentials with your own
    url: 'YOUR_SUPABASE_URL',
    anonKey: 'YOUR_SUPABASE_ANON_KEY',
  );
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Supabase Flutter',
      theme: ThemeData.dark().copyWith(
        primaryColor: Colors.green,
        elevatedButtonTheme: ElevatedButtonThemeData(
          style: ElevatedButton.styleFrom(
            onPrimary: Colors.white,
            primary: Colors.green,
          ),
        ),
      ),
      initialRoute: '/',
      routes: <String, WidgetBuilder>{
        '/': (_) => const SplashPage(),
        '/login': (_) => const LoginPage(),
        '/account': (_) => const AccountPage(),
      },
    );
  }
}

设置AuthState

为了处理 Android 和 iOS 的深链接,让我们创建一个可以做到这一点的类。创建一个AuthState类继承supabase_flutter的SupabaseAuthState类,使我们可以响应各种深链接事件。

lib/components/auth_state.dart

import 'package:flutter/material.dart';
import 'package:supabase/supabase.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:memfiredb_quickstart/utils/constants.dart';

class AuthState<T extends StatefulWidget> extends SupabaseAuthState<T> {
  @override
  void onUnauthenticated() {
    if (mounted) {
      Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
    }
  }

  @override
  void onAuthenticated(Session session) {
    if (mounted) {
      Navigator.of(context)
          .pushNamedAndRemoveUntil('/account', (route) => false);
    }
  }

  @override
  void onPasswordRecovery(Session session) {}

  @override
  void onErrorAuthenticating(String message) {
    context.showErrorSnackBar(message: message);
  }
}

设置AuthRequiredState

我们可能希望仅在用户登录时才向用户显示某些页面。为此,我们可以创建一个AuthRequiredState类继承supabase_flutter包中的SupabaseAuthRequiredState类。在需要对用户进行身份验证的页面继承该类即可。

lib/components/auth_required_state.dart

import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

class AuthRequiredState<T extends StatefulWidget>
    extends SupabaseAuthRequiredState<T> {
  @override
  void onUnauthenticated() {
    /// Users will be sent back to the LoginPage if they sign out.
    if (mounted) {
      /// Users will be sent back to the LoginPage if they sign out.
      Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
    }
  }
}

让我们还创建一个常量文件,以便更轻松地使用 Supbase 客户端。我们还将包含一个扩展方法声明,以showSnackBar使用一行代码调用。

lib/utils/constants.dart

import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

final supabase = Supabase.instance.client;

extension ShowSnackBar on BuildContext {
  void showSnackBar({
    required String message,
    Color backgroundColor = Colors.white,
  }) {
    ScaffoldMessenger.of(this).showSnackBar(SnackBar(
      content: Text(message),
      backgroundColor: backgroundColor,
    ));
  }

  void showErrorSnackBar({required String message}) {
    showSnackBar(message: message, backgroundColor: Colors.red);
  }
}

设置启动页面

让我们创建一个启动页面,在用户打开应用程序后立即显示给他们。此启动页面继承AuthState以根据用户的身份验证状态将用户重定向到适当的页面。

lib/pages/splash_page.dart

标题文本样式列表图片链接目录代码片表格注脚注释自定义列表LaTeX 数学公式插入甘特图插入UML图插入Mermaid流程图插入Flowchart流程图插入类图快捷键
代码片复制

下面展示一些 内联代码片

import 'package:flutter/material.dart';
import 'package:memfiredb_quickstart/components/auth_state.dart';

class SplashPage extends StatefulWidget {
  const SplashPage({Key? key}) : super(key: key);

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

class _SplashPageState extends AuthState<SplashPage> {
  @override
  void initState() {
    recoverSupabaseSession();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(child: CircularProgressIndicator()),
    );
  }
}

设置登录页面

让我们创建一个 Flutter 组件来管理登录和注册。我们将使用 Magic Links,因此用户无需使用密码即可使用电子邮件登录。该页面也将继承AuthState,因为它将处理用户登录。

lib/pages/login_page.dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:supabase/supabase.dart';
import 'package:memfiredb_quickstart/components/auth_state.dart';
import 'package:memfiredb_quickstart/utils/constants.dart';

class LoginPage extends StatefulWidget {
  const LoginPage({Key? key}) : super(key: key);

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

class _LoginPageState extends AuthState<LoginPage> {
  bool _isLoading = false;
  late final TextEditingController _emailController;

  Future<void> _signIn() async {
    setState(() {
      _isLoading = true;
    });
    final response = await supabase.auth.signIn(
        email: _emailController.text,
        options: AuthOptions(
            redirectTo: kIsWeb
                ? null
                : 'com.memfire.flutterquickstart://login-callback/'));
    final error = response.error;
    if (error != null) {
      context.showErrorSnackBar(message: error.message);
    } else {
      context.showSnackBar(message: 'Check your email for login link!');
      _emailController.clear();
    }

    setState(() {
      _isLoading = false;
    });
  }

  @override
  void initState() {
    super.initState();
    _emailController = TextEditingController();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sign In')),
      body: ListView(
        padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12),
        children: [
          const Text('Sign in via the magic link with your email below'),
          const SizedBox(height: 18),
          TextFormField(
            controller: _emailController,
            decoration: const InputDecoration(labelText: 'Email'),
          ),
          const SizedBox(height: 18),
          ElevatedButton(
            onPressed: _isLoading ? null : _signIn,
            child: Text(_isLoading ? 'Loading' : 'Send Magic Link'),
          ),
        ],
      ),
    );
  }
}

设置账户页面

用户登录后,我们可以允许他们编辑他们的个人资料详细信息并管理他们的帐户。让我们为此创建一个新的组件account_page.dart。请注意,此页面将继承AuthRequiredState,因为用户需要经过身份验证才能查看此页面。

lib/pages/account_page.dart

import 'package:flutter/material.dart';
import 'package:supabase/supabase.dart';
import 'package:memfiredb_quickstart/components/auth_required_state.dart';
import 'package:memfiredb_quickstart/utils/constants.dart';

class AccountPage extends StatefulWidget {
  const AccountPage({Key? key}) : super(key: key);

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

class _AccountPageState extends AuthRequiredState<AccountPage> {
  final _usernameController = TextEditingController();
  final _websiteController = TextEditingController();
  String? _userId;
  String? _avatarUrl;
  var _loading = false;

  /// Called once a user id is received within `onAuthenticated()`
  Future<void> _getProfile(String userId) async {
    setState(() {
      _loading = true;
    });
    final response = await supabase
        .from('profiles')
        .select()
        .eq('id', userId)
        .single()
        .execute();
    final error = response.error;
    if (error != null && response.status != 406) {
      context.showErrorSnackBar(message: error.message);
    }
    final data = response.data;
    if (data != null) {
      _usernameController.text = (data['username'] ?? '') as String;
      _websiteController.text = (data['website'] ?? '') as String;
      _avatarUrl = (data['avatar_url'] ?? '') as String;
    }
    setState(() {
      _loading = false;
    });
  }

  /// Called when user taps `Update` button
  Future<void> _updateProfile() async {
    setState(() {
      _loading = true;
    });
    final userName = _usernameController.text;
    final website = _websiteController.text;
    final user = supabase.auth.currentUser;
    final updates = {
      'id': user!.id,
      'username': userName,
      'website': website,
      'updated_at': DateTime.now().toIso8601String(),
    };
    final response = await supabase.from('profiles').upsert(updates).execute();
    final error = response.error;
    if (error != null) {
      context.showErrorSnackBar(message: error.message);
    } else {
      context.showSnackBar(message: 'Successfully updated profile!');
    }
    setState(() {
      _loading = false;
    });
  }

  Future<void> _signOut() async {
    final response = await supabase.auth.signOut();
    final error = response.error;
    if (error != null) {
      context.showErrorSnackBar(message: error.message);
    }
    Navigator.of(context).pushReplacementNamed('/login');
  }

  /// Called when image has been uploaded to Supabase storage from within Avatar widget
  Future<void> _onUpload(String imageUrl) async {
    final response = await supabase.from('profiles').upsert({
      'id': _userId,
      'avatar_url': imageUrl,
    }).execute();
    final error = response.error;
    if (error != null) {
      context.showErrorSnackBar(message: error.message);
    }
    setState(() {
      _avatarUrl = imageUrl;
    });
    context.showSnackBar(message: 'Updated your profile image!');
  }

  @override
  void onAuthenticated(Session session) {
    final user = session.user;
    if (user != null) {
      _userId = user.id;
      _getProfile(user.id);
    }
  }

  @override
  void onUnauthenticated() {
    Navigator.of(context).pushReplacementNamed('/login');
  }

  @override
  void dispose() {
    _usernameController.dispose();
    _websiteController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Profile')),
      body: ListView(
        padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12),
        children: [
          TextFormField(
            controller: _usernameController,
            decoration: const InputDecoration(labelText: 'User Name'),
          ),
          const SizedBox(height: 18),
          TextFormField(
            controller: _websiteController,
            decoration: const InputDecoration(labelText: 'Website'),
          ),
          const SizedBox(height: 18),
          ElevatedButton(
              onPressed: _updateProfile,
              child: Text(_loading ? 'Saving...' : 'Update')),
          const SizedBox(height: 18),
          ElevatedButton(onPressed: _signOut, child: const Text('Sign Out')),
        ],
      ),
    );
  }
}

完成后,所有环境配置好后,按照如下步骤打开浏览器运行

快速入门|使用MemFire Cloud构建Flutter应用程序_第15张图片

快速入门|使用MemFire Cloud构建Flutter应用程序_第16张图片

实现:上传头像及更新用户信息

每个 MemFire Cloud项目都配置了存储,用于管理照片和视频等大文件。

将图片上传功能添加到帐户页面

我们将使用image_picker插件从设备中选择图像。

运行以下命令进行安装。

flutter pub add image_picker

根据平台的不同,使用image_picker需要一些额外的准备。按照 README.md 上image_picker有关如何为您使用的平台进行设置的说明进行操作。

完成上述所有操作后,就该深入研究编码了。

创建上传小组件

lib/components/avatar.dart

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:memfiredb_quickstart/utils/constants.dart';

class Avatar extends StatefulWidget {
  const Avatar({
    Key? key,
    required this.imageUrl,
    required this.onUpload,
  }) : super(key: key);

  final String? imageUrl;
  final void Function(String) onUpload;

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

class _AvatarState extends State<Avatar> {
  bool _isLoading = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        if (widget.imageUrl == null)
          Container(
            width: 150,
            height: 150,
            color: Colors.grey,
            child: const Center(
              child: Text('No Image'),
            ),
          )
        else
          Image.network(
            widget.imageUrl!,
            width: 150,
            height: 150,
            fit: BoxFit.cover,
          ),
        ElevatedButton(
          onPressed: _isLoading ? null : _upload,
          child: const Text('Upload'),
        ),
      ],
    );
  }

  Future<void> _upload() async {
    final _picker = ImagePicker();
    final imageFile = await _picker.pickImage(
      source: ImageSource.gallery,
      maxWidth: 300,
      maxHeight: 300,
    );
    if (imageFile == null) {
      return;
    }
    setState(() => _isLoading = true);
    final bytes = await imageFile.readAsBytes();
    final fileName = '${DateTime.now().millisecondsSinceEpoch}-${imageFile.name}';
    final filePath = fileName;
    final response =
        await supabase.storage.from('avatars').uploadBinary(filePath, bytes);

    setState(() => _isLoading = false);

    final error = response.error;
    if (error != null) {
      context.showErrorSnackBar(message: error.message);
      return;
    }
    final imageUrlResponse =
        supabase.storage.from('avatars').getPublicUrl(filePath);
    widget.onUpload(imageUrlResponse.data!);
  }
}

添加新的组件

然后我们可以将组件和逻辑添加到帐户页面,用于在用户上传新头像时更新avatar_url。

lib/pages/account_page.dart

import 'package:flutter/material.dart';
import 'package:supabase/supabase.dart';
import 'package:memfiredb_quickstart/components/auth_required_state.dart';
import 'package:memfiredb_quickstart/components/avatar.dart';
import 'package:memfiredb_quickstart/utils/constants.dart';

class AccountPage extends StatefulWidget {
  const AccountPage({Key? key}) : super(key: key);

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

class _AccountPageState extends AuthRequiredState<AccountPage> {
  final _usernameController = TextEditingController();
  final _websiteController = TextEditingController();
  String? _userId;
  String? _avatarUrl;
  var _loading = false;

  /// Called once a user id is received within `onAuthenticated()`
  Future<void> _getProfile(String userId) async {
    setState(() {
      _loading = true;
    });
    final response = await supabase
        .from('profiles')
        .select()
        .eq('id', userId)
        .single()
        .execute();
    final error = response.error;
    if (error != null && response.status != 406) {
      context.showErrorSnackBar(message: error.message);
    }
    final data = response.data;
    if (data != null) {
      _usernameController.text = (data['username'] ?? '') as String;
      _websiteController.text = (data['website'] ?? '') as String;
      _avatarUrl = (data['avatar_url'] ?? '') as String;
    }
    setState(() {
      _loading = false;
    });
  }

  /// Called when user taps `Update` button
  Future<void> _updateProfile() async {
    setState(() {
      _loading = true;
    });
    final userName = _usernameController.text;
    final website = _websiteController.text;
    final user = supabase.auth.currentUser;
    final updates = {
      'id': user!.id,
      'username': userName,
      'website': website,
      'updated_at': DateTime.now().toIso8601String(),
    };
    final response = await supabase.from('profiles').upsert(updates).execute();
    final error = response.error;
    if (error != null) {
      context.showErrorSnackBar(message: error.message);
    } else {
      context.showSnackBar(message: 'Successfully updated profile!');
    }
    setState(() {
      _loading = false;
    });
  }

  Future<void> _signOut() async {
    final response = await supabase.auth.signOut();
    final error = response.error;
    if (error != null) {
      context.showErrorSnackBar(message: error.message);
    }
    Navigator.of(context).pushReplacementNamed('/login');
  }

  /// Called when image has been uploaded to Supabase storage from within Avatar widget
  Future<void> _onUpload(String imageUrl) async {
    final response = await supabase.from('profiles').upsert({
      'id': _userId,
      'avatar_url': imageUrl,
    }).execute();
    final error = response.error;
    if (error != null) {
      context.showErrorSnackBar(message: error.message);
    }
    setState(() {
      _avatarUrl = imageUrl;
    });
    context.showSnackBar(message: 'Updated your profile image!');
  }

  @override
  void onAuthenticated(Session session) {
    final user = session.user;
    if (user != null) {
      _userId = user.id;
      _getProfile(user.id);
    }
  }

  @override
  void onUnauthenticated() {
    Navigator.of(context).pushReplacementNamed('/login');
  }

  @override
  void dispose() {
    _usernameController.dispose();
    _websiteController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Profile')),
      body: ListView(
        padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12),
        children: [
          Avatar(
            imageUrl: _avatarUrl,
            onUpload: _onUpload,
          ),
          const SizedBox(height: 18),
          TextFormField(
            controller: _usernameController,
            decoration: const InputDecoration(labelText: 'User Name'),
          ),
          const SizedBox(height: 18),
          TextFormField(
            controller: _websiteController,
            decoration: const InputDecoration(labelText: 'Website'),
          ),
          const SizedBox(height: 18),
          ElevatedButton(
              onPressed: _updateProfile,
              child: Text(_loading ? 'Saving...' : 'Update')),
          const SizedBox(height: 18),
          ElevatedButton(onPressed: _signOut, child: const Text('Sign Out')),
        ],
      ),
    );
  }
}

恭喜,就是这样!您现在已经使用 Flutter 和 Memefire Cloud 构建了一个功能齐全的用户管理应用程序!

你可能感兴趣的:(MemFireDB,flutter,android,java,数据库,ios)