• Stars
    star
    226
  • Rank 175,416 (Top 4 %)
  • Language
    C#
  • License
    MIT License
  • Created over 1 year ago
  • Updated 7 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Spice 🌶, a spicy cross-platform UI framework!

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!

Spice running on iOS and Android

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:

Screenshot of the Spice project template in Visual Studio

Build it as you would for other .NET MAUI projects:

dotnet build
# To run on Android
dotnet build -f net7.0-android -t:Run
# To run on iOS
dotnet build -f net7.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:

Graph of an app size comparison

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

Graph of a startup comparison

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 StackView { label, button };
    }
}

These "view" types are mostly just POCOs.

Thus you can easily write unit tests in a vanilla net7.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 net7.0 project are not real UI, while net7.0-android and net7.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:

Screenshot of Blazor app on iOS

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 to Android.Views.View and UIKit.View.
  • Label: maps to Android.Widget.TextView and UIKit.UILabel
  • Button: maps to Android.Widget.Button and UIKit.UIButton
  • StackView: maps to Android.Widget.LinearLayout and UIKit.UIStackView
  • Image: maps to Android.Widget.ImageView and UIKit.UIImageView
  • Entry: maps to Android.Widget.EditText and UIKit.UITextField
  • WebView: maps to Android.Webkit.WebView and WebKit.WKWebView
  • BlazorWebView extends WebView adding support for Blazor. Use the spice-blazor template to get started.

Custom Controls

Let's review an implementation for Image.

First, you can write the cross-platform part for a vanilla net7.0 class library:

public partial class Image : View
{
    [ObservableProperty]
    string _source = "";
}

[ObservableProperty] comes from the MVVM Community Toolkit -- 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
    }
}

Accessing the native views don't require any weird design patterns. Just #if as you please.

Hot Reload

C# Hot Reload (in Visual Studio) works fine, as it does for vanilla .NET iOS/Android apps:

Hot Reload Demo

Note that this only works for Button.Clicked because the method is invoked when you click. If the method that was changed was already run, something has to force it to run again. MetadataUpdateHandler is the solution to this problem, giving frameworks a way to "reload themselves" for Hot Reload.

Unfortunately, MetadataUpdateHandler does not currently work for non-MAUI apps in Visual Studio 2022 17.5:

[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(HotReload))]

static class HotReload
{
    static void UpdateApplication(Type[]? updatedTypes)
    {
        if (updatedTypes == null)
            return;
        foreach (var type in updatedTypes)
        {
            // Do something with the type
            Console.WriteLine("UpdateApplication: " + type);
        }
    }
}

The above code works fine in a dotnet new maui app, but not a dotnet new spice or dotnet new android application.

And so we can't add proper functionality for reloading ctor's of Spice 🌶 views. The general idea is we could recreate the App class and replace the views on screen. We could also create Android activities or iOS view controllers if necessary.

Hopefully, we can implement this for a future release of Visual Studio.

More Repositories

1

dotnes

.NET for the NES game console
C#
617
star
2

Xamarin.Forms.Mocks

Library for running Xamarin.Forms inside of unit tests
C#
195
star
3

glidex

glidex.forms is a library using Glide for faster Xamarin.Forms images on Android. Find out more about Glide at https://github.com/bumptech/glide
C#
193
star
4

Xamarin.Android.Lite

Prototype/proof of concept of a "lite" Xamarin.Android that only supports Xamarin.Forms
C#
124
star
5

boots

boots is a .NET global tool for "bootstrapping" vsix & pkg files. Just "boots https://url/to/your/package"!
C#
86
star
6

Android-NativeAOT

A .NET 8, NativeAOT example on Android
C#
66
star
7

maui-profiling

Repository for building MAUI apps over time. So we can install & profile them.
C#
57
star
8

XPlatUtils

A set of helpers for cross platform apps. Right now has a ServiceContainer and Messenger implementation.
C#
37
star
9

memory-analyzers

C# code analyzers for finding memory leaks
C#
35
star
10

Mono.Profiler.Android

Support for the Mono profiler in .NET 6 Android applications
Shell
33
star
11

inclusive-code-reviews-browser

A chrome web extension for improving code reviews on Github or Azure DevOps
JavaScript
31
star
12

Xamarin.InAppPurchasing

Sample project for secure in-app purchases with Xamarin for iOS and Google Play
C#
27
star
13

XamChat

Simple chat application for iOS and Android
C#
26
star
14

UnityBuild

Example project showing automated builds with Unity3D
C#
24
star
15

lols

Performance test: LOLs per second
C#
24
star
16

HelloGlide

A simple sample using glidex.forms
C#
15
star
17

inclusive-code-reviews-ml

Machine learning for code reviews!
C#
14
star
18

Xamarin.SSLPinning

Test project to setup SSL pinning with Xamarin.iOS
C#
14
star
19

Xamarin.Forms.Benchmarks

Using BenchmarkDotNet to write some benchmarks for Xamarin.Forms concepts.
C#
11
star
20

measure-startup

C# console app to launch a process, wait for stdout, report a time
C#
8
star
21

MemoryLeaksOniOS

Testing memory leaks on iOS
C#
6
star
22

maui-scrolling-performance

Test repository for measuring scrolling performance of .NET MAUI apps on Android
C#
5
star
23

maui-workload

Prototype of a dotnet/maui "workload"
PowerShell
3
star
24

wpf-memory-leaks

Unit tests using WPF, to discover if/how it avoids memory leaks
C#
3
star
25

android-profiled-aot

This is a repo for recording .NET 6 AOT profiles.
C#
3
star
26

MyPal

A .NET MAUI sample application that uses omni-input LLMs to create "My Pal", a friendly koala that insults you based on a selfie!
C#
3
star
27

MonoGameSaveImage

Example of using Texture2D.SaveAsPng and Texture2D.SaveAsPng
C#
2
star
28

Benchmarks

Group of C# benchmarks for testing purposes
C#
2
star
29

baseline-profiles

For recording baseline profiles, see: https://developer.android.com/topic/performance/baselineprofiles
Java
2
star
30

CustomResourceDesigner

Example of making a "slimmer" Resource.designer.cs
C#
2
star
31

BenchmarkDotNet-Android

BenchmarkDotNet projects for comparing .NET 6 to Xamarin.Android
C#
2
star
32

DeleteBinObjBug

Reproducing DeleteBinObj and/or FastDev bug
Shell
2
star
33

Live.Forms.iOS

Xamarin.Forms live XAML reloading (iOS only)
C#
2
star
34

twitch-embeddinator

Demo project taking a Xamarin.Forms app and running it with Embeddinator-4000 in Android Studio
Java
2
star
35

maui-workload-pinning

This is an example of installing .NET MAUI Preview 7 and using RC1 packs
C#
2
star
36

XamarinAndroidBuildTimes

Quick repo I'll use to profile build times on different Xamarin.Android projects
PowerShell
2
star
37

Xamarin.Forms.Performance

Repo for testing performance in Xamarin.Forms on Android
C#
2
star
38

dotnet-using-task

Testing MSBuild <UsingTask/>
C#
2
star
39

Xamarin.Firebase.Messaging.Repro

Xamarin.Firebase.Messaging.Repro
C#
1
star
40

net6-finalizer-repro

Repro for: https://github.com/xamarin/xamarin-android/pull/6363#issuecomment-937379007
C#
1
star
41

Simulations

Levi's simulations
C#
1
star
42

class-lib-pack-r2r

Include ReadyToRun compiled assemblies in a NuGet/class library.
C#
1
star
43

GetRich

A mobile app that will basically make you rich if you submit it to the app store
C#
1
star
44

Embeddinator-MonoGame

Testing running MonoGame through Embeddinator
1
star
45

IntuneRepro

Trying to repro: https://github.com/msintuneappsdk/intune-app-sdk-xamarin/issues/5
C#
1
star
46

MSBuildIncrementalClean

This is a quick sample to figure out some nuances with MSBuild
PowerShell
1
star
47

MicrosoftExtensionsRepro

Trying to repro startup performance issue on Android
C#
1
star
48

gitinfotest

This is a repro for an MSBuild issue.
C#
1
star
49

maui-view-performance

Repo for measuring .NET MAUI views & page navigation
C#
1
star
50

inclusive-code-reviews-demo

Source code for my presentation "Using ML.NET and LLMs in C#"
C#
1
star