作者別: Gen

Floobitsを試してみた

Floobitsというのはリアルタイムコラボレーションを実現するためのサービス。
VSCodeのVisual Studio Live Shareみたいなやつで、リモート経由でのペアプログラミングとかを実現するためのサービスである。

リモート経由でペアプロしたいが、Android開発でそんなことできるのかというのが出発点だった。
Android StudioのベースとなっているIntelliJ IDEAでもそういう機能があるのか調べたところ、今の所ないというのが結論。
要望自体ははるか昔からあるみたいだが、実現するにはコラボレーション用のサーバ用意したりとハードルがあまりに高いだろうことは想像に難くない。
公式には用意されていないが、見つけたのがこのFloobitsというサービスである。

別にIntelliJに限らず、他のプラットフォームでもプラグインが用意されている。
https://floobits.com/help/plugins
Emacs, Sublime Texxt, Neovim, IntelliJ, Atomと多数のエディタに対応している。
Free planであれば5つのWorkSpaceを持てる。
Android Stuioでいえば1つのプロジェクト=WorkSpaceになるだろう。
Free planではprivateなWorkSpaceを持つことはできないので、ソースコードは誰でも見れる状態になる。
Edit権限を与えなければ勝手に編集されることはない。
ちなみにWorkSpaceはActive Workspaceで確認できる。

どんな感じなのか気になったので、パソコン2台使ってとりあえず試してみた。
試した環境が同一LAN内にあるPCではあったことが関係している可能性はあるけれど、遅延はほぼないと思っていいだろう。

Summonなる機能があって、これを使えば他のコラボレーターを自分が編集しているファイルに瞬時に招集することが可能。
「このファイルがさー」「どれだよ?」
なんてときに活躍しそう。
とりあえず試したのはコード編集だけではあるが、他にもチャットができたりコラボレーションのための便利な機能が盛り込まれている。

ただ、ペアプロに便利とはいうものの、Floobitsはあくまでリアルタイムでのペアプロ目的に使うにとどめた方がいいだろう。
Floobitsにアップロードしたプロジェクトを、Floobitsに接続しない状態で更新→FloobitsのWorkSpaceに接続という流れになると、リモートのファイルを上書きするか、リモートのファイルでローカルを上書きするかの2択になってしまう。
Gitでバージョン管理していても、FloobitsのWorkSpace自体はGitで管理されているわけではないので、下手するとGitの履歴自体が上書きされてなかったことにされる恐れもありそう。
プロジェクトはGitで別途管理して、サブ的にFloobitsを使うという感じがいいのかもしれない。
ゼロからいきなりFloobitsのWorkSpaceにジョインして開発を進めるとおかしなことになりそう。

gitと併用してFloobitsを使う場合のFAQがあった。
https://floobits.com/help/faq
運用でカバーする必要があるっぽい。

リモート経由でペアプロするのには非常に便利だと思う。
またFloobitsはAndroidに限らず使えるというのは便利な点だろう。
セキュリティ的にどうなんだというところがクリアできるなら、普通に便利な気がする。

TouchDelegateを使ってタッチ可能領域を拡張する

Buttonのクリックに反応する範囲を拡張したい時がある。
今回は下図のようなカスタムViewを作りたかった。

Image

ImageButtonとTextViewを内包したViewである。
親レイアウトのViewGroupをタッチする=ImageButtonをタッチするという扱いにしたかった。

ImageButtonにしているのは、他のViewとの兼ね合いである。
他のViewはImageButtonにstyle="@style/Base.Widget.AppCompat.ActionButton"を適用して、アイコン画像の周りにだけリップルエフェクトがかかるようになっていて、このカスタムViewもそれに合わせたかったのである。

これはTouchDelegateを使うと実現できる。

TouchDelegate – Android Developers

使い方

TouchDelegateは親のViewGroupに設定する。

TouchDelegateのインスタンスを作成するには、タッチエリアを表すRectと委譲先のView(今回でいうとImageButton)への参照が必要。
Rectはローカル座標系(ディスプレイ上での絶対座標ではなく、ViewGroupの左上を0とした相対座標)を使う。
今回はViewGroupのgetDrawingRect()を使って取得したRectを使った。

ぐぐるとgetHitRect()を使った例がみつかったが、これだとうまく動かなかった。絶対座標になっているからだと思われる。

コード例

class CommentStatusView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {

    private lateinit var binding: ViewCommentStatusBinding

    init {
        binding = ViewCommentStatusBinding.inflate(LayoutInflater.from(context), this, true)
        doOnPreDraw {
            val rect = Rect()
            binding.viewGroup.getDrawingRect(rect)
            binding.viewGroup.touchDelegate = TouchDelegate(rect, binding.imageButton)
        }
    }

    override fun setOnClickListener(l: OnClickListener?) {
        binding.imageButton.setOnClickListener(l)
    }

}

Kotlinで、Android-ktxとDataBindingを使っている。

doOnPreDrawがAndroid-ktxを使っている部分で、ViewTreeObserverを利用してViewの大きさが決まった後で中のブロックの処理(TouchDelegateの設定)を行っているだけである。

後はこのカスタムViewに対するsetOnClicklistenerをImageButtonに対して設定するようにしている。

ポイントは

  1. 親のViewGroupに対してTouchDelegateを設定する
  2. rectはTouchDelegateを指定するViewGroupからみて、どの位置のタッチイベントを委譲先に渡すのかをローカル座標系で指定する

ドキュメントに書いてあるとおりなのだが、英語力がなくていまいちわからず、ググったコードを参考にしながらやってもうまくいかずでちょっとハマった。

実際の動きはこんな感じになった。
リップルエフェクトもクリックリスナもViewGroupに対するものがImageButtonを押している扱いになっている。

Ezgif 4 fc7d03caa2

TextViewに設定したテキスト内のURLに遷移する

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

TextViewに設定したテキスト内にURLがあった場合に、そのリンクをクリックできるようにしたい。
クリックしたらブラウザが開いて該当ページに移動できるようにしたい。

手っ取り早くこの要望を満たそうと思ったら、TextViewには便利な機能が用意されている。
TextViewにandroid:autoLink="web"を追加すればよいだけである。
これでテキスト内のURLをクリックしたらブラウザが開いてくれる。
めでたしめでたし。

といくなら楽でよかったのだが、この機能によるURLの処理はあまり正確ではない。

autoLinkの問題点

Spannable

URLが半角スペースで区切られていたり、2バイト文字以外で区切られていたりしたら正しくリンクとして拾ってもらえる。
例えばあいうえお https://android.gcreate.jp/ かきくけこというテキストであればURLの部分のみがURLとして識別される。
しかしあいうえおhttps://android.gcreate.jp/だとリンクを拾ってくれない。
またあいうえお https://android.gcreate.jp/がリンクになってほしいとした場合、「がリンクになってほしい」という部分までURLとして拾われてしまう。
2バイト文字でない半角カッコで囲ってかっこで(https://android.gcreate.jp/)とした場合、閉じカッコもURLに含まれてしまう。

URLの抽出がうまくいかない場合があるのが最大の問題であるが、URLのハンドリングをカスタマイズできないのもちょっと不便である。
例えばChromeカスタムタブでリンクを開きたい場合に、autoLinkでは対応できない。
autoLinkの場合、リンクをクリックするとACTION_VIEWの暗黙的インテントが発行される。

自前で処理する

以上の問題点を回避するには、自分でテキストにClickableSpanを設定してやると良い。

  1. テキストをSpannableStringに変換する
  2. テキストから正規表現を利用してURLを抽出する
  3. 抽出したURLを用いてSpannableStringにClickableSpanを設定する
  4. TextViewにSpannableStringをsetTextで設定する
  5. TextViewにsetMovementMethodを設定する

以上の手順で独自のClickableSpanを設定することができる。

コード的にはこんな感じ(Kotlinとandroid-ktxを利用している)。

val text = "あいうえおhttps://android.gcreate.jp/かきくけこ"
val spannable = text.toSpannable()
val matcher = Pattern.compile("(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]").matcher(text)
while (matcher.find()) {
  val url = matcher.group()
  val start = matcher.start()
  val end = matcher.end()
  spannable.setSpan(MyUrlSpan(url), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
textView.text = spannable
textView.movementMethod = LinkMovementMethod.getInstance()

MyUrlSpanは自分で定義する。
といっても大したことはやっていない。

class MyUrlSpan(val url: String) : ClickableSpan() {
    override fun onClick(view: View) {
        Snackbar.make(view, "$url clicked", Snackbar.LENGTH_SHORT).show()
    }
}

SnackbarでURLを表示しているだけ。
リンクをクリックした際の挙動はこのonClickでカスタマイズできる。

もう少し詳しく

このあたりの挙動が知りたければ、TextViewのソースコードを確認するのが手っ取り早い。
mAutoLinkMaskをキーに見ていくとだいたい分かると思う。

ClickableSpanを設定する際に必要なのは、文字列中のどこからどこまでにClickableSpanを適用するかが必要になる。
これは正規表現を使ってマッチさせれば該当する文字列、その文字列の開始位置・終了位置が分かるのでそれを使えば良い。
正規表現はこちらの記事を参考にさせていただいた。
AndroidのTextViewのautolink=webが冗長になる

setSpan()する際の第4引数はflagsである。
https://developer.android.com/reference/android/text/Spanned
これはEditTextにSpanを設定しないのであれば、おそらく何を設定しても影響はないと思う。
(Spanを設定したテキストの内容が動的に変化する場合に、変更前に設定したClickableSpanの開始位置・終了位置がどう変動するかを指定するフラグだと思うので)

TextViewのTextをSpannableにして、ClickableSpanを設定しただけではリンクをクリックすることはできない。
ClickableSpanのonClickが呼び出されるためには、setMovementMethodで何らかのMovementMethodがTextViewに設定されていなければならない。
autoLinkを使った場合のLinkをタップしたときの動きはLinkMovementMethodが使われているのでここはそのまま流用する。
ClickableSpanのonClickを呼び出すかどうかは、TextViewのonTouchメソッド内の処理を確認すれば分かるが、mMovementがnullではないことが条件になっている。
だからTextView.setMovementMethod(LinkMovementMethod.getInstance())を行っているのである。

Spannable.Factory

上記のコード例では、TextViewにsetTextする際にSpannableStringを準備してからsetTextで渡している。
この方法だとRecyclerViewなどでTextViewを再利用する状況を考えるとややめんどくさい。
ViewHolderにbindするときに、設定するテキストをSpannableに変換して、正規表現使って検索して・・・となるわけで、bindする部分のコードがすっきりしない。

かといってTextViewを継承した独自のカスタムViewを用意するのも、TextViewのsetTextをオーバーライドできないのでこれもあまり美しくない。

そういう場合にはSpannable.Factoryを使うと良い。

TextViewにsetSpannableFactory()を使ってSpannable.Factoryのクラスを設定すると、そのTextViewのsetTextを呼ぶだけで指定したSpannableの処理を行ってくれる。

object UrlSpanFactory : Spannable.Factory() {
    private val regex = Pattern.compile(
        "(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]")

    override fun newSpannable(source: CharSequence): Spannable {
        val spannable = source.toSpannable()
        val matcher = regex.matcher(source)
        while (matcher.find()) {
            val url = matcher.group()
            val start = matcher.start()
            val end = matcher.end()
            spannable.setSpan(MyUrlSpan(url), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        }
        return spannable
    }
}

...


val textView: TextView by lazy { findViewById<TextView>(R.id.text_view) }

textView.setSpannableFactory(UrlSpanFactory)
textView.setText("あいうえおhttps://android.gcreate.jp/かきくけこ")
// これでURLの部分がリンクとして修飾される

https://medium.com/google-developers/underspanding-spans-1b91008b97e4

babelのセットアップ覚書

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

JavaScriptのモジュールシステムが全く理解できなくて困っていたのだが、ようやく友だちになれそうな程度にはわかるようになった。

React Nativeやるときにはimportとかexportを使うのに、Node.jsのときはrequireになるのが解せない。
JavaScriptやるのにこいつらは避けて通れないものなのに、そもそもモジュールの仕組みもわからなくて往生した。
歴史的経緯でそうなっているのはわかったけれども、これからJavaScriptやるならどっちかにまとめたいところ。

今からやるなら新しい構文のimportで統一したいが、Node.jsはimportはそのままでは理解してくれない。
そこでトランスパイルが必要になる。

で、トランスパイルが必要なのもわかったけれども、ではどうやって準備すればいいのかがわからなくてこれまたハマる。
このあたりのセットアップがよくわからなくてJavaScript敬遠していたのもある。

ようやくbabelのセットアップのやり方、意味がわかったので覚書として残しておく。
とりあえずこれでJavaScriptであれこれ気楽に試せるようになった。

セットアップ

Node.jsのインストールとかは割愛。
nodeとかnpmは使える状態での手順。

  1. npm initでnode moduleのセットアップ
  2. babelのインストールnpm i --save-dev @babel/core @babel/cli @babel/node @babel/preset-env
  3. .babelrcの設定
  4. npx babel-node xxx.jsでxxx.jsをbabelでトランスパイルした上でnodeで実行

勉強のためにES6で書いたJavaScriptをとりあえずnodeで動かして動作確認できるようにするだけの目的での話。
実際にプロダクトとして動かす場合はbabel-nodeは使わない。

https://babeljs.io/docs/usage/cli/#babel-node

補足

babelのインストールはcore, cli, node, preset-envがあればとりあえずは足りた。

とりあえずnpm i -gは使わない。
babelのバージョンをプロジェクトごとに管理するかどうかはわからないけれども、グローバルにインストールしなくてもnpxを使えばちゃんと動かせるので問題ない。

@babel/coreなどの@babelの部分はscoped packageといって、別にアノテーションではなくnpmのユーザ名である。
npmの仕組み上パッケージ名が早いもの勝ちになってしまうので、例えば私がbabel-some-great-utilみたいな名前でパッケージを上げたりできてしまう。
紛らわしいし、使いたいパッケージ名が使えないなんて問題が起こるので、その対策として生まれた仕組み。

@babel/coreなどはbabelのバージョン7になる。

今回はとりあえず動けばいいので、.babelrcはこれだけで問題ない。

{
  "presets": ["@babel/preset-env"]
}

本来なら動作環境に合わせて設定すべき。

https://babeljs.io/docs/usage/babelrc/

.babelrcではなくpackage.json内に"babel"ブロックを定義することでも代用できる。
この設定ならpackage.json内に書いても問題ない気もする。

スクリプトの実行はnpx babel-node xxx.jsで、トランスパイル→nodeで実行をひとまとめにできる。

ファイルの監視

さらにJSファイルを書き換えて都度コマンドを叩くのは面倒くさいので、変更を監視して自動的に再実行させる。

npm i --save-dev nodemonでnodemonをインストールする。
npx babel-node xxx.jsnpx nodemon xxx.js --exec babel-nodeに置き換えて実行する。
これだけ。

package.jsonのscriptを使う

毎回コマンドを打つのは面倒くさいので、package.jsonのscriptブロックを利用して簡略化する。

  "scripts": {
    "start": "nodemon xxx.js --exec babel-node"
  },

startよりwatchの方が適切だろうか。とにかく、"start"の部分は自分で好きにすればいい。
こうしておくとnpm run startで記述したコマンドが実行される。
package.json内だとnpxコマンドは必要ない。

shields.ioを使ってバッジを表示する

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

http://shields.io/を使ってバッジを表示する。

今回はDart Packagesで公開されているパッケージのバージョンを表示するバッジを出したかった。
Dart Packagesはshields.ioでサポートされており、https://img.shields.io/pub/v/box2d.svgというような感じでパッケージのバージョンを表示することができる。
ちなみにbox2dの部分がパッケージ名なので、ここを自分が表示したいパッケージ名に書き換えればよい。

この方法で表示されるパッケージのバージョンは、安定版の最新バージョンが表示される。(安定版というのは1.0.0-rc1みたいにセマンティックバージョンの後ろに何もつかないやつのことを指している)

https://img.shields.io/pub/v/box2d.svg box2d

https://img.shields.io/pub/v/pointycastle.svg pointycastle

本題

ところでDartはバージョン2への移行時期であり、Flutterで利用する場合には開発版を利用する必要があったりする。
例えばredux_persistpointycastleなどがそう。
ではこれら開発版のバージョンのバッジを表示するためにはどうしたら良いのだろうというのが今回の本題。

今回はPointy Castleというライブラリの開発版バージョンを表示する。
これを書いている時点での安定版は0.11.1で開発版は1.0.0-rc1となっている。

Dart Packagesでは各パッケージのバージョン情報をjsonで取得することができる。
https://github.com/PointyCastle/pointycastleの最後に.jsonとすればよい。
shields.ioではJSONからクエリを使って直接バッジを作成することができる。https://shields.io/#dynamic-badge
クエリはjsonpathを使ったクエリが使える。
今回の場合使うクエリは$.versions[?(@=="1.0.0-rc1")]になる。

https://img.shields.io/badge/dynamic/json.svg?label=pub&url=https%3A%2F%2Fpub.dartlang.org%2Fpackages%2Fpointycastle.json&query=%24.versions%5B%3F(%40%3D%3D%221.0.0-rc1%22)%5D pub

ちなみにこの方法では動的に最新バージョンを取得することはできない。
(クエリをうまいこと扱えばできるのかもしれないが、私にはわからなかった)
例えばクエリを$.versions[?(@.includes("rc"))]とするとバージョンにrcを含むものだけが取得できる。
しかしrcを含むバージョンが複数ある場合、それら全てが列挙されてしまう。

$.versions[?(@.includes("+"))]をクエリに使った場合: +versions

FlutterでBottomAppBarを表示

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

サンプルはここを見るといい。

https://github.com/flutter/flutter/blob/master/examples/flutter_gallery/lib/demo/material/bottom_app_bar_demo.dart

このサンプルではFABの位置やノッチの有無などをカスタマイズできるようにしている関係でなんだかややこしそうに見えるが、やることは非常に単純。

  • Scaffold内のbottomNavigationBarにBottomAppBarを渡す
  • Scaffold内のfloatingActionButtonLocationでFABの位置を指定
  • ノッチの有無はBottomAppBarのhasNotchで指定
  • 必要なメニュー等はBottomAppBar内のchildに追加

これだけでBottomAppBarを表示できる。非常に簡単。
BottomAppBar内のアイテムを複数指定したい場合、childなのでRowでくるむなどの工夫が必要。

flutterの公式リポジトリをクローンしてきて、material demoを動かすとどんな動きか確認することができる。

ちなみに公式リポジトリ内のexampleを動かすにはここを見ればよい。

flutterの初期設定を済ませていることを前提として、手っ取り早くexampleを実行するために必要なことをまとめるとこうなる。

  • クローンしたあとbinディレクトリにパスを通す(リポジトリ内の最新版flutterを使うため)
  • flutter update-packagesを実行して各種依存パッケージの解決を行う
  • IntelliJ IDEAを使っているならflutter ide-config --overwriteを実行することでRun Configurations等の設定をしてくれる
  • 後は実行したいexampleをIDEから選んで実行

binディレクトリにパスを通すと、普段使っているflutter(私の環境だと0.3.2)と公式リポジトリのflutter(これ書いてる時点では0.4.4-pre.8)が混じってしまうのだけど、公式リポジトリを触っているときだけ後者を有効にするみたいな設定の仕方ってあるのだろうか。
ありそうな気がするけど私は知らないので、都度パスを追加して対応している。

追記

別にリポジトリをクローンしてこなくてもPlayストアにアプリが公開されていることを知った。
https://play.google.com/store/apps/details?id=io.flutter.demo.gallery
動きを確認するだけならこっちのほうが手っ取り早い。

Settings Repositoryプラグインを使ってIDEの設定を共有する

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

いつもOverwrite local/Overwrite remoteが、どっちがどっちなんだっけと使うときに混乱するのでメモ。

どっちがどっち、というのはovwerwrite localが、現在の設定をリモートリポジトリの設定で上書きするのか、現在の設定でリモートの設定を上書きするのか混乱してしまうのである。
Overwrite localは現在の設定をリモートの設定で上書きする、が結論なんだけど。
これがわかりにくいと思うのは私の英語力がないせいなのだろうか。

Settings repositoryの設定について

ちなみにSettings Repositoryをご存じない方向けに簡単に紹介。

IntelliJ IDEAで使える設定共有用のプラグイン。
私の場合で言うと、Android Studio(stable/canary)とIntelliJ IDEA community editionを行ったり来たりすることがあるので、このプラグインを使って設定を共有している。
バージョンアップする際に以前の設定を引き継ぐというのはできるが、同時に運用しているときにAで行った設定変更をBでもまたやらないといけない、というのが起こらなくなるので非常に便利だと思う。

利用しているプラグインによっては、そのプラグインの設定ファイルもバックアップ対象になるらしい。
例えば私はIdeaVimを利用しているが、どのファイルのどの位置にカーソルが移動した、という情報までバックアップ対象になってしまっている。
そのため必要に応じて不要な設定ファイルは.gitignoreで管理対象から外すなど工夫が必要。
(私はvim_settings.xmlは管理対象から外している)

MacであればFileメニューのところにSettings Repositoryがあるので、GitHubなりBitbucketなりで管理用リポジトリを用意してそのURLを設定すれば使えるようになる。

ちなみにどうもエディタでタブを使わないとか、キーマップの設定でどれを使うかなどまでは合わせてくれないようだ。
例えば私の使っているキーマップはMac OS X 10.5+ copyとなっているのだが、このキーマップ設定自体は共有してくれるものの、これが有効な状態にまでは復元してくれない。
そのため、どのキーマップを使うか、コードスタイルはどれを使うかなどは手動で直さないといけないようだ。

FlutterでTwitterクライアントを作ってみた

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

FlutterでTwitterクライアントを作ってみた。
レイバン製造機になる未来しか見えないので公開したりはしないけれど。
とりあえずマルチアカウント対応とタイムラインの取得を実装して力尽きた。
twitter_loginなどのライブラリがflutter(dart)でも存在しているようだが、そういうのは利用せずに直接APIとやり取りする形で実装した。

使ったライブラリ

Access Tokenの取得はこの記事を参考にして実行した。

Kotlinで書きたい問題

Androidネイティブから入門した身からすると、dartではなくKotlinで書きたいという気持ちでいっぱい。

はじめはセミコロンのつけ忘れが多発し、その次はインスタンスを生成する際にnewを付け忘れるが多発して困ったからという理由が大きかった。newのつけ忘れは、なくても動く場合と、つけないと動かない場合があって、その違いがいまいち分かっていない。
しかも付け忘れて動かないのは、実行しないとわからないのが更に困りものだった。

Kotlinで書きたい理由は、今だとこの2種類の理由から。

  • data classが使いたい
  • Null許容・非許容を型で表現したい

data classについてはdartのissueに上がっているので、そのうち実装されるかもしれない。
Javaで書くことに比べたらdartでのデータオブジェクトの記述はスッキリしている(getter/setterを定義する必要はない)のだが、data classならequalsメソッドをいちいちオーバーライドしなくても済むとか、そういうところが便利だと思うので、早くdartにもdata class来てほしい。

まあそれは来たらいいな程度の気持ちだけど、null許容・非許容を型で表現したいのは結構切実な問題である。
dartはカジュアルにnullが飛んできすぎな気がする。
dartではあらゆるものがオブジェクトなので、intだろうと初期化されていなければnullとなるので、nullと格闘することが多かった。
このあたりは私の実装の仕方が悪いという面も大きいかもしれない。
が、それにしてもnull許容・非許容が型で表現できるKotlinを知っていると、nullで消耗するのがつらい。
Textにnullが渡ってアプリがクラッシュする→どこでnullが渡ったのか把握しきれないとかいうのが多くてね・・・。

dartのasync/await便利

asyncなメソッドを定義してやればそれだけで非同期処理が記述できてしまうのは非常に便利だと思った。
awaitと組み合わせて複数のリクエストを同期的に書けるのはめちゃくちゃ楽だった。
Androidネイティブだと、RxJavaを使って連結させてやるような処理がシンプルに書けるのはよい。
ネットワークを使った処理がシンプルに書けるのはすごい楽だった。

redux

アプリ内の状態を管理するのにreduxを使った。

最初はflutter_reduxから導入の仕方を学んだのだが、redux初見の私にとってはこれは悪手だった。
なぜシングルトンで管理しているはずの状態を、わざわざ_ViewModelに移し替えているのか最初は理解できなくて混乱した。
flutter_reduxはredux.dartとflutterの橋渡しをするライブラリなので、そこを切り離して考えないといけないだろう。

dart.redux

dart.redux

アプリケーションの状態を1つのクラスに集約する(名前は別になんでもいい。以下利便性のためにAppStateと記述するけど、名前はなんでもいい)
状態クラスはイミュータブルで変更不可にする(重要)

AppStateはアプリケーション内で唯一の存在となるStoreが保持する

状態を変更するのはActionで、ActionはStore経由でdispatchする。dartには型があるので、○○Actionみたいなクラスを作っていく。

Actionと状態の変更を対応付けるのがReducer

Reducerはネストというか、AppStateが持つ各フィールド(個々のアプリケーションの状態。カウンタの値とか、Twitterクライアントならツイートの一覧とか)とReducerを個別に対応させることもできる。
この場合、AppStateの特定のフィールドの値と、それを変更するActionの対だけをそのReducerで管理すれば良くなるので、見通しが良くなる。

Reducerでは変更する状態とActionの組み合わせを使って新たな状態を返す。

AppStateの変化はStoreからStreamとして流れてくる。このStreamはbroadcast streamなので、状態変更に関心を持つやつが、このStreamlisten()すれば状態変更をキャッチできる。

MiddlewareStoreActionがdispatchされて、そのActionReducerに渡される前に処理を挟むことができるやつ。例えば流れてくるActionをログに保存するとか、状態をファイルに保存するとか、1つのActionを複数のActionに分割するとか。
今回のTwitterクライアントでは、サインインのアクションをMiddlewareで処理して、その結果からAccess Tokenを更新するActionを発行してアプリ内のAccess Tokenを更新するような処理を行った。

dart.reduxのおさらい

  1. アプリケーションの全状態を保持するAppStateを用意する
  2. StoreAppStateを保持する
  3. Store経由でActionを投げる
  4. ReducerActionを元にAppStateを更新する
  5. StoreonChangelisten()することでAppStateの変更を監視する

flutter_redux

flutter_reduxが担うのは、各WidgetからStoreへアクセスするしくみを提供すること。

  1. Storeの保持
  2. 保持したStoreへのアクセス
  3. Storeからの変更のlisten()の隠蔽

たぶんflutter_reduxを使わなくても、Storeは結局シングルトンなので、直接アクセスするしくみを作ってしまえばそれで済む(はずだし、そっちのがシンプルでわかりやすい気もする)。
一方で、AppStateの変更はStreamで流れてくるので、この購読と解除をしなければならない。その部分をflutter_reduxは隠蔽してくれるので自分で管理しなくて済む、というところにメリットがあると思われる。

アプリケーションのルートWidgetStoreProviderにして、Storeを保持する。

子のWidgetではStoreConnectorを通じてStoreにアクセスする。
StoreConnectorconverterを経由してAppStateViewModelに変換する。

ViewModelというのは、該当のWidgetを構築するのに必要なだけの状態を別途切り出したやつというイメージ。
状態変更の度にWidgetの更新が走らないようにする役目もある。

状態を持ってるのにStatelessWidgetになるというのがいまいち理解できなかったが、状態を管理しているのはStoreであってこのWidgetではないからだろうか。

データの永続化

redux_persist_flutterは、reduxで管理するアプリの状態をファイルに書き出して永続化するライブラリ。
今回Twitterクライアントをflutterで作っていて、データの永続化をどうすればいいのかいまいち分からなくてとりあえずこれを使った感じである。
別にシリアライズ方法はなんでもいいのだろうけれど、JSONにパースするようになっていたのでそのようにした。
しかしStoreで管理する状態が増えれば増えるほど、JSONへのパース処理を追加するのが大変になって辛くなっていく。

簡単なKey/Valueのデータであれば、shared_preferencesを使えばいいのだろうが、アプリのデータを保存するのには向かない。
自前でファイルに保存するか、Databaseのライブラリを導入して保存するかといったところなのだろうか。
どっちにしろデータオブジェクトとのマッピング処理を自前で実装しないといけないのが面倒くさいところ。

またデータの暗号化もどうしたらいいのやらという感じでよくわからない。

その他雑感

エラーメッセージがわかりにくいところがツライ。エラーの内容は分かるものの、そのエラーが自分の書いたコードのどの部分で発生しているのかを把握するのに大変労力を必要とする。

UIの記述がツライ。
ネストが深くなりすぎてツライ。
これ絶対後からいじれなくなるやつだと思う。

IDEのサポートが受けられるのはめちゃくちゃメリットだと思う。
IntelliJは偉大だ。
無料で使えるCommunity Edditionが使えるのは偉い。
React NativeだとWebStormになると思うので(IntelliJ系IDE使うなら)、そこはメリットだろう。

テストコードも書きやすいしいい感じだと思う。
ネストしたテストコードがすっきりと書けるのが非常に良い。
パラメタライズテストはちょっとやり方がよくわからなかった。
Mockitoも使えるし、使い方も普通にMockitoなので特に新しく学習する必要が無いのも良い。
Mockクラスを用意するやり方がちょっと変わってると思ったけども(class MockClass extends Mock implements RealClass{}と定義してやる)。

UIのテストは実際には書かなかったが、サンプル見る限りEspressoでUIテストを記述するより簡単にできそうな印象。
async/awaitがあるので待ち合わせとか特に考えなくても普通にコードを書いていくだけで実現できるのが良いと思う。

Android Studio2.x時代のプロジェクトを3.0環境へ更新

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

ストアに公開しているアプリは2018年8月までにtargetSdkVersionを26以上にする必要がある。その関係で以前にリリースしたアプリを更新しているのだが、その際に最初のハードルとなるのが古いAndroid Studioで作ったプロジェクトを現在のバージョンに更新する作業である。

最近何回も同じことをやっているので、備忘録的に書いておく。

基本的には全部Migrate to Android Plugin for Gradle 3.0.0に書いてあるので、そのとおりにやるだけである。

書いている途中でAndroid Studio 3.1が正式版になった。が、3.0から3.1への更新はそんなに大きく変更しなくても済むので、基本は3.0へのマイグレーションガイドに書いてあることに対応すればよいはず。

Gradleのバージョン更新

まず始めにGradle(Gradle wrapper)のバージョン更新を行う。Android Studio3.0は最低でもGradle4.1以上が必要。(3.1はGradle4.4以上)

gradle wrapperの更新はgradle-wrapper.propertiesファイルを書き換えてgradle syncを行うだけ。

私は./gradlew wrapper --gradle-version=<Gradleのバージョン>コマンドで更新している。これで更新したらシェルスクリプトの更新もかかる場合があるので、こっちのほうがいいのかなという雰囲気で選んでいるだけだったりする。

コマンドで更新した場合、distributionUrlで指定されるgradle wrapperが4.x-bin.zipといった感じでバイナリのみのものになる。Android Studioはallにしとけと変更を促してきて、結局手書きでdistributionUrlを書き換えることになるので、普通にマイグレーションドキュメントにあるようにgradle-wrapper.propertiesを書き換えるだけでいいと思う。

ちなみに利用するgradleのバージョンだが、私の場合Twitterで見かけたGradleのリリースツイートを見て覚えてるバージョンを利用している。今だと4.6。

使えるバージョンはhttps://gradle.org/releases/で確認できる。基本的には最新使っとけばいいんじゃないだろうか。

build.gradleの更新

  • android gradle pluginのバージョンを更新
  • repositoryにgoogle()を追加する(先にGradle wrapperのバージョンを上げておく必要がある)
  • 利用しているgradle pluginのバージョン更新

プロジェクトで利用しているgradle pluginのバージョンが古い場合、原因がよくわからないエラーが多発する。例えばNo signature of method: com.android.build.gradle.internal.scope.VariantScopeImpl.getGenerateRClassTask() is applicable for argument types: () values: []とか。こういうのは利用しているgradle pluginを最新のバージョンに更新すると解消されることが多かった。

基本的にAndroid Studioが利用しているプラグインの新しいバージョンがあれば教えてくれるはず(網掛けになって新しいバージョンがあることを示唆してくれる)ので、それに従ってgradle pluginのバージョンを上げてみると解決する場合が多いだろう。

エラーメッセージで検索してもこれといった解決策が見つからないという場合には、利用しているプラグインのバージョンを上げることを試してみよう。

ライブラリプロジェクトを利用している場合

昔はライブラリプロジェクトを利用している場合、publishNonDefault trueを設定して、アプリケーションモジュールをdebugビルドするときは、ライブラリプロジェクトもdebugビルドにするなんて指定をしている人もいただろう。

この設定をしている場合、Android Studio3.xにアップデートするにあたってちょっとした手直しが必要になる。

Android Studio3.0からはライブラリモジュールのビルド設定は、利用するアプリケーションモジュールのものと同じものが利用されるようになった。appモジュールでdebugビルドを選んだら、自動的に依存しているライブラリモジュールもdebugビルドでビルドされる。

そのため以下の手直しが必要。

ライブラリモジュール

publishNonDefault trueの記述を削除する。

アプリケーションモジュール

例えば以下のように記述していたとする。

debugCompile project(path: ":library", configuration: "debug")
releaseCompile project(path: ":library", configuration: "release")

この場合、この2行は不要になるので削除。その後あらためてimplementation project(":library")とライブラリモジュールへの依存を記述する。

buildTypeを増やしている場合

追加でbuildTypeに手を加えている場合(デフォルトのdebugrelease以外に定義している場合)、増やしたbuildTypeの定義にmatchingFallbacksという行を追加してやる必要がある。

これはそのbuildTypeが使われるときに、それが存在しないライブラリプロジェクトなどがどのbuildTypeを使えばいいかを指定するものだ。

Android Wearアプリを利用しているプロジェクト

ライブラリモジュールと同様の問題と変更が行われている。publishNonDefault trueが要らなくなって、debugWearApp ...という記述群がwearApp(":wear")の1行で済むようになった。

プロダクトフレーバーを利用している場合

新しくflavorDimensionsを最低1つは定義しないといけなくなった。

productFlavorは何らかの基準で使い分けているはずなので、その基準を定義すれば良い。例えばhttps://github.com/gen0083/FilteredHatebuこのプロジェクトは、ネットワークのレスポンスという観点からmockprodというプロダクトフレーバーを定義している。

ネットワークの観点で切り分けているので、flavorDimensions "network"という感じで定義してやる。

後はproductFlavorの定義の部分で、どのdimensionに属する定義なのかを指定してやればいい。flavorDimensionsが1つしかないのであれば省略可能である。

私は最初勘違いしていたのだが、これは決して各フレーバーごとに異なるdimensionを割り当てなければならないということではない。このプロジェクトでは最初mocktestproddefaultというdimensionを割り当てたのだが、そうするとプロダクトフレーバーはmockProdDebugという形になってしまい、mockprodを切り替えられなくなってしまった。切り替えて使うものについては、同じdimensionを割り当てないといけない。

apt → annotationProcessorへの置き換え

これは確かmustだったような気がする。(気がするというのは、最近更新したプロジェクトの中にはaptを使っているものがなかったので)

古いプロジェクトだとgradle pluginを使ってアノテーションによるコード生成ライブラリを使っていたと思うが、その機能は公式に取り込まれているので、gradle pluginの削除とaptannotationProcessorに置き換えてやる。

やらなくても問題ない(けどやっといたほうがいいこと)

compile → implementationへの置き換え

dependenciesのcompile "com.android.support:appcompat-v7:xxx"のcompileの部分をimplementationに置き換える。

これは直さなくてもビルド自体は通るものの、Android Studio3.1からはwarningとして警告されるので、もうさっさと書き換えてしまったほうが良いだろう。

アプリケーションモジュールしかないプロジェクトであれば、全部implementationにしておけばいいはずである。基本的にimplementationにしておいて、それではうまくいかない場合にapiに変更するばよいと思う。apiが必要になってくるのは、ライブラリモジュールを使っている場合だろう。

ライブラリモジュールでのみcompileで依存性を組み込んでいて、利用側のアプリケーションモジュールではdependenciesに記述していないという状態があるのであればapiで指定しないとうまくいかないことがあるだろう。

flutterやるならぜひ登録しておきたいLive Template

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

Flutterを触り始めたのだが、とにかくWidgetのレイアウト変更がツライと感じていた。

AndroidでいうところのViewのレイアウトがコードでゴリゴリ書いていく感じになっているので、インデントが深くなってツライ。Widgetの考え方もAndroidのViewとはちょっと違う(レイアウトに関する設定はレイアウト情報を持つWidgetでくるむとか)

ゼロから作る分にはまだいいのだが、一度作ったレイアウトに対して、「このTextにmargin付け加えたい」となったときがツライ。シンプルなレイアウトならまだいいが、Widgetが何個も登場したり、何回層もネストされてたりすると正直触りたくない。

例えばこんな感じ。

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("FriendlyChat"),
      ),
      body: new Column(
        children: <Widget>[
          new Flexible(
              child: new ListView.builder(
                padding: const EdgeInsets.all(8.0),
                reverse: true,
                itemBuilder: (_, index) => _messages[index],
                itemCount: _messages.length,
              ),
          ),
          new Divider(height: 1.0,),
          new Container(
            decoration: new BoxDecoration(
              color: Theme.of(context).cardColor,
            ),
            child: new Text("hoge"),
          ),
        ],
      ),
    );
  }

これはまだかわいいものだが、それでもめんどうくさい。

そこでLive Templateを追加して、IntelliJのSurrond With機能を使って簡単にWidgetを囲えるようにしてみた。Flutterのチュートリアルを試したりするのに活躍するのでかなり捗ると思う。

Live Templateを定義

Preference > Editor > Live Templateが定義場所。

定義する場所は別にどこでもいいのだろうけれど、Flutterに関することなのでFlutterのところに追加した。

new Column(
  children: <Widget>[
    $SELECTION$
  ]
),

Dartファイルに適用されるように指定して、Reformat according to styleにチェックも入れておく。

Sample

new Container(
  child: $SELECTION$
),

childを1つしか取らないWidgetでラップする場合に使うやつもついでに追加。使っているのがColumnContainerであることに特に意味はない。まあそこは必要に応じてクラス名を書き換えればいいだろうから気にしない。

Live Templateを定義したら、後は囲いたいWidgetを選択した状態で、Surround with機能を呼び出し(私の環境だとcmd+opt+t)、定義したLive Templateを適用すればよい。楽ちん。

test movie

Live Templateは初めて活用したので、もっとこうした方がいいよというのがあったら教えて欲しい。

追記:

記事を書き終わった後で、そもそも標準でWidgetをラップする機能が存在していることを知った。ラップしたいWidgetのクラス名にカーソルをあわせた状態で、Intention Actions(Macならopt+enterで出るやつ)を使うとWidgetを別のWidgetでラップすることができる。わざわざLive Templateを追加する必要はなかった・・・。