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

将 Supabase 作为下一个后端服务 本文介绍了如何使用 Supabase 作为后端服务,使开发人员可以更快地构建和部署应用程序,无需配置数据库或编写复杂的身份验证代码。将使用 Nuxt.js 和 Supabase,以实现一个图床网站为例,来演示如何在前端中使用 Supabase API 和 Storage 服务。 阅读详情

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

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

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

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

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

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

在这里插入图片描述

创建应用

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

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

在这里插入图片描述

创建数据表

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

  1. 创建profiles表;

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

在这里插入图片描述

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

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

在这里插入图片描述

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

在这里插入图片描述

  1. 仅允许用户增删改查本人的个人资料信息;
    点击"新规则"按钮,在弹出弹框中,选择"根据用户ID为用户启用访问权限",输入策略名称,选择"ALL(所有)"操作,点击“创建策略”按钮,如下图。
    在这里插入图片描述

创建avatars存储桶

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

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述
选择INSERT操作,输入策略名称,点击“生成策略”按钮,如下图所示。

在这里插入图片描述
查看结果

在这里插入图片描述

所有数据表及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信息。

在这里插入图片描述

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

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

构建应用程序

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

初始化一个 Flutter应用程序

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

flutter pub add supabase_flutter

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

flutter pub get

设置深层链接

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

在这里插入图片描述

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项目都配置了存储,用于管理照片和视频等大文件。

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

我们将使用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 构建了一个功能齐全的用户管理应用程序!

Supabase: 一个开源的后端服务,可以让你更快地构建产品 Supabase 是一个开源的 Firebase 替代品,提供了一系列的后端功能,让你可以更快地构建产品。数据库:Supabase 使用 PostgreSQL 作为数据库,支持 SQL 和 RESTful API 访问。认证:Supabase 提供了一个完整的认证系统,支持邮箱、手机号、第三方服务等多种登录方式。实时订阅:Supabase 允许你通过 WebSocket 实现实时数据同步,无需编写额外的代码。存储:Supabase 提供了一个对象存储服务,可以方便地上传、下载和管理文件。 阅读详情

相关推荐

Supabase 自托管部署实践

Supabase 是一个开源的后端即服务(BaaS)平台,提供了一系列工具和服务,帮助开发者快速构建和部署应用程序。实时数据同步和协作应用Supabase 提供了实时功能,可以让开发者轻松实现数据同步和实时更新,这对于需要协作和同步功能的应用程序非常有用。例如:聊天应用:用户之间的消息可以实时同步。协作编辑器:多个用户可以同时编辑文档,并实时看到对方的修改。实时仪表盘:数据变化可以即时反映在用户界面上。2. Web 和移动应用的后端。

weixin_38255219的博客 3815

Supabase全面介绍与使用指南

Supabase 提供了一个开箱即用的后端解决方案,基于成熟的 PostgreSQL 数据库构建而成。与 Firebase 不同,Supabase 是完全开源的,开发者可以选择直接使用官方托管的服务,也可以自建部署,享受更高的灵活性与自由度。其目标是让前端开发者能够专注于业务逻辑,而无需担心后端服务器的部署与运维工作。Supabase 是一个以 PostgreSQL 为核心的开源 BaaS 平台,为开发者提供了一整套后端服务,包括身份验证、实时数据同步、文件存储和边缘函数等。

斯黄人民的博客 6397

软件工程案例教程·韩万江【课后习题】

!!!!

weixin_57858263的博客 6419

后端代码不用写了?前端操作数据库?一文精通Supabase,实战教程+本地部署

2025年最火的后端开源项目那必须是Supabase。Supabase是一个开源的后端级服务框架,在强大的PostgreSQL数据库的基础上,封装了用户认证、文件存储、可视化的运维面板等功能,为开发者提供了一整套开箱即用的后端基础设施。Supabase在Github上面有恐怖的9万star,这已经是整个Github上面最顶级的开源项目之一了。总的来说,Supabase为开发者提供了三大部分的能力:后端、前端与免费的云服务。Supabase在后端提供数据库、文件存储、边缘函数、用户鉴权等各种基础设施。

techshrimp的博客 5914

FPGA实现高效数字下变频:CORDIC与CIC滤波器的Verilog优化设计

本文深入探讨了FPGA实现高效数字下变频的技术方案,重点介绍了CORDIC算法与CIC滤波器的Verilog优化设计。通过详细的算法原理分析、FPGA实现技巧和实战经验分享,展示了如何利用CORDIC的旋转运算替代乘法操作,以及CIC滤波器在降采样中的高效性能。文章还提供了完整的系统集成方案和资源优化策略,为无线通信和雷达信号处理领域的工程师提供了实用参考。

redis7keeper的博客 723

三相SVPWM逆变器双闭环PI解耦控制:从dq坐标系到参数整定实战

本文深入解析了三相SVPWM逆变器的双闭环PI解耦控制技术。通过引入dq坐标系,将交流量转换为直流量,并结合前馈补偿实现电流环的解耦控制。文章详细阐述了从数学模型推导、控制器设计到参数整定与SVPWM实现的完整流程,为工程师提供了从理论到实战的系统性指导。

weixin_28292761的博客 405

高效全栈开发,MemFire Cloud让项目快人一步!

是一款专为懒人开发者设计的全栈开发工具,简化了开发过程中最复杂的部分,让开发者可以专注于核心功能的实现。它提供了一站式的解决方案,从后端服务到前端功能,甚至包括云数据库第三方认证服务云函数对象存储等,无论是个人项目还是商业产品,都能轻松实现。这款国产工具已经稳定运行了三年,是开发者们信赖的BaaS(Backend as a Service)平台,特别适合那些想快速启动项目、减少繁琐开发工作的开发者。

weixin_44957042的博客 952

sentaurus学习笔记(VDMOS仿真)(三)

在Sentaurus中,这通常意味着在2D仿真中,x轴指向下,y轴指向右。定义了在2D模拟中,网格尺寸按照一定的增长率进行扩展,这里的增长率为2.0,意味着远离最小尺寸区域的网格单元大小将是最小尺寸的两倍。定义了2D模拟中网格单元的最小尺寸为0.001微米,这有助于控制网格的最小大小,从而影响仿真的精度和计算成本。表示启用中等级别的日志信息输出,这有助于用户了解仿真过程中发生的事件和可能的问题。表示启用保存调试文件,这可以帮助用户在仿真过程中检查和验证模型的状态。这行代码设置了仿真过程中使用的线程数。

qq_52640792的博客 1807

Supabase:从Firebase替代者到“全能“后端平台

强大、可扩展的后端平台,提供实时数据库、身份验证、存储和无服务器功能,以实现快速应用程序开发。

coderroad的博客 2311

String+CytoScape构建PPI网络

1.下载CytoScape Cytoscape是一款专业强大好用的物生信息分析软件,主要达用于专业的研究人员使用,软件主要适用于整合模块化网络和生物科学联系网络图的绘制,支持多种文件格式,导入后就可以直接进行编辑或者分析等操作了,除此之外,软件还提供了强大的编辑工具,支持添加相应的注释信息,软件基于java环境组件打造。 下载地址:https://cytoscape.org/ 因为软件基于java...

xiaobai1_1的博客 1万+

分析攻击流量样本

利用wireshark分析.pcap攻击样本的攻击流程,利用漏洞等。

一段旅途 6844

Supabase安装保姆级教程:小微应用免费postgresql数据库最佳搭档!

使用supabase免费使用postgresql数据库及API查询数据库

技术Z先生的博客 1万+

1、JMP脚本编程全面指南:从基础到高级应用

本博客全面介绍了JMP脚本编程的各个方面,从JMP基础和JSL入门,到数据类型、数据结构、编程方法、数据表操作、图形与三维场景脚本,再到应用程序创建与扩展功能。内容涵盖脚本工具使用、常见任务实现、兼容性说明及未来发展趋势,旨在帮助用户掌握JMP脚本语言的强大功能,提升数据分析的自动化与定制化能力。适合从初学者到高级用户的深入学习与实践参考。

flink9streamer的博客 266

开发者的秘密武器:用MemFire Cloud快速上线项目

简单来说,是一款专为懒人开发者准备的神器。它是一站式的开发平台,提供了丰富的后端即服务(BaaS)功能,包括免费云数据库、第三方认证服务、对象存储、云函数和静态部署。更棒的是,它完全不需要你搭建服务或者开发API,真正实现了**“拎包入住式”开发**。它不仅适合那些技术小白,还特别适合那些想要省时省力的独立开发者。无论你是为了接私活还是实现自己的创意,都能让你事半功倍。在开发的道路上,每个开发者都希望能够高效、便捷地完成项目。的出现,无疑为我们提供了一个强有力的工具。

weixin_44957042的博客 1373

【AI应用实战-WorkBuddy】WorkBuddy 能帮你做什么(一)

文章摘要: WorkBuddy是一款AI工作助手,专为提升效率而设计,适用于经常处理文档、代码、数据分析等重复性任务的职场人士。它能通过自然语言交互快速完成五大核心场景任务: 周报撰写:输入工作要点,10分钟生成专业周报; 代码调试:解析报错信息,3分钟提供解决方案; Excel分析:自动计算数据趋势并生成图表和结论; 自动化脚本:将重复流程转为脚本,节省98%时间; 文档生成:根据代码自动补充README、API文档和注释。 效果对比:传统手动操作耗时1-3小时的任务,WorkBuddy可将时间缩短至10

351

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

轻松掌握“商品后台管理系统”角色权限的配置

关于如何配置和管理中的用户角色和权限的询问,编写了一个简易教程,指导您完成从前端路由和按钮权限配置到在 MemFire Cloud 中轻松设置用户角色和权限的各个步骤。本教程以“商品后台管理系统”中设置的不同角色(超级管理员:可以对商品管理系统的所有数据进行任何操作;普通管理员:可以查看和新增商品;普通用户:只能查看商品)和权限为例来管理商品进行讲解。

weixin_44957042的博客 1151

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

MemFire教程|多表关联查询教程(JS版本)

查询学生的studentId为20222201的课表信息和学校信息当您想要自定义查询主表与关联表的一些字段时,您可以在select里面用逗号把主表的字段隔开,后面加上想要查询的关联表,关联表紧挨着用括号包裹它的字段,也需要用逗号隔开。.select(`id,studentId,结果id: 1,subject: {id: 1,subjectName: '数据库原理',teacherName: '张珊',adress: '计科楼2-1003'},

NimbleX_的博客 436

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

MemFire教程|PostgreSQL RLS介绍

在使用MemFire Cloud的BaaS服务进行应用开发时,比较困扰开发者的是RLS的使用。本篇先从基本概念入手对RLS进行简要介绍,帮助大家理解RLS的工作机制和基本使用方法。

NimbleX_的博客 841

MemFire教程|RLS使用指南

前面讲过PostgreSQL RLS介绍,在使用MemFire Cloud的BaaS服务进行应用开发时,比较困扰开发者的是RLS的使用。本篇从基本的RLS使用以及基本示例入手,让用户理解如何使用RLS。全称:Row level security,行级安全,允许系统管理员为数据库表创建访问策略(policy),以约束数据的可见性。创建策略语法详见:语法说明。当为一个表创建了policy后,相当于为该表增加了一个高优先级的过滤器。

NimbleX_的博客 315

我们为什么要做MemFire Cloud 1 - MemFireDB

MemFireDB,带你体验不一样的云端飞翔。 如果你是一个大学生、程序员、创业者,当你脑子里不时冒出一些千奇百怪的想法,想要自己动手去实现它,却受限于资源,无法开展你的工作,不妨关注一下https://memfiredb.com ,我们正在尝试为您解决这些困扰。 缘起 / 当前的服务模式限制了创造力的发挥 传统云厂商的收费方式对于个人开发者不友好 经常面临这样的困境:当我有了一个很好的idea时,作为程序员,能快速着手去实现它是我最大的优势。但是这些想法需要得到用户的验证就需要快速上线、迭代测试,于是.

NimbleX_的博客 434

免费开源的后端API服务-supabase安装和使用-简直是前端学习者福音

一个开源免费的后端框架,firebase的替代品。可以简单理解类似于,但是不仅仅只提供内容,它还集成了服务订阅、即时API,用户身份认证(包括第三方身份认证,比如使用githubGoogle等账号实现快速登录和注册)、边缘函数、文件存储等功能。截止到2024-7-1的数:68.2K。

商务合作 / 项目定制开发 / 前端一对一培训、答疑 5840

Supabase国内版,MemFire Cloud应用开发

MemFire Cloud应用开发服务,采用开源的Supabase,兼容国内开发生态,内置通用服务,简单易学,加速小程序/移动APP/WEB应用的开发,降低开发/运维成本。

whdxlxl的专栏 3193

MemFire Cloud为何短短几年变成这样?

这个名字可能对你来说还很陌生,但它所代表的便捷和强大,绝对值得你一试。它不仅仅是一个工具,更是你开发路上的得力助手,让你能够更加专注于创意和设计,而不是被技术细节所困扰。现在就访问MemFire Cloud,开启你的懒人开发之旅吧!的成功,是因为它真正理解了开发者的需求,并且提供了切实可行的解决方案。它的故事,是关于创新、效率和用户体验的故事。如果你也是一名追求效率的开发者,那么绝对值得你深入了解。让我们一起期待,在未来的日子里,会给我们带来更多的惊喜。

weixin_44957042的博客 721

开发者不再焦虑:MemFire Cloud如何帮你减少工作量

是一款国产的后端即服务(BaaS)工具,已经稳定运行三年。第三方认证服务:支持二十多种认证方式,包括微信登录,让用户快速登录你的应用。对象存储:文件、图片、视频上传和管理,从未如此简单。云函数:复杂逻辑?一个云函数帮你搞定。静态部署:直接托管前端资源,无需操心配置。最佳实践源码:提供微信小程序、Web 应用等十几种应用模板,下载后直接运行,效率翻倍!开发不应该是一件让人焦虑的事,而是一次创造的旅程。用最简单的方式,帮助开发者减少工作量,专注于实现自己的创意。

weixin_44957042的博客 1002

MemFire教程|Express框架在云函数开发中的应用指南

MemFire Cloud 开发过程中,一些业务场景需要用到云函数,下面是使用Express开发云函数的方法。

NimbleX_的博客 1142

用 Supabase CLI 进行本地开发环境搭建

supabase官方基于AWS云上的托管服务,因没有中国区的选择实在太慢了。

Shion's bla bla blog 5443
上一篇: 超轻量级的VSCode插件,数据库想用就用~
下一篇: 系统公告 | MemFire Cloud推出微信小程序SDK
NimbleX_
博客等级 码龄6年 241粉丝 82原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值