EditText 中の任意の文字列に対してハイライト操作を行う
EditText に入力された文字列が存在したとして,任意の区間の文字列に対してハイライト表示したいケースを考えます. Twitterの文字数制限のUIみたいな感じです.
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 ファイルに表示されているような要件を満たすアプリを作ります.
Flux Architecture
キーワードは 「単一方向のデータフロー」 です.
ユーザーからのイベントやAPI通信後に取得されるデータなど全て同じデータの流れに従い制御します.データの流れが一方向に制限されることによって,状態やロジック管理に関して無駄に悩むことが減ります.詳しくは公式サイトを見てみてください.
View(ボタンやスクロールイベント)
👉ActionCreator(必要に応じてリモート等からデータを取得)
👉Action(取得したデータを詰め込む)
👉Store(状態を保持しViewに適切な形で公開)
👉Viewの状態を更新
この流れを頭の片隅においておくと理解しやすいです.
画面
用意する画面を列挙します.
AuthorizeActivity
MainActivity
AuthorizeActivity
Action
まずAction
を作っていきます.
Action
はActionCreator
とStore
とを結ぶデータの入れ物のような存在です.
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
というアクションクラスを用意しています.
後述するActionCreator
はToken
を取得するための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>
を継承することで,AuthorizeActionCreator
がdispatch
できるAction
の型を制限しています.
abstract class ActionCreator<Action : Any>( private val dispatcher: Dispatcher ) { fun dispatch(action: Action) = dispatcher.dispatch(action) }
ブラウザで認証させActivity#onResume()
にcallback
して取得したIntent
中に含まれるcode
をrepository.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) } }
また後述しますが,このDispatcher
はKOIN
によってSingleton
で管理されています.
各ActionCreator
でdispatch
すると,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
を使っています.StoreLiveData
はStore
クラス内でのみpostValue
が可能なカスタムLiveData
で,ボイラープレートコードを削減できます.Kotlin
のinternal修飾子(他モジュールからは参照できない)
という仕組みを利用して実現しています.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 Component
の ViewModel
クラスを使っています.これによってActivity
とStore
の生存期間を同一に保つことができます.(画面回転などによってActivity
が再生成されてもStore
のインスタンスは破棄されない)
View
最後にActivity
です.DataBinding
を使っています.by viewModel()
とかby inject()
とか書いてあるのはKOIN
で依存注入するための記述です.
AAC ViewModel
をStore
として扱っているので,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 } } }
流れとしては,
authorizeButton
が押され,Android
標準のWeb
ブラウザが立ち上がる.(User Interaction)- 認証が済んで
callback
してきたタイミングでonResume
が呼ばれ,そのままactionCreator.authorize(intent)
をコールする(View 👉 ActionCreator)
ActionCreator
内部でアクセストークンが取得できればdispath
し,AuthorizeStore#on()
メソッドにトークン情報が入ったActionが流れてくる.(ActionCreator 👉 Store)
Store
はHome
画面に遷移する為のLiveData
にUnit
を渡して,Activity
のgotoHomeState.observe(this)
が呼ばれMainActivity
に遷移する(Store 👉 View)
みたいな感じです.これで,アプリ起動からアクセストークン取得までの流れを掴むことができました.
AuthorizeStore
の方でappSetting.accessToken = action.accessToken.accessToken
と記述しているのは,SharedPreferences
にアクセストークンを保存するためです.今回少し拡張してAppSetting
というクラスを作ってみました.こちらもKOIN
でSingleton
に管理されています.今後認証が必要な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
に送るだけです.
今回ローディングを表示する処理も入れているので,適切なタイミングでShowLoading
もdispath
してやります.
こうすることで,ShowLoading(true)
👉 LoadRepositoryList(リポジトリ一覧データ)
👉 ShowLoading(false)
という順番でHomeStore
にHomeAction
が流れてくることになります.
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
したリストをLiveData
でView
に流すだけです.また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()
で解決します.例えば,HomeActionCreator
はAppSetting
,GithubRepository
,Dispatcher
が必要ですが,AppSetting
はappModule
内でSingleton
として定義され,GithubRepository
はnetworkModule
内でSingleton
として定義され(かつGithubRepository
はGithubApi
が必要ですが同一モジュール内でSingleton
で提供されます),Dispatcher
もappModule
内で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!!).また,GithubのOAuth認証周りについては,詳しくはAuthorizing OAuth Appsを参照してください.
終わりに
いくつかモダンなライブラリを使いつつ,Flux ArchitectureでAndroidアプリを作ってみました!10分で読めると言いつつ,気がつけばかなり分量が多くなってしまい反省してます😅
Flux は状態管理がしやすく,初めて見る画面でもActionを見ればどんなことをする画面なのかなんとなく把握できたりします.さらに,単一方向のデータフローに強制することで,誰が書いても同じようなコードになりやすく,責務が分割されやすく,コードの保守性が向上します.
この記事を読んで少しでも興味を持たれた方は, Android/iOSにかかわらずぜひFluxに挑戦してみてください. (※完成形はこちらのリポジトリで公開しています)
おしまい
Kotlin Fest 2018 に参加した話
皆さんはじめまして @mzkii です.普段は Kotlin を使って Android アプリ開発をしたり,Python を使って データ分析をしたりしています.
この記事では,8/25(土)に開催された Kotlin Fest 2018 に スカラシップ枠 として参加しましたので,その様子をレポートしていきたいと思います.
目次
- 参加するまでの経緯
- 参加する目的
- セッション
- LT大会
- ブースと懇談会
- おわりに
参加するまでの経緯
このカンファレンスは,DroidKaigi 2018 のオープニングセッションにてサプライズ告知され,自分自身とても楽しみにしていたのですが,ついに7月の頭に開催決定のツイートが流れてきました!!
Kotlin Fest 2018 開催のお知らせ #kotlinfest - 算譜王におれはなる!!!! https://t.co/Pm0JmjLHsN
— Kotlin Fest 2018 (@kotlin_fest) 2018年7月3日
Kotlin Fest 2018 のイベントページを公開しました!スピーカーのみなさまのご紹介を掲載しております。お申込みは7/24正午に開始します #kotlinfest #jkughttps://t.co/CU7kJJt0qC
— Kotlin Fest 2018 (@kotlin_fest) 2018年7月18日
さらに,8月に入って サイバーエージェント さんが「Kotlin Fest 2018」スカラシップ枠を募集していると Twitter で知り,(既にチケット買っちゃってましたが) ダメ元で応募してみると... 見事参加できることになりました!
【Developers Blog更新✏️✨】
— 【公式】サイバーエージェント新卒技術採用 (@ca_tec_des) 2018年8月2日
サイバーエージェントにて「Kotlin Fest 2018」スカラシップ枠の募集を開始https://t.co/140Sf56Uco #サイバーエージェント #ブログ #CAエントリー
Kotlin Fest 2018 における スカラシップ枠 とは,
・Kotlin Fest 2018 のチケット提供(懇親会 有) ・希望者には8月24日(金)の社内見学・技術者とのランチ会に参加いただけます。 ・遠方在住の方には宿泊場所を手配。 ・交通費支給。
※ 公式ページ より抜粋
となっており,地方学生にとってはとっても嬉しいプランでした😉
また,申し込みには,
・Kotlin Conferenceに参加したい理由を300文字以内で ・githubアカウント ・これでの制作物で一番よく出来たもののURLなど
※ 公式ページ より抜粋
以上3項目を書いて送るだけ!とっても簡単でした!
参加する目的
このカンファレンスは,「Kotlinを愛でる」をビジョンに,Kotlin の知識の共有や,Kotlin を愛してやまない人々に対して交流の場を提供することを目的に開催されました.僕自身,普段から Kotlin は使っていますし,第一線で活躍されているエンジニアの方々と交流を深めながら Kotlin の言語仕様を理解することはもちろん,Android x Kotlin に関する最新技術をキャッチアップしたい!と思って参加しました(特に Kotlin Coroutine).
セッション
早速セッションについてみていきましょう.本当は全部のセッション見たかったのですが,2セッション同時に発表されるので,タイトル見て直感で,面白そう x 明日からすぐに役立ちそう
なセッションをピックアップしてみました.
Kotlin で改善する Android アプリの品質
概要
なぜ品質の話?
Android アプリにおける品質とは?
- 速い
- 落ちない
- 使いやすい
- 変更しやすい
- 読みやすい
ソフトウェアの品質 = 要因の組み合わせ
- 内的品質要因 ... スピード,使いやすさなど
- 外的品質要因 ... モジュール性,読みやすさなど
- 特に重要な要因は...
- 正確さ + 頑丈さ = まとめて信頼性と呼ぶ
- 拡張性 + 再利用性 = まとめてモジュール性と呼ぶ
- 特に重要な要因は...
Java で広く実践されている規則は Kotlin だとどうなる??
Kotlin アプリのリファクタリングポイント
概要
- Gradle Kotlin DSL
- Gradle のスクリプトを Kotlin で書ける(build.gradle.kts)
- 補完・ジャンプコードが効くぞい
- 処理の共通化
- Mutable を避ける
- 今どんな値が入っているのか常に考えないといけなくなる
- 暗黙の前提はバグの温床
- その var は本当に var にしないといけないのか? (ここの話に使われていた遷移図がめっちゃ分かりやすかった)
- その MutableList は本当に MutableList にしないといけないのか?
- var と MutableList の併用は避ける
- val x MutableList の組み合わせ
- var x List の組み合わせ
- var と MutableList の併用は避ける
- 今どんな値が入っているのか常に考えないといけなくなる
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 の言語機能で実現可能
- 構造的なデータ宣言,設定値の記述が簡潔になる
- Anko や Spek などに使われてる機能らしいが僕にはまだ早かった...
- 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 など
- ただしローカル変数名との衝突に注意
- Intent の putExtra など
- キャプチャする時
- データ構造と状態数
- 肥大しがちな data class
- バグの温床なので,ビジネスロジックを考慮して,ありえない状態を避けよう
- 状態整理 --> Either,Pair
- 意味付け --> sealed class,data 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 を返す
- Any を KotlinItem に変換できるか?
obj as? KotlinItem
Kotlin コルーチンを理解しよう
- コルーチンとはなにか
- Kotlin ではコルーチンをどうやって実現しているのか
- コルーチンをステートマシンに変換している
- 中断と再開を状態遷移と考える
- もちろん再開に必要な情報もステートマシンに持っておく
- 逐次内部状態を変えながら実行
- コルーチン --> ステートマシン
- ここで出てくるのが
suspend修飾子
ラムダ式
につけるとコルーチン本体関数
につけると中断する場所を表す
- コルーチンを任意のタイミングで再開するには??
- 実は,suspend 関数にはコンパイル時に継続インターフェースの引数が生える
- 継続インターフェースを使って再開処理
- suspend ラムダはコルーチンの範囲を決める
- ここで出てくるのが
- 正直コルーチンを自作するのはしんどい
- そこで出てくるのがコルーチンビルダー
- launch(結果を伴わないコルーチンビルダー),async(結果を返すコルーチンビルダー) ...
- 継続インターセプターで実行スレッドを指定できる
- CommonPool,JavaFx,UI,Swing
- そこで出てくるのがコルーチンビルダー
- コルーチンをステートマシンに変換している
- Kotlin コルーチンの基本的な使い方
- まずは launch(結果を伴わないコルーチン)を作成してみる
- start ... コルーチンの開始方法
- parent ... 複数のコルーチンを一気にキャンセルしたい場合などに親Jobを指定する
- onCompletion ... コルーチン終了時に呼び出されるコールバック関数を指定
- context ... 共有データや継続インターセプター
- 例えば Android で UI をいじるコルーチンを作る場合は,デフォルトではスレッドプールでコルーチンが走ってしまうので,launch(UI) でコルーチンの処理は常にUIスレッド上で再開するように継続インターセプターを指定する.
- async / await
- エラーハンドリング
- 普通に try-catch を使う
- コルーチンの直列・並列実行
- 擬似コードで表すと,
- まずは launch(結果を伴わないコルーチン)を作成してみる
直列実行 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 して結果待ち
- キャンセル
- リトライ
- コルーチン内で,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 環境になってしまう
- Illegalstateexception や Nullpointerexception の原因に
解決策
カスタムコンバータを導入してみて
- hotfix 切ることが激減した
- コードの信頼感が高まりレビューが簡素化
- 何より手軽に Java --> Kotlin ができるようになった😉
ブースと懇談会
最後に,展示ブースと懇談会の様子をお届けします.
ブースで面白かったのが,サイバーエージェントさんの Kotlin Puzzlers です.
午前午後と2問ずつ Kotlin に関する問題が出題されていたのですが,
これがめちゃ難問でした(それぞれ1問ずつしか解けなかった😂).
本日はKotlin Fest 2018です。サイバーエージェントのブースでは、Kotlin Puzzlersを実施中!ぜひ遊びに来てください〜! pic.twitter.com/iJu3kSGw8Y
— CyberAgentDevelopers (@ca_developers) 2018年8月25日
また,ヤフーさんのブースでは黒帯本がもらえる抽選会や,
モブプログラミング教室も開催されており,かなり盛り上がっていました!
写真は黒帯抽選会の様子です
そしてなんと抽選会当たりました笑(ありがとうございます)
#KotlinFest Yahoo さんのブースにて頂きました!ありがとうございます😊 pic.twitter.com/7KkNwEp6ru
— はんちょー (@fmzk326) 2018年8月25日
クロージング後はブースやっていた場所で懇談会をやることになりました.
とりあえず写真貼っていきます(肉が美味しかった).
おわりに
Kotlin Fest 2018 の様子をレポートしてみましたがいかがでしたか? さらに詳しく知りたい方は,#kotlinfest で ツイッター検索してみると,ここでは紹介しきれなかったセッション資料や,ブース展示の様子などよく分かりますよ!
次回開催はまだ未定とのことですが,今回のイベントは大盛況でしたし,是非来年も開催してほしいです(今度はLT枠などで喋ってみたい).今後も Android x Kotlin のイベントには積極的に参加していくので,僕もイベントを盛り上げられる一員となれるように楽しくやっていきたいと思います!
最後に,Kotlin Fest を開催してくださった運営の皆様,スポンサーの皆様,さらにスカラシップ枠を用意してくださったサイバーエージェント様ありがとうございました.こういったスカラシッププログラムは,地方学生にとっては非常にありがたい内容ですし,今後も継続していただけると,全国の学生の助けになると思います.
ここまでお読み頂きありがとうございました🙇
Kotlin かわいい!
おしまい
場数を踏む大切さ
最近,藤田晋の仕事学を買いました. その中で「効率よりも場数が能力を決める」というコラムを読んで プログラミングを学ぶ上でも大事そうなことだったので自戒も込めて記事にします.
効率よりも場数が能力を決める
プログラミングを学習する上で,「短時間で効率的に学びたい」と思う人は少なくないと思います. これはプログラミングに限った話ではなく,例えば英会話レッスンなどにも当てはまるでしょう.
個人的に,プログラミングとは目的を達成するための手段の一つに過ぎないと考えていて,
プログラミングを学習することが目的化してしまっては本末転倒です.
特に,学生時代は(自分もまだ学生だけど)講義や趣味でプログラミングを勉強したり,
あるいはインターンしながらプログラミングを学んだりする機会が多く,プログラミングの学習
が目的化しやすい環境なのではないでしょうか.
僕もつい最近まではそうでした. 書籍やプログラミング学習サイトで言語仕様は理解しますが,理解するだけです.理解止まり. 実は,脳内の知識を,自分の技術として落とし込むためには.実際学んだことを活かす環境を作ってアウトプットすることが大事なんですね. もっと言えば,知識を吸収している段階で,「この本のこのページの文法,今作っている個人アプリのこの処理に適用したらもっと簡略化できるなぁ,早速試してみるか!」 くらいの脳内フットワークの軽さがあればグッドだと思います.
ここで話を戻して,「短時間で効率的に」プログラミングを学ぶために重要なことについて考えます. 今までこうしたネット記事を読んでいて分かったのですが,この場合の対象者って大体これからプログラミングを学び始めるか, あるいは最近学習を始めた人が多い気がします.そして僕も実際学生です.
こうした初学者が,「短時間で効率的に」といって情報を探し回る事自体が非効率です.
プログラミングができる,できないといった能力の差は,場数=経験の差で決まってくるからです.
経験も何もないのに,効率論
を学んでも,まるで無意味でしょう.
重要なのは場数です. とにかく目で情報を読み取り,頭で理解して記憶する.そして手を動かして実行する. この一連のサイクルが自分の技術力を向上させる秘訣ではないでしょうか.
ひたすら場数を踏む,こうしたやり方は効率が悪いように感じる人がいるでしょう. 確かに,私も最初はそう思いました.しかし実際は,場数を踏むことでしか自分の中の知識や選択肢を広げられず効率は上がらないのです.
この文章は例の本からの引用です,とても説得力があります. 自分の考え,行動,その全てはこれまでの人生の経験が影響しているんですね.
プログラミングを経験
するためには,コードを書いて実行して,デバッグする必要があります.
変なプライドは捨てて,がむしゃらに知識を吸収してコードを書く,
この行動の全てが結局「短時間で効率的に」プログラミングを学ぶことに繋がってくるのではないでしょうか.
結局の所,毎日コツコツが大事なんですよね.
最後までお読みいただき,ありがとうございました.