Vue.js / Nuxt.js で Cookies を isomorphic に扱えるライブラリを公開しました

風邪をひいたので、休養の間に作った vue-universal-cookies, nuxt-universal-cookies を npm および GitHub へ公開しました。
Vue.js / Nuxt.js にてIsomorphicにCookiesを扱え、node標準のhttp, Express, ブラウザなどに対応したプラグインです。

github.com

年明け約4時間前の滑り込み npmjs.com デビューです。

Nuxt.js でのセットアップ

以下でインストールします。

npm install --save nuxt-universal-cookies

nuxt.config.js へ、以下のようにmodulesを書き足します。今のところoptionsへ設定できる値はありません。

// nuxt.config.js
module.exports = {
  modules: [
    {
      src: 'nuxt-universal-cookies',
      options: {}
    }
  ],
};

Express でのセットアップ

正確には vue-server-renderer などを用いたSSR環境にて、ブラウザとExpress両方で共通のコンポーネントを利用する場合です。
以下でインストールします。

npm install --save vue-universal-cookies

以下のように、サーバ / クライアント 両方でinstallします

// in TypeScript
import VueUniversalCookies from 'vue-universal-cookies'
import * as express from 'express';

Vue.use(VueUniversalCookies);

クライアント側で、以下のようにして設定します。

// in TypeScript
import VueUniversalCookies, { BrowserHandler } from 'vue-universal-cookies'

new Vue({
  cookies: ({
    handler: new BrowserHandler()
  } as VueUniversalCookies.Options)
});

Express側で、以下のように設定します。

// in TypeScript
import VueUniversalCookies, { ExpressHandler } from 'vue-universal-cookies'
import * as express from 'express';

app.use(/^.*/, (req: express.Request, res: express.Response) => {
  // do something
  
  new Vue({
    cookies: ({
      handler: new ExpressHandler(req, res)
    } as VueUniversalCookies.Options)
  });
  
  // do something
});

使用方法

以下のようにして利用できます。

<template>
  <div>
    <p>{{ $cookies.get('key') }}</p>
    <button v-on:click="$cookies.set('key', 'value', {})">Update</button>
  </div>
</template>

JAX-RSでMoshi使ったら `~but <null> is of type null` と怒られてハマった

JAX-RS (Jersey) で Moshi (moshi-kotlin) 使ってて、雰囲気でレスポンスをreturnさせたら以下のように怒られた:

java.lang.IllegalArgumentException: Expected a Class, ParameterizedType, or GenericArrayType, but <null> is of type null
    at com.squareup.moshi.Types.getRawType(Types.java:167)
    at com.squareup.moshi.ClassJsonAdapter$1.createFieldBindings(ClassJsonAdapter.java:83)
    at com.squareup.moshi.ClassJsonAdapter$1.create(ClassJsonAdapter.java:75)
    at com.squareup.moshi.Moshi.adapter(Moshi.java:100)
    at com.squareup.moshi.Moshi.adapter(Moshi.java:58)
    at com.squareup.moshi.CollectionJsonAdapter.newArrayListAdapter(CollectionJsonAdapter.java:52)
    at com.squareup.moshi.CollectionJsonAdapter$1.create(CollectionJsonAdapter.java:36)
    at com.squareup.moshi.Moshi.adapter(Moshi.java:100)
    at com.squareup.moshi.ClassJsonAdapter$1.createFieldBindings(ClassJsonAdapter.java:91)
    at com.squareup.moshi.ClassJsonAdapter$1.create(ClassJsonAdapter.java:75)
    at com.squareup.moshi.Moshi.adapter(Moshi.java:100)
    at com.squareup.moshi.Moshi.adapter(Moshi.java:58)
    at com.jakewharton.moshi.rs.MoshiMessageBodyWriter.writeTo(MoshiMessageBodyWriter.java:57)
    at org.glassfish.jersey.message.internal.WriterInterceptorExecutor$TerminalWriterInterceptor.invokeWriteTo(WriterInterceptorExecutor.java:266)

そんなnullで埋めてる値なんて無いしなぁとか思いながら昨日の深夜3時頃は 諦めてラーメン食べ行った のだけど、さっき気分でMoshiの実装読んでたら納得。勘が悪かった。

要約すると:

  • レスポンスに javax.ws.rs.core.Response を使ってた
  • List<E>のようなParameterized Typeなオブジェクトを直接entityとして食わせるとダメ

という話。
具体的にはこういうコードのことだ:

// lateinit var ebean: EbeanServer

@GET
@Path("/list")
@Produces(MediaType.APPLICATION_JSON)
fun listItems(): Response {
  val items = QMyItm(ebean) // ebean-querybean
    .deletedAt.isNull()
    .findList()
  
  if(anErrorOccured()) {
    return Response.serverError().build()
  }
  
  return Response.ok(items).build() // `IllegalArgumentException`
}

// fun anErrorOccured(): Boolean  = false

なぜ起きるか

これはJavaの仕様であるType Erasureによるもの。勘の良い人であればResponseentityAny! (Object) になっている時点で気付けたかも。
Type Erasureについては既に語られ尽くしているため詳細な解説を避けるが、今回のケースの場合、要は

  • ResponseへentityとしてmyObj: List<MyItm>が与えられる
  • Any! (Object) なので、Moshiは型情報を取ろうとする。List.classであることがわかる
  • Moshi自身の実装として、Listの場合はParameterized Typeのindex: 0を元に展開する必要がある
  • Parameterized Typeのindex: 0を取ろうとしたが、これは既にコンパイル時点で消去されている (Type Erasure)
  • type情報が不明だと続行できないので、IllegalArgumentExceptionを投げる

ということ。

とりあえずどうすればいいか

手っ取り早く解決する方法は、

  • return typeをList<E>のようにする (= Response使わない)
  • MyResponse#getItems で取れるような形でwrapする (= Parameterized Typeでないオブジェクトにする)

など。
恐らくResponseを使っている理由はstatus codeなんかを適宜変えたいからだと思うのだけど、ExceptionMapperを実装してあげて自前のThrowableを投げればレスポンスを変えられるので、前者を取ることをお勧めする。

たとえば以下のような形になる:

// lateinit var ebean: EbeanServer

@GET
@Path("/list")
@Produces(MediaType.APPLICATION_JSON)
fun listItems(): List<MyItm>? {
  val items = QMyItm(ebean) // ebean-querybean
    .deletedAt.isNull()
    .findList()
  
  if(anErrorOccured()) {
    throw MySomethingWrongException()
  }
  
  return items
}

// fun anErrorOccured(): Boolean  = false

// class MySomethingWrongException : RuntimeException()

// もしResponse<T>のような仕様だったら、こういった事は起こらなかったのかも

端末設定でNavigationBarを持っていなかった場合の挙動を考慮し、"android-inset-views" を0.2.0にアップデートしました。

StatusBarNavigationBar分のinset領域の高さを持つview郡、"android-inset-views"を0.2.0にアップデートしました。

github.com

INavigationBarView#setZeroHeightIfNavigationBarDisabled というメソッドでtrueにしておくと、端末設定を認識してViewの高さを0にすることが可能になりました。ハードウェアボタンを持つデバイスなんかで上手く使えるかと思います。エミュレーターやカスタムROMなんかでまれに存在する特殊なoverrideも考慮されています。

CoordinatorLayoutをネストさせる時用にNestedCoordinatorLayoutを作った

社内で前から使ってはいたのだけど、いくらかバグってたりしておかしな挙動もあったので。これを機にまるっと書き直しました。
「完璧な実装」みたいなものがなかなか見つからないので、とりあえずライブラリとしてMavenから取れるようにもしておきました。何か問題が見つかったらアップデートします。

github.com

使い方

README.mdに書いてあるとおり、プロジェクトをdependsに加えてください。


Add following lines to your buildscripts.

buildscript {
    ext {
        nested_scrolling_views_version = '0.0.1'
    }
}
repositories {
    maven {
        url 'http://dl.bintray.com/s64/maven'
    }
}

dependencies {
    compile("jp.s64.android.nestedscrollingviews:support-v25:${nested_scrolling_views_version}")
}

あとは通常のCoordinatorLayoutを使うのと同様です。
たとえばこんなかんじですね:

<jp.s64.android.nestedscrollingviews.support25.NestedCoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <android.support.design.widget.AppBarLayout
            android:id="@+id/navigable_appbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="top"/>
        ...
    </android.support.design.widget.AppBarLayout>
    ...
</jp.s64.android.nestedscrollingviews.support25.NestedCoordinatorLayout>

support-v26からNestedScrollまわりがガラッと変わるらしいとウワサで聞いたので、必要になったらそれも作ります。

ToolbarおよびActionBarをアニメーション対応させた "AnimatedToolbar" をOSSとして公開しました

AndroidのToolbarおよびActionBar部分をアニメーションに対応させたCustom view、"AnimatedToolbar" をOSSとして公開しました。

github.com

以下のコンポーネントが含まれます。

  • AnimatedToolbar
  • AnimatedToolbarActivity
  • AnimatedToolbarHelper
  • ActionBarWrapper

サンプル

こちらでサンプルアプリケーションをダウンロードできます。

play.google.com

挙動はこんなかんじ。

f:id:S64:20170730010427g:plain:w250

左右のNavigation Bar分の幅を持つViewを追加し、android-inset-views 0.1.0 を公開しました。

Androidにおける Status Bar, Navigation Bar 部分にあたる領域分の高さを持つCustom view郡、"android-inset-views" を 0.1.0 にアップデートしました。

github.com

旧バージョン 0.0.1 の記事はこちら。

従来の以下のコンポーネント:

  • StatusBarInsetFrameLayout
  • BottomNavigationBarInsetFrameLayout

の2つから、今回は以下のコンポーネントを加えました:

  • LeftNavigationBarInsetFrameLayout
  • RightNavigationBarInsetFrameLayout
  • HorizontalPositionNavigationBarInsetFrameLayout
  • DisplayCompat
  • NavigationBarUtils

主にHandsetサイズの端末において左右に配置されるNavigationBarをカバーする用途を想定しています。

AndroidのStatusBar, NavigationBarの高さを持つView群 "android-inset-views" をOSSとして公開しました

Androidにおける StatusBar, NavigationBar 部分にあたる領域分の高さを持つCustom view郡を “android-inset-views” をOSSとして公開しました。
仕事で必要になったモノを汎用的に焼き直したものです。

github.com

以下のコンポーネントが含まれます:

  • StatusBarInsetFrameLayout
  • BottomNavigationBarInsetFrameLayout

想定する用途

android:fitsSystemWindows="true" を指定したり、android:windowTranslucentStatusandroid:windowTranslucentNavigationandroid:statusBarColorandroid:navigationBarColorなどを指定し、StatusBar, NavigationBar 領域 (いわゆるinsets) までコンテンツを広げている際、状況によって領域分のpaddingを持ったり、特別カラーを変えたいなどのシーンがよく発生します。

多くの場合は自前でlistenし計算したものを直接setLayoutParamsするなどしていることが多いかと思いますが、これを利用することでより簡単に実現できるようになります。

License

Apache License 2.0 です。

今後の予定

  • CoordinatorLayoutに対応したい
  • 趣味ではXamarinをよくやるので、Bindingを作りたい