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:
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.