Xamarin.Formsで簡単なTwitterクライアントを作った際の小ネタまとめ

  • 30
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

概要

Xamarin.Formsで、TimeLineの表示とツイートができる簡単なTwitterクライアントを作ってみました。
そこで、作成する過程で見つけた便利なPluginや小ネタをソースを追いながらまとめました。
また、UIをXAMLで書いているサンプルが少ない気がするので、その辺りもご参考になれば幸いです。

めっちゃ長い投稿になってしまった(´・ω・`)

完成品

GitHubに公開しておきました。
TwitterSample

こんな感じです。(両方とも実機のスクリーンショットですが、一部ぼかしています。)
iOS
iOS
Android
Android

制作環境

  • MacOS X 10.11.5
  • Xamarin Studio Community 6.1(Alpha channel)

プロジェクトの種類

  • Xamarin.Forms FormApp
  • Shared CodeはPCL (Profile 7)
    Profileは プロジェクトオプションのビルドの一般 から選択できます。
    Profileの詳細はこちらが参考になります。

導入したPlugin

下記を導入しました。
- CoreTweet
- Corcav.Behaviors
- Xam.Plugin.Iconize / Xam.FormsPlugin.Iconize / Xam.Plugin.Iconize.FontAwesome

CoreTweet

有名な.Net向けのTwitterライブラリです。
詳細や使い方はこちらです。

Corcav.Behaviors

EventとCommandを簡単に紐づけたり(EventToCommand)できるBehavior Pluginです。
これで簡単にEvent時(例:EditorのTextChangedイベント)に何かを実行することができます。
詳細や使い方はこちらです。

Xam.Plugin.Iconize

Font Awesome等のアイコンフォントを簡単に利用できるPluginです。
Xamarin.Formsの場合、ButtonやImage、Labelにアイコンフォントを使用したり、
TabbedPageやToolbarItemのアイコン部分にも使用できます。
詳細や使い方はこちらです。
※Xamarin.Formsの場合、Xam.FormsPlugin.Iconizeも必要です。
※アイコンフォントは、Xam.Plugin.Iconize.FontAwesomeといったPluginの形でNugetから導入します。
 なお、これらのアイコンフォントPluginはPCLには導入不要です。(追加できません)

ソース

ViewModelBase.cs

ViewModelの基底クラスです。

ViewModelBase.cs
using System.ComponentModel;

namespace TwitterSample
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged(string info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }
    }
}

Model.cs

どこに書くか迷いましたが、ツイートするメソッドもTimeLineを取得するメソッドもModelに書きました。
ツイートするテキストなどViewをまたいで保持したい情報もModelで保持します。(そのためにSingletonにします)

API KeyやSecretはTwitter Application Managementから取得できます。

Model.cs
using System;
using System.Threading.Tasks;
using CoreTweet;
using System.Collections.ObjectModel;

namespace TwitterSample
{
    public sealed class Model
    {
        // Singleton instance.
        private static readonly Model _Model = new Model();

        public static Model Instance
        {
            get { return _Model; }
        }

        private Model() { }

        // Twitter API tokens. ここから入手可(https://apps.twitter.com)
        public readonly Tokens tokens = Tokens.Create("API key",
                                               "API secret",
                                               "Access token",
                                               "Access token secret");

        public string TweetText;
        public int WordCount;
        public bool IsBusy;

        public ObservableCollection<Data> TimeLine = new ObservableCollection<Data>();
        public class Data
        {
            public string ScreenName { get; set; }
            public string Name { get; set; }
            public string Tweet { get; set; }
            public string CreatedAt { get; set; }
            public string Icon { get; set; }
        }

        async public Task TweetAsync(string TweetText)
        {
            await tokens.Statuses.UpdateAsync(status => TweetText);
        }

        async public Task FetchTimeLine()
        {
            try
            {
                var status = await tokens.Statuses.HomeTimelineAsync(count => 50);

                TimeLine.Clear();
                foreach (var tweet in status)
                {
                    TimeLine.Add(new Data
                    {
                        ScreenName = tweet.User.ScreenName,
                        Name = tweet.User.Name,
                        Tweet = tweet.Text,
                        CreatedAt = tweet.CreatedAt.AddHours(9).ToString("f"),
                        Icon = tweet.User.ProfileImageUrl
                    });
                }
            }
            catch (Exception)
            {
                throw;
            }
        }
    }
}

MainPage.xaml

ListViewでTwitterのTimeLineのようなUIを作ります。
なお、IsPullToRefreshEnabledをtrueにすると、引っ張って更新が使えるようになります。
その際のCommandはRefreshCommandにBindします。

MainPage.xaml
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             xmlns:behaviors="clr-namespace:Corcav.Behaviors;assembly=Corcav.Behaviors" 
             xmlns:iconize="clr-namespace:FormsPlugin.Iconize;assembly=FormsPlugin.Iconize" 
             x:Class="TwitterSample.MainPage" Title="TimeLine">
    <ContentPage.Content>
        <StackLayout Padding="10,0,10,0">
            <ListView ItemsSource="{Binding TimeLine}" HasUnevenRows="true" IsPullToRefreshEnabled="true" IsRefreshing="{Binding IsBusy, Mode=TwoWay}" RefreshCommand="{Binding FetchTimeLine}">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <ViewCell>
                            <StackLayout Orientation="Horizontal" Padding="5,5,5,5">
                                <Image Source="{Binding Icon}" WidthRequest="55" HeightRequest="55" VerticalOptions="Start" />
                                <StackLayout Spacing="0">
                                    <StackLayout Orientation="Horizontal">
                                        <Label Text="{Binding Name}" FontSize="13" />
                                        <Label Text="{Binding ScreenName}" FontSize="10" TextColor="Gray" />
                                    </StackLayout>
                                    <Label Text="{Binding CreatedAt}" FontSize="10" TextColor="Gray" />
                                    <Label Text="{Binding Tweet}" FontSize="12" />
                                </StackLayout>
                            </StackLayout>
                        </ViewCell>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

MainPage.xaml.cs

コードビハインドでOnAppearing()をoverrideすると、Viewが表示された時の挙動を作ることができます。
今回は、MainPageViewModelのInitializeメソッドを実行するようにしています。

MainPage.xaml.cs
using Xamarin.Forms;

namespace TwitterSample
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
        }

        protected override async void OnAppearing()
        {
            base.OnAppearing();

            var ViewModel = BindingContext as MainPageViewModel;
            await ViewModel.Initialize();
        }
    }
}

MainPageViewModel.cs

InitializeメソッドでTimelineの更新をさせています。
これでMainPageViewを表示する度にTimeLineが自動更新されるようになります。

MainPageViewModel.cs
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows.Input;
using Xamarin.Forms;

namespace TwitterSample
{
    public class MainPageViewModel : ViewModelBase
    {
        Model Model = Model.Instance;

        public ICommand FetchTimeLine { get; set; }

        public MainPageViewModel()
        {
            FetchTimeLine = new Command(async () =>
            {
                //ListViewのIsRefreshingプロパティを手動で更新。
                //本来、自動で更新されるため不要だが、自動にすると更新されない場合がある為追加。(原因不明)
                IsBusy = true;
                try
                {
                    await Model.FetchTimeLine();
                }
                catch (Exception e)
                {
                    await Application.Current.MainPage.DisplayAlert("エラー", e.ToString(), "OK");
                }
                IsBusy = false;
            });
        }

        async public Task Initialize()
        {
            try
            {
                await Model.FetchTimeLine();
            }
            catch (Exception e)
            {
                await Application.Current.MainPage.DisplayAlert("エラー", e.ToString(), "OK");
            }
        }

        public ObservableCollection<Model.Data> TimeLine
        {
            get
            {
                return Model.TimeLine;
            }
            set
            {
                Model.TimeLine = value;
            }
        }

        public bool IsBusy
        {
            get { return Model.IsBusy; }
            set
            {
                Model.IsBusy = value;
                OnPropertyChanged("IsBusy");
            }
        }
    }
}

TweetPage.xaml

ツイートする画面を作成します。
入力されている文字数を自動で表示させるため、テキスト入力欄(Editor)のTextChangedイベントに対して、
文字数をカウントするメソッドをCommandとしてBindしています。(Corcav.Behaviorsの機能です)

画面を横に向けた場合など、UIが画面外にはみ出た時に操作できなくなってしまうので、
StackLayoutをScrollViewで囲んでいます。

iconize:IconButtonとしたButtonでは、TextプロパティにFont Awesome等のclass名を入れるだけで、
そこがアイコンになって表示されます。通常のテキストを入れると通常のテキストが表示されます。

また、下記で当該のページ名に遷移(進む)します。
Application.Current.MainPage.Navigation.PushAsync(new Page名())

TweetPage.xaml
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             xmlns:behaviors="clr-namespace:Corcav.Behaviors;assembly=Corcav.Behaviors" 
             xmlns:iconize="clr-namespace:FormsPlugin.Iconize;assembly=FormsPlugin.Iconize" 
             x:Class="TwitterSample.TweetPage" 
             Title="Tweet">
    <ContentPage.Content>
        <ScrollView>
            <StackLayout Padding="2,0,2,0">
                <Editor Text="{Binding TweetText,Mode=TwoWay}" HeightRequest="230">
                    <behaviors:Interaction.Behaviors>
                        <behaviors:BehaviorCollection>
                            <behaviors:EventToCommand EventName="TextChanged" Command="{Binding CountWord}" />
                        </behaviors:BehaviorCollection>
                    </behaviors:Interaction.Behaviors>
                </Editor>
                <Label Text="{Binding WordCount, StringFormat='{0}文字' ,Mode=OneWay}" HorizontalTextAlignment="End" />
                <iconize:IconButton Command="{Binding Tweet}" Text="fa-twitter" FontSize="40" BorderColor="Gray" BorderWidth="1" />
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="Auto" />
                    </Grid.ColumnDefinitions>
                    <iconize:IconButton Command="{Binding GoToFixedPhrasePage}" Text="定形文" Grid.Column="0" Grid.Row="1" BorderColor="Gray" BorderWidth="1" />
                    <Button Command="{Binding Reset}" Text="クリア" Grid.Column="2" Grid.Row="1" BorderColor="Gray" BorderWidth="1" />
                </Grid>
            </StackLayout>
        </ScrollView>
    </ContentPage.Content>
</ContentPage>

TweetPage.xaml.cs

ここでは、TweetPageViewを表示する度にTweetPageViewModelのInitializeメソッドを実行するようにしています。

MTweetPage.xaml.cs
using System;
using System.Collections.Generic;

using Xamarin.Forms;

namespace TwitterSample
{
    public partial class TweetPage : ContentPage
    {
        public TweetPage()
        {
            InitializeComponent();
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();

            var ViewModel = BindingContext as TweetPageViewModel;
            ViewModel.Initialize();
        }
    }
}

TweetPageViewModel.cs

Initializeメソッドでは、Modelに保持したツイートするテキストと文字数を読み出しています。
これは、後述する定形文の呼び出しページで選択した定形文(ツイートするテキスト)を、Viewをまたいで受け取るための処理です。

TweetPageViewModel
using System.Windows.Input;
using Xamarin.Forms;
namespace TwitterSample
{
    public class TweetPageViewModel : ViewModelBase
    {
        public ICommand CountWord { get; set; }
        public ICommand Tweet { get; set; }
        public ICommand GoToFixedPhrasePage { get; set; }
        public ICommand Reset { get; set; }

        Model Model = Model.Instance;

        public TweetPageViewModel()
        {
            CountWord = new Command(() => WordCount = TweetText.Length);

            Tweet = new Command(async () =>
            {
                var accepted = await Application.Current.MainPage.DisplayAlert("確認", "Tweetしますか?", "OK", "Cancel");
                if (accepted)
                {
                    await Model.TweetAsync(TweetText);
                    await Application.Current.MainPage.DisplayAlert("(`・ω・´)", "Tweetしました!", "OK");
                    TweetText = "";
                }
            });

            GoToFixedPhrasePage = new Command(() => Application.Current.MainPage.Navigation.PushAsync(new FixedPhrasePage()));

            Reset = new Command(() =>
            {
                TweetText = "";
                WordCount = Model.WordCount;
            });
        }

        public void Initialize()
        {
            TweetText = Model.TweetText;
            WordCount = Model.WordCount;
        }

        public string TweetText
        {
            get { return Model.TweetText; }
            set
            {
                Model.TweetText = value;
                OnPropertyChanged("TweetText");
            }
        }

        public int WordCount
        {
            get { return Model.WordCount; }
            set
            {
                Model.WordCount = value;
                OnPropertyChanged("WordCount");
            }
        }
    }
}

FixedPhrasePage.xaml

定形文を選択できるページです。とりあえず2個だけ置いておきました。

FixedPhrasePage.xaml
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="TwitterSample.FixedPhrasePage" 
             Title="定形文を選んでね">
    <ContentPage.Content>
        <ScrollView Padding="10,20,10,0">
            <StackLayout>
                <Button Command="{Binding SetFixedPhrase}" Text="進捗ダメです" CommandParameter="進捗ダメです" />
                <Button Command="{Binding SetFixedPhrase}" Text="お腹痛い(´・ω・`)" CommandParameter="お腹痛い(´・ω・`)" />
            </StackLayout>
        </ScrollView>
    </ContentPage.Content>
</ContentPage>

FixedPhrasePage.xaml.cs

ここではInitializeメソッドは不要なのでシンプルに下記だけです。

FixedPhrasePage.xaml.cs
using Xamarin.Forms;

namespace TwitterSample
{
    public partial class FixedPhrasePage : ContentPage
    {
        public FixedPhrasePage()
        {
            InitializeComponent();

            BindingContext = new FixedPhrasePageViewModel();
        }
    }
}

FixedPhrasePageViewModel.cs

TweetPageViewに値を受け渡すため、SetFixedPhraseメソッドでは値を全てModelに格納しています。

なお、下記で遷移元のページへ遷移(戻る)します。
Application.Current.MainPage.Navigation.PopAsync()

FixedPhrasePageViewModel
using System.Windows.Input;
using Xamarin.Forms;

namespace TwitterSample
{
    public class FixedPhrasePageViewModel : ViewModelBase
    {
        public ICommand SetFixedPhrase { get; set; }

        Model Model = Model.Instance;

        public FixedPhrasePageViewModel()
        {
            SetFixedPhrase = new Command<string>((arg) =>
            {
                Model.TweetText = arg;
                Model.WordCount = Model.TweetText.Length;
                Application.Current.MainPage.Navigation.PopAsync();
            });
        }
    }
}

TwitterSample.cs

TabbedpageのIconにFontAwesomeを使用するため、Iconize pluginの機能を使用します。
そのためには、そのUI部分だけC#でないとダメなようなので(もしかしたらXAMLで書けるかもしれません)C#で記載しました。
追記:XAMLでも書けました。

TabbedPageのChildrenとして、MainPageとTweetPageを追加し、それぞれにIconを割り当てています。

TwitterSample.cs
using FormsPlugin.Iconize;
using Xamarin.Forms;

namespace TwitterSample
{
    public class App : Application
    {
        public App()
        {
            // The root page of your application

            //IconizeライブラリのIconTabbedPageを使用する為、TabbedPageの生成のみXAMLではなくC#で記載。
            var TabbedPage = new IconTabbedPage { Title = "Twitter" };

            foreach (var module in Plugin.Iconize.Iconize.Modules)
            {
                TabbedPage.Children.Add(new MainPage
                {
                    BindingContext = new MainPageViewModel(),
                    Icon = "fa-home"
                });

                TabbedPage.Children.Add(new TweetPage
                {
                    BindingContext = new TweetPageViewModel(),
                    Icon = "fa-twitter"
                });
            }

            MainPage = new IconNavigationPage(TabbedPage);
        }

        protected override void OnStart()
        {
            // Handle when your app starts
        }

        protected override void OnSleep()
        {
            // Handle when your app sleeps
        }

        protected override void OnResume()
        {
            // Handle when your app resumes
        }
    }
}

まとめ

Xamarin.Forms、まだまだ情報が少なくてとっつきにくい感じもしますが、
単純なアプリなら簡単にクロスプラットフォームで開発ができて楽しいですね(∩´∀`)∩