Spice
Spice š¶, a spicy cross-platform UI framework!
Install / Use
/learn @jonathanpeppers/SpiceREADME
Spice š¶, a spicy cross-platform UI framework!
A prototype (and design) of API minimalism for mobile.
If you like this idea, star for approval! Read on for details!

Getting Started
Simply install the template:
dotnet new install Spice.Templates
Create either a plain Spice project, or a hybrid "Spice+Blazor" project:
dotnet new spice
# Or if you want hybrid/web support
dotnet new spice-blazor
Or use the project template in Visual Studio:

Build it as you would for other .NET MAUI projects:
dotnet build
# To run on Android
dotnet build -f net10.0-android -t:Run
# To run on iOS
dotnet build -f net10.0-ios -t:Run
Of course, you can also just open the project in Visual Studio and hit F5.
Startup Time & App Size
In comparison to a dotnet new maui project, I created a Spice
project with the same layouts and optimized settings for both project
types. (AndroidLinkMode=r8, etc.)
App size of a single-architecture .apk, built for android-arm64:

The average startup time of 10 runs on a Pixel 5:

This gives you an idea of how much "stuff" is in .NET MAUI.
In some respects the above comparison isn't completely fair, as Spice
š¶ has very few features. However, Spice š¶ is fully
trimmable, and so a Release build of an app without
Spice.Button will have the code for Spice.Button trimmed away. It
will be quite difficult for .NET MAUI to become fully
trimmable -- due to the nature of XAML, data-binding, and
other System.Reflection usage in the framework.
Background & Motivation
In reviewing, many of the cool UI frameworks for mobile:
Looking at what apps look like today -- it seems like bunch of rigamarole to me. Can we build mobile applications without design patterns?
The idea is we could build apps in a simple way, in a similar vein as minimal APIs in ASP.NET Core but for mobile & maybe one day desktop:
public class App : Application
{
public App()
{
int count = 0;
var label = new Label
{
Text = "Hello, Spice š¶",
};
var button = new Button
{
Text = "Click Me",
Clicked = _ => label.Text = $"Times: {++count}"
};
Main = new StackLayout { label, button };
}
}
These "view" types are mostly just POCOs.
Thus you can easily write unit tests in a vanilla net10.0 Xunit
project, such as:
[Fact]
public void Application()
{
var app = new App();
var label = (Label)app.Main.Children[0];
var button = (Button)app.Main.Children[1];
button.Clicked(button);
Assert.Equal("Times: 1", label.Text);
button.Clicked(button);
Assert.Equal("Times: 2", label.Text);
}
The above views in a net10.0 project are not real UI, while
net10.0-android and net10.0-ios projects get the full
implementations that actually do something on screen.
So for example, adding App to the screen on Android:
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(new App());
}
And on iOS:
var vc = new UIViewController();
vc.View.AddSubview(new App());
Window.RootViewController = vc;
App is a native view on both platforms. You just add it to an the
screen as you would any other control or view. This can be mix &
matched with regular iOS & Android UI because Spice š¶ views are just
native views.
NEW Blazor Support
Currently, Blazor/Hybrid apps are strongly tied to .NET MAUI. The
implementation is basically working with the plumbing of the native
"web view" on each platform. So we could have implemented
BlazorWebView to be used in "plain" dotnet new android or
dotnet new ios apps. For now, I've migrated some of the source code
from BlazorWebView from .NET MAUI to Spice š¶, making it available
as a new control:
public class App : Application
{
public App()
{
Main = new BlazorWebView
{
HostPage = "wwwroot/index.html",
RootComponents =
{
new RootComponent { Selector = "#app", ComponentType = typeof(Main) }
},
};
}
}
From here, you can write Index.razor as the Blazor you know and love:
@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.
To arrive at Blazor web content inside iOS/Android apps:

This setup might be particularly useful if you want web content to take full control of the screen with minimal native controls. No need for the app size / startup overhead of .NET MAUI if you don't actually have native content?
Scope
- No XAML. No DI. No MVVM. No MVC. No data-binding. No System.Reflection.
- Do we need these things?
- Target iOS & Android only to start.
- Implement only the simplest controls.
- The native platforms do their own layout.
- Document how to author custom controls.
- Leverage C# Hot Reload for fast development.
- Measure startup time & app size.
- Profit?
Benefits of this approach are full support for trimming and eventually NativeAOT if it comes to mobile one day. š
Thoughts on .NET MAUI
.NET MAUI is great. XAML is great. Think of this idea as a "mini" MAUI.
Spice š¶ will even leverage various parts of .NET MAUI:
- The iOS and Android workloads for .NET.
- The .NET MAUI "Single Project" system.
- The .NET MAUI "Asset" system, aka Resizetizer.
- Microsoft.Maui.Graphics for primitives like
Color.
And, of course, you should be able to use Microsoft.Maui.Essentials by
opting in with UseMauiEssentials=true.
It is an achievement in itself that I was able to invent my own UI framework and pick and choose the pieces of .NET MAUI that made sense for my framework.
Implemented Controls
View: maps toAndroid.Views.ViewandUIKit.View.Label: maps toAndroid.Widget.TextViewandUIKit.UILabelButton: maps toAndroid.Widget.ButtonandUIKit.UIButtonStackLayout: maps toAndroid.Widget.LinearLayoutandUIKit.UIStackViewImage: maps toAndroid.Widget.ImageViewandUIKit.UIImageViewEntry: maps toAndroid.Widget.EditTextandUIKit.UITextFieldWebView: maps toAndroid.Webkit.WebViewandWebKit.WKWebViewBlazorWebViewextendsWebViewadding support for Blazor. Use thespice-blazortemplate to get started.
Custom Controls
Let's review an implementation for Image.
First, you can write the cross-platform part for a vanilla net10.0
class library:
public partial class Image : View
{
[ObservableProperty]
string _source = "";
}
[ObservableProperty] comes from the [MVVM Community
Toolkit][observable] -- I made use of it for simplicity. It will
automatically generate various partial methods,
INotifyPropertyChanged, and a public property named Source.
We can implement the control on Android, such as:
public partial class Image
{
public static implicit operator ImageView(Image image) => image.NativeView;
public Image() : base(c => new ImageView(c)) { }
public new ImageView NativeView => (ImageView)_nativeView.Value;
partial void OnSourceChanged(string value)
{
// NOTE: the real implementation is in Java for performance reasons
var image = NativeView;
var context = image.Context;
int id = context!.Resources!.GetIdentifier(value, "drawable", context.PackageName);
if (id != 0)
{
image.SetImageResource(id);
}
}
}
This code takes the name of an image, and looks up a drawable with the
same name. This also leverages the .NET MAUI asset system, so a
spice.svg can simply be loaded via new Image { Source = "spice" }.
Lastly, the iOS implementation:
public partial class Image
{
public static implicit operator UIImageView(Image image) => image.NativeView;
public Image() : base(_ => new UIImageView { AutoresizingMask = UIViewAutoresizing.None }) { }
public new UIImageView NativeView => (UIImageView)_nativeView.Value;
partial void OnSourceChanged(string value) => NativeView.Image = UIImage.FromFile($"{value}.png");
}
This implementation is a bit simpler, all we have to do is call
UIImage.FromFile() and make sure to append a .png file extension
that the MAUI asset system generates.
Now, let's say you don't want to create a control from scratch. Imagine a "ghost button":
class GhostButton : Button
{
public GhostButton() => NativeView.Alpha = 0.5f;
}
In this case, the NativeView property returns the underlying
Android.Widget.Button or UIKit.Button that both conveniently have
an Alpha property that ranges from 0.0f to 1.0f. The same code
works on both platforms!
Imagine the APIs were different, you could instead do:
class GhostButton : Button
{
public GhostButton
{
#if ANDROID
NativeView.SomeAndroidAPI(0.5f);
#elif IOS
NativeView.SomeiOSAPI(0.5f);
#endif
Related Skills
node-connect
345.9kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
106.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
345.9kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
345.9kQQBot åÆåŖä½ę¶åč½åćä½æēØ <qqmedia> ę ē¾ļ¼ē³»ē»ę ¹ę®ęä»¶ę©å±åčŖåØčÆå«ē±»åļ¼å¾ē/čÆé³/č§é¢/ęä»¶ļ¼ć
