TRY ANDROID DEV

Android アプリ開発のコーディングネタ。あとエンジニアとしての活動

DroidKaigi 2019 official appを読んでみる④:Kotlin-coroutines-channel

背景

  • DroidKaigi 2019 のofficial appが公開されている:参照
  • せっかくだしコントリビュートしたいなと思ったけど思った以上に皆新しい技術使っててよくわからない。。
  • とりあえず一つずつ理解することにする。

Storeを読む

ついにMainActivityから脱出してStoreの処理を見ます。
DroidKaigi2019はFluxベースで開発されているため、以下のような処理順になるようです。

  1. Activity / FragmentからActionCreatorに対してアクションのメッセージを送る
  2. ActionCreatorはデータ等取ってきて、Dispatcherに渡す
  3. DispacherはStoreに通知する。
  4. StoreはActivity / Fragmentに通知する

https://github.com/DroidKaigi/conference-app-2019/blob/master/images/architecture.png?raw=true

conference-app-2019から引用しています。

こんな構成だとクラス間の依存関係がめちゃめちゃになりそうですが、どうやらkotlin-coroutines-channnelというものを利用してクラス間で情報を渡しているようです。今回はこのKotlin-croutines-channelについて調べてみます。

kotlin-coroutineのchannels

基本の使い方

まずは公式ドキュメント1を読みます。
channelはBlockingQueueによく似ているらしいです。違いは一時停止と再開があるところだそう。

    fun getValue() = runBlocking {
        var value = ""


        //sampleStart
        val channel = Channel<Int>()

        // 5回sendするが、非同期処理のため処理を待たないで次に行く
        launch {
            for (x in 1..5) channel.send(x * x)
        }
        // receive()は一つずつsendされた情報を受け取る
        // なお、ここで中断されるため、sendされない限り次の処理に進まない。非同期でないところに注意。
        repeat(5) {
            value += channel.receive()
            value += System.lineSeparator()
        }
        println("Done!")
    }

つまりsendした情報をreceiveで受け取るという流れみたいですね。 これだけだと何に使うんだって感じです。

パイプライン

もう一つ、パイプラインという使い方があるようです。
これはchannelとchannelを繋ぐという意味ですね。
例えば以下の二つのchannelを考えます。

channel 1 : produceNumbers()

// produceはchannelを渡すという意味。
// このchannelは数値を垂れ流す。
fun CoroutineScope.produceNumbers() = produce<Int> {
    var x = 1
    while (true) send(x++) // infinite stream of integers starting from 1
}

channel 2 : square()

// このchannelはパラメータのchannelからIntを受け取り、二乗するchannel
fun CoroutineScope.square(numbers: ReceiveChannel<Int>): ReceiveChannel<Int> = produce {
    for (x in numbers) send(x * x)
}
fun main() = runBlocking {
    val numbers = produceNumbers() // Intを垂れ流すChannelを取得
    val squares = square(numbers) // 二乗するChannel
    for (i in 1..5) println(squares.receive()) // 5回だけ受け取る
    println("Done!") 
    coroutineContext.cancelChildren() // このCoroutine以下すべてのCoroutineをストップ
}

コメントに記載している通り、チャンネルから発行されてくる値を受け取って、別のチャンネルでまた発行していくということが可能です。これをパイプラインと呼ぶみたいです。

DroidKaigiではどのように使っているのか

Flubベースの話に戻ると、Channelを利用すれば以下のようなことが可能っぽいです。

  1. ActionCreatorとStoreは共通のDispatcherインスタンスを持つ
  2. ActionCreatorが通信などの処理を終えて、Dispatcherにchannelを通じてデータをsendする
  3. StoreはDispatcherを通じてChannelからデータが来ないか購読している。データが来たら処理して画面表示。

最初Dispatcherいらなくね?と思ったのですが、ないとStoreは対応するActionCreatorをすべて知ってないとダメですね。

Channelのサンプルプログラム

5つのクラスを利用しました。

Action

/**
 * Actionクラス
 * - sealed : enum拡張みたいなもの。
 * sealedクラス単体ではインスタンス化できない。
 * またメンバーにはサブクラスを持つことができるので、データクラスも持てる。
 * 今回は「どんな処理かを示しつつデータも渡したい」のでsealed classを使う。
 */
sealed class Action {
    data class TestActionFinished(val str : String): Action()
}

ActionCreator

// まだChannelって実験段階なので、アノテーションで実験段階APIって書いてあげる必要があるみたい。
@ExperimentalCoroutinesApi
class ActionCreator(val dispatcher: Dispatcher) {
    fun load() {
        dispatcher.launchAndDispatch(Action.TestActionFinished("Test"))
    }
}

Dispatcher

@ExperimentalCoroutinesApi
class Dispatcher {

    // ActionCreatorからデータを送るChannel
    private val actions = BroadcastChannel<Action>(Channel.CONFLATED)

    // StoreがこのChannelを購読する
    val receiveChannel : ReceiveChannel<Action> = actions.openSubscription()

    fun launchAndDispatch(action: Action) {
        GlobalScope.launch {
            actions.send(action)
        }
    }

store

@ExperimentalCoroutinesApi
class Store(val dispatcher: Dispatcher) {
    val sendData = dispatcher.receiveChannel
}

MainActivity

@ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity() {

    // dispatcherはとりあえずActivityに置いているけど、
    // 別のところから供給する方が依存関係的にいいと思います。
    private val dispatcher: Dispatcher = Dispatcher()

    private val actionCreator: ActionCreator by lazy {
        ActionCreator(dispatcher)
    }

    private val store: Store by lazy {
        Store(dispatcher)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // load 実施
        actionCreator.load()

        // ActionCreatorのload()の結果がDispatcherを通じてstoreに返却されているので、storeでレシーブしてtext表示する
        // LiveDataを利用するとよりよい感じでデータ反映できそう。
        GlobalScope.launch {
            val action = store.sendData.receive()
            when (action) {
                is Action.TestActionFinished -> findViewById<TextView>(R.id.textView).text = action.str
            }
        }
    }

}

実行結果

testが表示された。

f:id:off2white:20190117155616p:plain

感想

  • ひとまずChannelを利用したFluxの動作は理解できてきた。
  • ただ、もうちょっと理解するスピードをあげたいなぁと思う。理解するより技術が進むスピードの方が早いとかこわい。
  • DroidKaigi 2019 アプリだと拡張関数を使ってLiveDataに変換してるみたいだけど仕組みがわかんない...
  • あとKotlin - coroutine使う上でrunBlockingなりlauchなり使う場所ってどこが適切なんだろう。

今回作成したサンプルコード

github.com

参照

DroidKaigi 2019 official appを読んでみる③:Dagger2 Android Support

背景

  • DroidKaigi 2019 のofficial appが公開されている:参照
  • せっかくだしコントリビュートしたいなと思ったけど思った以上に皆新しい技術使っててよくわからない。。
  • とりあえず一つずつ理解することにする。

コンテンツの実装を見ようとした。

Navigationを理解したので、早速各処理をみていこうと思ったのですが、

    @Inject lateinit var userActionCreator: UserActionCreator
    @Inject lateinit var systemStore: SystemStore
    @Inject lateinit var userStore: UserStore

上記StoreクラスとActionCreatorクラスがどこでインジェクトされているかわからない。。 Activity なら以下のような記述があると思ってたのに。。

    ((SampleApplication) getContext().getApplicationContext())
        .getApplicationComponent()
        .activity(this)
        .build()
        .inject(this);

どうやらDagger2のAndroid Supportを利用することで、今までOSのライフサイクルによって生成されておりInjectできなかったActivityやFragmentもInjectできるようになったそうです。まだMainActivityから出られない。。

build.gradleを編集

dagger-android-support 2.20を追加します。 なお、kotlin-kaptのplugin入れるの忘れないこと。

    implementation 'com.google.dagger:dagger:2.20'
    implementation 'com.google.dagger:dagger-android:2.20'
    implementation 'com.google.dagger:dagger-android-support:2.20'

実装してみる

今回の目標は、サンプルCoffeeMakerをActivityにInjectとすることです。

CoffeeMaker

class CoffeeMaker @Inject constructor(private val heater: Heater) {

    fun getTemperature() : String {
        return heater.getTemperature()
    }
}

Heater

class Heater @Inject constructor() {

    fun getTemperature() : String {
        return "38度"
    }
}

MainActivity

class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var coffeeMaker: CoffeeMaker < ここにInjectしたい。

    private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
        when (item.itemId) {
            R.id.navigation_home -> {
                message.setText(R.string.title_home)
                return@OnNavigationItemSelectedListener true
            }
            R.id.navigation_dashboard -> {
                message.setText(R.string.title_dashboard)
                return@OnNavigationItemSelectedListener true
            }
            R.id.navigation_notifications -> {
                message.setText(R.string.title_notifications)
                return@OnNavigationItemSelectedListener true
            }
        }
        false
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
    }
}

Android Type(Activity, Fragmentなど)のメンバーにインジェクションする仕組み

正直ソースコードをみててもあんまりわからなかった。。。(途中で気力が折れた)
なんとなくですが、以下の流れみたいです。

  1. ApplicationクラスにDispatchingAndroidInjectorを持たせておく
  2. Activity 起動後、ApplicationクラスがDispachingAndroidInjectorに対象のActivityを渡す
  3. するとDispachingAndroidInjectorが、うまいことActivityのメンバーにInjectしてくれるAndroidInjector.Factoryを探してくれる。
  4. このAndroidInjector.FactoryがAndroidInjectを生成できる。
  5. AndroidInjectにActivityを渡すとメンバーにInjectできる

なのでまずはAndroidInjector.Factoryを作成するところからですね。

ActivityModuleを作成する

AndroidInjection.Factoryを生成するには、二つ方法があるみたいです。 ただ特にパラメータを設定したいとかでないのであれば、MainActivityを返却するメソッドに@ContributesAndroidInjectorをつけてあげるだけでよいみたいです。

@Module
abstract class ActivityModule {

    @ContributesAndroidInjector
    abstract fun contributeMainActivityInjector(): MainActivity
}

ApplicationComponentを作成する

ApplicationクラスにDispatchingAndroidInjectorを供給するため、ApplicationComponentを作成します。 ActivityModule::classは先ほど作成したモジュールですが、 AndroidInjcectionModule::classはDaggerが用意している「Dagger フレームワークの使いやすさを確保するためのBindingを含んでいる」moduleだそうです。
なんのこっちゃですが、必須なので入れましょう。

@Component(modules = [AndroidInjectionModule::class, ActivityModule::class])
interface ApplicationComponent {
    fun inject(application: SampleApplication)
}

Applicationクラスをカスタマイズする

カスタムApplicationクラスを作成して、DispatchingAndroidInjectorを注入します。 このHasActivityInjectorを実装して、activityInjcetor()でdispatchingAndroidInjectorをDaggerが取り出せるようにしましょう。 なお、先ほど作ったComponentのinjectを読んでおけばdispatchingAndroidInjectorは注入されます。

class SampleApplication : Application(), HasActivityInjector {

    @Inject
    lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity>

    override fun onCreate() {
        DaggerApplicationComponent.create().inject(this)
        super.onCreate()
    }

    override fun activityInjector(): AndroidInjector<Activity> {
        return dispatchingAndroidInjector
    }

}

Activityに注入する

あとはAndroidInjection.inject(this)をonCreateで呼べば完成です。 AndroidInjcectionがApplicationクラスからAndroidInjectorを取ってきてinjectしてくれるみたいです。

class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var coffeeMaker: CoffeeMaker

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        AndroidInjection.inject(this)

        findViewById<TextView>(R.id.testText).text = coffeeMaker.getTemperature()
    }
}

とはいえAndroidInjection.inject(this)は毎回書く必要がある?

DroidKaigi アプリをみると。。。書いてない!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setSupportActionBar(binding.toolbar)
        setupNavigation()
        setupStatusBarColors()

        systemStore.errorMsg.changed(this) { message ->
            val messageStr = when (message) {
                is ErrorMessage.ResourceIdMessage -> getString(message.messageId)
                is ErrorMessage.Message -> message.message
            }
            Snackbar.make(binding.root, messageStr, Snackbar.LENGTH_LONG).show()
        }
        userStore.registered.changed(this) { registered ->
            if (!registered) {
                userActionCreator.load()
            }
        }
    }

DaggerApplication, DaggerActivityを継承する

上記継承したクラスだと、今回手動で書いていた大部分をやってくれるらしい。

なので以下のように修正します。

ApplicationComponent

Component(modules = [AndroidInjectionModule::class, ActivityModule::class])
interface ApplicationComponent : AndroidInjector<SampleApplication>  // injectとか取っ払ってAndroidInjector<SampleApplication>を継承する

SampleApplication

class SampleApplication : DaggerApplication() {
    // injectはDaggerApplicationがやってくれるので、AndroidInjectorを渡すだけ。
    override fun applicationInjector(): AndroidInjector<SampleApplication> {
        return DaggerApplicationComponent.builder().build()
    }
}

MainActivity

class MainActivity : DaggerAppCompatActivity() {

    @Inject
    lateinit var coffeeMaker: CoffeeMaker // DaggerActivityがinjectしてくれるのでinjectは記載しなくてよい

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<TextView>(R.id.testText).text = coffeeMaker.getTemperature()
    }
}

これでようやくDroidKaigiのDIが見えてきました。

感想

  • この書き方で注入作業もActivityから消えたので、よりonCreateが本質的な処理しか無くなってきている。素敵。
  • そろそろMainActivityから脱出したい。。

サンプルプログラム

github.com

DroidKaigi 2019 official appを読んでみる②:Navigation

背景

  • DroidKaigi 2019 のofficial appが公開されている:参照
  • せっかくだしコントリビュートしたいなと思ったけど思った以上に皆新しい技術使っててよくわからない。。
  • とりあえず一つずつ理解することにする。

アプリ起動から順に追っていく

DroidKaigi 2019のReadmeに記載されている通り、 Fluxベースのマルチモジュール構成になっているよう。

https://github.com/DroidKaigi/conference-app-2019/blob/master/README.md

早速起動アクティビティを確認しようとManifestを確認していくと、ActivityはMainActivity一つしかなく、表示画面はすべてFragmentで構成されている模様。FragmentManagerに殺された経験がある身としてはなんて構成なんだ...と震えていたのですが、どうやらNavigationという物を使っているようです。今回はこのNavigationを調査していきます。

まずはNavigationの公式ドキュメントを見てみる

Navigationはアプリの目的地を案内するためのフレームワークらしいです。要するに画面遷移系を楽にしてくれるみたい。 名前からするとNavigation Drowerの親戚かと思ったけどどちらかというとiOSのSegueに近いのかな。

公式サイトには以下の利点が書いてありました。

Navigation Architecture Componentには、次のような他の多くの利点があります。

- フラグメントトランザクションの処理
- デフォルトでUpとBackアクションを正しく処理する
- アニメーションとトランジションのための標準化されたリソースを提供する
- 第一級の操作としてディープリンクを扱う
- 最小限の追加作業で、ナビゲーションパネルや下部ナビゲーションなどのナビゲーションUIパターンを含める
- ナビゲート中に情報を渡すときにタイプセーフを提供する
- Android StudioのNavigation Editorを使用したナビゲーショングラフの視覚化と編集

・・・もうフラグメントトランザクションを楽にしてくれるというのであればその時点で使いたくなりますよね! とりあえず実装してみないことには理解が進まないので、早速実装してみることにします。

Navigation Code Labなるものが存在しているみたいなので、これ通りに順を追って開発してみます。

Navigationを実装してみる(サンプル)

build.gradleにdependenciesを追加する。

以下のように追加します。

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    // Navigation
    implementation Dep.AndroidX.Navigation.ui
    implementation Dep.AndroidX.Navigation.uiKtx
    implementation Dep.AndroidX.Navigation.fragment
    implementation Dep.AndroidX.Navigation.fragmentKtx
    implementation Dep.AndroidX.Navigation.runtime
    implementation Dep.AndroidX.Navigation.runtimeKtx
}
    object AndroidX {
        object Navigation {
            val version = "1.0.0-alpha08"
            val runtime = "android.arch.navigation:navigation-runtime:$version"
            val runtimeKtx = "android.arch.navigation:navigation-runtime-ktx:$version"
            val fragment = "android.arch.navigation:navigation-fragment:$version"
            val ui = "android.arch.navigation:navigation-ui:$version"
            val fragmentKtx = "android.arch.navigation:navigation-fragment-ktx:$version"
            val uiKtx = "android.arch.navigation:navigation-ui-ktx:$version"
        }
    }

公式を見ると、common, ui, fragment, runtimeとあるようですが、commonとruntimeの違いがわからない。。
とりあえずDroidKaigi2019で選択されているruntimeを追加しました。

Navigationの基本的な3つの要素

Navigationは以下の3つの要素で構成されるようです。

  1. Navigation Graph (XML Resource) : すべての遷移先が記載されているxml
  2. NavHostFragment (Layout XML View) : レイアウトに追加する特殊なwidgetだそうです。コンテナFragmentみたいな感じです。
  3. NavController(Kotlin/Java object) : Navigation Graph内のどこにいるか、currentPositionを持つクラスだそうです。

このNavigationの基本的な考え方は以下のようです。

  1. NavControllerで次の遷移先をNavigation Graphから示す
  2. それをNavHostFragmentが受け取って画面遷移する

Navigation Graph

アプリ内における対象の目的地(Destination)から到達可能な目的地を視覚的に表示できる新しいリソースタイプです。 ここでいう目的地というのはActivityやFragmentが該当するようです。要するにiOSのstoryboard的なやつだと思います。

navigation.xmlAndroid Studio -> Design で表示すると以下のような感じです。
注:Navigation Code LabにはAndroid Studio 3.2 以上ならNavigation ツールが使えると書いてあったけど、3.2では表示されませんでした。Android Studio 3.3 にあげたほうが無難かもしれません。(Android Studio 3.3のWhats new に書いてあったし。)

f:id:off2white:20190115204314p:plain

画面のようなものがDestination、矢印がActionというそうです。

このxmlは以下のようになります。

<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2018 The Android Open Source Project
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  -->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:app="http://schemas.android.com/apk/res-auto"
            xmlns:tools="http://schemas.android.com/tools"
    app:startDestination="@+id/home_dest">
    <fragment
        android:id="@+id/home_dest"
        android:name="com.example.android.codelabs.navigation.HomeFragment"
        android:label="@string/home"
        tools:layout="@layout/home_fragment">

        <!-- TODO STEP 7.1 - Add action with transitions -->
        <!--<action-->
            <!--android:id="@+id/next_action"-->
            <!--app:destination="@+id/flow_step_one_dest"-->
            <!--app:enterAnim="@anim/slide_in_right"-->
            <!--app:exitAnim="@anim/slide_out_left"-->
            <!--app:popEnterAnim="@anim/slide_in_left"-->
            <!--app:popExitAnim="@anim/slide_out_right" />-->
        <!-- TODO END STEP 7.1 -->

    </fragment>

    <fragment
        android:id="@+id/flow_step_one_dest"
        android:name="com.example.android.codelabs.navigation.FlowStepFragment"
        tools:layout="@layout/flow_step_one_fragment">
        <argument
            android:name="flowStepNumber"
            app:argType="integer"
            android:defaultValue="1"/>

        <action
            android:id="@+id/next_action"
            app:destination="@+id/flow_step_two_dest">
        </action>
    </fragment>

    <fragment
        android:id="@+id/flow_step_two_dest"
        android:name="com.example.android.codelabs.navigation.FlowStepFragment"
        tools:layout="@layout/flow_step_two_fragment">

        <argument
            android:name="flowStepNumber"
            app:argType="integer"
            android:defaultValue="2"/>

        <action
            android:id="@+id/next_action"
            app:popUpTo="@id/home_dest">
        </action>
    </fragment>

    <!-- TODO STEP 4 Create a new navigation destination pointing to SettingsFragment -->
    <!--<fragment-->
        <!--android:id="@+id/settings_dest"-->
        <!--android:name="com.example.android.codelabs.navigation.SettingsFragment"-->
        <!--android:label="@string/settings"-->
        <!--tools:layout="@layout/settings_fragment" />-->
    <!-- TODO END STEP 4 -->

    <fragment
        android:id="@+id/deeplink_dest"
        android:name="com.example.android.codelabs.navigation.DeepLinkFragment"
        android:label="@string/deeplink"
        tools:layout="@layout/deeplink_fragment">

        <argument
            android:name="myarg"
            android:defaultValue="Android!"/>
        <!-- TODO STEP 12.1 - Add a deep link to www.example.com/{myarg}/ -->

        <!--<deepLink app:uri="www.example.com/{myarg}" />-->

        <!-- TODO END STEP 12.1 -->
    </fragment>
</navigation>

属性については以下のようになっています。

  • navigation: Navigation Graphのルート
    • navigationは少なくともActivityかFragmentのどちらか一つは含む
    • app:startDestination には起動時に表示する目的地を指定する
  • android:id : 対象目的地のid
  • android:name : 対象目的地が選択された際にインスタンス化するActivity/Fragmentのクラス名を指定する
  • tools:layout : Designに表示するレイアウトファイルの指定する

NavHostFragment

画面遷移の設計図のようなNavigation Graphはできたけれど、それを使うためにはNavHostFragmentという特別なWidgetが必要です。
NavHostFragmentはコンテナみたいなもので、このNavHostFragmentの中身が入れ替わっていくことで目的地の移動が表現されるみたい。 このNavHostFragmentの推奨利用方法は、一つのActivityにNavHostFragmentを含めることとされています。
以下の図(Navigation Gode Labから引用)のような感じです。

f:id:off2white:20190115213724p:plain

これを満たすActivityの単純なレイアウトは以下のようになるそうです。

<LinearLayout
    .../>
    <androidx.appcompat.widget.Toolbar
        .../>
    <fragment
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:id="@+id/my_nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/mobile_navigation"
        app:defaultNavHost="true"
        />
    <com.google.android.material.bottomnavigation.BottomNavigationView
        .../>
</LinearLayout>
  • android:name="androidx.navigation.fragment.NavHostFragment" でこのFragmentはNavHostFragmentとなる
  • app:NavGraph : ここに先ほど作成したnavigation.xmlを指定する
  • app:defaultNavHost : これはバックキーの動作をNavHostFragmentに紐づけるかどうかを指定するフラグ。

NavController

ここまで用意すればあとはユーザーアクションに応じて移動処理を実施するのみ。以下のように記載できます。

   val button = view.findViewById<Button>(R.id.navigate_destination_button)
   button?.setOnClickListener {
         findNavController().navigate(R.id.flow_step_one_dest)
   }

これだけです。まじかよって気分です。
仮にnavigateがActivityだった場合は、startActivity()と同じような処理がされるそうです。すごいですね。

ただActivity, Fragment, ViewのどこでNavController()を呼ぶかについてメソッドが変わるので注意してね、だそうです。

  • Fragment.findNavController()
  • View.findNavController()
  • Activity.findNavController(viewId: Int)

感想

  • 画面遷移がグッと楽になるNavigation。今後ぜひ使っていきたい。けどまだalpha版。。
  • やたら画面数のある大規模アプリを開発することになったらどうなるんだろう。storyBoardみたいに重くて開けないとかないといいな。。
  • あとは記載してないけどsafeArgsとかアニメーションとかDeepLinkとかの機能がある。つよい。
  • Code Labがすごいわかりやすかった。ありがとうございます。

DroidKaigi 2019 official appを読んでみる①:Gradle Kotlin DSL

背景

  • DroidKaigi 2019 のofficial appが公開されている:参照
  • せっかくだしコントリビュートしたいなと思ったけど思った以上に皆新しい技術使っててよくわからない。。
  • とりあえず一つずつ理解することにする。

DroidKaigi 2019 official app のbuild.gradle

build.gradleをみてみると以下のように記載してありました。

dependencies {
 ・
 ・
 ・
    implementation Dep.Kotlin.stdlibJvm
    api Dep.Kotlin.coroutines
    implementation Dep.Kotlin.androidCoroutinesDispatcher
    implementation Dep.OkHttp.okio
}

調べてみるとKotlin-DSLとbuildSrcを利用したdependencyの管理みたいです。
もうここからよくわからないので調べてみます。

DSLとは

ドメイン特化言語Domain Specific Language)のことで、 特定の問題解決のために専用の構文が用意されているようなことを言うみたい。

Gradleの例に当てはめて考えてみると、 今まで利用されていたのは「Groovy-DSL」、つまりビルドスクリプトを書くのにGroovy言語が専用に構文を用意してくれていたよう。

今回は、ビルドスクリプトを書くためにkotlinが専用の構文を用意してくれたのでこれを使ってみようということですね。

Gradle Kotlin DSL

DroidKaigi公式をみてみると、次のようにしています。

  1. build.gradleは通常通りGroovyで記載。
  2. ただし、dependenciesはbuildSrc/Dep.ktにまとめており、build.gradleはこちらを参照している模様。
dependencies {
    implementation Dep.AndroidX.Navigation.runtime
    implementation Dep.AndroidX.Navigation.fragment
    implementation Dep.AndroidX.Navigation.ui
    implementation Dep.AndroidX.Navigation.runtimeKtx
    implementation Dep.AndroidX.Navigation.fragmentKtx
}

buildSrc:dependencies/Dev.kt

object Dep {
    object AndroidX {
        object Navigation {
            val version = "1.0.0-alpha08"
            val runtime = "android.arch.navigation:navigation-runtime:$version"
            val runtimeKtx = "android.arch.navigation:navigation-runtime-ktx:$version"
            val fragment = "android.arch.navigation:navigation-fragment:$version"
            val ui = "android.arch.navigation:navigation-ui:$version"
            val fragmentKtx = "android.arch.navigation:navigation-fragment-ktx:$version"
            val uiKtx = "android.arch.navigation:navigation-ui-ktx:$version"
        }
    }
}

これはbuildSrcでのdependences管理方法で、gradleの公式ドキュメントに載ってました。
Organizing Gradle Projects

この管理をするとIDEの補完サポート(オートコンプリート)が受けられるみたいですね!
マルチモジュール化でどんどん依存関係が煩雑になってくるので、これはありがたいです。

サンプルを作成してみる。

実際に作成してみました。

  1. file -> new -> package で「buildSrc」パッケージを作成
  2. ①の構成になるようにファイルを作成
  3. build.gradle.ktsには②のように記載
  4. Dep.ktは自分の管理しやすいように。今回はDroidKaigiを真似て作成。
  5. Syncする
  6. Dep.ktを使いたいモジュールのbuild.gradleにimport dependencies.Depを記載
  7. Dep.ktのオブジェクトを参照可能! f:id:off2white:20190115132749p:plain

①:パッケージ構成

├── build.gradle.kts(root)
├── buildSrc
│   ├── build.gradle.kts
│   └── src
│       ├── main
│       │   └── dependencies
│       │              └── Dep.kt

②: build.gradle.kts

plugins {
    `kotlin-dsl`
}

感想

できたけど、なんか以下のように補完してもらってもめっちゃ候補出てくる... 思ってたのと違う... f:id:off2white:20190115132150p:plain

誰かいい感じの設定ご存知でしたら教えてください。

ハマったところ

  • buildSrcを通常のandroid library として生成して'com.andriod.library' not found が発生。

    • buildSrcディレクトリはインクルードビルドで、gradleがbuildSrcを見つけると勝手にビルドしてくれる。適当な名前じゃダメみたい。
    • そんな特殊なディレクトリなので、モジュール扱いするとエラーが発生する。でもnot found エラーにすることなくない...?
    • DroidKaigi アプリみてこんな感じだろうと思い込みで作業したら1時間のハマり。公式を読めとあれほど。。。
  • ktsのファイル名をgradle.ktsにしたら、他モジュールで全然参照できなかった。

    • build.gradle.ktsでないとダメみたい。
  • sync すると Failed to notify project evaluation listener. が発生

    • 当初はandroid libraryモジュールとして作成していたのでSettings.gradleにモジュール名が含まれており、これが原因だった模様。
    • Settings.gradleから名前を削除したらビルドできた。

今回作成したサンプルコード

github.com

2019/1/16 追記

以下の環境にしたらbuildSrcのオブジェクトが参照できなくなった。。。

謎です。わかったらまた追記します。

Kotlinでprivate constructorを利用する(companion object)

背景

  • static public な文字列を定義したくなった。(途中で思い返して異なる方法をとったけれど。)

内容

以下のような形でdomainに文字列を設定していたが、利用可能なドメインは限られているため、いくつかの固定文字列から選択させる形を取ろうとした。

fun getNovelList(word: String): List<NovelIntroduction> {
        val webSite = DaggerDomainComponent.builder()
            .domain("https://api.syosetu.com/")  < ここを定数定義する
            .build() // DomainComponentを生成する
            .inject() // inject実施。repositoryを注入する。

        return webSite.search(word)
    }

でもよく考えたらこうしても別に他のStringを注入できるし、あんまり意味ない。 結論として以下のようにprivate constructorを利用して、生成できるドメインを制限した。

class WebSite private constructor(
        val domain: String,
        private val searchRepository: ISearchRepository = DaggerRepositoryComponent.create().makeSearchRepository(),
        private val getPageRepository: IGetPageRepository = DaggerRepositoryComponent.create().makeGetPagePepository()
) {

    companion object {
        fun accessNarou(): WebSite {
            return WebSite("https://yomou.syosetu.com/")
        }
    }

    fun search(word: String): List<NovelIntroduction> {
        return searchRepository.search(word)
    }

    fun getIndex(ncode: String): Single<Contents> {
        return getPageRepository.getIndex(ncode)
    }
}

private constructorの実装としては以下の2手順で良い。

  1. constructorにprivateをつける
  2. companion object で自身を返却するメソッドを作成する。

ここでcompanion object がjavaで言う所のstatic なメソッドの指定となる。
ただし、javaのstatic宣言とは異なり、実体オブジェクトのインスタンスメンバーなのでinterfaceを実装できるらしい。
(今の所用途があんまり見えないけど。。) インスタンス生成時に実体を選択できるのはDI的に良いのかも。

javaのstaticにしたい場合は@JvmStaticというアノテーションをつけるといいらしい。

参照

RecyclerViewのAdapterについて再度復習する

背景

  • 毎回RecyclerViewを利用する時に「どうやって作るんだっけ」と悩む
  • 根本的に理解し直す必要があるので再度復習する。

構成

登場人物は以下の4つ。 f:id:off2white:20190108094926j:plain

Adapter

RecyclerViewに表示するViewを生成・管理・紐付ける役割を持つ。

表示するViewを生成するためにはfindViewByIdを呼び出す必要があるが、毎回呼び出すとコストが高い。 そのため、ViewHolderで生成したViewを保持しておき、そのViewが表示されるタイミングでBindする仕組みとなっている。

  1. getItemCount()
    まずここでアイテムの数を把握する。
    基本的にはアイテム配列を持っているだろうから、そのsizeを返すことになる。

  2. getItemViewType(position :int) : int
    ここでポジションに対するViewType()を返却する。
    0番目のポジションはヘッダーを表示したいのであれば、HEADER定数を予め設定しておき返却する形になる。

  3. onCreateViewHolder(parent: ViewGroup, viewType: Int) : RecyclerView.ViewHolder(view)
    ここで、各アイテムのViewHolderを生成する。
    ViewTypeがパラメータにあるので、それに沿ったViewHolderを返却する

  4. onBindViewHolder(holder: ViewHolder, position: Int)
    RecyclerViewに対してViewHolderが紐づいた時に呼ばれる。
    ここでViewに対してItemの情報を紐付ける。
    このタイミングでタップ時のリスナーも設定したりする。

その他よく使うもの。

  1. notifyDataSetChanged()
    データ更新時に再描画を依頼するためのメソッド。

  2. @BindingAdapter(item)
    itemをdata bindingする際に利用するAnnotation Processer

まとめ

整理してみるとそこまで複雑じゃない。 でも忘れやすいのでたまに復習しようと思う。

Dagger2でDIする際、インスタンス生成時にパラメータを設定したい

背景

以下のクラスを考える。

f:id:off2white:20190104132506j:plain

ここではパラメータが二つあるが、それぞれ意味合いが異なる。

このクラスのインスタンスを生成すると以下のようになるが、意味合いが異なる二つのパラメータを同時に指定していて気持ち悪かった。

val website = WebSite("google.co.jp", NarouRepository())

そのため、以下のように実装したい。

  • repositoryはDagger2によって自動的に依存性を注入したい。
  • domainは生成時に指定したい。

環境

実装

以下の3クラスで構成する。

DomainComponent

@Component(modules = [RepositoryModule::class, ApiModule::class])
interface DomainComponent {

    @Component.Builder
    interface Builder {
        @BindsInstance
        fun domain(domain: String): Builder
        fun build(): DomainComponent
    }

    fun inject() : WebSite
}

WebSite

class WebSite @Inject constructor(val domain: String, private val searchRepository: ISearchRepository) {

    fun search(word: String): List<NovelIntroduction> {
        return searchRepository.search(word)
    }
}

ShowNovelListUseCase

class ShowNovelListUseCase {

    fun getNovelList(word: String): List<NovelIntroduction> {
        val webSite = DaggerDomainComponent.builder()
            .domain("https://api.syosetu.com/")
            .build()
            .inject()

        return webSite.search(word)
    }

}

解説

Dagger2でインスタンスにパラメータを設定する場合は@BindsInstantceを利用する。
また、パラメータを設定するためのBuilderクラスは@Component.Builderで指定できる。

    @Component.Builder
    interface Builder {
        @BindsInstance
        fun domain(domain: String): Builder
        fun build(): DomainComponent
    }

Builderパターンを用いるため、パラメータを設定するメソッドはBuilderを返却している。
また、injectメソッドがDIの発火元であることは変わらないため、build()は対象コンポーネントを返却するように実装する。
あとは通常のComponentクラスと同様である。

DI対象のWebSiteは通常のコンストラクタインジェクションの実装でよい。

class WebSite @Inject constructor(val domain: String, private val searchRepository: ISearchRepository) {

    fun search(word: String): List<NovelIntroduction> {
        return searchRepository.search(word)
    }
}

利用する側のUsecaseは以下のように実装する。
Componentクラスで実装したBuilderを用いてWebSiteインスタンスを生成する。

    fun getNovelList(word: String): List<NovelIntroduction> {
        val webSite = DaggerDomainComponent.builder()
            .domain("https://api.syosetu.com/") // 生成時にドメインを指定する
            .build() // DomainComponentを生成する
            .inject() // inject実施。repositoryを注入する。

        return webSite.search(word)
    }

これで生成時にパラメータを指定し、本質的でない依存性の注入のみDaggerに任せることができた。