AndiOSDev Building

Data and UI

Siamak (Ash) Ashrafi
24 min readMay 27, 2024

Section 2: Building Your App

  • Data Persistence: Dive into the world of databases with RoomDB (Android) and SwiftData (iOS).
  • User Interface: Explore the power of declarative UI with Compose and SwiftUI, including state management and navigation.
  • Development Environment: Set up your development environment with Android Studio or Xcode, and explore additional tooling options.
  • Project Structures: Understand how to organize your project effectively.

This is the second part of a three part series:

DataBase

Room & SwiftData: A Tale of Two Data Stores

For persistent data storage in mobile apps, both Android and iOS offer options beyond the built-in file system. Here’s a comparison of RoomDB (Android) and SwiftData (iOS):

RoomDB (Android)

  • Type: Object Relational Mapper (ORM)
  • Philosophy: Provides an abstraction layer on top of SQLite, a lightweight relational database engine.

Key Features

  • Annotations for defining data classes that map to database tables.
  • Automatic generation of SQL queries (CRUD — Create, Read, Update, Delete) based on data class definitions.
  • Support for migrations to handle schema changes across app versions.
  • Use Hilt (DI) for creation and injection to Repo and ViewModel
  • Kotlin Flow capabilities, enabling automatic UI updates when underlying data changes. Reactive Streams from DB for the UI
@Query("SELECT * FROM word_table ORDER BY word ASC")
fun getAlphabetizedWords(): Flow<List<Word>>

And now RoomDB is now KMP!

SwiftData(iOS)

  • Type: CoreData Framework Wrapper (CoreData uses SQLite)
  • Philosophy: Offers a simplified API to interact with Apple’s Core Data framework, a built-in persistent storage solution with a reactive layer.

Key Features

  • Type-safe access to Core Data entities and attributes.
  • Provides functions for common operations like fetching, saving, and deleting data.
  • Handles relationships between entities.
  • Supports asynchronous operations for improved performance.
  • Provides a reactive flow to SwiftUI
  • Easy sync to iCloud

https://www.hackingwithswift.com/quick-start/swiftdata/how-to-sync-swiftdata-with-icloud

Comparison

Although they run on completely different platforms they some similarities and some differences. Let’s list these:

Similarities

  • Both RoomDB and SwiftData aim to simplify persistent data storage for mobile developers.
  • They offer abstractions that shield developers from writing raw SQL or directly manipulating Core Data objects.
  • Both provide mechanisms for fetching, saving, and updating data.
  • Both provide a reactive stream for the UI!

Differences

  • Underlying Technology: RoomDB utilizes SQLite, a built-in database engine, while SwiftData interacts with Core Data, a built-in framework built on top of SQLite.
  • Level of Abstraction: RoomDB offers a high level of abstraction with automatic SQL generation, while SwiftData provides a simple high layer wrapper around Core Data’s functionalities. Both provide a reactive stream to the UI.

Additional Information

  • RoomDB might be more familiar to developers with experience in relational databases.
  • SwiftData leverages Apple’s native Core Data framework, ensuring tight integration with the iOS ecosystem.
  • Both libraries are actively maintained and offer extensive documentation.

Database & Architecture

There are some limitations to using SwiftData directly within the MVVM pattern. Here’s a breakdown of the challenges and how RoomDB in Kotlin integrates well with MVVM

RoomDB and MVVM: A Perfect Match

  • Flow Data Support: RoomDB offers built-in Flow support. Changes to data in the database automatically trigger updates to Kotlin Flow, which Repo/ViewModels can observe. This simplifies UI updates based on data changes.
  • Separation of Concerns: RoomDB encourages separating data access logic from the UI layer by interacting with it through a repository or data access object. This aligns perfectly with the MVVM pattern.

Example (RoomDB & Koltin Flow):

DB → Dao → Repo → VM (Use Hilt for DI — not shown*)

class UserViewModel(private val userRepository: UserRepository) : ViewModel() {

// Define a Flow to emit a list of Users
val userList: Flow<List<User>> = userRepository.getUsers()
.flowOn(Dispatchers.IO) // Perform network or database operations on IO thread

fun loadUsers() {
// Launch a coroutine to fetch users
viewModelScope.launch {
userRepository.fetchUsers()
}
}
}


// Use the repo to get the data
class UserRepository(private val userDao: UserDao) {

// Define a function to get users from the database as Flow
fun getUsers(): Flow<List<User>> = userDao.getAllUsers()

// Define a suspend function to fetch users from a network or other source and update the database
suspend fun fetchUsers() {
// Fetch data from network or other source
val users = // fetch users logic (e.g., API call)

// Update the database with fetched users
userDao.insertAll(users)
}
}

// Define the SQL calls
@Dao
interface UserDao {

// Define a method to get all users from the database
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>

// Define a method to insert users into the database
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(users: List<User>)
}

// Define User
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
val name: String,
val email: String
)


// Define DB with User
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}

In this example, the view model observes the user list with Kotlin Flowand updates the UI based on changes. The data access logic resides in the UserRepository, promoting separation of concerns.

SwiftData & MVVM: Challenges

  • Direct Data Access in Views: While SwiftData simplifies Core Data interaction, it can lead to views directly accessing and modifying data. This violates the separation of concerns principle in MV*.
  • Missing MV* Observation Layer: SwiftData itself doesn’t provide a built-in Layer for view models to observe changes in the underlying data. This makes it difficult to automatically update the UI when data changes using a MV* pattern.

SwiftData updates the UI when the DB changes. This looks like the “Active Record Pattern” where the records in the DB are bound to the fields in the UI.

→ SwiftData expects to be used inside the UI with the reactive @Query call ←

@Model
class Event {
var name: String
init(name: String, date: Date) {
self.name = name
}
}

struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: \.date, order: .forward) private var events: [Event]

var body: some View {
List {
ForEach(events) { event in
Text(event.name)
}
}
}
}

Nice SwiftData Guide from Mohammad Azam with an explanation of architecture.

Since 2019, I have used many different architectural patterns when building SwiftUI applications. This included MVVM, Container pattern, Redux, MV pattern and Active Record Pattern. Apple has a sample SwiftData application called Backyard Birds: Building an app with SwiftData and widgets, which uses a variation of Active Record Pattern.

~~~

Hacking with Swift (Paul Hudson) Why he does not use MVVM with SwiftData

“It works really badly with SwiftData, at least right now. This might improve in the future, but right now using SwiftData is basically impossible with MVVM.”

Azam Sharp post on Paul Hudson post about MVVM and SwiftData

Summary:

azamsharp … has been saying this for years.

Details of how to build Swift/SwiftUI/SwiftData Apps without a ViewModel

While SwiftData offers a convenient API for Core Data, it requires additional effort to achieve clean separation of concerns in MVVM. RoomDB, on the other hand, provides built-in features like Kotlin Flow that streamline data observation and integration with MV* architecture.

User Interface — Compose is Swift(UI)

Android & iOS are both similar/familiar and very different …

Jetpack Compose & SwiftUI

Bridging the Gap: How Compose and SwiftUI Unify Mobile App Development

The mobile development landscape is evolving. Traditionally, Android and iOS demanded separate toolsets and languages, creating a hurdle for developers targeting both ecosystems. However, the emergence of declarative UI frameworks like Jetpack Compose (Android) and SwiftUI (iOS) is bridging this gap.

Shared Philosophy: Declarative UI

Both Compose and SwiftUI move away from the imperative UI approach. Instead of manipulating the UI state directly, developers describe the desired state declaratively. This means defining what the UI should look like for a given data set, and the framework handles rendering and updates efficiently. This shared philosophy leads to a similar development experience with concise and easy-to-reason-about code.

Key Aspects of Declarative UI:

  • State Focus: UI is defined based on the current application data state.
  • Composable/View Functions: Developers create composable functions (Kotlin) or view functions (Swift) that represent UI components and their properties.
  • Automatic Updates: The UI automatically recomposes and updates itself when the underlying data changes.

Benefits of Declarative UI:

  • Improved Readability: Code is more concise and focuses on the desired UI ("what") rather than implementation details ("how").
  • Maintainability: Changes to the UI are easier to track and manage by manipulating the data state.
  • State Management: Declarative UI integrates well with state management solutions, simplifying UI state handling.

Familiar Building Blocks

Both frameworks offer a core set of UI components like buttons, text fields, and layouts (Columns/Rows in Compose, VStacks/HStacks in SwiftUI) with similar functionalities and properties. This allows developers familiar with one framework to grasp the concepts of the other more easily.

Clean Code Structure

The use of modern languages like Kotlin and Swift, known for their emphasis on readability, translates well to Compose and SwiftUI code. Both frameworks rely on functions and composables/views to define UI elements, resulting in clear and concise code.

Smoother Learning Curve

Kotlin developers can transition to SwiftUI with a smoother learning curve due to familiarity with the concepts. Similarly, iOS developers can more easily grasp Compose. This reduces the overall learning curve for cross-platform developers.

The rise of Compose and SwiftUI marks a significant shift in mobile development. While not identical, their shared philosophy of declarative UI and similar building blocks create a more unified development experience. This empowers developers to learn both frameworks and potentially write apps for both platforms more efficiently.

~~~

Both Kotlin Compose and Swift SwiftUI embrace a declarative approach to building user interfaces. This approach prioritizes describing the desired UI state rather than explicitly outlining the steps to achieve it.

Here’s a breakdown of 3 main concepts in building app UI.

  1. UI (Composable Material Design — SwiftUI HIG)
  2. State (remember — @State)
  3. Navigation

We will review these below 👇🏾

User Interface — 1. Building Compose & SwiftUI

Compose & SwiftUI

UI Rule: Beauty is skin deep, Keep your UI as thin as possible!

Keep your UI as simple as possible:

  • Stateless
  • Side-effects Free
  • No Logic / No Threading / No Anything
  • Just Looks — No Brains!!!

This is the correct way of stripping everything off as you get to the content:

@Composable
fun ListPhotodoRoute(
paddingValues: PaddingValues,
navTo: (String) -> Unit,
viewModel: ListPhotodoViewModel = hiltViewModel(),
) {
val onEvent: (ListPhotodoEvent) -> Unit = viewModel::onEvent
// https://medium.com/androiddevelopers/consuming-flows-safely-in-jetpack-compose-cde014d0d5a3
val state by viewModel.uiState.collectAsStateWithLifecycle()
ListPhotodoScreen(
paddingValues = paddingValues,
onEvent = onEvent,
photodoState = state,
navTo = navTo
)

}
@Composable
internal fun ListPhotodoScreen(
modifier: Modifier = Modifier,
paddingValues: PaddingValues,
onEvent: (ListPhotodoEvent) -> Unit,
photodoState: ListPhotodoUiState,
navTo: (String) -> Unit,
) {
when (photodoState) {
ListPhotodoUiState.Loading -> {
//ListPhotodoUiState.Loading
CircularProgressIndicator()
}

is ListPhotodoUiState.Success -> ListPhotodoContent(
modifier,
paddingValues = paddingValues,
onEvent = onEvent,
photodoList = photodoState.data,
navTo = navTo,
)
ListPhotodoUiState.Error -> TODO()
}
}


// path here Screen.PhotoList.route
@Composable
fun ListPhotodoContent(
modifier: Modifier = Modifier,
paddingValues: PaddingValues,
onEvent: (ListPhotodoEvent) -> Unit,
photodoList: List<Photodo /* = PhotodoEntity */>,
navTo: (String) -> Unit,
) {

This makes it very easy for generative AI to build your UIs.

My DeSign 👇🏾 (left) & AI generated design on the right 👇🏾 Wow !!!

My UI (right) compared to Chat GPT 4.0 (left)

Generative AI should do all your design and testing …

Composition — Kotlin Compose & Swift SwiftUI

Kotlin

Composable Functions: Compose functions are the building blocks for UI elements. They describe the UI based on the current state and return a Composable representing the UI component.

Modifiers: Compose provides a rich set of modifiers that can be applied to composables to customize their appearance and behavior (e.g., padding, clickable). Order is bottom to top. Modifiers are applied from bottom to top, meaning the bottom-most modifier is applied first and the top-most last.

Composable Layouts: Built-in layout composables like Column, Row, and Box help structure and organize UI components within your hierarchy.

Layout Steps

Composition:

  • Composing UI: The process starts with the composition phase, where the composables (UI elements) are defined and their structure is established. This step involves creating a tree of composable functions.

Measurement:

  • Parent Measures Children: Each composable measures its children. The measurement starts from the root and proceeds down the tree. A parent composable requests each child to measure itself, passing constraints that limit the possible sizes the child can take.
  • Constraints Propagation: Constraints are propagated down the tree, with each parent composable potentially modifying the constraints for its children.
  • Child Measurement: Each child returns its measured size to the parent. This process continues until all children are measured.

Layout:

  • Parent Lays Out Children: After measuring, the layout phase begins. In this phase, the parent composable sets the positions of its children. This is based on the sizes determined during the measurement phase and the layout logic defined by the parent.
  • Positioning Children: Each child is positioned within the bounds of the parent, taking into account alignment, padding, and other layout modifiers.

Drawing:

  • Rendering to Screen: Finally, after measurement and layout, the drawing phase renders the composables to the screen. This involves drawing the UI elements based on their measured sizes and positions.

Key points include the sequential execution of these phases, the ability to skip unnecessary phases, the importance of modifiers in layout, and the triggering of recomposition when state changes affect the UI.

Videos:

Compose Layouts and Modifiers

SwiftUI

View Functions: View functions are the building blocks for UI elements. They describe the UI based on the current state and return a View representing the UI component.

Modifiers: SwiftUI also offers modifiers to customize views (e.g., .padding(), .onTapGesture()). Order is top to bottom. Modifiers are applied from top to bottom, meaning the top-most modifier is applied first and the bottom-most last.

Stacks and Containers Layouts: Built-in views like VStack (vertical stack) and HStack (horizontal stack) help organize and arrange views within the hierarchy.

Steps

View Creation:

  • View Hierarchy: The process starts by defining the view hierarchy, where each view is created and its structure is established through SwiftUI view declarations.

Measurement:

  • Parent Measures Children: Each parent view measures its children. The measurement starts from the root view and proceeds down the hierarchy. A parent view provides each child with a proposed size or constraints.
  • Intrinsic Content Size: Views calculate their intrinsic content size, which is the size they need to display their content properly.

Layout:

  • Parent Lays Out Children: After measurement, the layout phase begins. The parent view determines the final size and position of its children based on the proposed sizes and its own layout rules.
  • Alignment and Positioning: Views are positioned within their parent, considering alignment, padding, and other layout modifiers.

Drawing:

  • Rendering to Screen: Finally, views are rendered to the screen based on their measured sizes and positions. This involves drawing the visual content of each view.

Android Material & SwiftUI Components

Some links need fixing … we are updating as time allows …

HTML table with links … https://developery.github.io/

Table illustrates the point they have similar functionality.

Compose & SwiftUI UI Concepts

Kotlin Compose UI:

@Composable
fun MyScreen() {
var count by remember { mutableStateOf(0) }

Column(modifier = Modifier.padding(16.dp)) {
Text(text = "Count: $count", modifier = Modifier.padding(8.dp))
Button(onClick = { count++ }) {
Text("Increment")
}
}
}

SwiftUI:

struct ContentView: View {
@State private var count = 0

var body: some View {
VStack {
Text("Count: \(count)")
.padding(8)
Button(action: { count += 1 }) {
Text("Increment")
}
}
.padding(16)
}
}

NEW: Foldables are HERE!!! (coming soon to iOS)

Adaptive layouts are a new set of Compose APIs for building layouts that adapt as users expect when switching between small and large window sizes. Material guidance is used in these APIs. These layouts are helpful when thinking about your app in terms of panes rather than screens. For example, on a phone you might only display one pane, but on a tablet you might show two panes at the same time.

There are three new scaffolds that adapt to different window sizes:

  • NavigationSuiteScaffold,
  • ListDetailPaneScaffold
  • upportingPaneScaffold.

Benefits:

  • Shows bottom bar on smaller screens for easy thumb reach.
  • Switches to navigation rail or drawer on larger screens for better organization.
  • Reduces development effort by handling responsiveness.

Steps:

  1. Add necessary dependencies (NavigationSuite and Adaptive navigation libraries).
  2. Define your screens using an enum class with title and icon information.
  3. Create a NavigationSuiteScaffold composable, specifying the list of screens.
  4. Use the NavigationSuiteScope to define navigation items with icons, titles, and click actions.
  5. Implement the content area for your screens within the NavigationSuiteScaffold. (Optional) Customize navigation UI based on screen size for a more tailored experience.

Overall, NavigationSuiteScaffold streamlines navigation implementation in Jetpack Compose, ensuring a smooth user experience across various devices.

Design Focused Companies

Material Design and Apple’s HIG share a common ground in their core principles, aiming to create user-friendly and visually appealing interfaces. The implementation details might differ to align with the specific aesthetics and functionalities of each platform.

This table provides a high-level comparison of the two design systems, highlighting their unique approaches and common goals in creating user-friendly, aesthetically pleasing, and accessible interfaces.

Material and Apple HIG

User Interface —2. State Management

State management is a crucial aspect of building dynamic user interfaces. It involves handling the data that defines the current state of your app and ensuring the UI updates automatically when that data changes. Both Kotlin Compose and SwiftUI provide different approaches to state management. Let’s explore these strategies:

Compose — State Management

Just use a Flow from the ViewModel and not these …

remember

Purpose: remember is used to store a value in memory during the composition of a composable function. The value survives recompositions of the composable, but not configuration changes like device rotations.

Usage:

  • State Initialization: Typically used to initialize and retain state within a composable.
  • Performance Optimization: Helps avoid expensive calculations or initializations during recompositions.
@Composable
fun Counter() {
// Remember a mutable state that persists across recompositions
var count by remember { mutableStateOf(0) }

Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}

Key Points:

  • Values created with remember are not retained across configuration changes (e.g., screen rotations).
  • Should be used for values that are expensive to compute or need to persist during recompositions.

LaunchedEffect

Purpose: LaunchedEffect is used to launch a coroutine that runs within the scope of a composable. It's often used for side-effects like making network requests, reading from a database, or any asynchronous operation that needs to be tied to the lifecycle of a composable.

Usage:

  • Side-Effects: To perform one-time or continuous side-effects that need to be associated with the composition lifecycle.
  • Lifecycle Awareness: Automatically cancels the coroutine when the composable leaves the composition.
@Composable
fun Timer() {
var time by remember { mutableStateOf(0) }

// Launch a coroutine when this composable enters the composition
LaunchedEffect(Unit) {
while (true) {
delay(1000L) // Delay for 1 second
time++
}
}

Text("Time: $time seconds")
}

Key Points:

  • Takes a key as a parameter. When the key changes, the existing coroutine is canceled, and a new one is launched.
  • Commonly used for tasks like fetching data when a screen appears or reacting to state changes with asynchronous operations.
  • Ensures that the coroutine work is correctly scoped to the composable’s lifecycle, preventing memory leaks and ensuring proper cleanup.

Summary

remember:
-Stores a value across recompositions.
-Does not persist through configuration changes.
-Used for maintaining state within a single composition.

LaunchedEffect:
-Launches a coroutine tied to the composable’s lifecycle.
-Suitable for side-effects and asynchronous operations.
-Automatically handles cancellation and cleanup when the composable leaves the composition.

Best Practices

Use remember and LaunchedEffect for simple, composable-scoped state and side-effects that do not need to persist beyond the lifecycle of the composable.

Use ViewModel for complex state management, shared state across multiple composables, and when you need state to survive configuration changes.

By choosing the appropriate tool based on the needs of your app and the scope of your state, you can effectively manage state and side-effects in Jetpack Compose.

Better to move to ViewModel …

Counter Example:

// ViewModel
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count

fun increment() {
_count.value += 1
}
}

@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val count by viewModel.count.collectAsState()

Column {
Text("Count: $count")
Button(onClick = { viewModel.increment() }) {
Text("Increment")
}
}
}

Timer Example:

class TimerViewModel : ViewModel() {
private val _time = MutableStateFlow(0)
val time: StateFlow<Int> = _time

init {
viewModelScope.launch {
while (true) {
delay(1000L)
_time.value += 1
}
}
}
}

@Composable
fun TimerScreen(viewModel: TimerViewModel = viewModel()) {
val time by viewModel.time.collectAsState()

Text("Time: $time seconds")
}

To me … makes the Composable more reusable.

Swift — State Management

This simple graph show what to use when. This is all you need. Just follow this diagram and you are done!

Tweet by Chris Eidhof from objc.io

More info: but you do not need it

Update after iOS17.

New

  • @Environment can do everything @EnvironmentObject can do.
  • @State is the same as before.
  • @Bindable is replace @Binding
import SwiftUI
import SwiftData // @Observable is here!


@Observable
class Light {
var isOn: Bool = false
}

struct Room: View {
@Bindable var light: Light

var body: some View {
Toggle(isOn: $light.isOn) {
EmptyView()
}.fixedSize()
}
}

struct ContentView:View {
@State private var light: Light = Light()

var body: some View {
VStack {
Room(light: light)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(light.isOn ? .yellow: .black)
}
}

#Preview {
ContentView()
}

@Enviroment is easy to use … just have to just @Observable

import SwiftUI
import SwiftData // @Observable is here!


@Observable
class Light {
var isOn: Bool = false
}

struct LightView:View {
@Bindable var light:Light

var body: some View {
Toggle(isOn: $light.isOn) {
EmptyView()
}.fixedSize()
}
}

struct Room: View {
@Environment(Light.self) private var light

var body: some View {
LightView(light: light)
}
}

struct Cabin: View {
@Environment(Light.self) private var light


var body: some View {
Image(systemName: light.isOn ? "lightbulb.fill": "lightbulb")
.font(.largeTitle)
}
}


struct ContentView:View {
@Environment(Light.self) private var light

var body: some View {
VStack {
Room()
Cabin()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(light.isOn ? .yellow: .black)
}
}

#Preview {
ContentView()
.environment(Light())
}

Example taken from our friend Mohammad Azam …

azamsharp

Similarities State Management Compose & SwiftUI

  • Both Compose and SwiftUI utilize a declarative approach.
  • They both offer composable/view functions to define UI components.
  • Modifiers provide a way to customize UI properties.
  • Built-in layout components help structure the UI.

User Interface—3. Navigation

Jetpack Compose Navigation

Compose uses the NavHost component as the central navigation container. It manages a NavGraph that defines the available screens and their transitions.

  • Navigation Graph: Blueprint defining destinations and navigation paths. (Map)
  • NavHost: Special composable displaying current destination content. (Window)
  • NavController: Central controller managing navigation history and transitions. (Brain)
NavHost(navController = rememberNavController()) {
NavGraph(startDestination = "home") {
composable("home") { HomeScreen() }
composable("details") { DetailsScreen(argument = remember { /* extract argument */ }) }
}
}

New Type Safe Navigation

Here are the steps on how to set up type-safe navigation in Jetpack Compose:

Add the following dependencies to your project’s Gradle file:

  • navigation-compose
  • coil-ktx-serialization-json
  1. Define a sealed class for each screen in your app. Annotate the class with @Serializable.
  2. Define the arguments for each screen as properties inside the sealed class. You can also specify if the argument is mandatory or optional.
  3. Use the NavHost composable to define your navigation host.
  4. Set the start destination of your navigation host to a screen object.
  5. Use navigation to navigate between screens by creating an instance of the corresponding screen object and passing any necessary arguments.

Source:

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TypeSafeComposeNavigationTheme {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = ScreenA
) {
composable<ScreenA> {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = {
navController.navigate(ScreenB(
name = null,
age = 25
))
}) {
Text(text = "Go to screen B")
}
}
}
composable<ScreenB> {
val args = it.toRoute<ScreenB>()
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "${args.name}, ${args.age} years old")
}
}
}
}
}
}
}

@Serializable
object ScreenA

@Serializable
data class ScreenB(
val name: String?,
val age: Int
)

SwiftUI Navigation

SwiftUI uses a navigation stack for hierarchical navigation. You push new views onto the stack and pop them to navigate back.

  • Navigation Stacks: stacking plates metaphor for managing views.
  • NavigationLink: acts as triggers for navigation(like button).
  • Navigation State Management: keeps track of the current view and navigation history.
import SwiftUI

struct ContentView: View {
// Sample data
let items = ["Item 1", "Item 2", "Item 3"]

var body: some View {
NavigationView { // Top-level navigation view
List(items, id: \.self) { item in
NavigationLink(destination: DetailsView(item: item)) {
Text(item)
}
}
.navigationTitle("Items List") // Title for the navigation bar
}
}
}

struct DetailsView: View {
// Argument passed from ContentView
let item: String

var body: some View {
Text("Details for \(item)")
.navigationTitle(item) // Title for the navigation bar
}
}

@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

Comparison of Compose & SwiftUI

UI Similarities

Android Jetpack Compose and SwiftUI have more similarites than differences.

Declarative Approach:Both frameworks use a declarative programming model, allowing developers to describe what the UI should look like based on the current state.

Composable/View Functions:

  • Jetpack Compose: Uses @Composable functions to define UI components.
  • SwiftUI: Uses structs conforming to the View protocol to define UI components.

State Management:Both provide mechanisms to manage state and automatically update the UI when the state changes.

  • Jetpack Compose: Uses State, remember, and other state management tools.
  • SwiftUI: Uses @State, @Binding, @ObservedObject, and other property wrappers.

Modifiers:Modifiers are used to customize and style UI components, such as setting padding, background, alignment, etc.

  • Jetpack Compose: Uses Modifier to adjust layout and appearance.
  • SwiftUI: Uses ViewModifiers for similar purposes.

Layout Components:Both provide built-in layout components to help structure the UI.

  • Jetpack Compose: Components like Column, Row, Box.
  • SwiftUI: Components like VStack, HStack, ZStack.

Recomposition/Update:Both frameworks automatically handle UI updates when the underlying data changes, reducing the need for manual UI updates.

  • Jetpack Compose: Emphasizes recomposition, where composables are re-evaluated based on state changes.
  • SwiftUI: Uses a similar mechanism where views are updated based on state changes.

Hierarchical Layout: The layout process starts from the root view and propagates down to child views, creating a hierarchical structure.

Custom Layouts: Both frameworks allow for creating custom layouts by defining how children are measured and placed.

  • Jetpack Compose: Uses the Layout composable for custom arrangements.
  • SwiftUI: Uses GeometryReader and custom layout protocols.

Reactive Programming: Both leverage reactive programming principles, reacting to state changes and re-rendering the necessary parts of the UI.

Integration with Existing UI Frameworks:Both can be integrated with their respective platform’s traditional UI framework components.

  • Jetpack Compose: Can interoperate with existing Android Views.
  • SwiftUI: Can interoperate with existing UIKit/AppKit components.

Tooling Support: Both have strong tooling support for real-time previews and debugging within their respective IDEs.

  • Jetpack Compose: Integrated with Android Studio.
  • SwiftUI: Integrated with Xcode.

These similarities highlight the shared principles and design philosophies behind Jetpack Compose and SwiftUI, making them powerful tools for modern UI development on their respective platforms.

Development Environment

Android Studio

These two features are needed in XCode badly …

  • Visual Lint — Check Material Design.
Visual Lint — Check the code against Material Design
  • AI code completion

Without any prompt Android Studio knows what to code (letters in gray)

AI Generated Code in Gray.

Makes coding what it should be. No more memorizing the functions, just know which one you need.

AI Generated Code in Gray.
Android Studio & XCode

Project Structures

Please see the architecture section as the project structure flows from the architecture for each platform.

Android

The idea has been the same for some time. Arch used in 2019.

Slide from 2019

Modern Android App with layers.

We used to build a flat structure.

Single level app.

Android & iOS Architecture

We can make Android and iOS architecture completely mirror each other.

Android:

  • Kotlin (Coroutines, Flow / Channels)
  • Jetpack Compose / MaterialU (3)
  • Clean Architecture with MVI pattern
  • Hilt — for Dependency Injection pattern implementation
  • Room — for local database
  • Coil — for image loading
  • Ktor — for networking
  • Kotlin Serialization converter — for JSON parsing
  • Gradle Version Catalog [TOML] — for dependency management
  • Modularized project by features
  • Compose test APIs
  • GitHub Actions (CI/CD pipeline)

iOS:

  • Swift (with structured concurrency)
  • SwiftUI / Apple HIG
  • Clean Architecture with MVVM pattern
  • Swinject — Dependency Injection
  • CoreData — for local database
  • Combine Framework
  • Swift Package Manager — for dependency management
  • Modularized workspaces by features
  • XCTest Framework
  • GitHub Actions (CI/CD pipeline)

Everything is the same down to the file structure.

iOS source file structure is exactly the same as Android.

Android Architecture (By Google)

Building modern Android apps using Modern Android Development (MAD) principles. Key features include intuitive interfaces with Jetpack Compose and Material 3, reactive performance with Kotlin Coroutines and Flow, seamless data management with Ktor and Room, modular architecture with Clean Architecture and MV*, and modern tooling.

This article goes into aspects like naming conventions, project setup, architecture, file structure, state-driven interactions, ViewModels, and modern Android components.

Mutli Module Android Code. Now each screen is an isolated application.

Building modern Android apps using Modern Android Development (MAD) principles. Key features include intuitive interfaces with Jetpack Compose and Material 3, reactive performance with Kotlin Coroutines and Flow, seamless data management with Ktor and Room, modular architecture with Clean Architecture and MVI, and modern tooling.

This article goes into aspects like naming conventions, project setup, architecture, file structure, state-driven interactions, ViewModels, and modern Android components.

Android Programming Centers Around Hilt!

Managing dependencies in Android development can be cumbersome, often leading to boilerplate code and tight coupling between components. Here’s where Hilt shines! Hilt, an extension of Dagger 2, simplifies dependency injection (DI) in Kotlin/Compose applications. It automates dependency creation and management, resulting in cleaner code and improved maintainability. This guide explores key aspects of using Hilt with Compose, including ViewModel, Navigation, Room, and testing functionalities.

Hilt is the center of Android Development …

Hilt Functions

Hilt is the Swiss army knife of Android programming

Hilt does not support Kotlin Multiplatform (KMP)

iOS Architecture (By Apple)

SwiftUI

Currently the best way to build an Apple app is to make it flat with nothing but views and SwiftData.

AzamSharp
The Ultimate Guide to Building SwiftData Applications

If you are interested in further reading about SwiftUI architecture then I have written several articles on this topic. This includes Building Large-Scale Apps with SwiftUI: A Guide to Modular Architecture and Active Record Pattern for Building SwiftUI Apps with Core Data.

Active Record — involves representing database tables or views as classes, with instances of these classes corresponding to individual rows in the table. Each active record object encapsulates the database access and contains data as well as behavior related to that data.

Active Record Pattern & SwiftData*

  • Integration: Both Active Record and SwiftData integrate tightly with their respective frameworks.
  • Simplicity: Both aim to simplify database interactions by providing a straightforward interface.
  • Tight Coupling: Both patterns can result in tight coupling between the database schema and the application logic.

*Please see the Database section above for more details

This forces a “flat architecture” as seen in Apple examples.

Completely Flat — No Data Layer, No Rep, No UseCases, No ViewModel!

We are waiting for WWDC 2024 to see the full evolution of iOS architecture with SwiftData.

Full Comparison of Arch.

Please tell us if you want anything added to the tables or any errors. We will do our best to keep the tables as up-to-date as possible.

Section 3: Polishing & Deployment

  • App Functionality: Explore a range of frameworks to enhance your app’s capabilities in various domains like payments, sensors, and AR/VR, AI/ML.
  • Testing: Learn best practices for testing your app with Hilt Test (Android) and XCTest (iOS).
  • Deployment: Learn the ropes of publishing your app on the Google Play Store and Apple App Store.

~Ash

--

--