Hello,
Combining MAUI's built-in Image
control with an ActivityIndicator
gives you everything you need:
<Grid>
<!-- Standard Image Control -->
<Image Source="{Binding ImageSource}"
IsVisible="{Binding IsImageVisible}" />
<!-- Loading Indicator -->
<ActivityIndicator IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}" />
</Grid>
I've put together a working example that handles all the common scenarios - loading, success, errors, and even lets users input different image URLs to test with. Here's how it looks:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:ImageLoadingTest2.ViewModels"
x:Class="ImageLoadingTest2.MainPage">
<ContentPage.BindingContext>
<viewModels:ImageLoadingViewModel/>
</ContentPage.BindingContext>
<VerticalStackLayout Padding="20" Spacing="20">
<!-- URL Input -->
<Entry
Text="{Binding CurrentImageUrl}"
Placeholder="Enter image URL"
FontSize="14" />
<!-- Load Button -->
<Button
Text="Load Image"
Command="{Binding LoadImageCommand}"
BackgroundColor="DodgerBlue"
TextColor="White" />
<!-- Image Container with Loading State -->
<Border
WidthRequest="300"
HeightRequest="300"
BackgroundColor="White"
Stroke="LightGray"
StrokeThickness="1">
<Grid>
<!-- Loading State Background -->
<BoxView
BackgroundColor="LightGray"
Opacity="0.2"
IsVisible="{Binding IsLoading}" />
<!-- Standard Image Control -->
<Image
x:Name="MainImage"
Source="{Binding ImageSource}"
Aspect="AspectFit"
IsVisible="{Binding IsImageVisible}"
BackgroundColor="Transparent" />
<!-- Loading ActivityIndicator -->
<ActivityIndicator
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"
Color="DodgerBlue"
HeightRequest="40"
WidthRequest="40"
HorizontalOptions="Center"
VerticalOptions="Center" />
<!-- Loading Text -->
<Label
Text="Loading image..."
IsVisible="{Binding IsLoading}"
FontSize="12"
TextColor="Gray"
HorizontalOptions="Center"
VerticalOptions="End"
Margin="0,0,0,20" />
</Grid>
</Border>
</VerticalStackLayout>
</ContentPage>
I've tried to keep the view model simple:
public class ImageLoadingViewModel : INotifyPropertyChanged
{
private bool isLoading = false;
private bool isImageVisible = false;
private ImageSource imageSource;
private string currentImageUrl = "https://picsum.photos/id/15/4000/3000";
private readonly HttpClient _httpClient;
public bool IsLoading
{
get => isLoading;
set { isLoading = value; OnPropertyChanged(); }
}
public bool IsImageVisible
{
get => isImageVisible;
set { isImageVisible = value; OnPropertyChanged(); }
}
public ImageSource ImageSource
{
get => imageSource;
set { imageSource = value; OnPropertyChanged(); }
}
public string CurrentImageUrl
{
get => currentImageUrl;
set { currentImageUrl = value; OnPropertyChanged(); }
}
public ICommand LoadImageCommand { get; }
public ImageLoadingViewModel()
{
_httpClient = new HttpClient();
LoadImageCommand = new Command(async () => await LoadImageAsync());
// Auto-load image on startup
_ = LoadImageAsync();
}
private async Task LoadImageAsync()
{
try
{
// Set loading state immediately - hides current image
IsImageVisible = false;
IsLoading = true;
// Download image data to verify it exists and is valid
using var response = await _httpClient.GetAsync(CurrentImageUrl);
response.EnsureSuccessStatusCode();
// Read the image data
var imageData = await response.Content.ReadAsByteArrayAsync();
// Create ImageSource from downloaded data
ImageSource = ImageSource.FromStream(() => new MemoryStream(imageData));
// Show the image
IsLoading = false;
IsImageVisible = true;
}
catch (Exception ex)
{
// Handle loading errors
IsLoading = false;
IsImageVisible = false;
System.Diagnostics.Debug.WriteLine($"Error loading image: {ex.Message}");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
A simple command implementation:
public class Command : ICommand
{
private readonly Func<Task> _execute;
private readonly Func<bool> _canExecute;
public Command(Func<Task> execute, Func<bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true;
public async void Execute(object parameter) => await _execute();
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
If you prefer, there is a community fork of FFImageLoading
called FFImageLoading.Maui
that brings some of the familiar API to MAUI.
I hope this helps you! Let me know if you have questions about any part of the implementation.
For reference: