Save Actionsというプラグインに感動した話

DroidKaigi2017の会場には行かなかったけれども、参加した人や公開されたスライドなんかは一通りチェックしている。そんな中でこちらのスライドでSave Actionsなるプラグインを知る。

少し幸せになる技術

Android Studioで保存時にフォーマットを自動で揃える

Save Actions – JetBrains Plugin Repository

Save Actions – GitHub

Android Studioでビルドしたりファイルの保存が行われた際に、自動的にoptimize importやreformat codeを実行してくれるプラグインである。

ファイルの保存時にrefomart codeを実行する方法として、cmd+sにマクロを割り当てて行うという方法は以前から知っていたのだが、私は導入していなかった。というのも、私はAndroid Studioを使っていてただの一度も自分からファイルの保存を行ったことがないからである。そもそもcmd+Sを押す習慣がなかったのだ。それだったら気がついたときにalt+cmd+lでreformat codeを実行するのと大差ないなと思って導入しなかった。なのでreformat codeを実行することもよく忘れていた。(droidkaigi2017のリポジトリにフォーマットしてないコードをプッシュしたりしていた、申し訳なかった)

しかしこのSave Actionsプラグインに出会えたことで、今後そんなミスは起こさないだろう。

このSave Actionsは、Android StudioのPreference > Pluginsからは検索することができない。作者の方がAndroid Studioによる動作確認が取れていないのがその理由らしい。

導入するにあたっては、JetBrainsのPlugin Repositoryからzipファイルをダウンロードしてきて直接インストールする必要がある。

ちなみに、今のところAndroid Studioだからきちんと動作をしないという事象には出くわしていない。

素晴らしいなと感じたのが、ファイルが自動保存されるタイミングやビルド(デバッグ実行)したタイミングで、自動的にreformat codeが走ってくれること。しかもそのSave Actionsのreformat codeで修正された部分は、自分が修正したコードとは異なる色で表示される(新規が緑、変更が青、Save Actionsによるreformat分はグレーみたいな感じで色分けされる)。あくまでAndroid Studioのエディタ上での表示の話なので、コミットする際に別れてくれるわけではないけれども。

なお、Save Actionsを導入する場合は、必ずFile path exclusionsに.gradleファイルを追加する必要がある。

https://github.com/dubreuia/intellij-plugin-save-actions/issues/51

Gradleファイルをいじる際に、キーをタイプするたびにreformat codeなどが走るというバグがあるからだ。

個人的にはプラグインのバグというより、IntelliJ系IDEの仕様なのではないのかと思っている。gradleファイルはspaceを追加するだけでも「syncしろ」と言ってくるので、このあたりの動きが関連しているようにも思う。ともあれ、当座のところはgradleファイルをSave Actionsの対象外にすることでしのげる。

ともあれ、個人的にとても便利なプラグインだと思う。あまりに感動して作者にお礼のツイートを送ってしまうほど、ドンピシャなプラグインだった。

少し幸せになる技術というスライドで紹介されていたが、私は少しどころではなくかなり幸せになれた。ありがたい。

setLayoutParamsには親のViewGroupに属するLayoutParamsをセットする

基本的に私はViewをJavaのコードで生成することは稀なので、適当にやっていたのだけれど。

RelativeLayout parent = new RelativeLayout(context);
TextView hope = new TextView(context);
parent.addView(hoge);

みたいな感じでコードから生成したとき、このViewのwidth/heightにmatch_parentとかwrap_contentを設定するのに、LayoutParamsを使うことになると思う。

parent.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.MATCH_PARENT, RelativeLayout.WRAP_CONTENT));
hoge.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.MATCH_PARENT, RelativeLayout.WRAP_CONTENT));

みたいに。そこでLayoutParamsを指定するのに、なぜいちいちRelativeLayoutとかクラスを指定しているのだろうと、毎度のこと不思議に思っていた。別にViewGroupでもいいんじゃないのかとか思いながら、基本的にはAndroid Studioの補完によって出てきたものをそのまま使っていた。

とりあえずViewGroup指定しとけばオッケーだと思っていたのだが、どうもそれはオッケーではなかったらしい。たまたま今まで問題に出くわしていなかっただけだった。

親のViewGroupのLayoutParamsを使わなければならない

まあ大抵の場合、ViewGroupを使っておけば問題ないのだろう。LinearLayoutとかRelativeLayoutとかFrameLayoutとかにコードから生成したViewを追加していくだろうから。しかしバージョンによってはそれではうまく動かないことがあるらしいということがわかった。

私が出くわしたのは、ListViewに表示するViewをコードから生成したときに、ViewGroup.LayoutParamsを使うとクラッシュするという症状だった。ちなみにAndroid7で確認したら問題なく動いていたので、普通にスルーしていた。4.4で確認したらクラッシュした。

ListViewもViewGroupを継承しているのだからViewGroupでも問題ない気がするのに・・・。

ちなみにこれはAbsListView.LayoutParamsを使うことで、4.4でも7でもクラッシュしなくなくなった。

この問題から学んだことは、LayoutParamsは親のViewGroupに属するLayoutParamsを使わないといけないのだということである。LayoutParamsいっぱいあって適当に選んでいたが、今後はちゃんと指針が持てたという意味で、よい経験ができた。

CIを導入してみた

FilteredHatebuにCIを導入してみました。

CI上での設定も必要なので、これだけで導入できるわけではありませんが、CI導入のための変更はこんな感じです。エミュレータが必要なEspressoでのテストがこけないよう、テストコードを修正した変更も含まれています。

そもそもこのアプリをpublicなリポジトリでやっているのは、実はCIを導入してみたかったからという理由がありました。

最初はとりあえずCIを導入してみようというだけで作業を始めたのですが、結局よく目にするTravisCI、CircleCI、Werckerの3つを試してみました。

CIを使うにあたっては、publicなリポジトリで運用する、無料でできる範囲でやる、あまりにややこしいことはやらない(できればそのCI単体で完結させる、ただしSlackへの通知は除く)という3点を念頭にやってみました。

各CIサービスを使ってみた感想

  • TravisCIは秘匿情報を比較的安全に運用しやすい
  • CircleCIはテストレポートを確認しやすい
  • Werckerは圧倒的にビルドが早い

publicなリポジトリでの運用という観点で、秘匿情報(署名ファイルなど)の取扱、テストレポートの確認手段、ビルド時間の長さ、導入難易度について私の主観でまとめてみるとこんな感じです。

CIサービス秘匿情報テストレポートビルド時間導入難易度
TravisCI××
CircleCI××
Wercker×
秘匿情報については後述。

テストレポートは、./gradlew connectedAndroidTestなどを実行した後に生成されるHTML形式のレポートを確認できるかということです。CircleCIはCIサービス上でHTMLファイルにアクセスすることが可能です。Werckerは直接見れませんがファイルをダウンロードできます。Travisはそもそも見れません。どこか外部のストレージサービスでも使って、ファイルをアップロードするしかないようです。

ビルド時間はWerckerの圧勝です。TravisCIもCircleCIもSDKのアップデートに時間を食われるため、一度のビルドに18分くらいかかります。Werckerビルド環境をDockerで構築してしまえるので、SDKのアップデートが発生しません。初回を除けばだいたい6分くらいで済んでいます。圧倒的な早さです。

導入難易度については、TravisCIとCircleCIはどちらもあまり大差はないと思います。ドキュメントも日本語情報も充実しています。設定もYAMLで記述するだけですから、CIサービスごとの方言はあるもののそうハマるものでもありません(CircleCIは微妙にドキュメントが現状に追いついていない部分があって惑わされたりもしましたけど)。

一方でWerckerはドキュメントが他2つと比較して充実しているわけではありません(他2つと比較すると分かりにくいと感じました)。さらにDockerの知識が必要にもなるので、導入までにかかった時間は一番長かったです。(逆にDockerの知識があって、Androidの環境を構築するのが簡単にできてしまう人であれば、Wercker使うのが一番ラクかもしれません)

3つのサービスを使ってみましたが、それぞれ一長一短で、このサービスが最強といえないもどかしさがありました。ただprivateリポジトリで使うなら、Werckerが一番いい気がします(早さと設定の自由度が魅力)。

秘匿情報の取扱

困ったのは、署名ファイル(release.keystore)をCIでどう扱うのかという問題です。

どのCIサービスでも同じですが、release.keystoreなどの秘匿情報をCIでも扱えるようにするためには、そのファイルをリポジトリに含めるか、もしくはインターネット経由でアクセスできるどこかに別途公開しておくかしかありません。

前者の方法はpublicなリポジトリでは使えません。privateリポジトリであれば問題ないのでしょうが、私のケースでは採用できませんでした。

かといって後者のどこか別の場所に置くという方法も、適切な場所が思いつきませんでした。誰でもアクセスできるような場所に置くことはできませんし、適切なアクセス制限がかけられる置き場所となると、選択肢はそう多くはないと思います。

どのCIサービスでも、privateな情報についてはCI側で環境変数を利用することができます。publicなリポジトリで運用しても、パスワードなどが見えないように配慮することができます。TravisとWerckerは環境変数にセキュアな項目にする設定があるので、その点いくらか安心です。ただしCircleCIは、publicなプロジェクトではパスワードとか漏れたら困る情報を環境変数に入れるなと注意書きがありました。

しかし環境変数で扱えるのは文字列です。署名に使うrelease.keystoreというバイナリファイルを環境変数で扱うことはできません。

私が試した3つのサービスで、唯一秘匿情報ファイルの取扱がCIサービス単体で解決できるのはTravisだけです。解決できると言っても、暗号化してリポジトリに含めるという方法ですけれど。

他のサービスでは別途ストレージサービスを利用するなどして、そこからファイルをとってくるという手法を使わなければなりません。

エミュレータを使うテストは鬼門

今回CIを導入したプロジェクトでは、Espressoを使ったテストを行っています。手元の実機では安定していても、CIで実行すると失敗ばかりで非常に困りました。

Espressoテストレコーダーは万能ではない

ローカルの実機だと問題ないのにCI上だとエラーが起こりました。例えばこんなコード。

-        ViewInteraction appCompatTextView = onView(
-                allOf(withId(android.R.id.text1), withText("test.com/"),
-                      childAtPosition(
-                              allOf(withClassName(
-                                      is("com.android.internal.app.AlertController$RecycleListView")),
-                                    withParent(withClassName(is("android.widget.FrameLayout")))),
-                              0),
-                      isDisplayed()));
-        appCompatTextView.perform(click());
+        onData(anything())
+                .atPosition(0)
+                .perform(click());

onData~にコードを書き換えるとCI上でも問題なく動くようになりました。Espressoテストレコーダーは便利ですが、こればかりに頼る訳にはいかないという教訓です。

Read full post gblog_arrow_right

ActivityScopeを使ってActivityクラスごとにシングルトンになるようにした話

まずはじめに。Dagger2の話をしていますが、きちんと理解しているわけではないので間違った内容があるかもしれません。鵜呑みにしないでください。

この記事で言ってることのサンプルコードはGutHubで公開しています。

この記事の要旨は「MainActivityの中でシングルトンを実現したい(した)」ということです。

Dagger2を知ったキッカケ

私がDagger2を知ったきっかけはdroidkaigi2016です。その時からずっと腑に落ちなかったのがActivityScopeの存在です。ActivityScopeがわからないというか、子コンポーネントをわざわざ作る意義がわからなかったのです。

droidkaigi2016のコードを見よう見まねでDagger2を使ったアプリを作ったのですが、そのアプリではほぼすべての依存性をAppModuleに定義してありました。そのためAppModuleだけがやたらと肥大化し、ActivityModuleには何も定義されていないような状態で、ActivityScopeを作っている意味がまったくありませんでした。

結果そのアプリでは、Dagger2を依存性の充足のために用いるのではなく、シングルトンパターンを使うことなくアプリ内でインスタンスが1つになるようにするための道具として使っている状態でした。

ActivityScopeでやりたかったこと

私は、例えば端末の画面が回転しても、同じMainActivityであれば常に同じコントローラなりPresenterなりがセットされるようにしたいと思っていました。そうすれば非同期処理を引き継ぐためにアレコレする煩雑さから解放されます。

それを実現するために子コンポーネントを区切ってActivityScopeを作ってるんだろうと考えていたのですが、実際の挙動はそうはなりません。ActivityModuleで@ActivityScopeなんて指定したところで、画面回転したら注入されるのは異なるインスタンスです。

(これはActivityComponentをActivityのonCreateで初期化して、Activityがそのインスタンスを保持していることに原因がありましたが、詳細は後述)

そもそもActivityのライフサイクルは非常に短命で、初心者がまず躓くポイントとして挙げられるほどに感覚値とずれたものです。画面回転しただけでインスタンスが変わる。同じMainActivityなのに。同じMainActivityが表示されてるのに、実は内部では異なるインスタンスのものなんですというのがややこしいポイントです。

私はずっとActivityScopeを使えば、同じMainActivityなら常に同じインスタンスを注入できるようになるんじゃないかなと思っていました。でもそれができない。それが私の「Dagger2よく分からん」の原因の1つでした。

Dagger2がインスタンスをシングルトンのように扱うことが出来る仕組み

そもそもDagger2でインスタンスを使いまわせるのは、スコープをアノテーションで指定しているからではありません。ApplicationModuleで@Singletonを指定したインスタンスが常に同一であるのは、アプリ内で同じApplicationComponentを参照しているからできていることです。

ApplicationComponentはApplicationクラスを拡張した独自のクラスに保持して、そこにアクセスしていると思います。例えばこれを、(普通そんなことはしませんが)ActivityのonCreateでApplicationComponentを生成するようにしたらどうなるでしょう。画面回転によるActivityの再生成が起こる度に@Singletonとしたインスタンスであろうが毎回異なるインスタンスが注入されることになります。つまり@Singletonをつけてるからシングルトンになるわけではないということです。

ActivityのonCreateでApplicationComponentを生成した場合、同じActivityのインスタンス内ではシングルトンにできます。例えばそのActivityでViewPagerを使っていて、Fragmentを複数内包しているとしましょう。そのFragmentたちはActivityのもつApplicationComponent(ややこしい)にアクセスすることで、@Singletonで指定したインスタンスを使いまわすことが出来ます。

ApplicationクラスでActivityComponentのインスタンスを保持

同じActivityクラスでActivityComponentを使いまわせるようにするためには、Activityより長いライフサイクルを持つものにComponentのインスタンスを保持してもらう他ありません。

私はとりあえずApplicationクラスにActivityクラス名をキーとしたHashMapを持たせて管理させるようにしました。CustomApplication.classのソースコード

こうすることで、例えばFilterEditActivity.classでは、画面回転でActivityのインスタンスが変わろうと、常に同じActivityComponentを取得でき、FilterEditAcitivy内で常に同じPresenterが使えるようになります。

デメリット

ほんとうの意味でのActivityのライフサイクルと異なるわけなので、逆にわかりづらくなっている気がしないでもありません。ActivityScopeといいながらその生存期間はApplicationと同じになってしまっています。

Fragmentを使う場合に更に混乱します。実際にサンプルのプロジェクトでは、Activity+ViewPager+Fragmentを使う部分でややこしいことになっています。

Activityの場合はActivityクラスで識別すればいいのですが、ViewPager内のFragmentはクラス名で識別することが出来ません(同じクラスでも中身が異なるため)。

またActivityContextを注入したい場合に困ります。まず間違っても@ActivityScopeで定義してはいけません。それをやると一番最初に生成されたActivityのインスタンスが使いまわされることになってしまいます。ただスコープをつけなくても、Componentが参照しているActivityContextは最初に作成されたActivityのインスタンスとなってしまうので、スコープをつけないだけでも足りません。

このサンプルプロジェクトでは、苦肉の策としてApplicationクラスにActivityModuleのインスタンスも一緒に管理させるようにしています。ActivityModuleがもつContext(Activity)を更新するためです。

しかしそうやったところで、ActivityScopeで使いまわしたい何らかのインスタンスに、ActivityContextを持たせなければならない場合はどうしようもありません。サンプルプロジェクトでは幸いActivityContextに依存するものがないのでなんとかなっていますが、将来的には不明です。

変な依存を産んでしまっている気がしないでもない

依存性をなくすためのDagger2で、逆に変な依存を産んでしまっているような気がしないでもありません。

Read full post gblog_arrow_right

はてなブックマークのホッテントリリーダーを作った

勉強がてらホッテントリリーダーを作ってみました。ソースコードはGitHubで公開しています。

アプリも公開中です。

Get it on Google Play

自分の勉強のためというのが目的のアプリです。最初はDagger2に慣れるために適当に遊んでいたのですが(その名残が微妙に残っている)、それをちゃんとしたアプリに落とし込んだときに使いこなせるのかという不安がありました。そこでアプリとして動くものを作ろうと考え、じゃあいっそいろいろなライブラリを使いながら勉強しようと、このような形になりました。

とりあえずアプリとして動くところまではできたので、Google Playで公開してみました。アプリ名をもうちょっとひねろうかと思ったのですが、思いつかなかったのでそのままな名前をしております。

公開している部分にはまだ含まれていませんが、Dagger2でモジュールを差し替えて通信をモックしたり、テストコードを加えたりといい勉強になっています。そのあたりもそのうち公開できたらなと思っています。

Dagger以外にもRetrofitをはじめて使ってみたり、いい勉強になっている気がします。

テスト周りとかCIの勉強も出来たらなぁと考えています。

Realmのテストのやり方を知りたい

Realmを使ってみました。ちなみに私は、今まではGreenDAOとAndroid Ormaしか使ったことがありません。

とりあえずCRUD操作のやり方をつかもうとテストを書いてみました。テストの書き方が根本的に間違っている可能性が無きにしもあらずですが、こんな感じで作りました。

public class FilterDataSourceRealmTest {
    private static RealmConfiguration    config;
    private static FilterDataSourceRealm sut;

    @BeforeClass
    public static void initializeTest() {
        config = new RealmConfiguration.Builder()
                .name("test_realm")
                .deleteRealmIfMigrationNeeded()
                .build();
        sut = new FilterDataSourceRealm(config);
    }

    @Before
    public void setUp() {
        Realm.deleteRealm(config);
    }

    @After
    public void tearDown() {
        Realm.deleteRealm(config);
    }

    @Test
    public void insertFilter() throws Exception {
        final CountDownLatch latch = new CountDownLatch(1);
        sut.insertFilter("test.com/");
        sut.getFilter("test.com/")
           .subscribe(new Action1<UriFilter>() {
               @Override
               public void call(UriFilter uriFilter) {
                   assertThat(uriFilter.getFilter(), is("test.com/"));
                   latch.countDown();
               }
           });
        latch.await(2, TimeUnit.SECONDS);
    }
}

テスト対象のコード(一部抜粋)はこんな感じです。

public class FilterDataSourceRealm implements FilterDataSource {
    private RealmConfiguration config;

    public FilterDataSourceRealm(RealmConfiguration config) {
        this.config = config;
    }

    @Override
    public void insertFilter(String insert) {
        Realm realm = Realm.getInstance(config);
        realm.beginTransaction();
        realm.copyToRealmOrUpdate(new UriFilter(insert));
        realm.commitTransaction();
        realm.close();
    }

このテストコードはandroidTestに配置して実機で実行します(Instrumentation Test)。

Read full post gblog_arrow_right

DataBindingを使っていてexecutePendingBindingsを呼び出さないとどうなるか

私はfindViewByIdをしなくていいからという理由でDataBindingを使っています。利用するためにbuild.gradleに

dataBinding {
    enabled = true
}

とするだけでいいのも気に入っています。

今回RecyclerViewのViewHolderにDataBindingを適用したときに、executePendingBindings()を呼び出さないことによる弊害がわかったのでご紹介します。

https://developer.android.com/topic/libraries/data-binding/index.html#advanced_binding

ドキュメントにはholder.getBinding().executePendingBindings();と、executePendingBindings()を呼び出すように書いてあります。私はDataBindingを使っていて、このようなメソッドを呼び出したことがなかったので、「なんでいるんだろう?」と疑問に思いました。オブジェクトのバインドはスケジュールされるだけですぐに行われるわけではないと書いてますけど、これまで使わずとも特に問題を感じなかったから、別になくてもいいのではと思ったのです。

私はこんな感じで使ってました。(Adapterのコードの一部ですが)

    @Override
    public void onBindViewHolder(DataBindingViewHolder<ItemHatebuFeedBinding> holder,
                                 int position) {
        ItemHatebuFeedBinding binding = holder.getBinding();
        final HatebuFeedItem  item    = items.get(position);
        binding.setItem(item);
    }

レイアウトファイルはこんな感じです。

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    >

    <data>
        <variable
            name="item"
            type="jp.gcreate.sample.daggersandbox.model.HatebuFeedItem"
            />
    </data>

    <LinearLayout
        style="@style/RecyclerViewContainer.Clickable"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingBottom="@dimen/item_padding_with_item"
            >

            <TextView
                android:id="@+id/count"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="@dimen/item_padding_with_item"
                android:text="@{String.valueOf(item.count)}"
                android:textAppearance="@style/TextAppearance.AppCompat.Title"
                android:textColor="@color/red_600"
                />

            <TextView
                android:id="@+id/title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@{item.title}"
                android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
                android:layout_gravity="fill_horizontal"
                />


        </LinearLayout>

        <TextView
            android:id="@+id/description"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{item.description}"
            android:paddingBottom="@dimen/item_padding_with_item"
            />

        <TextView
            android:id="@+id/date"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{item.date}"
            android:textAppearance="@style/TextAppearance.AppCompat.Caption"
            />

    </LinearLayout>
</layout>

executePendingBindings()を呼び出さなくても普通に動作します。下に向かってスクロールする分には何も変なことはありません。しかし、下から上に向かってスクロールすると、時折妙な動き方をします。時折ブレるような挙動をするのです。(ちなみに動画を撮って用意したのですが、ファイルサイズが大きいので貼るのは止めました)

この動きはexecutePendingBindings()を呼び出していると起こりません。なるほど、executePendingBindings()を呼び出さないとこのようなことになるわけですね。

微妙にブレるように感じたのは、RecyclerViewをスクロールして次のViewが要求される→onBindViewHolderが呼び出され、setItem()でオブジェクトをバインドする→Viewが見え始める→バインドしたオブジェクトが実際にViewに描画される→中身によってViewの高さが変わる→アイテムが見え始めてからViewの高さが変わり、表示中のアイテムが動いたようにみえる、という経過を辿っているのでしょう。

下に向かっていく分には、Viewの高さが変わっても伸びた部分は画面外にいくので、特に違和感を感じません。しかし、上に戻っていくときにはViewが見え始めてから高さが変わるため、下に伸びるとそれまで表示していた部分が下に押し出されて、自分がスクロールした分以上にスクロールしたように感じる。もしくは短くなった場合には、スクロールしたのが取り消されて上に引っ張られたかのように感じる。それが違和感の原因でした。

これは各アイテムのViewの高さが一定であれば生じない問題です(高さのズレが生じなくなるため)。この例ではwarp_contentを使っていて、かつ中身の長さがアイテムによって異なっていたために生じました。

これまで特にDataBindingによるタイミングのズレなど気にしたことがなかったのですが、RecyclerViewで使うときには気をつけないといけないんですねぇ。

Instrumentation Testで生成されるAPKは何をしているのだろう

以前、Viewの描画をテストするためのリポジトリを作りました。記事はこれです。

Viewが想定通り描画されているか確認するため、Spoonを使ってスクリーンショットを撮るようにしました。GitHubにあげたコードでは、TextViewの周りに枠線を描画するCustomViewを作成し、その枠線が描画されるかを確認するというものでした。

しかしSpoonで撮ったスクリーンショットでは、右側と下側に描画されるはずの線が表示されていません。実機で動かすと描画されているのですが、スクリーンショット上では見えない。

Spoonのバグなんじゃないかななんて最初は思っていたのですが、調べてみると原因は違うところにありました。いえ、Spoonのせいではないということはわかったのですが、じゃあなぜそうなるのかというところが分からないので困っている状態です。

Spoonのスクリーンショットで線が描画されない理由は、右と下の線が画面外に描画されてしまっているからです。

CustomViewは右側・下側に描画する位置を、onDrawメソッドの引数で渡ってくるCanvasのサイズ(canvas.getWidth()canvas.getHeight())を使って描画しています。

実機で実行した場合、ここに渡ってくるCanvasのサイズは、CustomViewと同じサイズになっているようなので、TextViewの周りに枠線が描画されます。

一方で、androidTestで実行した場合、このcanvas.getWidth()で得られる数値は、想定したものよりはるかに大きい数値になります。数値の大きさから察するに、画面全体と同じ大きさになっているような気がします。

実機で実行した場合:

10-05 17:35:09.972 1992-1992/jp.gcreate.sample.viewdrawingtest.uiTest D/test: canvas:android.view.GLES20RecordingCanvas@30073153, height:96, width:983
10-05 17:35:15.412 1992-1992/jp.gcreate.sample.viewdrawingtest.uiTest D/test: canvas:android.view.GLES20RecordingCanvas@2a498faf, height:96, width:983

androidTestで実行した場合:

10-05 17:37:37.955 3888-3888/jp.gcreate.sample.viewdrawingtest.uiTest D/test: canvas:android.view.GLES20RecordingCanvas@375e49fb, height:1436, width:983
10-05 17:37:37.982 3888-3888/jp.gcreate.sample.viewdrawingtest.uiTest D/test: canvas:android.graphics.Canvas@1cad5ead, height:1919, width:1079

androidTestで実行すると、渡ってくるCanvasが実機の場合と異なるようです。

onDrawメソッドで渡されるCanvasとは一体何なのかという点についても、私はよく分かっていないのですが、androidTestで実行されるtest用のAPK(この場合app-UiTest-debug-androidTest.apk)が何をやっているのかもよく分からなくなってきました。

androidTestを実行すると、実機上に画面が表示され、テストコードに書いた動きが実行されていくので、それは全てtest用のAPKで実行されているのだとばかり思っていました。しかしそう考えると、実機で表示されている画面では枠線が描画されているのに、Spoonで撮影したスクリーンショットには映っていないことの理由が説明できません。

そんなことを考えていると、Instrumentation Testとは一体何なのかがよく分からなくなってきました。

Android Studio2.2でProject Windowのパッケージ名表示が省略されない

タイトルが分かりにくいんですが、こちらの画像をご覧ください。

パッケージ名が省略されない

画像の例ではアプリのパッケージ名がjp.gcrete.sample.daggersandboxで、そこからさらにapiとかdiとかのパッケージに分化してます。

Android Studio2.2にしてから、なぜかそのサブパッケージの部分が単にapiではなく、jp.gcreate.sample.daggersandbox.apiと省略されずに表示されていました。

Layout EditorのようにAndroid Studioの設定でそうなっているのかとも思いましたが、該当するような設定項目はありませんでした。

なんでだろうなと思って探してみたところ、issueが立ってました。

https://code.google.com/p/android/issues/detail?id=223389

https://code.google.com/p/android/issues/detail?id=222914

どうもDataBindingを有効にすると発生するそうです。実際、この画像のプロジェクトでもDataBindingを使っており、これをfalseに変更すると普段通りの表示になりました。

DataBindingTrue

DataBindingFalse

DataBindingを使っている人のみ影響を受けるみたいです。

最近はfindViewByIdを使わなくて済むからくらいの軽い理由で、DataBindingを多用しているので早く直ってほしいです。

まあProjectウィンドウが見づらくてなんか気持ち悪いってだけなんですけどね。

Android Studio 2.2.1で直ったみたいです。

Android Studio2.2でレイアウトエディタを開いた際にTextで開くようにする

layout.xmlを開いた際に、デフォルトではDesignタブで開かれると思います。これをTextに変更する方法です。

Android Studio 2.1まではレイアウトのPreview画面に歯車アイコンがあって、Prefer XML EditorにチェックをつければOKでしたが、Android Studio 2.2のPreview画面にはそのようなものが見当たりません。

Android Studio 2.2からは、設定画面から変更するようです。

レイアウトエディタをTextで開く

Preference > Editor > Layout Editorで設定できます。