Cross-Platform Mobile App Development Tutorial (React Native and Flutter Side by Side) - Part 2

This is the second part of the tutorial that teaches both React Native and Flutter side by side, this will allow to build the same app in both frameworks.

Table of Contents

  1. Introduction to React Native and Flutter
  2. Development Environment Setup
  3. Project Structure and Fundamentals
  4. UI Components and Styling
  5. Building the Notes App
  1. Database Integration with Appwrite
  1. Authentication
  2. Deployment and Publishing

6. Database Integration with Appwrite

Now we’ll move into database integration using Appwrite for both our React Native and Flutter apps. This section will cover setting up Appwrite, configuring the SDK, and implementing CRUD (Create, Read, Update, Delete) operations for our notes app.

6.1 Understanding Appwrite

What is Appwrite?

Appwrite is an open-source backend server that provides ready-to-use APIs for building web and mobile applications. It offers authentication, database, storage, and other essential backend services, making it an excellent choice for developers who want to focus on building the frontend while having a robust backend system.

Key features of Appwrite:

  • Authentication and user management
  • Database collections and documents
  • File storage
  • Serverless functions
  • Realtime subscriptions
  • REST and SDK APIs

6.2 Appwrite Project and Database Setup

Let’s set up our Appwrite project for both React Native and Flutter applications.

React Native Setup

  1. First, create an account on Appwrite if you don’t have one already.
  2. Create a new project called “NotesApp”.
  3. Navigate to the “Database” section and create a new database called “NotesDB”.
  4. Inside the database, create a collection called “notes” with the following attributes:
    • title (string, required)
    • content (string, required)
    • userId (string, required)
    • createdAt (datetime, required)
    • updatedAt (datetime, required)
  5. Configure collection permissions to allow read and write access to authenticated users.
  6. Create indexes for faster querying:
    • Create an index on the userId field for filtering notes by user.
    • Create an index on the createdAt field for sorting.

Flutter Setup

The Appwrite setup process remains identical for Flutter as it is platform-independent:

  1. Create the same “NotesApp” project.
  2. Create “NotesDB” database.
  3. Create the “notes” collection with the same structure and indexes.

6.3 Environment Variables

Managing environment variables is crucial for keeping sensitive information like API keys separate from your code.

React Native Environment Variables

Let’s set up environment variables in our React Native app:

// Install the necessary package
// npm install react-native-dotenv

// Create a .env file in your project root
// APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
// APPWRITE_PROJECT_ID=your-project-id
// APPWRITE_DATABASE_ID=your-database-id
// APPWRITE_COLLECTION_ID=your-collection-id

Now, configure babel to use these environment variables:

// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
    plugins: [
      [
        "module:react-native-dotenv",
        {
          moduleName: "@env",
          path: ".env",
          blacklist: null,
          whitelist: null,
          safe: false,
          allowUndefined: true,
        },
      ],
    ],
  };
};

Flutter Environment Variables

In Flutter, we’ll use the flutter_dotenv package:

// Install the package
// Add to pubspec.yaml:
// dependencies:
//   flutter_dotenv: ^5.0.2

// Create a .env file in your project root with the same variables as React Native
// APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
// APPWRITE_PROJECT_ID=your-project-id
// APPWRITE_DATABASE_ID=your-database-id
// APPWRITE_COLLECTION_ID=your-collection-id

Configure Flutter to include the .env file in assets:

# pubspec.yaml
flutter:
  assets:
    - .env

Then load the environment variables in your main.dart:

import 'package:flutter_dotenv/flutter_dotenv.dart';

Future main() async {
  // Load environment variables before running the app
  await dotenv.load(fileName: ".env");
  runApp(MyApp());
}

6.4 Appwrite SDK Installation and Configuration

Let’s install and configure the Appwrite SDK for both platforms.

React Native Appwrite SDK

Install the required packages:

// Terminal
npm install appwrite

Create a configuration file:

// src/services/appwrite-config.js
import { Client } from "appwrite";
import { APPWRITE_ENDPOINT, APPWRITE_PROJECT_ID } from "@env";

// Initialize the Appwrite client
const client = new Client();

client.setEndpoint(APPWRITE_ENDPOINT).setProject(APPWRITE_PROJECT_ID);

export default client;

Flutter Appwrite SDK

Add Appwrite to your Flutter project:

# pubspec.yaml
dependencies:
  appwrite: ^8.3.0

Create a configuration file:

// lib/services/appwrite_config.dart
import 'package:appwrite/appwrite.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';

// Initialize Appwrite client
Client getClient() {
  Client client = Client();
  return client
    .setEndpoint(dotenv.env['APPWRITE_ENDPOINT']!)
    .setProject(dotenv.env['APPWRITE_PROJECT_ID']!);
}

6.5 Database Service

Let’s create services to interact with our Appwrite database.

React Native Database Service

// src/services/database-service.js
import { Databases, Query } from "appwrite";
import client from "./appwrite-config";
import { APPWRITE_DATABASE_ID, APPWRITE_COLLECTION_ID } from "@env";

// Initialize the Databases SDK
const databases = new Databases(client);

// List all documents/notes in the collection
export const listDocuments = async (queries = []) => {
  try {
    // Fetch documents from the specified database and collection
    // 'queries' parameter allows filtering, sorting, and limiting results
    const response = await databases.listDocuments(
      APPWRITE_DATABASE_ID,
      APPWRITE_COLLECTION_ID,
      queries
    );
    // Return the documents array from the response
    return response.documents;
  } catch (error) {
    // Log and rethrow any errors that occur during the operation
    console.error("Error listing documents:", error);
    throw error;
  }
};

Flutter Database Service

// lib/services/database_service.dart
import 'package:appwrite/appwrite.dart';
import 'package:appwrite/models.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'appwrite_config.dart';

class DatabaseService {
  // Get the Appwrite client from our config
  final Client _client = getClient();
  late final Databases _databases;

  // Constructor initializes the Databases instance
  DatabaseService() {
    _databases = Databases(_client);
  }

  // List all documents/notes in the collection
  Future<List<Document>> listDocuments({List<String>? queries}) async {
    try {
      // Fetch documents from the specified database and collection
      final response = await _databases.listDocuments(
        databaseId: dotenv.env['APPWRITE_DATABASE_ID']!,
        collectionId: dotenv.env['APPWRITE_COLLECTION_ID']!,
        queries: queries,
      );
      // Return the documents list from the response
      return response.documents;
    } catch (e) {
      // Log and rethrow any errors
      print('Error listing documents: $e');
      throw e;
    }
  }
}

6.6 Note Service

Now, let’s create a specialized service for our notes operations.

React Native Note Service

// src/services/note-service.js
import { Query } from "appwrite";
import { listDocuments } from "./database-service";
import client from "./appwrite-config";
import { Databases, ID } from "appwrite";
import { APPWRITE_DATABASE_ID, APPWRITE_COLLECTION_ID } from "@env";

const databases = new Databases(client);

// Get all notes, potentially filtered by userId
export const getNotes = async (userId = null) => {
  try {
    // Create query array - initially empty
    const queries = [];

    // If userId is provided, add a filter to only get notes for that user
    if (userId) {
      queries.push(Query.equal("userId", userId));
    }

    // Add sorting by createdAt in descending order (newest first)
    queries.push(Query.orderDesc("createdAt"));

    // Use the listDocuments function from database-service
    const notes = await listDocuments(queries);
    return notes;
  } catch (error) {
    console.error("Error getting notes:", error);
    throw error;
  }
};

// Create a new note
export const createNote = async (data) => {
  try {
    // Add timestamps to the note data
    const noteData = {
      ...data,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    };

    // Create a document in the database
    const response = await databases.createDocument(
      APPWRITE_DATABASE_ID,
      APPWRITE_COLLECTION_ID,
      ID.unique(), // Generate a unique ID
      noteData
    );

    return response;
  } catch (error) {
    console.error("Error creating note:", error);
    throw error;
  }
};

// Delete a note by ID
export const deleteNote = async (noteId) => {
  try {
    // Delete the document with the specified ID
    await databases.deleteDocument(
      APPWRITE_DATABASE_ID,
      APPWRITE_COLLECTION_ID,
      noteId
    );

    return true;
  } catch (error) {
    console.error("Error deleting note:", error);
    throw error;
  }
};

// Update an existing note
export const updateNote = async (noteId, data) => {
  try {
    // Add updated timestamp
    const noteData = {
      ...data,
      updatedAt: new Date().toISOString(),
    };

    // Update the document in the database
    const response = await databases.updateDocument(
      APPWRITE_DATABASE_ID,
      APPWRITE_COLLECTION_ID,
      noteId,
      noteData
    );

    return response;
  } catch (error) {
    console.error("Error updating note:", error);
    throw error;
  }
};

Flutter Note Service

// lib/services/note_service.dart
import 'package:appwrite/appwrite.dart';
import 'package:appwrite/models.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'appwrite_config.dart';

class NoteService {
  final Client _client = getClient();
  late final Databases _databases;

  NoteService() {
    _databases = Databases(_client);
  }

  // Get all notes, potentially filtered by userId
  Future<List<Document>> getNotes({String? userId}) async {
    try {
      // Create query list - initially empty
      List<String> queries = [];

      // If userId is provided, add a filter
      if (userId != null) {
        queries.add(Query.equal('userId', userId));
      }

      // Add sorting by createdAt descending
      queries.add(Query.orderDesc('createdAt'));

      // Fetch documents from the database
      final response = await _databases.listDocuments(
        databaseId: dotenv.env['APPWRITE_DATABASE_ID']!,
        collectionId: dotenv.env['APPWRITE_COLLECTION_ID']!,
        queries: queries,
      );

      return response.documents;
    } catch (e) {
      print('Error getting notes: $e');
      throw e;
    }
  }

  // Create a new note
  Future<Document> createNote(Map<String, dynamic> data) async {
    try {
      // Add timestamps to the note data
      final noteData = {
        ...data,
        'createdAt': DateTime.now().toIso8601String(),
        'updatedAt': DateTime.now().toIso8601String(),
      };

      // Create a document in the database
      final response = await _databases.createDocument(
        databaseId: dotenv.env['APPWRITE_DATABASE_ID']!,
        collectionId: dotenv.env['APPWRITE_COLLECTION_ID']!,
        documentId: ID.unique(), // Generate a unique ID
        data: noteData,
      );

      return response;
    } catch (e) {
      print('Error creating note: $e');
      throw e;
    }
  }

  // Delete a note by ID
  Future<bool> deleteNote(String noteId) async {
    try {
      // Delete the document with the specified ID
      await _databases.deleteDocument(
        databaseId: dotenv.env['APPWRITE_DATABASE_ID']!,
        collectionId: dotenv.env['APPWRITE_COLLECTION_ID']!,
        documentId: noteId,
      );

      return true;
    } catch (e) {
      print('Error deleting note: $e');
      throw e;
    }
  }

  // Update an existing note
  Future<Document> updateNote(String noteId, Map<String, dynamic> data) async {
    try {
      // Add updated timestamp
      final noteData = {
        ...data,
        'updatedAt': DateTime.now().toIso8601String(),
      };

      // Update the document in the database
      final response = await _databases.updateDocument(
        databaseId: dotenv.env['APPWRITE_DATABASE_ID']!,
        collectionId: dotenv.env['APPWRITE_COLLECTION_ID']!,
        documentId: noteId,
        data: noteData,
      );

      return response;
    } catch (e) {
      print('Error updating note: $e');
      throw e;
    }
  }
}

6.7 Fetch Notes from Screen Component

Now let’s integrate our services with the UI to fetch and display notes.

React Native Notes Screen

// src/screens/NotesScreen.js
import React, { useState, useEffect } from "react";
import {
  View,
  Text,
  FlatList,
  StyleSheet,
  ActivityIndicator,
} from "react-native";
import { getNotes } from "../services/note-service";
import NoteItem from "../components/NoteItem";

const NotesScreen = () => {
  const [notes, setNotes] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Function to fetch notes from the database
    const fetchNotes = async () => {
      try {
        setLoading(true);
        // Call the getNotes service function
        const fetchedNotes = await getNotes();
        // Update state with the fetched notes
        setNotes(fetchedNotes);
      } catch (err) {
        console.error("Error fetching notes:", err);
        setError("Failed to load notes. Please try again.");
      } finally {
        setLoading(false);
      }
    };

    // Call the fetch function when component mounts
    fetchNotes();
  }, []); // Empty dependency array means this runs once on mount

  // Show loading indicator while fetching data
  if (loading) {
    return (
      <View style={styles.centered}>
        <ActivityIndicator size="large" color="#0000ff" />
      </View>
    );
  }

  // Show error message if there was a problem
  if (error) {
    return (
      <View style={styles.centered}>
        <Text style={styles.errorText}>{error}</Text>
      </View>
    );
  }

  // Render the list of notes
  return (
    <View style={styles.container}>
      <Text style={styles.title}>My Notes</Text>
      <FlatList
        data={notes}
        keyExtractor={(item) => item.$id}
        renderItem={({ item }) => <NoteItem note={item} />}
        contentContainerStyle={styles.listContent}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    backgroundColor: "#f5f5f5",
  },
  title: {
    fontSize: 24,
    fontWeight: "bold",
    marginBottom: 16,
    color: "#333",
  },
  listContent: {
    paddingBottom: 20,
  },
  centered: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  errorText: {
    color: "red",
    fontSize: 16,
    textAlign: "center",
  },
});

export default NotesScreen;

Flutter Notes Screen

// lib/screens/notes_screen.dart
import 'package:flutter/material.dart';
import 'package:appwrite/models.dart';
import '../services/note_service.dart';
import '../widgets/note_item.dart';

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

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

class _NotesScreenState extends State<NotesScreen> {
  final NoteService _noteService = NoteService();
  List<Document> _notes = [];
  bool _isLoading = true;
  String? _error;

  @override
  void initState() {
    super.initState();
    // Fetch notes when the screen initializes
    _fetchNotes();
  }

  // Function to fetch notes from the database
  Future<void> _fetchNotes() async {
    try {
      setState(() {
        _isLoading = true;
        _error = null;
      });

      // Call the getNotes service function
      final fetchedNotes = await _noteService.getNotes();

      // Update state with the fetched notes
      setState(() {
        _notes = fetchedNotes;
        _isLoading = false;
      });
    } catch (e) {
      print('Error fetching notes: $e');
      setState(() {
        _error = 'Failed to load notes. Please try again.';
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    // Show loading indicator while fetching data
    if (_isLoading) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }

    // Show error message if there was a problem
    if (_error != null) {
      return Center(
        child: Text(
          _error!,
          style: const TextStyle(color: Colors.red, fontSize: 16),
        ),
      );
    }

    // Render the list of notes
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'My Notes',
              style: TextStyle(
                fontSize: 24,
                fontWeight: FontWeight.bold,
                color: Colors.black87,
              ),
            ),
            const SizedBox(height: 16),
            Expanded(
              child: ListView.builder(
                itemCount: _notes.length,
                itemBuilder: (context, index) {
                  return NoteItem(note: _notes[index]);
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

6.8 Add Note to Database

Now let’s implement the functionality to create new notes.

React Native Add Note

// src/components/AddNoteModal.js
import React, { useState } from "react";
import {
  Modal,
  View,
  TextInput,
  Button,
  StyleSheet,
  Text,
  TouchableOpacity,
} from "react-native";
import { createNote } from "../services/note-service";

const AddNoteModal = ({ visible, onClose, onNoteAdded }) => {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // Reset form state
  const resetForm = () => {
    setTitle("");
    setContent("");
    setError(null);
  };

  // Close modal and reset form
  const handleClose = () => {
    resetForm();
    onClose();
  };

  // Save the new note
  const handleSave = async () => {
    // Basic form validation
    if (!title.trim() || !content.trim()) {
      setError("Please fill in both title and content");
      return;
    }

    try {
      setLoading(true);
      setError(null);

      // Prepare note data
      const noteData = {
        title: title.trim(),
        content: content.trim(),
        userId: "current-user-id", // We'll replace this with actual user ID later
      };

      // Call create note service
      const newNote = await createNote(noteData);

      // Reset form and close modal
      resetForm();
      onClose();

      // Notify parent component about the new note
      if (onNoteAdded) {
        onNoteAdded(newNote);
      }
    } catch (err) {
      console.error("Error creating note:", err);
      setError("Failed to save note. Please try again.");
    } finally {
      setLoading(false);
    }
  };

  return (
    <Modal
      visible={visible}
      animationType="slide"
      transparent={true}
      onRequestClose={handleClose}
    >
      <View style={styles.centeredView}>
        <View style={styles.modalView}>
          <Text style={styles.modalTitle}>Add New Note</Text>

          {error && <Text style={styles.errorText}>{error}</Text>}

          <TextInput
            style={styles.input}
            placeholder="Title"
            value={title}
            onChangeText={setTitle}
          />

          <TextInput
            style={[styles.input, styles.contentInput]}
            placeholder="Content"
            value={content}
            onChangeText={setContent}
            multiline={true}
            textAlignVertical="top"
          />

          <View style={styles.buttonContainer}>
            <TouchableOpacity
              style={[styles.button, styles.cancelButton]}
              onPress={handleClose}
              disabled={loading}
            >
              <Text style={styles.buttonText}>Cancel</Text>
            </TouchableOpacity>

            <TouchableOpacity
              style={[styles.button, styles.saveButton]}
              onPress={handleSave}
              disabled={loading}
            >
              <Text style={styles.buttonText}>
                {loading ? "Saving..." : "Save Note"}
              </Text>
            </TouchableOpacity>
          </View>
        </View>
      </View>
    </Modal>
  );
};

const styles = StyleSheet.create({
  centeredView: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "rgba(0, 0, 0, 0.5)",
  },
  modalView: {
    width: "90%",
    backgroundColor: "white",
    borderRadius: 10,
    padding: 20,
    shadowColor: "#000",
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.25,
    shadowRadius: 4,
    elevation: 5,
  },
  modalTitle: {
    fontSize: 18,
    fontWeight: "bold",
    marginBottom: 16,
    textAlign: "center",
  },
  input: {
    borderWidth: 1,
    borderColor: "#ddd",
    borderRadius: 5,
    padding: 10,
    marginBottom: 15,
  },
  contentInput: {
    height: 150,
  },
  buttonContainer: {
    flexDirection: "row",
    justifyContent: "space-between",
  },
  button: {
    borderRadius: 5,
    padding: 10,
    elevation: 2,
    minWidth: "45%",
    alignItems: "center",
  },
  cancelButton: {
    backgroundColor: "#ccc",
  },
  saveButton: {
    backgroundColor: "#2196F3",
  },
  buttonText: {
    color: "white",
    fontWeight: "bold",
  },
  errorText: {
    color: "red",
    marginBottom: 10,
    textAlign: "center",
  },
});

export default AddNoteModal;

Update the Notes Screen to include the modal:

// src/screens/NotesScreen.js (updated)
import React, { useState, useEffect } from "react";
import {
  View,
  Text,
  FlatList,
  StyleSheet,
  ActivityIndicator,
  TouchableOpacity,
} from "react-native";
import { getNotes } from "../services/note-service";
import NoteItem from "../components/NoteItem";
import AddNoteModal from "../components/AddNoteModal";

const NotesScreen = () => {
  const [notes, setNotes] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [modalVisible, setModalVisible] = useState(false);

  useEffect(() => {
    fetchNotes();
  }, []);

  // Function to fetch notes from the database
  const fetchNotes = async () => {
    try {
      setLoading(true);
      const fetchedNotes = await getNotes();
      setNotes(fetchedNotes);
    } catch (err) {
      console.error("Error fetching notes:", err);
      setError("Failed to load notes. Please try again.");
    } finally {
      setLoading(false);
    }
  };

  // Add the new note to the state and avoid refetching
  const handleNoteAdded = (newNote) => {
    setNotes((currentNotes) => [newNote, ...currentNotes]);
  };

  // Show loading indicator while fetching data
  if (loading && notes.length === 0) {
    return (
      <View style={styles.centered}>
        <ActivityIndicator size="large" color="#0000ff" />
      </View>
    );
  }

  // Show error message if there was a problem
  if (error && notes.length === 0) {
    return (
      <View style={styles.centered}>
        <Text style={styles.errorText}>{error}</Text>
      </View>
    );
  }

  // Render the list of notes
  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.title}>My Notes</Text>
        <TouchableOpacity
          style={styles.addButton}
          onPress={() => setModalVisible(true)}
        >
          <Text style={styles.addButtonText}>+ Add Note</Text>
        </TouchableOpacity>
      </View>

      <FlatList
        data={notes}
        keyExtractor={(item) => item.$id}
        renderItem={({ item }) => <NoteItem note={item} />}
        contentContainerStyle={styles.listContent}
        refreshing={loading}
        onRefresh={fetchNotes}
      />

      <AddNoteModal
        visible={modalVisible}
        onClose={() => setModalVisible(false)}
        onNoteAdded={handleNoteAdded}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    backgroundColor: "#f5f5f5",
  },
  header: {
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
    marginBottom: 16,
  },
  title: {
    fontSize: 24,
    fontWeight: "bold",
    color: "#333",
  },
  addButton: {
    backgroundColor: "#2196F3",
    paddingVertical: 8,
    paddingHorizontal: 12,
    borderRadius: 5,
  },
  addButtonText: {
    color: "white",
    fontWeight: "bold",
  },
  listContent: {
    paddingBottom: 20,
  },
  centered: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  errorText: {
    color: "red",
    fontSize: 16,
    textAlign: "center",
  },
});

export default NotesScreen;

Flutter Add Note

// lib/widgets/add_note_modal.dart
import 'package:flutter/material.dart';
import '../services/note_service.dart';

class AddNoteModal extends StatefulWidget {
  final Function(Map<String, dynamic>) onNoteAdded;

  const AddNoteModal({
    Key? key,
    required this.onNoteAdded,
  }) : super(key: key);

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

class _AddNoteModalState extends State<AddNoteModal> {
  final _titleController = TextEditingController();
  final _contentController = TextEditingController();
  final _noteService = NoteService();
  bool _isLoading = false;
  String? _error;

  @override
  void dispose() {
    _titleController.dispose();
    _contentController.dispose();
    super.dispose();
  }

  // Reset form state
  void _resetForm() {
    _titleController.clear();
    _contentController.clear();
    setState(() {
      _error = null;
    });
  }

  // Save the new note
  Future<void> _handleSave() async {
    // Basic form validation
    final title = _titleController.text.trim();
    final content = _contentController.text.trim();

    if (title.isEmpty || content.isEmpty) {
      setState(() {
        _error = 'Please fill in both title and content';
      });
      return;
    }

    try {
      setState(() {
        _isLoading = true;
        _error = null;
      });

      // Prepare note data
      final noteData = {
        'title': title,
        'content': content,
        'userId': 'current-user-id', // We'll replace this with actual user ID later
      };

      // Call create note service
      final newNote = await _noteService.createNote(noteData);

      // Reset form
      _resetForm();

      // Notify parent component and close modal
      widget.onNoteAdded(newNote.data);
      Navigator.pop(context);

    } catch (e) {
      print('Error creating note: $e');
      setState(() {
        _error = 'Failed to save note. Please try again.';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Dialog(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
      ),
      elevation: 0,
      backgroundColor: Colors.transparent,
      child: contentBox(context),
    );
  }

  Widget contentBox(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        shape: BoxShape.rectangle,
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children

:

        mainAxisSize: MainAxisSize.min,
        children: [
          const Text(
            'Add New Note',
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 16),

          // Show error message if there is one
          if (_error != null)
            Padding(
              padding: const EdgeInsets.only(bottom: 10),
              child: Text(
                _error!,
                style: const TextStyle(color: Colors.red),
                textAlign: TextAlign.center,
              ),
            ),

          // Title input field
          TextField(
            controller: _titleController,
            decoration: const InputDecoration(
              hintText: 'Title',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 15),

          // Content input field
          TextField(
            controller: _contentController,
            decoration: const InputDecoration(
              hintText: 'Content',
              border: OutlineInputBorder(),
            ),
            maxLines: 5,
          ),
          const SizedBox(height: 20),

          // Buttons row
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              // Cancel button
              TextButton(
                onPressed: _isLoading ? null : () => Navigator.pop(context),
                child: const Text('Cancel'),
              ),

              // Save button
              ElevatedButton(
                onPressed: _isLoading ? null : _handleSave,
                child: Text(_isLoading ? 'Saving...' : 'Save Note'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

Update the Notes Screen to include the modal:

// lib/screens/notes_screen.dart (updated)
import 'package:flutter/material.dart';
import 'package:appwrite/models.dart';
import '../services/note_service.dart';
import '../widgets/note_item.dart';
import '../widgets/add_note_modal.dart';

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

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

class _NotesScreenState extends State<NotesScreen> {
  final NoteService _noteService = NoteService();
  List<Document> _notes = [];
  bool _isLoading = true;
  String? _error;

  @override
  void initState() {
    super.initState();
    _fetchNotes();
  }

  // Function to fetch notes from the database
  Future<void> _fetchNotes() async {
    try {
      setState(() {
        _isLoading = true;
        _error = null;
      });

      final fetchedNotes = await _noteService.getNotes();

      setState(() {
        _notes = fetchedNotes;
        _isLoading = false;
      });
    } catch (e) {
      print('Error fetching notes: $e');
      setState(() {
        _error = 'Failed to load notes. Please try again.';
        _isLoading = false;
      });
    }
  }

  // Show the add note dialog
  void _showAddNoteDialog() {
    showDialog(
      context: context,
      builder: (context) => AddNoteModal(
        onNoteAdded: _handleNoteAdded,
      ),
    );
  }

  // Add the new note to the state and avoid refetching
  void _handleNoteAdded(Map<String, dynamic> noteData) {
    // Create a temporary Document object
    final newNote = Document(
      $id: noteData['\$id'] ?? 'temp-id',
      $collectionId: 'notes',
      $databaseId: 'NotesDB',
      $createdAt: DateTime.now().toString(),
      $updatedAt: DateTime.now().toString(),
      $permissions: [],
      data: noteData,
    );

    setState(() {
      _notes = [newNote, ..._notes];
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Header with title and add button
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text(
                  'My Notes',
                  style: TextStyle(
                    fontSize: 24,
                    fontWeight: FontWeight.bold,
                    color: Colors.black87,
                  ),
                ),
                ElevatedButton(
                  onPressed: _showAddNoteDialog,
                  child: const Text('+ Add Note'),
                ),
              ],
            ),
            const SizedBox(height: 16),

            // Show loading indicator
            if (_isLoading && _notes.isEmpty)
              const Center(child: CircularProgressIndicator()),

            // Show error message
            if (_error != null && _notes.isEmpty)
              Center(
                child: Text(
                  _error!,
                  style: const TextStyle(color: Colors.red, fontSize: 16),
                ),
              ),

            // Show the notes list
            if (!_isLoading || _notes.isNotEmpty)
              Expanded(
                child: RefreshIndicator(
                  onRefresh: _fetchNotes,
                  child: ListView.builder(
                    itemCount: _notes.length,
                    itemBuilder: (context, index) {
                      return NoteItem(note: _notes[index]);
                    },
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }
}

6.9 Delete Notes

Let’s implement the functionality to delete notes.

React Native Delete Note

Update the NoteItem component:

// src/components/NoteItem.js
import React, { useState } from "react";
import { View, Text, StyleSheet, TouchableOpacity, Alert } from "react-native";
import { deleteNote } from "../services/note-service";

const NoteItem = ({ note, onNoteDeleted }) => {
  const [deleting, setDeleting] = useState(false);

  // Format the date for display
  const formatDate = (dateString) => {
    const date = new Date(dateString);
    return date.toLocaleDateString();
  };

  // Handle delete confirmation and execution
  const handleDelete = () => {
    // Show confirmation dialog
    Alert.alert("Delete Note", "Are you sure you want to delete this note?", [
      {
        text: "Cancel",
        style: "cancel",
      },
      {
        text: "Delete",
        style: "destructive",
        onPress: async () => {
          try {
            setDeleting(true);
            // Call the deleteNote service function
            await deleteNote(note.$id);
            // Notify parent component if callback exists
            if (onNoteDeleted) {
              onNoteDeleted(note.$id);
            }
          } catch (error) {
            console.error("Error deleting note:", error);
            Alert.alert("Error", "Failed to delete note. Please try again.");
          } finally {
            setDeleting(false);
          }
        },
      },
    ]);
  };

  return (
    <View style={styles.container}>
      <View style={styles.content}>
        <Text style={styles.title}>{note.title}</Text>
        <Text style={styles.date}>
          Last updated: {formatDate(note.updatedAt)}
        </Text>
        <Text style={styles.noteContent} numberOfLines={3}>
          {note.content}
        </Text>
      </View>

      <TouchableOpacity
        style={styles.deleteButton}
        onPress={handleDelete}
        disabled={deleting}
      >
        <Text style={styles.deleteText}>Delete</Text>
      </TouchableOpacity>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    backgroundColor: "white",
    borderRadius: 8,
    padding: 16,
    marginBottom: 12,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.2,
    shadowRadius: 2,
    elevation: 2,
    flexDirection: "row",
  },
  content: {
    flex: 1,
  },
  title: {
    fontSize: 18,
    fontWeight: "bold",
    marginBottom: 4,
  },
  date: {
    fontSize: 12,
    color: "#666",
    marginBottom: 8,
  },
  noteContent: {
    fontSize: 14,
    color: "#333",
  },
  deleteButton: {
    justifyContent: "center",
    paddingLeft: 12,
  },
  deleteText: {
    color: "red",
    fontWeight: "500",
  },
});

export default NoteItem;

Update the Notes Screen to handle note deletion:

// src/screens/NotesScreen.js (updated for delete)
// ... previous imports ...

const NotesScreen = () => {
  // ... previous state variables ...

  // Handle note deletion by removing it from state
  const handleNoteDeleted = (noteId) => {
    setNotes((currentNotes) =>
      currentNotes.filter((note) => note.$id !== noteId)
    );
  };

  // ... previous code ...

  // Render the list of notes
  return (
    <View style={styles.container}>
      {/* ... header code ... */}

      <FlatList
        data={notes}
        keyExtractor={(item) => item.$id}
        renderItem={({ item }) => (
          <NoteItem note={item} onNoteDeleted={handleNoteDeleted} />
        )}
        contentContainerStyle={styles.listContent}
        refreshing={loading}
        onRefresh={fetchNotes}
      />

      {/* ... AddNoteModal code ... */}
    </View>
  );
};

// ... styles ...

export default NotesScreen;

Flutter Delete Note

Update the NoteItem widget:

// lib/widgets/note_item.dart
import 'package:flutter/material.dart';
import 'package:appwrite/models.dart';
import '../services/note_service.dart';

class NoteItem extends StatefulWidget {
  final Document note;
  final Function(String)? onNoteDeleted;

  const NoteItem({
    Key? key,
    required this.note,
    this.onNoteDeleted,
  }) : super(key: key);

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

class _NoteItemState extends State<NoteItem> {
  final NoteService _noteService = NoteService();
  bool _isDeleting = false;

  // Format the date for display
  String _formatDate(String dateString) {
    final date = DateTime.parse(dateString);
    return '${date.day}/${date.month}/${date.year}';
  }

  // Handle delete confirmation and execution
  Future<void> _handleDelete() async {
    // Show confirmation dialog
    final confirmed = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Delete Note'),
        content: const Text('Are you sure you want to delete this note?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(false),
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () => Navigator.of(context).pop(true),
            child: const Text(
              'Delete',
              style: TextStyle(color: Colors.red),
            ),
          ),
        ],
      ),
    );

    // If user confirmed deletion
    if (confirmed == true) {
      try {
        setState(() {
          _isDeleting = true;
        });

        // Call the deleteNote service function
        await _noteService.deleteNote(widget.note.$id);

        // Notify parent component if callback exists
        if (widget.onNoteDeleted != null) {
          widget.onNoteDeleted!(widget.note.$id);
        }
      } catch (e) {
        print('Error deleting note: $e');

        // Show error snackbar
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('Failed to delete note. Please try again.'),
            backgroundColor: Colors.red,
          ),
        );
      } finally {
        if (mounted) {
          setState(() {
            _isDeleting = false;
          });
        }
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    // Extract note data
    final title = widget.note.data['title'] as String;
    final content = widget.note.data['content'] as String;
    final updatedAt = widget.note.$updatedAt;

    return Card(
      elevation: 2,
      margin: const EdgeInsets.only(bottom: 12),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Row(
          children: [
            // Note content
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    title,
                    style: const TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    'Last updated: ${_formatDate(updatedAt)}',
                    style: const TextStyle(
                      fontSize: 12,
                      color: Colors.grey,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    content,
                    style: const TextStyle(fontSize: 14),
                    maxLines: 3,
                    overflow: TextOverflow.ellipsis,
                  ),
                ],
              ),
            ),

            // Delete button
            IconButton(
              icon: _isDeleting
                  ? const SizedBox(
                      width: 20,
                      height: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Icon(Icons.delete, color: Colors.red),
              onPressed: _isDeleting ? null : _handleDelete,
            ),
          ],
        ),
      ),
    );
  }
}

Update the Notes Screen to handle note deletion:

// lib/screens/notes_screen.dart (updated for delete)
// ... previous imports ...

class _NotesScreenState extends State<NotesScreen> {
  // ... previous state variables and methods ...

  // Handle note deletion by removing it from state
  void _handleNoteDeleted(String noteId) {
    setState(() {
      _notes = _notes.where((note) => note.$id != noteId).toList();
    });
  }

  @override
  Widget build(BuildContext context) {
    // ... previous code ...

    // Show the notes list
    if (!_isLoading || _notes.isNotEmpty)
      Expanded(
        child: RefreshIndicator(
          onRefresh: _fetchNotes,
          child: ListView.builder(
            itemCount: _notes.length,
            itemBuilder: (context, index) {
              return NoteItem(
                note: _notes[index],
                onNoteDeleted: _handleNoteDeleted,
              );
            },
          ),
        ),
      ),

    // ... rest of the build method ...
  }
}

6.10 Update Notes

Now let’s implement the functionality to update existing notes.

React Native Update Note

Create an EditNoteModal component:

// src/components/EditNoteModal.js
import React, { useState, useEffect } from "react";
import {
  Modal,
  View,
  TextInput,
  Button,
  StyleSheet,
  Text,
  TouchableOpacity,
} from "react-native";
import { updateNote } from "../services/note-service";

const EditNoteModal = ({ visible, onClose, onNoteUpdated, note }) => {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // Initialize form with note data when it changes
  useEffect(() => {
    if (note) {
      setTitle(note.title || "");
      setContent(note.content || "");
    }
  }, [note]);

  // Reset form state
  const resetForm = () => {
    setError(null);
  };

  // Close modal and reset form
  const handleClose = () => {
    resetForm();
    onClose();
  };

  // Save the updated note
  const handleSave = async () => {
    // Basic form validation
    if (!title.trim() || !content.trim()) {
      setError("Please fill in both title and content");
      return;
    }

    try {
      setLoading(true);
      setError(null);

      // Prepare update data
      const updateData = {
        title: title.trim(),
        content: content.trim(),
      };

      // Call update note service
      const updatedNote = await updateNote(note.$id, updateData);

      // Reset form and close modal
      resetForm();
      onClose();

      // Notify parent component about the updated note
      if (onNoteUpdated) {
        onNoteUpdated(updatedNote);
      }
    } catch (err) {
      console.error("Error updating note:", err);
      setError("Failed to update note. Please try again.");
    } finally {
      setLoading(false);
    }
  };

  // Don't render if no note is provided
  if (!note) return null;

  return (
    <Modal
      visible={visible}
      animationType="slide"
      transparent={true}
      onRequestClose={handleClose}
    >
      <View style={styles.centeredView}>
        <View style={styles.modalView}>
          <Text style={styles.modalTitle}>Edit Note</Text>

          {error && <Text style={styles.errorText}>{error}</Text>}

          <TextInput
            style={styles.input}
            placeholder="Title"
            value={title}
            onChangeText={setTitle}
          />

          <TextInput
            style={[styles.input, styles.contentInput]}
            placeholder="Content"
            value={content}
            onChangeText={setContent}
            multiline={true}
            textAlignVertical="top"
          />

          <View style={styles.buttonContainer}>
            <TouchableOpacity
              style={[styles.button, styles.cancelButton]}
              onPress={handleClose}
              disabled={loading}
            >
              <Text style={styles.buttonText}>Cancel</Text>
            </TouchableOpacity>

            <TouchableOpacity
              style={[styles.button, styles.saveButton]}
              onPress={handleSave}
              disabled={loading}
            >
              <Text style={styles.buttonText}>
                {loading ? "Saving..." : "Save Changes"}
              </Text>
            </TouchableOpacity>
          </View>
        </View>
      </View>
    </Modal>
  );
};

const styles = StyleSheet.create({
  // Same styles as AddNoteModal
  centeredView: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "rgba(0, 0, 0, 0.5)",
  },
  modalView: {
    width: "90%",
    backgroundColor: "white",
    borderRadius: 10,
    padding: 20,
    shadowColor: "#000",
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.25,
    shadowRadius: 4,
    elevation: 5,
  },
  modalTitle: {
    fontSize: 18,
    fontWeight: "bold",
    marginBottom: 16,
    textAlign: "center",
  },
  input: {
    borderWidth: 1,
    borderColor: "#ddd",
    borderRadius: 5,
    padding: 10,
    marginBottom: 15,
  },
  contentInput: {
    height: 150,
  },
  buttonContainer: {
    flexDirection: "row",
    justifyContent: "space-between",
  },
  button: {
    borderRadius: 5,
    padding: 10,
    elevation: 2,
    minWidth: "45%",
    alignItems: "center",
  },
  cancelButton: {
    backgroundColor: "#ccc",
  },
  saveButton: {
    backgroundColor: "#2196F3",
  },
  buttonText: {
    color: "white",
    fontWeight: "bold",
  },
  errorText: {
    color: "red",
    marginBottom: 10,
    textAlign: "center",
  },
});

export default EditNoteModal;

Update the NoteItem component to include edit functionality:

// src/components/NoteItem.js (updated for edit)
import React, { useState } from "react";
import { View, Text, StyleSheet, TouchableOpacity, Alert } from "react-native";
import { deleteNote } from "../services/note-service";
import EditNoteModal from "./EditNoteModal";

const NoteItem = ({ note, onNoteDeleted, onNoteUpdated }) => {
  const [deleting, setDeleting] = useState(false);
  const [editModalVisible, setEditModalVisible] = useState(false);

  // Format the date for display
  const formatDate = (dateString) => {
    const date = new Date(dateString);
    return date.toLocaleDateString();
  };

  // Handle delete confirmation and execution
  const handleDelete = () => {
    // Same as before
    // ...
  };

  // Handle opening the edit modal
  const handleEdit = () => {
    setEditModalVisible(true);
  };

  // Handle when a note is updated
  const handleNoteUpdated = (updatedNote) => {
    if (onNoteUpdated) {
      onNoteUpdated(updatedNote);
    }
  };

  return (
    <View style={styles.container}>
      <TouchableOpacity style={styles.content} onPress={handleEdit}>
        <Text style={styles.title}>{note.title}</Text>
        <Text style={styles.date}>
          Last updated: {formatDate(note.updatedAt)}
        </Text>
        <Text style={styles.noteContent} numberOfLines={3}>
          {note.content}
        </Text>
      </TouchableOpacity>

      <View style={styles.buttonContainer}>
        <TouchableOpacity style={styles.editButton} onPress={handleEdit}>
          <Text style={styles.editText}>Edit</Text>
        </TouchableOpacity>

        <TouchableOpacity
          style={styles.deleteButton}
          onPress={handleDelete}
          disabled={deleting}
        >
          <Text style={styles.deleteText}>Delete</Text>
        </TouchableOpacity>
      </View>

      <EditNoteModal
        visible={editModalVisible}
        onClose={() => setEditModalVisible(false)}
        onNoteUpdated={handleNoteUpdated}
        note={note}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    backgroundColor: "white",
    borderRadius: 8,
    padding: 16,
    marginBottom: 12,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.2,
    shadowRadius: 2,
    elevation: 2,
  },
  content: {
    flex: 1,
  },
  title: {
    fontSize: 18,
    fontWeight: "bold",
    marginBottom: 4,
  },
  date: {
    fontSize: 12,
    color: "#666",
    marginBottom: 8,
  },
  noteContent: {
    fontSize: 14,
    color: "#333",
  },
  buttonContainer: {
    flexDirection: "row",
    justifyContent: "flex-end",
    marginTop: 12,
    borderTopWidth: 1,
    borderTopColor: "#eee",
    paddingTop: 8,
  },
  editButton: {
    marginRight: 16,
  },
  editText: {
    color: "#2196F3",
    fontWeight: "500",
  },
  deleteButton: {},
  deleteText: {
    color: "red",
    fontWeight: "500",
  },
});

export default NoteItem;

Update the Notes Screen to handle note updates:

// src/screens/NotesScreen.js (updated for edit)
// ... previous imports ...

const NotesScreen = () => {
  // ... previous state variables ...

  // Handle note deletion by removing it from state
  const handleNoteDeleted = (noteId) => {
    setNotes((currentNotes) =>
      currentNotes.filter((note) => note.$id !== noteId)
    );
  };

  // Handle note update by updating it in state
  const handleNoteUpdated = (updatedNote) => {
    setNotes((currentNotes) =>
      currentNotes.map((note) =>
        note.$id === updatedNote.$id ? updatedNote : note
      )
    );
  };

  // ... previous code ...

  // Render the list of notes
  return (
    <View style={styles.container}>
      {/* ... header code ... */}

      <FlatList
        data={notes}
        keyExtractor={(item) => item.$id}
        renderItem={({ item }) => (
          <NoteItem
            note={item}
            onNoteDeleted={handleNoteDeleted}
            onNoteUpdated={handleNoteUpdated}
          />
        )}
        contentContainerStyle={styles.listContent}
        refreshing={loading}
        onRefresh={fetchNotes}
      />

      {/* ... AddNoteModal code ... */}
    </View>
  );
};

// ... styles ...

export default NotesScreen;

Flutter Update Note

Create an EditNoteModal widget:

// lib/widgets/edit_note_modal.dart
import 'package:flutter/material.dart';
import 'package:appwrite/models.dart';
import '../services/note_service.dart';

class EditNoteModal extends StatefulWidget {
  final Document note;
  final Function(Document) onNoteUpdated;

  const EditNoteModal({
    Key? key,
    required this.note,
    required this.onNoteUpdated,
  }) : super(key: key);

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

class _EditNoteModalState extends State<EditNoteModal> {
  late TextEditingController _titleController;
  late TextEditingController _contentController;
  final _noteService = NoteService();
  bool _isLoading = false;
  String? _error;

  @override
  void initState() {
    super.initState();
    // Initialize text controllers with note data
    _titleController = TextEditingController(text: widget.note.data['title']);
    _contentController = TextEditingController(text: widget.note.data['content']);
  }

  @override
  void dispose() {
    _titleController.dispose();
    _contentController.dispose();
    super.dispose();
  }

  // Reset form error state
  void _resetForm() {
    setState(() {
      _error = null;
    });
  }

  // Save the updated note
  Future<void> _handleSave() async {
    // Basic form validation
    final title = _titleController.text.trim();
    final content = _contentController.text.trim();

    if (title.isEmpty || content.isEmpty) {
      setState(() {
        _error = 'Please fill in both title and content';
      });
      return;
    }

    try {
      setState(() {
        _isLoading = true;
        _error = null;
      });

      // Prepare update data
      final updateData = {
        'title': title,
        'content': content,
      };

      // Call update note service
      final updatedNote = await _noteService.updateNote(widget.note.$id, updateData);

      // Reset form
      _resetForm();

      // Notify parent component and close modal
      widget.onNoteUpdated(updatedNote);
      Navigator.pop(context);

    } catch (e) {
      print('Error updating note: $e');
      setState(() {
        _error = 'Failed to update note. Please try again.';
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Dialog(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
      ),
      elevation: 0,
      backgroundColor: Colors.transparent,
      child: contentBox(context),
    );
  }

  Widget contentBox(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        shape: BoxShape.rectangle,
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Text(
            'Edit Note',
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 16),

          // Show error message if there is one
          if (_error != null)
            Padding(
              padding: const EdgeInsets.only(bottom: 10),
              child: Text(
                _error!,
                style: const TextStyle(color: Colors.red),
                textAlign: TextAlign.center,
              ),
            ),

          // Title input field
          TextField(
            controller: _titleController,
            decoration: const InputDecoration(
              hintText: 'Title',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 15),

          // Content input field
          TextField(
            controller: _contentController,
            decoration: const InputDecoration(
              hintText: 'Content',
              border: OutlineInputBorder(),
            ),
            maxLines: 5,
          ),
          const SizedBox(height: 20),

          // Buttons row
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              // Cancel button
              TextButton(
                onPressed: _isLoading ? null : () => Navigator.pop(context),
                child: const Text('Cancel'),
              ),

              // Save button
              ElevatedButton(
                onPressed: _isLoading ? null : _handleSave,
                child: Text(_isLoading ? 'Saving...' : 'Save Changes'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

Update the NoteItem widget for edit functionality:

// lib/widgets/note_item.dart (updated for edit)
import 'package:flutter/material.dart';
import 'package:appwrite/models.dart';
import '../services/note_service.dart';

React Native vs Flutter Comparison

Feature React Native Flutter
Language JavaScript/TypeScript Dart
UI Rendering Uses native components via bridge Custom rendering engine (Skia)
Performance Good, but bridge can cause overhead Excellent, close to native
Development Speed Fast with hot reload Fast with hot reload
Component Library Mix of native and custom components Material Design and Cupertino widgets
Learning Curve Moderate (familiar for web devs) Moderate (Dart is similar to many languages)
Community Support Very large, mature ecosystem Growing rapidly, strong Google support
App Size Smaller Larger due to embedded runtime
Code Reuse ~90% between platforms ~90% between platforms
State Management Many options (Redux, Context, MobX) Many options (Provider, Riverpod, Bloc)
Integration with Native Through native modules Through platform channels
Build Process Expo or manual native builds Flutter CLI
Hot Reload Yes Yes
Development Tools Various Flutter DevTools
Testing Framework Jest, React Testing Library Flutter Test
Backend Integration Any REST/GraphQL API Any REST/GraphQL API
Navigation React Navigation, others Navigator 2.0, GoRouter
Animation Animated API, Reanimated Flutter Animation framework

Whether you choose React Native or Flutter for your mobile development needs, both frameworks provide excellent tools for building cross-platform applications.

The side-by-side approach shows how similar concepts are implemented differently in each framework, which should help you transfer knowledge between them and make informed decisions about which one to use for your projects.


By Wahid Hamdi