Разработка приложения на Android с помощью Xamarin и F#
Привет!
Недавно Xamarin объявил конкурс на разработку мобильного приложения на функциональном языке программирования F#.Это было связано с выходом Xamarin 3 с полной поддержкой F#. Я решил отвлечься от повседневных задач и попробовать поучаствовать, тем более что я давно смотрю на F#, но шансов познакомиться с ним подробнее у меня не было. Для участия в соревновании я решил разработать приложение идея которого была предложена кем-то в процессе обсуждения внезапного взлета мобильного приложения Yo. Вот цитата:
Идея для стартапа, рабочее название «ты где?».
Смысл прост, девушка устанавливает приложение, указывает в нем номер своего молодого человека и после этого появляется большая гнопка отправки сообщения «ты где?» #startup #idea
Почему бы и нет? ПримечаниеЯ писал этот пост параллельно работая над приложением. Поэтому он большой и местами не очень логичный.
Футболочка Первое что я сделал, это скачал и запустил приложение Xamarin Store чтобы получить футболку с F#. Такая же с C# у меня уже есть Вернее я попробовал, но сразу же схватил проблему с построением. Оказывается текущая версия Xamarin поддерживает F# версии 3.0, а свободно скачиваемой является только версия F# 3.1.1
F# 3.0 находится внутри пакета Visual Studio Express 2012 for Web и устанавливается вместе со студией с помощью Microsoft Web Platform Installer. Странный подход.Для работы Xamarin и F# достаточно чтобы сборка FSharp.Core версии 4.3.0.0 была в GAC. В любом случае, вот прямая ссылка если кто-нибудь захочет попробовать.
Начало работы Сейчас Xamarin поддерживает F# только внутри Xamarin Studio. Так что пришлось на время забыть о своей любимой VS2013 и поработать в этой, в целом довольно неплохой, среде. Создание нового приложения под Android заняло пару секунд и вот перед нами рабочее Hello-world приложение для Android на F#MainActivity.fs namespace Xakpc.WhereAreYou.Droid
open System
open Android.App open Android.Content open Android.OS open Android.Runtime open Android.Views open Android.Widget
[
let mutable count: int = 1
override this.OnCreate (bundle) =
base.OnCreate (bundle)
// Set our view from the «main» layout resource this.SetContentView (Resource_Layout.Main)
// Get our button from the layout resource, and attach an event to it let button = this.FindViewById
Ну что же, остается только подождать пока оно будет «там», время еще есть. Оставим пока Google Play Services в покое.Немного слов о F#
Начиная проект я ничего не знал о F#, кроме того что это «круто», «современно», и «крайне удобно». Попытка взять его с наскоку в новом проекте с треском провалилась. Почти пятнадцать минут я потратил пытаясь понять почему let values = [«item1»; «item2»; «item3»] нельзя передать в конструктор ArrayAdapter’а listView.Adapter <- new ArrayAdapter
Сразу в Бой, попытка номер два Начинаем реализовывать первый экран — регистрацию. Для регистрации я собираю телефон и генерирую hashВот как выглядит функция MD5 для F#
let MD5Hash (input: string) = use md5 = System.Security.Cryptography.MD5.Create () input |> System.Text.Encoding.ASCII.GetBytes |> md5.ComputeHash |> Seq.map (fun c → c.ToString («X2»)) |> Seq.reduce (+) Оператор |> это pipeline оператор, он передает результат выражения дальше.Таким образом имеем следующий алгоритм: получаем байты из GetBytes → вычисляется хеш → для каждого байта конвертация в HEX формат → получившийся массив символов склеиваем в строку (метод reduce выполняет функцию + для каждого элемента начиная с первой пары в накопленный итог) → возвращаем результат вычисления функции.
Для сравнения, тот же метод на C# using System; public string CreateMD5Hash (string input) { MD5 md5 = System.Security.Cryptography.MD5.Create (); byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes (input); byte[] hashBytes = md5.ComputeHash (inputBytes); StringBuilder sb = new StringBuilder (); for (int i = 0; i < hashBytes.Length; i++) { sb.Append (hashBytes[i].ToString ("X2")); } return sb.ToString(); } Одна из проблем над которой я завис на некоторое время: это то что одни модули не видели другие. Например у меня есть модуль для AzureServiceWorker (который в CLR транслируется в статичный класс)
И сколько я не пытался вызвать его в активити — ничего не получалось. Оказывается для F# важен порядок файлов! И оказывается Xamarin Studio не позволяет его поменять никаким другим образом кроме как в файле проекта.
module Async = open System.Threading open System.Threading.Tasks
let inline AwaitPlainTask (task: Task) = // rethrow exception from preceding task if it fauled let continuation (t: Task) : unit = match t.IsFaulted with | true → raise t.Exception | arg → () task.ContinueWith continuation |> Async.AwaitTask Ее можно было бы решить еще проще вызвав Async.AwaitIAsyncResult >> Async.Ignore, но тогда теряется исключения внутри таскиА вот как я получаю контакты и делаю над ними операции
let ExtractUserInfo (x: Contact) = let first = x.Phones |> Seq.tryPick (fun x → if x.Type = PhoneType.Mobile then Some (x) else None) match first with | Some (first) → let phone = first.Number |> StripChars [' ';'-';'(';')'] UserInfo.CreateUserInfo ((MD5Hash phone), phone, x.DisplayName) | None → UserInfo.CreateUserInfo («no mobile phone», «no mobile phone», «no mobile phone»)
// function for async list filling let FillContactsAsync = async { let book = new AddressBook (this) let! result = book.RequestPermission () |> Async.AwaitTask if result then _contacts <- book.ToList() |> Seq.filter (fun (x: Contact) → not (Seq.isEmpty x.Phones)) |> Seq.map ExtractUserInfo |> Seq.sortBy (fun x → x.DisplayName) |> Seq.toList
let finalContacts = _contacts |> Seq.map (fun x → x.DisplayName.ToUpperInvariant ()) |> Seq.toArray
this.ListAdapter <- new ArrayAdapter(this, Resource_Layout.row_contact, finalContacts)
else
System.Diagnostics.Debug.WriteLine("Permission denied by user or manifest")
this.ListAdapter <- new ArrayAdapter(this, Resource_Layout.row_contact, Array.empty)
}
Разберем ключевую последовательность действий функцииbook.ToList() конвертируем в List
|> Seq.filter (fun (x: Contact) → not (Seq.isEmpty x.Phones)) фильтруем все контакты без телефонов
|> Seq.map ExtractUserInfo конвертируем все элементы из класса Contracts в UserInfo, далее у нас коллекция элементов UserInfo
|> Seq.sortBy (fun x → x.DisplayName) Сортируем
|> Seq.toList конвертируем в List
_contacts <- кладем все в mutable поле
_contacts |> Seq.map (fun x → x.DisplayName.ToUpperInvariant ())<-конвертируем все элементы UserInfo в string, далее у нас коллекция элементов-строк — имена заглавными буквами
|> Seq.toArray<-конвертируем List в Array чтобы его принял ArrayAdapter
Тут есть нелогичность — два раза происходит конвертирование в List. Надо будет исправить.
Azure Mobile Servcies
В качестве бэк-энда традиционно я использую Azure Mobile Services. Пока я не стал заморачиваться с NotificationHub, который призван обеспечить доставку Push уведомлений на все платформы. Описывать подключение Azure я тоже не буду, т.к. у них есть свои подробнейшие мануалы.В приложении я создаю пару констант, они помечаются тегом
module WruConstants =
[
member this.RegisterMe phone name regId = async {
try
let table = this.MobileService.GetTable
let usr =
{ Id = »
PhoneHash = MD5Hash phone
Nickname = name
RegistrationId = regId }
do! table.InsertAsync usr |> Async.AwaitPlainTask
return (usr.Id, usr.PhoneHash, usr.Nickname)
with | e → System.Diagnostics.Trace.WriteLine (e.ToString)
return (String.Empty, String.Empty, String.Empty) }
member this.RegisterMe phone name regId = async { — тут создается функция член определения типа (будет транслировано в статичный публичный метод) с тремя входящими параметрами. Дальнейший код размещается в так называемом «computation expression» или асинхронном workflow. Внутри скобок { } можно использовать специальные конструкции с суффиксом! (читается bang), например do! (do-bang) или let! (let bang)
Я создаю объект записи User. Интересной особенностью является то, что F# сам определит к какому типу относиться данный объект, с помощью набора заданных полей
let usr = { Id = »
PhoneHash = MD5Hash phone
Nickname = name
RegistrationId = regId }
do! table.InsertAsync usr |> Async.AwaitPlainTask делаем do-bang, что эквивалентно await из C#. Т.е. запускаем асинхронную задачу на выполнение, а весь последующий код продолжится выполнятся в continuation после завершения асинхронной задачи.
return (usr.Id, usr.PhoneHash, usr.Nickname) и наконец возвращаем кортеж эквивалентный Tuple
let id (c,_,_) = c let phonehash (_, c,_) = c let nickname (_,_, c) = c их использование очень понятное: id tuple вернет Id и т.п.Всего у меня 5 функций Azure, две из них используются для выполнения Push уведомлений. Чтобы их использовать мне пришлось написать Azure Custom Api функцию
Вот она если кому интересно exports.post = function (request, response) { // Use «request.service» to access features of your mobile service, e.g.: // var tables = request.service.tables; // var push = request.service.push;
//response.send (statusCodes.OK, { message: 'Hello World!' });
console.log ('Incoming call with requst: ', request.body.RequestId);
var usersTable = request.service.tables.getTable ('User');
usersTable.where ({ id: request.body.TargetId })
.read (
{ success: function (results)
{
if (results.length > 0)
{
var user = results[0]
console.log ('Send to results: ', user.Nickname, user.RegistrationId);
request.service.push.gcm.send (user.RegistrationId,
{
RequesterId: request.body.RequesterId,
RequesterNickname: request.body.RequesterName,
TargetId: user.id,
TargetNickname: user.Nickname
},
{
success: function (gcm_response) {
console.log ('Push notification sent: ', gcm_response);
response.send (statusCodes.OK, { RequestedNickname: user.Nickname });
},
error: function (gcm_error) {
console.log ('Error sending push notification: ', gcm_error);
response.send (statusCodes.INTERNAL_SERVER_ERROR, { RequestedNickname: user.Nickname });
}
});
}
else
{
response.send (statusCodes.NO_CONTENT, { RequestedNickname:» });
}
},
error: function (error)
{
console.log ('Error read table: ', error);
response.send (statusCodes.INTERNAL_SERVER_ERROR, { RequestedNickname:» });
}
});
};
Ну, а выполнение Push операции в мобильном приложении тривиально
// Perform Push operation
member this.PushAsync targetId myId myNickname = async {
try
let (request: PushRequest) =
{
TargetId = targetId
RequesterId = myId
RequesterName = myNickname
}
let! result = this.MobileService.InvokeApiAsync
Пуш нотификации Для пушей проекту нужна поддержка Google Play Services. Однако они несовместимы с F# в данный момент. Пришлось полазить по зависимостям и найти ту сборку которая ломала проект. Оказалось что это сборка: Xamarin.Android.Support.v7.AppCompatУдаляем ее и все собирается, Google Play Services работают, можно создавать уведомления.Вообще процесс получения и обработки push notification достаточно унылая штука. Телефон регистрируется в GCM, получает ID, дальше мы сохраняем этот ID на сервере и по нему отрабатываем Push уведомления (см. серверную функцию pushhim). Простое получение запроса требует от нас создания BroadcastReciever и сервиса и подробно описано на developer.android.com. Переписывать мне это на F# абсолютно не хотелось и тут мне снова помог Xamarin Component Store. Внутри него есть компонент Google Cloud Messaging Client который инкапсулирует в себя большую часть работы с GCM и этим очень удобен. Вот все что нужно сделать для получения ID
//Check to see that GCM is supported and that the manifest has the correct information GcmClient.CheckDevice (this) GcmClient.CheckManifest (this)
// check google play if CheckPlayServices () then // Try to get registration id let regId = GcmClient.GetRegistrationId this if String.IsNullOrEmpty (regId) then // Call to Register the device for Push Notifications GcmClient.Register (this, WruConstants.GcmSender); Если наберется сотня пользователей воткну сюда картуВот пожалуй и все.Заявка на конкурс подана, блог-пост написанисходники доступны на битбакете bitbucket.org/xakpc/whereareyouсамо приложение доступно в гуглоплее, могу дать ссылку интересующимсяЯ понимаю что предложенный тут код во многом не функциональный, буду рад за любые предложения по превращению кода в более «функциональную» версию.
Вердикт Да, я написал Android приложение на F#. Это был интересный и увлекательный опыт.Нет, я никогда больше не буду писать что-то под Android на F#. По крайней мере, пока не увижу явных удобств в этом.