Start Debugging

Migrando uma ListView de alto desempenho do Xamarin.Forms para CollectionView do MAUI

Migração passo a passo da ListView do Xamarin.Forms 5.0 para a CollectionView do .NET MAUI 11 para apps que já espremiam desempenho da ListView. Cobre reciclagem de células, virtualização, agrupamento, pull-to-refresh, ações de contexto, seleção, ItemsLayout, EmptyView e as armadilhas que pegam apps reais.

A versão curta: substitua ListView por CollectionView envolto em um RefreshView se você precisava de pull-to-refresh, troque ContextActions por SwipeView, descarte HasUnevenRows, RowHeight e CachingStrategy porque virtualização com reciclagem de células é o único modo no MAUI, e reescreva a seleção de ItemTapped / ItemSelected para SelectionMode mais SelectionChangedCommand. Se sua ListView antiga estava ajustada com CachingStrategy="RecycleElement", HasUnevenRows="True" e um DataTemplateSelector, a mudança é majoritariamente mecânica. Testado com .NET MAUI 11.0 GA em net11.0-android35.0, net11.0-ios18.0 e net11.0-maccatalyst18.0. Espere de um a três dias de trabalho focado para um app com cinco a dez listas, mais tempo se você depender muito de agrupamento estilo TableView ou de renderers customizados de ViewCell.

O Xamarin.Forms saiu de suporte em 1 de maio de 2024, e o MAUI 11 GA chegou em novembro de 2025 com a terceira geração de correções da CollectionView para travamentos no Android. Se você estava adiando porque o desempenho do seu app foi ajustado manualmente em torno de peculiaridades da ListView como RecycleElementAndDataTemplate, vale a pena fazer a atualização agora: a CollectionView no MAUI 11 finalmente iguala ou supera a antiga ListView do Xamarin.Forms no Android assim que você desliga ItemSizingStrategy.MeasureAllItems. Este guia é para esse público.

Por que migrar agora

O que quebra

ÁreaXamarin.Forms ListViewMAUI CollectionViewSeveridade
Estratégia de cacheCachingStrategy="RetainElement" ou RecycleElementSempre recicla. A propriedade não existe.média
Altura da linhaHasUnevenRows, RowHeightItemSizingStrategy="MeasureAllItems" ou MeasureFirstItemmédia
Pull to refreshIsPullToRefreshEnabled, RefreshCommand na ListViewEnvolva em RefreshViewalta
Tipo de célulaViewCell, TextCell, ImageCellDataTemplate simples com qualquer layout raizalta
Tratamento de toqueItemTapped, ItemSelectedSelectionMode, SelectionChanged, SelectionChangedCommandalta
Ações de swipeContextActionsSwipeViewalta
SeparadoresSeparatorVisibility, SeparatorColorNenhum. Adicione um BoxView no template.baixa
Cabeçalhos / rodapésHeader, Footer (objeto ou template)Mesmos nomes, mas apenas como templates / viewsbaixa
AgrupamentoIsGroupingEnabled, GroupHeaderTemplateIsGrouped, GroupHeaderTemplate, GroupFooterTemplatemédia
Scroll paraScrollTo(item, ScrollToPosition)ScrollTo(item, position, animate)baixa
Estado vazioLabel irmão manualEmptyView, EmptyViewTemplatebaixa

As duas mudanças que doem são pull-to-refresh e seleção. Tudo o mais é renomeação direta ou exclusão.

Checklist de pré-voo

  1. SDK do .NET 11 instalado (dotnet --version reporta 11.0.x). O MAUI 11 exige o SDK do .NET 11. SDKs anteriores não têm o manifesto do workload.
  2. Workload do MAUI instalado: dotnet workload install maui. Se você atualizou sobre uma instalação mais antiga, rode dotnet workload repair antes.
  3. Plataformas Android SDK 35, iOS 18 e Mac Catalyst 18 disponíveis. Visual Studio 2022 17.13 ou Visual Studio Code com a extensão .NET MAUI 1.6+.
  4. Um branch git que você possa descartar. Mantenha o projeto Xamarin.Forms compilando na main até o build do MAUI estar verde em um dispositivo real.
  5. Uma lista de cada instância de ListView no projeto. Agrupe-as por área de recurso; migre um recurso por vez e entregue atrás de uma feature flag se sua cadência de release é mensal.
  6. Rode o .NET Upgrade Assistant no projeto uma vez para tratar as mudanças de csproj, namespace e MauiProgram. O Upgrade Assistant não reescreve o XAML da ListView para você, que é o que o resto deste post cobre.

Passos da migração

1. Substitua ListView por CollectionView

O caso mais simples é uma lista plana com um DataTemplate customizado:

<!-- Before. Xamarin.Forms 5.0 -->
<ListView ItemsSource="{Binding Articles}"
          CachingStrategy="RecycleElementAndDataTemplate"
          HasUnevenRows="True"
          SeparatorVisibility="None"
          ItemTapped="OnArticleTapped">
    <ListView.ItemTemplate>
        <DataTemplate>
            <ViewCell>
                <Grid Padding="12" RowDefinitions="Auto,Auto">
                    <Label Text="{Binding Title}" FontAttributes="Bold" />
                    <Label Grid.Row="1" Text="{Binding Excerpt}" />
                </Grid>
            </ViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>
<!-- After. .NET MAUI 11.0 -->
<CollectionView ItemsSource="{Binding Articles}"
                SelectionMode="Single"
                SelectionChangedCommand="{Binding OpenArticleCommand}"
                SelectionChangedCommandParameter="{Binding Source={RelativeSource Self}, Path=SelectedItem}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="vm:ArticleVm">
            <Grid Padding="12" RowDefinitions="Auto,Auto">
                <Label Text="{Binding Title}" FontAttributes="Bold" />
                <Label Grid.Row="1" Text="{Binding Excerpt}" />
            </Grid>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

O que mudou:

Verifique: compile para Android (dotnet build -t:Run -f net11.0-android35.0) e role uma lista de 1000 itens em um dispositivo intermediário. O frame time deve ficar abaixo de 16 ms na visualização Profile GPU Rendering do Android Studio. Se não estiver, verifique se x:DataType está definido e se nenhum binding dentro do template usa Source={x:Reference ...}.

2. Envolva em RefreshView para pull-to-refresh

IsPullToRefreshEnabled, IsRefreshing e RefreshCommand não estão mais na lista. Eles vivem no RefreshView, que envolve qualquer conteúdo rolável.

<!-- After. .NET MAUI 11.0 -->
<RefreshView IsRefreshing="{Binding IsRefreshing}"
             Command="{Binding RefreshCommand}">
    <CollectionView ItemsSource="{Binding Articles}"
                    SelectionMode="Single">
        <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="vm:ArticleVm">
                <!-- as before -->
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</RefreshView>

O RefreshView só dispara quando o rolável envolto está no offset zero, então o comportamento bate com a antiga ListView. Não há IsPullToRefreshEnabled; se você precisa desativar, defina IsEnabled="False" no RefreshView.

Verifique: puxe para baixo na lista enquanto estiver no offset zero. O spinner da plataforma deve aparecer e RefreshCommand deve disparar exatamente uma vez por gesto.

3. Reescreva a seleção

O evento ItemTapped do Xamarin.Forms dispara em cada toque, mesmo quando SelectedItem não mudou. A CollectionView separa isso claramente: toque é seleção, e seleção é observável via SelectionChanged ou SelectionChangedCommand. Se você dependia do toque para navegar para o mesmo item duas vezes seguidas, precisa optar por sair do estado de seleção lendo e limpando SelectedItem após cada navegação:

// MainViewModel.cs, .NET MAUI 11.0, C# 14
[RelayCommand]
private async Task OpenArticleAsync(ArticleVm? selected)
{
    if (selected is null) return;
    await Shell.Current.GoToAsync(nameof(ArticleDetailPage),
        new Dictionary<string, object> { ["article"] = selected });
    SelectedArticle = null; // re-arm so the same row can fire next time
}

SelectionMode="None" mais um TapGestureRecognizer dentro do template é o equivalente mais próximo da antiga semântica de ItemTapped. Use só se você tiver um motivo. O padrão baseado em seleção é o que as plataformas esperam e é o que te dá anéis de foco de acessibilidade de graça no Windows.

Verifique: toque um item, navegue de volta, toque o mesmo item. Os dois toques devem abrir a página de detalhes.

4. Substitua ContextActions por SwipeView

<!-- Before. Xamarin.Forms 5.0 -->
<ViewCell>
    <ViewCell.ContextActions>
        <MenuItem Text="Delete" IsDestructive="True"
                  Command="{Binding DeleteCommand}" />
    </ViewCell.ContextActions>
    <Grid>...</Grid>
</ViewCell>
<!-- After. .NET MAUI 11.0 -->
<SwipeView>
    <SwipeView.RightItems>
        <SwipeItems Mode="Execute">
            <SwipeItem Text="Delete"
                       BackgroundColor="Crimson"
                       Command="{Binding DeleteCommand}" />
        </SwipeItems>
    </SwipeView.RightItems>
    <Grid>...</Grid>
</SwipeView>

SwipeView é um swipe real para esquerda e direita com fundos customizados. IsDestructive vira um BackgroundColor normal. Mode="Execute" corresponde ao comportamento antigo de descartar ao tocar; Mode="Reveal" mantém o menu aberto. O long-press do iOS para revelar um menu acabou; se você precisa, use MenuFlyout no MAUI 11 (novo nesta release).

Verifique: deslize para a esquerda no iOS e Android. Toque em delete. O item é removido. Deslize de novo na próxima linha para confirmar que apenas um swipe fica aberto por vez.

5. Troque o agrupamento

<!-- After. .NET MAUI 11.0 -->
<CollectionView ItemsSource="{Binding ArticlesByDay}"
                IsGrouped="True">
    <CollectionView.GroupHeaderTemplate>
        <DataTemplate x:DataType="vm:ArticleDayVm">
            <Label Text="{Binding Day, StringFormat='{0:D}'}"
                   FontAttributes="Bold"
                   Padding="12,8" />
        </DataTemplate>
    </CollectionView.GroupHeaderTemplate>
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="vm:ArticleVm">
            <!-- as before -->
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

Duas coisas mudam. A flag é IsGrouped, não IsGroupingEnabled. E a fonte precisa ser uma coleção de coleções onde cada item externo é, por sua vez, enumerável. O padrão mais limpo é uma classe Grouping<TKey, TItem> que deriva de List<TItem> e expõe Key. É o mesmo padrão que o Xamarin.Forms usava e ele porta sem alterações.

Verifique: role uma lista agrupada por 30 dias. Os cabeçalhos de grupo devem grudar no topo do seu grupo no iOS por padrão (GroupHeadersStick="True" no nativo do iOS; o padrão do MAUI espelha isso).

6. Escolha um ItemsLayout

O padrão é LinearItemsLayout vertical, que combina com a ListView. Mude para uma grade com uma única linha:

<CollectionView ItemsSource="{Binding Photos}">
    <CollectionView.ItemsLayout>
        <GridItemsLayout Orientation="Vertical"
                         Span="3"
                         HorizontalItemSpacing="4"
                         VerticalItemSpacing="4" />
    </CollectionView.ItemsLayout>
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="vm:PhotoVm">
            <Image Source="{Binding ThumbnailUrl}" Aspect="AspectFill" />
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

Span="3" é uma contagem fixa de colunas. Não há grade adaptativa embutida; para isso você dimensiona as células com base na largura disponível usando um behaviour ou refazendo o bind em mudanças de tamanho.

Verifique: gire o dispositivo. A grade deve ser redesenhada sem perder a posição de scroll.

7. Adicione um EmptyView

<CollectionView ItemsSource="{Binding Articles}">
    <CollectionView.EmptyView>
        <VerticalStackLayout HorizontalOptions="Center"
                             VerticalOptions="Center" Spacing="8">
            <Label Text="No articles yet" FontSize="18" />
            <Button Text="Refresh" Command="{Binding RefreshCommand}" />
        </VerticalStackLayout>
    </CollectionView.EmptyView>
</CollectionView>

Para múltiplos estados vazios (sem dados vs erro), faça o bind de EmptyView a uma propriedade e use EmptyViewTemplate com um DataTemplateSelector.

Verifique: limpe a fonte. A empty view aparece. Reabasteça a fonte. A empty view some sem um frame de sobreposição.

Checklist de verificação

Depois que cada lista é migrada:

Plano de rollback

Esta é, na prática, uma migração de mão única. Uma vez que o projeto está nos target frameworks net11.0-* e no MauiProgram.cs, voltar para o Xamarin.Forms significa restaurar o formato antigo do csproj, o Xamarin.Forms.Forms.Init e os pontos de entrada do app específicos da plataforma. Nada neste post sobre a CollectionView em si é reversível separadamente da atualização do projeto. Planeje entregar um build do MAUI funcionando antes de deletar o branch do Xamarin.Forms.

Armadilhas que pegamos

Relacionados

Fontes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Voltar