DEV Community

János Kukoda
János Kukoda

Posted on • Edited on

The Declarative Revolution: React, SwiftUI, and Jetpack Compose

Traditionally, imperative programming required developers to specify every step in building and updating a UI. Declarative frameworks, on the other hand, allow developers to define what the UI should look like for a given state, while the framework takes care of rendering and updates. Mobile development has taken a significant leap forward with the rise of declarative UI frameworks. SwiftUI for iOS, Jetpack Compose for Android, and React for web development allow developers to efficiently build responsive user interfaces. Unlike cross-platform tools like React Native, these frameworks retain the full power of their respective platforms while adopting a declarative approach. This article explores how to harness the strengths of SwiftUI, Jetpack Compose, and React for parallel development across iOS, Android, and the web.

1. Preparing Data Bundles for Mobile Development

When developing mobile applications, it’s common to include assets such as JSON data, images, or other resources locally. Here’s how to prepare and access bundled data in iOS (SwiftUI) and Android (Jetpack Compose).

Comparison:

SwiftUI: Accessing Bundled Data

if let url = Bundle.main.url(forResource: "Books", withExtension: "json"),
   let data = try? Data(contentsOf: url) {
    let jsonString = String(data: data, encoding: .utf8)
    print(jsonString ?? "No data")
}
Enter fullscreen mode Exit fullscreen mode

Jetpack Compose: Accessing Bundled Data

val context: Context = LocalContext.current
val json = context.assets.open("Books.json").bufferedReader().use { it.readText() }
println(json)
Enter fullscreen mode Exit fullscreen mode

2. Persistent Storage Solutions

Persistent storage is crucial for maintaining user preferences and lightweight data across sessions.

Comparison:

SwiftUI - UserDefaults

UserDefaults.standard.set("Sample Book", forKey: "LastOpenedBook")
let lastOpenedBook = UserDefaults.standard.string(forKey: "LastOpenedBook")
Enter fullscreen mode Exit fullscreen mode

Jetpack Compose - SharedPreferences

sharedPreferences.edit().putString("LastOpenedBook", "Sample Book").apply()
val lastOpenedBook = sharedPreferences.getString("LastOpenedBook", null)
Enter fullscreen mode Exit fullscreen mode

React - localStorage

localStorage.setItem("LastOpenedBook", "Sample Book");
const lastOpenedBook = localStorage.getItem("LastOpenedBook");
Enter fullscreen mode Exit fullscreen mode

3. Layout Structures

Declarative frameworks simplify UI composition with flexible layout components:

  • SwiftUI: VStack and HStack for vertical and horizontal layout.
  • Jetpack Compose: Column and Row, functioning similarly to Flexbox.
  • React: div elements styled with display: flex or grid.

4. Data Passing Between Components

React Approach

Data is passed using props. The parent can pass state and setter functions for the child to modify state.

Example:

// Parent Component
function ParentComponent() {
  const [message, setMessage] = useState("Hello from Parent");
  return <ChildComponent message={message} setMessage={setMessage} />;
}

// Child Component
function ChildComponent({ message, setMessage }) {
  return (
    <div>
      <p>{message}</p>
      <button onClick={() => setMessage("Updated by Child")}>Update Message</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

SwiftUI Approach

If the child component does not need to modify the parent’s state, a standard parameter can be passed:

// Parent View
struct ParentView: View {
    @State private var message = "Hello from Parent"
    var body: some View {
        ChildView(message: message)
    }
}

// Child View
struct ChildView: View {
    var message: String
    var body: some View {
        VStack {
            Text(message)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If the child component needs to modify the parent’s state, use @Binding:

// Parent View
struct ParentView: View {
    @State private var message = "Hello from Parent"
    var body: some View {
        ChildView(message: $message)
    }
}

// Child View
struct ChildView: View {
    @Binding var message: String
    var body: some View {
        VStack {
            Text(message)
            Button("Update Message") {
                message = "Updated by Child"
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Jetpack Compose Approach

State is passed via function arguments, with lambda functions for state modification.

Example:

// Parent Composable
@Composable
fun ParentComponent() {
    var message by remember { mutableStateOf("Hello from Parent") }
    ChildComponent(message = message, onMessageChange = { newMessage -> message = newMessage })
}

// Child Composable
@Composable
fun ChildComponent(message: String, onMessageChange: (String) -> Unit) {
    Column {
        Text(text = message)
        Button(onClick = { onMessageChange("Updated by Child") }) {
            Text("Update Message")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Sharing State Across Components

React: useContext

Provides a way to pass state throughout the component tree without prop drilling.

Example:

const MessageContext = React.createContext();

function ParentComponent() {
  const [message, setMessage] = useState("Hello from Context");
  return (
    <MessageContext.Provider value={{ message, setMessage }}>
      <DeepChildComponent />
    </MessageContext.Provider>
  );
}

function DeepChildComponent() {
  const { message, setMessage } = useContext(MessageContext);
  return (
    <div>
      <p>{message}</p>
      <button onClick={() => setMessage("Updated by Deep Child")}>Update Message</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

SwiftUI: @EnvironmentObject

Used for sharing data across views.

Example:

class SharedDataModel: ObservableObject {
    @Published var message = "Hello from Environment"
}

struct ParentView: View {
    @StateObject var sharedData = SharedDataModel()
    var body: some View {
        DeepChildView().environmentObject(sharedData)
    }
}

struct DeepChildView: View {
    @EnvironmentObject var sharedData: SharedDataModel
    var body: some View {
        VStack {
            Text(sharedData.message)
            Button("Update Message") {
                sharedData.message = "Updated by Deep Child"
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Jetpack Compose: CompositionLocalProvider

Works like React’s Context API for providing global state.

Example:

val LocalMessage = compositionLocalOf { "Default Message" }

@Composable
fun ParentComponent() {
    var message by remember { mutableStateOf("Hello from CompositionLocal") }
    CompositionLocalProvider(LocalMessage provides message) {
        DeepChildComponent()
    }
}

@Composable
fun DeepChildComponent() {
    val message = LocalMessage.current
    Column {
        Text(text = message)
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The shift to declarative programming in SwiftUI, Jetpack Compose, and React has redefined UI development. Each framework simplifies building modern, responsive UIs. While choosing between cross-platform and native solutions or experimenting with transpilers like Skip, it’s clear that technologies such as ChatGPT can bridge coding across languages, making multi-platform development more feasible.

Top comments (0)