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

この記事は、[[初心者さん・学生さん大歓迎！] Xamarin その1 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/xamarin-student)の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](http://flat-icon-design.com/)からお借りしました)

<img src=https://qiita-image-store.s3.amazonaws.com/0/152199/cebf068a-eb18-677a-3df6-bd5a197ec0fa.png width=50%>

## XAML

```xml: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>
```

## コードビハインド

```C#: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に使うデータクラス

```C#: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とバインドしてます。

```xml: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
データの生成をコンストラクタでしているだけです。

```C#: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);
    }
}
```

# 実行結果
<img src=https://qiita-image-store.s3.amazonaws.com/0/152199/a949a8c4-9131-4c11-81e8-4e14935f7297.gif width=50%>

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

# RecycleElementの指定

```xml: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生成にかかるコストを削減でき、読み込みが速くなります。

<img src=https://qiita-image-store.s3.amazonaws.com/0/152199/e0ed64f5-6883-db1d-4ea9-0af30b434477.gif width=50%>

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

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

|RetainElement(デフォルト)|RecycleElement|
|---|---|
|![](https://qiita-image-store.s3.amazonaws.com/0/152199/a949a8c4-9131-4c11-81e8-4e14935f7297.gif)|![](https://qiita-image-store.s3.amazonaws.com/0/152199/e0ed64f5-6883-db1d-4ea9-0af30b434477.gif)|



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