Xamarin
Xamarin.Forms

[Xamarin.Forms] CachingStrategyでListViewを高速化しよう

この記事は、[初心者さん・学生さん大歓迎!] Xamarin その1 Advent Calendar 2017の23日目の記事です。

高速化の方法

この記事ではListViewのCaching Strategyを指定することで、ListViewを高速化します。詳しく知りたい方はこちらを参照してください。https://developer.xamarin.com/guides/xamarin-forms/user-interface/listview/performance/

ViewCell(MyCell)

テキスト1つと画像4つで構成されたViewCellです。大きい3枚の画像は常に同じ画像の順で描画し、左上の小さい画像は3枚の画像からランダムに選んだものを描画しています。(画像はFLAT ICON DESIGNからお借りしました)

XAML

MyCell.xaml
<ViewCell 
    xmlns="http://xamarin.com/schemas/2014/forms" 
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="ViewCellTest.MyCell">
    <ViewCell.View>
        <StackLayout Orientation="Horizontal">
            <Image
                x:Name="iconImage"
                VerticalOptions="StartAndExpand"
                WidthRequest="50"
                HeightRequest="50"/>
            <StackLayout Orientation="Vertical">
                <Label
                    x:Name="titleLabel"
                    FontSize="Large"/>
                <StackLayout Orientation="Horizontal">
                    <!-- 大きな3つの画像はここでSourceを指定 -->
                    <Image
                        WidthRequest="100"
                        HeightRequest="100"
                        Source="image1.png"/>
                    <Image
                        WidthRequest="100"
                        HeightRequest="100"
                        Source="image2.png"/>
                    <Image
                        WidthRequest="100"
                        HeightRequest="100"
                        Source="image3.png"/>
                </StackLayout>
            </StackLayout>
        </StackLayout>
    </ViewCell.View>
</ViewCell>

コードビハインド

MyCell.xaml.cs
public partial class MyCell : ViewCell
{
    public static readonly BindableProperty IconImageSourceProperty = BindableProperty.Create(
        nameof(IconImageSource),
        typeof(ImageSource),
        typeof(MyCell),
        propertyChanged: (b, o, n) => (b as MyCell).iconImage.Source = n as ImageSource);

    public static readonly BindableProperty TitleProperty = BindableProperty.Create(
        nameof(Title),
        typeof(string),
        typeof(MyCell),
        propertyChanged: (b, o, n) => (b as MyCell).SetTitle(n as string));

    public ImageSource IconImageSource
    {
        get { return (ImageSource)GetValue(IconImageSourceProperty); }
        set { SetValue(IconImageSourceProperty, value); }
    }

    public string Title
    {
        get { return (string)GetValue(TitleProperty); }
        set { SetValue(TitleProperty, value); }
    }

    // このようにメソッドからテキストをセットしてもOK
    private void SetTitle(string title)
    {
        titleLabel.Text = title;
    }

    protected override void OnBindingContextChanged()
    {
        base.OnBindingContextChanged();
        // BindablePropertyのpropertyChangedを使わないなら、ここでコントロールの設定をする
    }

    public MyCell()
    {
        InitializeComponent();
        Console.WriteLine("MyCellが生成されました");
    }
}

BindablePropertyのpropertyChangedかオーバーライドしたOnBindingContextChanged()のどちらかで、XAMLで定義したコントロールのプロパティを設定してあげましょう。

ListViewを表示するページ(MainPage)

MyCellを用いたListViewを表示するMainPageを作ります。コードビハインドはInitializeComponent()以外何も書いていないので、省略します。

MyCellに使うデータクラス

CellItem.cs
public class CellItem
{
    public ImageSource IconImageSource { get; set; }
    public string Title { get; set; }

    public CellItem(string iconImageName, string title)
    {
        IconImageSource = ImageSource.FromFile(iconImageName);
        Title = title;
    }
}

XAML

ItemsSourceはMainViewModelのItemsとバインドしてます。

MainPage.xaml
<ContentPage 
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:ViewCellTest"
    x:Class="ViewCellTest.MainPage">

    <ContentPage.BindingContext>
        <local:MainViewModel/>
    </ContentPage.BindingContext>

    <ListView
        ItemsSource="{Binding Items}"
        HasUnevenRows="True">
        <ListView.ItemTemplate>
            <DataTemplate>
                <local:MyCell
                    IconImageSource="{Binding IconImageSource}"
                    Title="{Binding Title}"/>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

ViewModel

データの生成をコンストラクタでしているだけです。

MainViewModel.cs
public class MainViewModel
{
    public ObservableCollection<CellItem> Items { get; set; }

    public MainViewModel()
    {
        var random = new Random();
        // データ生成。画像はimage1.png, image2.png, image3.pngの三枚を用意した 
        var data = Enumerable.Range(1, 100)
            .Select(x => new CellItem($"image{random.Next(1, 4)}.png", $"MyCell{x}"));
        Items = new ObservableCollection<CellItem>(data);
    }
}

実行結果

プログラムが完成したので起動してみるとこんな感じになります。
スクロールに画像の読み込みが追いついてませんね。それではこれを高速化していきます。

RecycleElementの指定

MainPage.xaml
<ContentPage 
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:ViewCellTest"
    x:Class="ViewCellTest.MainPage">

    <ContentPage.BindingContext>
        <local:MainViewModel/>
    </ContentPage.BindingContext>

    <ListView
        ItemsSource="{Binding Items}"
        HasUnevenRows="True"
        CachingStrategy="RecycleElement"> <!-- 新しく追加 -->
        <ListView.ItemTemplate>
            <DataTemplate>
                <local:MyCell
                    IconImageSource="{Binding IconImageSource}"
                    Title="{Binding Title}"/>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

ListViewにCachingStrategy="RecycleElement"(デフォルトはRetainElement)を指定してあげるだけです。こうすることで、生成されたMyCellクラスのインスタンスを使いまわしながらBindingContextを切り替えてくれるので、MyCell生成にかかるコストを削減でき、読み込みが速くなります。

MyCellのコンストラクタに書いたConsole.WriteLine("MyCellが生成されました");より、初回のMyCellの生成後は、インスタンスが使い回されていることが確認できます。

おわりに

並べて比べて見ると、これだけ差があります。

RetainElement(デフォルト) RecycleElement

今回作成したMyCellではCachingStrategyにRecycleElementを指定することで高速化が出来ましたが、どのようなCellでもRecycleElementで高速化出来るとは限りません。データバインディングの数が20以上などの場合には、デフォルトのRetainElementの方が適しているみたいです。作成したCellに合わせてCachingStrategyを適切に設定する必要があります。