Editor’s note: This guide to .NET MAUI was last updated on 21 July 2023 to include information on recent performance enhancements and to compare .NET MAUI more directly with its predecessor, Xamarin.Forms.
If you want to create a cross-platform application that works natively on mobile and desktop platforms, there are many great frameworks to choose from. One of these is .NET Multi-platform App UI (MAUI), Microsoft’s open source, cross-platform framework for building mobile and desktop apps using C# and XAML.
.NET MAUI enables you to develop applications that can run on iOS, macOS, Android, and Windows, all from a single shared codebase. It is an advancement over Xamarin.Forms, which now includes UI controls redesigned and optimized for better performance and scalability.
.NET MAUI comes equipped with XAML hot reload, which updates your application UI as you modify your XAML code without you needing to recompile. Similarly, it also supports .NET hot reload, which applies your C# code changes to your running application without recompiling the whole thing.
In this article, we’ll explore .NET MAUI, reviewing its architecture; evaluating its performance; comparing its features to React Native, Flutter, and Xamarin.Forms; and, finally, using it in a project. If you want to get straight to building, you can find the code for our demo app in this GitHub repo. Let’s get started!
Jump ahead:
.NET MAUI offers a write-once, run-anywhere experience, while still letting you access native, platform-specific APIs. Under the hood, .NET MAUI uses platform-specific frameworks for creating apps on different target devices:
The diagram below explains this in more detail:
For more insight into .NET MAUI’s architecture, you can also refer to the official documentation.
.NET MAUI is designed to deliver high-performance cross-platform applications. Below is a list of some key factors that contribute to its performance:
It’s important to note that while .NET MAUI offers performance improvements, the overall performance of an application also depends on factors such as the complexity of the UI, the efficiency of the code, network interactions, and data processing. Developers should follow best practices, apply performance profiling, and optimize critical sections of their code to achieve optimal performance with .NET MAUI.
Xamarin.Forms and .NET MAUI both allow developers to build cross-platform applications using C# and .NET. However, there are some key differences between the two. Let’s compare Xamarin.Forms and .NET MAUI across various aspects.
Xamarin.Forms is a UI toolkit that enables developers to create user interfaces for iOS, Android, and Windows using a single codebase. It simplifies cross-platform development by providing a shared UI abstraction layer. .NET MAUI is an evolution of Xamarin.Forms, extending it to support additional platforms and provide a more unified development experience.
Xamarin.Forms follows a page-centric architecture, where developers create pages and combine them to build the user interface. It relies on platform renderers to render the UI components on each platform.
.NET MAUI introduces a new architecture based on the Model-View-Update (MVU) pattern. It simplifies UI development by allowing engineers to define the application’s state and logic in a single code file, providing a more unified and reactive approach to UI development.
Xamarin.Forms offers good performance and allows for native-level access to platform-specific APIs. However, it may suffer from slightly slower startup times due to the need to initialize the Xamarin.Forms framework.
.NET MAUI improves on the performance and startup time of its predecessor. It achieves this through various performance optimizations and by reducing the overhead of the framework. Additionally, .NET MAUI introduces the concept of single-project and ahead-of-time (AOT) compilation, which can further improve performance.
Developers can use Xamarin.Forms to create apps for iOS, Android, and Windows platforms using a single, shared codebase. In addition to the platforms supported by Xamarin.Forms, .NET MAUI offers support for macOS and Tizen as well. It provides a more comprehensive cross-platform development experience.
Xamarin.Forms benefits from an established ecosystem and a mature set of development tools. It integrates with popular development environments like Visual Studio and offers a wide range of plugins and libraries to extend its capabilities.
.NET MAUI leverages the existing .NET ecosystem and tooling. It offers a more streamlined development experience, with improved tooling integration and a focus on productivity. It also benefits from the cross-platform capabilities of .NET, such as the ability to share code across different .NET platforms.
Existing Xamarin.Forms projects can be migrated to .NET MAUI using migration tools provided by Microsoft. However, there may be some effort involved in updating the codebase and adapting to the new patterns and APIs.
.NET MAUI was designed with backward compatibility in mind, aiming to provide a smooth upgrade path for existing Xamarin.Forms projects. This means developers can take advantage of new features and improvements without rewriting their entire application.
In summary, .NET MAUI builds upon the foundation of Xamarin.Forms, providing an enhanced cross-platform development experience with expanded platform support, improved performance, and a more modern architecture. If you are starting a new project or planning to migrate an existing Xamarin.Forms project, .NET MAUI would be the recommended choice.
While there are many cross-platform frameworks, Flutter and React Native are the most popular, used by developers across the globe.
If we compare these on the basis of community size and third-party library compatibility .NET MAUI is a less mature option, released in May 2022. Therefore, it can be challenging to get help from the community if you get stuck somewhere. Additionally, Visual Studio for Mac can be a little buggy and less performant than on Windows OS.
.NET MAUI mainly uses C# and XAML code, so if you’re already familiar with the .NET ecosystem, then MAUI can easily become your go-to framework. On the other hand, Flutter uses Dart, which is a programming language introduced by Google that has a significantly higher learning curve. Finally, we have React Native; developed by Facebook and built on top of JavaScript, React Native is a cross-platform application development framework.
Although all of these frameworks can deploy to Android, iOS, macOS, and Windows, some require extra tweaking to do so, like React Native, which uses react-native-windows
and react-native-macos
for Windows and macOS support. On the other hand, .NET MAUI comes with support for these out of the box.
However, unlike Flutter and React Native, you can’t deploy .NET MAUI apps directly on the web. As an added bonus for Flutter, Flutter apps can also be distributed and run on Linux based operating systems. Ultimately, it’s up to you to decide which framework is the best fit. But if you need a .NET ecosystem, .NET MAUI can surely be your framework of choice.
Before writing our .NET MAUI app, we first need to correctly install and set up .NET MAUI in our systems. Since I’m using macOS, I’ll guide you through the installation steps for macOS. If you’re using Windows, you can refer to the docs.
First, we’ll download Visual Studio 2022 for Mac. You’ll be prompted to add configuration for the installation; select .NET → .NET MAUI → iOS → Android. That’s it! Now, you should be able to run Visual Studio 2022 for Mac on your system.
When you open VS 2022, you’ll see a popup like the one below. To create a new project, click New:
Then, you’ll be prompted to select the project type. Select Multiplatform App and .NET MAUI App, then click Continue:
Select .NET 7.0 as the target framework and click Continue:
Enter the project name and uncheck the Put project in a subfolder
checkbox. Click Create to create your app:
Now, your project should be created, and VS 22 should be open. After selecting your preferred debug device or simulator from the list, move your mouse cursor to the top left corner of the app and click the play icon:
You’ll notice that the build times for Android and iOS are very fast. Your simulator will open the app with predefined dummy text on the screen:
And with that, you’ve just created your first .NET MAUI app for Android and iOS. Before we jump into the code, first, let’s understand what we’ll build.
Our example application will be fairly simple, displaying just two screens, including a list screen that will display a vertical list of products with images. When we tap an image, we’ll navigate to the Product Detail
screen, which will display all the information about that product. We’ll also use HTTP services to fetch the list of products from a REST API.
Our final app UI will look like the images below:
You can find the code for this tutorial in this GitHub repository.
CollectionView
XAML is the basic building block to create a UI in .NET MAUI; let’s create a UI to render a list of products. Before we start, we need to organize our code files so that all of our respective code chunks belong in a particular folder:
Views
: UI elements and screenModels
: Data structures and typesServices
: API handlers and business logicDelete the MainPage.xaml
file and its code-behind file, or the .cs
file associated with that file. In this case, it’s MainPage.xaml.cs
.
Create a new folder inside Project
by right-clicking on the project in the Solution Explorer
window and naming it Views
. In the next step, we’ll add the ProductList
file inside this folder:
Right click on the created Views
folder and select Add → New Class:
You’ll be prompted to select the type of file you want to create. Select .Net MAUI → .NET MAUI ContentPage(XAML). Change file name to ProductList
and press Create:
This will create two files, ProductList.xaml
and its code-behind file, ProductList.xaml.cs
, which will contain all of its code logic.
Before writing any code in ProductList.xaml
, we first need to update our AppShell.xaml
file, which is the root of our UI, and as the name suggests, is the Shell
or structure of our app’s UI:
<?xml version="1.0" encoding="UTF-8" ?> <Shell x:Class="MAUIPostFeed.AppShell" xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" // Added views to refer to Views folder inside project xmlns:views="clr-namespace:MAUIPostFeed.Views" Shell.FlyoutBehavior="Disabled"> <ShellContent Title="Products" // Using views to access Views.ProductList file ContentTemplate="{DataTemplate views:ProductList}" /> </Shell>
Now that our page is set up correctly in AppShell
, let’s write the code in the ProductList.xaml
file:
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MAUIPostFeed.Views.ProductList" > <CollectionView x:Name="productsCollection" ItemsSource="{Binding Products}" Margin="10" SelectionMode="Single" SelectionChanged="productsCollection_SelectionChanged" > <CollectionView.ItemsLayout> <LinearItemsLayout Orientation="Vertical" ItemSpacing="20" /> </CollectionView.ItemsLayout> <CollectionView.ItemTemplate> <DataTemplate> <VerticalStackLayout> <Image HeightRequest="180" Aspect="AspectFill" > <Image.Source> <UriImageSource Uri="{Binding thumbnail}" CacheValidity="00:12:00:00" /> </Image.Source> </Image> <HorizontalStackLayout> <Label Text="{Binding title}" FontSize="Title" FontAttributes="Bold" /> <Label Text="{Binding discountPercentage}" FontSize="Default" Margin="40,4,0,0" FontAttributes="Italic" FontFamily="Open-Sans" /> <Label Text=" % OFF" FontSize="Default" Margin="0,4,0,0" FontAttributes="Italic" FontFamily="Open-Sans" /> </HorizontalStackLayout> <Label Text="{Binding description}" MaxLines="2" FontSize="Subtitle" TextColor="Gray" /> </VerticalStackLayout> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> </ContentPage>
In the code above, we added x:Class="MAUIPostFeed.Views.ProductList"
to give a class name to ContentPage
so that we can access its Controls
properties from the code-behind file.
We then created a CollectionView
with ItemSource = Products
, which comes from Binding
. Binding is a mechanism in XAML through which XAML and code-behind classes can communicate. We haven’t yet created Products
; we’ll do that in the next step. Finally, we used CollectionView.ItemTemplate
to define how a list item should render.
With that, the UI part for the list page is done. Now, let’s work on actually setting up an HTTP service to fetch products from the REST API and store it in ObservableCollection
.
Let’s start with the code-behind file of ProductList
. Paste the following code in the ProductList.xaml.cs
file:
using MAUIPostFeed.Models; namespace MAUIPostFeed.Views; public partial class ProductList : ContentPage { public ProductList() { InitializeComponent(); // Initializing the BindingContext with Products BindingContext = new Models.AllProducts(); } }
In the code above, we set BindingContext
to Models.AllProducts
. But, we haven’t created any models yet, so let’s do that now.
Add a new folder to your project named Models
. Inside, create a new class called AllProducts.cs
. This model will contain the list of Products
we want to display. Paste the following code inside the AllProducts.cs
file:
using System.Collections.ObjectModel; using MAUIPostFeed.Services; namespace MAUIPostFeed.Models { public class AllProducts { public ObservableCollection<Product> Products { get; set; } = new ObservableCollection<Product>(); readonly IProductsRepository ProductsRepository = new ProductsService(); public AllProducts() => LoadProducts(); public async void LoadProducts() { ObservableCollection<Product> temp = await ProductsRepository.LoadProducts(); for (int i = 0; i < temp.Count; i++) { Products.Add(temp[i]); } } } }
In the code above, we created a class called AllProducts
and added a Products
property, which is an ObservableCollection
of Product
. We’ll create the Product
class later.
Next, we added a ProductsRepository
property on type IProductsRepository
, which instantiates with ProductService
. We’ll create these classes in the next steps.
In the constructor of the class, we have called the LoadProducts
method, which fetches the Product
list from ProductsRepository
. Then, we push each product into our class property Products
.
We need one more Model
in our code, which will hold the structure of Products
. Add a new class in Models
and name it Products.cs
. Then, add the following code into the new class:
using System; using System.Collections.ObjectModel; namespace MAUIPostFeed.Models; public class Product { public int id { get; set; } public string title { get; set; } public string description { get; set; } public int price { get; set; } public double discountPercentage { get; set; } public double rating { get; set; } public int stock { get; set; } public string brand { get; set; } public string category { get; set; } public string thumbnail { get; set; } public List<string> images { get; set; } } public class Products { public ObservableCollection<Product> products { get; set; } public int total { get; set; } public int skip { get; set; } public int limit { get; set; } }
In the file above, we’ve created two classes. The first, Product
, holds the structure of a single Product
. The second, Products
, contains a Product
list and some other data that we get from the API.
With that, we’re done with Models
. Now, let’s create Services
, which will interact with the network and fetch products.
In the project, add a new folder called Services
. Inside it, create a new class called IProductsRepository.cs
and add the following code inside the file:
using System; using System.Collections.ObjectModel; using MAUIPostFeed.Models; namespace MAUIPostFeed.Services { public interface IProductsRepository { Task<ObservableCollection<Product>> LoadProducts(); } }
In the code above, we’ve created an IProductsRepository
interface, which has a LoadProducts
property, or a Task
that returns the ObservableCollection
of Product
.
Next, we need to create the ProductsService
class, which will implement the IProductsRepository
class and fetch Products
using HTTPClient
. Create a new class called ProductsService.cs
and add the following code to it:
using System; using System.Collections.ObjectModel; using System.Diagnostics; using System.Text.Json; using MAUIPostFeed.Models; namespace MAUIPostFeed.Services; public class ProductsService: IProductsRepository { HttpClient client; JsonSerializerOptions serializerOptions; public ObservableCollection<Product> Products { get; set; } private static string BASE_URL { get; set; } = "https://dummyjson.com/"; public ProductsService() { client = new HttpClient(); serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true }; } public async Task<ObservableCollection<Product>> LoadProducts() { Products = new ObservableCollection<Product>(); Uri uri = new Uri(string.Format($"{BASE_URL}products?limit=10", string.Empty)); try { HttpResponseMessage response = await client.GetAsync(uri); if (response.IsSuccessStatusCode) { string content = await response.Content.ReadAsStringAsync(); Products temp = JsonSerializer.Deserialize<Products>(content, serializerOptions); Products = temp.products; } } catch (Exception ex) { Debug.WriteLine(@"\tERROR {0}", ex.Message); } return Products; } }
As you may have noticed, we’re using the dummyjson
endpoint to get a list of Products
. We used the GetAsync
method from HttpClient
to call a GET
request on the specified API endpoint. Then, we Deserialized
it using our Products
class and assigned only the products
received from the API response.
That’s it. Rebuild your app, and you’ll see a list of 10 products on the screen:
Details
pageAt this point, we can see a list of products on the Products
page. When the user clicks on any product, they should be navigated to the ProductDetails
page; all the relevant product info should be visible there.
To implement this functionality, we’ll pass a Product
as a NavigationParam
to the ProductDetails
page.
Create a new .NET MAUI content page inside the Views
folder and name it ProductDetails
. Paste the following code inside ProductDetails.xaml
:
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MAUIPostFeed.Views.ProductDetails" Title="Product Details" > <ScrollView BackgroundColor="#eee" > <VerticalStackLayout> <CarouselView Loop="False" ItemsSource="{Binding product.images}" VerticalOptions="Start" HeightRequest="300" HorizontalScrollBarVisibility="Never" > <CarouselView.ItemTemplate> <DataTemplate> <Image Source="{Binding}" Aspect="AspectFit" HeightRequest="300" /> </DataTemplate> </CarouselView.ItemTemplate> </CarouselView> <VerticalStackLayout Margin="24, 10" Spacing="10" > <Label Text="{Binding product.title}" FontSize="Title" FontFamily="OpenSansSemibold" /> <Label Text="{Binding product.description}" FontSize="Body" FontFamily="OpenSansRegular" /> <Grid ColumnDefinitions="*,*" ColumnSpacing="30" Margin="0, 10" > <HorizontalStackLayout> <Label Text="💵 $" FontSize="30" VerticalTextAlignment="Center" FontFamily="OpenSansRegular" /> <Label Text="{Binding product.price}" FontSize="40" FontFamily="OpenSansRegular" /> </HorizontalStackLayout> <HorizontalStackLayout Grid.Column="1"> <Label Text="⭐️ " FontSize="30" VerticalTextAlignment="Center" FontFamily="OpenSansRegular" /> <Label Text="{Binding product.rating}" FontSize="40" FontFamily="OpenSansRegular" /> </HorizontalStackLayout> </Grid> <HorizontalStackLayout> <Label Text="Stocks left: " FontSize="20" VerticalTextAlignment="Center" FontFamily="OpenSansRegular" /> <Label Text="{Binding product.stock}" FontSize="30" FontFamily="OpenSansRegular" /> </HorizontalStackLayout> <HorizontalStackLayout> <Label Text="Brand: " FontSize="20" VerticalTextAlignment="Center" FontFamily="OpenSansRegular" /> <Label Text="{Binding product.brand}" FontSize="30" FontFamily="OpenSansRegular" /> </HorizontalStackLayout> <HorizontalStackLayout> <Label Text="Category: " FontSize="20" VerticalTextAlignment="Center" FontFamily="OpenSansRegular" /> <Label Text="{Binding product.category}" TextTransform="Uppercase" FontSize="30" FontFamily="OpenSansRegular" /> </HorizontalStackLayout> </VerticalStackLayout> </VerticalStackLayout> </ScrollView> </ContentPage>
In the code above, we wrapped the complete UI in a ScrollView
and used a CarouselView
to display a list of images
of the Product
. We then used a VerticalStackLayout
to display all the relevant information about the product using Label
.
However, because we haven’t mapped the navigation params that we got from Navigation
with the BindingContext
, this won’t work. To do so, add the following code in ProductDetails.xaml.cs
:
using System.ComponentModel; using MAUIPostFeed.Models; namespace MAUIPostFeed.Views; [QueryProperty(nameof(Product), "product")] public partial class ProductDetails : ContentPage, IQueryAttributable, INotifyPropertyChanged { public Product product { get; private set; } public void ApplyQueryAttributes(IDictionary<string, object> query) { product = query["product"] as Product; OnPropertyChanged("product"); } public ProductDetails() { InitializeComponent(); BindingContext = this; Console.WriteLine("product inside details:" + product + "BINDINGCONTEXT::" + BindingContext); } }
In the code above, we used IQueryAttributable
and its ApplyQueryAttributes
to set the product
property to the value that we get from the QueryProperty
.
Finally, add the navigation logic inside the ProductList
page. Add the SelectionChanged
event handler in the CollectionView
of products
:
<CollectionView x:Name="productsCollection" ItemsSource="{Binding Products}" Margin="10" SelectionMode="Single" SelectionChanged="productsCollection_SelectionChanged" >
Now that we’ve passed an eventHandler
, let’s create the eventHandler
in the code-behind file. Add the method below in ProductList.xaml.cs
:
using MAUIPostFeed.Models; using MAUIPostFeed.Services; namespace MAUIPostFeed.Views; public partial class ProductList : ContentPage { public ProductList() { InitializeComponent(); BindingContext = new Models.AllProducts(); } async void productsCollection_SelectionChanged(System.Object sender, Microsoft.Maui.Controls.SelectionChangedEventArgs e) { if (e.CurrentSelection.Count != 0) { Product product = e.CurrentSelection.FirstOrDefault() as Product; var navigationParams = new Dictionary<string, object> { { "product", product } }; await Shell.Current.GoToAsync("ProductDetails", navigationParams); productsCollection.SelectedItem = null; } } }
In the code above, we’re navigating to the ProductDetails
page when any item in CollectionView
is selected. Then, we’re setting the selectedItem
of the productsCollection
list to null
, resetting the UI.
If you run this code now, it won’t work because we haven’t yet registered the ProductDetails
page in Router1
. To do so, paste the following code in AppShell.xaml.cs
:
using MAUIPostFeed.Views; namespace MAUIPostFeed; public partial class AppShell : Shell { public AppShell() { InitializeComponent(); Routing.RegisterRoute("ProductDetails", typeof(ProductDetails)); } }
With that, our app is complete. Now, if you build and run the project, the output will be similar to the image below:
In this article, we learned how to create cross-platform apps for iOS and Android using .NET MAUI. But, .NET MAUI isn’t limited to just iOS and Android; we could also deploy our application on macOS and Windows using the same codebase.
.NET MAUI is a performant framework that is great for building cross-platform apps, especially if you want to remain in a .NET ecosystem without having to learn a new framework. I hope you enjoyed this article, and thanks for reading!
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.