Five-minute walkthrough to spin up a cross‑platform MAUI app in VS Code that takes a city name and shows live weather using a lightweight public API. macOS leads the way with notes for Windows and Linux.

Set up your tools

Install the .NET 8 SDK or newer and Visual Studio Code. In VS Code, add the C# Dev Kit and C# extensions. On macOS, install Xcode for the iOS Simulator and Android Studio for the Android SDK and Emulator. On Windows, install Android Studio and create an emulator device; you can also target WinUI for a desktop build. Linux is not officially supported for MAUI targets, but you can still follow the shared code and build Android elsewhere.

Install MAUI workloads

Use the .NET CLI to install MAUI once the platform SDKs are in place.

dotnet --info

dotnet workload install maui

dotnet workload list

Create the project

Create a new MAUI project, change into the folder, and confirm a clean build before opening VS Code.

dotnet new maui -n MauiWeather
cd MauiWeather

dotnet build

Design a tiny UI in XAML

Replace the default page with an entry box for the city, a button to fetch weather, and an area for results and status.

MainPage.xaml

    
        
        
        

        
            
                
                
                
                
                
                
            
        
    

Call a free weather service

Open‑Meteo’s APIs are ideal for demos because they need no API key and respond quickly. The service below geocodes the city to latitude and longitude and then fetches the current weather.

Services/WeatherService.cs

using System.Net.Http.Json;
using System.Text.Json;

namespace MauiWeather.Services;

public class WeatherService
{
    private readonly HttpClient _http = new();

    public async Task<(double? TempC, string? Conditions, string? Location, DateTimeOffset? Time)> GetCurrentAsync(string city, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(city))
            return (null, "Enter a city.", null, null);

        // 1) Geocode city → lat/lon
        var geoUrl = $"https://geocoding-api.open-meteo.com/v1/search?name={Uri.EscapeDataString(city)}&count=1";
        using var geo = await _http.GetAsync(geoUrl, ct);
        if (!geo.IsSuccessStatusCode)
            return (null, $"Geocoding failed: {geo.StatusCode}", null, null);

        using var geoStream = await geo.Content.ReadAsStreamAsync(ct);
        var geoDoc = await JsonDocument.ParseAsync(geoStream, cancellationToken: ct);
        var first = geoDoc.RootElement.GetProperty("results").EnumerateArray().FirstOrDefault();
        if (first.ValueKind == JsonValueKind.Undefined)
            return (null, "City not found.", null, null);

        var lat = first.GetProperty("latitude").GetDouble();
        var lon = first.GetProperty("longitude").GetDouble();
        var locationName = first.GetProperty("name").GetString();
        var country = first.TryGetProperty("country", out var c) ? c.GetString() : null;
        var displayLocation = country is null ? locationName : $"{locationName}, {country}";

        // 2) Current weather
        var wxUrl = $"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}&current_weather=true";
        using var wx = await _http.GetAsync(wxUrl, ct);
        if (!wx.IsSuccessStatusCode)
            return (null, $"Weather failed: {wx.StatusCode}", displayLocation, null);

        using var wxStream = await wx.Content.ReadAsStreamAsync(ct);
        var wxDoc = await JsonDocument.ParseAsync(wxStream, cancellationToken: ct);
        var current = wxDoc.RootElement.GetProperty("current_weather");
        var temp = current.GetProperty("temperature").GetDouble();
        var time = DateTimeOffset.Parse(current.GetProperty("time").GetString()!);
        var code = current.TryGetProperty("weathercode", out var wc) ? wc.GetInt32() : -1;
        var cond = code switch
        {
            0 => "Clear",
            1 or 2 or 3 => "Partly cloudy",
            45 or 48 => "Foggy",
            51 or 53 or 55 => "Drizzle",
            61 or 63 or 65 => "Rain",
            71 or 73 or 75 => "Snow",
            95 or 96 or 99 => "Thunderstorm",
            _ => "—"
        };

        return (temp, cond, displayLocation, time);
    }
}

Bind it all together with a ViewModel

The ViewModel exposes bindable properties and a command to perform the lookup, manages a simple busy state, and prints plain-language errors. Register it and the service for dependency injection, then set the BindingContext in the page’s constructor.

ViewModels/MainViewModel.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using MauiWeather.Services;

namespace MauiWeather.ViewModels;

public class MainViewModel : INotifyPropertyChanged
{
    private readonly WeatherService _weather;

    public MainViewModel(WeatherService weather)
    {
        _weather = weather;
        GetWeatherCommand = new Command(async () => await GetWeatherAsync());
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    void OnPropertyChanged([CallerMemberName] string? name = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

    string _city = string.Empty;
    public string City { get => _city; set { _city = value; OnPropertyChanged(); } }

    bool _isBusy;
    public bool IsBusy { get => _isBusy; set { _isBusy = value; OnPropertyChanged(); } }

    string _status = "Ready";
    public string Status { get => _status; set { _status = value; OnPropertyChanged(); } }

    string _temp = string.Empty;
    public string TemperatureDisplay { get => _temp; set { _temp = value; OnPropertyChanged(); } }

    string _conditions = string.Empty;
    public string Conditions { get => _conditions; set { _conditions = value; OnPropertyChanged(); } }

    string _location = string.Empty;
    public string Location { get => _location; set { _location = value; OnPropertyChanged(); } }

    string _updated = string.Empty;
    public string Updated { get => _updated; set { _updated = value; OnPropertyChanged(); } }

    public ICommand GetWeatherCommand { get; }

    async Task GetWeatherAsync()
    {
        if (IsBusy) return;
        try
        {
            IsBusy = true;
            Status = "Looking up weather...";
            TemperatureDisplay = Conditions = Location = Updated = string.Empty;

            var (tempC, cond, loc, time) = await _weather.GetCurrentAsync(City);
            if (tempC is null)
            {
                Status = cond ?? "No result.";
                return;
            }

            TemperatureDisplay = $"{tempC:F1} °C";
            Conditions = cond ?? string.Empty;
            Location = loc ?? City;
            Updated = time is null ? string.Empty : $"Updated {time:yyyy-MM-dd HH:mm}";
            Status = "Done";
        }
        catch (Exception ex)
        {
            Status = $"Error: {ex.Message}";
        }
        finally { IsBusy = false; }
    }
}

MainPage.xaml.cs

using MauiWeather.ViewModels;

namespace MauiWeather;

public partial class MainPage : ContentPage
{
    public MainPage(MainViewModel vm)
    {
        InitializeComponent();
        BindingContext = vm;
    }
}

MauiProgram.cs

using MauiWeather.Services;
using MauiWeather.ViewModels;

namespace MauiWeather;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

        builder.Services.AddSingleton();
        builder.Services.AddSingleton();

        return builder.Build();
    }
}

Android needs internet permission

Add the INTERNET permission inside the Android manifest. iOS and Mac Catalyst do not need extra entitlements for simple HTTPS calls.


Run the app

Start the simulator or emulator first, then use the CLI to target a platform.

iOS Simulator (macOS)

dotnet build -t:Run -f net8.0-ios

Android Emulator (macOS or Windows)

dotnet build -t:Run -f net8.0-android

Mac Catalyst (macOS desktop)

dotnet build -t:Run -f net8.0-maccatalyst

Windows (WinUI desktop)

dotnet build -t:Run -f net8.0-windows10.0.19041.0

Try it

Enter a city such as Brisbane, tap the button, and the app will display the temperature, a readable condition, and an updated timestamp.

Troubleshooting and next steps

If no devices appear, open the simulator or emulator first. On Android, open Android Studio once to complete SDK setup. If iOS signing blocks you on a physical device, stick to the Simulator while you learn. Next, add a forecast view, store the last city with Preferences, or swap to a richer API with icons.