この記事は
Androidその2 Advent Calendar 2016 の7日目の記事です。
Android Wear
Google I/O2016にてAndroid Wear2.0がアナウンスされました。
最近のAndroidといえば、バージョンの命名規則が5.0から変わってちょっとした機能の追加でバージョン番号が大きく変わるようになりました。
それまではAndroidでは1単位の変化はUIを含めた大幅な変更が加わったときで割と大きな機能追加が行われても0.1単位でバージョンが増えていくというルールになっていました。
それが、5.0以降はUIに大きな変化が加わらなくても1単位でバージョンが変わるようになりました。
多くの人が4.4から5.0への変化より5.0から7.1への変化の方が小規模な変化と感じるのではないでしょうか。
それに対してAndroidWearについては従来のAndroidをベースとしたバージョン番号の考え方が継続されています。
実際にAndroidWearはベースとなるAndroidが5.0や6.0になったときにもバージョン番号は0.1単位でしか増えることがなく、1.4と言いながら当初のバージョンとはもはや別物と言えるレベルで機能が強化されています。
そして、AndroidWear2.0はAndroidWearとして初めてのメジャーバージョンアップとなります。
それだけに、追加される機能や変更される内容は大掛かりな変化となり、最近のAndroidスマートフォンではあまり見ることのないレベルでの大掛かりな変化が行われ、ユーザー目線でも開発者目線でも別物レベルになりました。
現在はデベロッパープレビューという状態で、alpha3がリリースされています。
当初の予定では2016年中に正式版がリリースされる予定でしたが2017年前半にスケジュールが変更されました。
Wear2.0で変わる大きな変更点は次のとおりです。
システムUIの大幅な変化。
Complications APIの導入によるGlanceable性の向上。
スタンドアローンアプリの強化とそれに伴う利用形態の拡大。
配布形式の変更。
システムUIの大幅な変化
システムUIは斬新に変更され、白を基調とした軽快な印象がある1に対して黒を基調とした引き締まったデザインに変更されます。
デザインの印象としてはよりフラットになり未来感が強調され、5.0からのMaterial Designより3.0からのHoloテーマに近い印象を受けます。
カードUIのような画面を分断するデザインが削られ、画面全体を使ったWearならではのデザインに取り替えられています。
![]()
実際の操作感としては、明らかに1.xよりコンテンツが見やすくなりました。
1.xの時はメッセージが来た場合、Wearでタイトルを見て本文を見る必要があればスマートフォンを取り出していましたが、2.0を使うようになってからはWearで本文まで見るようになり、長文であったり返信が必要な場合以外はWearで済ませることが増えました。
この流れはAndroid Wearの一連の流れに沿ったものになります。1.0では極端なほど機能が制限されており、アプリランチャーすらすぐには開けない場所にあるなどユーザーや開発者がWearに多機能さを求めないようなデザインがされていましたが、それらはバージョンを経るごとに緩和されて次第にWearで行える操作を増やしてきました。
![]()
通知については追加機能を使わない限りはアプリ側の対応は必要ありません。自動的に2.0風の見た目に変わります。
しかしActivityに関しては自動で2.0風になったりはしないので見た目を合わせるなら作り込みが必要になります。
サポートライブラリーが用意されているため、単純に見た目を揃えるだけならそれほど苦労はしません。ただし操作感が大きく変わるため、ユーザーが使いやすいデザインにするためには画面構成を1から考え直す必要があります。
![]()
Complications APIの導入によるGlanceable性の向上
Wearでやれることが増える一方でWearならではの一瞬で情報を受け取れるGlanceable性もComplicationsAPIによって向上しています。
Complications APIはWatch face上に任意のアプリの情報を表示することが出来る仕組みです。
ちょうどスマートフォンにおけるホームウィジェットのような存在です。
これまで、Watch face上に情報を表示するには、Watch face自体がデータを取得して描画するまでのしょりを行う必要がありました。 データ提供元はデータを表示したいだけなのにWatch faceまで用意しないといけないし、Watch face作者からしてみたら、よりユーザーのニーズに答えるには多種多様なサービスの情報を表示する機能を付ける必要があり、ユーザーからしてみたら好きなデザインのWatch faceで好きな情報を得ることが出来ないという問題がありました。
Complications APIを使うとデータをWeb APIなどのサービスから取ってきて定形に合わせる処理をData providerアプリが行い、Watch faceは定形で渡されたデータを単に表示するだけでよくなります。
![]()
Android Wear1.xの場合
Watch face作者はデータ提供元それぞれの取得方法を調べて実装する必要があり、実装コストが大きく、Watch face側が対応していないサービスについて情報を表示することはできませんでした。
そのためユーザーが見たい情報を表示できるとは限りませんでした。
![]()
Android Wear2.0の場合
データ提供元はデータを取得してComplications APIに提供するData provider APKを作り、Watch face作者はComplications APIから渡されるデータを表示します。
データ提供元はデータさえ渡すだけでよく、Watch faceを作らなくても良くなりました。
Watch face作者はComplications APIからのデータを表示するだけで様々なデータを表示できるようなり、ユーザーはどのWatch faceでどのData providerを表示するかをカスタマイズできるようになりました。
Complications APIについてはNotificationとの使い分けが気になるところですが、Notificationが全てのアプリが時系列で並ぶのに対してComplicationsは特定のアプリが同じ場所にとどまり続けることが違います。
このため、ユーザーはWatchを覗くことで確実に現状を把握することが出来るようになります。
例えば、メールが来ていないこと を確認した場合に、従来の方式ではメール以外を含む全ての通知を見てメールの通知がないことを確認する必要がありました。Complications APIであれば、メールの新着数を表示させることで一瞬でメールが来ていないことを確認することが出来ます。
その分、特定アプリに場所を専有されるため、全てのアプリの情報を表示することは出来ません。その場合はNotificationが使われることになります。
詳しくは後ほど、実際にData Providerを作ってみます。
スタンドアローンアプリの強化とそれに伴う利用形態の拡大
Wear のAPKでインターネットへ接続できるようになりました。
これにより、Data syncのようなWear独自のコードを実装すること無く、スマートフォン用のコードと同じコードでサーバーからデータを取得できます。
インターネットの接続は必ずしもWearにWi-FiやLTEなどがついている必要はなく、直接インターネットに接続しているかスマートフォンを経由して接続しているかはアプリ作者は意識しなくても良くなります。(ちょうどスマートフォンのアプリをWi-Fi経由かLTE経由かを意識せずにコードが書けるのに似ています。)
これにより、データを中継するためのスマートフォン用APKを用意しなくても良くなり、スマートフォン向けのロジックをそのまま流用してウェアラブルでデータを取得できるようになります。
Wi-FiやLTEで接続されている時はスマートフォンが無くてもデータの通信ができるため、Wearだけを身に着けてスマートフォンは家においたままにしておくというような利用形態が今後広がってくるかもしれません。
これはシステムUIの変化でWearでの操作をしやすくしていることにも合致します。
配布形式の変更
すでにAndroidWear 向けのアプリを作っている人には悲しいお知らせです。Android Wear2.0向けのアプリをGooglePlayで配布する場合、従来のバージョンと互換性ありません。(Notificationのみを使用しておりWear向けAPKを含まないアプリを除く)
Android Wear1.x向けに作られているAPKはそのままではAndroid Wear2.0にはインストールされず必ず対応が必要になります。
![]()
Android Wear1.x向けのAPKはスマートフォン内のAPKに内包する形でGoogle Play storeにアップロードしていました。
スマートフォンはGoogle Play storeからAPKをダウンロードした時に、スマートウェア向けのAPKが含まれていたらそれを取り出してスマートウォッチへ転送することでスマートウォッチのインストールを行っていました。
![]()
Android Wear2.0ではWear向けのAPKをMobile向けに内包する必要はなくなり、それぞれのAPKをどちらもGoogle Play storeにアップロードするようになります。
Wear 1.xとWear2.0のどちらも対象とするアプリの場合は1.xのために依然Wear向けのAPKをMobile向けに含める必要がありますが、それはWear2.0では無視されるため別にWear2.0用のAPKをアップロードする必要があります。
もし、スマートフォンを必要とせずAndroid Wear2.0のみで動作するアプリを作成する場合はWear向けのAPKだけを直接Google Play storeにアップロードします。
このようにWearとPhoneのAPKが別れた理由としては、次のような理由が考えられます。
1.非WearユーザーにとってはWear向けのAPKが一緒にダウンロードされるのは冗長だった。
2.今後スマートウォッチ以外の様々なウェアラブルプラットフォームが登場した場合に、それぞれのAPKを全てスマートフォン向けのAPKに含める方式は破綻する。
3.スマートフォンにはインストールしたいがウェアラブルにはインストールしたくない、あるいはその逆といった柔軟な対応をユーザーが行えるようになる。
4.ウェアラブルのuses-featureやminSdkVersionなどによって異なるAPKを使い分けることができるようになる
5.スマートフォンにアプリが入っていなくても動作するウェアラブルアプリ開発の促進。
とはいえ、開発者にとっては大変です。
従来のウェアラブルアプリを2.0対応するには
すでにWear1.x向けにアプリを作っている人の場合、あるいは今後1.Xと2.0のどちらにも対応したウェアラブルアプリを作りたい場合、次のような手順を取ります。
ウェアラブルのbuild.gradleでdependenciesのcom.google.android.support:wearableを2.0.0-alpha3に変更する。
Android Studioのバグでエラーが表示されますが、そのまま動作します。
compile 'com.google.android.support:wearable:2.0.0-alpha3'
Product flavorsでSdkVersionを元にWear1系と2系を分ける
2系はSdkVersionが24以降となるためminSdkVersionに24を指定すると2系のみにインストールされるようになります。
1系から2系にアップロードされる端末を想定してVersionCodeは2系の方を上げておきます。
これによりSdkVersionが24になった端末はより新しいversionCodeのAPKをインストールしに行くようになります。
注意すべき点として、こんごバージョンアップを行う上でバージョン番号が逆転しないようにするために、番号は大きめにとっておくようにしてください。
例えばWear1を1,Wear2を2などのようにしておくと、今後のバージョンアップでWear1を3にしたタイミングでWear1よりWear2向けのAPKのバージョンコードが小さくなってしまいます。
一般的には最上位の桁をminSdkVersionと一致するようにしておくなどして必ずminSdkVersionが大きな数字を持ったAPKの方がバージョンコードが高くなるようにします。
productFlavors {
wear1 {
versionCode 21001 // 次のバージョンアップでは21002
minSdkVersion 21
}
wear2 {
versionCode 24001// 次のバージョンアップでは24002
minSdkVersion 24
}
}
スマートフォン向けのbuil.gradleを開いてwearApp projectでFlavorを指定するように変更し、Wear1のAPKを内包するようにする。
wearApp project(path: ':wear', configuration: 'wear1Release')
※Google Play storeの取扱方式については現在議論中らしく今後変更される可能性が高いです。
あとはMultiple APKの仕組みを利用してスマートフォン用のAPKとFlavorがWear2で作成したWear向けの署名済みAPKをどちらもGoogle Play Storeにアップロードします。
また、内部ロジックにおいても スマートフォン側のアプリが存在していないことがある。ということを考慮した開発を行う必要があります。
アプリを作ってみる
それでは実際にWear2.0ならではのアプリを作ってみましょう。
今回もComplications APIを利用したData providerを作成します。
以前の連載ではスマートフォンのデータを表示するDataProviderを作成しましたが、今回はWear2.0で追加された直接インターネットへ接続できる仕組みを使ってWear単体でインターネットから天気予報を取得するDataProviderを作成します。
新規プロジェクトの作成
Android Studioで新規プロジェクトを作成します。
Target Android DeviceにはWearを選択し、MinimumSDKはAndroid 7.0 Nougat preview を指定します。(何と素晴らしいことでしょう)
![]()
Add an Activity to WearではData ProviderはActivityを必要としないためAdd No Activityを選びます。
Finishを押して新規プロジェクトを作成したら、appのbuild.gradleを開いて修正を行っていきます。
build.gradleの修正
折角なのでdefaultConfigにjackOptions{ enabled true]を記載してjackを使いましょう。
defaultConfig {
//中略
jackOptions {
enabled true
}
}
compileOptionsでJava1.8を使用できるようにします。
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
dependenciesを記載していきます。
com.google.android.support:wearable:1.4.0
を2系対応に変更します
compile 'com.google.android.support:wearable:2.0.0-alpha3'
provided 'com.google.android.wearable:wearable:2.0.0-alpha3'
Webサーバーからのデータ取得はRetlofit2とRxAndroidを使用してHTTPクライアントはokhttp、JsonのデコードはGSONを使います。
このあたりはスマートフォン向けと同じですね。
compile 'io.reactivex:rxandroid:1.1.0'
compile 'com.squareup.okhttp:okhttp:2.1.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'
compile 'com.squareup.retrofit2:converter-gson:2.0.2'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0'
build.gradle全体では次の通り
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.1"
defaultConfig {
applicationId "jp.co.sample.wear2"
minSdkVersion 24
targetSdkVersion 25
versionCode 1
versionName "1.0"
jackOptions {
enabled true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.google.android.gms:play-services-wearable:10.0.1'
compile 'com.google.android.support:wearable:2.0.0-alpha3'
provided 'com.google.android.wearable:wearable:2.0.0-alpha3'
compile 'io.reactivex:rxandroid:1.1.0'
compile 'com.squareup.okhttp:okhttp:2.1.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'
compile 'com.squareup.retrofit2:converter-gson:2.0.2'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0'
}
続いてAndroid manifestを修正します。
ライブラリーの使用を宣言し、
<uses-library
android:name="com.google.android.wearable"
android:required="false"/>
DataProviderとなるProviderServiceを宣言して、DataProviderとして提供するデータ型にSHORT_TEXTとLONG_TEXTを指定します。
更新間隔は14400(3時間)としておきます。
<service android:name=".ProviderService"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST"/>
</intent-filter>
<meta-data android:name="android.support.wearable.complications.SUPPORTED_TYPES"
android:value="SHORT_TEXT,LONG_TEXT"/>
<meta-data android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS" android:value="14400"/>
</service>
AndroidManifest.xml全体は次の通り
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.co.sample.wear2">
<uses-feature android:name="android.hardware.type.watch"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault">
<uses-library
android:name="com.google.android.wearable"
android:required="true"/>
<service android:name=".ProviderService"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST"/>
</intent-filter>
<meta-data android:name="android.support.wearable.complications.SUPPORTED_TYPES"
android:value="SHORT_TEXT,LONG_TEXT"/>
<meta-data android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS" android:value="14400"/>
</service>
</application>
</manifest>
Gsonの受け皿を作る。
今回はLivedoorの
お天気Webサービスを使用しました。
戻り値のJsonをもとに必要な形式をクラスで作ります。
お天気Webサービスは様々なデータを返してくれますが、今回はテロップと詳細のテキストだけを切り抜きます。
新規クラスでResultWeatherを作り、Jsonの形式に合わせた形にします。
package jp.co.sample.wear2;
import java.util.List;
/**
* Created by kenz on 12/6/16.
*/
class ResultWeather {
List<Forecast> forecasts;
Description description;
static class Forecast{
String telop;
}
static class Description{
String text;
}
}
お天気Webサービスへのデータ取得を行うクラスを作ります。
Networkクラスを作成し、その中にjson/v1へcityをクエリパラメータとして問い合わせを行いResultWeather
を返すようにApiインターフェイスを作成します。
interface Api {
@GET("json/v1")
Observable<ResultWeather> getWeather(@Query("city") String city);
}
static final Api api;
Httpのログとかタイムアウトとかコンバーターなどなどを指定していきます。
基準となるURLはhttp://weather.livedoor.com/forecast/webservice/としています。
Jsonはキャメルケースで帰ってくるため、それも指定しておきます。(今回は使いませんが)
注意点としてウェアラブルは性能が低い上にサービスは実行速度も絞られるためタイムアウトの時間は広めにとっておいたほうが良いです。
どのみち、データプロバイダーの場合はレスポンスは求められないです。
static {
HttpLoggingInterceptor logging = new HttpLoggingInterceptor(message -> {Log.d("newtwork", message);});
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(logging).connectTimeout(30, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS).build();
Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://weather.livedoor.com/forecast/webservice/")
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
api = retrofit.create(Api.class);
}
Network全体は次の通り
package jp.co.sample.wear2;
/**
* Created by kenz on 12/6/16.
*/
import android.util.Log;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.http.GET;
import retrofit2.http.Query;
import rx.Observable;
class Network {
interface Api {
@GET("json/v1")
Observable<ResultWeather> getWeather(@Query("city") String city);
}
static final Api api;
static {
HttpLoggingInterceptor logging = new HttpLoggingInterceptor(message -> {Log.d("newtwork", message);});
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(logging).connectTimeout(30, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS).build();
Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://weather.livedoor.com/forecast/webservice/")
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
api = retrofit.create(Api.class);
}
}
最後にデータプロバイダー部分を作成します。
見ての通り、データを取得する部分はごく普通にRetlofitとRxAndroidのそれです。
取得する天気は私の実利を兼ねて福岡の天気を取得しています。
どの地域を取得するかはCityコードを変えることによって変更できますので好みのコードに変えてみてください。
package jp.co.sample.wear2;
import android.support.wearable.complications.ComplicationData;
import android.support.wearable.complications.ComplicationManager;
import android.support.wearable.complications.ComplicationProviderService;
import android.support.wearable.complications.ComplicationText;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
/**
* データ提供を行う
* Created by kenz on 12/6/16.
*/
public class ProviderService extends ComplicationProviderService {
@Override
public void onComplicationUpdate(int complicationId, int dataType, ComplicationManager complicationManager) {
// 福岡市のデータを取得
Network.api.getWeather("400010").
subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
if (result.forecasts.size() < 0) {
return; // データを取れなかったら何もしない
}
ResultWeather.Forecast forecast = result.forecasts.get(0);
ComplicationData complicationData;
// データを取得できたら定型文に整形して
if (dataType == ComplicationData.TYPE_SHORT_TEXT) {
complicationData = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setShortText(ComplicationText.plainText(forecast.telop))
.build();
} else if (dataType == ComplicationData.TYPE_LONG_TEXT) {
complicationData = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT)
.setLongTitle(ComplicationText.plainText(forecast.telop))
.setLongText(ComplicationText.plainText(result.description.text))
.build();
} else {
return;
}
// Complications に投げる
complicationManager.updateComplicationData(complicationId, complicationData);
});
}
}
Wear特有のコードはComplicationData complicationData;以降の部分で、単純にデータを投入しているだけ。
それでは実際に動かしてみましょう。
Elements AnalogでDataProvidersにWeatherを選ぶと・・・
ちょっと日本語がマッチしていない感じもしますが、無事にデータを取得することができました。
MobileのAPKは使用していないためスマートフォンがつながっていなくてもWi-Fiがあればデータを取得できます。
サンプルコードをGitHubにアップしました6日目のカレンダー こわくない! Fragment8日目のカレンダー