パルカワ2

最近はFlutterをやっています

minneをAndroid Studio 3.0に対応させた

hisaichi5518.hatenablog.jp

↑の続きです。
Android Studio Release Notes | Android Studio
Android Developers Blog: Android Studio 3.0

com.android.tools.build:gradle:3.0.0 を使う

まあ、そうですよね

assertThat(images).isNotNull が使えなくなったのでやめた

minneではテストでKotlinとassertjを利用しているんですが、 .isNotNull()ではなく .isNotNullと書けてた。一応.isNotNull()を利用するようにしましょうという感じだったんだけど、残念なことにいくつか漏れがあってAndroid Studio 3.0にしたタイミングで.isNotNullコンパイルができなくなったので、修正した。

testCompile "org.robolectric:httpclient:3.4.2"

まあまあ昔からあるアプリなので、 org.apache.http.NameValuePairに依存してたりする。今まではエラーになってなかったけど、Android Studio 3.0にしたタイミングでテストでClassNotFoundExceptionを吐くようになったので、robolectricのhttpclientを追加した。

このあたりは、消したい気持ちをこらえる必要がありました。

Annotation Processorがオフになってるよと言ってきたのでONにした

Android Studioが警告を出してきたのでポチポチした。
前も出てたけど、クリックしても設定画面に飛ばなかったんですが、今回は設定画面に飛んで設定出来るようになりました。

まとめ

というわけで、minneを運営するGMOペパボは本日めでたくリリースされたAndroid Studio 3.0 stable対応企業です。よろしくお願いいたします。
【minne】Androidアプリエンジニア / GMOペパボ株式会社

ActivityOptionsCompat.makeSceneTransitionAnimation(...) を使う

Activity間で共通の画像があって、それをシューンと移動させるのがマテリアルしてんじゃん!という感じなのでやってみた。
試したのは、エミュレータAndroid 7.0です。全部のコードはここにある。

github.com

f:id:hisaichi5518:20170927234954g:plain

このような感じに動くシンプルなやつ

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        GlideApp.with(this)
                .load(R.drawable.sample)
                .into(binding.view1);

        RxView.clicks(binding.view1).subscribe(__ -> {
            ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
                    this, binding.view1, "image");

            ContextCompat.startActivity(this, new NextActivity.IntentBuilder(this).build(), options.toBundle());
        });
    }
}

遷移先のActivity

public class NextActivity extends AppCompatActivity {

    private ActivityNextBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_next);

        GlideApp.with(this)
                .load(R.drawable.sample)
                .into(binding.view1);

        RxView.clicks(binding.view1).subscribe(__ -> {
            ActivityCompat.finishAfterTransition(this);
        });
    }


    static class IntentBuilder {
        private final Context context;

        IntentBuilder(Context context) {
            this.context = context;
        }

        Intent build() {
            return new Intent(context, NextActivity.class);
        }
    }
}

あとは遷移先のImageViewにandroid:transitionName="image"を指定してあげると動く。
案外簡単だった。windowに値をセットして〜〜〜とか説明してる記事がいくつかあったけど、特にいらなかった。試したのがAndroid 7.0だからかもしれない。またtransitionNameはstrings.xmlとかに書くのが本当はいいと思う。

時々変に移動する時がある。これはよくわからなかった。

テストでAnnotation Processorを利用する

testAnnotationProcessor "..."

と書くと実行されてクラスは作られてテストも想定通り動くが、Android Studio上ではクラスが参照できずにエラーになる。
https://issuetracker.google.com/issues/37121918

関連したgistに回答があった。
gist.github.com

android {
  defaultConfig {
      android.libraryVariants.all {
        def aptOutputDir = new File(buildDir, "generated/source/apt/${it.unitTestVariant.dirName}")
        it.unitTestVariant.addJavaSourceFoldersToModel(aptOutputDir)
      }
  }
}

Annotation Processor のテスト

Annotation Processor のテスト書きたいなと思って書いてた。

[java] モダンな Annocation Processor の開発手順まとめ - tokuhirom's blog

package com.github.hisaichi5518.konohana.processor;

import com.google.common.truth.Truth;
import com.google.testing.compile.JavaFileObjects;
import com.google.testing.compile.JavaSourceSubjectFactory;

import org.junit.Test;

public class KonohanaProcessorTest {
    @Test
    public void test() {
        Truth.assert_().about(JavaSourceSubjectFactory.javaSource())
                .that(JavaFileObjects.forResource("Success.java"))
                .processedWith(new KonohanaProcessor())
                .compilesWithoutError();
    }
}

コンパイルが通ったらテストが通るというのをやろうとしたら通らない!!!なぜ!!javapoetのTypeName.get() できてない!

java.lang.RuntimeException: java.lang.IllegalArgumentException: part '<any>' is keyword

	at com.sun.tools.javac.main.Main.compile(Main.java:553)
	at com.sun.tools.javac.api.JavacTaskImpl.doCall(JavacTaskImpl.java:129)
	at com.sun.tools.javac.api.JavacTaskImpl.call(JavacTaskImpl.java:138)
	at com.google.testing.compile.Compiler.compile(Compiler.java:146)
	at com.google.testing.compile.JavaSourcesSubject$CompilationClause.compilation(JavaSourcesSubject.java:293)
	at com.google.testing.compile.JavaSourcesSubject$CompilationClause.compilesWithoutError(JavaSourcesSubject.java:267)
	at com.github.hisaichi5518.konohana.processor.KonohanaProcessorTest.test(KonohanaProcessorTest.java:15)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:117)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:262)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:84)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Caused by: java.lang.IllegalArgumentException: part '<any>' is keyword
	at com.squareup.javapoet.Util.checkArgument(Util.java:64)
	at com.squareup.javapoet.ClassName.<init>(ClassName.java:49)
	at com.squareup.javapoet.ClassName.<init>(ClassName.java:43)
	at com.squareup.javapoet.ClassName.get(ClassName.java:218)
	at com.squareup.javapoet.TypeName$1.visitDeclared(TypeName.java:268)
	at com.squareup.javapoet.TypeName$1.visitError(TypeName.java:290)
	at com.squareup.javapoet.TypeName$1.visitError(TypeName.java:243)
	at com.sun.tools.javac.code.Type$ErrorType.accept(Type.java:1855)
	at com.squareup.javapoet.TypeName.get(TypeName.java:243)
	at com.squareup.javapoet.TypeName.get(TypeName.java:238)
	at com.github.hisaichi5518.konohana.processor.definition.KeyDefinition.<init>(KeyDefinition.java:35)
	at com.github.hisaichi5518.konohana.processor.definition.StoreDefinition.lambda$keyDefinitionStream$3(StoreDefinition.java:88)
	at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
	at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
	at java.util.Iterator.forEachRemaining(Iterator.java:116)
	at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
	at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
	at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
	at com.github.hisaichi5518.konohana.processor.builder.StoreMethods.build(StoreMethods.java:23)
	at com.github.hisaichi5518.konohana.processor.writer.StoreWriter.buildTypeSpec(StoreWriter.java:41)
	at com.github.hisaichi5518.konohana.processor.writer.StoreWriter.write(StoreWriter.java:73)
	at com.github.hisaichi5518.konohana.processor.writer.StoreWriter.write(StoreWriter.java:30)
	at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
	at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
	at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
	at java.util.Iterator.forEachRemaining(Iterator.java:116)
	at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
	at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
	at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
	at com.github.hisaichi5518.konohana.processor.KonohanaProcessor.process(KonohanaProcessor.java:31)
	at com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor(JavacProcessingEnvironment.java:794)
	at com.sun.tools.javac.processing.JavacProcessingEnvironment.discoverAndRunProcs(JavacProcessingEnvironment.java:705)
	at com.sun.tools.javac.processing.JavacProcessingEnvironment.access$1800(JavacProcessingEnvironment.java:91)
	at com.sun.tools.javac.processing.JavacProcessingEnvironment$Round.run(JavacProcessingEnvironment.java:1035)
	at com.sun.tools.javac.processing.JavacProcessingEnvironment.doProcessing(JavacProcessingEnvironment.java:1176)
	at com.sun.tools.javac.main.JavaCompiler.processAnnotations(JavaCompiler.java:1170)
	at com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:856)
	at com.sun.tools.javac.main.Main.compile(Main.java:523)
	... 33 more

コンパイル対象のファイルを見てたら、java.util.Listがimportされてなかった…マジか………。追加したら無事なおった。


追記:
gfxさんにテストの内容について教えてもらった
正常系はlibrary側でやるのがよさそう


Annotation processorを利用したAndroidライブラリをつくる

作る度に毎度似たようなことをしていてめんどくさい。しかも忘れていたりするのでテンプレート化したいんですが、Android Studioを更新するときにはテンプレートを消さないといけないとかあるらしくてそれはそれでだるいので、とりあえず将来の自分のためにメモとして残しておく。

ここでは、Konohanaという名前のライブラリを作ろうとしています。 名前の由来は、此花区です。かわいい。

全部のログはここにあります。

github.com

New Project

まずは、サンプルアプリを作ります。

File -> New Project を選択

  • Application NameにKonohanaExampleをつける
  • package nameを com.github.hisaichi5518.konohana.exampleに変更する
  • Project Location を Konohana にする

そのあとは自分がmin sdkなどを指定してFinishを押す。 ビルド出来るようになるので、とりあえずビルドして安心感を得る。

f:id:hisaichi5518:20170919172225p:plain

Git

プロジェクト生成が終われば、Gitをアレしてソレする。

  • git init
  • curl https://www.gitignore.io/api/vim%2Candroid%2Candroidstudio > .gitignore
  • git add .
  • git commit -m "first commit"

gitignoreを自分で頑張るのは不毛なので gitignore.io に任せる。僕が利用するVim, Android, Android Studio を指定してある。 Githubリポジトリ作るとかは良きタイミングで。

app -> example

  • app を右クリックして、Refactor -> Rename を選択
  • example にする
  • git commit

Android Libraryを追加する

File -> New Module -> Android Library を選択

  • library name は Konohana
  • module name を library
  • package nameは com.github.hisaichi5518.konohana
  • min sdkはよしなにする
  • git commit

f:id:hisaichi5518:20170919172257p:plain

annotation library (Java Library)を追加する

Android libraryの中にannotationを入れてもいいような気がしますが、後述するProcessor library(Java library)がAndroid libraryに依存するのが許せなかったので分けます。

File -> New Module -> Java Library を選択

  • Library name は annotation
  • package name は com.github.hisaichi5518.konohana.annotation
  • class name は つくる予定のアノテーション名をいれます。 今回の場合は Key
  • git commit

annotations としているライブラリがいくつかありましたが、他は単数形でこれだけ複数形にするのが気になったのでやめました。なにか複数形にするべき理由があるのかもしれない。

f:id:hisaichi5518:20170919172313p:plain

processor library (Java Library) を追加する

File -> New Module -> Java Library を選択

  • Library nameは processor
  • package name は com.github.hisaichi5518.konohana.processor
  • class name は KonohanaProcessor
  • git commit

f:id:hisaichi5518:20170919172324p:plain

Java 8を利用する

processorのみJava8を指定する。

  • processor/build.gradle で JavaVersion.VERSION_1_8 を指定する
  • git commit

https://github.com/hisaichi5518/annotation-processor-example/commit/6ac6d78b59bda4befa5b6dde13e07f2d73dce236

追加したModuleを利用する

以下のように変更し、git commit する。

https://github.com/hisaichi5518/annotation-processor-example/commit/92ce2654b48da75d94f518ea21bd180e87c6a6b4

library/build.gradle

以下を追加する

  • compile project(':annotation')

example/build.gradle

以下を追加する

  • annotationProcessor project(':processor')
  • compile project(':library')

processor/build.gradle

以下を追加する

  • compile project(':annotation')

maven.google.com を利用する

Android Studio 3.0だと不要だと思う。 以下のコミットのように追加する。

https://github.com/hisaichi5518/annotation-processor-example/commit/bcfd9c8a9021e90ffa6b8dd86cfb16be27c10d4e

必要なライブラリを追加と削除

以下のコミットのように追加していく。

https://github.com/hisaichi5518/annotation-processor-example/commit/12d377943af8d35a68be1e52a8210083a0558960

また様々な箇所でバージョンを指定する必要があり、揃える必要があるライブラリ(例えばサポートライブラリ)は、バージョンを変数にいれる。

https://github.com/hisaichi5518/annotation-processor-example/commit/5b0ae810915aa3f4a36f803b0dfc498f5dd48b0d

ライブラリは、最新版を利用したほうがいいので、以下のURLを確認

最後に library/build.gradleにある appcompat-v7 は不要であることもあるので、不要なら消す。

Processorの準備

KonohanaProcessor を変更していく。

https://github.com/hisaichi5518/annotation-processor-example/commit/b59b76ce3989396ab71b29139cf08fcab6e5adc5

Annotationをつくる

自動生成されるのはclassなのでAnnotationにする。 あとexampleで利用するコード書いておく。

@Target(ElementType.FIELD)
public @interface Key {
}
public class MainActivity extends AppCompatActivity {

    @Key
    String name;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

Rebuild Projectを実行

Build -> Rebuild Project

おわり

なんかもっとスマートなやり方があったら知りたい。 github.com

ライブラリをつくったときの雑メモ

annotation processor 毎度忘れる。

  • ElementはVariableElementやTypeElementなどにcast出来たりするよ
  • ClassName.get(int.class)できない。
    • TypeName.INTなどがある
  • TypeNameを色々と渡すような実装にしたけど、auto/commonとかTypesUtils, ElementUtilsがあるのでElementを渡すようにしたほうがよかったかも???またはClassName???
  • google/auto/commonなど参考にするとよい
    • DeepLinkDispatchも使ってた
  • debugger でデバッグ出来るようにしておくとよい
  • ProcessingContext, Writer, Definition という役割にクラスを分けるとキレイぽい
    • Ormaがこんな感じだった
  • getAnnotaton().hoge() でClass型取ろうとしても取れない。今回つくったAnnotationExtendでやってるように取るとよい
  • processorではJava8つかうとよい streamが使えてシュッ
  • VariableElement#getConstantValue() は primitive型かStringの値しかとれない それら以外はnullになる
  • TypeElementとかはドキュメントよむ
  • 毎度読んでる気がする Aptを使ったAndroidライブラリの作り方 - Qiita
  • http://qiita.com/opengl-8080/items/beda51fe4f23750c33e9
  • JavaPoet 使い方メモ - Qiita
  • Observable<String>を作りたい場合はこうする ParameterizedTypeName.get(RxJavaTypes.Observable, JavaTypes.String)
  • ClassNameなどをRxJavaTypesやJavaTypesなどに定義しておくと使いまわせてまとめられて便利

新たなライブラリを導入するときに考える事

社内向けに書いたけど転載。

はじめに

開発をしていると判断していくことが数多くあります。世界中で公開されているライブラリの中から1つを選び、導入を決めるのもその1つです。

判断とはとても難しいもので、ほぼ確実に間違いは起きます。例えば、「実行速度が遅いが高速な開発には必要であろう」と判断して導入したが、実際運用してみるとやっぱり遅くてやいのやいの言われるとか。入れてみたいからという理由でライブラリを入れる判断をして、ある場所では使われてるけど、ある場所では使われておらずゴチャゴチャになってるとか。似たようなライブラリが3,4個導入されていて、どれが一番使うべきなのか判断出来ないとか。働き始めて5,6年の僕が失敗したな〜と思うだけでも色々あるわけです。

そこで僕が仮にポックリ逝ってしまったとしても、僕の失敗をみなさんが活かせていい感じに開発出来ると僕も生きた意味あると思うので、新たなライブラリを導入するときに考えていることを共有します。

⚠️ 注意 ⚠️

あくまでひさいちが考えてる事なので、書いてる事以外に重要なこともあるでしょうし、より良い方法考え続ける事でバンバン良くしていきましょう。(これだけ抑えておけば絶対破滅しないというわけではない)

ライブラリについて考える

ライブラリを導入するからにはライブラリについて考える。

メリット・デメリット

ライブラリのメリットとデメリットは何か?必ず複数個挙げる。 気をつける点として、メリットで「わかりやすくなる」をあげる場合があるが、これはあくまで個人の感想で事実ではない。メリットとデメリットには事実のみ書くべき。

理科系の作文技術 (中公新書 (624))

理科系の作文技術 (中公新書 (624))

ライブラリのサイズ

モバイルアプリはインストールする必要があるので、アプリサイズをなるべく小さくしたい気持ちがある。なので、ライブラリのサイズがあまりに大きいものは採用しない。

ライブラリ実行速度

多少気にする。必要があれば計測する。

ライブラリは課題をどう解決するのか

  • 画像表示ライブリではキャッシュやBitmap Poolやらスレッド数変更やらなんやらをどう実現出来るのか調べる
  • ORMだとSQLを書く必要がない等をどう実現するのか調べる

自分やチームは今どういう課題を抱えていてを考え、ライブラリはどう解決するのか調べる。

似たライブラリの比較する

似たライブラリといってもどう解決するのか?の部分は違っていたりするので、違いを理解する。

実行時エラーではなくコンパイルエラーになるか

具体的な例を出すとOrmaはSQLをつくるときにテーブル名を文字列で渡す必要がない。文字列でテーブル名を渡す場合、テーブル名がtypoしていたら実行時にエラーとなるが、Ormaはメソッド名なのでコンパイルエラーになる。

コードや仕様がシンプルであるか

  • 仕様が複雑すぎるとあとから見ると意味不明になる
  • コードが複雑すぎると読むのに時間がかかる

自分の場合、仕様が複雑だと理解すると便利!!って大体なるけど、数日後には忘れてる。またライブラリのコードは結局読むので、読みやすい・シンプルなものを選ぶのが後々楽な事が多い

メンテンスされてるか

数年間リリースされてないとかだと、ちょっと不安ですね。

いざとなったら自分が直せるか

無理そうなら複数人でメンテナンスされてるものを選ぶ

枯れているのか

それなりに他社でも事例があると安心ですね

導入後を考える

残念なことに導入して終わりではないので、先を見越して考えておく。ここが雑だとライブラリが優れていても途中で失敗したりする。

チームへどう広げるか

広げる事について考えている時にメリットやデメリットを話せないと説得力が無なので広がらない。だから、そのあたりは先に考えておく。

Fearless Change アジャイルに効く アイデアを組織に広めるための48のパターンは大げさだけど、活かせることがあるかもしれません。

Fearless Change アジャイルに効く アイデアを組織に広めるための48のパターン

Fearless Change アジャイルに効く アイデアを組織に広めるための48のパターン

どこから適用して、どこまで適用するのか

すでにあるプロダクトに新たなライブラリを導入する場合は考えます。 どこから始めて、どこで終わりとするのか見えてないと導入後いつの間にか負債となってしまう…

またこれを決める時にどういう実装になるのかを軽く実装して確認する。 無理なく、全員で進められるというのを大事にしている。

所感

こうして改めて文章にしてみるとライブラリについて考えるだけではなく、チームにどう広げるか等も考えている事に気づいた。そう思うとペパボの評価軸である「先を見通す力」「影響を広げる力」が主に必要で、なんならライブラリを導入のために何か作る必要があるのなら「作り上げる力」が必要になってくるのだなと思いました(Retrofit2導入時には、テスト用のクラスを作ったりしました)

追記:当たり前すぎて忘れてたけど、ライセンスも見てた。