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}¤t_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.
Leave A Comment