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
- Introduction to React Native and Flutter
- Development Environment Setup
- Project Structure and Fundamentals
- UI Components and Styling
- Building the Notes App
- Database Integration with Appwrite
- Authentication
- 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) |
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:
-
Install Node.js and npm: Download and install from https://nodejs.org/
-
Install Expo CLI:
npm install -g expo-cli
-
Create a new project:
expo init NotesApp
Select a “blank” template when prompted.
-
Navigate to the project directory:
cd NotesApp
-
Start the development server:
expo start
Setting Up Flutter
Steps:
-
Download Flutter SDK: Visit https://flutter.dev/docs/get-started/install and follow the instructions for your operating system.
-
Add Flutter to your path: Follow the OS-specific instructions from the Flutter website.
-
Run flutter doctor:
flutter doctor
Address any issues identified by the command.
-
Create a new project:
flutter create notes_app
-
Navigate to the project directory:
cd notes_app
-
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) andText
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 appMyApp
is a stateless widget that builds our UIMaterialApp
is the root widget that provides material design look and feelScaffold
provides the basic material design visual layout structureCenter
centers its child widgetText
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 textImage
: For displaying imagesTextInput
: For text inputScrollView
: For scrollable contentFlatList
: For efficient rendering of listsTouchableOpacity
: 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 ofbackground-color
) flex: 1
makes the container take up all available spacealignItems
andjustifyContent
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 NativeText
: For displaying textImage
: For displaying imagesTextField
: For text inputSingleChildScrollView
: Similar to ScrollViewListView
: Similar to FlatListGestureDetector
: 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 spaceflexDirection: '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 toflexDirection: 'column'
)Expanded
makes its child take all available space (similar toflex: 1
)Container
widgets define the header, content, and footer sectionsEdgeInsets
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 titlecontent
uses flex to center its childrenTouchableOpacity
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 bottomExpanded
withColumn
centers the contentSizedBox
is used for spacing between elements (similar to margins in React Native)ElevatedButton
provides a material design buttonNavigator.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’sFlatList
)AlertDialog
provides a modal dialog for adding/editing notes instead of a full-screen ModalTextEditingController
manages the input text stateInkWell
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
andNoteInput
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