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

This is a comprehensive 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
  2. Authentication
  3. Deployment and Publishing

Let’s begin with a detailed presentation of both technologies.

1. Introduction to React Native and Flutter

React Native Overview

React Native is a JavaScript framework created by Facebook (now Meta) that allows developers to build native mobile applications using React. It enables writing code once and deploying to both iOS and Android platforms.

Key Characteristics of React Native:

  • Uses JavaScript/TypeScript as the primary language
  • Leverages the React component model
  • Provides a bridge to native components
  • Has a large ecosystem with many third-party libraries
  • Uses npm/yarn for package management
  • Hot reloading for faster development cycles
  • Community-driven with corporate backing from Meta

Architecture: React Native translates your JavaScript code into native components through a “bridge” that communicates with the native platform. Your UI is rendered using actual native components, not WebViews, giving your app a truly native feel.

Flutter Overview

Flutter is a UI toolkit created by Google that allows developers to build natively compiled applications for mobile, web, and desktop from a single codebase.

Key Characteristics of Flutter:

  • Uses Dart as the primary language
  • Everything is a widget (similar to components in React)
  • Provides its own rendering engine (Skia)
  • Has a growing ecosystem with many packages
  • Uses pub for package management
  • Hot reload for faster development cycles
  • Backed by Google with growing community support

Architecture: Flutter doesn’t rely on native components but instead uses its own rendering engine to draw the UI elements. This gives Flutter precise control over every pixel on the screen and ensures consistent rendering across platforms.

Comparison Between React Native and Flutter

Aspect React Native Flutter
Language JavaScript/TypeScript Dart
UI Components Bridge to native components Custom rendering engine
Performance Good, with some overhead due to bridge Excellent, with direct compilation
Development Experience Hot reloading Hot reload
Learning Curve Easy if familiar with React Moderate, new language to learn
Community Support Extensive, mature ecosystem Growing rapidly
Corporate Backing Meta (Facebook) Google

2. Development Environment Setup

Setting Up React Native with Expo

Expo is a framework that simplifies React Native development by providing a set of tools and services around the React Native framework.

Steps:

  1. Install Node.js and npm: Download and install from https://nodejs.org/

  2. Install Expo CLI:

    npm install -g expo-cli
  3. Create a new project:

    expo init NotesApp

    Select a “blank” template when prompted.

  4. Navigate to the project directory:

    cd NotesApp
  5. Start the development server:

    expo start

Setting Up Flutter

Steps:

  1. Download Flutter SDK: Visit https://flutter.dev/docs/get-started/install and follow the instructions for your operating system.

  2. Add Flutter to your path: Follow the OS-specific instructions from the Flutter website.

  3. Run flutter doctor:

    flutter doctor

    Address any issues identified by the command.

  4. Create a new project:

    flutter create notes_app
  5. Navigate to the project directory:

    cd notes_app
  6. Start the development server:

    flutter run

    (Make sure you have an emulator running or a device connected)

3. Project Structure and Fundamentals

React Native Project Structure

NotesApp/
β”œβ”€β”€ App.js           // Main application component
β”œβ”€β”€ app.json         // Expo configuration
β”œβ”€β”€ assets/          // Static assets like images
β”œβ”€β”€ node_modules/    // Dependencies
β”œβ”€β”€ package.json     // Project metadata and dependencies
└── babel.config.js  // Babel configuration

Let’s reset the boilerplate code:

App.js (React Native):

import React from "react";
import { View, Text, StyleSheet } from "react-native";

export default function App() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Hello, React Native!</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
  text: {
    fontSize: 24,
    fontWeight: "bold",
  },
});

Explanation:

  • We import core components from React Native: View (similar to div in web) and Text
  • StyleSheet.create is used to define styles (similar to CSS but with some differences)
  • The component returns a view with a text element

Flutter Project Structure

notes_app/
β”œβ”€β”€ lib/            // Source code
β”‚   └── main.dart   // Main entry point
β”œβ”€β”€ pubspec.yaml    // Project configuration and dependencies
β”œβ”€β”€ android/        // Android-specific code
β”œβ”€β”€ ios/           // iOS-specific code
└── test/          // Test files

Let’s reset the boilerplate code:

main.dart (Flutter):

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Text(
            'Hello, Flutter!',
            style: TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ),
    );
  }
}

Explanation:

  • We import material.dart which provides Material Design widgets
  • main() is the entry point that runs our app
  • MyApp is a stateless widget that builds our UI
  • MaterialApp is the root widget that provides material design look and feel
  • Scaffold provides the basic material design visual layout structure
  • Center centers its child widget
  • Text displays text with styling similar to React Native’s Text component

4. UI Components and Styling

React Native Components and Styling

React Native provides a set of core components that map to native UI elements:

  • View: A container (like div in web)
  • Text: For displaying text
  • Image: For displaying images
  • TextInput: For text input
  • ScrollView: For scrollable content
  • FlatList: For efficient rendering of lists
  • TouchableOpacity: For creating touchable elements

Styling Example:

import React from "react";
import { View, Text, StyleSheet } from "react-native";

export default function StyleDemo() {
  return (
    <View style={styles.container}>
      <View style={styles.box}>
        <Text style={styles.text}>Styled Component</Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "#f5f5f5",
  },
  box: {
    width: 200,
    height: 200,
    backgroundColor: "#3498db",
    borderRadius: 10,
    alignItems: "center",
    justifyContent: "center",
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 5,
  },
  text: {
    color: "white",
    fontSize: 18,
    fontWeight: "bold",
  },
});

Explanation:

  • Styles in React Native are similar to CSS but use camelCase (e.g., backgroundColor instead of background-color)
  • flex: 1 makes the container take up all available space
  • alignItems and justifyContent control positioning of children
  • Shadow properties add depth to components (note: elevation is Android-specific)

Flutter Widgets and Styling

Flutter uses widgets for everything in the UI:

  • Container: Similar to View in React Native
  • Text: For displaying text
  • Image: For displaying images
  • TextField: For text input
  • SingleChildScrollView: Similar to ScrollView
  • ListView: Similar to FlatList
  • GestureDetector: For detecting gestures

Styling Example:

import 'package:flutter/material.dart';

class StyleDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          width: 200,
          height: 200,
          decoration: BoxDecoration(
            color: Colors.blue,
            borderRadius: BorderRadius.circular(10),
            boxShadow: [
              BoxShadow(
                color: Colors.black.withOpacity(0.25),
                spreadRadius: 2,
                blurRadius: 3.84,
                offset: Offset(0, 2),
              ),
            ],
          ),
          child: Center(
            child: Text(
              'Styled Component',
              style: TextStyle(
                color: Colors.white,
                fontSize: 18,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Explanation:

  • Container serves as a box model similar to View in React Native
  • Styling is applied directly to widgets or through the decoration property
  • BoxDecoration provides styling options like background color, border radius, and shadows
  • Flutter uses a nested widget approach rather than a separate StyleSheet

Layout in React Native and Flutter

React Native Layout

React Native primarily uses Flexbox for layout:

import React from "react";
import { View, Text, StyleSheet } from "react-native";

export default function LayoutDemo() {
  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerText}>Header</Text>
      </View>
      <View style={styles.content}>
        <Text style={styles.contentText}>Content</Text>
      </View>
      <View style={styles.footer}>
        <Text style={styles.footerText}>Footer</Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: "column",
  },
  header: {
    height: 80,
    backgroundColor: "#3498db",
    justifyContent: "center",
    alignItems: "center",
    paddingTop: 30,
  },
  headerText: {
    color: "white",
    fontSize: 20,
    fontWeight: "bold",
  },
  content: {
    flex: 1,
    backgroundColor: "#ecf0f1",
    justifyContent: "center",
    alignItems: "center",
  },
  contentText: {
    fontSize: 18,
  },
  footer: {
    height: 60,
    backgroundColor: "#2c3e50",
    justifyContent: "center",
    alignItems: "center",
  },
  footerText: {
    color: "white",
    fontSize: 16,
  },
});

Explanation:

  • flex: 1 makes the container take all available space
  • flexDirection: 'column' stacks children vertically
  • The header and footer have fixed heights
  • The content area grows to fill the remaining space with flex: 1

Flutter Layout

Flutter offers several layout widgets:

import 'package:flutter/material.dart';

class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Container(
            height: 80,
            color: Colors.blue,
            child: Center(
              child: Padding(
                padding: EdgeInsets.only(top: 30),
                child: Text(
                  'Header',
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            ),
          ),
          Expanded(
            child: Container(
              color: Color(0xFFECF0F1),
              child: Center(
                child: Text(
                  'Content',
                  style: TextStyle(fontSize: 18),
                ),
              ),
            ),
          ),
          Container(
            height: 60,
            color: Color(0xFF2C3E50),
            child: Center(
              child: Text(
                'Footer',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 16,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Explanation:

  • Column arranges children vertically (similar to flexDirection: 'column')
  • Expanded makes its child take all available space (similar to flex: 1)
  • Container widgets define the header, content, and footer sections
  • EdgeInsets provides padding similar to React Native

5. Building the Notes App

Now let’s start building our Notes app with both technologies. We’ll begin with the home screen:

Setting up Navigation

React Native Navigation with React Navigation

First, let’s create a directory structure:

NotesApp/
β”œβ”€β”€ App.js
β”œβ”€β”€ screens/
β”‚   β”œβ”€β”€ HomeScreen.js
β”‚   └── NotesScreen.js
└── components/
    └── NoteItem.js

Then, install the necessary packages:

npm install @react-navigation/native @react-navigation/stack
npm install react-native-screens react-native-safe-area-context
npm install react-native-gesture-handler

Create a navigation setup:

// App.js
import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import HomeScreen from "./screens/HomeScreen";
import NotesScreen from "./screens/NotesScreen";

const Stack = createStackNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen
          name="Home"
          component={HomeScreen}
          options={{
            title: "Notes App",
            headerStyle: {
              backgroundColor: "#f4511e",
            },
            headerTintColor: "#fff",
          }}
        />
        <Stack.Screen
          name="Notes"
          component={NotesScreen}
          options={{
            title: "My Notes",
          }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Flutter Navigation

Let’s create a similar directory structure:

notes_app/
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ main.dart
β”‚   β”œβ”€β”€ screens/
β”‚   β”‚   β”œβ”€β”€ home_screen.dart
β”‚   β”‚   └── notes_screen.dart
β”‚   └── components/
β”‚       └── note_item.dart

Flutter has built-in navigation capabilities:

// lib/main.dart
import 'package:flutter/material.dart';
import 'screens/home_screen.dart';
import 'screens/notes_screen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Notes App',
      theme: ThemeData(
        primarySwatch: Colors.deepOrange,
      ),
      initialRoute: '/',
      routes: {
        '/': (context) => const HomeScreen(),
        '/notes': (context) => const NotesScreen(),
      },
    );
  }
}

// lib/screens/home_screen.dart
import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Notes App'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pushNamed(context, '/notes');
          },
          child: const Text('Go to Notes'),
        ),
      ),
    );
  }
}

React Native Home Screen

HomeScreen.js:

import React from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";

export default function HomeScreen({ navigation }) {
  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerTitle}>Notes App</Text>
      </View>

      <View style={styles.content}>
        <Text style={styles.welcomeText}>Welcome to Notes App</Text>
        <Text style={styles.instructionText}>
          Keep your ideas, lists, and reminders in one place
        </Text>

        <TouchableOpacity
          style={styles.notesButton}
          onPress={() => navigation.navigate("Notes")}
        >
          <Text style={styles.buttonText}>Go to Notes</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#f5f5f5",
  },
  header: {
    height: 100,
    backgroundColor: "#3498db",
    justifyContent: "flex-end",
    paddingBottom: 15,
    paddingHorizontal: 20,
  },
  headerTitle: {
    color: "white",
    fontSize: 24,
    fontWeight: "bold",
  },
  content: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    padding: 20,
  },
  welcomeText: {
    fontSize: 28,
    fontWeight: "bold",
    marginBottom: 10,
    textAlign: "center",
  },
  instructionText: {
    fontSize: 18,
    color: "#7f8c8d",
    textAlign: "center",
    marginBottom: 40,
  },
  notesButton: {
    backgroundColor: "#3498db",
    paddingVertical: 12,
    paddingHorizontal: 30,
    borderRadius: 8,
  },
  buttonText: {
    color: "white",
    fontSize: 18,
    fontWeight: "bold",
  },
});

Explanation:

  • This screen displays a welcome message and a button to navigate to the Notes screen
  • header has a fixed height with padding at the bottom for the title
  • content uses flex to center its children
  • TouchableOpacity provides a button with a tap effect
  • The onPress handler uses navigation to move to the Notes screen

Flutter Home Screen

home_screen.dart:

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Container(
            height: 100,
            color: Colors.blue,
            padding: EdgeInsets.only(
              bottom: 15,
              left: 20,
              right: 20,
            ),
            alignment: Alignment.bottomLeft,
            child: Text(
              'Notes App',
              style: TextStyle(
                color: Colors.white,
                fontSize: 24,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
          Expanded(
            child: Padding(
              padding: EdgeInsets.all(20),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    'Welcome to Notes App',
                    style: TextStyle(
                      fontSize: 28,
                      fontWeight: FontWeight.bold,
                    ),
                    textAlign: TextAlign.center,
                  ),
                  SizedBox(height: 10),
                  Text(
                    'Keep your ideas, lists, and reminders in one place',
                    style: TextStyle(
                      fontSize: 18,
                      color: Color(0xFF7F8C8D),
                    ),
                    textAlign: TextAlign.center,
                  ),
                  SizedBox(height: 40),
                  ElevatedButton(
                    onPressed: () {
                      Navigator.pushNamed(context, '/notes');
                    },
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.blue,
                      padding: EdgeInsets.symmetric(
                        vertical: 12,
                        horizontal: 30,
                      ),
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(8),
                      ),
                    ),
                    child: Text(
                      'Go to Notes',
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Explanation:

  • The overall structure is similar to the React Native version
  • Container is used for the header with alignment to position text at the bottom
  • Expanded with Column centers the content
  • SizedBox is used for spacing between elements (similar to margins in React Native)
  • ElevatedButton provides a material design button
  • Navigator.pushNamed is used for navigation instead of the navigation prop

React Native Notes Screen

NotesScreen.js:

import React, { useState } from "react";
import {
  View,
  Text,
  StyleSheet,
  FlatList,
  TouchableOpacity,
  Modal,
  TextInput,
} from "react-native";

// Sample initial notes data
const initialNotes = [
  {
    id: "1",
    content: "Learn React Native",
    createdAt: new Date().toISOString(),
  },
  {
    id: "2",
    content: "Complete the tutorial",
    createdAt: new Date().toISOString(),
  },
];

export default function NotesScreen() {
  const [notes, setNotes] = useState(initialNotes);
  const [modalVisible, setModalVisible] = useState(false);
  const [noteText, setNoteText] = useState("");
  const [editingNote, setEditingNote] = useState(null);

  // Function to add a new note
  const addNote = () => {
    if (noteText.trim() === "") return;

    if (editingNote) {
      // Update existing note
      setNotes(
        notes.map((note) =>
          note.id === editingNote.id
            ? {
                ...note,
                content: noteText,
                updatedAt: new Date().toISOString(),
              }
            : note
        )
      );
      setEditingNote(null);
    } else {
      // Add new note
      const newNote = {
        id: Date.now().toString(),
        content: noteText,
        createdAt: new Date().toISOString(),
      };
      setNotes([newNote, ...notes]);
    }

    setNoteText("");
    setModalVisible(false);
  };

  // Function to delete a note
  const deleteNote = (id) => {
    setNotes(notes.filter((note) => note.id !== id));
  };

  // Function to open edit mode
  const editNote = (note) => {
    setEditingNote(note);
    setNoteText(note.content);
    setModalVisible(true);
  };

  // Note item component
  const renderNote = ({ item }) => (
    <View style={styles.noteItem}>
      <Text style={styles.noteContent}>{item.content}</Text>
      <View style={styles.noteActions}>
        <TouchableOpacity onPress={() => editNote(item)}>
          <Text style={styles.editButton}>Edit</Text>
        </TouchableOpacity>
        <TouchableOpacity onPress={() => deleteNote(item.id)}>
          <Text style={styles.deleteButton}>Delete</Text>
        </TouchableOpacity>
      </View>
    </View>
  );

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerTitle}>My Notes</Text>
        <TouchableOpacity
          style={styles.addButton}
          onPress={() => {
            setEditingNote(null);
            setNoteText("");
            setModalVisible(true);
          }}
        >
          <Text style={styles.addButtonText}>+</Text>
        </TouchableOpacity>
      </View>

      {notes.length > 0 ? (
        <FlatList
          data={notes}
          renderItem={renderNote}
          keyExtractor={(item) => item.id}
          contentContainerStyle={styles.notesList}
        />
      ) : (
        <View style={styles.emptyContainer}>
          <Text style={styles.emptyText}>No notes yet. Create one!</Text>
        </View>
      )}

      {/* Modal for adding/editing notes */}
      <Modal
        animationType="slide"
        transparent={true}
        visible={modalVisible}
        onRequestClose={() => setModalVisible(false)}
      >
        <View style={styles.modalContainer}>
          <View style={styles.modalContent}>
            <Text style={styles.modalTitle}>
              {editingNote ? "Edit Note" : "Add New Note"}
            </Text>

            <TextInput
              style={styles.textInput}
              multiline
              placeholder="Enter your note here..."
              value={noteText}
              onChangeText={setNoteText}
              autoFocus
            />

            <View style={styles.modalButtons}>
              <TouchableOpacity
                style={[styles.modalButton, styles.cancelButton]}
                onPress={() => setModalVisible(false)}
              >
                <Text style={styles.buttonText}>Cancel</Text>
              </TouchableOpacity>

              <TouchableOpacity
                style={[styles.modalButton, styles.saveButton]}
                onPress={addNote}
              >
                <Text style={styles.buttonText}>Save</Text>
              </TouchableOpacity>
            </View>
          </View>
        </View>
      </Modal>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#f5f5f5",
  },
  header: {
    height: 100,
    backgroundColor: "#3498db",
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "flex-end",
    paddingBottom: 15,
    paddingHorizontal: 20,
  },
  headerTitle: {
    color: "white",
    fontSize: 24,
    fontWeight: "bold",
  },
  addButton: {
    width: 36,
    height: 36,
    borderRadius: 18,
    backgroundColor: "white",
    justifyContent: "center",
    alignItems: "center",
  },
  addButtonText: {
    fontSize: 24,
    color: "#3498db",
    fontWeight: "bold",
    marginTop: -2,
  },
  notesList: {
    padding: 15,
  },
  noteItem: {
    backgroundColor: "white",
    borderRadius: 8,
    padding: 15,
    marginBottom: 12,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.2,
    shadowRadius: 2,
    elevation: 2,
  },
  noteContent: {
    fontSize: 16,
    marginBottom: 10,
  },
  noteActions: {
    flexDirection: "row",
    justifyContent: "flex-end",
  },
  editButton: {
    color: "#3498db",
    marginRight: 15,
  },
  deleteButton: {
    color: "#e74c3c",
  },
  emptyContainer: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  emptyText: {
    fontSize: 18,
    color: "#7f8c8d",
  },
  modalContainer: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "rgba(0, 0, 0, 0.5)",
  },
  modalContent: {
    width: "80%",
    backgroundColor: "white",
    borderRadius: 12,
    padding: 20,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 5,
  },
  modalTitle: {
    fontSize: 20,
    fontWeight: "bold",
    marginBottom: 15,
  },
  textInput: {
    height: 150,
    borderWidth: 1,
    borderColor: "#ddd",
    borderRadius: 6,
    padding: 10,
    marginBottom: 20,
    textAlignVertical: "top",
  },
  modalButtons: {
    flexDirection: "row",
    justifyContent: "flex-end",
  },
  modalButton: {
    paddingVertical: 8,
    paddingHorizontal: 16,
    borderRadius: 6,
    marginLeft: 10,
  },
  cancelButton: {
    backgroundColor: "#95a5a6",
  },
  saveButton: {
    backgroundColor: "#3498db",
  },
  buttonText: {
    color: "white",
    fontWeight: "bold",
  },
});

Explanation:

  • We use useState to manage the list of notes, modal visibility, input text, and the note being edited
  • FlatList efficiently renders the list of notes
  • The renderNote function defines how each note is displayed
  • We implement functions for adding, editing, and deleting notes
  • A modal is used for the note creation/editing interface
  • Conditional rendering shows either the list of notes or an empty state message

Flutter Notes Screen

notes_screen.dart:

import 'package:flutter/material.dart';

class NotesScreen extends StatefulWidget {
  @override
  _NotesScreenState createState() => _NotesScreenState();
}

class _NotesScreenState extends State<NotesScreen> {
  // Sample initial notes data
  List<Map<String, dynamic>> notes = [
    {
      'id': '1',
      'content': 'Learn Flutter',
      'createdAt': DateTime.now().toIso8601String(),
    },
    {
      'id': '2',
      'content': 'Complete the tutorial',
      'createdAt': DateTime.now().toIso8601String(),
    },
  ];

  // Controllers and state variables
  TextEditingController noteController = TextEditingController();
  Map<String, dynamic>? editingNote;

  // Function to add a new note
  void addNote() {
    if (noteController.text.trim().isEmpty) return;

    setState(() {
      if (editingNote != null) {
        // Update existing note
        for (int i = 0; i < notes.length; i++) {
          if (notes[i]['id'] == editingNote!['id']) {
            notes[i] = {
              ...notes[i],
              'content': noteController.text,
              'updatedAt': DateTime.now().toIso8601String(),
            };
            break;
          }
        }
        editingNote = null;
      } else {
        // Add new note
        final newNote = {
          'id': DateTime.now().millisecondsSinceEpoch.toString(),
          'content': noteController.text,
          'createdAt': DateTime.now().toIso8601String(),
        };
        notes.insert(0, newNote);
      }
    });

    noteController.clear();
    Navigator.pop(context); // Close the dialog
  }

  // Function to delete a note
  void deleteNote(String id) {
    setState(() {
      notes.removeWhere((note) => note['id'] == id);
    });
  }

  // Function to open edit mode
  void editNote(Map<String, dynamic> note) {
    editingNote = note;
    noteController.text = note['content'];
    showNoteDialog();
  }

  // Show dialog for adding/editing notes
  void showNoteDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(editingNote != null ? 'Edit Note' : 'Add New Note'),
        content: TextField(
          controller: noteController,
          decoration: InputDecoration(
            hintText: 'Enter your note here...',
            border: OutlineInputBorder(),
          ),
          maxLines: 5,
          autofocus: true,
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('Cancel'),
            style: TextButton.styleFrom(foregroundColor: Colors.grey),
          ),
          TextButton(
            onPressed: addNote,
            child: Text('Save'),
            style: TextButton.styleFrom(foregroundColor: Colors.blue),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          // Header with title and add button
          Container(
            height: 100,
            color: Colors.blue,
            padding: EdgeInsets.only(
              bottom: 15,
              left: 20,
              right: 20,
            ),
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.end,
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  'My Notes',
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 24,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                InkWell(
                  onTap: () {
                    editingNote = null;
                    noteController.clear();
                    showNoteDialog();
                  },
                  child: Container(
                    width: 36,
                    height: 36,
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(18),
                    ),
                    child: Center(
                      child: Text(
                        '+',
                        style: TextStyle(
                          fontSize: 24,
                          color: Colors.blue,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),

          // Notes list or empty state
          Expanded(
            child: notes.isNotEmpty
                ? ListView.builder(
                    padding: EdgeInsets.all(15),
                    itemCount: notes.length,
                    itemBuilder: (context, index) {
                      final note = notes[index];
                      return Container(
                        margin: EdgeInsets.only(bottom: 12),
                        decoration: BoxDecoration(
                          color: Colors.white,
                          borderRadius: BorderRadius.circular(8),
                          boxShadow: [
                            BoxShadow(
                              color: Colors.black.withOpacity(0.1),
                              blurRadius: 4,
                              offset: Offset(0, 2),
                            ),
                          ],
                        ),
                        child: Padding(
                          padding: EdgeInsets.all(15),
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Text(
                                note['content'],
                                style: TextStyle(fontSize: 16),
                              ),
                              SizedBox(height: 10),
                              Row(
                                mainAxisAlignment: MainAxisAlignment.end,
                                children: [
                                  TextButton(
                                    onPressed: () => editNote(note),
                                    child: Text(
                                      'Edit',
                                      style: TextStyle(color: Colors.blue),
                                    ),
                                  ),
                                  TextButton(
                                    onPressed: () =>
                                        deleteNote(note['id']),
                                    child: Text(
                                      'Delete',
                                      style: TextStyle(color: Colors.red),
                                    ),
                                  ),
                                ],
                              ),
                            ],
                          ),
                        ),
                      );
                    },
                  )
                : Center(
                    child: Text(
                      'No notes yet. Create one!',
                      style: TextStyle(
                        fontSize: 18,
                        color: Color(0xFF7F8C8D),
                      ),
                    ),
                  ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    // Clean up the controller when the widget is disposed
    noteController.dispose();
    super.dispose();
  }
}

Explanation:

  • Flutter’s StatefulWidget is used because we need to maintain state
  • setState() is called to update the UI when data changes (similar to React’s state updates)
  • ListView.builder is used for efficient list rendering (equivalent to React Native’s FlatList)
  • AlertDialog provides a modal dialog for adding/editing notes instead of a full-screen Modal
  • TextEditingController manages the input text state
  • InkWell provides a ripple effect when tapped (similar to TouchableOpacity)
  • We implement similar CRUD operations (Create, Read, Update, Delete) as in the React Native version
  • The UI structure follows Material Design conventions but achieves a similar layout
  • dispose() is called to clean up resources when the widget is removed

Component Refactoring

Now, let’s refactor our code by extracting reusable components. This is a good practice in both React Native and Flutter development.

React Native Component Refactoring

First, let’s create a NoteItem component for React Native:

import React from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";

export default function NoteItem({ note, onEdit, onDelete }) {
  return (
    <View style={styles.noteItem}>
      <Text style={styles.noteContent}>{note.content}</Text>
      <View style={styles.noteActions}>
        <TouchableOpacity onPress={() => onEdit(note)}>
          <Text style={styles.editButton}>Edit</Text>
        </TouchableOpacity>
        <TouchableOpacity onPress={() => onDelete(note.id)}>
          <Text style={styles.deleteButton}>Delete</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  noteItem: {
    backgroundColor: "white",
    borderRadius: 8,
    padding: 15,
    marginBottom: 12,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.2,
    shadowRadius: 2,
    elevation: 2,
  },
  noteContent: {
    fontSize: 16,
    marginBottom: 10,
  },
  noteActions: {
    flexDirection: "row",
    justifyContent: "flex-end",
  },
  editButton: {
    color: "#3498db",
    marginRight: 15,
  },
  deleteButton: {
    color: "#e74c3c",
  },
});

Now, let’s create a NoteInput component for the modal:

import React from "react";
import {
  View,
  Text,
  StyleSheet,
  Modal,
  TextInput,
  TouchableOpacity,
} from "react-native";

export default function NoteInput({
  visible,
  onClose,
  onSave,
  noteText,
  setNoteText,
  isEditing,
}) {
  return (
    <Modal
      animationType="slide"
      transparent={true}
      visible={visible}
      onRequestClose={onClose}
    >
      <View style={styles.modalContainer}>
        <View style={styles.modalContent}>
          <Text style={styles.modalTitle}>
            {isEditing ? "Edit Note" : "Add New Note"}
          </Text>

          <TextInput
            style={styles.textInput}
            multiline
            placeholder="Enter your note here..."
            value={noteText}
            onChangeText={setNoteText}
            autoFocus
          />

          <View style={styles.modalButtons}>
            <TouchableOpacity
              style={[styles.modalButton, styles.cancelButton]}
              onPress={onClose}
            >
              <Text style={styles.buttonText}>Cancel</Text>
            </TouchableOpacity>

            <TouchableOpacity
              style={[styles.modalButton, styles.saveButton]}
              onPress={onSave}
            >
              <Text style={styles.buttonText}>Save</Text>
            </TouchableOpacity>
          </View>
        </View>
      </View>
    </Modal>
  );
}

const styles = StyleSheet.create({
  modalContainer: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "rgba(0, 0, 0, 0.5)",
  },
  modalContent: {
    width: "80%",
    backgroundColor: "white",
    borderRadius: 12,
    padding: 20,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 5,
  },
  modalTitle: {
    fontSize: 20,
    fontWeight: "bold",
    marginBottom: 15,
  },
  textInput: {
    height: 150,
    borderWidth: 1,
    borderColor: "#ddd",
    borderRadius: 6,
    padding: 10,
    marginBottom: 20,
    textAlignVertical: "top",
  },
  modalButtons: {
    flexDirection: "row",
    justifyContent: "flex-end",
  },
  modalButton: {
    paddingVertical: 8,
    paddingHorizontal: 16,
    borderRadius: 6,
    marginLeft: 10,
  },
  cancelButton: {
    backgroundColor: "#95a5a6",
  },
  saveButton: {
    backgroundColor: "#3498db",
  },
  buttonText: {
    color: "white",
    fontWeight: "bold",
  },
});

Now, we need to update the NotesScreen to use these components:

import React, { useState } from "react";
import {
  View,
  Text,
  StyleSheet,
  FlatList,
  TouchableOpacity,
} from "react-native";
import NoteItem from "../components/NoteItem";
import NoteInput from "../components/NoteInput";

// Sample initial notes data
const initialNotes = [
  {
    id: "1",
    content: "Learn React Native",
    createdAt: new Date().toISOString(),
  },
  {
    id: "2",
    content: "Complete the tutorial",
    createdAt: new Date().toISOString(),
  },
];

export default function NotesScreen() {
  const [notes, setNotes] = useState(initialNotes);
  const [modalVisible, setModalVisible] = useState(false);
  const [noteText, setNoteText] = useState("");
  const [editingNote, setEditingNote] = useState(null);

  // Function to add or update a note
  const saveNote = () => {
    if (noteText.trim() === "") return;

    if (editingNote) {
      // Update existing note
      setNotes(
        notes.map((note) =>
          note.id === editingNote.id
            ? {
                ...note,
                content: noteText,
                updatedAt: new Date().toISOString(),
              }
            : note
        )
      );
      setEditingNote(null);
    } else {
      // Add new note
      const newNote = {
        id: Date.now().toString(),
        content: noteText,
        createdAt: new Date().toISOString(),
      };
      setNotes([newNote, ...notes]);
    }

    setNoteText("");
    setModalVisible(false);
  };

  // Function to delete a note
  const deleteNote = (id) => {
    setNotes(notes.filter((note) => note.id !== id));
  };

  // Function to open edit mode
  const editNote = (note) => {
    setEditingNote(note);
    setNoteText(note.content);
    setModalVisible(true);
  };

  // Function to close the modal
  const closeModal = () => {
    setModalVisible(false);
    setNoteText("");
    setEditingNote(null);
  };

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerTitle}>My Notes</Text>
        <TouchableOpacity
          style={styles.addButton}
          onPress={() => setModalVisible(true)}
        >
          <Text style={styles.addButtonText}>+</Text>
        </TouchableOpacity>
      </View>

      {notes.length > 0 ? (
        <FlatList
          data={notes}
          renderItem={({ item }) => (
            <NoteItem note={item} onEdit={editNote} onDelete={deleteNote} />
          )}
          keyExtractor={(item) => item.id}
          contentContainerStyle={styles.notesList}
        />
      ) : (
        <View style={styles.emptyContainer}>
          <Text style={styles.emptyText}>No notes yet. Create one!</Text>
        </View>
      )}

      <NoteInput
        visible={modalVisible}
        onClose={closeModal}
        onSave={saveNote}
        noteText={noteText}
        setNoteText={setNoteText}
        isEditing={!!editingNote}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#f5f5f5",
  },
  header: {
    height: 100,
    backgroundColor: "#3498db",
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "flex-end",
    paddingBottom: 15,
    paddingHorizontal: 20,
  },
  headerTitle: {
    color: "white",
    fontSize: 24,
    fontWeight: "bold",
  },
  addButton: {
    width: 36,
    height: 36,
    borderRadius: 18,
    backgroundColor: "white",
    justifyContent: "center",
    alignItems: "center",
  },
  addButtonText: {
    fontSize: 24,
    color: "#3498db",
    fontWeight: "bold",
    marginTop: -2,
  },
  notesList: {
    padding: 15,
  },
  emptyContainer: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  emptyText: {
    fontSize: 18,
    color: "#7f8c8d",
  },
});

Explanation:

  • By extracting NoteItem and NoteInput as separate components, we’ve improved code organization
  • Each component now has a single responsibility, making the code more maintainable
  • We pass the necessary props to each component, including event handlers
  • The main NotesScreen component now focuses on state management and rendering the overall layout
  • The refactoring improves reusability - these components could be used in other parts of the app

Flutter Component Refactoring

Similarly, let’s refactor our Flutter code by creating separate widget classes:

import 'package:flutter/material.dart';

class NoteItem extends StatelessWidget {
  final Map<String, dynamic> note;
  final Function(Map<String, dynamic>) onEdit;
  final Function(String) onDelete;

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

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(bottom: 12),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(8),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 4,
            offset: Offset(0, 2),
          ),
        ],
      ),
      child: Padding(
        padding: EdgeInsets.all(15),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              note['content'],
              style: TextStyle(fontSize: 16),
            ),
            SizedBox(height: 10),
            Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                TextButton(
                  onPressed: () => onEdit(note),
                  child: Text(
                    'Edit',
                    style: TextStyle(color: Colors.blue),
                  ),
                ),
                TextButton(
                  onPressed: () => onDelete(note['id']),
                  child: Text(
                    'Delete',
                    style: TextStyle(color: Colors.red),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Now, let’s create a NoteInputDialog widget:

import 'package:flutter/material.dart';

class NoteInputDialog extends StatelessWidget {
  final TextEditingController controller;
  final bool isEditing;
  final Function() onSave;

  const NoteInputDialog({
    Key? key,
    required this.controller,
    required this.isEditing,
    required this.onSave,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text(isEditing ? 'Edit Note' : 'Add New Note'),
      content: TextField(
        controller: controller,
        decoration: InputDecoration(
          hintText: 'Enter your note here...',
          border: OutlineInputBorder(),
        ),
        maxLines: 5,
        autofocus: true,
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: Text('Cancel'),
          style: TextButton.styleFrom(foregroundColor: Colors.grey),
        ),
        TextButton(
          onPressed: () {
            onSave();
            Navigator.pop(context);
          },
          child: Text('Save'),
          style: TextButton.styleFrom(foregroundColor: Colors.blue),
        ),
      ],
    );
  }
}

Finally, let’s update the NotesScreen to use these components:

import 'package:flutter/material.dart';
import '../components/note_item.dart';
import '../components/note_input_dialog.dart';

class NotesScreen extends StatefulWidget {
  @override
  _NotesScreenState createState() => _NotesScreenState();
}

class _NotesScreenState extends State<NotesScreen> {
  // Sample initial notes data
  List<Map<String, dynamic>> notes = [
    {
      'id': '1',
      'content': 'Learn Flutter',
      'createdAt': DateTime.now().toIso8601String(),
    },
    {
      'id': '2',
      'content': 'Complete the tutorial',
      'createdAt': DateTime.now().toIso8601String(),
    },
  ];

  // Controllers and state variables
  TextEditingController noteController = TextEditingController();
  Map<String, dynamic>? editingNote;

  // Function to save a new or updated note
  void saveNote() {
    if (noteController.text.trim().isEmpty) return;

    setState(() {
      if (editingNote != null) {
        // Update existing note
        for (int i = 0; i < notes.length; i++) {
          if (notes[i]['id'] == editingNote!['id']) {
            notes[i] = {
              ...notes[i],
              'content': noteController.text,
              'updatedAt': DateTime.now().toIso8601String(),
            };
            break;
          }
        }
        editingNote = null;
      } else {
        // Add new note
        final newNote = {
          'id': DateTime.now().millisecondsSinceEpoch.toString(),
          'content': noteController.text,
          'createdAt': DateTime.now().toIso8601String(),
        };
        notes.insert(0, newNote);
      }
    });

    noteController.clear();
  }

  // Function to delete a note
  void deleteNote(String id) {
    setState(() {
      notes.removeWhere((note) => note['id'] == id);
    });
  }

  // Function to open edit mode
  void editNote(Map<String, dynamic> note) {
    editingNote = note;
    noteController.text = note['content'];
    showNoteDialog();
  }

  // Show dialog for adding/editing notes
  void showNoteDialog() {
    showDialog(
      context: context,
      builder: (context) => NoteInputDialog(
        controller: noteController,
        isEditing: editingNote != null,
        onSave: saveNote,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          // Header with title and add button
          Container(
            height: 100,
            color: Colors.blue,
            padding: EdgeInsets.only(
              bottom: 15,
              left: 20,
              right: 20,
            ),
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.end,
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  'My Notes',
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 24,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                InkWell(
                  onTap: () {
                    editingNote = null;
                    noteController.clear();
                    showNoteDialog();
                  },
                  child: Container(
                    width: 36,
                    height: 36,
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(18),
                    ),
                    child: Center(
                      child: Text(
                        '+',
                        style: TextStyle(
                          fontSize: 24,
                          color: Colors.blue,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),

          // Notes list or empty state
          Expanded(
            child: notes.isNotEmpty
                ? ListView.builder(
                    padding: EdgeInsets.all(15),
                    itemCount: notes.length,
                    itemBuilder: (context, index) {
                      return NoteItem(
                        note: notes[index],
                        onEdit: editNote,
                        onDelete: deleteNote,
                      );
                    },
                  )
                : Center(
                    child: Text(
                      'No notes yet. Create one!',
                      style: TextStyle(
                        fontSize: 18,
                        color: Color(0xFF7F8C8D),
                      ),
                    ),
                  ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    // Clean up the controller when the widget is disposed
    noteController.dispose();
    super.dispose();
  }
}

Explanation:

  • Similar to React Native, we’ve extracted the note item and input dialog into separate components
  • In Flutter, we create these as StatelessWidget classes to make them reusable
  • We pass required data and callbacks as parameters to these widgets
  • The NoteItem widget takes a note object and callbacks for edit and delete actions
  • The NoteInputDialog widget manages the input interface for adding or editing notes
  • The main NotesScreen focuses on state management and overall layout
  • This refactoring makes the code easier to maintain and understand

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