Edited at

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

More than 3 years have passed since last update.


概要

Xamarin.Formsで、TimeLineの表示とツイートができる簡単なTwitterクライアントを作ってみました。

そこで、作成する過程で見つけた便利なPluginや小ネタをソースを追いながらまとめました。

また、UIをXAMLで書いているサンプルが少ない気がするので、その辺りもご参考になれば幸いです。

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


完成品

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

TwitterSample

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

iOS



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、まだまだ情報が少なくてとっつきにくい感じもしますが、

単純なアプリなら簡単にクロスプラットフォームで開発ができて楽しいですね(∩´∀`)∩