will and way

ただの自分用メモを人に伝える形式で書くことでわかりやすくまとめてるはずのブログ

ImeFragmentというライブラリを公開しました!キーボード開発でもFragmentを使う!

この記事はCyberAgent Developers Advent Calendar 201620日目の記事です。

www.adventar.org

19日目はstrskさんでGKEのノードプールを利用したKubernetesのアップグレードでした。 ちなみにstrskさんは元々飲食業界ではたらいていてCSで入社→今はAbemaTVでGKE運用してる方です。スゴイ、、、!

明日は...○○です。

アドベントカレンダーには去年から参加し始めていて、2015年に書いた記事はこちらです。

同期系スマホアプリのリリースサイクル・テストについて - will and way

1年前はAppleのアプリレビューが1週間くらいだったのか。。。2, 3日で返ってくるようになったのは革命的な出来事だったな〜。

さて、本題のImeFragmentに入っていきましょう!

ImeFragmentというライブラリを公開しました

https://github.com/matsuokah/ImeFragmentgithub.com

一言で言うと、InputMethodServiceでもFragmentとほぼ同じように使って実装ができるというライブラリです。
とりあえず作った感じなので、整理はこれからですが。

IME開発のキモとなりそうなポイントをAndroidのAdventCalendar::day11でInputMethodService(キーボード)開発の勘所となりそうな項目という記事に書きました。

その中にServiceではFragmentは使えないという項目がありました。ImeFragmentはそれを解決しライブラリ化したものです。

Fragmentが使えると何が良いのか?

アプリを実装している感覚で部品が開発できる。アプリの実装が使いまわしやすいということです。

アプリ開発の中でFragmentというインターフェースに慣れ親しんでいます。それは、フラグメントはアプリのライフサイクルだったり、ViewPagerのように動的にアタッチ/デタッチがされた場合のハンドリングだったりします。

Fragmentのようなインターフェースを持つクラスがないので、InputMethodServiceの実装がもりもりになってしまいます いわゆる、"マッチョなActivity"のように、"マッチョなInputMethodService"が避けられない状態です。

"マッチョなInputMethodService"を分割していくということは、コントローラとなりうるクラスを作るということになります。また、そのコントローラの要件はInputMethodServiceに応じたライフサイクルを持つことや、アプリ同様にonTrimMemoryのようなアプリのライフサイクルにも対応している必要があります。

結局、Fragmentが欲しいということです。

Fragmentという粒度のクラスができることによって、Fragmentが依存したいクラス(PresenterやUseCaseなど)の単位もアプリと同じように使えますし、使うイメージも湧きやすいです。

Imeに対するImeFragmentのライフサイクルのマッピングについて

InputMethodServiceはActivityよりも幾つかのライフサイクルのステップが少ないことや、アクションバーを持たないなどの違いがありますが、基本となるonCreateからonDestroyまでのライフサイクルは同じようにマッピングすることが出来ました。
なので、実際にはActivityで使う場合と同じように使うことが出来ます。

サンプルの紹介

InputMethodServiceでViewPagerを使ってみた例です。

f:id:matsuokah:20161220002736g:plain

build.gradle
dependencies {
    compile 'jp.matsuokah.imefragment:imefragment:1.0.1'
}

bintray, jcenterにホストしてあります。

SampleImeService.java
public class SampleImeService extends ImeFragmentService {

  @Override public View onCreateInputView() {
    setContentView(R.layout.ime_main);
    adapter = new SampleFragmentPagerAdapter(getImeFragmentManager());

    WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
    DisplayMetrics dm = new DisplayMetrics();
    wm.getDefaultDisplay().getMetrics(dm);
    int windowHeight = dm.heightPixels;


    View wrapper = findViewById(R.id.ime_wrapper);
    ViewGroup.LayoutParams params = wrapper.getLayoutParams();
    params.height = (windowHeight * INPUT_VIEW_HEIGHT_PERCENTAGE) / 100;
    wrapper.setLayoutParams(params);

    ViewPager pager = (ViewPager) findViewById(R.id.pager);
    pager.setAdapter(adapter);

    return super.onCreateInputView();
  }
}
SamplePageFragment.java
ublic class SamplePageFragment extends ImeFragment {

  private static final String POSITION_KEY = "position_key";

  @Nullable @Override
  public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
      @Nullable Bundle savedInstanceState) {

    View view = inflater.inflate(R.layout.fragment_ime_page, null);

    Bundle bundle = getArguments();
    int position = bundle.getInt(POSITION_KEY);

    TextView label = (TextView)view.findViewById(R.id.position);
    label.setText(String.valueOf(position));

    return view;
  }

  public static ImeFragment newInstance(int position) {
    ImeFragment fragment = new SamplePageFragment();
    Bundle bundle = new Bundle();
    bundle.putInt(POSITION_KEY, position);
    fragment.setArguments(bundle);
    return fragment;
  }
}

解説

public class SampleImeService extends ImeFragmentService {

  @Override public View onCreateInputView() {
    setContentView(R.layout.ime_main);
    return super.onCreateInputView();
  }
}

onCreateはこれはお作法になります。setContentでrootとなるViewをImeFragmentServiceでinflateしています。

理由は、以下の2つです

  1. InputMethodServiceはonCreateInputViewの時にViewをさわる状態ができている
  2. Fragmentを管理する機構の中で親のビューを必要とするため

また、ImeFragmentServiceにはfindViewByIdを実装したので、ActivityのようにViewを取得できるようになっています。

Fragmentに関してはほぼ、解説する必要はないですね。元のFragmentと全く同じです。

実装内容について

本家のサポートライブラリの実装を参考にし、必要なメソッドを再実装していった形になります。Fragmentの管理に使われているクラスにはActivity/Serviceに依存しない実装のものがいくつかあったのですがパッケージプライベートな処理に手を入れる必要があり、コピーしてパッケージにいれています。サポートライブラリのクラスをそのまま使っていたりもします。インターフェースとか。

変わっている点は以下のとおりです。

  • Activityにあって、Serviceにない機能の削除
    • Picture in PictureやMultiWindowMode、OptionsMenuはImeでは不要。
  • InputMethodServiceに合わせて、fragmentのライフサイクルのマッピングを調整
    • onCreate ⇔ onCreate
    • onCreateInputView ⇔ onCreateView
    • onStartInput ⇔ onStart
    • onWindowShown ⇔ onResume
    • onFinishInput ⇔ onPause
    • onWindowHidden ⇔ onStop
    • onDestory ⇔ onDestroy

バグや機能追加のPRまってます!

Androidの開発を始めてから2ヶ月の人間が作ったライブラリなので、保証はできません!リファクタもまだまだ。。。 ということで、コントリビューションをお待ちしております!issueだけでもっ!

https://github.com/matsuokah/ImeFragmentgithub.com

まとめ

InputMethodServiceでもFragmentを使えるようにしました。これによってActivityを作る感覚でキーボードを開発できるようになりました!
趣味でキーボード触ってるんですが、アプリとまた違った可能性を感じています!
変換予測とか考えるのタノシイっ(๑•̀ㅂ•́)و✧

また、このライブラリを開発した副産物として、ActivityとFragmentの関係やFragmentのライフサイクルがどのようなコードなのかを知ることが出来ました。
AndroidのBaseやAndroid Support Libraryのリポジトリ、読んでみると面白いですね!

キーボードを掃除した

そういえば、今年HHKBの無印字を買ったんです。今年買ってよかったものの一つです。

そんなHHKBですがホームページに行くと以下のような文章が書いてあります。

アメリカ西部のカウボーイたちは、馬が死ぬと馬はそこに残していくが、どんなに砂漠を歩こうとも、鞍は自分で担いで往く。馬は消耗品であり、鞍は自分の体に馴染んだインタフェースだからだ。 いまやパソコンは消耗品であり、キーボードは大切な、生涯使えるインタフェースであることを忘れてはいけない。 [東京大学 和田英一 名誉教授の談話]

Happy Hacking Keyboard | 和田先生関連ページ | PFUより転載

ということで、オレたちとっての鞍を大切にあつかうべく掃除しました!
キーボードとか携帯とか常に手で触ってるものって汚いって言いますしね!

掃除風景

f:id:matsuokah:20161217171139j:plain

実は箱に入れて毎日持ち歩いています笑
少し箱の角が擦れてますね。専用のケースが有るらしいですが、この箱で十分です。

f:id:matsuokah:20161217171210j:plain キーボードのトップを取るにはKey Pullerを使います。
クリップとかで頑張れば取れますが、傷がつくので気をつけてください...

ちなみに、WindowsPCがメインの頃、FILCOMajestouchの茶軸を使ってたんですが、Key Pullerがキーボードについてきました。ヤサシイっ!!

f:id:matsuokah:20161217171211j:plain こんな感じで抜けます

f:id:matsuokah:20161217171213j:plain キートップを横に並べてみました。
叩きやすいように、1行ごとに角度が違っています。
ラップトップのように平面と何が変わらないんだ?と最初思ってたけど、平面のキーボードは手首の移動の距離が多く、不便に慣れてたんだな〜と感じました。(主観)

f:id:matsuokah:20161217171214j:plain こんな感じで全部取りました。混ざると面倒なので、行ごとに100円ショップの水切りネットに小分けします。小分けダイジ!!

f:id:matsuokah:20161217171217j:plain キートップの洗浄には重曹を使います。
洗面台にお湯を張って、大さじ3杯ほど溶かし、かき回して30分くらい放置します。

f:id:matsuokah:20161217171219j:plain つけ置きが完了したら、十分に水で洗いで、外に干します。水切りネットのお陰で干すのも楽! だいたい乾いたら最後にドライヤーで完全に乾かします。これもネットの上からでOK

f:id:matsuokah:20161217171218j:plain キートップを洗浄している間に、キーボードのゴミを掃除機で吸い取ります。
取れない細かな汚れは、カメラを清掃する用のブラシ付きのブロアーを使いました。

f:id:matsuokah:20161217171220j:plain 掃除に夢中になって、最後の方の写真がかなり抜けちゃってますがこんな感じで掃除が終わりました!
キーボードの間から見えるチリがなくなったのでかなり清潔感が出たのでは!?!?!

[asin:B000EXZ0VC:detail] HHKB最高っ!

良いルーターを使うのはもはやライフハックつだ!!! TP-LINK AC3150 レビュー

Amazon Cyber Mondayあざす!!!

[asin:B01LYTYIKF:detail]

買ってしまった!良いルーター!それは TP-LINK Archer 3150!

TP-LINK? 聞いたこと無い?

私自身も日本ではバッファローエレコムNECが有名すぎて、TP-LINKを知らなかったんですがTP-LINKは世界シェア43%の企業。

最速のワイヤレス規格である802.11adのルータを世界初でお披露目するほどです。

ガジェット好きの先輩の家に行ったらTP-LINK Archer C9が置いてあって、繋いで見たところ接続の安定性・速度に感動したのがきっかけ。

そして、現在使っているBuffalo WZR-600DHPも2012年発売の機種なので古くなってきたし、TP-LINKのルータに買い換えよう!と
2週間程しらべたり、チャンスを伺っていました。

"日本で発売される最上位機種"のArcher5400が12月中に発売予定なので、こちらを買うつもりでいたが、
AmazonのCyber MondayでArcher3150がセールになっていて15,800円に値下がりしてたので買ってしまった!!!ヨドバシでは22,000円程なので、ポイントを考えても安かったです。

現在でも20,000円前後なのでAmazonで買うのがおすすめかな?ポイントが付くヨドバシでもいいかも(๑•̀ㅂ•́)و✧

上記で"日本で発売される最上位機種"と書いたのは
日本で公式に発売される予定がたっていないであろうTP-LINK AD7200Amazonで販売されているから。

AD7200は"世界初の802.11ad対応のルータ"で、Amazonでは最安で4万円程ですが、元値は$350前後のようです。発売されてから1年たつのに割高。 Amazon, Rakuten, 公式サイトをみても品薄な感じです。 公式販売されないということは、技適が通っていないかもしれません。

Archer 3150で満足できない!というかたは素直に12月中発売のArcher 5400を待ちましょう。

話を戻しまして、、、金曜日に注文して、本日土曜日に到着したので早速開封・セットアップしレビューしていきます!

開封の儀・セットアップ

はい、箱ドーーン

f:id:matsuokah:20161210214020j:plain

でかい。開封の儀を執り行います。

f:id:matsuokah:20161210214846j:plain

同梱物は本体、アンテナ x 4, 電源コード, 電源アダプタ

f:id:matsuokah:20161210214152j:plain

ポートが
背面にはWAN x 1, LAN x 4
側面にはUSB2.0 x 1, USB3.0 x 1

f:id:matsuokah:20161210213801j:plain

Mac Book Air 11inchを横に並べると大きさがより分かる。

あとは接続設定しておしまい

PCでセットアップした場合Wi-Fiに接続しセットアップの手順に従って入力していけば3分くらいで終わりました。

personal-studio9.com

上記のブログではWebのセットアップ手順が載っています。MacBookPro Touchbar 13inchとの大きさの比較も載ってます。考える事同じ笑
そもそも、MacBookAirの11inchと同じくらいの大きさってことになるMacBookProのTouchbarの13inchは相当コンパクトだな・・・!!!

が、今回はせっかくなのでアプリから設定しなおしてみる。

play.google.com

今回はAndroidのアプリです

1. ルーターを選択

f:id:matsuokah:20161210224640p:plain

2. 設定項目 > インターネット

f:id:matsuokah:20161210224633p:plain

3. 各欄の入力

f:id:matsuokah:20161210224638p:plain

これで終わりです。

基本スペック

Archer 3150の製品情報仕様より抜粋して転載しています。

ワイヤレス規格規格

IEEE 802.11ac/n/a 5GHz
IEEE 802.11b/g/n 2.4GHz

セキュリティ

64/128-bit WEP、WPA/WPA2、WPA-PSK/WPA-PSK2 暗号化方式

特徴

1. MU-MIMO

マルチユーザー、マルチインプット、マルチアウトプットの略で

簡単にいうと、複数機種を同時接続してもサクサク!ということです。

しかし、受信側もMU-MIMOに対応していないと有効になりません。

私の普段使いであるスマホのXperiaZ5とNexus9はMIMO対応で、MU-MIMOではないようです。(製品ページのスペックを読んだり、ググったりしたが見つからなかったので。)

私が自宅で使っている2012 midのMac Book Pro Retina802.11acにすら対応していない。買い替えどきかな。

2. デュアルコアコプロセッサx2搭載

ルーターデュアルコアってきいたことなかったんですが、ハイエンドルーターでは当たり前のようです。

NAS接続したり複数の端末を同時に動画を見てもヌルサクなのはこのおかげかも?

欠点としては若干の発熱があることです。ファンはうるさくないのですが表面がほんのり温かくなります。

ネットワークテストしてみる

5GHzのネットワークに繋いでテストしてみます。

スピードテスト1 単体でテストしてみる

使用アプリ
Speed Test

使用デバイス Nexus9(MIMO対応) DL: 67.49Mbps
UL: 93.80Mbps

スピードテスト2 複数台で同時にテストしてみる

使用デバイス

Nexus9(左)の結果
DL: 25.91Mbps
UL: 64.04Mbps

XperiaZ5(MIMO対応)(右)
DL: 24.73Mbps
UL: 32.43Mbps

さすがにWANがボトルネックですね!各値を足すと単体でテストしたときとほぼ同じ数字になります。

スピードテストYoutubeのシークを同時にしてみる

※音が出ます

使用デバイス
Nexus9(左)
XperiaZ5(右)

シークが早い気がする。多分。

RSSI値を調べる

ルーターがを置いているリビングと廊下を隔ててたところにある寝室でテストしてみました。
dB値が大きい(-なので値が低い)方が電波強度は強いです。

MBPのWi-FiOption + Clickで調べられます。
また、このテストはルーターの置き場所や遮蔽物の有無によって結果が変わって来ることが注意点です。

5GHz

f:id:matsuokah:20161211000056p:plain

リビング: -39dBm
寝室:-65dBm

2.4GHz

f:id:matsuokah:20161211000128p:plain

リビング: -40dBm
寝室:-53dBm

ということで、2.4GHzの方がさすがに隔てた部屋でも強いですね!

前のルータだと2.4GHzのみで寝室に移動すると電波強度が80dBm程度でした。(スクショ撮り忘れた)
また主観ではありますが、接続自体も不安定だったので改善されています。寝室では、頻繁にWi-Fiの検索中になっていた。

まとめ

ということでTP-LINKのWi-FiルータArcher 3150を紹介しましたが
快適なWi-Fiはもはやライフハックなのでは?と感じる体験レポートでした。ぜひご検討あれ!

[asin:B01LYTYIKF:detail]

InputMethodService(キーボード)開発の勘所となりそうな項目

この記事はAndroid Advent Calendar 2016 - Qiitaの11日目の記事です。

昨日は@yuyakaidoさんのData Binding Tipsでした。 明日は@rei-mさんのDagger2とMockitoでUIテストはじめる話です。

11日目はAndroid StudioでKotlinのプロジェクトが新規で作られるソースを
そろそろKotlinを選べるになってほしいmatsuokahが担当いたします。

InputMethodServiceに触れていて、キーボードのクセを掴まねば!ということで、勘所となりそうな項目を書いてみます。

InputMethodService(以下、ほぼ同義のIME)とは

いわゆるキーボードの継承元となるServiceです。
Simeji、Google日本語入力POBoxATOKが日本では有名ですね。

IMEはググっても、ヒット数が少ないのでマイナーな分野ですね。

IMEアプリの作り方はハンズオン記事を見ればわかります。
サンプルもあります。

ざっくりキーボードの作り方

  1. InputMethodServiceを継承したクラスを作成
  2. Android Manifestにサービスの定義を行う
  3. カスタムしていく

本当にざっくりですがこんな感じです。
ハンズオン記事ではMainActivityに<category android:name="android.intent.category.LAUNCHER"/>を記載していないですが、
設定アプリとしてActivityを使うはずなので消さなくて良いでしょう。

何のメソッドをオーバーライドすべきか?

を見て、リストしてみました。

Intentionally emptyとコメントが書かれているメソッド

  • onBindInput
  • onUnbindInput
  • onInitializeInterface
  • onStartInput・・・初期化
  • onStartInputView
  • onStartCandidatesView
  • onWindowShown
  • onWindowHidden
  • onDisplayCompletions
  • onViewClicked
  • onUpdateCursor
  • onUpdateCursorAnchorInfo

デフォルトでnullを返却しているメソッド

  • onCreateInputView・・・ここでビューを作成する
  • onCreateCandidatesView

デフォルトでfalseを返却している

  • onTrackballEvent
  • onGenericMotionEvent

フルスクリーンモード

  • onEvaluateFullscreenMode・・・画面を回転させた時に、フルスクリーンモードにするか否か

ということで、基本的にはここらへんのメソッドをオーバーライドしてIMEをカスタムしていくことになります。
リストにはライフサイクルに関わるメソッドも含まれています。

また、

ライフサイクル

f:id:matsuokah:20161211020251p:plain

Android Developers - creating-input-methodより転載

というような単純なライフサイクルとなっています。
しかし、思わぬ所でイベントが発火されるケースが幾つかあります。

操作して、ライフサイクルでログを追っていきましょう。

f:id:matsuokah:20161211021719g:plain

これが最もシンプルなIMEの操作だと思います。実際のログは以下のとおりです

## MyImeへ切り替えを開始
D/ImeService: onCreate
D/ImeService: onCreateInputMethodInterface
D/ImeService: onCreateInputMethodSessionInterface
D/ImeService: onInitializeInterface
D/ImeService: onBindInput
D/ImeService: onStartInput, restarting : false
D/ImeService: onCreateInputView
D/ImeService: onCreateCandidatesView
D/ImeService: onStartInputView, restarting : false
D/ImeService: onWindowShown
## IMEの表示が完了

## 別のIMEへの切り替えを開始
D/ImeService: onFinishInputView, finishingInput :   true
D/ImeService: onFinishInput
D/ImeService: onStartInput, restarting : false
D/ImeService: onStartInputView, restarting : false
D/ImeService: onUnbindInput
D/ImeService: onFinishInputView, finishingInput :   true
D/ImeService: onFinishInput
D/ImeService: onDestroy

思わぬ所でイベントが発火されるケース

と、述べましたが onStartInput, onStartInputViewが何故かonFinishXXXの後に呼ばれています。
私はStartと書いてあるのでIMEが立ち上がって入力が開始された時だけ発火すると勘違いしていました。

ではなぜ発火されているか。それは、編集していたEditTextのフォーカスが外れた時に、IMEの状態をリフレッシュするためです。

IMEは1つのインスタンスであるのに対し、画面には複数のEditTextが存在することが多いです。
編集するEditTextが切り替わると、すでに入力されているテキストや入力タイプ(Numeric, AlphaNumeric, Passwordなど)が変わります。
したがって、onStartInputでは一時的な入力データなどを破棄し、引数で与えられているEditorInfoオブジェクトの情報から次の入力に備える必要があります

実はSDKのドキュメント冒頭にあるGenerating Textという項目で述べられています。ドキュメントはしっかり読もう。

そして、ログに出てきてないので気づきにくいのですがonWindowShownに対し、onWindowHiddenが呼ばれていません。
onWindowHiddenIMEが破棄されないが非表示になるときタスクマネージャ起動時に発火されます。

従ってonWindowHiddenでTearDownするのは好ましくない実装だと思います。

onStart系のライフサイクルメソッドでスクラップアンドビルドしていくのが良いかなと個人的に思います。

Fragmentが使えない

私はIMEでもFragmentが使えると思いこんでいました。
ところがFragmentはActivityに依存しているのでServiceで使えないのは当然なわけであります。

iOSのLINEのスタンプのようなUIを実装してみようと思っていたのですが、
Fragmentが使えないのでViewPagerを使うにはAdapterの独自実装が必要となります。

LINEのUIなので、画像が多い前提となるため、Fragmentのライフサイクルの様にページを破棄することも考慮に入れる必要があります。

ということで、ひとまず最低限の実装でFragment・FragmentManager・FragmentPagerAdapterの実装を真似て作ってみました。FragmentのTransactionは実装していません。

f:id:matsuokah:20161211035021g:plain

ImeFragment.kt

abstract class ImeFragment() {
  lateinit var service : InputMethodService
  val context : Context
  get() = service


  var tag : String = ""
  var containerId : Int = 0
  var view : View? = null

  open fun onCreate() {}
  open fun onCreateView(inflater: LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? { return null }
  open fun onAttach() {}
  open fun onDetach() {}
  open fun onDestroyView() {}
  open fun onDestroy() {}
}

ImeFragmentManager.kt

class ImeFragmentManager(val service : InputMethodService, val rootView : View) {

  val fragments : HashMap<String, ImeFragment> = HashMap()
  val attached : HashMap<String, ImeFragment> = HashMap()

  fun add(containerId : Int, imeFragment : ImeFragment, tag : String) {
    if (fragments.containsKey(tag)) throw IllegalArgumentException("already container has fragment")

    imeFragment.containerId = containerId
    imeFragment.tag = tag

    fragments[tag] = imeFragment
    attach(containerId, imeFragment, tag)
  }

  fun remove(tag : String) {
    if (attached.containsKey(tag)) {
      detach(attached[tag]!!)
    }

    val fragment = fragments[tag]!!
    fragment.onDestroy()
    fragments.remove(tag)
  }

  fun findFragmentByTag(tag : String) : ImeFragment? = fragments[tag]

  fun attach(containerId : Int, imeFragment : ImeFragment, tag : String) {
    if (!fragments.containsKey(tag)) throw IllegalArgumentException("fragment is not added")
    if (attached.containsKey(tag)) throw IllegalArgumentException("already container had attached")
    attached[tag] = imeFragment

    imeFragment.service = service
    val container = rootView.findViewById(containerId) as? ViewGroup
    imeFragment.view = imeFragment.onCreateView(LayoutInflater.from(container?.context), container, null)

    container?.addView(imeFragment.view)
    imeFragment.onAttach()
  }

  fun detach(imeFragment : ImeFragment) {
    if (!attached.containsValue(imeFragment)) throw IllegalArgumentException("fragment not found")
    val container = rootView.findViewById(imeFragment.containerId) as? ViewGroup
    container?.removeAllViews()
    imeFragment.onDestroyView()

    attached.remove(imeFragment.tag)
    imeFragment.onDetach()
  }
}

ImeFragmentPagerAdapter.kt

abstract class ImeFragmentPagerAdapter(private val imeFragmentManager : ImeFragmentManager) : PagerAdapter() {

  abstract fun getItem(position : Int) : ImeFragment

  override fun instantiateItem(container : ViewGroup?, position : Int) : Any {
    container ?: throw IllegalArgumentException("")

    val tag = createTag(container.id, position)
    var fragment = imeFragmentManager.findFragmentByTag(tag)

    if (fragment == null) {
      fragment = getItem(position)
      imeFragmentManager.add(container.id, fragment, createTag(container.id, position))
    } else {
      imeFragmentManager.attach(container.id, fragment, createTag(container.id, position))
    }

    return fragment
  }

  override fun destroyItem(container : ViewGroup?, position : Int, obj : Any?) {
    val fragment = obj as ImeFragment
    imeFragmentManager.remove(fragment.tag)
  }

  override fun isViewFromObject(view : View?, obj : Any?) : Boolean = (obj as ImeFragment).view == view

  companion object {
    fun createTag(viewId : Int, itemId : Int) : String = "android:ime_switcher:$viewId:$itemId"
  }
}

Fragmentっぽい実装を用意することで、Page単位にコントローラを分離できること・ドメインレイヤーとの繋ぎの所にもできるので欠かせないな〜という所。
ココらへん、ライブラリ化して公開します。多分。

小ネタ. Macで音のボリュームを無段階調節

知らなかったので驚いた

Shift + Option + Functionキーで音量とかディレスプレイ輝度の調整がより細かくできる!!!

普通にボリュームup/downすると以下のようにメモリ毎にボリュームが変わりますが

f:id:matsuokah:20161205223454g:plain

Shift + Optionを推しながら調節すると・・・

f:id:matsuokah:20161205223539g:plain

うおおおおおおおおおおおおおおおおお!!

f:id:matsuokah:20161205223815g:plain

うおおおおおおおおおおおおおおおおおおおおおおおおおおおおおお!!

f:id:matsuokah:20161205223902g:plain

ま、そんなに細かく調整する必要ないんですけどね

僕のPCはMBPですが壁紙はiMacの基盤です

Androidのコードを書く前にコーディングに集中できる状態を作る

はじめに

この投稿はAndroid その3 Advent Calendar 2016 - Qiitaの5日目の記事です。

4日目はkimukouさんのrealm-gradle-plugin 2.2.1 と Android Realm Browserでした。
6日目はkimukouさんの2016年末のAndroidでのSSL対応に関してです。

Androidのコードを書く前にコーディングに集中できる状態を作る

今年のAdvent CalendarはAndroidでKotlinを導入する話がかなり多くて、Kotlinが浸透しているんだな〜と感じています。
かくいう私もKotlinを使ってみた所感やテクニックのネタを考えてたんですが、Kotlinは愚かAndroidを本格的に開発し始めて2ヶ月ですので、エキスパートな部分は先人達にお願いするとします。

最近異動して、0からのプロジェクトに携わらせてもらっていて、コードを書く前に環境を整えることを最初にやりました。その時のまとめになります。

目次

  • なぜやるべきか。ずばり何をするのか
  • 命名規則, Code Style
  • プロジェクト構成, BuildType/Product Flavor, DI
  • データ層からプレゼンテーション層のサンプル実装
  • CI
  • プルリクのルールを作る
  • 必ず使うSDK, ツールの導入
  • テンプレートコード・スニペットをつくる
  • 開発環境のプロビジョニングをつくる

なぜやるべきか。ずばり何をするのか

プロジェクト初期になぜやるべきか。
先に決めて開発期〜リリースの間にコーディングに集中できる環境があると、エンジニアの精神衛生が良い状態が保たれるからです。 そして、プロジェクトが発足したての頃は仕様が 無い or 部分的 or 一朝一夕で変わるが当然なので機能実装以外に時間を割く時間が作りやすいです。

ずばり何をするのか。
ルール化自動化です。
プルリクを前提とした開発では、コードレビューのコストを大幅にされるでしょう。ルールと自動化をメンテしましょう。コードレビューで主観的(書き方の好み)な議論をするのは楽しいですが開発スピードとのトレードオフになります。世に出ていないサービスはスピードの方が重要でしょう。

では、さっそく各項目について、まとめをつらつらと書いていきます。

命名規則, Code Style

命名規則

大雑把なルールを作り、必要になったらルールを追加していきます。

接頭辞 or 接尾辞

リソースは接頭辞ベース、クラスは接尾辞ベース
Android Best Practices#Resourcesをベースにすると良い。日本語にも翻訳されています。

レイアウトの変数に名前をつける

レイアウトの変数名に概念や機能性をもたせることで、デザイナーと列挙された数値の共通認識をもつことが出来ます。 Androidはldpi(x0.75)を考慮して4の倍数でレイアウトをする必要があるので、そこだけ3dpみたいなことは極力避けたいです。

現在のプロジェクトではマージンやパディングの数値(4, 8, 16, 24)にspacing_s, spacing_m ... と名前をつけてます。

フォントのサイズにはHTMLのタグ名を参考にすると良いかもしれません。
title, header, body, paragraph ...
コンテンツの優先順位でフォントサイズを変えるということが直感的になります。

※ルール化することでデザイナーの発想の範囲を狭めることがあります。

Code Styleをととのえる

スペースや改行は好みが分かれるので、先にルールを決めるのがいいです。
Code StyleはAndroid界隈の神らしい、Jakeが在籍するSquareのコードフォーマットを拝借しましょう。

フォークして微調整してプロジェクトのルールとするのが手っ取り早いです。Google Java Style Guideをベースにチューニングします

プロジェクト構成, BuildType/BuildVariants, DI

プロジェクト構成をつくる

いわゆるアーキテクチャです。MVP/MVVM/MVCなどのパターンから、どれで実装していくかを決める必要があります。

BuildType/Product Flavorを追加する

ビルドタイプを分ける(Debug, Release)
  • ロギングの切り替え(コンソールログ/クラッシュログをサーバーに転送する)など
環境を分ける(development, staging, production)
  • Endpointの切り替え
  • アプリアイコンなどのリソースを環境毎に変える
  • KeyStoreの切り替えをtaskに含める

DIを導入する

上記のアーキテクチャとビルドタイプを統合した構成を考える必要があります。 勘所はAPIのエンドポイントやログ、モックデータを返却するAPIなどを切り替えられるように意識しましょう。

例えば、Retrofitを使っているプロジェクトでdebug/release毎にRetrofitのインタセプタのDIの戦略を変えるというような事です。 debug時だけloggerのインタセプタのDIを有効にしてAPIアクセス時のに詳細なロギングをするといったことが可能です。

現プロジェクトではDIにDagger2を使っており、インスタンス戦略は

  • Context(Application)スコープ
  • View(Activity/Fragment)スコープ

の2つに着地しています。Contextレベルなのはドメインレイヤーから下です。

データ層からプレゼンテーション層までの一気通貫サンプルを実装する

プロジェクト構成が出来たら、その構成が本当に使いやすいか検証する必要があります。自分で実装してみるのが一番です

サンプル実装でマニュアルも同時に終わらせることが出来ますし。そして、Android初心者な私にとって一気通貫させるのは実装イメージを沸きたてるにはベストでした。

CIを導入する

とりあえずFastlaneを導入してFastfileで各環境のビルドをできるようにしておくことが重要。

Gemfileも定義して、bundle exec fastlane [ENV] type:Debugくらいのコマンドでビルドできるようにしておけば、Circle CIを導入してもdeploymentディレクティブでの細かな設定が不要になります。

実際に運用しているのは

  1. Circle CIでbundlerをつかってfastlaneをフック
  2. ターゲットとなる環境のアプリをビルド
  3. Crashlytics Betaにアプリをポストし、テスターグループにアプリを公開
  4. Slackにビルド完了通知

Fastfileにはフローを定義し、Contextにプロジェクト情報を埋め込む形に着地しています。

※FastfileはBuildType, BuildVariants, Keystoreの設定と合わせる必要があります。

プルリクエストルールを作る

前のプロジェクトのチームのプルリクにはルールが定められていて、1年以上つづけていて、とてもよかったたので概要を紹介。

  1. プルリクのフォーマット化
    1. 概要、バグフィクスの原因など
    2. 目的やアクションを記載する(仕様を含む、編集の意図をソースコードから汲み取らない)
    3. 修正の影響範囲がわかるURLを含める(開発ロードマップやCSのバグチケ)
  2. UI変更はBefore/Afterのスクショ必須
  3. インタラクションならGIFアニメ必須

詳細は割愛します。Cookpadさんの開発速度を上げるための Pull-Request のつくり方に近いです。

必ず使うSDK, ツールの導入やリスト作成する

仕様にかかわらず必要となる機能やツールは少なくとも決まってるので時間があるときに導入だけしておくことで、終盤で導入して急遽Multi Dex対応が必要になるみたいな事が減らせると思います。

テンプレートコード・スニペットをつくる

Dagger2を使う場合にはComponent, Module, Scopeを大量生産するでしょう。
IntelliJにはファイルテンプレート機能があり、予めApache Velocityのフォーマットに従った形式でソースコードを書いておけば、ファイルの新規作成時に選ぶことが出来ます。 テンプレート化によって、フレームワークを使うときのフットワークを軽くすることが出来ます。

#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}

#end
#parse("File Header.java")

import javax.inject.Scope
import kotlin.annotation.AnnotationRetention.RUNTIME

@Scope
@Retention(RetentionPolicy.RUNTIME)
annotation class ${NAME}

開発環境のプロビジョニングをつくる

AnsibleでAndroid SDKのインストールを図る。

Android Studioがよしなにやってくれることも多いので、ツール群の導入くらい。

まとめ

プロジェクトを始める発射台となるような手順をまとめた。とにかく最初は開発しやすいように、大雑把なルールや開発を加速させる周辺を整える事が重要。

そして、ユニットテストやUIテストの導入がまだできてないので、これからやりたい。

あわせて読みたい

blog.matsuokah.jp

Android 2.3系, 3.0系を対応しているならそろそろ潮時なのかもしれない

Highlights from the Google Play services 10.0 release.

Google Play Servicesのv10系がリリースされ、リリース文の序文に

Google Play services 10.0.x is the final release that includes full support for Android version 2.3.x

2.3を切るよと書かれている。

android-developers.blogspot.jp

The next scheduled release of these libraries, version 10.2.0, will increase the minimum supported API level from 9 to 14 (Android 4.0.1, Ice Cream Sandwich).

次のリリース予定である10.2.0ではminSdkをv14(Android 4.0.1)に引き上げる予定とのこと。

更にAndroid Developersブログの案内では続けて v10.0.0系をlegacy, v10.2.0系をcurrentというProduct Flavorsを作成し、Multiple APKを使えばいいと案内しているが

メンテコストが膨らむのが目に見えてるでしょ。

10.0.0系と10.2.0系以降のライブラリのインターフェースや動きが同じとは限らない。

切り時なのだね。きっと。

Androidの開発にはIntelliJでも良いじゃんと気づいた話

f:id:matsuokah:20161204181038p:plain

IntelliJをインストールしていながらもAndroid Studioを使っていたが IntelliJAndroidプロジェクトを読み込んでみたら、Android Studioと全く同じように開発できた。

そして、上位互換だった。そんな話。

社会人になってからの職歴とエディタ歴は

ざっくりこんな感じ。

IntelliJに1年だけデビューしておき、その後は惰性でアップデートしていたので損してたな〜。

Android StudioIntelliJの若干古いバージョンがベースとなって無料配布されているだけ

Android Studio2.2はIntelliJ IDEA 2016.2がベースとなっている。

IntelliJ最新バージョンは 2016.3である。

Constraints Layoutは2016.2ベースのAndroid2.2に含まれているのに対し、2016.3の機能となっている。

メリット/デメリット

メリット: Kotlinの最新の機能がいち早く使える

ちょっとしたメリット. どっちも持ってるならIntelliJを使うという選択をすべき。

What's New in IntelliJ IDEA - 2021.3

上記の2016.3 whats newにも記載されていますがKotlinのPostfix Completionが先行して使えるようになっている。

Postfix Completionというのは以下のGIFのようなコード補完機能です

f:id:matsuokah:20161204171400g:plain

いわゆるLiveTemplateをその変数にスコープを絞った機能といった所。

以下のGIFはLiveTemplateで同じコードを書いた場合。変数を絞り込んでいく手間を感じる。

f:id:matsuokah:20161204171834g:plain

ちなみに、Android Studio 2.2ではまだ使えない

blog.jetbrains.com

上記の記事によると

Note that the feature depends on platform changes made in IntelliJ IDEA 2016.2 and is therefore unavailable in Android Studio 2.2;

ということで、Android Studio 2.2ではこの恩恵を授かれない。LiveTemplateは使える。

もちろん、IDEとしての機能はIntelliJを使ったほうが先に使えるので、そのアドバンテージはあると思います。

デメリット: IntelliJのほうがAndroidの最新機能を使えるようになるのが遅いかも

Android Studi 2.2 Release
-> 2016/09/19 Release!

IntelliJ IDEA 2016.3 GA: Java 8 and ES6, Debugger and UI improvements, and a Ton More
-> 2016/11/22 Release

  1. IntelliJ 新機能(Android Studioのベースとなるバージョン)
  2. Android Studio(ここでAndroidの機能が追加)
  3. IntelliJ(IntelliJAndroid Studioと同等の機能が追加される)

というアップデート順だとしたら、Androidの新機能が使えるようになるのは若干遅いかも?

Android Studioで試験的に導入してから、IntelliJに導入してるのだろうか。

Android StudioからIntelliJへの移行をするには?

Android SDKを設定する

  1. Android Studioの設定からAndroid SDKのパスを設定する

  2. Confiture > Project Defaults > Project Structure f:id:matsuokah:20161204172242p:plain

  3. SDK > + > Android SDK f:id:matsuokah:20161204172331p:plain

これでAndroidプロジェクトが作成・ビルド可能になります。

f:id:matsuokah:20161204173032p:plain

見慣れた編集画面。

Kaptを使っている場合はgenerateStubsを有効化する

kapt {
    generateStubs true
}

上記の設定を入れるだけで筆者の環境ではビルドできた。

before : 17sec after : 20sec

コンパイル時間が若干、長くなる。

参考:
qiita.com

IntelliJユーザーとAndroid Studioユーザーが共存できるのか?

できます。 Code Styleの設定ファイルなんかも、形式が同じなのでAndroid Studioの設定をExportしてIntelliJに読み込むとも可能