Let's Work Together
March 27, 2025 · 7 min read

From Jetpack Compose to React Native: An Android Developer's Perspective

A native developer's first look at React Native

Author Amanda Scheetz
From Jetpack Compose to React Native: An Android Developer's Perspective

As someone deeply familiar with Android’s Jetpack Compose (and somewhat familiar with SwiftUI), exploring React Native with Expo has been quite the experience. All three frameworks share a declarative UI pattern, which surprisingly made the transition to React Native much easier than expected. After spending some time creating a multi-platform app, there are a few bigger differences (and similarities) that I noticed coming from a Compose background.


UI Components: Familiar Territory

Creating reusable UI components is an important part of maintaining a large app and consistency in design. Luckily React Native’s component structure is familiar to the work of Compose and SwiftUI.

Jetpack Compose

In Compose, we might write a reusable Composable that applies two texts into a column on a framework supplied Card Composable

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Composable
fun MealCard(
  title: String,
  description: String,
  modifier: Modifier = Modifier
  ) {
    Card(
      modifier = modifier.fillMaxWidth()
    ) {
      Column {
        Text(text = title)
        Text(text = description)
      }
    }
}



SwiftUI

Utilizing SwiftUI, a similar Card view would have to be custom created in order to match the Jetpack Compose version. This can be done a few ways, but I’ve included an example of using a custom modifier (with the help of the expert iOS developers at Atomic Robot)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct MealCard: View {
    let title: String
    let description: String

    var body: some View {
        VStack(alignment: .leading) {
            Text(title)
            Text(description)
        }
        .cardStyle()
        .frame(maxWidth: .infinity)
    }
}

extension View {
    func cardStyle() -> some View {
        self
            .modifier(CardModifier())
    }
}

struct CardModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color(.systemBackground))
            .cornerRadius(10)
            .shadow(radius: 5)
    }
}



React Native

Creating a similar cross-platform view in a React Native project, the pattern is remarkably similar yet has a hint of web-like tags. I struggled most here with Typescript as it is also my first experience with this language.

1
2
3
4
5
6
7
8
function MealCard({ title, description }: Props) {
  return (
    <View style={{ flexDirection: "column" }}>
      <Text>{title}</Text>
      <Text>{description}</Text>
    </View>
  );
}




State Management: Different Yet Not Far Off

While each framework has its own syntax and conventions, the underlying concepts remain similar. React Native’s approach with hooks provides a clean, functional way to manage component state that will feel familiar to developers coming from either SwiftUI or Jetpack Compose.

They’re all built upon the same fundamental concept: declarative UI updates driven by state changes.

The following example of an expandable component is a great real world implementation of state management.

Jetpack Compose

Android Compose utilizes remember along with a type of mutableState. Mutable state types allow for direct assignment of the state value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    @Composable
    fun ExpandableCard() {
        var isExpanded by remember { mutableStateOf(false) }

        Column {
            Button(onClick = { isExpanded = !isExpanded }) {
                Text(if (isExpanded) "Show Less" else "Show More")
            }

            AnimatedVisibility(visible = isExpanded) {
                Text("Additional content here")
            }
        }
    }



SwiftUI

Swift uses a @State property wrapper. This allows for direct mutation of a boolean state like isExpandedwith .toggle()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    struct ExpandableCard: View {
        @State private var isExpanded = false

        var body: some View {
            VStack {
                Button(action: { isExpanded.toggle() }) {
                    Text(isExpanded ? "Show Less" : "Show More")
                }

                if isExpanded {
                    Text("Additional content here")
                        .transition(.opacity)
                }
            }
            .animation(.default, value: isExpanded)
        }
    }



React Native

So coming from the previous two frameworks, the React Native approach isn’t that far off from the familiar. It has a concept of a useState hook that can be applied to a value variable along with a setter function. The setter function is then used to update the state properly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function ExpandableCard() {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <View>
      <TouchableOpacity onPress={() => setIsExpanded(!isExpanded)}>
        <Text>{isExpanded ? "Show Less" : "Show More"}</Text>
      </TouchableOpacity>

      {isExpanded && (
        <Animated.View>
          <Text>Additional content here</Text>
        </Animated.View>
      )}
    </View>
  );
}






One of the biggest changes that I had to wrap my head around with React Native was navigation. While they're fundamentally similar, React Native Expo specifically utilizes file based routing.



Jetpack Compose Navigation

Compose’s NavHost and Navigation Controller is heavily integrated with Android’s navigation component, offering a type-safe way to handle navigation:

1
2
3
4
5
6
7
NavHost(navController, startDestination = "home") {
    composable("home") { HomeScreen() }
    composable("details/{id}") { backStackEntry ->
        val id = backStackEntry.arguments?.getString("id")
        DetailsScreen(id)
    }
}



SwiftUI Navigation

SwiftUI utilizes NavigationStack and NavigationLink to setup relationships between screens within the app.

1
2
3
4
5
6
7
8
NavigationStack {
    List {
        NavigationLink("Details") {
            DetailView()
        }
    }
    .navigationTitle("Home")
}



React Native with Expo Router

Expo Router introduces a file-system based routing approach that might feel unfamiliar at first but becomes intuitive quickly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// app/index.tsx (home screen)
function HomeScreen() {
  const router = useRouter();

  return (
    <View>
      <TouchableOpacity onPress={() => router.push("/details/123")}>
        <Text>Go to Details</Text>
      </TouchableOpacity>
    </View>
  );
}

// app/details/[id].tsx (dynamic route)
function DetailsScreen() {
  const { id } = useLocalSearchParams();
  return <Text>Details for {id}</Text>;
}



The key differences I noticed:

  1. File-Based Routing: Expo Router’s file-system based routing feels more web-like compared to Compose’s programmatic route definitions. Instead of explicitly defining routes, the file structure itself defines the navigation hierarchy.

  2. Type Safety: While Compose Navigation provides compile-time safety for route arguments, Expo Router requires additional type definitions and runtime checks. However, TypeScript helps bridge this gap significantly.

  3. Navigation State: React Native’s navigation state management feels more explicit with hooks like useRouter() and useLocalSearchParams(), whereas Compose’s NavController and SwiftUI’s navigation state are more tightly integrated into the framework.




Development Experience

Across all three platforms, there are some noticeable similarities and differences:

  1. Hot Reload: Both Compose and React Native excel here, though Expo’s implementation feels more stable at times and is more responsive.
  2. Preview: Compose’s @Preview annotation and SwiftUI’s Previews are missed, but Expo’s live reload partially makes up for it. I don’t feel like I’m missing the quick development of components because of how quickly the reloads occur. It does require a change of mindset in implementing a UI component somewhere in the app so that the changes are visible.
  3. Component Isolation: Android, iOS, and React Native frameworks encourage building and testing components in isolation.




Conclusion

As an Android developer, the transition to React Native from Jetpack Compose (and SwiftUI) feels more natural than it might for developers coming from traditional Android XML layouts. The declarative nature of both frameworks, component-based architecture, and state management patterns share enough similarities that it’s easy enough to make that mental swap.

The main adjustments I experienced were:

  • Learning JavaScript/TypeScript languages and their ecosystem
  • Adapting to React’s hooks system and the file based navigation
  • Understanding cross-platform considerations and how to make updates that are platform dependent
  • Working with different styling paradigms

However, the mental model of building UIs remains surprisingly similar. If you’re comfortable with Compose (and even SwiftUI or other declarative UI patterns), you’ll feel right at home in React Native. It just requires embracing a new language and ecosystem.

Looking to bridge the gap between native and React Native development? Atomic Robot’s team of native platform experts can help guide your cross-platform strategy while maintaining the polish and performance your users expect. Reach out to us at Atomic Robot — we’d be happy to help.