{"id":17950827,"url":"https://github.com/jonathanpeppers/spice","last_synced_at":"2025-04-09T21:22:58.348Z","repository":{"id":65678867,"uuid":"594226889","full_name":"jonathanpeppers/spice","owner":"jonathanpeppers","description":"Spice 🌶, a spicy cross-platform UI framework!","archived":false,"fork":false,"pushed_at":"2024-02-21T21:42:51.000Z","size":1411,"stargazers_count":235,"open_issues_count":0,"forks_count":8,"subscribers_count":14,"default_branch":"main","last_synced_at":"2025-04-02T19:07:05.431Z","etag":null,"topics":["android","cross-platform","dotnet","hacktoberfest","ios","maui"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jonathanpeppers.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-01-27T22:35:31.000Z","updated_at":"2025-03-30T00:27:27.000Z","dependencies_parsed_at":"2024-02-21T22:46:42.996Z","dependency_job_id":null,"html_url":"https://github.com/jonathanpeppers/spice","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonathanpeppers%2Fspice","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonathanpeppers%2Fspice/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonathanpeppers%2Fspice/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonathanpeppers%2Fspice/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jonathanpeppers","download_url":"https://codeload.github.com/jonathanpeppers/spice/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248112977,"owners_count":21049764,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","cross-platform","dotnet","hacktoberfest","ios","maui"],"created_at":"2024-10-29T09:40:40.786Z","updated_at":"2025-04-09T21:22:58.326Z","avatar_url":"https://github.com/jonathanpeppers.png","language":"C#","funding_links":[],"categories":["UI","URLS"],"sub_categories":[],"readme":"# Spice 🌶, a spicy cross-platform UI framework!\n\nA prototype (and design) of API minimalism for mobile.\n\nIf you like this idea, star for approval! Read on for details!\n\n![Spice running on iOS and Android](docs/spice.png)\n\n## Getting Started\n\nSimply install the template:\n\n```sh\ndotnet new install Spice.Templates\n```\n\nCreate either a plain Spice project, or a hybrid \"Spice+Blazor\" project:\n\n```sh\ndotnet new spice\n# Or if you want hybrid/web support\ndotnet new spice-blazor\n```\n\nOr use the project template in Visual Studio:\n\n![Screenshot of the Spice project template in Visual Studio](docs/vs-template.png)\n\nBuild it as you would for other .NET MAUI projects:\n\n```sh\ndotnet build\n# To run on Android\ndotnet build -f net8.0-android -t:Run\n# To run on iOS\ndotnet build -f net8.0-ios -t:Run\n```\n\nOf course, you can also just open the project in Visual Studio and hit F5.\n\n## Startup Time \u0026 App Size\n\nIn comparison to a `dotnet new maui` project, I created a Spice\nproject with the same layouts and optimized settings for both project\ntypes. (`AndroidLinkMode=r8`, etc.)\n\nApp size of a single-architecture `.apk`, built for `android-arm64`:\n\n![Graph of an app size comparison](docs/appsize.png)\n\nThe average startup time of 10 runs on a Pixel 5:\n\n![Graph of a startup comparison](docs/startup.png)\n\nThis gives you an idea of how much \"stuff\" is in .NET MAUI.\n\nIn some respects the above comparison isn't completely fair, as Spice\n🌶 has very few features. However, Spice 🌶 is [fully\ntrimmable][trimming], and so a `Release` build of an app without\n`Spice.Button` will have the code for `Spice.Button` trimmed away. It\nwill be quite difficult for .NET MAUI to become [fully\ntrimmable][trimming] -- due to the nature of XAML, data-binding, and\nother System.Reflection usage in the framework.\n\n[trimming]: https://learn.microsoft.com/dotnet/core/deploying/trimming/prepare-libraries-for-trimming\n\n## Background \u0026 Motivation\n\nIn reviewing, many of the *cool* UI frameworks for mobile:\n\n* [Flutter](https://flutter.dev)\n* [SwiftUI](https://developer.apple.com/xcode/swiftui/)\n* [Jetpack Compose](https://developer.android.com/jetpack/compose)\n* [Fabulous](https://fabulous.dev/)\n* [Comet](https://github.com/dotnet/Comet)\n* An, of course, [.NET MAUI](https://dotnet.microsoft.com/apps/maui)!\n\nLooking at what apps look like today -- it seems like bunch of\nrigamarole to me. Can we build mobile applications *without* design\npatterns?\n\nThe idea is we could build apps in a simple way, in a similar vein as\n[minimal APIs in ASP.NET Core][minimal-apis] but for mobile \u0026 maybe\none day desktop:\n\n```csharp\npublic class App : Application\n{\n    public App()\n    {\n        int count = 0;\n    \n        var label = new Label\n        {\n            Text = \"Hello, Spice 🌶\",\n        };\n    \n        var button = new Button\n        {\n            Text = \"Click Me\",\n            Clicked = _ =\u003e label.Text = $\"Times: {++count}\"\n        };\n    \n        Main = new StackView { label, button };\n    }\n}\n```\n\nThese \"view\" types are mostly just [POCOs][poco].\n\nThus you can easily write unit tests in a vanilla `net8.0` Xunit\nproject, such as:\n\n```csharp\n[Fact]\npublic void Application()\n{\n    var app = new App();\n    var label = (Label)app.Main.Children[0];\n    var button = (Button)app.Main.Children[1];\n\n    button.Clicked(button);\n    Assert.Equal(\"Times: 1\", label.Text);\n\n    button.Clicked(button);\n    Assert.Equal(\"Times: 2\", label.Text);\n}\n```\n\nThe above views in a `net8.0` project are not real UI, while\n`net8.0-android` and `net8.0-ios` projects get the full\nimplementations that actually *do* something on screen.\n\nSo for example, adding `App` to the screen on Android:\n\n```csharp\nprotected override void OnCreate(Bundle? savedInstanceState)\n{\n    base.OnCreate(savedInstanceState);\n\n    SetContentView(new App());\n}\n```\n\nAnd on iOS:\n\n```csharp\nvar vc = new UIViewController();\nvc.View.AddSubview(new App());\nWindow.RootViewController = vc;\n```\n\n`App` is a native view on both platforms. You just add it to an the\nscreen as you would any other control or view. This can be mix \u0026\nmatched with regular iOS \u0026 Android UI because Spice 🌶 views are just\nnative views.\n\n[poco]: https://en.wikipedia.org/wiki/Plain_old_CLR_object\n[minimal-apis]: https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis\n\n## *NEW* Blazor Support\n\nCurrently, Blazor/Hybrid apps are strongly tied to .NET MAUI. The\nimplementation is basically working with the plumbing of the native\n\"web view\" on each platform. So we could have implemented\n`BlazorWebView` to be used in \"plain\" `dotnet new android` or\n`dotnet new ios` apps. For now, I've migrated some of the source code\nfrom `BlazorWebView` from .NET MAUI to Spice 🌶, making it available\nas a new control:\n\n```csharp\npublic class App : Application\n{\n    public App()\n    {\n        Main = new BlazorWebView\n        {\n            HostPage = \"wwwroot/index.html\",\n            RootComponents =\n            {\n                new RootComponent { Selector = \"#app\", ComponentType = typeof(Main) }\n            },\n        };\n    }\n}\n```\n\nFrom here, you can write `Index.razor` as the Blazor you know and love:\n\n```razor\n@page \"/\"\n\n\u003ch1\u003eHello, world!\u003c/h1\u003e\n\nWelcome to your new app.\n```\n\nTo arrive at Blazor web content inside iOS/Android apps:\n\n![Screenshot of Blazor app on iOS](docs/blazor.png)\n\nThis setup might be particularly useful if you want web content to\ntake full control of the screen with minimal native controls. No need\nfor the app size / startup overhead of .NET MAUI if you don't actually\nhave native content?\n\n## Scope\n\n* No XAML. No DI. No MVVM. No MVC. No data-binding. No System.Reflection.\n  * *Do we need these things?*\n* Target iOS \u0026 Android only to start.\n* Implement only the simplest controls.\n* The native platforms do their own layout.\n* Document how to author custom controls.\n* Leverage C# Hot Reload for fast development.\n* Measure startup time \u0026 app size.\n* Profit?\n\nBenefits of this approach are full support for [trimming][trimming]\nand eventually [NativeAOT][nativeaot] if it comes to mobile one day. 😉\n\n[nativeaot]: https://learn.microsoft.com/dotnet/core/deploying/native-aot/\n\n## Thoughts on .NET MAUI\n\n.NET MAUI is great. XAML is great. Think of this idea as a \"mini\"\nMAUI.\n\nSpice 🌶 will even leverage various parts of .NET MAUI:\n\n* The iOS and Android workloads for .NET.\n* The .NET MAUI \"Single Project\" system.\n* The .NET MAUI \"Asset\" system, aka Resizetizer.\n* Microsoft.Maui.Graphics for primitives like `Color`.\n\nAnd, of course, you should be able to use Microsoft.Maui.Essentials by\nopting in with `UseMauiEssentials=true`.\n\nIt is an achievement in itself that I was able to invent my own UI\nframework and pick and choose the pieces of .NET MAUI that made sense\nfor my framework.\n\n## Implemented Controls\n\n* `View`: maps to `Android.Views.View` and `UIKit.View`.\n* `Label`: maps to `Android.Widget.TextView` and `UIKit.UILabel`\n* `Button`: maps to `Android.Widget.Button` and `UIKit.UIButton`\n* `StackView`: maps to `Android.Widget.LinearLayout` and `UIKit.UIStackView`\n* `Image`: maps to `Android.Widget.ImageView` and `UIKit.UIImageView`\n* `Entry`: maps to `Android.Widget.EditText` and `UIKit.UITextField`\n* `WebView`: maps to `Android.Webkit.WebView` and `WebKit.WKWebView`\n* `BlazorWebView` extends `WebView` adding support for Blazor. Use the\n  `spice-blazor` template to get started.\n\n## Custom Controls\n\nLet's review an implementation for `Image`.\n\nFirst, you can write the cross-platform part for a vanilla `net8.0`\nclass library:\n\n```csharp\npublic partial class Image : View\n{\n    [ObservableProperty]\n    string _source = \"\";\n}\n```\n\n`[ObservableProperty]` comes from the [MVVM Community\nToolkit][observable] -- I made use of it for simplicity. It will\nautomatically generate various `partial` methods,\n`INotifyPropertyChanged`, and a `public` property named `Source`.\n\nWe can implement the control on Android, such as:\n\n```csharp\npublic partial class Image\n{\n    public static implicit operator ImageView(Image image) =\u003e image.NativeView;\n\n    public Image() : base(c =\u003e new ImageView(c)) { }\n\n    public new ImageView NativeView =\u003e (ImageView)_nativeView.Value;\n\n    partial void OnSourceChanged(string value)\n    {\n        // NOTE: the real implementation is in Java for performance reasons\n        var image = NativeView;\n        var context = image.Context;\n        int id = context!.Resources!.GetIdentifier(value, \"drawable\", context.PackageName);\n        if (id != 0) \n        {\n            image.SetImageResource(id);\n        }\n    }\n}\n```\n\nThis code takes the name of an image, and looks up a drawable with the\nsame name. This also leverages the .NET MAUI asset system, so a\n`spice.svg` can simply be loaded via `new Image { Source = \"spice\" }`.\n\nLastly, the iOS implementation:\n\n```csharp\npublic partial class Image\n{\n    public static implicit operator UIImageView(Image image) =\u003e image.NativeView;\n\n    public Image() : base(_ =\u003e new UIImageView { AutoresizingMask = UIViewAutoresizing.None }) { }\n\n    public new UIImageView NativeView =\u003e (UIImageView)_nativeView.Value;\n\n    partial void OnSourceChanged(string value) =\u003e NativeView.Image = UIImage.FromFile($\"{value}.png\");\n}\n```\n\nThis implementation is a bit simpler, all we have to do is call\n`UIImage.FromFile()` and make sure to append a `.png` file extension\nthat the MAUI asset system generates.\n\nNow, let's say you don't want to create a control from scratch.\nImagine a \"ghost button\":\n\n```csharp\nclass GhostButton : Button\n{\n    public GhostButton() =\u003e NativeView.Alpha = 0.5f;\n}\n```\n\nIn this case, the `NativeView` property returns the underlying\n`Android.Widget.Button` or `UIKit.Button` that both conveniently have\nan `Alpha` property that ranges from 0.0f to 1.0f. The same code\nworks on both platforms!\n\nImagine the APIs were different, you could instead do:\n\n```csharp\nclass GhostButton : Button\n{\n    public GhostButton\n    {\n#if ANDROID\n        NativeView.SomeAndroidAPI(0.5f);\n#elif IOS\n        NativeView.SomeiOSAPI(0.5f);\n#endif\n    }\n}\n```\n\nAccessing the native views don't require any weird design patterns.\nJust `#if` as you please.\n\n[observable]: https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/generators/observableproperty\n\n## Hot Reload\n\nC# Hot Reload (in Visual Studio) works fine, as it does for vanilla .NET\niOS/Android apps:\n\n![Hot Reload Demo](docs/hotreload.gif)\n\nNote that this only works for `Button.Clicked` because the method is\ninvoked when you click. If the method that was changed was already\nrun, *something* has to force it to run again.\n[`MetadataUpdateHandler`][muh] is the solution to this problem, giving\nframeworks a way to \"reload themselves\" for Hot Reload.\n\nUnfortunately, [`MetadataUpdateHandler`][muh] does not currently work\nfor non-MAUI apps in Visual Studio 2022 17.5:\n\n```csharp\n[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(HotReload))]\n\nstatic class HotReload\n{\n    static void UpdateApplication(Type[]? updatedTypes)\n    {\n        if (updatedTypes == null)\n            return;\n        foreach (var type in updatedTypes)\n        {\n            // Do something with the type\n            Console.WriteLine(\"UpdateApplication: \" + type);\n        }\n    }\n}\n```\n\nThe above code works fine in a `dotnet new maui` app, but not a\n`dotnet new spice` or `dotnet new android` application.\n\nAnd so we can't add proper functionality for reloading `ctor`'s of\nSpice 🌶 views. The general idea is we could recreate the `App` class and\nreplace the views on screen. We could also create Android activities\nor iOS view controllers if necessary.\n\nHopefully, we can implement this for a future release of Visual Studio.\n\n[muh]: https://learn.microsoft.com/dotnet/api/system.reflection.metadata.metadataupdatehandlerattribute\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjonathanpeppers%2Fspice","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjonathanpeppers%2Fspice","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjonathanpeppers%2Fspice/lists"}