かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Xamarin.Android + ReactivePropertyでListViewを使う

特にReactivePropertyでサポートはしてないので自前でやるっきゃないです!ということでこういうクラスを書いてみました。

ReadOnlyReactiveCollection型をIListAdapterに変換するコードです。

public static class ReadOnlyCollectionExtensions
{
    /// <summary>
    /// ReadOnlyReactiveCollectionをIListAdapterに変換する
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="self"></param>
    /// <param name="createRowView">行のデータを表示するためのViewを作る処理</param>
    /// <param name="setRowData">行にデータを設定する処理</param>
    /// <returns></returns>
    public static IListAdapter ToAdapter<T>(this ReadOnlyReactiveCollection<T> self, 
        Func<View> createRowView,
        Action<T, View> setRowData)
    {
        return new ReadOnlyReactiveCollectionAdapter<T>(self, createRowView, setRowData);
    }
}

/// <summary>
/// ReadOnlyReactiveCollection用のAdapterクラス
/// </summary>
/// <typeparam name="T"></typeparam>
class ReadOnlyReactiveCollectionAdapter<T> : BaseAdapter<T>
{
    // もとになるコレクション
    private ReadOnlyReactiveCollection<T> source;
    // 行のデータを表示するためのViewを作る処理
    private Func<View> createRowView;
    // 行にデータを設定する処理
    private Action<T, View> setRowData;

    public ReadOnlyReactiveCollectionAdapter(
        ReadOnlyReactiveCollection<T> source,
        Func<View> createRowView,
        Action<T, View> setRowData)
    {
        this.source = source;
        this.createRowView = createRowView;
        this.setRowData = setRowData;
    }

    public override T this[int position]
    {
        get { return source[position]; }
    }

    public override int Count
    {
        get { return this.source.Count; }
    }

    public override long GetItemId(int position)
    {
        return position;
    }

    public override View GetView(int position, View convertView, ViewGroup parent)
    {
        if (convertView == null)
        {
            convertView = this.createRowView();
        }
        this.setRowData(this[position], convertView);
        return convertView;
    }
}

使い方は簡単です。以下のようなコマンドを実行するとコレクションにデータを追加するだけのViewModelがあったとします。

public class MainActivityViewModel
{
    private ObservableCollection<string> source = new ObservableCollection<string> { "a", "b", "c" };
    public ReadOnlyReactiveCollection<string> Items { get; private set; }

    public ReactiveCommand AddItemCommand { get; private set; }

    public MainActivityViewModel()
    {
        this.Items = source.ToReadOnlyReactiveCollection();

        this.AddItemCommand = new ReactiveCommand();
        this.AddItemCommand.Subscribe(_ =>
        {
            this.source.Add("item" + DateTime.Now);
        });
    }
}

そして、ボタンとListViewを置いた画面でさくっと紐づけ。

[Activity(Label = "App1", MainLauncher = true, Icon = "@drawable/icon")]
public class MainActivity : Activity
{
    private MainActivityViewModel viewModel = new MainActivityViewModel();

    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);

        // Set our view from the "main" layout resource
        SetContentView(Resource.Layout.Main);

        this.FindViewById<Button>(Resource.Id.button1).Click += viewModel.AddItemCommand.ToEventHandler();

        var listView = this.FindViewById<ListView>(Resource.Id.listView1);
        listView.Adapter = viewModel.Items.ToAdapter(
            () => LayoutInflater.FromContext(this).Inflate(Resource.Layout.layout1, null),
            (x, v) => v.FindViewById<TextView>(Resource.Id.textView1).Text = x);
        this.viewModel.Items.CollectionChangedAsObservable().Subscribe(_ => listView.InvalidateViews());
    }
}

一応レイアウトファイルも

main.axml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <Button
        android:text="Button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/button1" />
    <ListView
        android:minWidth="25px"
        android:minHeight="25px"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/listView1" />
</LinearLayout>

layout1.axml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <ImageView
        android:src="@android:drawable/ic_menu_gallery"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:id="@+id/imageView1" />
    <TextView
        android:text="Medium Text"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:id="@+id/textView1" />
</LinearLayout>