App Development
Explore the pros and cons of Native Apps, PWAs, Cross-Platform solutions, and App Builders to find the perfect fit for your next mobile project.
Starting a new project and wondering which tech stack to use? Native, PWA, web app builders, or maybe an AI-powered solution?
At StartAppz, we’ve tested various technologies to help you assess your needs. Let’s take you on a journey through the pros and cons of each option and where they shine.
App Builder Websites
Pros:
- Fast Development: Pre-designed templates and drag-and-drop interfaces make it quick to create an app.
- No Programming Knowledge Required (Sometimes): Platforms like Adalo, Thunkable, or Glide allow non-developers to build apps.
- Great for MVPs: Ideal for testing an idea before committing to full development.
Cons:
- Locked-in Solution: Most platforms don’t allow you to export source code (e.g., Glide apps are bound to Glide’s infrastructure).
- Limited Features: Complex features, like advanced machine learning, are often unsupported.
- Custom Code is Limited: For example, while Bubble supports some plugins, it doesn’t allow deep customization like direct code.
Technical Example:
If you use Thunkable, creating a form app can be done in hours, but adding offline capabilities or custom animations will require switching to native development.
Custom PWA (Progressive Web Apps)
Pros:
- Cost-effective: Uses web technologies like HTML, CSS, and JavaScript to create apps that work on multiple platforms.
- Cross-platform: A single codebase runs on browsers, desktops, and mobiles.
- SEO-Friendly: Since PWAs are web-based, search engines can index them, which is helpful for discoverability.
Cons:
- Limited Access to Native Features: PWAs don’t have full access to device APIs, like Bluetooth or NFC. Visit What PWA Can Do Today for a detailed list.
- Limited User Engagement: PWAs rely on browser-based push notifications, which are less engaging than native ones.
- Relatively New: Features like the Web Share API or File System Access API aren’t fully supported across all browsers, especially Safari on iOS.
Technical Example:
PWAs like Twitter Lite offer fast performance but don’t support high-end features like offline video processing.
Native Apps
Pros:
- Fast & Efficient: Written directly in platform languages like Kotlin (Android) or Swift (iOS), native apps can utilize device hardware like GPUs for smooth performance.
- Polished UX: Native UI components look and feel like they belong on the platform.
- High Performance: Ideal for graphics-intensive tasks like gaming or video editing.
Cons:
- Slower Development: Separate teams are required for Android and iOS, increasing time.
- Maintenance Costs: Regular updates are needed to stay compatible with OS changes.
- Higher Costs: Typically requires larger budgets for development and ongoing support.
Technical Example:
A Telco App like Virgin Mobile UAE is developed natively because it requires high performance, real-time synchronization, and low latency, and plenty of customization.
Cross-Platform Native Apps
Pros:
- Write Once, Deploy Everywhere: Frameworks like Flutter, React Native, and Kotlin Multiplatform reduce time by sharing code across platforms.
- Cost-efficient: Shared codebase reduces development costs significantly.
Cons:
- Platform-independent UX: It may not feel truly native to users. For instance, React Native apps often use custom navigation patterns.
- Performance Glitches: Some frameworks, like React Native, can introduce performance issues with heavy animations or hardware access.
Technical Example:
Apps like Facebook Ads Manager (React Native) and Google Ads (Flutter) balance the trade-offs of cross-platform development for business apps.
When to Use Native Apps, PWA, or App Builders
- App Builders: Ideal for prototyping or creating a lightweight app with basic functionality.
Use Case: A small business creating a catalog or content app. - PWA: Best when you need to go to market quickly, have a limited budget, or focus on discoverability.
Use Case: News/media apps (e.g., The Washington Post), lightweight e-commerce (e.g., AliExpress PWA), or travel guides. - Native Apps: For polished, complex apps that leverage device features like sensors, cameras, or AR/VR.
Use Case: Gaming apps, fintech apps (e.g., Robinhood), or social media apps (e.g., Snapchat). - Cross-Platform Apps: When aiming to reduce costs while maintaining moderate performance and UX.
Use Case: Business tools, social media apps, or shopping apps like Shopify.
Deep Dive: App Builders, PWA, Cross-Platform or Native?
When developing a mobile app, the choice of technology can drastically affect your app's cost, UX and performance.
App Builder Websites
Platforms like Adalo, Thunkable, and Glide allow rapid app creation using drag-and-drop interfaces. However, these platforms abstract away the code, limiting flexibility.
Technical Limitation Example: If you want to add an offline caching mechanism, app builders typically don’t allow access to native APIs like Room
(Android) or Core Data
(iOS).
Custom PWAs (Progressive Web Apps)
PWAs use standard web technologies but can mimic native apps via features like service workers and Web APIs. They’re ideal for apps that don’t rely on intensive hardware.
Key Features:
- Service Workers for offline capabilities.
- Manifest.json for app-like appearance and installability.
PWA Code Snippet:
// Service Worker for caching assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('static-v1').then((cache) => {
return cache.addAll(['/index.html', '/styles.css', '/app.js']);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
Technical Limitation Example: PWAs cannot access hardware features like Bluetooth or Sensors. For instance, a Health app requiring access to health data and sensors is better suited for native development.
Native Apps
Native apps are developed using platform-specific languages like Kotlin/Java for Android and Swift/Objective-C for iOS. They provide unmatched performance and access to hardware features.
Technical Comparison: Push Notifications
Native apps handle push notifications through platform-specific services like Firebase Cloud Messaging (FCM) for Android and APNs for iOS.
Android (Native Push Notification Example):
// Firebase Messaging Service
class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val notification = NotificationCompat.Builder(this, "CHANNEL_ID")
.setContentTitle(remoteMessage.notification?.title)
.setContentText(remoteMessage.notification?.body)
.setSmallIcon(R.drawable.ic_notification)
.build()
NotificationManagerCompat.from(this).notify(1, notification)
}
}
iOS (Swift Push Notification Example):
// Register for notifications in AppDelegate
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
print("APNs token: \(token)")
}
Cross-Platform Native Apps
Cross-platform frameworks like React Native, Flutter, and Kotlin Multiplatform (KMP) aim to reduce development time while achieving near-native performance.
React Native
React Native uses JavaScript with a native bridge to render UI components. It’s a good choice for simple business apps.
React Native Code Snippet:
import React from 'react';
import { View, Text, Button } from 'react-native';
export default function App() {
return (
<View style={{ padding: 20 }}>
<Text>Hello, React Native!</Text>
<Button title="Click Me" onPress={() => alert('Button Pressed')} />
</View>
);
}
Limitation Example:
React Native can introduce performance bottlenecks in animation-heavy apps because of the bridge between JavaScript and native threads. Frameworks like Flutter bypass this by compiling directly to native code.
Flutter
Flutter uses Dart and offers a rich set of widgets for customizable UIs. It’s more performant than React Native for animations and graphical interfaces.
Flutter Code Snippet:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("Flutter App")),
body: Center(
child: ElevatedButton(
onPressed: () => print("Button Pressed"),
child: Text("Click Me"),
),
),
),
);
}
}
Use Case Example:
The Google Ads app was rebuilt using Flutter to take advantage of its smooth animations and faster updates.
Kotlin Multiplatform (KMP)
KMP allows sharing business logic across platforms while writing native UI for each. This strikes a balance between code sharing and native experience. Also, Compose UI can be used to write single UI for all framework.
KMP Code Snippet (Shared Logic):
class ApiService() {
private val httpClient: HttpClient = HttpClient {
install(ContentNegotiation) {
json(get())
}
}
fun fetchDashboard(): Flow<List<DashboardItem>> {
return flow {
val response = httpClient.get("${BuildConfig.API_URL}/dashboard")
emit(response.body())
}
}
}
To use ApiService
in iOS, we can export all network/data layer as framweork and import it in xcode.
Native-Specific Code (iOS):
class DashboardViewModel : ObservableObject {
@Published var items: [DashboardItem] = []
private let apiService = ApiService()
func loadDashboard() {
apiService.fetchDashboard { items in
if let items = items {
self.items = items
}
}
}
}
struct DashboardUIView: View {
@ObservedObject var viewModel = DashboardViewModel()
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale.current
formatter.dateFormat = "dd/MM/yyyy - HH:mm"
return formatter
}()
var body: some View {
NavigationStack {
List(viewModel.items) { item in
HStack {
AsyncImage(url: URL(string: item.avatar)) { phase in
switch phase {
case .empty:
ProgressView().frame(width: 50, height: 50)
case .success(let image):
image
.resizable()
.clipShape(Circle())
.frame(width: 50, height: 50)
case .failure(_):
Circle()
.fill(Color.gray)
.frame(width: 50, height: 50)
.overlay {
Text("X").foregroundColor(.white)
}
@unknown default:
EmptyView()
}
}
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(String(format: NSLocalizedString("lbl_created_at", comment: ""), dateFormatter.string(from: item.createdAt)))
.font(.subheadline)
.foregroundColor(.gray)
}
}
}
.navigationTitle(title)
.onAppear {
viewModel.loadDashboard()
}
}
}
}
Native-Specific Code (Android):
class DashboardViewModel(
dashboardRepository: DashboardRepository,
) : ViewModel() {
val state = dashboardRepository.dashboardList()
.map { items -> MainState.Success(items) }
.catch { MainState.Error }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = MainState.Loading
)
}
sealed class MainState {
data object Loading : MainState()
data class Success(val items: List<DashboardItem>) : MainState()
data object Error : MainState()
}
@Composable
fun DashboardScreen(
viewModel: DashboardViewModel,
) {
val state by viewModel.state.collectAsStateWithLifecycle()
when (state) {
MainState.Error -> // show error
MainState.Loading -> // show loading
is MainState.Success -> ListContent(
list = (state as MainState.Success).items,
)
}
}
@Composable
fun ListContent(
modifier: Modifier = Modifier,
list: List<DashboardItem>
) {
LazyColumn(
modifier = modifier.fillMaxSize().padding(16.dp)
) {
items(list) { item ->
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)) {
AsyncImage(
modifier = Modifier.clip(CircleShape).size(50.dp),
model = item.avatar,
contentDescription = item.name,
placeholder = painterResource(R.drawable.placeholder),
error = painterResource(R.drawable.placeholder),
)
Spacer(Modifier.width(10.dp))
Column {
Text(
text = item.name,
textAlign = TextAlign.Start,
style = MaterialTheme.typography.titleMedium
)
Text(
text = item.createdAt,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline
)
}
}
}
}
}
In addition to use native UI systems, we can use CMP Compose UI multiplatform, which is based on Android Compose UI to share the UI as well. Note that, CMP in beta for iOS and experimental for Web.
Using Expect / Actual
KMP offers expect/actual mechanism to write platform specific code, for example to get the screen width;
KMP code (shared)
expect fun getScreenWidth(): Dp
Android Source code
actual fun getScreenWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp
iOS Source code
actual fun getScreenWidth(): Dp = LocalWindowInfo.current.containerSize.width.pxToPoint().dp
Use Case Example:
Netflix uses KMP to share caching and analytics code between its Android and iOS apps.
Comparative Table: PWA vs Native vs Cross-Platform
Feature | PWA | Native | Cross-Platform |
---|---|---|---|
Performance | Moderate | High | Moderate-High |
Access to Hardware | Limited | Full | Partial (depends on framework) |
Development Cost | Low | High | Moderate |
Codebase | Single (Web) | Separate for iOS/Android | Shared for most logic |
Framework Examples | N/A | Kotlin/Swift | React Native, Flutter, KMP |
Use Cases | News, blogs, basic e-commerce | Gaming, Telcos, social media | Business apps, simple games |
Conclusion
Your choice depends on the project’s scope:
- Use App Builders for quick MVPs or proof-of-concept apps.
- Use PWAs for lightweight, SEO-friendly apps with minimal hardware dependencies.
- Use Native Apps for performance-critical, hardware-intensive applications.
- Use Cross-Platform frameworks for budget-conscious projects with moderate complexity.
Which option is best for your next project? It depends on your goals, budget, and technical requirements. Hopefully, this guide gives you a clearer perspective to make an informed choice.
This post was written and edited by Hisham Ghatasheh our Senior Mobile Solution Architect.