You are currently viewing Ditch the How, Embrace the What: A Dive into Shiprocket’s Declarative UI

Ditch the How, Embrace the What: A Dive into Shiprocket’s Declarative UI

  • By Bhupendra Rawat
  • Post category:Engineering
  • Reading time:6 mins read

Developing and maintaining complex UI components can be a challenging task, especially in imperative programming. At Shiprocket, we faced this challenge when revamping our logistics platform mobile app’s complex UI components, from our frequently changing onboarding UI’s to complex UI components like order tracking interfaces. We meticulously positioned views, handled clicks, and managed state, often ending up with codes that were complex and difficult to maintain. But what if there’s a better way?

Declarative UI, a powerful paradigm that’s transforming how we build user interfaces. Frameworks like Jetpack Compose (Android), Flutter, and Swift UI (iOS) are championing this approach, offering a breath of fresh air and a more intuitive way to create dynamic and engaging user experiences.

Smarter UI, Seamless Experience

What is Declarative UI?

Imagine describing your dream house to an architect. You wouldn’t tell them how to build it brick by brick. Instead, you’d describe what you want: “a spacious living room with large windows, a modern kitchen with an island, and a cozy fireplace.” That’s the essence of declarative UI.

Instead of dictating every step of UI construction, you simply declare the desired state of your UI. You describe what you want your interface to look like, and the framework takes care of the heavy lifting, figuring out the most efficient way to bring your vision to life.

Why use Declarative UI?

This shift in thinking brings a host of benefits:

  • Improved performance: Declarative frameworks often employ intelligent rendering techniques to optimise performance. They can efficiently update the UI by only re-rendering the components that have changed, leading to smoother animations and a more responsive user experience.
  • Conciseness and readability: Declarative code is inherently more concise and readable. You express your UI in a clear, descriptive manner, making it easier to understand, maintain, and collaborate on.
  • Simplified State Management: State management is a crucial aspect of UI programming. Declarative frameworks often provide elegant mechanisms to manage and update UI state, reducing complexity and making your code more predictable.
  • Enhanced Developer Productivity: With less boilerplate code and a more intuitive approach, you can focus on building features and delivering value to your users, rather than getting bogged down in implementation details.

Imperative vs declarative: a practical example

Some of the examples of comparing declarative UI in the Shiprocket codebase is the OnBoarding screen composition:

Imperative Approach (Conceptual)

  1. Get references to the button and text view
  2. Set an onClickListener on the button
  3. Inside the listener:
    • Increment the counter variable
    • Update the text view with the new count 

Code: 

    // make visibility –  invisible instead of gone, just to measure height
        mWelcomeContentBox?.let {
            recursivelyChangeVisibility(it, View.INVISIBLE)
        }
        // make visibility –  invisible instead of gone, just to measure height
        mContentBox?.let {
            recursivelyChangeVisibility(it, View.INVISIBLE)
        }
        if (titleTv != null) {
            titleTv?.text = textToShow
        }
        if (counterTv != null) {
            counterTv?.text = counterText
        }
        if (contentTv != null) {
            contentTv?.text = textToShow2 ?: “”
        }

Declarative Approach (Conceptual)

  1. Define a variable to hold the counter state
  2. Describe the UI:
    • A Text view displaying the counter value
    • A Button that, when clicked, updates the counter state

Notice how the declarative approach focuses on what the UI should look like and how it should behave, while the imperative approach delves into the how.

Code:

 setContent {
            navController = rememberNavController()
            currentStep = remember { mutableIntStateOf(1) }
            sheetVisible = remember { mutableStateOf(false) }
            var stepDone by remember { mutableStateOf(true) }
            NewOnboardingChangesTheme {
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(WindowInsets.systemBars.asPaddingValues())
                ) {
                    OnBoardingScreen(
                        this@OnboardingActivity,
                        navController,
                        currentStep,
                        sheetVisible
                    )
                }
            }

instead of imperatively managing views, the UI is declared using composable functions. The setContent block describes what the screen should look like, using nested components that clearly express the hierarchy and structure.

Button Creation: A Cross-Platform Comparison

To illustrate this further, let’s see how we create a simple button in Jetpack Compose, Flutter, and SwiftUI:

Jetpack Compose (Kotlin)

Button(onClick = { /* Action to perform */ }) {
   Text(“Click me!”)
}

Flutter (Dart)

ElevatedButton(
 onPressed: () { /* Action to perform */ },
 child: Text(‘Click me!’),
)

SwiftUI (Swift)

Button(“Click me!”) {
   // Action to perform
}

As you can see, the syntax and structure are remarkably similar across these frameworks. They all emphasize a declarative approach, focusing on describing the button’s appearance and behavior.

Advantages and Disadvantages at a Glance

https://gist.github.com/bhavya-shiprocket/41ef22111eaeb7f74a28082f1eb37392

Advantages and Disadvantages at a Glance

Embracing Change: Transitioning to Declarative UI

The allure of declarative UI is undeniable. Its conciseness, performance benefits, and streamlined state management are attracting developers in droves. But what if you’re staring at a legacy codebase, years of imperative code powering a live app? In that case, transitioning to a declarative approach doesn’t have to be an all-or-nothing leap.

Gradual adoption: a practical approach

Switching to a declarative UI doesn’t necessitate rewriting your entire app overnight. Here’s a more manageable strategy:

  1. Start Small: Identify self-contained sections of your UI that are good candidates for a declarative rewrite. These could be new features, isolated screens, or UI components that are particularly complex or prone to bugs in their imperative form.
  2. Compose Within Imperative: Modern frameworks like Jetpack Compose allow you to integrate declarative components within your existing imperative layouts seamlessly. This enables you to gradually introduce declarative code without disrupting your entire app.
  3. Iterative Migration: Treat the transition as an ongoing process. Refactor and migrate sections of your UI incrementally, allowing you to learn and adapt along the way.

Practical Example: A Hybrid Approach

Imagine you have an existing Android app with an imperative RecyclerView displaying a list of items. You want to introduce a new feature that involves a more complex, dynamic UI within each list item. This is an ideal scenario for a hybrid approach:

  • Keep the RecyclerView: Retain the existing RecyclerView for its core functionality of efficiently displaying a list.
  • Compose Individual Items: Create a Jetpack Compose function to define the UI for each list item. This allows you to leverage the power and flexibility of Compose for the more dynamic aspects of the item’s UI.
  • Integrate with ComposeView: Use the ComposeView element to embed your Compose UI within each RecyclerView item. This seamlessly bridges the imperative and declarative worlds.

Embracing the Declarative Future

Declarative UI is not just a passing trend; it represents a fundamental shift in how we think about and build user interfaces. By adopting this approach, you can write cleaner, more maintainable code, achieve better performance, and ultimately create more engaging and delightful experiences for your users.

Whether you’re building a complex logistics platform like Shiprocket or any other modern application, the benefits of declarative UI are clear. The initial investment in learning and migration pays dividends in code quality, developer productivity, and user experience. So, take the leap and explore the world of declarative UI — your future self (and your users) will thank you!