- Published on
Cross-Platform Mobile Development: iOS, Android, and Flutter Guide
- Authors
- Name
- Muhamad Riyan
- @muhamad-riyan
Introduction
Mobile development has evolved to encompass multiple platforms and frameworks. This guide explores native development for iOS and Android, as well as cross-platform development using Flutter, providing insights into best practices and patterns for each approach.
iOS Development with Swift
Basic iOS Project Setup
// AppDelegate.swift
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = MainTabBarController()
window?.makeKeyAndVisible()
return true
}
}
// MainTabBarController.swift
class MainTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
setupViewControllers()
}
private func setupViewControllers() {
let homeVC = UINavigationController(rootViewController: HomeViewController())
homeVC.tabBarItem = UITabBarItem(title: "Home",
image: UIImage(systemName: "house"),
selectedImage: UIImage(systemName: "house.fill"))
let profileVC = UINavigationController(rootViewController: ProfileViewController())
profileVC.tabBarItem = UITabBarItem(title: "Profile",
image: UIImage(systemName: "person"),
selectedImage: UIImage(systemName: "person.fill"))
viewControllers = [homeVC, profileVC]
}
}
iOS UI Implementation
// HomeViewController.swift
class HomeViewController: UIViewController {
private let tableView: UITableView = {
let tv = UITableView()
tv.translatesAutoresizingMaskIntoConstraints = false
tv.register(PostCell.self, forCellReuseIdentifier: "PostCell")
return tv
}()
private var posts: [Post] = []
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
fetchPosts()
}
private func setupUI() {
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
tableView.delegate = self
tableView.dataSource = self
}
private func fetchPosts() {
PostService.shared.fetchPosts { [weak self] result in
switch result {
case .success(let posts):
self?.posts = posts
DispatchQueue.main.async {
self?.tableView.reloadData()
}
case .failure(let error):
print("Error fetching posts: \(error)")
}
}
}
}
extension HomeViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return posts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "PostCell", for: indexPath) as? PostCell else {
return UITableViewCell()
}
cell.configure(with: posts[indexPath.row])
return cell
}
}
Android Development with Kotlin
Android Project Structure
// MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupNavigation()
}
private fun setupNavigation() {
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
findViewById<BottomNavigationView>(R.id.bottom_nav)
.setupWithNavController(navController)
}
}
Android MVVM Implementation
// UserViewModel.kt
class UserViewModel(
private val userRepository: UserRepository
) : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> = _user
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> = _loading
fun fetchUserProfile(userId: String) {
viewModelScope.launch {
_loading.value = true
try {
val result = userRepository.getUserProfile(userId)
_user.value = result
} catch (e: Exception) {
// Handle error
} finally {
_loading.value = false
}
}
}
}
// UserRepository.kt
class UserRepository(
private val api: ApiService,
private val userDao: UserDao
) {
suspend fun getUserProfile(userId: String): User {
return withContext(Dispatchers.IO) {
try {
val response = api.getUserProfile(userId)
userDao.insertUser(response)
response
} catch (e: Exception) {
userDao.getUser(userId) ?: throw e
}
}
}
}
Android UI with Jetpack Compose
// HomeScreen.kt
@Composable
fun HomeScreen(
viewModel: HomeViewModel = viewModel(),
onPostClick: (Post) -> Unit
) {
val posts by viewModel.posts.collectAsState()
val loading by viewModel.loading.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
if (loading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.CenterHorizontally)
)
} else {
LazyColumn {
items(posts) { post ->
PostCard(
post = post,
onClick = { onPostClick(post) }
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
}
@Composable
fun PostCard(
post: Post,
onClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
elevation = 4.dp
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = post.title,
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = post.content,
style = MaterialTheme.typography.body2
)
}
}
}
Flutter Cross-Platform Development
Flutter Project Setup
// lib/main.dart
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const HomeScreen(),
);
}
}
Flutter State Management with Riverpod
// lib/providers/auth_provider.dart
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier(ref.read(authRepositoryProvider));
});
class AuthNotifier extends StateNotifier<AuthState> {
final AuthRepository _authRepository;
AuthNotifier(this._authRepository) : super(const AuthState.initial());
Future<void> signIn(String email, String password) async {
state = const AuthState.loading();
try {
final user = await _authRepository.signIn(email, password);
state = AuthState.authenticated(user);
} catch (e) {
state = AuthState.error(e.toString());
}
}
void signOut() {
_authRepository.signOut();
state = const AuthState.initial();
}
}
// lib/models/auth_state.dart
class AuthState with _$AuthState {
const factory AuthState.initial() = _Initial;
const factory AuthState.loading() = _Loading;
const factory AuthState.authenticated(User user) = _Authenticated;
const factory AuthState.error(String message) = _Error;
}
Flutter UI Implementation
// lib/screens/home_screen.dart
class HomeScreen extends ConsumerWidget {
const HomeScreen({Key? key}) : super(key: key);
Widget build(BuildContext context, WidgetRef ref) {
final posts = ref.watch(postsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () => ref.read(authProvider.notifier).signOut(),
),
],
),
body: posts.when(
data: (posts) => ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) => PostCard(post: posts[index]),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const CreatePostScreen()),
),
child: const Icon(Icons.add),
),
);
}
}
// lib/widgets/post_card.dart
class PostCard extends StatelessWidget {
final Post post;
const PostCard({Key? key, required this.post}) : super(key: key);
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
post.title,
style: Theme.of(context).textTheme.headline6,
),
const SizedBox(height: 8),
Text(post.content),
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.favorite, size: 16),
const SizedBox(width: 4),
Text('${post.likes}'),
const Spacer(),
TextButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PostDetailScreen(post: post),
),
),
child: const Text('Read More'),
),
],
),
],
),
),
);
}
}
Platform-Specific Code
// lib/utils/platform_utils.dart
import 'dart:io';
class PlatformUtils {
static void showPlatformDialog(BuildContext context, String title, String message) {
if (Platform.isIOS) {
showCupertinoDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text(title),
content: Text(message),
actions: [
CupertinoDialogAction(
child: const Text('OK'),
onPressed: () => Navigator.pop(context),
),
],
),
);
} else {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
child: const Text('OK'),
onPressed: () => Navigator.pop(context),
),
],
),
);
}
}
}
Best Practices
Platform-Specific Design
- Follow Material Design for Android
- Adhere to Human Interface Guidelines for iOS
- Use platform-specific widgets in Flutter
State Management
- Use MVVM for Android
- Follow MVC/MVVM for iOS
- Implement Riverpod/Bloc for Flutter
Performance
- Optimize image loading
- Implement proper caching
- Use lazy loading when appropriate
Testing
- Write unit tests
- Implement integration tests
- Perform UI tests
Architecture
- Follow clean architecture principles
- Implement dependency injection
- Maintain separation of concerns
Resources
Happy coding!