diff --git a/images/integration-guides/flutterflow/component-parameters-child.png b/images/integration-guides/flutterflow/component-parameters-child.png new file mode 100644 index 0000000..30622ca Binary files /dev/null and b/images/integration-guides/flutterflow/component-parameters-child.png differ diff --git a/images/integration-guides/flutterflow/create-new-item.png b/images/integration-guides/flutterflow/create-new-item.png new file mode 100644 index 0000000..8be0448 Binary files /dev/null and b/images/integration-guides/flutterflow/create-new-item.png differ diff --git a/images/integration-guides/flutterflow/json-parameters.png b/images/integration-guides/flutterflow/json-parameters.png new file mode 100644 index 0000000..1190981 Binary files /dev/null and b/images/integration-guides/flutterflow/json-parameters.png differ diff --git a/images/integration-guides/flutterflow/list-item-title-text.png b/images/integration-guides/flutterflow/list-item-title-text.png new file mode 100644 index 0000000..a331a1d Binary files /dev/null and b/images/integration-guides/flutterflow/list-item-title-text.png differ diff --git a/images/integration-guides/flutterflow/listitems-component-children.png b/images/integration-guides/flutterflow/listitems-component-children.png new file mode 100644 index 0000000..9283697 Binary files /dev/null and b/images/integration-guides/flutterflow/listitems-component-children.png differ diff --git a/images/integration-guides/flutterflow/listitems-component-titles.png b/images/integration-guides/flutterflow/listitems-component-titles.png new file mode 100644 index 0000000..8517740 Binary files /dev/null and b/images/integration-guides/flutterflow/listitems-component-titles.png differ diff --git a/images/integration-guides/flutterflow/listitems-component.png b/images/integration-guides/flutterflow/listitems-component.png new file mode 100644 index 0000000..ea69107 Binary files /dev/null and b/images/integration-guides/flutterflow/listitems-component.png differ diff --git a/images/integration-guides/flutterflow/powersync-components.png b/images/integration-guides/flutterflow/powersync-components.png new file mode 100644 index 0000000..7fad068 Binary files /dev/null and b/images/integration-guides/flutterflow/powersync-components.png differ diff --git a/images/integration-guides/flutterflow/powersync-connectivity.png b/images/integration-guides/flutterflow/powersync-connectivity.png new file mode 100644 index 0000000..57073b7 Binary files /dev/null and b/images/integration-guides/flutterflow/powersync-connectivity.png differ diff --git a/images/integration-guides/flutterflow/powersync-library-config.png b/images/integration-guides/flutterflow/powersync-library-config.png new file mode 100644 index 0000000..7ff25d1 Binary files /dev/null and b/images/integration-guides/flutterflow/powersync-library-config.png differ diff --git a/images/integration-guides/flutterflow/sign-out-button.png b/images/integration-guides/flutterflow/sign-out-button.png new file mode 100644 index 0000000..e351205 Binary files /dev/null and b/images/integration-guides/flutterflow/sign-out-button.png differ diff --git a/images/integration-guides/flutterflow/slidable-action-widget.png b/images/integration-guides/flutterflow/slidable-action-widget.png new file mode 100644 index 0000000..b56591e Binary files /dev/null and b/images/integration-guides/flutterflow/slidable-action-widget.png differ diff --git a/images/integration-guides/flutterflow/state-management.png b/images/integration-guides/flutterflow/state-management.png new file mode 100644 index 0000000..17d6c14 Binary files /dev/null and b/images/integration-guides/flutterflow/state-management.png differ diff --git a/images/integration-guides/flutterflow/supabase-rows-custom-function.png b/images/integration-guides/flutterflow/supabase-rows-custom-function.png new file mode 100644 index 0000000..abb025a Binary files /dev/null and b/images/integration-guides/flutterflow/supabase-rows-custom-function.png differ diff --git a/images/integration-guides/flutterflow/test-run.png b/images/integration-guides/flutterflow/test-run.png new file mode 100644 index 0000000..bf5b738 Binary files /dev/null and b/images/integration-guides/flutterflow/test-run.png differ diff --git a/images/integration-guides/flutterflow/todos-page-list-title.png b/images/integration-guides/flutterflow/todos-page-list-title.png new file mode 100644 index 0000000..18aadb0 Binary files /dev/null and b/images/integration-guides/flutterflow/todos-page-list-title.png differ diff --git a/images/integration-guides/flutterflow/update-page-state-action.png b/images/integration-guides/flutterflow/update-page-state-action.png new file mode 100644 index 0000000..87eea4a Binary files /dev/null and b/images/integration-guides/flutterflow/update-page-state-action.png differ diff --git a/integration-guides/flutterflow-+-powersync.mdx b/integration-guides/flutterflow-+-powersync.mdx index 055df68..61777c7 100644 --- a/integration-guides/flutterflow-+-powersync.mdx +++ b/integration-guides/flutterflow-+-powersync.mdx @@ -1,79 +1,82 @@ --- title: "FlutterFlow + PowerSync" -description: "Integration guide for creating offline-first apps with FlutterFlow and PowerSync with Supabase as the backend." -sidebarTitle: Overview +description: "Integration guide for creating local-first apps with FlutterFlow and PowerSync with Supabase as the backend." +sidebarTitle: "Overview" --- import CreateCloudInstance from '/snippets/create-cloud-instance.mdx'; import SupabaseConnection from '/snippets/supabase-database-connection.mdx'; - - - - - -This is our new and improved guide that only requires using Custom Actions to integrate with PowerSync. Using GitHub is not required. +Used in conjunction with **FlutterFlow**, PowerSync enables developers to build local-first apps that are robust in poor network conditions and that have highly responsive frontends while relying on Supabase for their backend. This guide walks you through configuring PowerSync within your FlutterFlow project that has Supabase integration enabled. -The guide takes you through building a basic app from scratch. The app lets you manage a list of items. You should then be able to use this knowledge to build/extend your own app. - + +The PowerSync FlutterFlow Library is currently in the process of being published to the [Marketplace](https://marketplace.flutterflow.io/search). Check back here soon, or refer to our [legacy guide](/integration-guides/flutterflow-+-powersync/powersync-+-flutterflow-legacy). + -Used in conjunction with **FlutterFlow**, PowerSync enables developers to build offline-first apps that are robust in poor network conditions and that have highly responsive frontends while relying on Supabase for their backend. This guide provides instructions for how to configure PowerSync for use with your FlutterFlow project that has Supabase integration enabled. + +**New and Improved integration**: Welcome to our updated FlutterFlow integration guide. This version introduces a dedicated PowerSync FlutterFlow Library, offering a simpler and more robust solution compared to the [previous version](/integration-guides/flutterflow-+-powersync/powersync-+-flutterflow-legacy) which required extensive custom code. -## Guide Overview +Key improvements are: +* Uses the new PowerSync FlutterFlow Library +* Supports Web-based test mode +* Streamlined Setup +* No more dozens of custom actions +* Working Attachments package (Coming soon) - - Before you proceed, this guide assumes that you have already signed up for free accounts with both Supabase and PowerSync. If you haven't signed up for a **PowerSync** account yet, [click here](https://accounts.journeyapps.com/portal/powersync-signup?s=docs) (and if you haven't signed up for Supabase yet, [click here](https://supabase.com/dashboard/sign-up)). This guide also assumes that you already have **Flutter** set up. +Note that using libraries in FlutterFlow requires a [paid plan](https://www.flutterflow.io/pricing). If this is not an option for you, you can use our [legacy guide](/integration-guides/flutterflow-+-powersync/powersync-+-flutterflow-legacy) with custom code to integrate PowerSync in your FlutterFlow project. + - - - This guide also requires [FlutterFlow Local Run](https://flutterflow.io/desktop), so be sure to download and install that. - +## Guide Overview -This guide takes 30-40 minutes to complete. + + Before starting this guide, you'll need: + - A PowerSync account ([sign up here](https://accounts.journeyapps.com/portal/powersync-signup?s=docs)). + - A Supabase account ([sign up here](https://supabase.com/dashboard/sign-up)). + - A [paid plan](https://www.flutterflow.io/pricing) with FlutterFlow for the ability to import a Library into a project. + -1. Configure Supabase and PowerSync prerequisites -2. Initialize your FlutterFlow project -3. Build a sign-in screen -4. Initialize PowerSync -5. Reading data -6. Creating data -7. Deleting data -8. Signing out -9. Securing your app - 1. Enable RLS in Supabase - 2. Update Sync Rules in PowerSync +This guide walks you through building a basic item management app from scratch and takes about 30-40 minutes to complete. You should then be able to use this knowledge to build and extend your own app. + +1. Configure Supabase and PowerSync Prerequisites +2. Initialize Your FlutterFlow Project +3. Build a Sign-in Screen +4. Read Data +5. Create Data +6. Update Data (Coming soon) +7. Delete Data +8. Sign Out +9. (New) Display Connectivity and Sync Status +10. Secure Your App + 1. Enable RLS in Supabase + 2. Update Sync Rules in PowerSync ## Configure Supabase 1. Create a new project in Supabase. 2. PowerSync uses the Postgres [Write Ahead Log (WAL)](https://www.postgresql.org/docs/current/wal-intro.html) to replicate data changes in order to keep PowerSync SDK clients up to date. -Run the below SQL statement in your **Supabase SQL Editor**: - -```sql -create table - public.lists ( - id uuid not null default gen_random_uuid (), - created_at timestamp with time zone not null default now(), - name text not null, - owner_id uuid not null, - constraint lists_pkey primary key (id), - constraint lists_owner_id_fkey foreign key (owner_id) references auth.users (id) on delete cascade - ) tablespace pg_default -``` - -1. Create a Postgres publication using the SQL Editor. This will enable data to be replicated from Supabase so that your FlutterFlow app can download it. - -```sql -create publication powersync for table public.lists; -``` - - - **Note:** this guide uses the default `postgres` user in your Supabase account for replicating changes to PowerSync, since elevating custom roles to replication [has been disabled](https://github.com/orgs/supabase/discussions/9314) in Supabase. If you want to use a custom role for this purpose, contact the Supabase support team. - - - - **Note**: this is a static list of tables. If you add additional tables to your schema, they must also be added to this publication. - +3. Run the below SQL statement in your **Supabase SQL Editor**: + ```sql + create table + public.lists ( + id uuid not null default gen_random_uuid (), + created_at timestamp with time zone not null default now(), + name text not null, + owner_id uuid not null, + constraint lists_pkey primary key (id), + constraint lists_owner_id_fkey foreign key (owner_id) references auth.users (id) on delete cascade + ) tablespace pg_default + ``` +4. Create a Postgres publication using the SQL Editor. This will enable data to be replicated from Supabase so that your FlutterFlow app can download it. + ```sql + create publication powersync for table public.lists; + ``` + + This is a static list of tables. If you add additional tables to your schema, they must also be added to this publication. + + + + This guide uses the default `postgres` user in your Supabase account for replicating changes to PowerSync, since elevating custom roles to replication [has been disabled](https://github.com/orgs/supabase/discussions/9314) in Supabase. If you want to use a custom role for this purpose, contact the Supabase support team. + ## Configure PowerSync @@ -85,60 +88,66 @@ create publication powersync for table public.lists; ### Configure Sync Rules -[Sync Rules](/usage/sync-rules) allow developers to control which data gets synced to which user devices using a SQL-like syntax in a YAML file. For the demo app, we're going to specify that each user can only see their own to-do lists and list items. - -1\. To update your Sync Rules, open the `sync-rules.yaml` file. - - - - -1. Replace the `sync-rules.yaml` file's contents with the below: - -```yaml This will sync the entire table to all users - we will refine this later -bucket_definitions: - global: - data: - - SELECT * FROM lists -``` - -For additional information on PowerSync's Sync Rules, refer to the [Sync Rules](/usage/sync-rules) documentation. +[Sync Rules](/usage/sync-rules) allow developers to control which data gets synced to which user devices using a SQL-like syntax in a YAML file. For the demo app, we're going to specify that each user can only see their own lists. -If you're wondering how Sync Rules relate to Supabase Postgres [RLS](https://supabase.com/docs/guides/auth/row-level-security), see [this subsection](/integration-guides/supabase-+-powersync/rls-and-sync-rules). +1. To update your Sync Rules, open the `sync-rules.yaml` file. + + + +2. Replace the `sync-rules.yaml` file's contents with the below: + ```yaml + # This will sync the entire table to all users - we will refine this later + bucket_definitions: + global: + data: + - SELECT * FROM lists + ``` + +3. In the top right, click **"Validate sync rules"** and ensure there are no errors. This validates your sync rules against your Postgres database. +4. In the top right, click **"Deploy sync rules"** and select your instance. +5. Confirm in the dialog and wait a couple of minutes for the deployment to complete. + + +- For additional information on PowerSync's Sync Rules, refer to the [Sync Rules](/usage/sync-rules) documentation. +- If you're wondering how Sync Rules relate to Supabase Postgres [RLS](https://supabase.com/docs/guides/auth/row-level-security), see [this subsection](/integration-guides/supabase-+-powersync/rls-and-sync-rules). + ## Initialize Your FlutterFlow Project 1. Create a new Blank app, give it a name, and disable Firebase. -2. Under **"App Settings" -> "Integrations"**, enable Supabase. Enter your **"API URL"** and **"Anon Key"** and click **"Get Schema".** -3. Under **"App Values" -> "Constants"**, click **"Add App Constant".** - 1. For **Constant Name**, enter `PowerSyncUrl`. - 2. For **Constant Value**, copy and paste your instance URL from the PowerSync Dashboard: - - - - - -You should now see this under App Constants: - - - - +2. Under **"App Settings" -> "Integrations"**, enable "Supabase". + 1. Enter your Supabase "API URL" and public "Anon Key". You can find these under **"Project Settings" -> "API"** in your Supabase dashboard. + 2. Click "Get Schema". +3. Under **"App Settings" -> "Project Dependencies" -> "FlutterFlow Libraries"** click "Add Library". + 1. Select the "PowerSync" library. + 2. Add your schema: + 1. On the PowerSync Dashboard, right-click on your instance and select "Generate Client-Side Schema" and select "FlutterFlow" as the language. + + + + 2. Copy and paste the generated schema into the "PowerSyncSchema" field. + 3. Copy and paste your PowerSync instance URL into the "PowerSyncURL" field. + + + + 4. Close the library config. ## Build A Sign-In Screen -1. Under Pages, click **"Add Page, Component or Flow".** +1. Under the **"Page Selector"**, click **"Add Page, Component, or Flow"**. -2. Select the Auth1 template and name the page "Login". -3. Delete the _Sign Up_, _Forgot Password_ and _Social Login_ buttons — we will only be supporting Login for this demo app. +2. Select the "Auth1" template and name the page `Login`. +3. Delete the _Sign Up_, _Forgot Password_ and _Social Login_ buttons — we will only be supporting Sign In for this demo app. 4. Under **"App Settings" -> "App Settings" -> "Authentication"**: 1. Enable Authentication. - 2. Set Supabase as the Authentication Type. - 3. Set the Login page you just created as the Entry Page. - 4. Set HomePage as the Logged In Page: + 2. Set "Supabase" as the "Authentication Type". + 3. Set the `Login` page you just created as the "Entry Page". + 4. Set "HomePage" as the "Logged In Page": @@ -146,325 +155,338 @@ You should now see this under App Constants: -6. Launch your app on a physical or simulator device: - - +6. Test your app with test mode: + + - - **Checkpoint:** you should now be able to log into the app using the Supabase user account you just created. After logging in you should see a blank screen. - + + **Checkpoint:** You should now be able to log into the app using the Supabase user account you just created. After logging in you should see a blank screen. + + + + + -For once, a blank screen means success: - +## Read Data -## Initialize PowerSync +We will now create our first UI and bind it to the data in the local SQLite database on the device. -1. Click on **"Custom Code" -> "Add" -> "Action".** -2. Name the Custom Action `initpowersync`. - 1. **NOTE:** use all lowercase for this Custom Action is important due to naming conversion that FF performs behind the scenes. -3. Copy and paste the custom action code from here:[https://github.com/powersync-ja/powersync-flutterflow-template/blob/flutterflow/lib/custom\_code/actions/initpowersync.dart](https://github.com/powersync-ja/powersync-flutterflow-template/blob/flutterflow/lib/custom_code/actions/initpowersync.dart) -4. Import your schema: - 1. On the PowerSync Dashboard, right-click on your instance and select **"Generate Client-Side Schema"** and select Dart as the language. - - +There are three ways to read data from the SQLite database using PowerSync's FlutterFlow library: +1. Auto-updating queries for Layout Elements with Dynamic Children e.g. the ListView Element + * This uses the library's `PowerSyncQuery` component +2. Auto-updating queries for basic Layout Elements e.g. Text Elements + * This uses the library's `PowerSyncStateUpdater` component +3. Once-off reads for static data + * This uses the library's `PowerSyncQueryOnce` custom function + +### Prepare Supabase Tables for Reads + +For reading data in FlutterFlow, you need a Custom Function per Supabase table to map Supabase rows to data that can be used by the library. This is because FlutterFlow Libraries do not support Supabase classes. + +1. Navigate to **"Custom Code"** and add a Custom Function. +2. Name the function `supabaseRowsToList` (if your Supabase table name is "Customers", you would name this `supabaseRowsToCustomers`). +3. Under Function Settings, set the "Return Value" to `Supabase Row` + 1. Check "Is List". + 2. Uncheck "Nullable". + 3. Specify `lists` as the "Table Name". +4. Also under Function Settings, click "Add Arguments". + 1. Set its "Name" to `supabaseRows` + 2. Set its "Type" to "JSON". + 3. Check "Is List". + 4. Uncheck "Nullable". +5. In the Function Code, paste the following code: + ```dart + /// MODIFY CODE ONLY BELOW THIS LINE + return supabaseRows.map((r) => ListsRow(r)).toList(); + ``` +6. Click "Save Function". + + + + + +### 1. Auto-updating queries for Layout Elements with Dynamic Children + +#### Create a Component to display List Items + +1. Under the **"Page Selector"**, click **"Add Page, Component, or Flow"**. +2. Select the **"New Component"** tab. +3. Select "Create Blank" and call the component `ListItems`. +4. Under the **"Widget Palette"**, drag a "ListView" widget into the `ListItems` component. +5. Still under the **"Widget Palette"**, drag a "ListTile" into the `ListView` widget. + + - 2. Paste this into your Custom Action code on line 27 after the equals sign. - 3. Due to a limitation in FF, you now need to prefix each instance of `Schema`, `Column` and `Table` with `powersync`. - 4. Your custom action schema definition should now look like this: - - +6. Under the **"Widget Tree"**, select the `ListItems` component. + 1. At the top right under "Component Parameters" click "Add Parameters". + 2. Click "Add Parameter". + 3. Set its "Name" to `lists`. + 4. Set its "Type" to `Supabase Row`. + 5. Check "Is List". + 6. Specify `lists` as the "Table Name". + 7. Click "Confirm". +7. Still under the **"Widget Tree"**, select the "ListView" widget. + 1. Select the **"Generate Dynamic Children"** panel on the right. + 2. Set the "Variable Name" to `listItem`. + 3. Set the "Value" to the component parameter created in the previous step (`lists`). + 4. Click "Confirm". + 5. Click "Save". + 6. Click "Ok" when being prompted about the widget generating its children dynamically. + + -5. Under **"Action Settings"** on the right, add this dependency into "Pubspec Dependencies": `powersync: ^1.8.4` - 1. FlutterFlow imports and old version of sqflite by default and it's not possible to remove it, so you also need to add this dependency: `sqflite: ^2.3.3` - 2. Your dependencies should now look as follows: - - - +8. Still under the **"Widget Tree"**, select the `ListTile` widget. + 1. In the **"Properties"** panel on the right, under "Title", click on the settings icon next to "Text". + 2. Set as "listItem Item". + 3. Under "Available Options", select "Get Row Field". + 4. Under "Supabase Row Fields", select "name". + 5. Click "Confirm". + + + + 9. Repeat Step 8 above for the "Subtitle", setting it to "created_at". + + + -1. Save your new custom action -2. Still in Custom Actions, under **"Custom Files"** click on `main.dart` and set your new Custom Action as a Final Action and click Save. - - - - -**Checkpoint:** You should now be able to validate that PowerSync is initializing correctly by taking these steps: +#### Display the List Component and populate it with Data -1. Stop any running simulator app sessions -2. Restart the app by clicking "Test", and sign in -3. Click on "Open Device Logs" -4. You should see this kind of log message: +1. Under the **"Page Selector"**, select your `HomePage`. +2. Under the **"Widget Palette"**, select the "Components and custom widgets imported from library projects" panel. + + + +3. Drag the `PowerSyncQuery` library component into your page. +4. In the Properties panel on the right, under **"Component Parameters" -> "child"**: + 1. Click on "Unknown". + 2. Select `ListItems` we previously created. + 3. Click on `lists`. + 4. Set the "Value" to "Custom Functions" > `supabaseRowsToList` we created previously. + 5. Under the `supabaseRows` argument, set the "Value" to "Widget Builder Parameters" > `rows`. + + + + 6. Click "Confirm". + 7. Click "Confirm". +5. Still under "Component Parameters" add the SQL query to fetch all list items from the SQLite database: + 1. Paste the following into the "sql [String]" field: + ```select * from lists order by created_at;``` + 2. For this query there are no parameters - this will be covered further down in the guide. +6. Still under "Component Parameters", check "watch [Boolean]". This ensures that the query auto-updates. + +#### Test your App + +1. Check that there are no project issues or errors. +2. Reload your app or start another test session. +3. Notice that your homepage is still blank. This is because the `lists` table is empty in Supabase. Create a test row in the table by clicking on "Insert" -> "Insert Row" in your Supabase Table Editor. + 1. Leave `id` and `created_at` blank. + 2. Enter a name such as "Test from Supabase". + 3. Click "Select Record" for `owner_id` and select your test user. -``` -flutter: [PowerSync] FINE: 2024-04-16 13:47:52.259974: Credentials: PowerSyncCredentials + + **Checkpoint:** You should now see your single test row magically appear in your app: -flutter: [PowerSync] FINE: 2024-04-16 13:47:52.607802: Applied checkpoint 2 -``` - + + + + +### 2. Auto-updating queries for basic Layout Elements -## Reading Data +In this section, we will be making the `ListView` component clickable and navigate the user to a page which will eventually display the list's To-Do items. This page will show the selected list's name in the title bar ("AppBar"). This uses Page State and the `PowerSyncStateUpdater` library component. -We will now create our first UI and bind it to the data in the local SQLite database on the device. +#### Create a Page Parameter -### Create a Custom Action to Stream all Lists +This parameter will store the selected list's ID. - - For watched (realtime) queries in FlutterFlow, you need 2x Custom Actions per table. For delete, update and insert queries you only need 1x Custom Action. We are working to see if we can aleviate this constraint. - +1. Under the **"Page Selector"**, click **"Add Page, Component, or Flow"**. + + + +1. Create a blank page and name it `Todos`. -1. Create a new Custom Action and call it `watchLists` and paste the below code: +2. Under the **"Widget Tree"**, select your `Todos` page. +3. At the top right of the **"Properties"** panel on the right, click on the plus icon for Page Parameters. +4. Click "Add Parameter". +5. Set the "Parameter Name" to `id`. +6. Set the "Type" to "String". +7. Click "Confirm". -```dart -// Automatic FlutterFlow imports -import '/backend/supabase/supabase.dart'; -import '/flutter_flow/flutter_flow_theme.dart'; -import '/flutter_flow/flutter_flow_util.dart'; -import '/custom_code/actions/index.dart'; // Imports other custom actions -import '/flutter_flow/custom_functions.dart'; // Imports custom functions -import 'package:flutter/material.dart'; -// Begin custom action code -// DO NOT REMOVE OR MODIFY THE CODE ABOVE! +#### Create a Local Page State Variable -import 'package:powersync/powersync.dart' as powersync; -import '/custom_code/actions/initpowersync.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; -import 'dart:async'; - -Future watchLists( - Future Function(List? result) callback) async { - var stream = db.watch('SELECT * FROM lists'); - listsSubscription?.cancel(); //it's important to clean up any existing subscriptions otherwise app performance will degrade - listsSubscription = stream.listen((data) { - callback( - data.map((json) => ListsRow(Map.from(json))).toList()); - }); -} -``` +This variable will store the selected list row. -2. Hit Save and click "Yes" on the popup to set the Action Arguments for you: - - - -3. Your Action Arguments should now look as follows: +1. Still in the **"Widget Tree"** with the `Todos` page selected: +2. Select the **"State Management Panel"** on the right. - + -4. Create the second Custom Action called `getLists` and paste the following code into it: - -```dart -// Automatic FlutterFlow imports -import '/backend/supabase/supabase.dart'; -import '/flutter_flow/flutter_flow_theme.dart'; -import '/flutter_flow/flutter_flow_util.dart'; -import '/custom_code/actions/index.dart'; // Imports other custom actions -import '/flutter_flow/custom_functions.dart'; // Imports custom functions -import 'package:flutter/material.dart'; -// Begin custom action code -// DO NOT REMOVE OR MODIFY THE CODE ABOVE! +3. Click on "Add Field". +4. Set "Field Name" to `list`. +5. Set the "Type" to "Supabase Row". +6. Select `lists` as the "Table Name". +7. Click "Confirm". -import 'package:powersync/powersync.dart' as powersync; -import '/custom_code/actions/initpowersync.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; +#### Bind the Page Title to the Page State -Future?> getLists(List? results) async { - return results; -} -``` -1. Hit Save and click "Yes" on the popup to set the Action Arguments for you: - - - -2. Your Action Arguments should now look as follows: - - - -3. On the HomePage page, you will create a placeholder Page State variable required for the next step. - 1. Click on State Management. - 2. Add a dummy variable called "notused" or similar and click Confirm: +1. Under the **"Widget Palette"**, select the "Components and custom widgets imported from library projects" panel. + + + +2. Drag the `PowerSyncStateUpdater` library component into your page. +3. Under the **"Widget Tree"**, select the `PowerSyncStateUpdater` component. +4. In the **"Properties"** panel on the right, under "Component Parameters": + 1. Add the SQL query to fetch the selected list from the SQLite database. Paste the following into the "sql [String]" field: + ```select * from lists where id = :id;``` + 2. Click on "parameters [Json]" select "Create Map (JSON)" as the variable. + 1. Under "Add Map Entires", click "Add Key Value Pair". + 2. Set the "Key" to `id`. + 3. Set the "Value" to the page parameter created previously called `id`. + + + + 4. Click "Confirm". +5. Still under "Component Parameters", configure the "onData" action: + 1. Open the "Action Flow Editor". + 2. Select the "Callback" trigger type. + 3. Click "Add Action". + 4. search for "update page" and select "Update Page State". + 5. Click "Add Field". + 6. Select your `list` page state variable. + 7. Set "Select Update Type" to "Set Value". + 8. Set "Value to set" to "Custom Functions" > `supabaseRowsToList`. + 9. Set the "Value" to "Callback Parameters" > `rows` + 10. Click "Confirm". + 11. Under "Available Options", select "Item at Index". + 12. Set "List Index Options" to "First" + + + + 13. Click "Confirm". + 14. Close the Action Flow Editor. +6. Still under the **"Widget Tree"**, select the "AppBar" -> "Text" widget. + 1. In the **"Properties"** panel on the right, click on settings icon next to "Text". + 2. Click on "Page State" > "List". + 3. Set "Supabase Row Fields" to "name". + 4. (Optional) Set the "Default Variable Value" to `List Name`. + 5. Click "Confirm". + +#### Make the `ListView` Component Clickable + +1. Under the **"Page Selector"**, select your `ListItems` component. +2. Under the **"Widget Tree"**, select the `ListTile` widget. +3. In the **"Actions"** panel on the right, click "Add Action". "On Tap" should be selected by default. +4. In the "Navigate" subsection, select "Navigate To". +5. Select the "Todos" page. +6. Under "Parameters" click "Pass". +7. "id" should be auto-selected, click on it. +8. Click on the settings icon next to "Value" +9. Set it to "listItem Item". +10. Under "Available Options" select "Get Row Field" +11. Under "Supabase Row Fields" select "id". +12. Click "Confirm". +13. (Optional) Enable the back button to navigate back: + 1. Under the **"Page Selector"**, select your `Todos` page. + 2. Under the **"Widget Tree"**, select the "AppBar" component. + 3. In the **"Properties"** panel on the right, enable "Show Default Button". + + +#### Test your App + +Instant Reload your app or start another test session. - - - -4. Still on the HomePage page, select Actions and open **"Action Flow Editor".** - 1. Add the `watchLists` Custom Action. - 2. Click "Open" to edit the `callback` Argument for `watchLists`. - 3. Add the `getLists` Custom Action and set the `results` Action Argument to `result` and click **"Confirm":** - - - - 4. Set the **"Action Output Variable Name"** to `allLists` and you should now see this: - - - - 5. Add a second action to the chain, and set it to **"Update Page State"** and "**Rebuild Current Page"**. This is to ensure the page gets redrawn when the database updates. Your callback action should now look like this: - - - - 6. Click "Close" to exit the Action Flow Editor. -7. In the UI Builder on the HomePage page, add a ListView component and add a ListTile inside the ListView. -8. On the ListView component, click **"Generate Dynamic Children"**. Enter a variable name of `boundLists` and set its value to `allLists` (no further changes). Click Save. -9. On the ListTile component, set the Title field to **"Set from Variable"** and then get the `name` field from the `boundLists` variable: + + **Checkpoint:** You should now be able to click on a list item and it should navigate you to a new page showing the name of the list in the title bar: - - - + + + + -10. Do the same for the Subtitle field of the ListTile component, and set it to `created_at`. -11. Hot reload your app and the screen will still be blank. This is because the `lists` table is empty in Supabase. Create a test row in the table by clicking on **"Insert" -> "Insert Row"** in your Supabase Table Editor. - 1. Leave `id` and `created_at` blank. - 2. Enter a name such as "Test from Supabase". - 3. Click "Select Record" for `owner_id` and select your test user. +### 3. Once off reads for static data -Checkpoint: you should now see your single test row magically appear in your app - - - - + This section is a work in progress. Please reach out on [our Discord](https://discord.gg/powersync) if you have any questions. -## Creating Data +## Create Data You will now update the app so that we can capture new list entries. -1. Create a new Custom Action called `createListItem` and paste the following code: - -```dart -// Automatic FlutterFlow imports -import '/backend/supabase/supabase.dart'; -import '/flutter_flow/flutter_flow_theme.dart'; -import '/flutter_flow/flutter_flow_util.dart'; -import '/custom_code/actions/index.dart'; // Imports other custom actions -import '/flutter_flow/custom_functions.dart'; // Imports custom functions -import 'package:flutter/material.dart'; -// Begin custom action code -// DO NOT REMOVE OR MODIFY THE CODE ABOVE! +1. Under the **"Page Selector"**, select your `HomePage` page. +2. Under the **"Widget Palette"**, search for "float" and drag the "FAB" widget onto your page. +3. In the **"Actions"** panel on the right, click "Add Action". + 1. Under "Custom Action" > "PowerSync", select "powersyncWrite". + 2. Under the "Set Action Arguments" > "sql" section, add the SQL query to create a new list item. For the purpose of this guide we are hardcoding the list's name, normally you would build UI for this. + 1. Paste the following into the "Value" field: + ```INSERT INTO lists(id, created_at, name, owner_id) VALUES(uuid(), datetime(), 'new item', :userId);``` + 3. Under the "parameters" section, set the `userId` parameter we're using the above query: + 1. Click on "UNSET". + 2. Select "Create Map (JSON)" as the variable. + 3. Under "Add Map Entires", click "Add Key Value Pair". + 4. Set the "Key" to `userId`. + 5. Set the "Value" to "Authenticated User" > "User ID". + 6. Click "Confirm". -// Set your action name, define your arguments and return parameter, -// and then add the boilerplate code using the green button on the right! -import 'package:powersync/powersync.dart' as powersync; -import '/custom_code/actions/initpowersync.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; - -Future createListItem(String name) async { - var supaUserId = await Supabase.instance.client.auth.currentUser?.id; - - final results = await db.execute(''' - INSERT INTO - lists(id, created_at, name, owner_id) - VALUES(uuid(), datetime(), ?, ?) - ''', [name, supaUserId]); -} -``` + + **Checkpoint:** Reload your app and click on the + floating action button. A new list item should appear, which also automatically syncs to Supabase: -1. Hit Save and click "Yes" on the popup to set the Action Arguments for you: - - - -2. There should now be one argument for the Custom Action called `name` of type String and not nullable. -3. In the Widget Tree view, select the HomePage page and navigate to State Management. -4. Create a new State Field called `fabClicked` and set the type to boolean and toggle the **"Initial Field Value"** toggle twice to initialize the field to false. -5. - + + -6. In the Widget Tree view, drop a Floating Action Button (FAB) onto the page. -7. Click on the FAB and Open the Action Flow Editor. -8. Add an action to Update Page State. -9. Set the `fabClicked` value to `true` and click Close. - - - -10. On the Widget Palette again, add a Container child to the Column Widget. - 1. Now add a Column Widget to this Container. - 2. Add a TextField and a Button to this Column Widget. - 3. Your homepage layout should now look like this: - - - -11. Set the Container and TextField widgets to have a width of 100%. -12. Click on the Container and enable Conditional Visibility for `fabClicked`. - - - -13. Change the Button text to "Add". -14. Open the Action Flow Editor for the Add button: - 1. Add a Custom Action call to `createListItem`. - 2. Set the "name" Argument to Widget State -> TextField 1. - 3. Chain another Action of "Clear Text Fields / PIN Codes" to clear the TextField\_1 field. - 4. Chain another Action to Update Page State and set `fabClicked` to false. - 5. Your Action Editor should now look like this: - - - + + +## Update Data - **Checkpoint:** you should now be able hot reload your app, click on the FAB button and the TextField should appear. Enter a name and click Add. The new row should appear in the ListView and the TextField should be hidden again. + This section is a work in progress. Please reach out on [our Discord](https://discord.gg/powersync) if you have any questions. -## Deleting Data - -In this section we will add the ability to swipe on a ListTile to delete it. - -1. Create a new Custom Action called `deleteListItem` and paste the below code: - -```dart -// Automatic FlutterFlow imports -import '/backend/supabase/supabase.dart'; -import '/flutter_flow/flutter_flow_theme.dart'; -import '/flutter_flow/flutter_flow_util.dart'; -import '/custom_code/actions/index.dart'; // Imports other custom actions -import '/flutter_flow/custom_functions.dart'; // Imports custom functions -import 'package:flutter/material.dart'; -// Begin custom action code -// DO NOT REMOVE OR MODIFY THE CODE ABOVE! - -import 'package:powersync/powersync.dart' as powersync; -import '/custom_code/actions/initpowersync.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; -Future deleteListItem(ListsRow row) async { - await db.execute('DELETE FROM lists WHERE id = ?', [row.id]); -} -``` +## Delete Data -1. Hit Save and click "Yes" on the popup to set the Action Arguments for you: - - - -2. The Custom Action Arguments should now look as follows: - - - -3. In the Widget Tree select the ListTile and enable Slidable. -4. Select the SlidableActionWidget from the Widget Tree and set the values to the following: - - - -5. Click on Action Editor and click Add Action, passing in `boundLists` to `deleteListItem` as follows: - - - +In this section we will add the ability to swipe on a `ListTile` to delete it. - - **Checkpoint:** Stop and relaunch the app (Hot Reload won't work after adding the slidable package) and you should be able to swipe on items to delete them. Note that they are also magically deleted from Supabase! - +1. Under the **"Page Selector"**, select your `ListItems` component. +2. Under the **"Widget Tree"**, select the `ListTile` widget. +3. In the **"Properties"** panel on the right, enable "Slidable". +4. Click "Open Slidable". +5. Select the "SlidableActionWidget". + + + +6. In the **"Actions"** panel on the right, click "Add Action". + 1. Under "Custom Action" > "PowerSync", select "powersyncWrite". + 2. Under the "Set Action Arguments" > "sql" section, add the SQL query to delete the list item. + 1. Paste the following into the "Value" field: + ```delete from lists where id = :id;``` + 3. Under the "parameters" section, set the `id` parameter we're using the above query: + 1. Click on "UNSET". + 2. Select "Create Map (JSON)" as the variable. + 3. Under "Add Map Entires", click "Add Key Value Pair". + 4. Set the "Key" to `id`. + 5. Set the "Value" to "listItem Item". + 6. Under "Available Options" select "Get Row Field". + 7. Under "Supabase Row Fields" select "id". + 8. Click "Confirm". + 9. Click "Confirm". -## Updating Data + + **Checkpoint:** Reload your app and swipe on a list item. Delete it, and note how it is deleted from the list as well as from Supabase. + -In this section we will add the ability to update a list item. It entails: +## Sign Out -* A custom action to handle updating the data -* Setting and using state fields to show/hide UI dynamically and reference the list item to edit -* A button to edit a list item (set up similar to the Delete button in the previous section) -* UI to enter and save the new item name (set up similar to the Create functionality we covered earlier) -1. Create a new Custom Action called `updateListItem` and paste the below code: +1. Navigate to **"Custom Code"** and create a new Custom Action called `signOut` without Arguments or Return Values and paste the below code: ```dart // Automatic FlutterFlow imports import '/backend/supabase/supabase.dart'; -import '/flutter_flow/flutter_flow_theme.dart'; +import "package:power_sync_y2dtqn/backend/schema/structs/index.dart" + as power_sync_y2dtqn_data_schema; +import 'package:ff_theme/flutter_flow/flutter_flow_theme.dart'; import '/flutter_flow/flutter_flow_util.dart'; import '/custom_code/actions/index.dart'; // Imports other custom actions import '/flutter_flow/custom_functions.dart'; // Imports custom functions @@ -472,138 +494,54 @@ import 'package:flutter/material.dart'; // Begin custom action code // DO NOT REMOVE OR MODIFY THE CODE ABOVE! -// Set your action name, define your arguments and return parameter, -// and then add the boilerplate code using the green button on the right! -import 'package:powersync/powersync.dart' as powersync; -import '/custom_code/actions/initpowersync.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:power_sync_y2dtqn/custom_code/actions/initialize_power_sync.dart' + as ps; -Future updateListItem(String name, ListsRow row) async { - await db.execute('UPDATE lists SET name = ? WHERE id = ?', [name, row.id]); +Future signOut() async { + final database = await ps.getOrInitializeDatabase(); + //await database.disconnectAndClear(); // this will completely delete all the local data, use with caution as there may be items still in the upload queue + await database + .disconnect(); //this will simply disconnect from the PowerSync Service and preserve all local data } -``` - -1. Hit Save and click "Yes" on the popup to set the Action Arguments for you: - - - - -1. The Custom Action Arguments should now look as follows: - - - - - -1. In the Widget Tree view, select the HomePage page and navigate to State Management. -2. Create a new State Field called `editClicked`, set the type to Boolean and toggle the **"Initial Field Value"** toggle twice to initialize the field to false. -3. Create another new State Field called `listItemIndex`, set the type to Integer. Click Confirm. - - - -4. In the Widget Tree select the ListTile. -5. Under Slidable Properties click Add Action. -6. Select the new SlidableActionWidget from the Widget Tree and set its properties to the following: - - - -7. Open the Action Flow Editor. -8. Add an action to Update Page State. -9. Add Field: Set the `editClicked` value to `true`. -10. Add Field: Set the value of `listItemIndex` to the "Index in List" of the `boundLists` Item and click Close. - - - - -1. Chain another Action to "Set Form Field" -> TextField\_2\. This will initialize the text field to the current list item's name. - 1. Set the variable to the boundLists Item - 2. Under Available Options, select "Get Row Field" - 3. Under Supabase Row Field, select "name" - 4. Your action should look like this: - - - -2. On the Widget Palette again, add a Container child to the Column Widget. - 1. Now add a Column Widget to this Container. - 2. Add a TextField and a Button to this Column Widget. - 3. Your homepage layout should now look like this: - - - -3. Set the Container and TextField widgets to have a width of 100%. -4. Click on the Container and enable Conditional Visibility for `editClicked`. - - - -5. Change the Button text to "Save" -6. Open the Action Flow Editor for the Save button. - 1. Add a Custom Action call to `updateListItem`. - 2. Set the "name" Argument to Widget State -> TextField 2. - 3. Set the "row" Argument: - 1. Select Action Outputs -> allLists. - 2. Under Available Options select "Item at Index". - 3. Under List Index Options select "Specific Index". - 4. Set the Index value to the `listItemIndex` Page State variable - - - - 5. Click Confirm - 4. Chain another Action of "Clear Text Fields / PIN Codes" to clear the TextField\_2 field. - 5. Chain another Action to "Update Page State". - 6. Add Field: Set `editClicked` to false. - 7. Add Field: Set `listItemIndex` to Reset Value. - 8. Your Action Editor should now look like this: - - - -7. Close the Action Flow Editor. - - - **Checkpoint:** you should now be able hot reload your app, slide on an item to edit it. Enter the new item name into the text field that appears, and hit Save. The update should then reflect in Supabase. - - -## Signing Out - -1. Create a new Custom Action called `signOut` without Arguments or Return Values and paste the below code: - -```dart -// Automatic FlutterFlow imports -import '/backend/supabase/supabase.dart'; -import '/flutter_flow/flutter_flow_theme.dart'; -import '/flutter_flow/flutter_flow_util.dart'; -import '/custom_code/actions/index.dart'; // Imports other custom actions -import '/flutter_flow/custom_functions.dart'; // Imports custom functions -import 'package:flutter/material.dart'; -// Begin custom action code -// DO NOT REMOVE OR MODIFY THE CODE ABOVE! - // Set your action name, define your arguments and return parameter, // and then add the boilerplate code using the green button on the right! -import 'package:powersync/powersync.dart' as powersync; -import '/custom_code/actions/initpowersync.dart'; - -Future signOut() async { - listsSubscription?.cancel(); //close any open subscriptions from watch() queries - await db.disconnectAndClear(); -} ``` -1. Click Save Action. -2. In the Widget Tree, drag a Button onto the right of your App Bar. -3. Rename the button text to "Sign Out". -4. Open Action Editor and click Open to launch the editor. -5. Add a call to the `signOut`Custom Action. -6. Chain another call to Auth -> Log Out: +2. Click "Save Action". +3. Under the **"Page Selector"**, select your `HomePage` page. +4. Under the **"Widget Palette"**, drag a "Button" onto the right of your "AppBar". + + + +5. In the **"Properties"** panel on the right, rename the "Button Text" to `Sign Out`. +6. Switch to the **"Actions"** panel and open the **"Action Flow Editor"**. +7. Select "On Tap" as the action trigger. +8. Click "Add Action" and add a call to the `signOut` Custom Action. +9. Chain another Action and call to "Supabase Authentication" > "Log Out": -7. Click Close +10. Click "Close". - - **Checkpoint:** You should now be able to hot reload your app and sign out and in again. - + + **Checkpoint:** You should now be able to reload your app and sign out and in again. + -## Securing Your App +## (Optional) Display Connectivity and Sync Status + +The PowerSync library provides a built-in component that displays real-time connectivity and synchronization status. Since the sync state is available globally as part of your app state, you can easily monitor the database status throughout your application. To add this status indicator: + +1. Under the **Widget Palette**, select the "Components and custom widgets imported from library projects" panel. + + + +2. Drag the `PowerSyncConnectivity` component into your home page's "AppBar". + + + + +## Secure Your App PowerSync's [Sync Rules](/usage/sync-rules) and Supabase's support for [Row Level Security (RLS)](https://supabase.com/docs/guides/auth/row-level-security) can be used in conjunction. Here are some high level similarities and differences: @@ -628,7 +566,7 @@ create policy "owned lists" on public.lists for ALL using ( Currently all lists are synced to all users, regardless of who the owner of the list is. You will now update this so that only a user's lists are synced to their device: -1. Navigate to the [PowerSync Dashboard](/usage/tools/powersync-dashboard) and open your `sync-rules.yaml` file. +1. Navigate to the [PowerSync Dashboard](https://powersync.journeyapps.com/) and open your `sync-rules.yaml` file. 2. Delete the existing content and paste the below contents: ```yaml @@ -639,32 +577,23 @@ bucket_definitions: - select * from lists where owner_id = bucket.user_id ``` -1. Click on **"Validate".** -2. Click on **"Deploy sync rules".** +1. Click on **"Validate"**. +2. Click on **"Deploy sync rules"**. 3. Wait for the deploy to complete. - + **Checkpoint:** Your app should continue running seamlessly as before. - + ## Known Issues, Limitations and Gotchas Below is a list of known issues and limitations. -1. It's not currently possible to use the FlutterFlow Web Editor to test your app due to limitations with FlutterFlow. -2. When trying to compile any of the PowerSync Custom Actions, you will see errors — these can be safely ignored: - - - - - - -3. Using `watch()` queries creates a [StreamSubscription](https://api.flutter.dev/flutter/dart-async/StreamSubscription-class.html) and it's important to regularly call `.cancel()` on these to avoid multiple subscriptions for the same query running. -4. Deploying to the Apple App Store currently requires some workarounds due to limitations in FlutterFlow: - 1. Download the code from FlutterFlow - 2. Open the `Podfile` located in the `ios/`directory - 3. The following option in the `Podfile` needs to be updated from `use_frameworks! :linkage => :static` to `use_frameworks!` (remove everything after the exclamation sign) +1. Deploying to the Apple App Store currently requires some workarounds due to limitations in FlutterFlow: + 1. Download the code from FlutterFlow. + 2. Open the `Podfile` located in the `ios/` directory. + 3. The following option in the `Podfile` needs to be updated from `use_frameworks! :linkage => :static` to `use_frameworks!` (remove everything after the exclamation sign). 4. After removing that option, clean the build folder and build the project again. - 5. You should now be able to submit to the App Store -5. Exporting the code from FlutterFlow using the "Download Code" action in FlutterFlow requires the same workaround listed in 4\. above. -6. Other common issues and troubleshooting techniques are documented here: [Troubleshooting](/resources/troubleshooting) + 5. You should now be able to submit to the App Store. +2. Exporting the code from FlutterFlow using the "Download Code" action in FlutterFlow requires the same workaround listed above. +3. Other common issues and troubleshooting techniques are documented here: [Troubleshooting](/resources/troubleshooting). diff --git a/integration-guides/flutterflow-+-powersync/powersync-+-flutterflow-legacy.mdx b/integration-guides/flutterflow-+-powersync/powersync-+-flutterflow-legacy.mdx new file mode 100644 index 0000000..73120fa --- /dev/null +++ b/integration-guides/flutterflow-+-powersync/powersync-+-flutterflow-legacy.mdx @@ -0,0 +1,668 @@ +--- +title: "FlutterFlow + PowerSync Legacy Guide" +description: "Legacy integration guide for creating local-first apps with FlutterFlow and PowerSync with Supabase as the backend." +sidebarTitle: "Legacy Guide" +--- + +import CreateCloudInstance from '/snippets/create-cloud-instance.mdx'; +import SupabaseConnection from '/snippets/supabase-database-connection.mdx'; + + +This guide demonstrates our previous FlutterFlow integration approach that uses custom actions. For a simpler and more robust solution, we recommend following our [updated guide](/integration-guides/flutterflow-+-powersync) which leverages the official PowerSync FlutterFlow library. + + + + + + +Used in conjunction with **FlutterFlow**, PowerSync enables developers to build local-first apps that are robust in poor network conditions and that have highly responsive frontends while relying on Supabase for their backend. This guide provides instructions for how to configure PowerSync for use with your FlutterFlow project that has Supabase integration enabled. + +## Guide Overview + + + Before you proceed, this guide assumes that you have already signed up for free accounts with both Supabase and PowerSync. If you haven't signed up for a **PowerSync** account yet, [click here](https://accounts.journeyapps.com/portal/powersync-signup?s=docs) (and if you haven't signed up for Supabase yet, [click here](https://supabase.com/dashboard/sign-up)). This guide also assumes that you already have **Flutter** set up. + + + + This guide also requires [FlutterFlow Local Run](https://flutterflow.io/desktop), so be sure to download and install that. + + +This guide takes 30-40 minutes to complete. + +1. Configure Supabase and PowerSync prerequisites +2. Initialize your FlutterFlow project +3. Build a sign-in screen +4. Initialize PowerSync +5. Reading data +6. Creating data +7. Deleting data +8. Signing out +9. Securing your app + 1. Enable RLS in Supabase + 2. Update Sync Rules in PowerSync + +## Configure Supabase + +1. Create a new project in Supabase. +2. PowerSync uses the Postgres [Write Ahead Log (WAL)](https://www.postgresql.org/docs/current/wal-intro.html) to replicate data changes in order to keep PowerSync SDK clients up to date. +Run the below SQL statement in your **Supabase SQL Editor**: + +```sql +create table + public.lists ( + id uuid not null default gen_random_uuid (), + created_at timestamp with time zone not null default now(), + name text not null, + owner_id uuid not null, + constraint lists_pkey primary key (id), + constraint lists_owner_id_fkey foreign key (owner_id) references auth.users (id) on delete cascade + ) tablespace pg_default +``` + +1. Create a Postgres publication using the SQL Editor. This will enable data to be replicated from Supabase so that your FlutterFlow app can download it. + +```sql +create publication powersync for table public.lists; +``` + + + **Note:** this guide uses the default `postgres` user in your Supabase account for replicating changes to PowerSync, since elevating custom roles to replication [has been disabled](https://github.com/orgs/supabase/discussions/9314) in Supabase. If you want to use a custom role for this purpose, contact the Supabase support team. + + + + **Note**: this is a static list of tables. If you add additional tables to your schema, they must also be added to this publication. + + +## Configure PowerSync + +### Create a PowerSync Cloud Instance + + +### Connect PowerSync to Your Supabase + + +### Configure Sync Rules + +[Sync Rules](/usage/sync-rules) allow developers to control which data gets synced to which user devices using a SQL-like syntax in a YAML file. For the demo app, we're going to specify that each user can only see their own to-do lists and list items. + +1\. To update your Sync Rules, open the `sync-rules.yaml` file. + + + + +1. Replace the `sync-rules.yaml` file's contents with the below: + +```yaml This will sync the entire table to all users - we will refine this later +bucket_definitions: + global: + data: + - SELECT * FROM lists +``` + +For additional information on PowerSync's Sync Rules, refer to the [Sync Rules](/usage/sync-rules) documentation. + +If you're wondering how Sync Rules relate to Supabase Postgres [RLS](https://supabase.com/docs/guides/auth/row-level-security), see [this subsection](/integration-guides/supabase-+-powersync/rls-and-sync-rules). + +## Initialize Your FlutterFlow Project + +1. Create a new Blank app, give it a name, and disable Firebase. +2. Under **"App Settings" -> "Integrations"**, enable Supabase. Enter your **"API URL"** and **"Anon Key"** and click **"Get Schema".** +3. Under **"App Values" -> "Constants"**, click **"Add App Constant".** + 1. For **Constant Name**, enter `PowerSyncUrl`. + 2. For **Constant Value**, copy and paste your instance URL from the PowerSync Dashboard: + + + + + +You should now see this under App Constants: + + + + + +## Build A Sign-In Screen + +1. Under Pages, click **"Add Page, Component or Flow".** + + + +2. Select the Auth1 template and name the page "Login". +3. Delete the _Sign Up_, _Forgot Password_ and _Social Login_ buttons — we will only be supporting Login for this demo app. + + + +4. Under **"App Settings" -> "App Settings" -> "Authentication"**: + 1. Enable Authentication. + 2. Set Supabase as the Authentication Type. + 3. Set the Login page you just created as the Entry Page. + 4. Set HomePage as the Logged In Page: + + + +5. In your Supabase Dashboard, under **"Authentication"**, click on **"Add User" -> "Create new user"** and create a user for yourself to test with: + + + +6. Launch your app on a physical or simulator device: + + + + + + **Checkpoint:** you should now be able to log into the app using the Supabase user account you just created. After logging in you should see a blank screen. + + +For once, a blank screen means success: + + +## Initialize PowerSync + +1. Click on **"Custom Code" -> "Add" -> "Action".** +2. Name the Custom Action `initpowersync`. + 1. **NOTE:** use all lowercase for this Custom Action is important due to naming conversion that FF performs behind the scenes. +3. Copy and paste the custom action code from here:[https://github.com/powersync-ja/powersync-flutterflow-template/blob/flutterflow/lib/custom\_code/actions/initpowersync.dart](https://github.com/powersync-ja/powersync-flutterflow-template/blob/flutterflow/lib/custom_code/actions/initpowersync.dart) +4. Import your schema: + 1. On the PowerSync Dashboard, right-click on your instance and select **"Generate Client-Side Schema"** and select Dart as the language. + + + + 2. Paste this into your Custom Action code on line 27 after the equals sign. + 3. Due to a limitation in FF, you now need to prefix each instance of `Schema`, `Column` and `Table` with `powersync`. + 4. Your custom action schema definition should now look like this: + + + +5. Under **"Action Settings"** on the right, add this dependency into "Pubspec Dependencies": `powersync: ^1.8.4` + 1. FlutterFlow imports and old version of sqflite by default and it's not possible to remove it, so you also need to add this dependency: `sqflite: ^2.3.3` + 2. Your dependencies should now look as follows: + + + + + +1. Save your new custom action +2. Still in Custom Actions, under **"Custom Files"** click on `main.dart` and set your new Custom Action as a Final Action and click Save. + + + + + +**Checkpoint:** You should now be able to validate that PowerSync is initializing correctly by taking these steps: + +1. Stop any running simulator app sessions +2. Restart the app by clicking "Test", and sign in +3. Click on "Open Device Logs" +4. You should see this kind of log message: + +``` +flutter: [PowerSync] FINE: 2024-04-16 13:47:52.259974: Credentials: PowerSyncCredentials + +flutter: [PowerSync] FINE: 2024-04-16 13:47:52.607802: Applied checkpoint 2 +``` + + + +## Reading Data + +We will now create our first UI and bind it to the data in the local SQLite database on the device. + +### Create a Custom Action to Stream all Lists + + + For watched (realtime) queries in FlutterFlow, you need 2x Custom Actions per table. For delete, update and insert queries you only need 1x Custom Action. We are working to see if we can aleviate this constraint. + + +1. Create a new Custom Action and call it `watchLists` and paste the below code: + +```dart +// Automatic FlutterFlow imports +import '/backend/supabase/supabase.dart'; +import '/flutter_flow/flutter_flow_theme.dart'; +import '/flutter_flow/flutter_flow_util.dart'; +import '/custom_code/actions/index.dart'; // Imports other custom actions +import '/flutter_flow/custom_functions.dart'; // Imports custom functions +import 'package:flutter/material.dart'; +// Begin custom action code +// DO NOT REMOVE OR MODIFY THE CODE ABOVE! + +import 'package:powersync/powersync.dart' as powersync; +import '/custom_code/actions/initpowersync.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'dart:async'; + +Future watchLists( + Future Function(List? result) callback) async { + var stream = db.watch('SELECT * FROM lists'); + listsSubscription?.cancel(); //it's important to clean up any existing subscriptions otherwise app performance will degrade + listsSubscription = stream.listen((data) { + callback( + data.map((json) => ListsRow(Map.from(json))).toList()); + }); +} +``` + +2. Hit Save and click "Yes" on the popup to set the Action Arguments for you: + + + +3. Your Action Arguments should now look as follows: + + + +4. Create the second Custom Action called `getLists` and paste the following code into it: + +```dart +// Automatic FlutterFlow imports +import '/backend/supabase/supabase.dart'; +import '/flutter_flow/flutter_flow_theme.dart'; +import '/flutter_flow/flutter_flow_util.dart'; +import '/custom_code/actions/index.dart'; // Imports other custom actions +import '/flutter_flow/custom_functions.dart'; // Imports custom functions +import 'package:flutter/material.dart'; +// Begin custom action code +// DO NOT REMOVE OR MODIFY THE CODE ABOVE! + +import 'package:powersync/powersync.dart' as powersync; +import '/custom_code/actions/initpowersync.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +Future?> getLists(List? results) async { + return results; +} +``` +1. Hit Save and click "Yes" on the popup to set the Action Arguments for you: + + + +2. Your Action Arguments should now look as follows: + + + +3. On the HomePage page, you will create a placeholder Page State variable required for the next step. + 1. Click on State Management. + 2. Add a dummy variable called "notused" or similar and click Confirm: + + + + +4. Still on the HomePage page, select Actions and open **"Action Flow Editor".** + 1. Add the `watchLists` Custom Action. + 2. Click "Open" to edit the `callback` Argument for `watchLists`. + 3. Add the `getLists` Custom Action and set the `results` Action Argument to `result` and click **"Confirm":** + + + + 4. Set the **"Action Output Variable Name"** to `allLists` and you should now see this: + + + + 5. Add a second action to the chain, and set it to **"Update Page State"** and "**Rebuild Current Page"**. This is to ensure the page gets redrawn when the database updates. Your callback action should now look like this: + + + + 6. Click "Close" to exit the Action Flow Editor. +7. In the UI Builder on the HomePage page, add a ListView component and add a ListTile inside the ListView. +8. On the ListView component, click **"Generate Dynamic Children"**. Enter a variable name of `boundLists` and set its value to `allLists` (no further changes). Click Save. +9. On the ListTile component, set the Title field to **"Set from Variable"** and then get the `name` field from the `boundLists` variable: + + + + + +10. Do the same for the Subtitle field of the ListTile component, and set it to `created_at`. +11. Hot reload your app and the screen will still be blank. This is because the `lists` table is empty in Supabase. Create a test row in the table by clicking on **"Insert" -> "Insert Row"** in your Supabase Table Editor. + 1. Leave `id` and `created_at` blank. + 2. Enter a name such as "Test from Supabase". + 3. Click "Select Record" for `owner_id` and select your test user. + + +Checkpoint: you should now see your single test row magically appear in your app + + + + + + +## Creating Data + +You will now update the app so that we can capture new list entries. + +1. Create a new Custom Action called `createListItem` and paste the following code: + +```dart +// Automatic FlutterFlow imports +import '/backend/supabase/supabase.dart'; +import '/flutter_flow/flutter_flow_theme.dart'; +import '/flutter_flow/flutter_flow_util.dart'; +import '/custom_code/actions/index.dart'; // Imports other custom actions +import '/flutter_flow/custom_functions.dart'; // Imports custom functions +import 'package:flutter/material.dart'; +// Begin custom action code +// DO NOT REMOVE OR MODIFY THE CODE ABOVE! + +// Set your action name, define your arguments and return parameter, +// and then add the boilerplate code using the green button on the right! +import 'package:powersync/powersync.dart' as powersync; +import '/custom_code/actions/initpowersync.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +Future createListItem(String name) async { + var supaUserId = await Supabase.instance.client.auth.currentUser?.id; + + final results = await db.execute(''' + INSERT INTO + lists(id, created_at, name, owner_id) + VALUES(uuid(), datetime(), ?, ?) + ''', [name, supaUserId]); +} +``` + +1. Hit Save and click "Yes" on the popup to set the Action Arguments for you: + + + +2. There should now be one argument for the Custom Action called `name` of type String and not nullable. +3. In the Widget Tree view, select the HomePage page and navigate to State Management. +4. Create a new State Field called `fabClicked` and set the type to boolean and toggle the **"Initial Field Value"** toggle twice to initialize the field to false. +5. + + +6. In the Widget Tree view, drop a Floating Action Button (FAB) onto the page. +7. Click on the FAB and Open the Action Flow Editor. +8. Add an action to Update Page State. +9. Set the `fabClicked` value to `true` and click Close. + + + +10. On the Widget Palette again, add a Container child to the Column Widget. + 1. Now add a Column Widget to this Container. + 2. Add a TextField and a Button to this Column Widget. + 3. Your homepage layout should now look like this: + + + +11. Set the Container and TextField widgets to have a width of 100%. +12. Click on the Container and enable Conditional Visibility for `fabClicked`. + + + +13. Change the Button text to "Add". +14. Open the Action Flow Editor for the Add button: + 1. Add a Custom Action call to `createListItem`. + 2. Set the "name" Argument to Widget State -> TextField 1. + 3. Chain another Action of "Clear Text Fields / PIN Codes" to clear the TextField\_1 field. + 4. Chain another Action to Update Page State and set `fabClicked` to false. + 5. Your Action Editor should now look like this: + + + + + + **Checkpoint:** you should now be able hot reload your app, click on the FAB button and the TextField should appear. Enter a name and click Add. The new row should appear in the ListView and the TextField should be hidden again. + +## Deleting Data + +In this section we will add the ability to swipe on a ListTile to delete it. + +1. Create a new Custom Action called `deleteListItem` and paste the below code: + +```dart +// Automatic FlutterFlow imports +import '/backend/supabase/supabase.dart'; +import '/flutter_flow/flutter_flow_theme.dart'; +import '/flutter_flow/flutter_flow_util.dart'; +import '/custom_code/actions/index.dart'; // Imports other custom actions +import '/flutter_flow/custom_functions.dart'; // Imports custom functions +import 'package:flutter/material.dart'; +// Begin custom action code +// DO NOT REMOVE OR MODIFY THE CODE ABOVE! + +import 'package:powersync/powersync.dart' as powersync; +import '/custom_code/actions/initpowersync.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +Future deleteListItem(ListsRow row) async { + await db.execute('DELETE FROM lists WHERE id = ?', [row.id]); +} +``` + +1. Hit Save and click "Yes" on the popup to set the Action Arguments for you: + + + +2. The Custom Action Arguments should now look as follows: + + + +3. In the Widget Tree select the ListTile and enable Slidable. +4. Select the SlidableActionWidget from the Widget Tree and set the values to the following: + + + +5. Click on Action Editor and click Add Action, passing in `boundLists` to `deleteListItem` as follows: + + + + + + **Checkpoint:** Stop and relaunch the app (Hot Reload won't work after adding the slidable package) and you should be able to swipe on items to delete them. Note that they are also magically deleted from Supabase! + + +## Updating Data + +In this section we will add the ability to update a list item. It entails: + +* A custom action to handle updating the data +* Setting and using state fields to show/hide UI dynamically and reference the list item to edit +* A button to edit a list item (set up similar to the Delete button in the previous section) +* UI to enter and save the new item name (set up similar to the Create functionality we covered earlier) +1. Create a new Custom Action called `updateListItem` and paste the below code: + +```dart +// Automatic FlutterFlow imports +import '/backend/supabase/supabase.dart'; +import '/flutter_flow/flutter_flow_theme.dart'; +import '/flutter_flow/flutter_flow_util.dart'; +import '/custom_code/actions/index.dart'; // Imports other custom actions +import '/flutter_flow/custom_functions.dart'; // Imports custom functions +import 'package:flutter/material.dart'; +// Begin custom action code +// DO NOT REMOVE OR MODIFY THE CODE ABOVE! + +// Set your action name, define your arguments and return parameter, +// and then add the boilerplate code using the green button on the right! +import 'package:powersync/powersync.dart' as powersync; +import '/custom_code/actions/initpowersync.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +Future updateListItem(String name, ListsRow row) async { + await db.execute('UPDATE lists SET name = ? WHERE id = ?', [name, row.id]); +} + +``` + +1. Hit Save and click "Yes" on the popup to set the Action Arguments for you: + + + + +1. The Custom Action Arguments should now look as follows: + + + + + +1. In the Widget Tree view, select the HomePage page and navigate to State Management. +2. Create a new State Field called `editClicked`, set the type to Boolean and toggle the **"Initial Field Value"** toggle twice to initialize the field to false. +3. Create another new State Field called `listItemIndex`, set the type to Integer. Click Confirm. + + + +4. In the Widget Tree select the ListTile. +5. Under Slidable Properties click Add Action. +6. Select the new SlidableActionWidget from the Widget Tree and set its properties to the following: + + + +7. Open the Action Flow Editor. +8. Add an action to Update Page State. +9. Add Field: Set the `editClicked` value to `true`. +10. Add Field: Set the value of `listItemIndex` to the "Index in List" of the `boundLists` Item and click Close. + + + + +1. Chain another Action to "Set Form Field" -> TextField\_2\. This will initialize the text field to the current list item's name. + 1. Set the variable to the boundLists Item + 2. Under Available Options, select "Get Row Field" + 3. Under Supabase Row Field, select "name" + 4. Your action should look like this: + + + +2. On the Widget Palette again, add a Container child to the Column Widget. + 1. Now add a Column Widget to this Container. + 2. Add a TextField and a Button to this Column Widget. + 3. Your homepage layout should now look like this: + + + +3. Set the Container and TextField widgets to have a width of 100%. +4. Click on the Container and enable Conditional Visibility for `editClicked`. + + + +5. Change the Button text to "Save" +6. Open the Action Flow Editor for the Save button. + 1. Add a Custom Action call to `updateListItem`. + 2. Set the "name" Argument to Widget State -> TextField 2. + 3. Set the "row" Argument: + 1. Select Action Outputs -> allLists. + 2. Under Available Options select "Item at Index". + 3. Under List Index Options select "Specific Index". + 4. Set the Index value to the `listItemIndex` Page State variable + + + + 5. Click Confirm + 4. Chain another Action of "Clear Text Fields / PIN Codes" to clear the TextField\_2 field. + 5. Chain another Action to "Update Page State". + 6. Add Field: Set `editClicked` to false. + 7. Add Field: Set `listItemIndex` to Reset Value. + 8. Your Action Editor should now look like this: + + + +7. Close the Action Flow Editor. + + + **Checkpoint:** you should now be able hot reload your app, slide on an item to edit it. Enter the new item name into the text field that appears, and hit Save. The update should then reflect in Supabase. + + +## Signing Out + +1. Create a new Custom Action called `signOut` without Arguments or Return Values and paste the below code: + +```dart +// Automatic FlutterFlow imports +import '/backend/supabase/supabase.dart'; +import '/flutter_flow/flutter_flow_theme.dart'; +import '/flutter_flow/flutter_flow_util.dart'; +import '/custom_code/actions/index.dart'; // Imports other custom actions +import '/flutter_flow/custom_functions.dart'; // Imports custom functions +import 'package:flutter/material.dart'; +// Begin custom action code +// DO NOT REMOVE OR MODIFY THE CODE ABOVE! + +// Set your action name, define your arguments and return parameter, +// and then add the boilerplate code using the green button on the right! +import 'package:powersync/powersync.dart' as powersync; +import '/custom_code/actions/initpowersync.dart'; + +Future signOut() async { + listsSubscription?.cancel(); //close any open subscriptions from watch() queries + await db.disconnectAndClear(); +} +``` + +1. Click Save Action. +2. In the Widget Tree, drag a Button onto the right of your App Bar. +3. Rename the button text to "Sign Out". +4. Open Action Editor and click Open to launch the editor. +5. Add a call to the `signOut`Custom Action. +6. Chain another call to Auth -> Log Out: + + + +7. Click Close + + + **Checkpoint:** You should now be able to hot reload your app and sign out and in again. + + +## Securing Your App + +PowerSync's [Sync Rules](/usage/sync-rules) and Supabase's support for [Row Level Security (RLS)](https://supabase.com/docs/guides/auth/row-level-security) can be used in conjunction. Here are some high level similarities and differences: + +* RLS should be used as the authoritative set of security rules applied to your users' CRUD operations that reach Postgres. +* Sync Rules are only applied for data that is to be downloaded to clients — they do not apply to uploaded data. + * Sync Rules can typically be considered to be complementary to RLS, and will generally mirror your RLS setup. + +### Enable RLS in Supabase + +Run the below in your Supabase console to ensure that only list owners can perform actions on the lists table where `owner_id` matches their user id: + +```sql +alter table public.lists + enable row level security; + +create policy "owned lists" on public.lists for ALL using ( + auth.uid() = owner_id +) +``` + +### Update Sync Rules + +Currently all lists are synced to all users, regardless of who the owner of the list is. You will now update this so that only a user's lists are synced to their device: + +1. Navigate to the [PowerSync Dashboard](/usage/tools/powersync-dashboard) and open your `sync-rules.yaml` file. +2. Delete the existing content and paste the below contents: + +```yaml +bucket_definitions: + user_lists: + parameters: select request.user_id() as user_id + data: + - select * from lists where owner_id = bucket.user_id +``` + +1. Click on **"Validate".** +2. Click on **"Deploy sync rules".** +3. Wait for the deploy to complete. + + + **Checkpoint:** Your app should continue running seamlessly as before. + + +## Known Issues, Limitations and Gotchas + +Below is a list of known issues and limitations. + +1. It's not currently possible to use the FlutterFlow Web Editor to test your app due to limitations with FlutterFlow. +2. When trying to compile any of the PowerSync Custom Actions, you will see errors — these can be safely ignored: + + + + + + +3. Using `watch()` queries creates a [StreamSubscription](https://api.flutter.dev/flutter/dart-async/StreamSubscription-class.html) and it's important to regularly call `.cancel()` on these to avoid multiple subscriptions for the same query running. +4. Deploying to the Apple App Store currently requires some workarounds due to limitations in FlutterFlow: + 1. Download the code from FlutterFlow + 2. Open the `Podfile` located in the `ios/`directory + 3. The following option in the `Podfile` needs to be updated from `use_frameworks! :linkage => :static` to `use_frameworks!` (remove everything after the exclamation sign) + 4. After removing that option, clean the build folder and build the project again. + 5. You should now be able to submit to the App Store +5. Exporting the code from FlutterFlow using the "Download Code" action in FlutterFlow requires the same workaround listed in 4\. above. +6. Other common issues and troubleshooting techniques are documented here: [Troubleshooting](/resources/troubleshooting) diff --git a/integration-guides/supabase-+-powersync.mdx b/integration-guides/supabase-+-powersync.mdx index 7b8ffd3..46ffd04 100644 --- a/integration-guides/supabase-+-powersync.mdx +++ b/integration-guides/supabase-+-powersync.mdx @@ -122,13 +122,13 @@ create publication powersync for table public.lists, public.todos; [Sync Rules](/usage/sync-rules) allow developers to control which data gets synced to which user devices using a SQL-like syntax in a YAML file. For the demo app, we're going to specify that each user can only see their own to-do lists and list items. -1\. To update your sync rules, open the `sync-rules.yaml` file. +1. To update your sync rules, open the `sync-rules.yaml` file. -1. Replace the `sync-rules.yaml` file's contents with the below: +2. Replace the `sync-rules.yaml` file's contents with the below: ```yaml bucket_definitions: @@ -140,16 +140,14 @@ bucket_definitions: - select * from todos where list_id = bucket.list_id ``` -1. In the top right, click **"Validate sync rules"** and ensure there are no errors. This validates your sync rules against your Postgres database. -2. In the top right, click **"Deploy sync rules"** and select your Instance - - - -3. Confirm in the dialog and wait a couple of minutes for the deployment to complete. - -For additional information on PowerSync's sync rules, refer to the [Sync Rules](/usage/sync-rules) documentation. +3. In the top right, click **"Validate sync rules"** and ensure there are no errors. This validates your sync rules against your Postgres database. +4. In the top right, click **"Deploy sync rules"** and select your instance. +5. Confirm in the dialog and wait a couple of minutes for the deployment to complete. -If you're wondering how Sync Rules relate to Supabase Postgres [RLS](https://supabase.com/docs/guides/auth/row-level-security), see [this subsection](/integration-guides/supabase-+-powersync/rls-and-sync-rules). + +- For additional information on PowerSync's Sync Rules, refer to the [Sync Rules](/usage/sync-rules) documentation. +- If you're wondering how Sync Rules relate to Supabase Postgres [RLS](https://supabase.com/docs/guides/auth/row-level-security), see [this subsection](/integration-guides/supabase-+-powersync/rls-and-sync-rules). + ## Test Everything (Using Our Demo App) diff --git a/mint.json b/mint.json index fdf5c28..b1249f2 100644 --- a/mint.json +++ b/mint.json @@ -252,7 +252,7 @@ "integration-guides/flutterflow-+-powersync", "integration-guides/flutterflow-+-powersync/flutter-web", "integration-guides/flutterflow-+-powersync/full-text-search", - "integration-guides/flutterflow-+-powersync/github-workflow" + "integration-guides/flutterflow-+-powersync/powersync-+-flutterflow-legacy" ] }, "integration-guides/railway-+-powersync",