Edited at

Fitbit Ionic & Versa 向けアプリを作ってみた。

More than 1 year has passed since last update.


【更新 2018.3.19】=========================

女性向けの新デバイスFitbit Versaの発表と共に、待望のSimulatorがプレビュー版でリリースされました。

これで実機がなくてもビルドおよび動作確認ができるようになりました。


【更新 2018.2.28】=========================

開発したアプリがFitbit Galleryに公開されました!

その名も"Asken Diet"です!


前書き

普段はAndroidアプリ開発をしている人間なのですが、ひょんなことから fitbit ionic 向けのアプリを

開発することになりましたので、その足跡を記録しておこうと思います。これから始めようって人の助走程度になれば。


開発環境

まずはオフィシャルのガイドを読めば、なんとなーく容易に開発スタートできます。

開発にはブラウザベースのオフィシャルIDE「 fitbit studio 」上で行います。現時点ではとてもシンプルな内容で、Android StudioやXcodeのように多くのことはできません。つまり使い方に迷うことはないでしょう。また、2018年2月現在では、 エミュレーターも無いため、アプリの実行には実機(ionic)が必要 です。ビルドはできます。このあたりについは、「 Getting Started 」でWatch Faceサンプルを試してみると分かります。


Folder Structure

ざーっくりで以下の感じです。

フォルダ
(個人的)Description

app
fitbitデバイス(ionic)側の実装を書く。
companionとMessageAPIを使ってやり取りする。

common
ガイドには"Shared Code"とあり、appとcompanionで共通利用できるコードを設置するのかな?
共通メソッドや定数をおいてたり?

companion
スマフォ(Android端末oriPhone端末)側の実装を書く。
appとMessageAPIを使ってやり取りしたり、インターネット通信もここでやります。
またsettingsとのやり取りはsettingsStorageの変更監視をトリガーに行われる。

resources
アプリで使う画像リソースを置くところ。
icon.pngはionic上でのアイコンなる。

settings
アプリ独自の設定実装を書く。
自社サービスとの接続機能(例えばログイン)はここに実装することになると思う。
設定データはsettingsStorageに自動保存され、その変更をcommanionが監視している。


豆知識的な

開発を始めるにあたり、たまたま参加したdeveloperカンファレンスで以下程度の情報はありました。

・バッテリーが 25%以下になると実機へインストールできなくなる

・JavaScript, SVG, CSSはいつものが動かないことがある

・フリーズしたり挙動がおかしくなったら物理ボタン3つ長押しで強制再起動してみる

・以下のコードを埋め込むことで、ionic画面がスリープしなくなる


app

 import { display } from "display";

display.autoOff = false;
display.on = true;

ionicは最長でも20秒で画面がスリープするので、この情報は凄く助かりました。また、ボタンイベントがループすることがあったり、ionicのフリーズは日常茶飯事で再起動に追い込まれること多々です。違った意味で開発に忍耐が必要かもしれません。


開発

特に難しいところはないのですが、少しだけ書いておきます。


画面遷移の仕方

例えば準備した2つのスクリーンを切り替えるには、displayの値を動的に変更してやればOK。


gui

<!-- screen 1 -->

<svg id="screen-1" display="inline">
<text class="defaultText" x="135" y="50%" text-anchor="start">Screen 1</text>
</svg>

<!-- screen 2 -->
<svg id="screen-2" display="none">
<text class="defaultText" x="135" y="50%" text-anchor="start">Screen 2</text>
</svg>


inlineで表示、noneで非表示ってことです。


app

let screen1 = document.getElementById("screen-1");

let screen2 = document.getElementById("screen-2");

// screen1からscreen2に切り替える
screen1.style.display = "none";
screen2.style.display = "inline";



AppとCompanionのデータやり取り

例えば、APIサーバのレスポンス内容をIonicに表示する場合、Companion側でサーバ通信し、レスポンスをMessage APIを使ってAppへ渡す流れになる。基本的にはfetchを使うのでGET/POSTもカスタムヘッダー埋め込みとかもできる。


companion

fetch(url, {

headers: {
"Accept":"application/json",
"Content-Type":"application/json",
"My-Custom-Header":xxxxxxx //カスタムヘッダーの埋め込みも可能
},
method: 'POST',
body: JSON.stringify({
id: mail,
password: pass
})
})
.then(function(res) {
return res.json();
})
.then(function(json) {
let jsonStr = JSON.stringify(json);
console.log("res: " + jsonStr);

let message = JSON.parse(jsonStr).message;
let data = JSON.stringify({
message: message
});

if (messaging.peerSocket.readyState === messaging.peerSocket.OPEN) {
// App側へデータ送る
messaging.peerSocket.send({ key: "api_response", newValue: data});
}
})
.catch(err => console.log("Fetching " + url + " failed: " + err));


CompanionからのデータをpeerSocket.onmessageで受け取る。


app

messaging.peerSocket.onmessage = evt => {

console.log(`App received: ${JSON.stringify(evt)}`);

if (evt.data.key === "api_response") {
console.log("api_response: " + evt.data.newValue);
}

};



Settingsとのデータやり取り

SettingsはCompanionとデータのやり取りを行うわけですが、流れとしてはSettingsが管理しているsettingsStrageをCompanionが監視していて、settingsStorageになんらかの変更が加わるとCompanionのonchangeイベントが発火します。


settings

<Section title={<Text bold align="center">Login</Text>}>

<TextInput label="ID" settingsKey="id" type="email" placeholder="Please enter the ID." />
<TextInput label="Password" settingsKey="password" type="password" placeholder="Please enter the Password." />
<Button label="Login" onClick={(event) => {
if (event.type == "click") {
props.settingsStorage.setItem("btn_login", "1");
}
}}
/>
</Section>

上ではログインボタン押下ベントを発火するためにsettingStorageに変更を加えている。

TextInputの変更値は自動でsettingsStrageに反映されるのでsetItemとかをしてやらなくてもOK。


companion

settingsStorage.onchange = evt => {

console.log("onchange.key: " + evt.key);
console.log("onchange.newValue: " + evt.newValue);

// ログインボタンが押された
if (evt.key === "btn_login") {
let id = (settingsStorage.getItem("id") != null) ? JSON.parse(settingsStorage.getItem("id")).name : null;
let password = (settingsStorage.getItem("password") != null) ? JSON.parse(settingsStorage.getItem("password")).name : null;

if (id != null && password != null && id != "" && password != "") {
fetchLogin(id, password);

} else {
// TODO ここでID・パスワード入力されてないよー!って表示とかする

}
}
};



Facebookログイン

連携先サービスにFacebookでログインしていた場合の対応が必要だったのですが、FitbitのSettings APIにあるOAuth Buttonを使ってaccess_tokenを取得できる


settings

 <Oauth

settingsKey="facebook"
title="Login"
label="Facebook Login"
status="Login"
authorizeUrl="https://www.facebook.com/dialog/oauth"
requestTokenUrl="https://graph.facebook.com/v2.3/oauth/access_token"
clientId="FacebookのクライアントID"
clientSecret="Facebookのクライアントシークレット"
scope="email"
onAccessToken={async (data) => {
console.log("json: ", JSON.stringify(data));
}}
/>

ボタンを押すとブラウザ連携でFacebookのOAuth認証が表示され、redirect_urlに https://app-settings.fitbitdevelopercontent.com/simple-redirect.html を設定しておくことで読み出し元の画面(Setting)に戻してくれる。この時code値のtoken交換までautoでやってくれる。


console

json:  {"access_token":"EAACJqX6ZC2NgBAGAx9mAhlI5b --省略-- ZBCu1wxIU4pDVmdy5u","token_type":"bearer","expires_in":5183893}


facebookのOAuth.クライアントシークレットをアプリ側に埋め込む形なので、セキュリティレベルが若干低下するので自社のWebに飛ばして認証させるのが良いかと思います。

※プリインのStravaみたく


Ionicデバイスボタンのイベント

Ionic左側面にあるハードボタンは"戻る"に使われているが、例えばアプリ内での画面戻りとかを制御する場合は以下のようになる


app

let screen1 = document.getElementById("screen-1");

let screen2 = document.getElementById("screen-2");

document.onkeypress = function(evt) {
if (evt.key === "back") {
if (screen2.style.display === "inline") {
screen1.style.display = "inline";
screen2.style.display = "none";

evt.preventDefault();
}
}
}



IonicとVersaの切り分け

たいていの場合、Ionic(348x250)とVersa(300x300)でデザインを切り分ける必要がでてくると思うが、リソース系はファイル名によって自動で出し分けられる。

/resources/styles~300x300.css // Versa用

/resources/styles.css // Ionic用(デフォルト)

コード側での処理は、Device APIのdevice.srceenを使ってできそうだが、注意点としてデフォルト設定をしておく必要があるらしい。


app

import { me as device } from "device";

if (!device.screen) device.screen = { width: 348, height: 250 };
console.log(`Dimensions: $‌{device.screen.width}x$‌{device.screen.height}`);


開発コミュニティ頼みになるケースが多々

オフィシャルガイドの内容が薄いため、UIまわりの実装に手間取りました。

これについては、時間と共に解決されていくとは思いますが。

例えば、Panorama Component(AndroidでいうPageView)の実装においては、デザインガイドラインはあるものの、コンポーネントガイドやSVGガイド等に一切実装方法が無いので困りました。

こんなときのために、fitbitが開発者コミュニティを作ってくれています。

Panorama Componentも率直に実装方法が分からない旨をコミュニティに投稿しました。

img_fitbit_1.png

すると、Fitbit中の方が回答をくれました。

img_fitbit_2.png

まだガイドに情報が無いそうです。:sweat_smile:

ただ、スニペット付きだったので助かりました。

困ったら悩まずコミュニティに聞くが解決の近道かと思います。

なお、Panorama Componentは、こんな感じでUIに追加できます。


widgets.gui

<link rel='import' href='/mnt/sysassets/widgets/baseview_widget.gui'/>

<link rel='import' href='/mnt/sysassets/widgets/panoramaview_widget.gui'/>
<link rel='import' href='/mnt/sysassets/widgets/pagination_dots.gui'/>


index.gui

  <defs>

<symbol id='panorama-symbol'>
<use id='container' href='#panoramaview' overflow='hidden'>

<!-- ページ1 -->
<use id="page-1" href="#panoramaview-item">
<text class="defaultText" x="125" y="50%" text-anchor="start">ページ1</text>
</use>

<!-- ページ2 -->
<use id="page-2" href="#panoramaview-item">
<text class="defaultText" x="125" y="50%" text-anchor="start">ページ2</text>
</use>

<!-- ページ3 -->
<use id="page-3" href="#panoramaview-item">
<text class="defaultText" x="125" y="50%" text-anchor="start">ページ3</text>
</use>

<!-- ページ4 -->
<use id="page-4" href="#panoramaview-item">
<text class="defaultText" x="125" y="50%" text-anchor="start">ページ4</text>
</use>

<!-- panorama view のページDot (y値で高さの配置調整)-->
<use id='pagination-dots' class='pagination' href='#pagination-widget' y='230'>
<!-- 背景固定Dot (ページ数分配置する) -->
<use href='#pagination-dot'/>
<use href='#pagination-dot'/>
<use href='#pagination-dot'/>
<use href='#pagination-dot'/>
<!-- ページ移動に追随する動くハイライトDot -->
<use href='#pagination-highlight-dot'/>
</use>

</use>
</symbol>
</defs>
<svg>
<use href="#panorama-symbol" />
</svg>



Scroll Viewの仕様が微妙

とても長い文章を表示するUIがあったので、上下にスクロールさせる必要がありました。

コンポーネントガイドをみるとScroll Viewがあるので、これを使ってみるわけですが、想像した動きとは違いました。

普段、Androidアプリを開発していると、こんな感じで書いてしまいがちです。

 <svg>

<use href="#scrollview">
<use href="#scrollview-item">
<textarea id="message" class="mytextarea"> ここにながーい文章を書く </textarea>
</use>
</use>
</svg>

ただ、これでは、まったくもってスクロールしません。。。。

色々やってみた結果、おそらくとても長いscrollview-item x1をスクロールする仕様ではなく、複数のscrollview-itemをスクロールするためのモノみたいです。縦版PageViewのスナップ効かないバージョンみたいといいますか、ガイドのGIFアニメの動きどおりです。

しょうがないので今回は1画面になんとか収まりそうな文字数を決めて、文章をsplitし5ページ固定で設置する対応をしました。

<use href="#scrollview">

<use href="#scrollview-item">
<textarea id="detail-page-1" class="message" text-length="1024"></textarea>
</use>
<use href="#scrollview-item">
<textarea id="detail-page-2" class="message" text-length="1024"></textarea>
</use>
<use href="#scrollview-item">
<textarea id="detail-page-3" class="message" text-length="1024"></textarea>
</use>
<use href="#scrollview-item">
<textarea id="detail-page-4" class="message" text-length="1024"></textarea>
</use>
<use href="#scrollview-item">
<textarea id="detail-page-5" class="message" text-length="1024"></textarea>
</use>
</use>

無論、この実装だと短い文章の場合にブランクのページが出来てしまい不細工です。

文章の長さに応じてscrollview-itemのdisplayを"none"又は"inline"→"none"に変更もできそうでしたが、、、

advice-detail-scroll-bug.png

みたいな感じで初期表示のみ全scrollview-itemが重なって表示されました。

ただ、画面に一度触るとぱっと正常に戻るっていうOSのバグらしき動きになりましたので動的なdisplay変更は断念しました。


所感

シンプルな要件下でのアプリ開発であったため、大きく困るケースは少なかったし、比較的サクサク開発していけました。始めてみれば楽しかったと思います。なお、オフィシャルガイドの内容が薄かったり、fitbit studioで出来ることが少なかったり、ionicの挙動がしょっちゅう微妙になったり、アプリ管理コンソールにはアナリティクス的な機能は一切なく、まだまだこれからって感じのプラットフォームではあります。しかし、モバイルアプリのプラットフォームにおいては2強時代にある今、一石を投じることが出来るかどうか期待したいとこです。