RO EN

Un singur cod pentru web, desktop și mobil: cât poți împărți, de fapt?

Un singur cod pentru web, desktop și mobil: cât poți împărți, de fapt? ✨ Imagine generată cu AI
Doru Bulubașa
04 noiembrie 2026
14 vizualizări

Firma de distribuție din articolele anterioare are acum trei aplicații. Managerii lucrează într-o aplicație web, cu tabele dense și rapoarte. Operatorii din depozit folosesc o aplicație desktop, legată de cântar și de imprimanta de etichete. Agenții de pe teren au o aplicație mobilă care merge și fără semnal. Trei aplicații, trei bucăți de cod, iar fiecare funcționalitate nouă se implementează de trei ori.

Directorul general a citit undeva că există framework-uri care merg peste tot: „Facem o singură aplicație pentru toți și terminăm cu trei echipe.” Promisiunea e veche. Write once, run everywhere a fost sloganul Java în anii ’90, apoi al Silverlight, apoi al lui Xamarin. De fiecare dată, a funcționat parțial. Iar partea care nu a funcționat a fost aproape întotdeauna aceeași: interfața.

Acesta e ultimul articol din seria despre alegerea tehnologiei de UI. După web, desktop-ul nativ în .NET, desktop-ul cu tehnologii web și mobil, încercăm să răspundem la întrebarea cea mai grea: cât cod poți împărți, de fapt, între web, desktop și mobil?

Codul nu se împarte uniform: cele șase straturi

Întrebarea „cât cod pot împărți?” nu are un singur răspuns, pentru că o aplicație nu e un bloc uniform. E făcută din straturi, iar fiecare strat se împarte cu un efort diferit. De la cel mai ușor la cel mai greu:

  1. Contractele și modelele — DTO-uri, enum-uri, reguli de validare. Se împart aproape gratuit și ar trebui împărțite întotdeauna, inclusiv cu backend-ul.
  2. Logica de business și accesul la date — calcule, reguli, clientul pentru API, cache-ul local, sincronizarea offline. Se împart ușor, cu condiția să nu depindă de o platformă anume.
  3. Starea interfeței — ViewModel-uri, gestionarea stării, logica de ecran. Se împarte bine între framework-uri din aceeași familie, cum am văzut în articolul despre desktop.
  4. Componentele de interfață — butoane, formulare, carduri, liste. Se pot împărți, dar numai când framework-ul de UI e același pe toate platformele.
  5. Layout-ul și navigația — cum sunt așezate ecranele și cum treci de la unul la altul. Aici se rupe de obicei promisiunea, pentru că un telefon și un monitor de 27 de inchi cer ecrane diferite.
  6. Integrarea cu platforma — camera, cântarul, notificările, fișierele. Nu se împarte. Se ascunde în spatele unor interfețe, cu câte o implementare pe platformă.

Concluzia e mai importantă decât orice framework: cea mai mare parte din valoarea partajării stă în primele trei straturi. Acolo stau regulile de business, adică exact codul pe care nu vrei să-l scrii și să-l depanezi de trei ori. Iar aceste straturi se pot împărți cu aproape orice tehnologie de UI, inclusiv cu trei tehnologii diferite.

De ce interfața e cea mai greu de împărțit

Nu e o problemă tehnică, ci una de utilizare. Managerul de la sediu vrea un tabel cu 12 coloane, filtre și export în Excel, pe un monitor mare, cu mouse și tastatură. Agentul din magazin vrea un singur buton mare, „Comandă nouă”, apăsat cu degetul mare, pe un telefon ținut într-o mână. Cele două ecrane lucrează cu aceleași date, dar nu sunt același ecran, și nu trebuie să fie.

La asta se adaugă convențiile fiecărei platforme. Butonul de back pe Android, gestul de întoarcere pe iOS, bara de meniu pe macOS, scurtăturile de tastatură pe desktop, hover-ul care nu există pe ecranele tactile. O interfață complet comună ajunge ușor la cel mai mic numitor comun: funcționează peste tot, dar nu se simte bine nicăieri.

De aceea, regula practică este: același cod nu înseamnă același ecran. Împarte componentele, nu neapărat paginile.

Trei strategii de partajare

1. Logică comună, interfață nativă pe fiecare platformă

Straturile 1–3 sunt comune, iar fiecare platformă are propria interfață, în tehnologia ei nativă. În lumea .NET, asta înseamnă biblioteci de clase comune, folosite de un Blazor Web App, o aplicație WPF și o aplicație MAUI. În lumea Kotlin, înseamnă Kotlin Multiplatform cu SwiftUI pe iOS. Câștigi cea mai bună experiență pe fiecare platformă și plătești de mai multe ori interfața.

2. Interfață comună, desenată de framework

Flutter, Avalonia, Uno Platform și Compose Multiplatform desenează singure interfața, deci aceeași interfață rulează peste tot, inclusiv în browser. Câștigi o singură bază de cod pentru aproape tot. Plătești prin aspectul mai puțin nativ și prin varianta web: în browser, aceste framework-uri desenează pe un canvas, cu un pachet inițial de câțiva MB și fără HTML pe care motoarele de căutare să-l poată indexa. Pentru ecrane din spatele unui login e acceptabil. Pentru un site public, din motivele discutate în primul articol, nu este.

3. Web-first, împachetat pentru desktop și mobil

Interfața e o aplicație web, iar desktop-ul și mobilul o rulează într-un WebView: Blazor cu Blazor Hybrid în WPF și MAUI, sau React cu Tauri ori Capacitor. Câștigi cea mai bună variantă web și o reutilizare mare de componente. Plătești printr-o experiență mobilă mai puțin nativă și prin dependența de WebView.

Niciuna nu e corectă în general. Iar în practică, multe produse reușite combină strategiile: logică comună peste tot, componente comune unde au sens și ecrane dedicate acolo unde utilizatorii sunt foarte diferiți.

Framework-urile, comparate pe același scenariu

Iată cum se poziționează principalele opțiuni pentru scenariul nostru, cu web, desktop și mobil:

Tehnologie Platforme Interfață Ce rămâne de rezolvat
.NET MAUI Android, iOS, Windows, macOS Controale native Fără Linux și fără web propriu
MAUI + Blazor Hybrid + Blazor Web Toate, mai puțin Linux HTML/Razor în WebView, plus web real Aspect mai puțin nativ pe mobil
Avalonia Windows, macOS, Linux, iOS, Android, web (WASM) Desenată proprie (Skia) Web pe canvas, nepotrivit pentru SEO
Uno Platform Windows, macOS, Linux, iOS, Android, web (WASM) API WinUI, desenată prin Skia Web pe canvas, acoperirea API-urilor WinUI
Flutter iOS, Android, Windows, macOS, Linux, web Desenată proprie (Impeller/Skia) Web pe canvas, Dart pentru echipă
Compose Multiplatform Android, iOS, desktop (JVM), web (Beta) Desenată proprie Web încă în Beta, ecosistem în creștere
React Native iOS, Android; web prin Expo; Windows și macOS prin proiectele Microsoft Controale native Web și desktop mai puțin mature decât mobilul
Web + Tauri sau Capacitor Web, desktop, mobil HTML în WebView Diferențe între WebView-uri, acces nativ prin pluginuri

Un detaliu important: „merge și pe web” nu înseamnă același lucru peste tot. Blazor sau React generează pagini web reale, cu HTML, accesibilitate nativă de browser și SEO. Flutter, Avalonia, Uno și Compose rulează o aplicație care desenează în browser. Ambele variante sunt legitime, dar pentru scopuri diferite.

Cum arată în .NET: structura soluției

Pentru o echipă .NET, strategia cea mai pragmatică în scenariul nostru este de obicei a treia, combinată cu prima: Blazor peste tot unde se poate, cu logica și componentele într-un set de biblioteci comune.

Distributie.sln
├── Distributie.Contracts   // DTO-uri, enum-uri, validări - folosite și de API
├── Distributie.Core        // reguli de business, sincronizare offline
├── Distributie.Client      // client pentru API, cache local
├── Distributie.Ui          // Razor Class Library: componente Blazor comune
├── Distributie.Api         // ASP.NET Core
├── Distributie.Web         // Blazor Web App - managerii
├── Distributie.Desktop     // WPF + BlazorWebView - depozitul
└── Distributie.Mobile      // .NET MAUI + Blazor Hybrid - agenții

Primul strat, contractele și validarea, este folosit de toate aplicațiile și de API. O regulă de business definită o singură dată se aplică identic pe serverul ASP.NET Core, în browser, pe desktop și pe telefon:

// 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("Comanda trebuie să conțină cel puțin un produs.");
        RuleForEach(c => c.Linii).ChildRules(linie =>
            linie.RuleFor(l => l.Cantitate).GreaterThan(0));
    }
}

Integrarea cu platforma trece printr-o interfață definită în Core, cu câte o implementare înregistrată de fiecare aplicație. Codul de business nu știe dacă codul de bare vine de la camera telefonului, de la un cititor USB sau de la tastatură:

// Distributie.Core
public interface IScanner
{
    Task<string?> ScaneazaAsync(CancellationToken ct = default);
}

// Distributie.Mobile - MauiProgram.cs
builder.Services.AddSingleton<IScanner, CameraScanner>();

// Distributie.Desktop
services.AddSingleton<IScanner, CititorUsbScanner>();

// Distributie.Web - Program.cs
builder.Services.AddScoped<IScanner, IntroducereManualaScanner>();

Pentru stratul de interfață, componentele din Distributie.Ui se adaptează la dispozitiv în loc să impună același ecran peste tot. Template-ul oficial pentru MAUI Blazor Hybrid și Web App folosește exact această idee, printr-un serviciu IFormFactor:

@* Distributie.Ui/Comenzi.razor *@
@inject IFormFactor FormFactor

@if (FormFactor.EsteTelefon)
{
    <ListaComenziCompacta Comenzi="comenzi" />
}
else
{
    <TabelComenzi Comenzi="comenzi" Filtre="true" Export="true" />
}

Componentele mici, precum un card de produs, un selector de client sau un formular de linie de comandă, sunt comune. Paginile pot fi diferite, compuse din aceleași piese. Pentru sincronizarea offline din Core, aceleași principii din seria despre reziliență se aplică și pe mobil: o coadă locală de operațiuni, de tip Outbox, trimisă când revine conexiunea, la fel ca în articolul despre graceful degradation.

Cât s-a împărțit, de fapt

În scenariul nostru, rezultatul arată cam așa:

  • Contracte, validări și reguli de business — comune în totalitate, inclusiv cu API-ul.
  • Clientul pentru API, cache-ul și sincronizarea — comune pentru toate aplicațiile client.
  • Componentele mici de interfață — în mare parte comune.
  • Paginile și navigația — parțial comune. Managerii și agenții au fluxuri diferite, iar ecranele lor rămân diferite.
  • Integrarea cu platforma — specifică fiecărei aplicații, dar izolată în câteva clase mici.

Nu e „o singură aplicație pentru toți”. Dar nici trei aplicații separate. Este un singur produs, cu un singur nucleu și trei interfețe subțiri, fiecare potrivită utilizatorilor ei. Iar asta se poate întreține de o singură echipă.

Greșelile pe care le văd cel mai des

  • Aceeași interfață pentru utilizatori diferiți. Dacă managerul și agentul au același ecran, unul dintre ei folosește o interfață gândită pentru celălalt.
  • Framework ales după numărul de platforme. Șase platforme bifate într-un tabel nu contează dacă ai nevoie de două. Alege după platformele reale și după echipă.
  • Abstracții înaintea celei de-a doua platforme. Un strat generic de platformă construit „pentru viitor” e aproape întotdeauna greșit. Extrage interfața când apare a doua implementare reală.
  • Prea mult cod comun. Când orice modificare din biblioteca comună obligă la lansarea simultană a trei aplicații, partajarea a devenit cuplare. Versionează bibliotecile comune și păstrează contractele stabile.
  • Web pe canvas pentru pagini publice. Un site de prezentare construit în Flutter, Avalonia sau Uno pierde SEO și viteza primei încărcări. Folosește-le pentru aplicații, nu pentru conținut.
  • Logica în interfață. Și în ultimul articol al seriei, aceeași greșeală: logica din code-behind sau din componente face imposibilă orice partajare.

Tabelul de decizie pentru toată seria

Pentru final, iată toate recomandările din cele cinci articole, adunate într-un singur loc:

Ce construiești Prima alegere Alternativă serioasă
Site public de conținut Astro sau Razor Pages Next.js / Nuxt
Produs web public, UI bogat Next.js sau Nuxt SvelteKit
SaaS B2B, echipă .NET Blazor Web App Angular
Aplicație web enterprise, multe echipe Angular React cu reguli stricte
Back-office, CRUD Razor Pages + HTMX Blazor Server
Aplicație desktop Windows existentă WPF / WinForms pe .NET 10 Modernizare cu BlazorWebView
Desktop nou, doar Windows WPF sau WinUI 3 WinForms pentru unelte interne
Desktop cross-platform Avalonia Uno Platform
Aplicație web care are nevoie de desktop și hardware Blazor Hybrid Tauri sau Electron
Aplicație mobilă internă PWA .NET MAUI
Aplicație mobilă, echipă .NET .NET MAUI Flutter
Aplicație mobilă de produs, UI de brand Flutter Nativ
Aplicație mobilă, echipă React React Native cu Expo PWA
Web, desktop și mobil, echipă .NET Blazor Web + Blazor Hybrid, cu biblioteci comune Uno Platform
Web, desktop și mobil, totul în spatele unui login Uno Platform sau Flutter Avalonia

Încheierea seriei

Am pornit în primul articol de la o ședință de kickoff în care toată lumea avea dreptate pe jumătate. După cinci articole, concluzia e aceeași, doar mai bine argumentată: nu există cea mai bună tehnologie de UI. Există tehnologia potrivită pentru ce construiești, cine o construiește și cât timp trebuie întreținută.

Dacă e să rămâi cu un singur lucru din toată seria, să fie acesta: separă logica de interfață. Framework-urile de UI se schimbă la câțiva ani. Unele dispar, altele apar, iar platformele pe care trebuie să ajungi se schimbă și ele. Regulile de business ale aplicației tale ar trebui să supraviețuiască tuturor acestor schimbări. Când logica e separată, alegerea framework-ului de UI devine o decizie reversibilă. Când nu e, fiecare alegere devine pe viață.

Seria se încheie aici, dar subiectul nu. Dacă ați trecut recent printr-o astfel de decizie, sau printr-o migrare, mi-ar plăcea să aflu ce ați ales și de ce.