Jetpack Composeでリストの最後を検知する
この記事は Jetpack Composeでリストの最後を検知する - Qiita のコピーです。
Jetpack Composeでリスト表示を作るとき、今までのRecyclerViewのようにリスト表示を作るとき、 LazyColumn
というComposableメソッドを使うと思います。
この LazyColumn
の引数には、 LazyListState
型の state
を渡すことができ、これによって今リストのどのへんをスクロールしてるかなどの状態を参照したり、操作したりできるようになります。
そして本題のリストが最後のItemまでスクロールされたかどうかを検知する方法ですが、自分は今の所下記の方法で実装しています。
val listState = rememberLazyListState() LazyColumn( contentPadding = LocalWindowInsets.current.systemBars .toPaddingValues(top = false, start = false, end = false), state = listState ) { if (listState.firstVisibleItemIndex + listState.layoutInfo .visibleItemsInfo.size == listState.layoutInfo.totalItemsCount) { // なにかする } } }
listState
がリストの状態を持っている変数です。
listState.firstVisibleItemIndex
が今画面に表示されている一番上のItemのindexを持っています。そして、listState.layoutInfo.visibleItemsInfo.size
が今画面に表示されているItemの数を持っています。これを足した値が、 listState.layoutInfo.totalItemsCount
である全体のリストの長さと同じになったときリストが一番最後までスクロールされた状態であることがわかります。
自分はこの方法でやっているのですが、もっとスマートなやり方がありそうなので詳しい方教えてくださると嬉しいです。
そのうち、デフォルトでリスナーみたいなのが LazyListState
から生えるといいな。。。。
ちなみに、 LazyListState
には listState.animateScrollToItem()
っていうのがいてよくある、リストを一番上まで一気にスクロールさせるようなものも簡単に実装できそうです。
Kotlinのinlineとcrossinlineの挙動をバイトコードで追う
この記事は、Kotlinのinlineとcrossinlineの挙動をバイトコードで追う - Qiita のコピーです。
ただのメソッドの呼び出し
Kotlinでこのようなコードを書いたとき
fun main() { doSomeThing() } fun doSomeThing() { println("This is doSomeThing") }
バイトコードはこんな感じになってます。 main()
の中で doSomeThing()
を呼び出すということを行っています。
public final class InlineCrossinlineKt { public static final void main() { doSomeThing(); } // $FF: synthetic method public static void main(String[] var0) { main(); } public static final void doSomeThing() { String var0 = "This is doSomeThing"; boolean var1 = false; System.out.println(var0); } }
inlineがやること
さっきまであった main()
の中の doSomeThing()
の呼び出しがなくなって、 doSomeThing()
の中身が main()
の中に展開された形になります。これがinlineの性質です。他のメソッドを呼び出すというのは少なからずコストがかかることで、それをなくす形で同じように実行してくれるというものです。
fun main() { doSomeThing() } inline fun doSomeThing() { println("This is doSomeThing") }
public final class InlineCrossinlineKt { public static final void main() { int $i$f$doSomeThing = false; String var1 = "This is doSomeThing"; boolean var2 = false; System.out.println(var1); } // $FF: synthetic method public static void main(String[] var0) { main(); } public static final void doSomeThing() { int $i$f$doSomeThing = 0; String var1 = "This is doSomeThing"; boolean var2 = false; System.out.println(var1); } }
ちなみにこのコードをIDEで書いた場合、このinlineはあまり意味がないよとワーニングが起こり、inlineをつけるときは、引数に関数型を持つメソッドにつけるとbestだよと言われます。
bestなinline
doSomeThing()
にラムダを引数に取るように変更しました。さきほどのワーニングも消えました。
fun main() { doSomeThing { println("This is block") } } inline fun doSomeThing(block: () -> Unit) { println("This is doSomeThing") block() }
このときのバイトコードがどうなってるか同じように確認してみましょう。
public final class InlineCrossinlineKt { public static final void main() { int $i$f$doSomeThing = false; String var1 = "This is doSomeThing"; boolean var2 = false; System.out.println(var1); int var3 = false; String var4 = "This is block"; boolean var5 = false; System.out.println(var4); } // $FF: synthetic method public static void main(String[] var0) { main(); } public static final void doSomeThing(@NotNull Function0 block) { int $i$f$doSomeThing = 0; Intrinsics.checkNotNullParameter(block, "block"); String var2 = "This is doSomeThing"; boolean var3 = false; System.out.println(var2); block.invoke(); } }
先ほどと同じように、 doSomeThing()
の中の This is doSomeThing
はinlineの影響で main()
の中で直接 println()
されたかのように振る舞います。
そして、今回 doSomeThing()
の引数に追加したラムダである block()
は doSomeThing()
で実行されて This is block
が出力されるわけではなく、 main()
の中で block()
の処理が直接展開されて実行されます。
実行結果は一緒ですが、中ではこのような処理の違いが起こります。
そして、ラムダの前に crossinline
をつけてdoSomeThing(crossinline block: () → Unit)
としたとき、バイトコードは同じになります。
なので、 inline fun
の引数がラムダを持つとき、そのラムダは crossinline
の影響によって、そのラムダの中身の処理は呼び出し元の main()
の中に展開されたような振る舞いをします。
ちなみに、 block()
に noinline
をつけた場合のバイトコードを見てみましょう。
fun main() { doSomeThing { println("This is block") } } inline fun doSomeThing(noinline block: () -> Unit) { println("This is doSomeThing") block() }
この場合、 inline
の影響で doSomeThing()
の処理である This is doSomeThing
の出力は main()
の中で行われますが、 block()
は doSomeThing()
の中で block.invoke()
が呼ばれて実行されています。
public final class InlineCrossinlineKt { public static final void main() { Function0 block$iv = (Function0)null.INSTANCE; int $i$f$doSomeThing = false; String var2 = "This is doSomeThing"; boolean var3 = false; System.out.println(var2); block$iv.invoke(); } // $FF: synthetic method public static void main(String[] var0) { main(); } public static final void doSomeThing(@NotNull Function0 block) { int $i$f$doSomeThing = 0; Intrinsics.checkNotNullParameter(block, "block"); String var2 = "This is doSomeThing"; boolean var3 = false; System.out.println(var2); block.invoke(); } }
inlineのメリット・デメリット
inline
をつけたメソッドを定義することで、そのメソッドの中身の処理がそのままコピーされたように呼び出し元に展開されて実行される挙動になります。これにより、inline
メソッドの中身の処理が呼び出し元にそのまま書かれているように実行され、メソッドを呼ぶコストを削減できます。 inline
メソッドの中身の処理によると思いますが、少しパフォーマンスの良いコードを書くことができるようになるというわけです。
じゃあ、全部 inline
つければすっごいパフォーマンスいいコード書けるようになるんじゃないか?と思うかもしれません。
しかし、Decompileしたバイトコードを見てみると、 inline
をつけた場合その中身の処理のコピーが呼び出し元に展開されて、 inline
メソッドを呼び出せば呼び出すほどこのコピーが増えてバイトコードの容量も大きくなっていきます。なので、 inline
をつければつけるほど apkファイルなどのファイルサイズが大きくなってしまうことが挙げられます。これが inline
をつけるときのデメリットというか、適材適所という訳です。
なので、 inline
メソッドは中身の処理はそこまで多くないけど、色々なとこで呼ばれるケースがあるとき効果を発揮します。
RecyclerViewのAdapterにClickListenerのいい感じの渡し方とKotlinのラムダ式がチョットわかったはなし
みなさん、Itemの中にボタンがあるRecyclerViewのAdapterを実装するとき、そのボタンの押されたときの処理はどのように書いてますか。
自分は今までこんな感じで書いてました。
class MyAdapter(private val listener: ClickButtonListener) { override fun onBindViewHolder(holder: FriendListViewHolder, position: Int) { holder.view.button.setOnClickListener { listener.onClick("data") } } } interface ClickButtonListener { fun onClick(data: String) }
class ListFragment : Fragment(), Adapter.ClickButtonListener { val adapter = MyAdapter(this) override fun onClick(data: String) { /** ~~~~~ **/ } }
おそらく、どこかの記事を参考に意味もわからずコピペしたものです。
これは以下ように書き換えることができます。
class MyAdapter(private val listener: ClickButtonListener) { override fun onBindViewHolder(holder: FriendListViewHolder, position: Int) { holder.view.button.setOnClickListener { listener.onClick("data") } } } interface ClickButtonListener { fun onClick(data: String) }
class ListFragment : Fragment() { val adapter = MyAdapter(object : ClickButtonListener { override onIconClick(data: String) { /** ~~~~~ **/ } }) }
Fragmentに interface ClickButtonListener
の実装を渡してましたが、Adapterのインスタンス化時に同時に object
としてinterfaceの実装を渡せるようになり、すっきり書くことができるようになりました。
さらにこれは、以下のように書き換えることができるようになります。
class MyAdapter(private val onClick: (data: String) -> Unit) { override fun onBindViewHolder(holder: FriendListViewHolder, position: Int) { holder.view.button.setOnClickListener { onClick.invoke("data") } } }
class ListFragment : Fragment() { val adapter = MyAdapter { data -> // itで省略可 /** ~~~~~ **/ } }
さらにすっきりしました。
MyAdapter
では (data: String) -> Unit
の関数型を受け取るようにして、 ListFragment
ではadapterのインスタンス化時にラムダ式を渡すようにしました。
参考
KMMでサンプル作ってみた
はじめに
KMMはKotlin Multiplatform Mobileといって、KotlinでiOSとAndroidの共通なビジネスロジックを書いて時間と労力を節約しようというものです。(公式より)
最近は、Flutterのコミュニティがかなり活発で人気があるように感じますが、KMMはいい意味でも悪い意味でもiOSとAndroidのUI部分を別々に記述できます。Flutterでは自分で工夫しないとiOSでもマテリアルデザインっぽいUIになってしまってiOSユーザからは少し見慣れないUIになりがちかなと思います。私もFlutterを触っていくつか趣味程度に個人アプリを開発してきましたが、Flutter開発に使うDartに比べてKotlinのほうが個人的に書きやすくて機能が豊富だったりサポートが手厚い感じがしていいなと思いました 。最近色々な豊富な機能が追加されてきているCoroutinesを使えるのもかなり大きいと思います。
KMMではそんなKotlinを使って今まで冗長になっていたiOSとAndroidのビジネスロジックのコードを共通化することで開発に必要なリソースの削減が可能になります。
作ったものはこちらにあります。
この記事では自分がKMMを触って得た知見をざっくりまとめていこうと思います。
この記事を読んでKMM触ったことない人がざっくりKMMでアプリを作るときどんな感じなのかイメージを掴んでもらえたらいいなと思います。
内容
モジュール・パッケージ構成
Android StudioでKMMのpluginがあってそれを入れるとNew ProjectするときにKMMの雛形ができたProjectを作ることができます。今回自分もそれを使いました。
そうすると、大きく androidApp
、iosApp
、shared
の3つのモジュールが作られます。
このshared
モジュールの中にiOSとAndroidの共通コードを記述していくのですが、この中は大きくandroidMain
、commonMain
、iosMain
という3つのpackageに分けられています。
主にcommonMain
の中に共通のロジックを書いていきます。自分がつくったサンプルではcommonMain
の中のdata
パッケージの中にDataSourceやそれにアクセスするRepositoryなどのロジックを入れました。これらのコードをAndroidはもちろん、iOSでも参照して使うことでプラットフォームごとに別々に同じようなコードを書くことを防いで開発リソースを削減するイメージです。
他のandroidMain
、iosMain
は共通コードの中でもプラットフォームに依存していて別々に記述しないといけないものをKotlinで記述していきます。その際、まずcommonMain
にそれぞれのプラットホームで実装する必要があるものをInterfaceのようにexpect class
として記述しておきます。そのあとにandroidMain
、iosMain
に同じクラス名でactual class
として実装します。KMMでNew Projectした段階ではPlatform.kt
がそれにあたって、デバイスの名前やOSの名前を取得しています。
ここで気をつけないといけないのが、それぞれのプラットフォームごとの実装クラスを書くときのクラスファイルを配置するpackage階層を`commonMain`と合わせないといけないことです。
たとえば、commonMain/hoge
の直下にPlatform.kt
をexpect class
として作った場合、androidMain
とiosMain
においてもandridMain/hoge/Platform.kt
、iosMain/hoge/Platform.kt
となるようにactual class
のファイルを配置しないとエラーが起こり続けます。
これを使う場面としては、後述するローカルDBに接続するために必要なDriverの作成を各プラットフォームごとに実装するときなどに使います。具体的にはDatabaseDriverFactory.kt
に当たります。
ちなみにこのcommonMain
モジュールのbuild.gradle.kts
を見てみると
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import dependencies.Dep plugins { kotlin("multiplatform") kotlin("plugin.serialization") id("com.android.library") id("com.squareup.sqldelight") } kotlin { android() ios { binaries { framework { baseName = "shared" } } } sourceSets { val commonMain by getting { dependencies { implementation(Dep.Kotlin.coroutines) implementation(Dep.Kotlin.serialization) implementation(Dep.Ktor.client) implementation(Dep.Ktor.serialization) implementation(Dep.SqlDelight.runtime) implementation(Dep.SqlDelight.coroutinesExtensions) implementation(Dep.Kodein.kodeinDi) } } val commonTest by getting { dependencies { implementation(kotlin("test-common")) implementation(kotlin("test-annotations-common")) } } val androidMain by getting { dependencies { implementation(Dep.AndroidX.design) implementation(Dep.Ktor.clientAndroid) implementation(Dep.SqlDelight.androidDriver) } } val iosMain by getting { dependencies { implementation(Dep.Ktor.clientIos) implementation(Dep.SqlDelight.nativeDriver) } }
こんな感じになっていて、commonMain
、androidMain
、iosMain
ごとに依存関係を書く感じになってます。
どこまで共通化させられるのか
私のサンプルではMVVMな感じで作ったのですが、ViewとViewMode以外を共通化させてます。頑張ったらViewModelまで共通化させられるのか私のレベルではわかりませんが、ViewModelで参照しているUseCaseまで共通化させています。ViewModelでは宣言したメンバー変数たちをViewでObserveする必要があってそれぞれのプラットフォームでObserveできるオブジェクトにする必要があるため、それぞれのプラットフォームごとにViewModel以降作ったほうがいいかなと思います。ライフサイクルの兼ね合いもありますね。
iOSでもKotlin Coroutines Flowが使える
Flowはマルチプラットフォームに対応するようにも作られているようで、マルチプラットフォーム用のCoroutinesのバージョンはKotlin1.4.20現在で1.4.1-native-mtが最新のようです。 Concurrency and coroutines—Kotlin Multiplatform Mobile Docs
そのままでもiOSでFlowで流れてきた値を受け取れるようですが、使いにくいようなので私のつくったサンプルではCFlowというFlowのラッパーを作って使いました。
これはM3 Tech BlogのKotlin Multiplatform Mobileを使ってBrainf*ckエディタアプリを作るで紹介されていて自分も真似しました。そちらのブログでも紹介されているのですが、JetBrainsが開発したkotlinconf-appのFlowUtils.ktでも使われているやり方みたいです。
Androidでは普通にCoroutineScopeの中でcollect
を使って受け取ってiOSでは下のように受け取ることができます。watchは先程のFlowのラッパーで実装したメソッドです。completionHandler
でエラーハンドリングもできるようです。
searchActressUseCase.searchActress(searchedKeyWord: searchedKeyWord, completionHandler: { response, error in response?.watch { [weak self] response in if let actresses = response?.result.actress { self?.actresses = actresses } } })
DataSource - Remote
サーバのAPIを叩くとき、素のAndroidアプリを開発するときはRetrofitとかを脳死で使うと思いますが、KMMではKtorを使うのが今の所のデファクトスタンダードみたいです。
こんな感じでクライアントを作って使います。
private val dmmApiClient = HttpClient { install(JsonFeature) { val json = Json { ignoreUnknownKeys = true isLenient = true } serializer = KotlinxSerializer(json) } }
実際にGETする処理はこんな感じです。
dmmApiClient.get { url(Constants.DMM_ENDPOINT) parameter("api_id", Constants.API_ID) parameter("affiliate_id", Constants.AFFILIATE_ID) parameter("keyword", searchedKeyWord) }
このコードでは、https://api.dmm.com/affiliate/v3/ActressSearch?api_id=API_ID&affiliate_id=AFFILIATE_ID&keyword=searchedKeyWord
というようなエンドポイントを叩いています。
あまり調べてもクエリパラメータを混ぜる方法がわからなかったので残しておこうと思います。
ちなみにそれっぽいこういう公式のドキュメントが出てきましたが僕には解読できませんでした。
DataSource - Local
Androidアプリ開発でいうとRoomに当たる部分です。サンプルアプリでは現在のデファクトスタンダードそうなSQLDelightというライブラリを使っています。 cashapp.github.io
ここで、DBにアクセスするためのSqlDriverを生成するコードが各プラットフォームに依存しているため、commonMain
にexpect class
を書いてandroidMain
とiosMain
でactual class
として実装します。
これはあまり難しいことはないです。公式のドキュメントも充実してます。
Flowにも対応しています。
DI
KMMのDIではマルチプラットフォーム対応しているKodein-DIというライブラリがいいみたいです。 kodein.org
DIというとAndroidアプリエンジニアだとDaggerとかを想像して超難しいイメージがあるかもしれません。自分がそうです。しかしこのライブラリのDIはそれにくらべてとても簡単でわかりやすかったです。私自身あまり使ったことがないのですがKoinを使ったDIみたいな雰囲気を感じました。 これについても M3 Tech Blogの方でわかりやすく解説されています。
おわり
iOSアプリ開発者目線でKMMがどのような受け取られ方をしているかあまりわからないのですが、KMMを使った開発がプロダクト開発でも取り入れられて開発リソースの削減から余ったリソースで今までできなかったことに手を出せるような世界になったらいいなと思ってます。
KMMが今後開発者界隈でどのように受け取られてどうなっていくか楽しみです。
Re:ゼロから始めるbuild.gradle.kts生活
はじめに
タイトルにあるように build.gradle.kts
をAndroidアプリ開発で使っていこうと思います.
また,マルチモジュールでプロジェクトを運用する際に同じような記述を共通化させる部分の紹介もしたいと思います.
最初,Android StudioでNew Projectすると build.gradle
というGroovyで記述されたスクリプトファイルが生成されます.
Groovyはあまり聞き慣れませんが,Javaから派生した動的型付けのプログラミング言語です.
build.gradle
でGroovyを使う際のイマイチな点として以下のものが挙げられると思います.
- 普段はJavaやKollinで開発するのに build.gradle
だけGroovyで書かなければいけないく,JavaやKotlinで得た知見をbuild.gradle
に活かせない
- 補完が効かない
大きくこの2つかなと個人的な見解として思っています. DroidKaigi2019のセッションでも紹介されていました.
これを解決するためにbuild.gradle
をKotlinで記述してAndroidアプリ開発をしようというときに出てくるのがbuild.gradle.kts
です.今までGroovyで記述していたbuild.gradle
をKotlinで記述することができるようになります.
ではやっていきましょう.
Re:ゼロから始めるbuild.gradle.kts生活
やっていく
Step1
まずは,プロジェクト直下にbuildSrc
という名前でdirectoryを作ります.
その中に build.gradle.kts
ファイル を作って以下の中身を書きます.
plugins { `kotlin-dsl` } repositories { jcenter() }
書いたらsync nowします 色々ファイルが生成されます
Step2
次に buildSrc/src/main/java/dependencies
となるようにpackageを作ります
dependencies
の中に Libs.kt
作ります.
この中にはプロジェクトで必要なライブラリの依存関係を記述します.
よく Dep.kt
という名前で作りますが,今回は後々 Dependencies.kt
というファイルを作るときに名前が被るので適当に Libs.kt
という名前にしておきたいと思います.
Libs.kt
の中身はこのように書いていきます.ここはDroidKaigi/conference-app-2020のDep.ktが参考になると思います.
object Dep { object Kotlin { val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.61" } }
Step3
buildSrc
の中のbuild.gradle.kts
に追記
plugins { `kotlin-dsl` } repositories { jcenter() google() // 追記 } dependencies { implementation("com.android.tools.build:gradle:4.0.1") // 追記 }
com.android.tools.build:gradle
のバージョンは適宜変えてください.
これを入れることでこのあとマルチモジュールした際の依存関係の共通化をするコードを書くことができるようになります.
Step4
次にもともとbuild.gradle
にあった android
ブロックを共通化させるためのコードを書いていきます,
マルチモジュールでプロジェクトを作るとき,デフォルトではこの部分はすべてのモジュールで同じことを書かなければいけません.
そこで,buildSrc/src/main/java/dependencies
の中に BuildConfig.kt
というような名前でファイルを作成します.
中身は以下のように記述します.
package dependencies import com.android.build.gradle.BaseExtension object BuildConfig { const val applicationId = "com.example.app" const val versionCode = 1 const val versionName = "1.0" const val androidTargetSdkVersion = 29 const val androidCompileSdkVersion = 29 const val androidMinSdkVersion = 23 const val buildToolsVersion = "29.0.3" const val testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" const val consumerProguardFiles = "consumer-rules.pro" } fun BaseExtension.baseExtension() { compileSdkVersion(BuildConfig.androidCompileSdkVersion) buildToolsVersion(BuildConfig.buildToolsVersion) defaultConfig { minSdkVersion(BuildConfig.androidMinSdkVersion) targetSdkVersion(BuildConfig.androidTargetSdkVersion) versionCode = BuildConfig.versionCode versionName = BuildConfig.versionName testInstrumentationRunner = BuildConfig.testInstrumentationRunner consumerProguardFiles(BuildConfig.consumerProguardFiles) } buildTypes { getByName("release") { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } }
Step5
実際に使ってみます
appにある build.gradle
を build.gradle.kts
にrenameします.
中身は以下のように書き換えます.
一番下の dependencies
のブロックは今はおいておきます.
plugins { id("com.android.application") id("kotlin-android") id("kotlin-android-extensions") } android { baseExtension() defaultConfig.applicationId = BuildConfig.applicationId }
先程の BuildConfig.kt
で baseExtension
という名前で定義したものが使えています.
このように実際に使う際は android
ブロック内で baseExtension()
と一行書くだけで冗長だったコードを共通化することができています.
Step6
では次にdependencies
ブロックについてです.いつも使いたいライブラリの依存関係を定義するところですね.
ここも例えば features/home
と features/settings
みたいなモジュールを作った際に使うライブラリの依存関係が同じようになることがよくあります.
その部分もある程度のベースとなる依存関係は共通化させるようにします.
buildSrc/src/main/java/dependencies
に Dependencies.kt
を作ります.
中身は以下のように書いてみます.
fun Project.baseDependencies(additionalConfiguration: DependencyHandlerScope.() -> Unit) { dependencies { implementation(Libs.Kotlin.stdlib) implementation(Libs.AndroidX.appCompat) implementation(Libs.AndroidX.coreKtx) implementation(Libs.AndroidX.constraint) } dependencies(additionalConfiguration) } private fun DependencyHandler.implementation(depName: Any) { add("implementation", depName) }
Step7
では,先程作った baseDependencies
を使ってみようと思います.
appの build.gradle.kts
に dependencies
を追加していきます.
中身を以下のようにします.
import dependencies.baseExtension import dependencies.BuildConfig plugins { id("com.android.application") id("kotlin-android") id("kotlin-android-extensions") } android { baseExtension() defaultConfig.applicationId = BuildConfig.applicationId } baseDependencies { }
いままで dependencies
で書いて中に implementation
を書いていましたが,どこでも使うようなベースになる依存関係はすべて baseDependencies
だけでかけるようになりました.
もちろん baseDependencies
の中でそのモジュール内でまた別に定義したい依存関係は implementation
で適宜追加できます.
おまけ
1つのライブラリを使いたいときにそれに関連する依存関係を複数まとめて書きたい場合があると思います.
そのときに, Dependencies.kt
に以下のようなコードを追加してみます.
fun DependencyHandler.groupie() { implementation(Libs.Groupie.groupie) implementation(Libs.Groupie.databinding) }
そうすると,build.gradle.kts
で今回だとGroupieで使いたい複数の依存関係を以下のように一行ですっきり書くことができるようになります.
import dependencies.baseExtension import dependencies.BuildConfig plugins { id("com.android.application") id("kotlin-android") id("kotlin-android-extensions") } android { baseExtension() defaultConfig.applicationId = BuildConfig.applicationId } baseDependencies { groupie() }
さいごに
こんな感じで今まで冗長だった依存関係の記述をすっきり,共通化させてしかもKotlinで書くことができるようになります.
みなさんも良き build.gradle.kts
生活を.
参考記事
Flutter初心者がおうちハッカソン参加してきた
はじめに
現在,新型コロナウィルス感染症毎年恒例で行われているオフラインハッカソンが軒並み延期しているようです.
これに21卒で同期になる予定の友達4人とCyberAgentの人事のごってぃさんの計5人のチームで参加してきました.
とても楽しかったので記事に残したいと思います.
おうちハッカソンとは
詳しくはconnpassの詳細を見てみてください.
簡単に説明すると期間は一応5日間あってフルリモートで行うハッカソンです.
そのうち何日参加するのかやメンバー構成は自由でした.
テーマはお家で楽しめるなにかです.
参加したのは全体で40人弱くらいいました.
チーム構成
チームは5人いましたが,フロントエンドが4人とバックエンド1人という偏った構成でした.
フロントエンド
バックエンド
やったこと
つくったもの
僕は今まで何回か大学の友達とハッカソンに参加したことかあるのですが,アイデア出しでいつも躓いて時間を取られて開発に時間があまり取れず最後死にかける経験をしてきたのでアイデア出しは収束させるのに苦手意識がありました.
ですが今回はサクッと決まって12時位にはアイデアが決まって昼休憩後には機能の洗い出しや,今回最低限実装したい機能の決定と役割を決められていたと思います.
作ったのはこれです.
新型コロナウィルスによって家にいる時間が増えた今,家の掃除をしたり断捨離する人が増えているそうです.
そこで,掃除する前のBefore画像と掃除した後のAfter画像を投稿してどのように掃除したかやこういう感じに掃除しましたというのを楽しく共有できるSNSを考えて作りました.
どのような機能があるのか簡単に説明すると,先程の画像の左上の投稿一覧TL画面の上の検索入力をタップすると左下の画面のようにカテゴリー別で検索できます.一覧に表示される投稿画像は掃除する前のBeforeの画像が表示されてユーザにAfter画像に興味をもたせるような意図があります.
投稿をタップすると上段の真ん中の投稿の詳細画面に飛んで,Before画像からAfter画像までゆっくりフェードイン・フェードアウトのアニメーションとともに,劇的ビフォー・アフターの匠のBGMが流れる仕様になっています.
この詳細画面の部分は僕が作ったのですが,画像をAppbarが表示される部分まで全画面で見せるようにしたり,詳細部分は自由に動かせるBottomSheet的なので実装したりもともとUIに興味があるということもありこだわって作りました.
右上の投稿画面はごってぃさんがほぼすべて作りました.普段エンジニアではなく,人事をしている方なのに作業時間的にもクオリティ的にも現役エンジニアだなという感じで尊敬です…
ごってぃさんほんとになんでもできちゃう.
バックエンドは,@kosuke_kawanoが実装してくれていたのですが,デプロイなどに苦戦して僕らのチームには他にバックエンドを担当している人が不在であまり協力してあげられることができず,Firebaseをもりもりに使って実装しました.力になれず申し訳ない…
でも,Firebaseに移行しようとなってから@ry_ittoが爆速に対応してくれて,僕がUIにこだわっている間に気づいたらなんかできてたって感じでた笑
使った技術
タイトルにあるようにFlutterを用いてフロント側は開発を行いました.
開発した人の中で,ごってぃさんは前から少し勉強したことがあったりしましたが他の3人は全く初めて(僕は以前にハッカソンで使って挫折していた)だったりと言う感じでした.
なので,最初の方はいろいろな記事を眺めて勉強してました.
チームメンバーの中にはUdemyで勉強してる人もいました.
Flutter 全部俺 Advent Calendar 2019
この記事は最初難しくてわからない箇所が多かったですが,実際に手を動かしてみたあとに振り返ってみるととても参考なることばかり書かれていておすすめです.
あとはmonoさんのブログもかなりおすすめです.
これらを主に参考に最初に読み込んで勉強しました.
あとは実際に手を動かしながらわからないことはググってやるというような感じで,Flutterは日本語の記事もかなり多くてわからなくて詰まったということはあまりなかったです.調べたら何かしら記事が出てきて解決できた気がします.
僕は普段KotlinでMVVMを使ったAndroid開発をよくするのですが,今回採用したProviderパッケージを使ったFlutter開発はとても似ていてとっつきやすかったです.
Widgetに書く以外のロジックだったり状態はすべてChangeNotifierにまとめて書いて状態が変更された際はWidgetにChangeNotifierProviderで通知するといった感じでシンプルでわかりやすいのに柔軟性があってなんでもできちゃう感じで大好きです.
このハッカソンでしかFlutterはここまで触ったことがないのですが,よく耳にするBLoCというのも気になりますがそれを知らなくてもこれだけでいいんじゃないかって思うくらいProviderいいなって思ってます.気になるのでBLoCや他の技術についても勉強したいなとは思ってます.
開発環境
MTGや作業するときはDiscordのサーバを使ってボイスチャットを使用して行いました.
フルリモートで5日間のハッカソンという性質上モチベーション維持が大変そうでしたが,Discordで話しながら夜中の3時頃まで作業したりなどでき,オンラインでもみんなと頑張れる雰囲気があってよかったです.
アイデア出しは初日の朝10時にDiscordに集まってmiroというサービスのリアルタイムに複数人で同時編集できるホワイトボードを使ってオンラインでもオフラインでいるように付箋を貼ってアイデア出しをできたり特に問題なく使えました.
実際に使っていた画面はこんな感じ.
技術よりな環境としては,CI/CD周りの環境も構築して複数人で短期間で色々変更が起こってもよくわからないバグでアプリが動かなくなるというようなことは一度もありませんでした.
また,FirebaseのAppDistributionというサービスを用いて手元の端末に簡単にデバッグアプリを配布して確認できるような仕組みも整えていい感じにしてました.
おわりに
すごいサクッとでしたが,おうちハッカソンについて書いてみました.
チームにいた@hohohorisがもっといい感じに書いてくれてるのでそちらもぜひ読んでみてください.
Flutterとても楽しいです.
約一年前も別のハッカソンでFlutterを使ったときは右も左も分からず挫折しましたが,Android開発を経ていま触ると最高に楽しかったです.
こんなに楽しくてAndroidとiOSとあわよくばWebフロントなども開発できるというもう最高です.
めちゃくちゃ直感的にレイアウトも組めるし,状態管理も簡単できるのでおすすめです.
僕は完全に小さい頃にLEGOブロックで遊んでいたときのような感覚でWidget触ってました.
皆さんもあまり外に出られない今だからこそぜひ.
AmebaでAndroidエンジニアとして内定者バイトしてきた
はじめに
今回,CyberAgentの21卒内定者として内定者バイトに行かせていただきました.
僕の場合はAndroidエンジニアを志望しているため,Androidエンジニアとして行かせていただきました.
配属された部署はAmebaです.
このアプリ↓
このアプリ,10年くらい前から存在するらしくて,びっくりしました.
日本でiPhone 3G が発売された頃からあるみたいです.すごいですよね.
このようなアプリの実装に触れるのはとても貴重な機会だったと思います.
そのことを今回は書きます.
コロナウィルスの影響で途中からリモートワークになったりなどして,普段体験できない働き方も体験できたと思います.
本編
トレーナーさん
トレーナーさんはどくぴーさんです.
Twitterはコチラ→どくぴー (@e10dokup) | Twitter
DroidKaigi2020でも登壇されて,動画がYouTubeに上がっています. まだ見てない人はぜひ見てみてください.
目標
今回内定者バイトに参加するに当たり,定めた目標は下の3つです.
- 雰囲気でAndroid開発しているとこを少しでも克服する
- 規模の大きいプロジェクトのコードを少しでも自分なりに解釈できるようになる
- 帰ってからも個人開発のときに応用して成長できるように解釈して学んだことを持ち帰る
僕がAndroid開発に興味を持って始めたのは去年の夏に行われたCA Tech Dojoというインターンシップでした.
そこからよく開発で使われる技術などを主に個人開発で触ってきていたのですが,
「なんでこう動くんだろう」,「この部分のコードはなんで書かないといけないんだろう」などわからないままとりあえず書くということが多々あって雰囲気でAndroid開発してる部分が多数ありそこを少しでも克服したいという気持ちがありました.これが1つ目の目標です.
2つ目の目標は,DroidKaigiのカンファレンスアプリのコードを見たときに規模感に圧倒されて全く読むことができなかったという経験から世の中にリリースされている規模の大きいプロジェクトのコードを通して慣れる+少しでも規模の大きいコードを読むアレルギー的なのが克服できればいいなというとこから来ました.
3つ目の目標は,普段このような規模の大きいコードを元に誰かに教えてもらえたり,わからないことを現役のエンジニアの人に直接聞ける環境がなかったので,教えてもらったことは自分の言葉で解釈して持ち帰って応用できるまで理解しようというものです.
チーム
AmebaのAndroidチームは7人でiOSチームよりも多くてびっくりしました.iOSからAndroidに行く人がいたり人の入れ替えは頻繁にあるみたいですが,僕が行ったタイミングではこのような感じでした.
これは僕が感じた感想なのですが,全員ノリが若いというか頻繁にボケる人がたくさんいてツッコミを入れる人がおらず,ボケっぱなしみたいなことが多々あったりしてとても楽しい雰囲気のチームでした笑
環境
ここでは開発環境的なことについて僕が印象に残ったことを書こうと思います.
入る前までは,「自分がもし実装でわからないことがあったらどういうふうに聞くんだろう.トレーナーさんの席まで行って直接聞くのかな…」や,「業務をするときのトレーナーさんとの席の距離感はどんな感じなんだろう」という面の環境で不安な部分が少しありました.
実際,わからないことがあったらそれを聞くSlackのチャンネルがあったり,トレーナーさんも自分の席の目の前にいて,席との間に仕切りみたいなものも無く,すぐに声をかけることができて,なにか聞くときに躊躇するような環境では全くありませんでした.
また,僕のトレーナーだったどくぴーさんは何か聞くと,席のすぐ隣にあったホワイトボードを使って丁寧に教えてくれて,自分が何か引っかかって理解できていないときは「どこが引っかかってる?」など聞いてくれて自分が完全に理解するまで教えてくれました.
トレーナーさんのサポートはとても手厚かったです.
Slackのチャンネルについてですが,他にも印象的だったのは,業務に関係のないことも話せる雑談チャンネルがあってチームの人と仲良くなれたのもとてもいい環境だったなと思います.
やったこと
Amebaは歴史が長いアプリですが,Kotlin化などはかなり進んでいてマルチモジュール化など積極的に採用している印象でした.
その一環として,まだマルチモジュール化されていない部分のマルチモジュール化とMVPで実装されていた部分をMVVM化するタスクをやらせてもらいました.
そのタスクを終えたあとは,MockKを用いたViewModelのテストを実装するタスクをやり,今まで触れたことのない部分に触れることができました.
リファクタのタスクの次は,どくぴーさんが担当していた新しいネイティブ化させる画面のエラー時のレイアウト表示をGroupieのアイテムとして表示するタスクをやらせてもらいました.
このタスクはPRを出す前にコロナウィルスの影響で一時内定者バイトが中断したため,最後までやりきることができませんでしたが,デザイナーの人が作ったデザインをもとにxmlでレイアウトを作るという個人開発にはないフローを体験することができて楽しかったです.
学び3選
1: BehaviorとStateを使ったLiveData管理
ViewModelにLiveDataを持たすときですが,今までの自分の実装だと,View側に通知したい値の数だけ宣言していました.
しかしそうすると,ViewModelに定義するLiveDataの数も多くなりますし,View側でobserveする数も多くなってしまいます.
そこで,同じ画面上でユーザによって値が複数回変更されるような値はStateというdata classでまとめて定義して,Toastをだすようなエラーが起きたかや,ログインが成功して画面遷移するかどうかのような値をViewに通知したいというときはBehaviorというsealed classでまとめて管理するというやり方をAmebaではしていました.
すぐに自分が趣味で開発しているアプリにも組み込めそうで,きれいにコードを書けることに繋がりそうでとても直感的に勉強になったなという学びでした.
このやり方をするとテストを書くときも,「こういうモックを作ったらこのStateのLiveDataが変わるはず」みたいな感じで「Stateの中のこの値」というような感じで書けていろんなStateやBehaviorのテストを書くとなると,他のテストと統一感が出てテストコードも見やすくかけるなと思いました.
2: Groupieを使ったリスト表示
今までGroupieを使って単純なサーバから取得したデータをリスト表示するということはやったことがあったのですが,データが空のときはその時専用のアイテムを表示したり,リストAの次にリストBを表示したりなど,複雑なリスト表示を作ったことがなく,いつかやってみたいなと思っていました.
そこで,今回の内定者バイトでまさにその部分に触れることができました.
モバイルアプリは狭い画面スペースで色々な情報を載せるため,リスト表示が多用されるので今回のバイトでその部分を手を動かしながら触れることができたのはとても良かったです.
3: MockKを使ったテスト
今までテストは一度も触れたことがなく,今回のバイトでも触れてみたいけどまだまだ自分には縁のないものかなと思っていたのですが,リファクタリングのタスクをする中で触れさせてもらいました.
たとえば,View側でエラー時はToastを出すという実装をしたとき,じゃあ本当にエラーが起きる挙動になったときにToastが出るか調べなければいけないというときにテストを書きます.
このとき,エラーが出るであろう処理をモックして実際にエラーを返すようにします.
このモックを作るというような考え方が今までしたことがなくて難しかったです.
モックしようとしてるメソッドの中身までモックしてしまったり,モックしようとしてるメソッドが同じクラス内にいてうまくモックできなかったりかなりハマりましたが,とてもいい学びでした.
ランチ
どくぴーさんにはランチもたくさん連れて行ってもらいました.
渋谷はおいしいごはん屋さんがいっぱいあって入社前から会社の近くのおいしいごはん屋さんを知れるのもとてもいいなと思いました.
いっぱい連れて行ってもらってありがとうございました!
今見返したらほんとに全部美味しそう…
完全飯テロ
さいごに
去年の夏にCAのインターンに参加してCAと関わるようになり,インターン後も自分はAndroidの分野をまずは得意になろうと決めて最初はCAのTech Jobという長期インターンシップに次は行くこと目標にAndroid開発を続けていましたが,選考を通して内定をいただき,Androidに興味を持ったCAでAmebaのような巨大なソースコードを読みながら与えられたタスクをこなしてPRを出せるほどまで成長を感じられてとても嬉しかったです.
Androidを始めてから半年ちょっとくらいで自分の成長を身を持って感じられていい経験でした.
次また内定者バイトに来ることがあったら更に成長を感じられるように修行したいと思います.
AmebaのAndroidチームの皆さん本当にお世話になりました!ありがとうございました!
Amebaでは最近Pickという新しい機能ができました.
自分のブログ記事にAmazonなどのリンクを貼り付けてそれを通して読者が購入するとその商品の○%の報酬が自分に入るというアフィリエイト機能です.
ぜひみなさんやってみてください.
詳しくは