Supadart Typesafe Supabase Flutter Queries

WHAT TO KNOW - Sep 7 - - Dev Community

Supadart: Typesafe Supabase Queries in Flutter

Introduction

Developing mobile applications often involves interacting with databases to store and retrieve user data, preferences, and application state. Supabase, a Firebase alternative, provides a robust backend-as-a-service (BaaS) solution with a PostgreSQL database and powerful tools for building real-time applications. Flutter, a popular cross-platform framework, offers an intuitive and efficient way to build native-looking mobile apps.

Combining Supabase with Flutter leads to a powerful development stack, but the process of querying the database can become cumbersome and error-prone without proper safeguards. Supadart emerges as a game-changer, introducing a layer of type safety and code clarity to your Flutter-Supabase interactions.

Why Type Safety Matters

Type safety, in essence, ensures that the data you work with adheres to specific types defined in your code. This helps you catch potential errors early, reducing bugs and making your code more reliable. In the context of Supabase queries, type safety helps prevent:

  • Incorrect data types: Attempting to assign a string to an integer field or vice versa.
  • Missing fields: Omitting required fields when creating or updating database records.
  • Unexpected data formats: Receiving data in a format not compatible with your Flutter models.

Introducing Supadart

Supadart is a powerful library that brings the benefits of type safety to your Supabase queries within Flutter. It provides a clean and intuitive API for defining your database schema, generating typesafe models, and executing queries in a secure and structured manner.

Key Features of Supadart

  1. Schema Definition: Supadart allows you to define your Supabase database schema using a simple declarative syntax. This ensures that your code always reflects the actual database structure.

  2. Automatic Model Generation: Based on your defined schema, Supadart automatically generates Dart models that represent your database tables. These models enforce type safety, preventing accidental type mismatches.

  3. Type-Safe Queries: Supadart offers a type-safe query builder that allows you to write queries using familiar SQL syntax but with the added benefits of type checking and code completion.

  4. Data Validation: Supadart integrates seamlessly with popular validation libraries like "built_value" to ensure data integrity and prevent invalid entries from reaching your database.

  5. Real-Time Updates: Supadart supports real-time updates, allowing your Flutter app to automatically react to changes in the Supabase database.

Getting Started with Supadart

Let's explore a step-by-step guide to integrate Supadart into your Flutter project and start building type-safe Supabase queries.

1. Set Up Supabase

First, you need a Supabase project. If you don't have one, create a free account at https://supabase.com/. Create a new database and define your tables and their corresponding columns.

2. Set Up Flutter Project

Create a new Flutter project using the flutter create command.

3. Install Supadart

Add the Supadart package to your pubspec.yaml file:

dependencies:
  # ... other dependencies
  supadart: ^x.x.x # Replace with the latest version
Enter fullscreen mode Exit fullscreen mode

Run flutter pub get to fetch the package.

4. Define Your Database Schema

Create a new file, for example, schema.dart, to define your database schema using Supadart's syntax:

import 'package:supadart/supadart.dart';

final schema = Schema(
  tables: {
    'users': Table(
      columns: {
        'id': Column(type: ColumnType.text, isPrimaryKey: true),
        'username': Column(type: ColumnType.text),
        'email': Column(type: ColumnType.text),
      },
    ),
  },
);
Enter fullscreen mode Exit fullscreen mode

5. Generate Typesafe Models

Run the following command to generate type-safe Dart models from your schema:

flutter pub run build_runner build
Enter fullscreen mode Exit fullscreen mode

6. Initialize Supabase Client

In your main app file, initialize the Supabase client using your project's credentials:

import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:supadart/supadart.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Replace with your actual Supabase credentials
  await Supabase.initialize(
    url: 'your-supabase-url',
    anonKey: 'your-supabase-anon-key',
  );

  runApp(MyApp());
}
Enter fullscreen mode Exit fullscreen mode

7. Use Supadart for Type-Safe Queries

Now you can use Supadart to interact with your database in a type-safe manner. Let's look at some examples:

a) Fetching Data

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

class UserList extends StatefulWidget {
  @override
  _UserListState createState() => _UserListState();
}

class _UserListState extends State
<userlist>
 {
  final _supabase = Supabase.instance.client;

  Future
 <list<user>
  &gt; _fetchUsers() async {
    final query = _supabase.from('users').select('*');
    final response = await query.execute();
    return response.map((row) =&gt; User.fromJson(row)).toList();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Users')),
      body: FutureBuilder
  <list<user>
   &gt;(
        future: _fetchUsers(),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            return ListView.builder(
              itemCount: snapshot.data!.length,
              itemBuilder: (context, index) {
                final user = snapshot.data![index];
                return ListTile(
                  title: Text(user.username),
                  subtitle: Text(user.email),
                );
              },
            );
          } else if (snapshot.hasError) {
            return Text('Error: ${snapshot.error}');
          } else {
            return CircularProgressIndicator();
          }
        },
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

b) Inserting Data

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

class AddUser extends StatefulWidget {
  @override
  _AddUserState createState() =&gt; _AddUserState();
}

class _AddUserState extends State
   <adduser>
    {
  final _supabase = Supabase.instance.client;
  final _formKey = GlobalKey
    <formstate>
     ();

  final _usernameController = TextEditingController();
  final _emailController = TextEditingController();

  Future
     <void>
      _addUser() async {
    final isValid = _formKey.currentState!.validate();
    if (isValid) {
      final user = User(
        username: _usernameController.text,
        email: _emailController.text,
      );
      final response = await _supabase.from('users').insert(user.toJson());
      // Handle the response (e.g., show a success message)
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Add User')),
      body: Form(
        key: _formKey,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            children: [
              TextFormField(
                controller: _usernameController,
                decoration: InputDecoration(labelText: 'Username'),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter a username';
                  }
                  return null;
                },
              ),
              TextFormField(
                controller: _emailController,
                decoration: InputDecoration(labelText: 'Email'),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter an email';
                  }
                  return null;
                },
              ),
              ElevatedButton(
                onPressed: _addUser,
                child: Text('Add User'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

c) Updating Data

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

class EditUser extends StatefulWidget {
  final User user;

  const EditUser({Key? key, required this.user}) : super(key: key);

  @override
  _EditUserState createState() =&gt; _EditUserState();
}

class _EditUserState extends State
      <edituser>
       {
  final _supabase = Supabase.instance.client;
  final _formKey = GlobalKey
       <formstate>
        ();

  final _usernameController = TextEditingController();
  final _emailController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _usernameController.text = widget.user.username;
    _emailController.text = widget.user.email;
  }

  Future
        <void>
         _updateUser() async {
    final isValid = _formKey.currentState!.validate();
    if (isValid) {
      final updatedUser = User(
        id: widget.user.id,
        username: _usernameController.text,
        email: _emailController.text,
      );
      final response = await _supabase
          .from('users')
          .update(updatedUser.toJson())
          .eq('id', widget.user.id);
      // Handle the response (e.g., show a success message)
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Edit User')),
      body: Form(
        key: _formKey,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            children: [
              TextFormField(
                controller: _usernameController,
                decoration: InputDecoration(labelText: 'Username'),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter a username';
                  }
                  return null;
                },
              ),
              TextFormField(
                controller: _emailController,
                decoration: InputDecoration(labelText: 'Email'),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter an email';
                  }
                  return null;
                },
              ),
              ElevatedButton(
                onPressed: _updateUser,
                child: Text('Update User'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

d) Deleting Data

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

class DeleteUser extends StatefulWidget {
  final User user;

  const DeleteUser({Key? key, required this.user}) : super(key: key);

  @override
  _DeleteUserState createState() =&gt; _DeleteUserState();
}

class _DeleteUserState extends State
         <deleteuser>
          {
  final _supabase = Supabase.instance.client;

  Future
          <void>
           _deleteUser() async {
    final response = await _supabase
        .from('users')
        .delete()
        .eq('id', widget.user.id);
    // Handle the response (e.g., navigate back to the previous screen)
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Delete User')),
      body: Center(
        child: ElevatedButton(
          onPressed: _deleteUser,
          child: Text('Delete User'),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Example Application

Let's bring these concepts together in a simple example application that allows users to manage a list of tasks:

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

// Schema definition (schema.dart)
final schema = Schema(
  tables: {
    'tasks': Table(
      columns: {
        'id': Column(type: ColumnType.text, isPrimaryKey: true),
        'title': Column(type: ColumnType.text),
        'description': Column(type: ColumnType.text),
        'isCompleted': Column(type: ColumnType.boolean),
      },
    ),
  },
);

// Generated model (models.dart)
class Task extends Object with Mappable {
  final String id;
  final String title;
  final String description;
  final bool isCompleted;

  Task({
    required this.id,
    required this.title,
    required this.description,
    required this.isCompleted,
  });

  factory Task.fromJson(Map
           <string, dynamic="">
            json) =&gt; _$TaskFromJson(json);

  Map
            <string, dynamic="">
             toJson() =&gt; _$TaskToJson(this);
}

// Main app (main.dart)
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Replace with your actual Supabase credentials
  await Supabase.initialize(
    url: 'your-supabase-url',
    anonKey: 'your-supabase-anon-key',
  );

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: TaskList(),
    );
  }
}

class TaskList extends StatefulWidget {
  @override
  _TaskListState createState() =&gt; _TaskListState();
}

class _TaskListState extends State
             <tasklist>
              {
  final _supabase = Supabase.instance.client;

  Future
              <list<task>
               &gt; _fetchTasks() async {
    final query = _supabase.from('tasks').select('*');
    final response = await query.execute();
    return response.map((row) =&gt; Task.fromJson(row)).toList();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Tasks')),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
            context,
            MaterialPageRoute(builder: (context) =&gt; AddTask()),
          );
        },
        child: Icon(Icons.add),
      ),
      body: FutureBuilder
               <list<task>
                &gt;(
        future: _fetchTasks(),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            return ListView.builder(
              itemCount: snapshot.data!.length,
              itemBuilder: (context, index) {
                final task = snapshot.data![index];
                return ListTile(
                  title: Text(task.title),
                  subtitle: Text(task.description),
                  trailing: Checkbox(
                    value: task.isCompleted,
                    onChanged: (value) {
                      // Update task completion status
                    },
                  ),
                  onTap: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) =&gt; EditTask(task: task),
                      ),
                    );
                  },
                );
              },
            );
          } else if (snapshot.hasError) {
            return Text('Error: ${snapshot.error}');
          } else {
            return CircularProgressIndicator();
          }
        },
      ),
    );
  }
}

class AddTask extends StatefulWidget {
  @override
  _AddTaskState createState() =&gt; _AddTaskState();
}

class _AddTaskState extends State
                <addtask>
                 {
  final _supabase = Supabase.instance.client;
  final _formKey = GlobalKey
                 <formstate>
                  ();

  final _titleController = TextEditingController();
  final _descriptionController = TextEditingController();

  Future
                  <void>
                   _addTask() async {
    final isValid = _formKey.currentState!.validate();
    if (isValid) {
      final task = Task(
        id: Uuid().v4(), // Generate a unique ID
        title: _titleController.text,
        description: _descriptionController.text,
        isCompleted: false,
      );
      final response = await _supabase.from('tasks').insert(task.toJson());
      // Handle the response (e.g., show a success message)
      Navigator.pop(context);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Add Task')),
      body: Form(
        key: _formKey,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            children: [
              TextFormField(
                controller: _titleController,
                decoration: InputDecoration(labelText: 'Title'),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter a title';
                  }
                  return null;
                },
              ),
              TextFormField(
                controller: _descriptionController,
                decoration: InputDecoration(labelText: 'Description'),
              ),
              ElevatedButton(
                onPressed: _addTask,
                child: Text('Add Task'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class EditTask extends StatefulWidget {
  final Task task;

  const EditTask({Key? key, required this.task}) : super(key: key);

  @override
  _EditTaskState createState() =&gt; _EditTaskState();
}

class _EditTaskState extends State
                   <edittask>
                    {
  final _supabase = Supabase.instance.client;
  final _formKey = GlobalKey
                    <formstate>
                     ();

  final _titleController = TextEditingController();
  final _descriptionController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _titleController.text = widget.task.title;
    _descriptionController.text = widget.task.description;
  }

  Future
                     <void>
                      _updateTask() async {
    final isValid = _formKey.currentState!.validate();
    if (isValid) {
      final updatedTask = Task(
        id: widget.task.id,
        title: _titleController.text,
        description: _descriptionController.text,
        isCompleted: widget.task.isCompleted,
      );
      final response = await _supabase
          .from('tasks')
          .update(updatedTask.toJson())
          .eq('id', widget.task.id);
      // Handle the response (e.g., show a success message)
      Navigator.pop(context);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Edit Task')),
      body: Form(
        key: _formKey,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            children: [
              TextFormField(
                controller: _titleController,
                decoration: InputDecoration(labelText: 'Title'),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter a title';
                  }
                  return null;
                },
              ),
              TextFormField(
                controller: _descriptionController,
                decoration: InputDecoration(labelText: 'Description'),
              ),
              ElevatedButton(
                onPressed: _updateTask,
                child: Text('Update Task'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Supadart empowers you to build robust and reliable Flutter applications that interact seamlessly with Supabase. By embracing type safety and leveraging its powerful features, you can streamline your development process, reduce errors, and create code that is easier to maintain and understand.

Best Practices for Using Supadart

  • Define Your Schema Thoroughly: A well-defined schema ensures that your code accurately reflects your database structure, preventing unexpected errors.
  • Validate Data Input: Use Supadart's integration with validation libraries to enforce data integrity and prevent invalid entries from reaching your database.
  • Utilize Real-Time Updates: Leverage Supadart's real-time capabilities to create dynamic and responsive user experiences.
  • Follow Code Conventions: Maintain code consistency and readability by adhering to Flutter's coding conventions and best practices.
  • Consider Async Operations: Use async/await or FutureBuilder widgets to handle asynchronous database interactions gracefully.

By following these best practices and utilizing Supadart's features effectively, you can unlock the full potential of Supabase within your Flutter projects, building secure, scalable, and user-friendly mobile applications.





















. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player