続続・Androidアプリ開発を学生に教えた話
今までと同じく、TRUNKから講師の依頼があったので講師をしてきました。
過去の話はこちらです
進め方
週一で、一回あたり2~3時間の全8回のコースで、全体を通すとおよそ2ヶ月のコースとなり、今までで最長のものでした。
周りと圧倒的に差をつけるスキルアップの1ヶ月 - TRUNK CAMP | TRUNK
「kotlinでアプリ開発をしてストアにアプリを公開する」というのが今回のゴールでした。
以前より回数があるので、各自でアプリを作る形にして進めていこうと考えました。具体的には、何かしらのWeb APIと連携してそれをリスト表示するものを考えていました。
じゃんけんアプリなども考えたのですが、皆が同じアプリを作ってストアに公開してもストアからしたらノイズになってしまうかなと考えて、別のアプリになるように前述のゴールにしました。
各週でやる内容は次のようにし、各週で課題を出すことで進めていこうと考えました。連絡にはFBグループを作成し、困ったりしたら個別にメッセージを送るのではなく、グループに投稿してもらうようにしました。
あなたが困ってることは他の誰かも困ってることかもしれないので、知識を共有する形にしたかったからです。
カリキュラム
- Kotlinの文法
- 作りたいアプリを考えてくる
- 作りたいアプリの設計・Android Studioの使い方
- ソースコードを管理できる状況にしておく
- データクラスを作る・レイアウトを作成する
- レイアウトを実現させる
- リスト表示する
- リスト表示できるまで
- APIをつなぐ
- APIからデータを受け取れるまで
- イベントを起こす・別画面を作る
- データを繋いで別画面に遷移させる
- Intent連携・アナリティクスを入れる
- シェア機能など実装したいものを各自実装
- 申請準備・ストアへ申請
Kotlinでアプリ開発ということで、Kotlinのことを初めに教えて、それからAndroidについて学んでいく構成で考えました。
しかし実際は次のようになりました。
- Kotlinの文法
- 作りたいアプリを考えてくる
- 作りたいアプリの設計・Android Studioの使い方
- ソースコードを管理できる状況にしておく
- プログラミングの基礎
- ソースコードを管理できる状況にしておく
- レイアウト作成・イベント発生
- アプリのレイアウトとイベント発生まで仕上げておく
- 別画面作成・Intent連携(1/2)
- 自分のアプリに足りない機能を開発していく
- 別画面作成・Intent連携(2/2)
- 自分のアプリに足りない機能を開発していく
- 申請準備
- 自分のアプリに足りない機能を開発していく
- オフィスアワー
さらに、7名いた参加者が最終的には3名になりました。 どうしてこうなったのか一週ずつ振り返ります。
1週目
Kotlinについては、Kotlin入門までの助走読本やKotlinスタートブックなど、体系的にまとめてある物があるので改めて自分の方で資料を用意して教えるのはやや不毛な気がしましたが、資料を作成しました。
資料はKotlinの特徴を攫う内容にして、書籍などでちゃんと学んで貰おうと考えました。そのため、わからないことがあっても大丈夫という事を強調して進めました。
プログラミングに触ったことのある方は半数程度いました。オンラインのサービスで学んだ方が多かったです。
実際に手を動かさないと理解しにくいと思ったので、IntelliJを入れてもらい、Kotlinを書きながら進めました。
ここでいくつか問題がありました。
IntelliJはそこそこ大きいので、事前にDLしておいてもらわないと待ち時間が発生します。Android Studioは皆DL済だったのですが、ここは事前共有できていなくて不手際でした。
待ち時間がもったいないのでJavaも設定出来ていなかった方たちには、try.kotlinで作業してもらうようにしました。
また、自分の資料が暗黙でプログラミング言語に触れたことがあること前提になってしまっていて、「Nullってなんですか?」と言われたときには、しまったー!!と思いました。
Kotlinについては先に詰め込もうと思って作った資料でしたが、ひとつずつ手を動かしていると、わからないことがあっても大丈夫とはいえ、あまりにも頭に入っていない感じが見受けられたのと、時間が迫っていたので、資料中、高階関数以降を割愛しました。
あまりに手応えが得られなかったので、終了後にTRUNKの方と相談し、コースを2つに分けることにしました。
写真加工アプリは、以前教えたことのある内容なので比較して簡単なのはわかっていました。
Web APIアプリを継続する方は7人中2人で、一人は当日欠席していた方でした。
残り5人のうち、一人は難易度の関係でコースを辞退しました。
資料はこちらです。
2周目
Android Studioの使い方を学ぶ週でした。
参加者は残り6名のうち、4名でした。来ない方の中には連絡が無い方もいました。
第1週のなかで紹介したKotlin助走読本を読んできたという方がいて、意欲的だこれはモチベーションが高いな!と感じました。
しかし、意図的にエラーを起こしてもらう箇所で、次週はプログラミングについて説明しないとヤバイと理解しました。
3週目
当日に3名辞退の連絡がありました。インターンやゼミの都合で時間が合わなくなったり、難易度が理由でした。
自分の教える順序や内容の問題もあったのですが、この週でプログラミングの基礎的な部分を補おうとしていたので、精神的にきつかったです。
残り3名になり、Web APIを利用するコースの方はいなくなり、全員写真加工アプリコースとなりました。
それなのに、写真加工に使用していたAdobe Creative SDKがサポート終了となり、メンテされなくなることが2週目の講義の翌日に発表され、散々でした。
また、Windowsユーザで前週と違うユーザアカウントでログインしていて、権限やpath周りの問題があるなど想定外なハマりどころがありました。
4週目
Web APIを利用するコースの参加者がいなくなったので、以前講義をした写真加工アプリの資料を変更する形で対応しました。
以前の学びから、必要になったときに必要なことを教えるようにスライドを作り直しました。
内容はレイアウト作成・イベントハンドリングでした。
GitHub上にコードを置いて連携させることを2週目から課題としていたのですが、4週目になっても出来ていない方がいました。
Android Studioを利用すればできるようだったので、こちらでGUIをキャプチャしてサポートしてあげられれば良かったなと思いました。
一方、それによって参加者同士で教え合う部分ができたのは良かったです。
5週目
Intent連携周りを教えました。
キリが良いところまでやろうとしたのですが詰め込みすぎました。実際は6週目の内容を含んでいたのですが、時間が遅くなってしまったので分けました。
また、先週の課題が終わってる前提の話だったので終わってない方の対応で30分くらい時間を使いました。
その間、終わってる方には並行して手を動かしてもらうとはいえ、待ってもらうことになったので良くなかったです。
やんわりと
「人を待たせることになるのでちゃんと課題としたことはやってきてほしいしわからなかったら質問してほしい」
という旨を伝えました。が、後の週で伝わってなかったことが自分に伝わりました。
複数人に対して教えるときは、理解が早い方に合わせるのか、理解が遅い方に合わせるのかどっちがいいんでしょうか?
今回の講義では、全編遅い方に合わせて進めました。
6週目
引き続きIntent連携周りを学ぶ週でした。
写真加工アプリを作ることになって、参加者に改めてどんなアプリにするか(どこにどのボタンを置いて機能を呼び出すか + α)を確認したところ、SDKをカスタムする必要のある機能を考えた方がいました。
それは出来ないから改めて考えてきてほしいとしていたのですが、改めて作るものをきちんと共有してもらえていなかったです。
この週で撮影機能を実装したのですが、その機能は入れないという方がいて、その方は他の方に教えている間は自習という形になりました。また、途中でその方から質問があったときに、作りたいアプリが見えていなくて、困る場面がありました。
実装方法だったり、やりたいことの実現方法は無数にあるので、何を作るのか?というのはきちんと把握していないとやりにくいです。
今回のアプリはAPI Levelを21からにしていたのですが、実機を持ってる方が4.3だったので、使えないという事がありました。その方はPCのスペックも足りておらず、エミュレータを使うのは現実的ではなかったため、アプリのAPI Levelを下げることで対応していました。
しかし、カメラでファイルを扱うので、その端末特有の問題が発生してしまいました。
実機があるから使いたいとはいえ、Androidの辛い部分に触れてしまう可能性が高まるので、事前に制限を共有することで今回のような事態はなるべく回避したいなと思いました。
SDKの利用登録部分で思ったより時間がかかってしまいました。Applicationクラスの作成や、Privateな情報の扱いなどもしたので、ちょっとやりすぎだったかなと思いました。
開発の間は、こちらで用意したPrivateキーを使うなどすれば簡単にできたのかなと思いました。
また、Windowsだけでハマる箇所やMultiDexの問題が見つかったりして時間がかかりました。
7週目
ストアに申請するためのアカウント作成やAPKの作成などの話をしました。
初めに、各自の詰まってる部分を共有してもらって、一人づつ画面に写しながらこれから何をしたらしたら良いのかを説明しました。
他の方の説明中に我関せずと小説を読む人(スライド待ち)もいれば指摘された部分を修正する方もいて、それぞれだなと思いました。
8週目
当初の予定と変わったのでオフィスアワーという時間の使い方をしました。
前回から何も変わっていなくて、何も取り組んでない方がいました。どうして何もしていなかったのかと聞くと、優先度が低かったからと言われて悲しくなりました。
しかしそんな学生も、写真加工ができると急にやる気が出てきたようで自分でこういうのをしたい!と取り組んでいました。正直初めからそれくらいのやる気で取り組んで、ググったりしてくれれば「わからない場所がわからない」という状態にはならなかったんじゃないかなと思い、残念でした。
結局写真加工アプリにしても、作りきって公開できる方はいませんでした。
反省
自分にとって一番反省したい点が
皆が同じアプリを作ってストアに公開してもストアからしたらノイズになってしまう
と考えてしまったところです。 初めてアプリ開発をする人にとっては、ストアにどんなアプリがアプリがあるかどうかということは関係なくて、自分がアプリを作ってストアに上げるという体験が大事です。同じアプリがどうとかより、自分が作ったものが皆が触れる所に上がるという喜びと興奮に気づくべきでした。
昔の自分もそうだったのに、初心を忘れていました。
目線がずれてしまったことが、今回のような結果になってしまった一番の原因だと思っています。
これで学生にアプリを教えたのは3回目だったのですが、わかったことがあります。
自分は、教えられる側のモチベーションを上げるのが苦手みたいです。熱意を持って取り組んでくれる前提で考えているようです。
また、熱意を持って取り組んでいてくれても、そもそも課題が難しくて最後までできていなかったです。
Facebookグループで質問して良いとしていたのですが、思ったより質問はされなかったです。グループで聞かずにteratailで質問してるのを見つけてしまったときは悲しかった。。(teratailだから悲しいという意味ではないです)
そもそもFacebookをそんな見ないようなので、今ならSlackとかDiscordでTeamを作成してしまうのが良い気がしました。作ったら見るということが保証されているわけではないですが。。FacebookよりSlackのほうが、時系列に表示されて、使っている人もちらほらいるのでマシかなと。
何度も教える機会があるならば、自分なりのAndroidのスタートブックを作成するか、教材を決めてしまってそれに即して教えていく形がいいかなと思いました。
まとめ
次の点が至らなかったので同じような機会がある方は参考にしてください。
Keep
- 臨機応変にコースの内容を変更できた。
- GitHubでの添削は、なんで動いていないのかが同じコードで確認できるので分かりやすかった。
- 講義の開始時に詰まっているところを共有してもらいながら皆で同じ画面を見て修正するのは良かった。
Problem & Try
- つまらなそうなアプリでも初めて作ったアプリなら嬉しい。
- つまらないとか決めつけない。
- プログラミング未経験の方がいる場合は言語に触れたことある前提で話さない。
- 参加者がプログラミング経験あるかどうかを事前に把握しておく。
- 参加者を経験者に絞るという方法も。
- 一定以上のPCのスペックが無いと現実的ではない。
- 3GB以上とあるが推奨通り8GBは欲しい。。
- IntelliJなど事前準備があるのに当日の連絡にしない。
- 募集ページに事前準備を記載する。
- 参加者と事前にやり取りできる場を用意する。
- 事前準備を記載できない場合はギリギリまで参加者の募集を行わない。
- Facebookグループは見る習慣や表示順序の関係でやり取りには適さない。
- 学生はFacebookを見ない。
- SlackやDiscordにすると良いかも?
- 既読が無いのでリアクション必須。
- 課題はしてこない。
- 課題を複数回持ち越させないために、終わっていない課題を一緒に終わらせる時間を講義後に設ける。
- 前日になって焦ってやったり、忘れているのでリマインドを送る。
- 中間締め切りを用意して確認するのも良いかも。
- 作るアプリの機能と画面を定義しておく。
- 箇条書きとメモ書きでもいい。
- 途中で変えた場合共有してもらう。
- 全員で全く同じアプリを作る場合は講師側でまとめておけば大丈夫。
- クレデンシャル情報は扱わない。
- 扱う場合は講師側で紛失しても対応できるものを用意して利用してもらう。
- 理解の早い遅いどちらに合わせるか方針を決めていなかった。
- やる気のない人を切ってしまってよいか確認する。
- 質問しにくい雰囲気を作ってしまっていたのかもしれない。
- 丁寧語で接してたが、もっと距離を縮めることが必要だったのかも。
3回やってこんな感じですが、めげずに参加した人の満足度を高めていきたいです。
参考
Kotlinの可変長引数とフォーマット
Kotlinの可変長引数とフォーマットを利用した置き換えで詰まったのでメモ
可変長引数
メソッドの引数として定義するときは vararg
を付けます。
fun someMethod(vararg args: String) { args.forEach { println(it) } } someMethod("one") // ok someMethod("one, two") // ok someMethod(arrayOf("one", "two")) // error someMethod(arrayListOf("one", "two")) // error
フォーマット
文字列内で置き換えできるやつです。
fun greet(name: String): { val greetText = "Hello %s." println(greetText.format(name)) } greet("kotlin") // Hello kotlin.
ハマったとこ
可変長引数の展開
次のようなフォーマットに対して
val singleGreet = "Hello %s."
次の2つは結果が異なります。
- フォーマットに直接指定
singleGreet.format("everyone") // Hello everyone.
- フォーマットに関数を経由して指定
fun greet(vararg args: String) { singleGreet.format(args) } greet("everyone") // Hello [Ljava.lang.String;@30dae81.
これは、greet関数の引数で可変長引数としているため、formatにはArray
として利用されるからです。
避けるためには、可変長引数を展開してあげる必要があります。
展開するには、可変長引数の先頭に *
を付けます。(spread演算子)
- 可変長引数を展開
fun greet(vararg args: String) { singleGreet.format(*args) } greet("everyone") // Hello everyone.
どちらもコンパイルが通るのでアレですが、空気を読んでほしかった部分はあります。
冒頭で書いた関数も、spread演算子を使えばコンパイルが通ります。
someMethod(*arrayOf("one", "two")) // ok
フォーマットが複数ある場合も展開するときれいに収まります。
val multiGreet = "Hello %1\$s, and %2\$s." // これでも出来るが `*` を使うほうが変更に強い fun greetMultiNoSpread(vararg args: String) { multiGreet.format(args[0], args[1]) } greetMultiNoSpread("mother", "father") // Hello mother, and father. fun greetMulti(vararg args: String) { multiGreet.format(*args) } greetMulti("mother", "father") // Hello mother, and father.
raw stringsでの展開
"
3つで囲むやつです。改行などもそのまま出力できます。
フォーマットが複数ある場合は $
を使う必要があるのですが、raw stringsだとバックスラッシュでのエスケープができません。
val rawEscapedMultiGreet = """ |Hello %1\$s. |Hello %2\$s. """.trimMargin() // Unresolved reference: s // コンパイルエラー
次のように書きます。
val rawMultiGreet = """ |Hello %1${'$'}s. |Hello %2${'$'}s. """.trimMargin()
${}
のString Templatesを利用して$
に置き換えます。
参考
KotlinでString.formatを使う / Qiita
Variable number of arguments (Varargs) / Kotlin Programming Language
JavaからKotlinに変換してSupport LibraryとMockitoとRobolectricでハマった話とその解決方法
先日勤務先で開発合宿に行きました。プロダクトのKotlin化が途中だったので、仕上げてきました。
スライドでは省略した、ハマった点の詳細を共有します。
確認できるサンプルを用意してあります。サンプルの環境は次の通りです。
- Android Studio: 2.3.3
- Support Library: 25.3.1
- Kotlin: 1.1.3-2
- Mockito: 2.8.47
- Robolectric: 3.3.2
ハマった点
Non-NullにNullが入る
JavaからKotlinへは、Android Studioの機能でファイル単位で変換できます。
Javaで定義されているメソッドを呼び出す際に、次のエラーが起きてアプリがクラッシュすることがあります。
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull
これはNon-Nullな引数にNullが入って起きます。
自分の環境では Activity#onActivityResult
で起きました。
回避するには @Nullable
アノテーションを付けておけば、変換時にも考慮されてNullableになります。
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); }
- Kotlin
override fun onCreate(savedInstanceState: Bundle?) { // Nullableになってる super.onCreate(savedInstanceState) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { // dataがNullableになっていない super.onActivityResult(requestCode, resultCode, data) }
Kotlinで定義されていればどこかで不整合が起きて気づくか、上の例外は発生しません。
プロジェクト内でKotlinからJavaを呼び出している箇所があればこれは起こり得るので、注意してください。
Abstractクラスのテストで変更が反映されない
Abstractクラスを継承したクラスで、変更したプロパティが反映されないというものがありました。
次のようなクラスです。要点だけ抜き出してます。
- Abstractクラス
abstract class Ticket { abstract var memo: String ... }
- 継承したクラス
class MovieTicket(...) : Ticket() { override var memo: String = "memo" ... }
こちらはこのような結果になります。
val mock = Mockito.mock(MovieTicket::class.java) Assert.assertNull(mock.memo) // nullになる mock.memo = "test memo" Assert.assertNull(mock.memo) // 代入してもnullになる
似たようなものをJavaで書きます。
- Abstractクラス
public abstract class JTicket { @NonNull String memo; ... }
- 継承したクラス
public class JMovieTicket extends JTicket { JMovieTicket(...) { memo = "memo"; ... } ... }
こちらはこのような結果になります。
JMovieTicket mock = Mockito.mock(JMovieTicket.class); Assert.assertNull(mock.memo); // nullになる mock.memo = "test memo"; Assert.assertNotNull(mock.memo); // 代入されるのでNullにはならない
Javaは直接フィールドに、Kotlinはアクセサーを利用している違いが、この挙動の変化の理由です。
はじめに定義したKotlinをcompileしてからdecompileしてJavaへ変換してみます。便宜上KJTicketとしておきます。
- Abstractクラス
public abstract class KJTicket { public KJTicket() { } public abstract String getMemo(); public abstract void setMemo(String s); ... }
- 継承したクラス
public final class KJMovieTicket extends KJTicket { .... private String memo; public KJMovieTicket() { this(...); } public KJMovieTicket(...) { ... memo = "memo"; ... } ... public String getMemo() { return memo; } ... public void setMemo(String s) { Intrinsics.checkParameterIsNotNull(s, "<set-?>"); memo = s; } }
KJTicketクラスにはフィールドがありません。そして、継承したKJMovieTicketにフィールドが生成されていますが、privateになっています。なので、フィールドには、メソッドでアクセスしていることになります。
このフィールドはBacking fieldsと呼ばれるもので、自動で生成されるものです。 Backing fieldsにアクセスするメソッドをアクセサーと言います。
このクラスを使うとこのような結果となります。
KJMovieTicket mock = Mockito.mock(KJMovieTicket.class); Assert.assertNull(mock.getMemo()); // nullになる mock.setMemo("test memo"); Assert.assertNull(mock.getMemo()); // 代入してもnullになる
無事Kotlinのときと同じ結果になりました。
MockitoのMockは、実際のメソッドを呼ばないので、アクセサーがうまく機能していません。
なので、MockをMockito.CALLS_REAL_METHODS
オプションを付けて生成することで、実際のメソッドを呼ぶようにしてあげます。
val mockWithOption = Mockito.mock(MovieTicket::class.java, Mockito.CALLS_REAL_METHODS) Assert.assertNull(mockWithOption.memo) // nullになる val memo = "test memo" mockWithOption.memo = memo Assert.assertEquals(memo, mockWithOption.memo) // test memoが代入されてる
これでうまくいきます。
もっとシンプルに解決したい場合はSpyすれば良いのですが、インスタンスを作れない制約が既存コードにあったので、このような解決方法を取りました。
見出しでAbstractクラスのテストと書いたのですが、これは普通のクラスでもKotlinなら起きます。
- 普通のクラス
class CouponTicket(var memo: String = "coupon")
- Mockを用いたテスト
val mock = Mockito.mock(CouponTicket::class.java) Assert.assertNull(mock.memo) // nullになる mock.memo = "test memo" Assert.assertNull(mock.memo) // 代入してもnullになる
Abstractクラスでもmockを作れます。
Abstractクラスでは、Mockito.CALLS_REAL_METHODS
オプションを付けてもnull
になります。まだ実装されてないので、何もしないメソッドが呼ばれるだけだからです。
val mockWithOption = Mockito.mock(Ticket::class.java, Mockito.CALLS_REAL_METHODS) Assert.assertNull(mockWithOption.memo) // nullになる val memo = "test memo" mockWithOption.memo = memo Assert.assertNull(mockWithOption.memo) // nullになる
Mockito.when
を使うことでメソッドをmockできます。
val mock = Mockito.mock(Ticket::class.java) Assert.assertNull(mock.memo) // nullになる val memo = "test memo" Mockito.`when`(mock.memo).thenReturn(memo) // メソッドの返り値を置き換えている Assert.assertEquals(memo, mock.memo)
@Jvmアノテーションを消せない
フルKotlinなら、Jvm系のアノテーションが削除できると思って、全て削除したところ一部期待通りの挙動にならない部分がありました。
Androidに依存する部分のtestを、ReobolectricのShadowを使って置き換えていた部分で期待通りに動きませんでした。
- Device.kt
object Device { /** * 端末の日付の設定を自動にしているかどうか。 * */ fun enabledAutoTime(context: Context): Boolean { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { return Settings.Global.getInt(context.contentResolver, Settings.Global.AUTO_TIME) > 0 } else { return Settings.System.getInt(context.contentResolver, Settings.System.AUTO_TIME) > 0 } } catch (e: Settings.SettingNotFoundException) { return false } } }
Device#enabledAutoTime
は、Roblectricで呼ぶとSettings.SettingNotFoundException
が発生して常にfalseになってしまうので、Shadowを作成して挙動を制御していました。
ObjectのままShadowを作成するとpublicなコンストラクタがないのでエラーになります。
Caused by: java.lang.RuntimeException: Could not instantiate shadow, missing public empty constructor. at org.robolectric.internal.bytecode.ShadowWrangler.getShadowCreator(ShadowWrangler.java:385) at org.robolectric.internal.bytecode.RobolectricInternals.getShadowCreator(RobolectricInternals.java:34) at org.robolectric.internal.bytecode.InvokeDynamicSupport.bindInitCallSite(InvokeDynamicSupport.java:115) at org.robolectric.internal.bytecode.InvokeDynamicSupport.bootstrapInit(InvokeDynamicSupport.java:53) at java.lang.invoke.CallSite.makeSite(CallSite.java:283) at java.lang.invoke.MethodHandleNatives.linkCallSiteImpl(MethodHandleNatives.java:307) at java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:297) at com.sakebook.android.sample.cornercasetestsample.Device.$$robo$init(Device.kt) at com.sakebook.android.sample.cornercasetestsample.Device.<init>(Device.kt)
そのため、次のようなShadowを作成しました。
- ShadowDevice.kt
@Implements(Device::class) class ShadowDevice { companion object { @Implementation fun enabledAutoTime(context: Context): Boolean { return true } } }
こちらも、KotlinからJavaに変換してみました。便宜上KJDeviceとします。
- KJDevice.java
public final class KJDevice { private KJDevice() { } static { new KJDevice(); } // public static final KJDevice INSTANCE = (KJDevice)this; public static final KJDevice INSTANCE = new KJDevice(); public final boolean enabledAutoTime(Context context) { Intrinsics.checkParameterIsNotNull(context, "context"); try { if(android.os.Build.VERSION.SDK_INT >= 17) { return android.provider.Settings.Global.getInt(context.getContentResolver(), "auto_time") > 0; } else { return Settings.System.getInt(context.getContentResolver(), "auto_time") > 0; } } catch (Settings.SettingNotFoundException e) { return false; } } }
コンストラクタがPrivateになっています。 コメントアウトしているところは表現できなかったのでコメントアウト直後のものに変えてます。
こちらもJavaから呼び出すとわかるのですが、 enabledAutoTime
を呼び出すとこうなります。
KJDevice#INSTANCE#enabledAutoTime(context)
staticメソッドになっていないので、INSTANCE
越しに呼び出すことになります。
ObjectはそのままShadowを作成できないので、classでShadowを作成する必要があります。その際、 @JvmStatic
アノテーション を付けないと、Objectで定義しているメソッドと同じSyntaxにならないので、Shadowでの置き換えがされません。
利用しているクラスも、ShadowクラスもKotlinで書いていても、こちらは`@JvmStaticアノテーションが必要になります。
まとめ
長々と書きましたが、ハマった点の要点は以下です。
- KotlinからJavaを呼び出している箇所でNon-NullにNullが入る可能性があるので、可能ならば
@Nullable
アノテーションを付けてからJava -> Kotlin変換をする - Kotlinはプロパティのアクセスはメソッドを利用するので、Mockするときは
Mockito.CALLS_REAL_METHODS
を使う - ObjectのShadowを作るときはClassで作成し、Syntaxが同じになるように
@JvmStatic
アノテーションをつける
Javaからどう見えるかというところを意識する必要があったり、言語仕様的な部分を意識する必要がたまにあるのですが、最終的に腑に落ちたので良いハマり方をしました。
参考
海で開発!? 2泊3日で伊豆に開発合宿行ってきました! / JX通信社 エンジニアブログ
Null Safety / Kotlin Programming Language
KotlinコードをJavaコードに変換してみた / アクトインディ技師部隊報告書
Working with the Command Line Compiler / Kotlin Programming Language
apkをコマンド一つでjavaにデコンパイルする / Qiita
Properties and Fields / Kotlin Programming Language