Fitbit Ionic向けアプリ開発をどうにかやってみた。

普段は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();
    }
  }
}

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

オフィシャルガイドの内容が薄いため、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強時代にある今、一石を投じることが出来るかどうか期待したいとこです。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.