My First React Native Expo Android App: KriCal
Building mobile apps is always exciting, especially when you get to create something useful from scratch. In this blog, I’ll walk you through how I built my very first React Native app using Expo — a calculator app named KriCal. I’ll share the entire development journey, including the code snippets, commands, and libraries I used.
Why React Native + Expo?
React Native lets you build cross-platform mobile apps using JavaScript and React. Expo makes this process easier by providing a managed workflow, so you don’t need to deal with native build tools directly.
Step 1: Setting Up the Environment
First, install Node.js (if you haven’t already) from nodejs.org
Next, install the Expo CLI globally:
Step 2: Create a New Expo Project
Create a new React Native project using Expo:
Command : expo init KriCal
Choose the blank (TypeScript or JavaScript) template (I used JavaScript for this project).
Navigate into the project folder:
Step 3: Run the App Locally
Start the development server:
This will open Expo Dev Tools in your browser. You can run the app on your phone via the Expo Go app (available on Android and iOS) by scanning the QR code.
Step 4: Our Folder Structure like this:
KriCal/
├── .expo/
├── assets/
├── Component/
│ └── Navbar.js
├── node_modules/
├── .gitignore
├── App.js
├── app.json
├── eas.json
├── index.js
├── package-lock.json
└── package.json
Step 5: Installing Dependencies
For my calculator app, I mainly used React Native’s built-in components. Here are the packages I installed:
- react-native-safe-area-context – To handle device safe areas (notches, status bars).
- expo-screen-orientation – To manage screen dimensions and orientation if needed.
Install them with:
npm install react-native-safe-area-context
expo install expo-screen-orientation
(Optional: If you use other libraries like vector icons or gesture handlers, you can add those too.)
Step 6: Building the Calculator UI and Its all Operations
I designed a simple but responsive UI using SafeAreaView, View, Text, TouchableOpacity, and ScrollView components.
Here’s a snippet of the UI layout for the calculator display and buttons:
Here’s a snippet of the UI layout for the calculator display and buttons:
import { StatusBar } from 'expo-status-bar';
import { useEffect, useState } from 'react';
import { Pressable, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import Navbar from './Component/Navbar';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Dimensions } from 'react-native';
const screenHeight = Dimensions.get('window').height;
export default function App() {
const [result, setresult] = useState('0')
const [input, setinput] = useState('')
const [eraseInterval, setEraseInterval] = useState(null);
const [history, setHistory] = useState([]);
const buttons = [
['%', '^', '.', '⌫'],
['7', '8', '9', '/'],
['4', '5', '6', '*'],
['1', '2', '3', '-'],
['0', 'C', '=', '+']
];
const handleeval = (value) => {
const operators = ['+', '-', '*', '/', '**', '.', '^', '%'];
if (value === 'C') {
setinput('')
setresult('')
} else if (value === '=') {
try {
const res = eval(input);
setresult(res.toString());
// console.log(res);
const newEntry = `${input} = ${res}`;
if (history[0] !== newEntry) {
const updatedHistory = [newEntry, ...history];
setHistory(updatedHistory);
{
(async () => {
await AsyncStorage.setItem('calcHistory', JSON.stringify(updatedHistory));
})()
}
}
} catch (error) {
console.error('Eval error:', error);
setresult('Error');
}
}
else if (value === '%') {
try {
const numbers = input.match(/(\d+\.?\d*)$/); // Get last number
if (numbers) {
const lastNumber = parseFloat(numbers[0]);
const percentageValue = lastNumber / 100;
const updatedInput = input.replace(/(\d+\.?\d*)$/, percentageValue.toString());
setinput(updatedInput);
setresult('');
} else {
setresult('Error');
}
} catch (error) {
setresult('Error');
}
}
else if (value === '^') {
const lastChar = input.slice(-1);
const lastTwoChars = input.slice(-2);
const operators = ['+', '-', '*', '/', '.', '%', '^'];
// Prevent adding ** if last is operator or already ends with **
if (
input === '' || // Don't start with **
lastTwoChars === '**' || // Already ended with **
operators.includes(lastChar) // Last char is an operator
) {
return; // Do not add power
}
setinput(input + '**');
}
else if (value === 'erase') {
setinput(input.slice(0, -1)); // remove last character
}
else {
const lastChar = input.slice(-1);
const isOperator = operators.includes(value);
// Prevent double operators (e.g. 1++ or 2**+)
if (
isOperator &&
(operators.includes(lastChar) || lastChar === '' || lastChar === '.')
) {
return; // don't add
}
setinput(input + value);
}
}
const getButtonValue = (label) => {
if (label === '⌫') return 'erase';
return label;
};
useEffect(() => {
const loadHistory = async () => {
const savedHistory = await AsyncStorage.getItem('calcHistory');
if (savedHistory) {
setHistory(JSON.parse(savedHistory));
}
};
loadHistory();
}, []);
const clearHistory = async () => {
try {
await AsyncStorage.removeItem('calcHistory'); // Remove from storage
setHistory([]); // Clear from state
} catch (error) {
console.log('Failed to clear history:', error);
}
};
// console.log('resy', history)
return (
<SafeAreaView style={{ width: '100%', height: '100%', backgroundColor: '#111212', }}>
<Navbar />
<View style={{ justifyContent: 'flex-start', width: '100%', height: '100%', marginBottom: 20 }}>
{
history &&
<View style={{ margin: 20, maxHeight: screenHeight * 0.2 }}>
<View style={{ flexDirection: 'row', justifyContent: 'space-around', width: '100%' }}>
{ history.length > 0 && <Text style={{ color: 'white', fontSize: 20, marginBottom: 10, fontWeight: 700 }}>History</Text>}
{history.length > 0 && (
<TouchableOpacity onPress={clearHistory} style={styles.clearHistoryButton}>
<Text style={{ color: 'white', fontSize: 20, marginBottom: 10, backgroundColor: '#8a4301', fontWeight: 700, padding: 3, borderRadius: 10 }}>Clear History</Text>
</TouchableOpacity>
)}
</View>
<ScrollView style={{ maxHeight: screenHeight * 0.12 }}>
{history.map((entry, index) => (
<Pressable key={index}>
<Text onPress={() => setinput(entry.split('=')[0])} style={{ color: '#ccc', fontSize: 18, margin: 10 }}>
{entry.replaceAll('*','x').replaceAll('xx','^')}
</Text>
</Pressable>
))}
</ScrollView>
</View>
}
<View style={{ alignItems: 'flex-end', padding: 12, marginTop: 10, maxHeight: screenHeight * 0.15 }}>
<ScrollView>
<Text style={{ color: 'white', fontSize: 28, fontWeight: '700' }}>
{input === '' ? '0' : input.replaceAll('**', '^').replaceAll('*', ' x ')}
</Text>
{input !== '' && (
<Text onPress={()=>setinput(result)} style={{ color: 'white', fontSize: 28, fontWeight: '700' }}>
= {String(Number(result).toLocaleString())}
</Text>
)}
</ScrollView>
</View>
<View style={{ borderColor: 'grey', width: '100%', borderWidth: 1 }}></View>
<View style={{ marginTop: 10 }}> {/* Main container for all rows */}
{buttons.map((row, rowIndex) => (
<View
key={rowIndex}
style={styles.buttonRow} // Apply row styles here
>
{row.map((btn) => (
<TouchableOpacity
key={btn}
onPress={() => handleeval(getButtonValue(btn))}
onPressIn={() => {
if (btn === '⌫') {
const interval = setInterval(() => {
setinput((prev) => prev.slice(0, -1));
}, 100); // speed of erase (adjustable)
setEraseInterval(interval);
}
}}
onPressOut={() => {
if (btn === '⌫' && eraseInterval) {
clearInterval(eraseInterval);
setEraseInterval(null);
}
}}
style={[styles.button, btn === '=' && styles.equalsButton, ['/', '*', '-', '+'].includes(btn) && styles.operatorButton,
btn === 'C' && styles.clearButton, !input && btn === '⌫' && styles.noterase]} // Apply individual button styles here
>
<Text style={styles.buttonText}>{btn}</Text>
</TouchableOpacity>
))}
</View>
))}
</View>
</View>
</SafeAreaView>
);
}
Step 7: Adding CSS Styling
This is the CSS style for the components used in this app :
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
buttonRow: {
width: '100%', // Ensure the row takes full width
flexDirection: 'row', // <-- CRITICAL: Arrange items horizontally
justifyContent: 'space-around', // <-- Now this will space out the buttons
alignItems: 'center', // Align buttons vertically in the center of the row
marginBottom: 10, // Space between rows
},
button: {
width: 50, // <-- Set a fixed width for each button
height: 50, // <-- Set a fixed height for each button
borderRadius: 25, // Makes it circular
backgroundColor: '#242423', // Example button background
justifyContent: 'center', // Center text horizontally within the button
alignItems: 'center', // Center text vertically within the button
margin: 3, // Small margin around each button for spacing
elevation: 3, // Android shadow
shadowColor: '#000', // iOS shadow
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
buttonText: {
color: 'white',
fontSize: 28,
fontWeight: '700', // Use string for fontWeight
},
equalsButton: {
backgroundColor: '#07e30e', // Green background for the equals button
},
operatorButton: {
backgroundColor: '#2e1a0b', // Orange background for operators (/, *, -, +)
},
clearButton: {
backgroundColor: '#10a5c7', // Light gray background for 'C'
},
noterase: {
backgroundColor: 'black',
opacity: 0.5, // make it look inactive
}
});
Step 8: Created Basic Navbar
import { StyleSheet, Text, View } from 'react-native'
import React from 'react'
const Navbar = () => {
return (
<View>
<Text
style={{
textAlign: 'center',
fontSize: 24,
fontWeight: '800', // Best to use string for fontWeight
color: 'white',
textShadowColor: 'white', // Shadow color
textShadowOffset: { width: 0, height: 0 }, // No offset (shadow directly behind)
textShadowRadius: 15, // Adjust for desired blur effect
}}
>
KriCal
</Text>
</View>
)
}
export default Navbar
const styles = StyleSheet.create({})
Step 9: Adding History Feature
One useful feature I added was saving calculation history so users can tap previous calculations and reuse them.
I used a state variable history (an array) and updated it whenever the user presses = with a valid result.
Example:
const [history, setHistory] = useState([]);
const handlePress = (btn) => {
// ... previous code ...
else if (btn === '=') {
try {
const res = eval(input).toString();
setInput(res);
setHistory((prev) => [input, ...prev]); // Add to history
} catch {
setInput('Error');
}
}
// ...
};
Then I displayed this history in a ScrollView, allowing tapping a history item to set it back in the input:
<ScrollView style={{ maxHeight: 120 }}>
{history.map((entry, index) => (
<TouchableOpacity key={index} onPress={() => setInput(entry)}>
<Text style={{ color: '#ccc', fontSize: 18, margin: 10 }}>{entry}</Text>
</TouchableOpacity>
))}
</ScrollView>
Step 10: Building APK using EAS Build
Expo Application Services (EAS) allows building Android and iOS binaries easily.
Installing EAS CLI
Logging into Expo
Configure your project for EAS build
Building Android APK
eas build -p android --profile preview
After the build finishes, Expo provides a public URL to download your APK.
Note: For iOS, you need an Apple Developer account, which is required to build iOS apps.
Final Thoughts
Building KriCal was a great learning experience. React Native + Expo made it straightforward to build, test, and deploy my app quickly. The app features a clean UI, history saving, and smooth usability with no unnecessary permissions, making it safe and lightweight.
If you want to try the app or check the source code, I’m happy to share it — just ask!
Even You Can Checkout this Calculator App APK by downloading it in your Android Phone:
Follow me on :
Linkedin - Krishna Shrivastava
Portfolio - Krishna Shrivastava