diff --git a/src/routes/docs/tutorials/flutter/step-1/+page.markdoc b/src/routes/docs/tutorials/flutter/step-1/+page.markdoc index 3a49cf1d88..c2f5a5fccb 100644 --- a/src/routes/docs/tutorials/flutter/step-1/+page.markdoc +++ b/src/routes/docs/tutorials/flutter/step-1/+page.markdoc @@ -1,19 +1,28 @@ --- layout: tutorial -title: Coming soon -description: Learn to build an Flutter app with no backend code using an Appwrite backend. -framework: Flutter -back: /docs/tutorials -category: Mobile and native +title: Build an ideas tracker with Flutter +description: Learn to build a Flutter app with no backend code using an Appwrite backend. step: 1 -draft: true +difficulty: beginner +back: /docs/tutorials +readtime: 15 +category: Mobile and native +framework: Flutter --- -Improve the docs, add this guide. +**Idea tracker**: an app to track all the side project ideas that you'll start, but probably never finish. +In this tutorial, you will build Idea tracker with Appwrite and Flutter. + +## Concepts {% #concepts %} +This tutorial will introduce the following concepts: + +1. Setting up your first project +2. Authentication +3. Databases and tables +4. Queries and pagination -We still don't have this guide in place, but we do have some great news. -The Appwrite docs, just like Appwrite, is completely open sourced. -This means, anyone can help improve them and add new guides and tutorials. -If you see this page, **we're actively looking for contributions to this page**. -Follow our contribution guidelines, open a PR to [our Website repo](https://github.com/appwrite/website), and collaborate with our core team to improve this page. \ No newline at end of file +## Prerequisites {% #prerequisites %} +1. Android, iOS simulators, or a physical device to run the app +2. Have [Dart](https://dart.dev/) and [Flutter SDK](https://flutter.dev/) installed on your computer +3. Basic knowledge of Flutter and Provider. \ No newline at end of file diff --git a/src/routes/docs/tutorials/flutter/step-2/+page.markdoc b/src/routes/docs/tutorials/flutter/step-2/+page.markdoc new file mode 100644 index 0000000000..c6099f7e50 --- /dev/null +++ b/src/routes/docs/tutorials/flutter/step-2/+page.markdoc @@ -0,0 +1,32 @@ +--- +layout: tutorial +title: Create app +description: Create a Flutter app project using Appwrite. +step: 2 +--- + +## Create Flutter project {% #create-flutter-project %} + +Create a Flutter app with the `flutter create` command. + +```sh +flutter create ideas_tracker +cd ideas_tracker +``` + +## Add dependencies {% #add-dependencies %} + +Install the Flutter Appwrite SDK and provider. + +```sh +flutter pub add appwrite +flutter pub add provider +``` + +For iOS, make sure you have CocoaPods installed. Then install the pods to complete the installation: + +``` +cd ios +pod install +cd .. +``` \ No newline at end of file diff --git a/src/routes/docs/tutorials/flutter/step-3/+page.markdoc b/src/routes/docs/tutorials/flutter/step-3/+page.markdoc new file mode 100644 index 0000000000..9414fbff41 --- /dev/null +++ b/src/routes/docs/tutorials/flutter/step-3/+page.markdoc @@ -0,0 +1,57 @@ +--- +layout: tutorial +title: Set up Appwrite +description: Import and initialize Appwrite for your Flutter application. +step: 3 +--- + +## Create project {% #create-project %} + +Head to the [Appwrite Console](https://cloud.appwrite.io/console). + +{% only_dark %} +![Create project screen](/images/docs/quick-starts/dark/create-project.png) +{% /only_dark %} +{% only_light %} +![Create project screen](/images/docs/quick-starts/create-project.png) +{% /only_light %} + +If this is your first time using Appwrite, create an account and create your first project. + +Then, under **Add a platform**, add a Flutter platform (Android/iOS/Linux etc.) with the package/bundle ID `com.example.ideas_tracker`. + +{% only_dark %} +![Add a platform](/images/docs/quick-starts/dark/add-platform.png) +{% /only_dark %} +{% only_light %} +![Add a platform](/images/docs/quick-starts/add-platform.png) +{% /only_light %} + +You can skip optional steps. + +## Initialize Appwrite SDK {% #init-sdk %} + +To use Appwrite in our Flutter app, you'll need to find our project ID. +Find your project's ID in the **Settings** page. + +{% only_dark %} +![Project settings screen](/images/docs/quick-starts/dark/project-id.png) +{% /only_dark %} +{% only_light %} +![Project settings screen](/images/docs/quick-starts/project-id.png) +{% /only_light %} + +Create a new file `lib/appwrite.dart` to hold our Appwrite related code. +Only one instance of the `Client()` should be created per app. +Add the following code to it, replacing `` with your project ID and `` with your project region. + +```dart +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint("https://.cloud.appwrite.io/v1") + .setProject(""); + +final account = Account(client); +final db = Databases(client); +``` \ No newline at end of file diff --git a/src/routes/docs/tutorials/flutter/step-4/+page.markdoc b/src/routes/docs/tutorials/flutter/step-4/+page.markdoc new file mode 100644 index 0000000000..cc03fcfa06 --- /dev/null +++ b/src/routes/docs/tutorials/flutter/step-4/+page.markdoc @@ -0,0 +1,247 @@ +--- +layout: tutorial +title: Add authentication +description: Add authentication to your Flutter application. +step: 4 +--- + +## User context {% #user-context %} + +In Flutter, you can use [provider](https://pub.dev/packages/provider) for state management. + +Create a new file `lib/providers/user_provider.dart` and add the following code to it. + +```dart +import 'package:appwrite/appwrite.dart'; +import 'package:appwrite/models.dart' as models; +import 'package:flutter/material.dart'; +import 'package:ideas_tracker/appwrite.dart'; + +class UserProvider extends ChangeNotifier { + models.User? _current; + + models.User? get current => _current; + + UserProvider() { + init(); + } + + Future login(String email, String password) async { + try { + await account.createEmailPasswordSession( + email: email, + password: password, + ); + _current = await account.get(); + notifyListeners(); + debugPrint('Welcome back. You are logged in'); + } catch (e) { + rethrow; + } + } + + Future logout() async { + try { + await account.deleteSession(sessionId: 'current'); + _current = null; + notifyListeners(); + debugPrint("Logged out"); + } catch(e) { + rethrow; + } + } + + Future register(String email, String password) async { + try { + await account.create(userId: ID.unique(), email: email, password: password); + await login(email, password); + notifyListeners(); + debugPrint("Account created"); + } catch (e) { + rethrow; + } + } + + Future init() async { + try { + _current = await account.get(); + notifyListeners(); + } catch (e) { + _current = null; + notifyListeners(); + } + } +} +``` + +Add the `UserProvider` to `main.dart` to make it accessible throughout the App. + +```dart +import 'package:flutter/material.dart'; +import 'package:ideas_tracker/providers/user_provider.dart'; +import 'package:ideas_tracker/screens/login.dart'; +import 'package:provider/provider.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => UserProvider()), + ], + child: const MyApp(), + ) + ); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: "Ideas Tracker", + debugShowCheckedModeBanner: false, + home: Login() + ); + } +} +``` + +Now, you can use `UserProvider` to access the user's data inside any Widget. + +## Styling {% #styling %} + +To maintain DRY principles, we will move all styling constants to `lib/styles.dart`. Defining these as static class members allows for consistent, reusable widget styling across the entire app. + +```dart +import 'package:flutter/material.dart'; + +class Styles { + static TextStyle heading = TextStyle(fontSize: 24, fontWeight: FontWeight.w600); + + static InputDecoration input = InputDecoration( + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.grey), + borderRadius: BorderRadius.circular(22), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.pink, width: 2), + borderRadius: BorderRadius.circular(22), + ), + ); + + static ButtonStyle button = ElevatedButton.styleFrom( + backgroundColor: Colors.pinkAccent, + foregroundColor: Colors.white, + disabledBackgroundColor: Colors.grey, + disabledForegroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 12, horizontal: 24), + ); +} +``` + +## Login page {% #login-page %} + +Create a new file `lib/screens/login.dart` and add the following code to it. +this page contains a basic form to allow the user to login or register. +Notice how this page utilizes the `UserProvider` to perform login and register actions. + +```dart +import 'package:flutter/material.dart'; +import 'package:ideas_tracker/providers/user_provider.dart'; +import 'package:ideas_tracker/styles.dart'; +import 'package:provider/provider.dart'; + +class Login extends StatefulWidget { + + const Login({super.key}); + + @override + State createState() => _LoginState(); +} + +class _LoginState extends State { + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Login or Register", style: Styles.heading), + SizedBox(height: 40,), + TextField( + controller: _emailController, + decoration: Styles.input.copyWith( + hintText: "Email" + ), + ), + SizedBox(height: 25), + TextField ( + controller: _passwordController, + obscureText: true, + decoration: Styles.input.copyWith( + hintText: "Password" + ), + ), + SizedBox(height: 25), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () async { + try { + await context.read().login( + _emailController.text, + _passwordController.text, + ); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString())), + ); + } + }, + style: Styles.button, + child: Text("Login") + ), + SizedBox(width: 24), + ElevatedButton( + onPressed: () async { + try { + await context.read().register( + _emailController.text, + _passwordController.text, + ); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString())), + ); + } + }, + style: Styles.button, + child: Text("Register"), + ), + ], + ) + ]), + ), + ) + ); + } +} +``` \ No newline at end of file diff --git a/src/routes/docs/tutorials/flutter/step-5/+page.markdoc b/src/routes/docs/tutorials/flutter/step-5/+page.markdoc new file mode 100644 index 0000000000..4cfd5caae7 --- /dev/null +++ b/src/routes/docs/tutorials/flutter/step-5/+page.markdoc @@ -0,0 +1,107 @@ +--- +layout: tutorial +title: Add routing +description: Add routing to your Flutter application using Appwrite. +step: 5 +--- +In this step, you'll add some basic routing to your app. +Based on the user's login status, you'll redirect them to the login page or the home page. + +## Home page {% #home-page %} + +Create a new file `lib/screens/home.dart` and add the following stub code to it. +We'll update this page later to display the ideas posted by other users and allow the user to post their ideas. + +```dart +import 'package:flutter/material.dart'; +import 'package:ideas_tracker/providers/user_provider.dart'; +import 'package:provider/provider.dart'; + +class Home extends StatelessWidget { + const Home({super.key}); + + @override + Widget build(BuildContext context) { + final userProvider = context.watch(); + final email = userProvider.current?.email ?? "Please login"; + + return Scaffold( + body: Center( + child: Text("Welcome, $email") + ) + ); + } +} +``` + +## Routing {% #routing %} +To handle routing, we can use provider to check the user's state. +This router consumes the `UserProvider` to determine if the user is logged in or not to redirect the user automatically. + +Create a file `lib/routes.dart` and add the following code to it. +```dart +import 'package:flutter/material.dart'; +import 'package:ideas_tracker/screens/home.dart'; +import 'package:ideas_tracker/screens/login.dart'; +import 'package:provider/provider.dart'; +import 'providers/user_provider.dart'; + +class Routes extends StatelessWidget { + const Routes({super.key}); + + @override + Widget build(BuildContext context) { + final userProvider = context.watch(); + + if (userProvider.current != null) { + return const Home(); + } else { + return const Login(); + } + } +} + +``` + +We'll display this router in the `main.dart` file by replacing `Login()` by `Routes()`. + +```dart +import 'package:flutter/material.dart'; +import 'package:ideas_tracker/providers/user_provider.dart'; +import 'package:ideas_tracker/routes.dart'; +import 'package:provider/provider.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => UserProvider()), + ], + child: const MyApp(), + ) + ); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: "Ideas Tracker", + debugShowCheckedModeBanner: false, + home: Routes() + ); + } +} +``` + +## Test the routing {% #test-routing %} +Now, if you run the app, you should see the login page. +If you create a new account, you should be logged in and redirected to the home page. + +Run the app using the following command. +```sh +flutter run +``` \ No newline at end of file diff --git a/src/routes/docs/tutorials/flutter/step-6/+page.markdoc b/src/routes/docs/tutorials/flutter/step-6/+page.markdoc new file mode 100644 index 0000000000..72d3c05cf6 --- /dev/null +++ b/src/routes/docs/tutorials/flutter/step-6/+page.markdoc @@ -0,0 +1,165 @@ +--- +layout: tutorial +title: Add database +description: Connect a database to your Flutter application using Appwrite Flutter SDK. +step: 6 +--- +In this step, you'll set up a database to store ideas in Appwrite, configure permissions, then create a +context to manage ideas in your Flutter app. + +## Create database {% #create-database %} +To store your ideas, you need to create a database first. + +1. Go to the Databases section in your Appwrite Console +2. Click *Create Database* +3. Give it a name and ID. For this tutorial, we'll use `Ideas Tracker` as the name and `default` as the ID. +4. You'll need to remember the database ID as you'll need it later. + +## Create table {% #create-table %} +In Appwrite, data is stored as a table of rows. Create a table in the [Appwrite Console](https://cloud.appwrite.io/) to store our ideas. +Make sure to the remember the table ID, for this tutorial, we'll use `ideas_tracker` as the table ID. + +{% only_dark %} +![Create project screen](/images/docs/tutorials/dark/idea-tracker-table.png) +{% /only_dark %} +{% only_light %} +![Create project screen](/images/docs/tutorials/idea-tracker-table.png) +{% /only_light %} + +Create a new table with the following columns: +| Field | Type | Required | Size | +|-------------|---------|----------|------| +| userId | Varchar | Yes | 200 | +| title | Varchar | Yes | 200 | +| description | Text | No | - | + +## Configure permissions {% #configure-permissions %} +{% only_dark %} +![Table permissions screen](/images/docs/tutorials/dark/idea-tracker-permissions.png) +{% /only_dark %} +{% only_light %} +![Table permissions screen](/images/docs/tutorials/idea-tracker-permissions.png) +{% /only_light %} + +Navigate to the **Settings** tab of your table, add the role **Any** and check the **Read** box. +Next, add a **Users** role and give them access to **Create** by checking those boxes. +These permissions apply to all rows in your new table. + +Finally, enable **Row security** to allow further permissions to be set at the row level. +Remember to click the **Update** button to apply your changes. + +## Ideas context {% #ideas-context %} + +Now that you have a table to hold ideas, we can read and write to it from our app. +Like you did with the user data, we will create a Flutter provider to hold our ideas. + +Create a new file `lib/providers/ideas_provider.dart` and add the following code to it. + +```dart +import 'package:appwrite/appwrite.dart'; +import 'package:flutter/material.dart'; +import 'package:appwrite/models.dart' as models; +import 'package:ideas_tracker/appwrite.dart'; + +class IdeasProvider extends ChangeNotifier { + static const String ideasDatabaseId = "default"; // Replace with your database ID + static const String ideasCollectionId = "ideas_tracker"; // Replace with your table ID + + List _ideas = []; + List get current => _ideas; + + IdeasProvider() { + init(); + } + + Future init() async { + try { + final response = await db.listDocuments( + databaseId: ideasDatabaseId, + collectionId: ideasCollectionId, + queries: [Query.orderDesc("\$createdAt"), Query.limit(10)], + ); + _ideas = response.documents; + notifyListeners(); + } catch (e) { + debugPrint("Error fetching ideas: $e"); + } + } + + Future add(Map data, String userId) async { + try { + final document = await db.createDocument( + databaseId: ideasDatabaseId, + collectionId: ideasCollectionId, + documentId: ID.unique(), + data: data, + permissions: [Permission.write(Role.user(userId))], + ); + + _ideas = [document, ..._ideas]; + if (_ideas.length > 10) _ideas = _ideas.sublist(0, 10); + + notifyListeners(); + debugPrint('Idea added'); + } catch (e) { + rethrow; + } + } + + Future remove(String id) async { + try { + await db.deleteDocument( + databaseId: ideasDatabaseId, + collectionId: ideasCollectionId, + documentId: id, + ); + + _ideas.removeWhere((item) => item.$id == id); + notifyListeners(); + debugPrint('Idea removed'); + } catch (e) { + rethrow; + } + } +} +``` + +Notice that new ideas have the added permission `Permission.write(Role.user(userId))`. +This permission ensures that only the user who created the idea can modify it. + +Remember to add the `IdeasProvider` to your `main.dart` file. + +```dart +import 'package:flutter/material.dart'; +import 'package:ideas_tracker/providers/ideas_provider.dart'; +import 'package:ideas_tracker/providers/user_provider.dart'; +import 'package:ideas_tracker/routes.dart'; +import 'package:provider/provider.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => UserProvider()), + // Add IdeasProvider to make it accessible throughout the App + ChangeNotifierProvider(create: (_) => IdeasProvider()), + ], + child: const MyApp(), + ) + ); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: "Ideas Tracker", + debugShowCheckedModeBanner: false, + home: Routes() + ); + } +} +``` \ No newline at end of file diff --git a/src/routes/docs/tutorials/flutter/step-7/+page.markdoc b/src/routes/docs/tutorials/flutter/step-7/+page.markdoc new file mode 100644 index 0000000000..548acf3b42 --- /dev/null +++ b/src/routes/docs/tutorials/flutter/step-7/+page.markdoc @@ -0,0 +1,162 @@ +--- +layout: tutorial +title: Create ideas page +description: Add database queries and pagination using Appwrite in your Flutter application. +step: 7 +--- + +Using `IdeasProvider` you can now display the ideas on the page and create a form to submit new ideas. + +If an idea is submitted by the logged-in user, remove button will be enabled to remove the idea. +While this check uses the user ID to determine the disable/enable logic, permissions set in step 6 will be used to enforce +that only the owner of the idea can remove it. + +Overwrite the contents of `lib/screens/home.dart` with the following: + +```dart +import 'package:flutter/material.dart'; +import 'package:ideas_tracker/providers/ideas_provider.dart'; +import 'package:ideas_tracker/providers/user_provider.dart'; +import 'package:ideas_tracker/styles.dart'; +import 'package:provider/provider.dart'; + +class Home extends StatefulWidget { + const Home({super.key}); + + @override + State createState() => _HomeState(); +} + +class _HomeState extends State { + final TextEditingController _titleController = TextEditingController(); + final TextEditingController _descriptionController = TextEditingController(); + + @override + void dispose() { + _titleController.dispose(); + _descriptionController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final userProvider = context.watch(); + final ideasProvider = context.watch(); + final user = userProvider.current; + + return Scaffold( + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (user != null) ...[ + Text( + "Submit Idea", + style: Styles.heading, + ), + const SizedBox(height: 10), + TextField( + controller: _titleController, + decoration: Styles.input.copyWith( + hintText: "Title" + ), + ), + const SizedBox(height: 10), + TextField( + controller: _descriptionController, + decoration: Styles.input.copyWith( + hintText: "Description" + ), + ), + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () async { + try { + await ideasProvider.add({ + 'userId': user.$id, + 'title': _titleController.text, + 'description': _descriptionController.text, + }, user.$id); + _titleController.clear(); + _descriptionController.clear(); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString())), + ); + } + }, + style: Styles.button, + child: Text("Submit"), + ), + ), + ] else ...[ + const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Text("Please login to submit an idea."), + ), + ], + + const SizedBox(height: 40), + Text( + "Latest Ideas", + style: Styles.heading, + ), + const SizedBox(height: 20), + + ...ideasProvider.current.map((idea) { + final String ideaUserId = idea.data['userId'] ?? ''; + final bool isOwner = user != null && user.$id == ideaUserId; + + return Card( + margin: const EdgeInsets.only(bottom: 16), + elevation: 4, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + idea.data['title'] ?? '', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text(idea.data['description'] ?? ''), + + const SizedBox(height: 8), + ElevatedButton( + onPressed: isOwner ? () async { + try { + await ideasProvider.remove(idea.$id); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString())), + ); + } + } : null, + style: Styles.button, + child: const Text( + "Remove", + ), + ), + ], + ), + ), + ); + }).toList() + ], + ), + ), + ) + ); + } +} +``` \ No newline at end of file diff --git a/src/routes/docs/tutorials/flutter/step-8/+page.markdoc b/src/routes/docs/tutorials/flutter/step-8/+page.markdoc new file mode 100644 index 0000000000..a5977d890e --- /dev/null +++ b/src/routes/docs/tutorials/flutter/step-8/+page.markdoc @@ -0,0 +1,17 @@ +--- +layout: tutorial +title: Next steps +description: Run your Flutter project built with Appwrite +step: 8 +--- + +## Test your project {% #test-project %} +You can run your project using the `flutter run` command. This will compile your app and install it on your connected device or emulator. + +While the app is running, you can use Hot Reload by pressing `r` in the terminal to see changes instantly without restarting the app. + +## Bundling for production {% #bundle-production %} +Appwrite's Flutter SDK is compatible with standard Flutter build targets. When you are ready to distribute your app, you can generate a release binary for your specific platform: +- Android (APK/App Bundle): `flutter build apk` or `flutter build appbundle` +- iOS: `flutter build ios` +- Web: `flutter build web` \ No newline at end of file