The distribution company from the previous articles now has three applications. Managers work in a web application, with dense tables and reports. Warehouse operators use a desktop application connected to the scale and label printer. Field agents have a mobile application that works even without a signal. Three applications, three codebases, and every new feature is implemented three times.
The general manager read somewhere that there are frameworks that work everywhere: "We make a single application for everyone and finish with three teams." The promise is old. Write once, run everywhere was Java's slogan in the '90s, then Silverlight's, then Xamarin's. Each time, it worked partially. And the part that didn't work was almost always the same: the interface.
This is the last article in the series about choosing UI technology. After web, native desktop in .NET, desktop with web technologies, and mobile, we try to answer the hardest question: how much code can you actually share between web, desktop, and mobile?
Code does not share evenly: the six layers
The question "how much code can I share?" has no single answer because an application is not a uniform block. It is made of layers, and each layer shares with different effort. From easiest to hardest:
- Contracts and models — DTOs, enums, validation rules. They share almost for free and should always be shared, including with the backend.
- Business logic and data access — calculations, rules, API client, local cache, offline synchronization. They share easily, provided they do not depend on a specific platform.
- Interface state — ViewModels, state management, screen logic. Shares well between frameworks from the same family, as seen in the desktop article.
- Interface components — buttons, forms, cards, lists. Can be shared but only when the UI framework is the same on all platforms.
- Layout and navigation — how screens are arranged and how you move from one to another. This is usually where the promise breaks because a phone and a 27-inch monitor require different screens.
- Platform integration — camera, scale, notifications, files. Does not share. It is hidden behind interfaces, with one implementation per platform.
The conclusion is more important than any framework: most of the value of sharing lies in the first three layers. That's where the business rules live, meaning exactly the code you don't want to write and debug three times. And these layers can be shared with almost any UI technology, including three different technologies.
Why the interface is the hardest to share
It is not a technical problem but a usability one. The manager at headquarters wants a table with 12 columns, filters, and Excel export, on a large monitor, with mouse and keyboard. The store agent wants a single big button, "New Order," pressed with the thumb, on a phone held in one hand. The two screens work with the same data but are not the same screen, and they shouldn't be.
Added to this are the conventions of each platform. The back button on Android, the swipe back gesture on iOS, the menu bar on macOS, keyboard shortcuts on desktop, hover that doesn't exist on touch screens. A completely common interface easily ends up at the lowest common denominator: it works everywhere but doesn't feel good anywhere.
Therefore, the practical rule is: the same code does not mean the same screen. Share components, not necessarily pages.
Three sharing strategies
1. Common logic, native interface on each platform
Layers 1–3 are common, and each platform has its own interface, in its native technology. In the .NET world, this means common class libraries used by a Blazor Web App, a WPF application, and a MAUI application. In the Kotlin world, it means Kotlin Multiplatform with SwiftUI on iOS. You gain the best experience on each platform and pay multiple times for the interface.
2. Common interface, drawn by the framework
Flutter, Avalonia, Uno Platform, and Compose Multiplatform draw the interface themselves, so the same interface runs everywhere, including in the browser. You gain a single codebase for almost everything. You pay with a less native look and the web variant: in the browser, these frameworks draw on a canvas, with an initial package of several MB and no HTML that search engines can index. For screens behind a login, this is acceptable. For a public site, for reasons discussed in the first article, it is not.
3. Web-first, packaged for desktop and mobile
The interface is a web application, and desktop and mobile run it inside a WebView: Blazor with Blazor Hybrid in WPF and MAUI, or React with Tauri or Capacitor. You gain the best web variant and a high reuse of components. You pay with a less native mobile experience and dependency on WebView.
None is generally correct. And in practice, many successful products combine strategies: common logic everywhere, common components where they make sense, and dedicated screens where users are very different.
Frameworks compared on the same scenario
Here is how the main options position themselves for our scenario, with web, desktop, and mobile:
| Technology | Platforms | Interface | What remains to be solved |
|---|---|---|---|
| .NET MAUI | Android, iOS, Windows, macOS | Native controls | No Linux and no native web |
| MAUI + Blazor Hybrid + Blazor Web | All except Linux | HTML/Razor in WebView, plus real web | Less native look on mobile |
| Avalonia | Windows, macOS, Linux, iOS, Android, web (WASM) | Own drawing (Skia) | Web on canvas, unsuitable for SEO |
| Uno Platform | Windows, macOS, Linux, iOS, Android, web (WASM) | WinUI API, drawn via Skia | Web on canvas, WinUI API coverage |
| Flutter | iOS, Android, Windows, macOS, Linux, web | Own drawing (Impeller/Skia) | Web on canvas, Dart for the team |
| Compose Multiplatform | Android, iOS, desktop (JVM), web (Beta) | Own drawing | Web still in Beta, growing ecosystem |
| React Native | iOS, Android; web via Expo; Windows and macOS via Microsoft projects | Native controls | Web and desktop less mature than mobile |
| Web + Tauri or Capacitor | Web, desktop, mobile | HTML in WebView | Differences between WebViews, native access via plugins |
An important detail: "works on the web too" does not mean the same thing everywhere. Blazor or React generate real web pages, with HTML, native browser accessibility, and SEO. Flutter, Avalonia, Uno, and Compose run an application that draws in the browser. Both variants are legitimate but for different purposes.
How it looks in .NET: solution structure
For a .NET team, the most pragmatic strategy in our scenario is usually the third, combined with the first: Blazor everywhere possible, with logic and components in a set of common libraries.
Distributie.sln
├── Distributie.Contracts // DTOs, enums, validations - also used by API
├── Distributie.Core // business rules, offline synchronization
├── Distributie.Client // API client, local cache
├── Distributie.Ui // Razor Class Library: common Blazor components
├── Distributie.Api // ASP.NET Core
├── Distributie.Web // Blazor Web App - managers
├── Distributie.Desktop // WPF + BlazorWebView - warehouse
└── Distributie.Mobile // .NET MAUI + Blazor Hybrid - agents
The first layer, contracts and validation, is used by all applications and the API. A business rule defined once applies identically on the ASP.NET Core server, in the browser, on desktop, and on phone:
// Distributie.Contracts
public sealed record LinieComanda(string CodProdus, decimal Cantitate);
public sealed record ComandaNoua(string CodClient, IReadOnlyList<LinieComanda> Linii);
public sealed class ComandaNouaValidator : AbstractValidator<ComandaNoua>
{
public ComandaNouaValidator()
{
RuleFor(c => c.CodClient).NotEmpty();
RuleFor(c => c.Linii).NotEmpty()
.WithMessage("The order must contain at least one product.");
RuleForEach(c => c.Linii).ChildRules(linie =>
linie.RuleFor(l => l.Cantitate).GreaterThan(0));
}
}
Platform integration goes through an interface defined in Core, with one implementation registered by each application. Business code doesn't know if the barcode comes from the phone camera, a USB reader, or the keyboard:
// Distributie.Core
public interface IScanner
{
Task<string?> ScanAsync(CancellationToken ct = default);
}
// Distributie.Mobile - MauiProgram.cs
builder.Services.AddSingleton<IScanner, CameraScanner>();
// Distributie.Desktop
services.AddSingleton<IScanner, UsbReaderScanner>();
// Distributie.Web - Program.cs
builder.Services.AddScoped<IScanner, ManualInputScanner>();
For the interface layer, components from Distributie.Ui adapt to the device instead of imposing the same screen everywhere. The official template for MAUI Blazor Hybrid and Web App uses exactly this idea, through an IFormFactor service:
@* Distributie.Ui/Comenzi.razor *@
@inject IFormFactor FormFactor
@if (FormFactor.IsPhone)
{
<CompactOrderList Orders="orders" />
}
else
{
<OrderTable Orders="orders" Filters="true" Export="true" />
}
Small components, like a product card, a client selector, or an order line form, are common. Pages can be different, composed of the same pieces. For offline synchronization from Core, the same principles from the resilience series apply on mobile: a local queue of operations, of the Outbox type, sent when the connection returns, just like in the article about graceful degradation.
How much was actually shared
In our scenario, the result looks like this:
- Contracts, validations, and business rules — completely common, including with the API.
- API client, cache, and synchronization — common for all client applications.
- Small interface components — largely common.
- Pages and navigation — partially common. Managers and agents have different flows, and their screens remain different.
- Platform integration — specific to each application but isolated in a few small classes.
It is not "one application for all." But neither three separate applications. It is a single product, with a single core and three thin interfaces, each suited to its users. And it can be maintained by a single team.
The most common mistakes I see
- The same interface for different users. If the manager and agent have the same screen, one of them is using an interface designed for the other.
- Framework chosen by the number of platforms. Six platforms checked in a table don't matter if you need two. Choose by real platforms and by the team.
- Abstractions before the second platform. A generic platform layer built "for the future" is almost always wrong. Extract the interface when the second real implementation appears.
- Too much common code. When any change in the common library forces simultaneous release of three applications, sharing has become coupling. Version common libraries and keep contracts stable.
- Web on canvas for public pages. A presentation site built in Flutter, Avalonia, or Uno loses SEO and first load speed. Use them for applications, not for content.
- Logic in the interface. And in the last article of the series, the same mistake: logic in code-behind or components makes any sharing impossible.
Decision table for the whole series
Finally, here are all the recommendations from the five articles, gathered in one place:
| What you build | First choice | Serious alternative |
|---|---|---|
| Public content site | Astro or Razor Pages | Next.js / Nuxt |
| Public web product, rich UI | Next.js or Nuxt | SvelteKit |
| SaaS B2B, .NET team | Blazor Web App | Angular |
| Enterprise web application, many teams | Angular | React with strict rules |
| Back-office, CRUD | Razor Pages + HTMX | Blazor Server |
| Existing Windows desktop application | WPF / WinForms on .NET 10 | Modernize with BlazorWebView |
| New desktop, Windows only | WPF or WinUI 3 | WinForms for internal tools |
| Cross-platform desktop | Avalonia | Uno Platform |
| Web application needing desktop and hardware | Blazor Hybrid | Tauri or Electron |
| Internal mobile application | PWA | .NET MAUI |
| Mobile application, .NET team | .NET MAUI | Flutter |
| Product mobile application, branded UI | Flutter | Native |
| Mobile application, React team | React Native with Expo | PWA |
| Web, desktop, and mobile, .NET team | Blazor Web + Blazor Hybrid, with common libraries | Uno Platform |
| Web, desktop, and mobile, all behind a login | Uno Platform or Flutter | Avalonia |
Series conclusion
We started in the first article from a kickoff meeting where everyone was half right. After five articles, the conclusion is the same, just better argued: there is no best UI technology. There is the right technology for what you build, who builds it, and how long it must be maintained.
If you take away one thing from the whole series, let it be this: separate logic from interface. UI frameworks change every few years. Some disappear, others appear, and the platforms you must reach change too. Your application's business rules should survive all these changes. When logic is separated, choosing the UI framework becomes a reversible decision. When it is not, every choice becomes lifelong.
The series ends here, but the subject does not. If you have recently gone through such a decision or migration, I would love to hear what you chose and why.