Notice: _filter_block_template_part_area(): "sidebar" is not a supported wp_template_part area value and has been added as "uncategorized". in /home/ntsnews/public_html/wp-includes/functions.php on line 6131

Notice: _filter_block_template_part_area(): "sidebar" is not a supported wp_template_part area value and has been added as "uncategorized". in /home/ntsnews/public_html/wp-includes/functions.php on line 6131
Article: Borrowing from Kotlin/Android to Architect Scala... - NTS News

Article: Borrowing from Kotlin/Android to Architect Scala…

Article: Borrowing from Kotlin/Android to Architect Scala…

Building iOS apps can feel like stitching together guidance from blog posts and Apple samples, which are rarely representative of how production architectures grow and survive. In contrast, the Kotlin/Android ecosystem has converged on well-documented, real-w…

For us iOS developers, it’s often hard to create scalable architecture out of simple one-page example apps from Apple. Sure it works for a simple app, but I have always struggled with what to do next when you want to build something scalable. After looking around, I discovered the Android world. I was surprised by what Google provides for developers compared to Apple. Android developers have clear guides and patterns, and most importantly, real-world examples that show how to structure production apps and not just toy projects.

In comparison, iOS developers are often left piecing together solutions from blog posts and Apple’s sample apps. These solutions are useful in isolation, but rarely represent how a real-world app’s architecture evolves, leaving us hoping our architecture doesn’t collapse as the app grows. But here is the encouraging thing: Good architecture is platform agnostic. The principles that make Android apps maintainable work just as well on iOS.

This article explores how iOS apps can be built using architecture patterns inspired by modern Kotlin and Android development. It demonstrates how these patterns translate to Swift and SwiftUI. We will start with a fundamental problem: managing state inside a view. This problem includes enforcing a single entry point for mutations and enabling cross-cutting concerns such as logging and debugging. Next, we will move one layer up and separate the view from its view model to improve reusability, testability, and previewability.

Finally, we will introduce an active repository layer to bring the concept of a single source of truth to life and show how data can automatically propagate across the app. This code works for a simple screen, but consider what happens as the ViewModel grows. Which state should the UI show? The compiler doesn’t help you here. Developers make different choices and bugs are created. You will add more methods such as loadMore(), then refresh(), then deleteWorkout(), filterWorkout(), and selectWorkout().

Now you have multiple methods all mutating state in their own way. Want to log every state change? Add logging to multiple places. Want to debug to determine why isLoading is stuck on true? Set breakpoints in ten places. Want to write a test? Figure out which combination of method calls reproduces the user flow. There is no central place where things happen. The ViewModel is a bunch of methods and you are left to remember how they interact.

Imagine you are working on a feature. It touches a ViewModel you haven’t seen in six months, or a new one that you have never seen. You open the file and it has six hundred lines and twenty methods. What does the thing do? Which methods are called from the view and which ones are internal helpers? You will have to read the whole class to understand it. There is no summary, no contract, no list of "here’s what this ViewModel can do".

Now multiply that by 100 other ViewModels. The state is defined by a single source of truth. Its type makes the possible states mutually exclusive, and the compiler enforces this. Being in both Loading and Success at the same time is impossible. Explicit state prevents contradictory states, but what about the problem of multiple mutating methods? Kotlin’s answer is to funnel everything through a single entry point: Every mutation flows through onAction(),t not just some mutations, but all of them.

This is a complete list of every action in the ViewModel. A new engineer can open this file, read the class and immediately understand the ViewModel’s capabilities. No scrolling through six hundred lines of code, no guessing which methods are public, no wondering if this is called from the View or if it is internal only. The sealed class is the contract. If an action isn’t declared there, the ViewModel can’t execute it.

This policy also forces you to think about your ViewModel’s responsibilities. When you add a new action, you add it to the sealed class first. It’s a conscious decision and not a method that just quietly appears somewhere in the file. But what exactly goes in DashboardAction? If the View can trigger an action, it should be declared as an action. Does the user tap to delete an item? Does the user select an item?

What stays out? Internal helpers, such as loadWorkouts(), is called only from inside perform(). It is a private method, not an Action. The Action is .refresh. What happens internally is an implementation detail. If you have been writing iOS apps for some years, this pattern feels unnecessary. Why funnel everything through one method when you can just call that method directly. Well, the answer is that it doesn’t matter when the team is small and you have just three to five screens.

It does matter, however, when the team and the codebase grow. Traditional iOS patterns optimize for the simple case, such as using @StateObject, @Published, and call methods directly. This approach is easy to understand and quick to write. Apple's sample code works this way because the code samples are small. But when scaling up, those direct method calls are problematic. Every method is a potential entry point.

Every entry point is a place where state can change. The more entry points, the harder it is to reason about your ViewModel. Centralizing actions enables things that are genuinely hard to manage otherwise when the codebase grows, including logging, debugging, testing, and analytics. One line in the base class, and you see every action across every ViewModel. It is not necessary to add print statements to multiple methods.

If the state is wrong, then you need to set just one breakpoint in perform(). You will see the exact sequence of actions that led to the current state. Compare this approach to setting breakpoints in ten different methods. Tests become readable. Because every action goes through the same path, you are testing the same code path the real app uses. This function isn't something new. It is a standard practice in Android development and is recommended in Google's official architecture guide.

Android developers call it unidirectional data flow Events flow downward (View → ViewModel → Repository) and state flows upward (Repository → ViewModel → View). The onAction() method is the entry point for the downward flow. Google's "Now in Android" sample app uses this pattern. So does most of the Kotlin community. When an Android developer joins a new project, they expect to find an Action enum and an onAction() method.

The state is readable from anywhere, but only writable from inside the ViewModel. This approach enforces unidirectional data flow where Views can read state, but they can't mutate it directly. All changes go through perform(). You could define this approach as an extension, but a base class gives you a place to put shared logic such as logging, analytics, and common state update patterns. Every ViewModel inherits that behavior.

The Action enum is the public contract. The private methods are implementation details. Looking at this file, you immediately know what it does. We've solved state management and action routing, but there is another problem: tight coupling. Views own their ViewModels, which breaks Previews and limits reusability. This View is doing two jobs. Owning the ViewModel (creating it, holding a reference, and observing changes) and rendering UI (laying out views and handling the switch statement).

The View creates a real ViewModel. The ViewModel might hit the network. It might require dependencies that don't exist in the preview context. It might crash. But now you either need a mock ViewModel, you have changed the initializer, and mock ViewModels are tedious to maintain, or you give up on previews entirely. Many iOS developers do give up on previews. Previews become that feature you tried once, couldn't get working reliably, and abandoned.

Say you want to show the same workout list in two places, the dashboard and a search results screen. With the current structure, you can't reuse DashboardView because it creates its own DashboardViewModel. Now you have DashboardView, WorkoutList, and WorkoutListContainer. The extraction happened without a clear principle. Another developer looking at this won't know which pattern to follow. In the Now in Android app example, there is a standard pattern: separate screen from content.

The screen is a wrapper that owns the ViewModel, while the content is a composable that just renders the UI. We can apply the same separation in SwiftUI. First, the Content, a View that takes state and an action handler: Notice that there is no @ObservedObject, no @StateObject, and no ViewModel reference, just data in, UI out. The screen observes the ViewModel and passes state down. Content doesn't know ViewModels exist.

We have covered individual patterns. Now let's see how they combine into a complete system made of distinct layers. Each layer only knows about the layer below it. The View doesn't know Repositories exist. The Repository doesn't know Views exist. Dependencies point one way. Why so many layers? Because each one can be tested, mocked, and replaced independently. Swap the RemoteSource for a fake source, and your Repository works offline.

Swap the Repository for a mock, and your ViewModel tests don't hit the network. Imagine this scenario. Your app has two screens: a workout list and a workout detail. The user opens a workout, edits the name, goes back to the list. The list still shows the old name. Why? Because each screen has its own copy of the data. The detail screen modified its copy. The list screen has no idea anything has changed.

SwiftUI's @Binding solves this for simple parent-child relationships. You could also pass a callback, post a notification, or refresh in onAppear. But once you have three screens, or independent features needing the same data, none of these scale. You need a single source of truth. What if there was only one copy of the data? Every screen observes that single copy. Update it once, everyone sees the change.

Both ViewModels observe the same source. When the repository updates, both receive the new data. No callbacks, no notifications, no manual refreshing. And for testing just inject the mock. Google's architecture guide calls this the "single source of truth" principle. For any piece of data, there's exactly one owner. Everyone else observes. ViewModels don't own data. They observe it and expose it to Views.

When they need to change something, they ask the repository. The repository updates its state, and the change propagates to everyone observing. One update provides an automatic propagation. If you want a third screen that shows workouts, it just subscribes to the repository. No code changes are required anywhere else. This is the pattern that makes large apps manageable. Without it, you're playing Whac-A-Mole with stale data across dozens of screens.

Good architecture transcends platforms. By adopting patterns proven in the Android ecosystem, including explicit state management, action based updates, the screen pattern, layered data flow, and reactive repositories, we can build iOS apps that are: We do not need to reinvent the wheel. We can learn from what works elsewhere and adapt it to our platform. The result is cleaner code and apps that don’t collapse under their own weight as they grow.

A round-up of last week’s content on InfoQ sent out every Tuesday. Join a community of over 250,000 senior developers. View an example

Summary

This report covers the latest developments in android. The information presented highlights key changes and updates that are relevant to those following this topic.


Original Source: InfoQ.com | Author: Ivan Bliznyuk | Published: February 26, 2026, 9:00 am

Leave a Reply