みずきち日記

ひらすらプログラミング

EditText 中の任意の文字列に対してハイライト操作を行う

EditText に入力された文字列が存在したとして,任意の区間の文字列に対してハイライト表示したいケースを考えます. Twitterの文字数制限のUIみたいな感じです.

f:id:mzkii:20190224233829p:plain

TextView に対してハイライト処理するサンプルはいくつもありましたが, EditText に関するサンプルが少なく,少しハマったので紹介します.

まずはコードです.

入力可能な文字列の長さが INPUT_LIMIT 以内であれば,SUBMIT ボタンが押下可能になり,

INPUT_LIMIT を超えた場合は,超えた分の文字列をハイライトし,SUBMIT ボタンが押せなくなります.

class MainActivity : AppCompatActivity() {

    private val binding by lazy {
        DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
    }

    private val overLimitColorSpan = BackgroundColorSpan(Color.GRAY)
    private val defaultColorSpan = BackgroundColorSpan(Color.TRANSPARENT)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        with(binding) {
            submit.setOnClickListener { editText.text.clear() }
            editText.addTextChangedListener(object : TextWatcher {
                override fun afterTextChanged(s: Editable?) {
                    if (s == null) return
                    Spannable.Factory.getInstance().newSpannable(editText.text).apply {
                        if (s.length <= INPUT_LIMIT) {
                            removeSpan(overLimitColorSpan)
                            setSpan(defaultColorSpan, 0, s.length, getSpanFlags(defaultColorSpan))
                        } else {
                            removeSpan(defaultColorSpan)
                            setSpan(overLimitColorSpan, INPUT_LIMIT, s.length, getSpanFlags(overLimitColorSpan))
                        }
                    }.also {
                        val selectionStart = editText.selectionStart
                        val selectionEnd = editText.selectionEnd
                        editText.removeTextChangedListener(this)
                        editText.setText(it, TextView.BufferType.SPANNABLE)
                        editText.setSelection(selectionStart, selectionEnd)
                        editText.addTextChangedListener(this)
                    }
                    updateSubmit(editText.text)
                }

                override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
                override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
            })
        }
        updateSubmit("")
    }

    private fun updateSubmit(inputText: CharSequence) {
        with(binding) {
            val textDiff = INPUT_LIMIT - inputText.length
            val canSubmit = textDiff >= 0
            textCount.setTextColor(if (canSubmit) Color.BLACK else Color.RED)
            textCount.text = textDiff.toString()
            submit.isEnabled = canSubmit
        }
    }

    companion object {
        private const val INPUT_LIMIT = 10
    }
}

TextWatcher は,EditText に入力されている(された / あるいはされる前の)文字列の状態を監視するクラスです.今回は 入力後の文字列の状態を把握したいので,afterTextChanged に処理を書いています.

このコードでのポイントは,setText する前に removeTextChangedListener() を呼び出していることです.

TextWatcher クラスメソッド内部で setText を呼び出すと,キーボード入力した時と同様に再び ~Changed() メソッドが呼ばれてしまいループ状態に陥ってしまうので,setText する直前に一旦自分自身の Listener を削除しておき,setText 完了後に再び addTextChangedListener() で自分自身を登録します.

BackgroundColorSpan() は文字列の背景色を変えますが,ForegroundColorSpan() を使えば文字色を変更できたりします.

簡単な サンプルプロジェクト を用意しました. 参考になれば幸いです.

pyenv install 中に BUILD FAILED してしまう

Python のバージョン管理に pyenv を使っています.

Jango を使ってみようと,新しく Python バージョンを入れようとしました.

$ pyenv install 3.6.0
python-build: use openssl from homebrew
python-build: use readline from homebrew
Downloading Python-3.6.0.tar.xz...
-> https://www.python.org/ftp/python/3.6.0/Python-3.6.0.tar.xz
Installing Python-3.6.0...
python-build: use readline from homebrew

BUILD FAILED (OS X 10.14.3 using python-build 20180424)

Inspect or clean up the working tree at /var/folders/px/90g_3pjj7jndszmdtk9ybnqw0000gn/T/python-build.20190129040150.78545
Results logged to /var/folders/px/90g_3pjj7jndszmdtk9ybnqw0000gn/T/python-build.20190129040150.78545.log

Last 10 log lines:
  File "/private/var/folders/px/90g_3pjj7jndszmdtk9ybnqw0000gn/T/python-build.20190129040150.78545/Python-3.6.0/Lib/ensurepip/__main__.py", line 4, in <module>
    ensurepip._main()
  File "/private/var/folders/px/90g_3pjj7jndszmdtk9ybnqw0000gn/T/python-build.20190129040150.78545/Python-3.6.0/Lib/ensurepip/__init__.py", line 189, in _main
    default_pip=args.default_pip,
  File "/private/var/folders/px/90g_3pjj7jndszmdtk9ybnqw0000gn/T/python-build.20190129040150.78545/Python-3.6.0/Lib/ensurepip/__init__.py", line 102, in bootstrap
    _run_pip(args + [p[0] for p in _PROJECTS], additional_paths)
  File "/private/var/folders/px/90g_3pjj7jndszmdtk9ybnqw0000gn/T/python-build.20190129040150.78545/Python-3.6.0/Lib/ensurepip/__init__.py", line 27, in _run_pip
    import pip
zipimport.ZipImportError: can't decompress data; zlib not available
make: *** [install] Error 1

なぜか怒られます.

すぐに xcode-select --install で治るらしいと知ったのですが,

僕の場合は再度試しても同じでした.

いろいろ調べてみると,こちら の issue に解決策が載っていました.

sudo installer -pkg /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg -target /

※ 現在 MAC OS Mojave を使っているので,たまたまバージョンが一緒だったので動きましたが,お使いの環境に合わせて変更したほうがいいと思います.

てことで,無事に Python3.6.0 が入り.

$ pyenv install 3.6.0
python-build: use openssl from homebrew
python-build: use readline from homebrew
Downloading Python-3.6.0.tar.xz...
-> https://www.python.org/ftp/python/3.6.0/Python-3.6.0.tar.xz
Installing Python-3.6.0...
python-build: use readline from homebrew
Installed Python-3.6.0 to /Users/fmzk/.pyenv/versions/3.6.0

django 用の仮想環境も作れました.

$ pyenv virtualenv 3.6.0 django3.6.0
Requirement already satisfied: setuptools in /Users/fmzk/.pyenv/versions/3.6.0/envs/django3.6.0/lib/python3.6/site-packages
Requirement already satisfied: pip in /Users/fmzk/.pyenv/versions/3.6.0/envs/django3.6.0/lib/python3.6/site-packages

作業用ディレクトリ配下で Python3.6.0 & 指定した仮想環境で実行できるように

$ pyenv local 3.6.0/envs/django3.6.0

とやっておしまい.

久々の更新でした(もっと頻度上げましょう)

Flux + Kotlin + RxJava + KOIN + Retrofit で作る Android アプリ

このエントリーはCyberAgent 19新卒 エンジニア Advent Calendar 2018の22日目の記事です.

はじめに

みなさんはじめまして, @mzkii です.

CA19新卒で,現在タップル誕生で AndroidEngineer として内定者バイトをしています.

この記事では,タップル誕生でも用いられている Flux Architecture について軽く知ってもらうために,GithubAPIを使った簡単なAndroidアプリケーションを開発していきたいと思います.

10分くらいで読める内容にしていますので,気になる方はぜひチャレンジしてみてください.

ちなみに完成形はこちらリポジトリで公開しています.

目標

以下の gif ファイルに表示されているような要件を満たすアプリを作ります.

captcha.gif

  1. login/oauth/access_token を叩きトークン取得
  2. /user/repos を叩き認証済みユーザーのRepository一覧を取得
  3. よしなにページング処理
  4. リポジトリ詳細に遷移可能

Flux Architecture

キーワードは 「単一方向のデータフロー」 です.

ユーザーからのイベントやAPI通信後に取得されるデータなど全て同じデータの流れに従い制御します.データの流れが一方向に制限されることによって,状態やロジック管理に関して無駄に悩むことが減ります.詳しくは公式サイトを見てみてください.

View(ボタンやスクロールイベント)👉ActionCreator(必要に応じてリモート等からデータを取得)👉Action(取得したデータを詰め込む)👉Store(状態を保持しViewに適切な形で公開)👉Viewの状態を更新

この流れを頭の片隅においておくと理解しやすいです.

画面

用意する画面を列挙します.

AuthorizeActivity

Action

まずActionを作っていきます.

ActionActionCreatorStoreとを結ぶデータの入れ物のような存在です.

sealed class AuthorizeAction {
    class Authorize(val accessToken: AccessToken) : AuthorizeAction()
}

ちなみに,AccessTokenは以下のようなDataClassです.

data class AccessToken(
    @field:Json(name = "access_token") val accessToken: String,
    @field:Json(name = "token_type") val tokenType: String
)

この画面では,認証ボタンを押すとAndroid標準のwebブラウザに遷移させるというアクションが必要なので,Authorizeというアクションクラスを用意しています.

後述するActionCreatorTokenを取得するためのAPIを叩き,結果をこのAuthorizeクラスのaccessTokenに代入し,dispatchすることでStoreに渡します.

ActionCreator

Authorizeアクションクラスを作成するActionCreatorは以下のようになっています.authorizeメソッドは今回Activityから呼ばれます.

class AuthorizeActionCreator(
    private val repository: AuthorizeRepository,
    dispatcher: Dispatcher
) : ActionCreator<AuthorizeAction>(dispatcher) {
    fun authorize(intent: Intent) {
        val uri = intent.data ?: return
        if (!uri.toString().startsWith(BuildConfig.REDIRECT_URI)) return
        repository
            .getAccessToken(uri.getQueryParameter("code")!!)
            .subscribeOn(Schedulers.io())
            .subscribeBy(
                onSuccess = { dispatch(AuthorizeAction.Authorize(it)) },
                onError = { Timber.e(it) }
            )
    }
}

ちなみに,AuthorizeActionCreatorが継承するActionCreatorクラスは以下のように定義されています.

今回マルチモジュールを使っていて, Flux モジュールに入れています.AuthorizeActionCreatorがこのActionCreator<T>を継承することで,AuthorizeActionCreatordispatchできるActionの型を制限しています.

abstract class ActionCreator<Action : Any>(
    private val dispatcher: Dispatcher
) {
    fun dispatch(action: Action) = dispatcher.dispatch(action)
}

ブラウザで認証させActivity#onResume()callbackして取得したIntent中に含まれるcoderepository.getAccessTokenメソッドに渡すと,アクセストークンを取得することができます.

repository
    .getAccessToken(uri.getQueryParameter("code")!!)
    .subscribeOn(Schedulers.io())
    .subscribeBy(
        onSuccess = { dispatch(AuthorizeAction.Authorize(it)) },
        onError = { Timber.e(it) }
    )

AccessTokenは以下のように定義されたDataClassです.

data class AccessToken(
        @field:Json(name = "access_token") val accessToken: String,
        @field:Json(name = "token_type") val tokenType: String)

onSuccessのタイミングで,取得したAccessTokenクラスをAuthorizeアクションクラスに詰め込みdispatchします.

dispatch(AuthorizeAction.Authorize(it))

ちなみにDispatcherは以下のように定義されています.

こちらもFluxモジュールに入れています.

class Dispatcher {
    private val bus = EventBus.builder()
        .throwSubscriberException(true)
        .build()

    fun dispatch(event: Any) {
        Timber.tag(this::class.java.simpleName).d("dispatch event=$event")
        bus.post(event)
    }

    fun register(observer: Any) {
        Timber.tag(this::class.java.simpleName).d("register observer=$observer")
        bus.register(observer)
    }

    fun unregister(observer: Any) {
        Timber.tag(this::class.java.simpleName).d("unregister observer=$observer")
        bus.unregister(observer)
    }
}

また後述しますが,このDispatcherKOINによってSingletonで管理されています.

ActionCreatordispatchすると,EventBusによって当該Store#on()メソッドにActionが流れてくる仕組みになっています.

AuthorizeStore

それではStoreの実装を見ていきましょう.

Storeは状態を保持し,Viewに対して保持した状態を適切な形に変換し公開します.

class AuthorizeStore(
    private val appSetting: AppSetting,
    dispatcher: Dispatcher
) : Store(dispatcher) {

    val gotoHomeState = StoreLiveData<Unit>()

    @Subscribe(threadMode = ThreadMode.MAIN)
    fun on(action: AuthorizeAction) { 👈
        when (action) {
            is AuthorizeAction.Authorize -> {
                appSetting.accessToken = action.accessToken.accessToken
                gotoHomeState.postValue(Unit) 👈
            }
        }
    }
}

AuthorizeActionCreator内でdispatch(AuthorizeAction.Authorize(it))すると,このStore#on()メソッドにAuthorizeActionが流れていきます.

今回は1つのアクションしか用意していないですが,通常複数のActionが流れてきますので,when (action)みたいな感じで分岐させます.当該スコープに入ると勝手にキャストされるので,action.(Actionのメンバ変数)としてアクセス可能です.

Store 👉 View については,LiveDataを使っています.StoreLiveDataStoreクラス内でのみpostValueが可能なカスタムLiveDataで,ボイラープレートコードを削減できます.Kotlininternal修飾子(他モジュールからは参照できない)という仕組みを利用して実現しています.StoreLiveDataはもちろんFluxモジュールに入れています.詳しくはこちらのブログを参考にしてみてください.

次にAuthorizeStoreクラスが継承しているベースのStoreクラスを見ていきます.

abstract class Store(
    private val dispatcher: Dispatcher
) : ViewModel() {
    init {
        register()
    }

    private fun register() {
        dispatcher.register(this)
    }

    override fun onCleared() {
        dispatcher.unregister(this)
        super.onCleared()
    }

    protected fun <T> StoreLiveData<T>.postValue(value: T) {
        internalPostValue(value)
    }

    protected fun <T> StoreLiveData<T>.setValue(value: T) {
        internalSetValue(value)
    }
}

Android Architecture ComponentViewModel クラスを使っています.これによってActivityStoreの生存期間を同一に保つことができます.(画面回転などによってActivityが再生成されてもStoreインスタンスは破棄されない)

View

最後にActivityです.DataBindingを使っています.by viewModel()とかby inject()とか書いてあるのはKOINで依存注入するための記述です.

AAC ViewModelStoreとして扱っているので,KOIN的にはAuthorizeStore by viewModel()となってしまい個人的に変な感じがします笑

class AuthorizeActivity : AppCompatActivity() {

    companion object {
        private const val URI = "https://github.com/login/oauth/authorize?client_id=" +
                "${BuildConfig.CLIENT_ID}&redirect_uri=${BuildConfig.REDIRECT_URI}"
    }

    private val store: AuthorizeStore by viewModel()
    private val actionCreator: AuthorizeActionCreator by inject()

    private val binding: ActivityAuthorizeBinding by lazy {
        DataBindingUtil.setContentView<ActivityAuthorizeBinding>(this, R.layout.activity_authorize)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_authorize)
        initView()
        observeState()
    }

    override fun onResume() {
        super.onResume()
        actionCreator.authorize(intent) 👈2
    }

    private fun initView() {
        binding.authorizeButton.setOnClickListener {
            startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(URI))) 👈1
        }
    }

    private fun observeState() {
        store.gotoHomeState.observe(this) {
            finish()
            startActivity(Intent(application, MainActivity::class.java)) 👈4
        }
    }
}

流れとしては,

  1. authorizeButtonが押され,Android標準のWebブラウザが立ち上がる.(User Interaction)
  2. 認証が済んでcallbackしてきたタイミングでonResumeが呼ばれ,そのまま actionCreator.authorize(intent) をコールする(View 👉 ActionCreator)
  3. ActionCreator内部でアクセストークンが取得できればdispathし,AuthorizeStore#on()メソッドにトークン情報が入ったActionが流れてくる.(ActionCreator 👉 Store)
  4. StoreHome画面に遷移する為のLiveDataUnitを渡して,ActivitygotoHomeState.observe(this)が呼ばれMainActivityに遷移する(Store 👉 View)

みたいな感じです.これで,アプリ起動からアクセストークン取得までの流れを掴むことができました.

AuthorizeStoreの方でappSetting.accessToken = action.accessToken.accessTokenと記述しているのは,SharedPreferencesにアクセストークンを保存するためです.今回少し拡張してAppSettingというクラスを作ってみました.こちらもKOINSingletonに管理されています.今後認証が必要なAPIを叩くときはAppSettingに保存されたaccessTokenを使います.

ここまでが認証画面のデータフローに関する説明です.

MainActivity

この画面は,認証済みユーザーのリポジトリ一覧を取得しリスト表示するための画面になります.

HomeAction

リスト表示するだけなので,基本的にはList<Repository>をラッパーしたActionを用意すればいいです.今回はローディング表示もさせたかったのでShowLoadingというActionも作っています.

こんな感じに,この画面ではどんなユーザーインタラクションが発生するだろう,と考えつつ最初にActionを作ったほうが個人的には楽だと思います.

sealed class HomeAction {

    class ShowLoading(val isLoading: Boolean) : HomeAction()
    class LoadRepositoryList(val repositoryList: List<Repository>) : HomeAction()
}

HomeActionCreator

基本的には,リポジトリ一覧を読み込んで,LoadRepositoryListにデータを詰め込んで,dispathしてStoreに送るだけです.

今回ローディングを表示する処理も入れているので,適切なタイミングでShowLoadingdispathしてやります.

こうすることで,ShowLoading(true) 👉 LoadRepositoryList(リポジトリ一覧データ) 👉 ShowLoading(false) という順番でHomeStoreHomeActionが流れてくることになります.

class HomeActionCreator(
    private val appSetting: AppSetting,
    private val repository: GithubRepository,
    dispatcher: Dispatcher
) : ActionCreator<HomeAction>(dispatcher) {

    fun getMyRepositoryList(page: Int) =
        repository
            .getMyRepositoryList(appSetting.accessToken, page)
            .subscribeOn(Schedulers.io())
            .doOnSubscribe { dispatch(HomeAction.ShowLoading(true)) }
            .doFinally { dispatch(HomeAction.ShowLoading(false)) }
            .subscribeBy(
                onSuccess = { dispatch(HomeAction.LoadRepositoryList(it)) },
                onError = { Timber.e(it) }
            )
}

HomeStore

次にStoreです.AuthorizeStoreと原理は一緒ですが,ページング処理が入っています.

Store#on()に流れてくるリポジトリデータは断片的なもので今回は10件ずつしか入ってきません.そこでStore内部で,これまで読み込んだリポジトリデータを保持しておき,毎回データが流れてくるたびにAddAllします.

後はAddAllしたリストをLiveDataViewに流すだけです.またHomeAction.LoadRepositoryListが流れてくるたびに,次のページ設定と,次ページ以降データが存在するかのフラグを立てておきます.View側でリポジトリをフェッチするActionCreatorを叩く際に必要になってきます.

class HomeStore(dispatcher: Dispatcher) : Store(dispatcher) {

    companion object {
        private const val INITIAL_PAGE = 1
    }

    private val repositoryList = mutableListOf<Repository>()

    var canFetchMore = false
        private set

    var pageNum = INITIAL_PAGE
        private set

    val loadingState = StoreLiveData<Boolean>()
    val loadedRepositoryListState = StoreLiveData<List<Repository>>()

    @Subscribe(threadMode = ThreadMode.MAIN)
    fun on(action: HomeAction) {
        when (action) {

            is HomeAction.ShowLoading -> {
                loadingState.postValue(action.isLoading)
            }
            is HomeAction.LoadRepositoryList -> {
                canFetchMore = action.repositoryList.isNotEmpty()
                pageNum++
                repositoryList.addAll(action.repositoryList)
                loadedRepositoryListState.postValue(repositoryList)
            }
        }
    }
}

View

最後にActivityです.initView()から見ていきましょう.recyclerViewの初期化が中心ですが,リストが最下部に到達したかどうかのタイミングが知りたいのでFetchMoreScrollListenerを自作しています.

最下部に到達し,かつ次ページのデータが存在する場合に,ctionCreator.getMyRepositoryList(store.pageNum)をして次ページを読み込みます.

そうすると,既存の(ロード済みの)リポジトリリストに,新しくフェッチしたリポジトリリストを加えたリストがloadedRepositoryListStateに流れてきます.

observeState()ではloadingStateの購読と,loadedRepositoryListStateの購読を行っています.

取得したリポジトリリストはloadedRepositoryListStateに流れてくるので,adapter.submitList(it)を実行します.リストの追加やアニメーションは内部でよしなにやってくれます.

class MainActivity : AppCompatActivity() {

    private val store: HomeStore by viewModel()
    private val actionCreator: HomeActionCreator by inject()

    private val binding: ActivityMainBinding by lazy {
        DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
    }

    private val repositoryAdapter: RepositoryAdapter by lazy {
        RepositoryAdapter(
            onCardClick = { repository ->
                repository.url?.let { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it))) }
            }
        )
    }

    private var isLoading = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initView()
        observeState()
        isLoading = true
        actionCreator.getMyRepositoryList(1)
    }

    private fun initView() {
        title = "My repositories"
        with(binding.recyclerView) {
            adapter = repositoryAdapter
            layoutManager = LinearLayoutManager(this@MainActivity, RecyclerView.VERTICAL, false)
            addOnScrollListener(object : FetchMoreScrollListener() {
                override fun onLoadMore() {
                    if (!isLoading && store.canFetchMore) {
                        isLoading = true
                        actionCreator.getMyRepositoryList(store.pageNum)
                    }
                }
            })
        }
    }

    private fun observeState() {
        store.loadingState.observe(this) {
            binding.progressBar.visibility = if (it) View.VISIBLE else View.GONE
            isLoading = it
        }
        store.loadedRepositoryListState.observe(this) {
            repositoryAdapter.submitList(it.toList())
        }
    }
}

以上がリポジトリ一覧画面の解説になります.

その他

今回使用したライブラリ一覧を列挙します. KOINとRetrofitに関しては解説も入れておきます.

    // EventBus
    implementation 'org.greenrobot:eventbus:3.1.1'

    // Timber
    implementation 'com.jakewharton.timber:timber:4.7.1'

    // KOIN
    implementation 'org.koin:koin-android:1.0.2'
    implementation 'org.koin:koin-android-scope:1.0.2'
    implementation 'org.koin:koin-androidx-scope:1.0.2'
    implementation 'org.koin:koin-android-viewmodel:1.0.2'
    implementation 'org.koin:koin-androidx-viewmodel:1.0.2'

    // Network
    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
    implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'
    implementation 'com.squareup.okhttp3:okhttp:3.11.0'

    // JsonParser
    implementation 'com.squareup.moshi:moshi:1.7.0'
    implementation 'com.squareup.moshi:moshi-kotlin:1.7.0'

    // Rx
    implementation 'io.reactivex.rxjava2:rxjava:2.2.2'
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
    implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'

    // CardView
    implementation 'com.android.support:cardview-v7:28.0.0'

KOIN

KOIN は Android 向けのシンプルな Dependency Injection フレームワークで,Kotlinの機能を使ってDIを実現しています.

このアプリでは,Applicationクラス内で以下のようなモジュールを定義しています.

private val appModule = module {
    single { Dispatcher() }
    single { AppSetting(getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)!!) }
}

private val networkModule = module {
    single {
        Retrofit
            .Builder()
            .client(OkHttpClient.Builder().build())
            .baseUrl("https://github.com/")
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .addConverterFactory(MoshiConverterFactory.create())
            .build()
            .create(AuthorizeApi::class.java)
    }
    single {
        Retrofit
            .Builder()
            .client(OkHttpClient.Builder().build())
            .baseUrl("https://api.github.com/")
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .addConverterFactory(MoshiConverterFactory.create())
            .build()
            .create(GithubApi::class.java)
    }
    single { AuthorizeRepository(get()) }
    single { GithubRepository(get()) }

}

private val uiModule = module {
    factory { HomeActionCreator(get(), get(), get()) }
    viewModel { HomeStore(get()) }

    factory { AuthorizeActionCreator(get(), get()) }
    viewModel { AuthorizeStore(get(), get()) }
}

Singletonで扱いたいクラスはsingleに,factoryを使えばget()が呼び出されるたびに毎回新しいインスタンスを提供します.

依存関係はget()で解決します.例えば,HomeActionCreatorAppSettingGithubRepositoryDispatcherが必要ですが,AppSettingappModule内でSingletonとして定義され,GithubRepositorynetworkModule内でSingletonとして定義され(かつGithubRepositoryGithubApiが必要ですが同一モジュール内でSingletonで提供されます),DispatcherappModule内でSingletonで提供されます.

AAC ViewModelの場合はviewmodelを使います.Daggerと違ってKOINは標準でAAC ViewModelをサポートしています.

Field Injectionする場合は,by inject()を使います.AAC ViewModelの場合はby viewmodel()なので注意です.

これでmoduleが作れたので,後はApplicationクラス内のonCreateのタイミングでstartKoinメソッドをコールし,先程作成したmoduleを指定します.

startKoin(this, listOf(appModule, uiModule, networkModule))

Retrofit

RetrofitはmoshiとRxJavaを組み合わせて使っています.

Retrofit
    .Builder()
    .client(OkHttpClient.Builder().build())
    .baseUrl("https://api.github.com/")
    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    .addConverterFactory(MoshiConverterFactory.create())
    .build()
    .create(GithubApi::class.java)

OAuth認証をするにあたって少しハマったところを書きます.

interface AuthorizeApi {
    @Headers("Accept: application/json")
    @POST("login/oauth/access_token")
    @FormUrlEncoded
    fun getAccessToken(
        @Field("client_id") clientId: String,
        @Field("client_secret") clientSecret: String,
        @Field("code") code: String) : Single<AccessToken>
}

RetrofitでAPIごとにヘッダーを設定する場合,@Headersを付けるみたいです. 今回はjsonで欲しかったのでAccept: application/jsonとしました. この設定を忘れてしまいかなり時間を食ってしまいました.

さらに,Retrofitを使用してフォームURLエンコードされたリクエストを実行するために@FormUrlEncodedも付け加えておきます.

ここら辺のアノテーション周りの情報についてはFuture Studioの動画がとても参考になりました(Thank you!!).また,GithubOAuth認証周りについては,詳しくはAuthorizing OAuth Appsを参照してください.

終わりに

いくつかモダンなライブラリを使いつつ,Flux ArchitectureでAndroidアプリを作ってみました!10分で読めると言いつつ,気がつけばかなり分量が多くなってしまい反省してます😅

Flux は状態管理がしやすく,初めて見る画面でもActionを見ればどんなことをする画面なのかなんとなく把握できたりします.さらに,単一方向のデータフローに強制することで,誰が書いても同じようなコードになりやすく,責務が分割されやすく,コードの保守性が向上します.

この記事を読んで少しでも興味を持たれた方は, Android/iOSにかかわらずぜひFluxに挑戦してみてください. (※完成形はこちらリポジトリで公開しています)

おしまい

Kotlin Fest 2018 に参加した話

f:id:mzkii:20180901212234j:plain

皆さんはじめまして @mzkii です.普段は Kotlin を使って Android アプリ開発をしたり,Python を使って データ分析をしたりしています.

この記事では,8/25(土)に開催された Kotlin Fest 2018スカラシップ枠 として参加しましたので,その様子をレポートしていきたいと思います.

目次

  • 参加するまでの経緯
  • 参加する目的
  • セッション
    • Kotlin で改善する Android アプリの品質
    • Kotlinアプリのリファクタリングポイント
    • start from Convert to Kotlin
    • Kotlin コルーチンを理解しよう
  • LT大会
  • ブースと懇談会
  • おわりに

参加するまでの経緯

このカンファレンスは,DroidKaigi 2018 のオープニングセッションにてサプライズ告知され,自分自身とても楽しみにしていたのですが,ついに7月の頭に開催決定のツイートが流れてきました!!

さらに,8月に入って サイバーエージェント さんが「Kotlin Fest 2018」スカラシップ枠を募集していると Twitter で知り,(既にチケット買っちゃってましたが) ダメ元で応募してみると... 見事参加できることになりました!

Kotlin Fest 2018 における スカラシップ枠 とは,

・Kotlin Fest 2018 のチケット提供(懇親会 有)

・希望者には8月24日(金)の社内見学・技術者とのランチ会に参加いただけます。

・遠方在住の方には宿泊場所を手配。

・交通費支給。

公式ページ より抜粋

となっており,地方学生にとってはとっても嬉しいプランでした😉

また,申し込みには,

・Kotlin Conferenceに参加したい理由を300文字以内で

・githubアカウント

・これでの制作物で一番よく出来たもののURLなど

公式ページ より抜粋

以上3項目を書いて送るだけ!とっても簡単でした!

参加する目的

f:id:mzkii:20180901212021j:plain このカンファレンスは,「Kotlinを愛でる」をビジョンに,Kotlin の知識の共有や,Kotlin を愛してやまない人々に対して交流の場を提供することを目的に開催されました.僕自身,普段から Kotlin は使っていますし,第一線で活躍されているエンジニアの方々と交流を深めながら Kotlin の言語仕様を理解することはもちろん,Android x Kotlin に関する最新技術をキャッチアップしたい!と思って参加しました(特に Kotlin Coroutine).

セッション

早速セッションについてみていきましょう.本当は全部のセッション見たかったのですが,2セッション同時に発表されるので,タイトル見て直感で,面白そう x 明日からすぐに役立ちそう なセッションをピックアップしてみました.

Kotlin で改善する Android アプリの品質

概要

  • なぜ品質の話?

    • 既存の Android アプリケーションを,品質を担保しつつ Java から Kotlin に移行することは,書き直しのコストや,デグレのリスクなどにより難易度が高い.
    • Java --> Kotlin は 上記のようなコストとリスクを上回るメリットがあるのか??
  • Android アプリにおける品質とは?

    • 速い
    • 落ちない
    • 使いやすい
    • 変更しやすい
    • 読みやすい
  • ソフトウェアの品質 = 要因の組み合わせ

    • 内的品質要因 ... スピード,使いやすさなど
    • 外的品質要因 ... モジュール性,読みやすさなど
      • 特に重要な要因は...
        • 正確さ + 頑丈さ = まとめて信頼性と呼ぶ
        • 拡張性 + 再利用性 = まとめてモジュール性と呼ぶ
  • Java で広く実践されている規則は Kotlin だとどうなる??

    • Kotlin では,いたるところに品質要因に関するベストプラクティスが言語仕様的に対応している
    • Java --> Kotlin にすることで,「明瞭で,正しく,再利用可能で,頑強で,柔軟性があり,保守可能なプログラムを書く」エッセンスが自然と取り入れられる.

Kotlin アプリのリファクタリングポイント

概要

  • Gradle Kotlin DSL
    • Gradle のスクリプトを Kotlin で書ける(build.gradle.kts)
    • 補完・ジャンプコードが効くぞい
  • 処理の共通化
    • 処理を共通化するためにどういった選択肢があるのか
      • abstract class
      • interface のデフォルト実装
      • class/property delegation
      • 拡張関数
        • クラスに関数を生やす
        • どこからでも呼び出せるが,スコープを限定したい時は,interface のデフォルト実装が利用できる
      • トップレベル関数
        • ファイルに直接書けるのでどこからでも呼び出せる
        • スコープを限定したい時は,interface に書くと良いが,それすなわち interface のデフォルト実装
  • Mutable を避ける
    • 今どんな値が入っているのか常に考えないといけなくなる
      • 暗黙の前提はバグの温床
    • その var は本当に var にしないといけないのか? (ここの話に使われていた遷移図がめっちゃ分かりやすかった)
      • コンパイル時からずっと不変 --> const val
      • インスタンス生成時に値が確定し不変 --> val
      • ある時点を過ぎると値が確定し不変 --> lazy
        • intent の getExtra などは by lazy で val かつ NonNull化できる
      • ある時点を過ぎると値が確定し不変 && 非Nullable --> lateinit
        • onCreate や onCreateView よりもあとで値が決まる場合は素直に Nullable にしよう
    • その MutableList は本当に MutableList にしないといけないのか?
      • var と MutableList の併用は避ける
        • val x MutableList の組み合わせ
        • var x List の組み合わせ
  • Collection

    • List と MutableList,実体はどっちも ArrayList
    • List は 読み取り専用であり,Immutable ではない
    • Collection の生成は,基本的には listOf,setOf,mapOf
      • 他にも,パフォーマンスを考慮して,setOf,linkedSetOf,hashSetOf,sortedSetOf があるので使うとよいぞ
    • Collection のリファクタリング
      • 2つのリストを同時に使う --> zip
      • 変換と除外 --> map,filter,mapNotNull
      • リストを N 個ずつに分割 --> withIndex でインデックスを付けて,さらに groupBy でグループ分け
      • できるだけ否定「!」は使わない --> all,any,none の活用
  • DSL の拡張

    • Domain Specific Language
    • 内部DSL とも呼ばれ,Kotlin の言語機能で実現可能
    • 構造的なデータ宣言,設定値の記述が簡潔になる
    • AnkoSpek などに使われてる機能らしいが僕にはまだ早かった...
  • Nullability
    • Kotlin といえば Nullability が嬉しい
    • しかし API が Nullable を返した場合,UI 層までのどの層まで Nullable として扱うか問題が出てくる
      • return team?.user?.name?.length
    • null が何を表しているか明確にしよう
      • 滅多に起きないエラーケース --> infra で Exception に変換
      • 任意項目でかつ存在していないを表していることが自明 --> UI 層まで Nullable で渡す
      • 自明じゃない何か --> sealed class を作ってその状態に意味付けする
  • スコープ関数
    • キャプチャする時
      • null 判定時の値が使えるので smart cast が効かないときに有効
      • 単に if 文の代わりにもなる
        • ただし if 文と全く同じなわけではない
    • 処理をまとめたい時
      • Intent の putExtra など
        • ただしローカル変数名との衝突に注意
  • データ構造と状態数
    • 肥大しがちな data class
      • バグの温床なので,ビジネスロジックを考慮して,ありえない状態を避けよう
        • 状態整理 --> Either,Pair
        • 意味付け --> sealed class,data class
      • 状態数を数式に落とし込むと,より厳密に精査できる👀

start from Convert to Kotlin

  • RecyclerView を使ったサンプルアプリを Java --> Kotlin にしながら言語仕様をみてみる
@Parcelize
data class KotlinItem(var id Int = 0, var name String? = null) : Parcelable {
    fun isSameItem(obj Any): Boolean {
        return ((obj as? KotlinItem)?.id == this.id)
    } 
}
KotlinItem(var id Int = 0, var name String? = null)
  • data class で toString などのメソッドを生やす
    • equals
    • hashCode
    • toString
    • copy
data class KotlinItem ...
@Parcelize
data class KotlinItem ...
  • 安全キャスト
    • Any を KotlinItem に変換できるか?
      • キャストできなかったら null を返す
obj as? KotlinItem

Kotlin コルーチンを理解しよう

  • コルーチンとはなにか
    • 一時停止可能な計算インスタンス
    • 特定のスレッドに束縛されない
    • 結果や例外を伴う場合もある
    • 継続状況を持つプログラムが簡単に書けるぞ
      • コールバックスタイルで記述されているとネストが深くなるが...
      • コルーチンだとawaitを使えばキレイに書ける
  • Kotlin ではコルーチンをどうやって実現しているのか
    • コルーチンをステートマシンに変換している
      • 中断と再開を状態遷移と考える
      • もちろん再開に必要な情報もステートマシンに持っておく
      • 逐次内部状態を変えながら実行
    • コルーチン --> ステートマシン
      • ここで出てくるのが suspend修飾子
        • ラムダ式につけるとコルーチン本体
        • 関数につけると中断する場所を表す
      • コルーチンを任意のタイミングで再開するには??
        • 実は,suspend 関数にはコンパイル時に継続インターフェースの引数が生える
        • 継続インターフェースを使って再開処理
        • suspend ラムダはコルーチンの範囲を決める
    • 正直コルーチンを自作するのはしんどい
      • そこで出てくるのがコルーチンビルダー
        • launch(結果を伴わないコルーチンビルダー),async(結果を返すコルーチンビルダー) ...
      • 継続インターセプターで実行スレッドを指定できる
        • CommonPool,JavaFx,UI,Swing
  • Kotlin コルーチンの基本的な使い方
    • まずは launch(結果を伴わないコルーチン)を作成してみる
      • start ... コルーチンの開始方法
      • parent ... 複数のコルーチンを一気にキャンセルしたい場合などに親Jobを指定する
      • onCompletion ... コルーチン終了時に呼び出されるコールバック関数を指定
      • context ... 共有データや継続インターセプター
        • 例えば Android で UI をいじるコルーチンを作る場合は,デフォルトではスレッドプールでコルーチンが走ってしまうので,launch(UI) でコルーチンの処理は常にUIスレッド上で再開するように継続インターセプターを指定する.
    • async / await
      • APIを叩くなど結果を伴うコルーチンを作成したい場合は async を使う
      • async とは,結果を持つコルーチンを作成するコルーチンビルダー関数のこと
      • async { fetchUserInfo(userId) }.await() でAPIを叩く処理は非同期で走らせて,結果は await を使って受け取ることができる
      • launch と async を組み合わせれば大体これで事足りる(そうです)
    • エラーハンドリング
      • 普通に try-catch を使う
    • コルーチンの直列・並列実行
直列実行
val token = async { getToken() } ... ここは中断しない
val profile = async { loadProfile(token.await()) }.await() ... token で結果を待ってから loadProfile を実行する

並列実行
val token = async { getToken() } ... 中断しない
val profile = async { loadProfile() } ... 中断しない
show(token.await(), profile.await()) ... 一気に await して結果待ち
  • キャンセル
    • launch は Job インスタンスを返すので,任意のタイミングで job.cancel() する
    • その時コルーチンは CancellationException を受け取るのでキャンセルしたタイミングで任意の処理を実行することができる
    • まとめて一気にキャンセルしたい場合は,launch の引数に parent があるので,親の Job インスタンス(例えばここでは rootJob)を保持しておいて,launch するタイミングで launch(UI, parent = rootJob) する <-- 結果は UI スレッドで受け取り,親の job を指定する
  • リトライ
    • コルーチン内で,repeat(3) などしておいて,処理が正常終了した場合は return@launch でコルーチンを終了し,なにか Exception が発生した場合に,リトライしなくなかったら return@repeat 等でリトライ処理を抜けるようにする.
    • (読めば理解できるけど,個人的に冗長で読みにくい気がしなくもない...)
  • コルーチンの応用例
    • retrofit ... 結果を Deferred< T > で受け取れる Jake作の Adapter がある
    • EventBus x Cnannel
    • onActivityResult ... android-coroutines を使えば,activity を起動して結果を受け取るまでを一つのコルーチン内で簡潔に書ける

LT大会

LT大会では,8名の方々が発表してくださいました.

一人あたり3分だったので,スピード感ある発表でどれも面白かったです😉

ここでは,スライドが公開されていて,特に印象に残っていた発表を紹介します.

3分で分かる Sequence

  • List では コレクション操作のたびに毎回新しい List を作成する(先行評価)
  • Sequence では 終端操作(toList など)を走らせるまでは処理が実行されない(遅延評価)
  • 素数と操作数が少ない --> List, 多い --> Sequence で使い分けよう

RxJavaを使っている 既存アプリに Kotlin Coroutinesを導入しよう

  • Single<List<Person>>suspend List<Person> に変換してみる
  • ただし,実際のところ Single なメソッドを複数箇所から呼び出していることが多いので,一つだけ置き換えはできない
  • そんなあなたに kotlinx-coroutines-rx2 があるよ
  • Single に await メソッドが生えるので,中断関数に変換できる
  • しかしプロダクションコードでは,テスト,ライフサイクル,エラーハンドリングも必要になってくる
    • この辺りの話は @takahirom さんのこの記事が参考になる

J2K コンバータをカスタマイズする

  • 純正J2Kコンバータが強制的にNonNull変換する問題

    • Convert Java File to Kotlin File を使うと Nullable なコードが NonNull コードに変換されてしまう
      • 親Class,Interface,abstractクラスなどメソッドを継承した場合
      • 引数に @Nullable がない場合
      • Kotlin は NonNull で Java は Nullable 環境になってしまう
  • 解決策

    • 目視でチェック or とりあえず Kotlin 化して動作チェック
    • コンバータカスタマイズしてみよう
      • Kotlin の開発環境構築
        • @shiraji さんの記事が参考になる
      • TypeConverter.kt をカスタマイズ
      • Kotlin のチャイルドIDEA実行
      • レッツコンバー
  • カスタムコンバータを導入してみて

    • hotfix 切ることが激減した
    • コードの信頼感が高まりレビューが簡素化
    • 何より手軽に Java --> Kotlin ができるようになった😉

ブースと懇談会

最後に,展示ブースと懇談会の様子をお届けします.

ブースで面白かったのが,サイバーエージェントさんの Kotlin Puzzlers です.

午前午後と2問ずつ Kotlin に関する問題が出題されていたのですが,

これがめちゃ難問でした(それぞれ1問ずつしか解けなかった😂).

また,ヤフーさんのブースでは黒帯本がもらえる抽選会や,

モブプログラミング教室も開催されており,かなり盛り上がっていました!

写真は黒帯抽選会の様子です

f:id:mzkii:20180901212013j:plain

そしてなんと抽選会当たりました笑(ありがとうございます)

クロージング後はブースやっていた場所で懇談会をやることになりました.

とりあえず写真貼っていきます(肉が美味しかった).

f:id:mzkii:20180901212045j:plain

f:id:mzkii:20180901212035j:plain

f:id:mzkii:20180901212039j:plain

f:id:mzkii:20180901212042j:plain

おわりに

Kotlin Fest 2018 の様子をレポートしてみましたがいかがでしたか? さらに詳しく知りたい方は,#kotlinfestツイッター検索してみると,ここでは紹介しきれなかったセッション資料や,ブース展示の様子などよく分かりますよ!

次回開催はまだ未定とのことですが,今回のイベントは大盛況でしたし,是非来年も開催してほしいです(今度はLT枠などで喋ってみたい).今後も Android x Kotlin のイベントには積極的に参加していくので,僕もイベントを盛り上げられる一員となれるように楽しくやっていきたいと思います!

最後に,Kotlin Fest を開催してくださった運営の皆様,スポンサーの皆様,さらにスカラシップ枠を用意してくださったサイバーエージェント様ありがとうございました.こういったスカラシッププログラムは,地方学生にとっては非常にありがたい内容ですし,今後も継続していただけると,全国の学生の助けになると思います.

ここまでお読み頂きありがとうございました🙇

Kotlin かわいい!

おしまい

場数を踏む大切さ

最近,藤田晋の仕事学を買いました.
その中で「効率よりも場数が能力を決める」というコラムを読んで
プログラミングを学ぶ上でも大事そうなことだったので自戒も込めて記事にします.

効率よりも場数が能力を決める

プログラミングを学習する上で,「短時間で効率的に学びたい」と思う人は少なくないと思います. これはプログラミングに限った話ではなく,例えば英会話レッスンなどにも当てはまるでしょう.

個人的に,プログラミングとは目的を達成するための手段の一つに過ぎないと考えていて, プログラミングを学習することが目的化してしまっては本末転倒です. 特に,学生時代は(自分もまだ学生だけど)講義や趣味でプログラミングを勉強したり, あるいはインターンしながらプログラミングを学んだりする機会が多く,プログラミングの学習が目的化しやすい環境なのではないでしょうか.

僕もつい最近まではそうでした. 書籍やプログラミング学習サイトで言語仕様は理解しますが,理解するだけです.理解止まり. 実は,脳内の知識を,自分の技術として落とし込むためには.実際学んだことを活かす環境を作ってアウトプットすることが大事なんですね. もっと言えば,知識を吸収している段階で,「この本のこのページの文法,今作っている個人アプリのこの処理に適用したらもっと簡略化できるなぁ,早速試してみるか!」 くらいの脳内フットワークの軽さがあればグッドだと思います.

ここで話を戻して,「短時間で効率的に」プログラミングを学ぶために重要なことについて考えます. 今までこうしたネット記事を読んでいて分かったのですが,この場合の対象者って大体これからプログラミングを学び始めるか, あるいは最近学習を始めた人が多い気がします.そして僕も実際学生です.

こうした初学者が,「短時間で効率的に」といって情報を探し回る事自体が非効率です. プログラミングができる,できないといった能力の差は,場数=経験の差で決まってくるからです. 経験も何もないのに,効率論を学んでも,まるで無意味でしょう.

重要なのは場数です. とにかく目で情報を読み取り,頭で理解して記憶する.そして手を動かして実行する. この一連のサイクルが自分の技術力を向上させる秘訣ではないでしょうか.

ひたすら場数を踏む,こうしたやり方は効率が悪いように感じる人がいるでしょう.
確かに,私も最初はそう思いました.しかし実際は,場数を踏むことでしか自分の中の知識や選択肢を広げられず効率は上がらないのです.

この文章は例の本からの引用です,とても説得力があります. 自分の考え,行動,その全てはこれまでの人生の経験が影響しているんですね.

プログラミングを経験するためには,コードを書いて実行して,デバッグする必要があります. 変なプライドは捨てて,がむしゃらに知識を吸収してコードを書く, この行動の全てが結局「短時間で効率的に」プログラミングを学ぶことに繋がってくるのではないでしょうか.

結局の所,毎日コツコツが大事なんですよね.

最後までお読みいただき,ありがとうございました.