ユニファ開発者ブログ

ユニファ株式会社システム開発部メンバーによるブログです。

JaSST'19 Tokyo に参加してきました

ごあいさつ

こんにちは。 ユニファのQAチームの斉藤です。 今回ははじめてJaSST東京に参加してきました。 JaSST九州には参加したことがあるのですが、東京は規模がとっても大きい!そして今年のテーマは「AI」でした。 私は初日だけ参加しましたが、AIの基調講演、直近の業務に活かせそうなテスト技法や開発プロセスのセッションを回らせて頂きました。 以下、レポートになります!

JaSST'19 Tokyoとは

http://jasst.jp/symposium/jasst19tokyo.html

NPO法人ソフトウェアテスト技術振興協会(ASTER)さんが開催するソフトウェアテストシンポジウムです。 海外から著名なエンジニアを招いての招待講演、テスト技術者や研究者による研究・実践・ツール適用事例の発表、登壇者と参加者の交流会などもあります。

弊社リーダーの鶴岡も、QAエンジニアのキャリアデザインセッションに登壇しました(^^)< お疲れさまでした!

基調講演:AI-Driven Testing:A New Era of Test Automation

Ultimate Software社のTariq Kingさんによる講演です。 AIや機械学習の概要から、AIにテスト設計~テスト実行してもらう時のイメージ、AIテストの限界についてお話をされてました。 AIにテストをしてもらう時、以下の流れになるようです。

 インプット → 魔法の箱 → アウトプット

まずインプットとして、ドメイン知識や仕様書の情報を与えます。 インプットを受け取る魔法の箱(AIの心臓部)はプログラムで、インプットを元に、テスト設計やテスト実行を行います。(これが最初のアウトプットになります。) Tariq Kingさんいわく、最初のアウトプットはおそらく散々なもの、人間の期待とずれたものになるだろう、とのことです。 そして、そのアウトプットを人間が評価します。 評価を、魔法の箱(AIの心臓部)へ学習させます。 学習によって、魔法の箱(AIの心臓部)は自分自身をアウトプットの精度があがっていく、という流れだそうです。 学習のために必要な、アウトプットの評価は、私たちの人間の役目になります。

AIテストに関しては、テスト技術者は「AIテストのアウトプットの評価」と「評価を学習させる」という関わり方をしていくのかな…と感じました。 講演後の質疑応答、AIのテスト結果に対して自信をもってOKを出すには?という質問に対して、Tariq Kingさんは

  • ブラックボックス(魔法の箱=AI)の中を理解しなければならない
  • またテストのどこでAIを適用するか?を考えなければならない

と回答されてました。 テスト業界で大きな注目を集めるAI。AIがもたらす変化に適応できるよう、少しずつでも学んでいきたいなと感じた講演でした。

セッション:テストプロセス改善「XDDPにおけるテストプロセス」

AFFORDD T4研究会の長友さんによるセッションです。 派生開発の特徴や起こりやすい問題点と、それに対応した開発プロセスであるXDDPについて説明されてました。 派生開発の特徴は

  • 短納期
  • 仕様書がないもしくは更新されてない中で部分理解を強いられたまま変更や追加
  • 「一人プロジェクト」になりやすい

これらは現場でよく見かける状況なので、イメージしやすかったです。 このような派生開発の振り返りで、「もっと全体を理解してから進めればよかった」という感想がでるそうです。(たしかに、聞いたことあります。) それに対して「全体を理解すれば問題が解決するのか?」という問いかけが、すごく印象に残りました。

全体理解するための資料や時間もない、アサインされた担当者によってはドメイン知識も少ないかもしれない、 XDDPはそれらの問題に対応しやすいプロセスや成果物で構成されています。 特に適用事例の中でご紹介された「変更要求T型マトリクス」は開発要件・機能・モジュールという点から、影響範囲を開発者とQAが認識合わせられるとのことで、今後、自分が仕様をよくわかっていないシステムのテストをする時には、作ってみたいなと思いました。

チュートリアル:中級者セッション「JSTQB Advanced Levelテストアナリストのシラバスでテストを学ぼう」

JSTQB技術委員会の須原さん、福田さんによる、JSTQB Advanced Levelテストアナリストのシラバスを使って、テスト技法を学ぶセッションです。

  • 同値分割法
  • 境界値分析
  • 原因結果グラフ法
  • 組み合わせテスト技法(ペアワイズテスト / クラシフィケーションツリー法)
  • ドメイン分析

について、講義とワークの二本立てで学びました。今回一番楽しみにしていたセッションです。

JaSSTのテスト技法のセッション参加は2回目ですが、前回も今回も感じたことは、テキストの図や説明がとても分かりやすい!ということ。 ひとつひとつのテスト技法について、他テスト技法とのメリット・デメリット・使いどころの比較があってイメージしやすかったです。

最初は、同値分割法と境界値分析について。 JaSST九州では焦って小さなミスが多かったのですが、今回は落ち着いてワークに取り組めました。 今回、[参考]として同値分割の「ズームインとズームアウト」、「どこまで同値分割する?」という問いかけがありました。 「どこまでテストするか?」は、現場でも意見分かれる問題かと思います。 これに対して、テスト技法を用いながら「どこまで?」を適切に設定することがテストアナリストに求められるのだろうな、と感じました。 日常のテスト業務でも「これはどこまでテストする?」と自問しながら取り組んでみると良いかもと思いました。

次に、原因結果グラフ法。 まず結果を見つけ、その結果に影響する原因(条件)を見つけ、結果と原因(条件)をAND / OR / NOTの関係性で結ぶグラフです。 これだけだとテストケースに落とし込みにくいので、グラフにした後、デシジョンテーブルに変換します。 私がテストを考える時、原因(条件)から洗い出して途中で膨大になったり、条件と条件の関係性で混乱しやすいので、これをうまく使えるようになるとテスト設計時にすごく楽になるなと感じました。

この他にも、組み合わせテスト技法のクラシフィケーションツリー法や、ドメイン分析などを学びました。セッションの残り時間の関係で、後半は駆け足になりついていくのがやっとでした。 (後で復習が必要です…!!)

テスト技法は、テスト設計をうまく行う手助けになるやり方。とテキストに書いてありました。 私はまだまだテスト技法を使いこなせていないので、ひとつひとつ勉強して現場のテストに活かしていけたらなと思います。

情報交換会

中級者セッションのワーク、ドメイン分析でわからないところがあったのですが、講師の方に声をかけるタイミングがつかめずにいた私。 初日終了後の情報交換会に講師の方も参加されていたので、思い切って声をかけてみました。

そうしたら、講師の方、テスト業界の先輩QAエンジニアさんが一緒にテキストを見て下さり、「たしかにここは疑問に感じるかも…」「テキストのレビューで私ここ指摘した気がする…あの時は…」など、皆であーでもないこーでもないとプチ勉強会みたいな雰囲気に。

疑問もスッキリ理解でき、「他の人と疑問を共有しながら考えるのって、楽しいな」と感じました。

反省

次回は、人見知りを克服して、いろんな人に話しかけるぞ。

良いプロダクトは良いチームから

 こんにちは、ユニファCTOの赤沼です。最近は私自身でプロダクションのコードを書くことはほとんどなくなり、チーム作りがミッションとなっているわけですが、少し前の Podcast で if-up 2019 というカンファレンスの参加レポートに含める形でチームについて話したので、改めて要点を書いてみたいと思います。

 ちなみにその時の Podcast はこちらです。

podcast.unifa-e.com

結局はチームが全て

 Podcast のタイトルにもしていて、いきなり結論からになりますが、プロダクト作りにおいては結局はチームが全てなのだと思っています。全く新しい技術を生み出すための研究や、エンジニアが一人でプロダクトを作っていくようなケースでは別でしょうが、ユニファのようにチームで自社サービスを作っていく場合には、技術的な問題というよりはチーム作りの良し悪しがプロダクトの成否を分けるのだと思います。プロダクト作りは総合力です。もちろん技術力は重要ですが、とにかく高い技術力があれば必ず成功するかと言えば、そうではありません。if-up で使われていた言葉を借りれば、最近のプロダクト作りは「ものづくり」から「ものごと作り」へと変わってきて、高いレベルの技術が使われていれば良いわけではなく、いかにユーザーの良い体験を作れるかが勝負になってきています。そうした中で、いかに目線を合わせ、技術至上主義にならず、いかにユーザへの良い体験を作ることにフォーカスできるか、その為には時にはエンジニアとしての技術面でのこだわりを妥協できるか、ひいては事業を加速していくためのエンジニアリングができるチームを作っていけるかということが、勝負になってくるのだと思います。どんなプロダクトができていくかは、どんなチームができているか次第ということです。

世界一を本気で目指すチームを

 チーム作りの目線を合わせていくという点でも、メンバーが皆本気で世界一を目指しているか、というのはかなり重要です。絵空事ではなく、本気で世界一になれると思って目指すチームを作る、またそういう環境に身を置くことは、個人の成長という点でも大きく影響してきます。ユニファでは創業時から代表の土岐は、グローバルで No.1 になる、と内外に向けて発信しています。やるからには本気で世界一を目指す。世界一になるには世界一のプロダクトが必要です。プロダクト開発に責任を負う開発チームとしては世界一のプロダクトを作っていく必要があるのです。本気で世界一を目指し続けるCEOと仕事ができる環境はあまり多くはないのではないかと思います。せっかくそういうCEOと仕事をしているのですから、私自身も改めて本気で世界一を目指したいと思っています。その為にはいかに良いチームを作っていけるかが勝負です。単純な技術レベルやチーム規模で行ったらそれこそ Google や Amazon などの大企業には太刀打ちできませんが、ユニファがサービスを提供する領域において、世界一のユーザ体験を提供するということであれば、その可能性は大いにあると思っています。

保育をハックする

 プロダクトを開発していく上では、様々な課題をクリアしていく必要があります。技術で簡単に解決できるような課題もあれば、解決の道筋がなかなか見えないような難題もあります。そうした時にも、その過程を楽しむ姿勢を持っていたいと思うのです。最近ではハックという言葉は色々なところで使われていますが、開発チームの指針としても、「保育をハックする」というのを打ち出しています。ハックするというのは、様々な制約を打破・回避していくことを、知的な難問を解いていくものと捉える、能動的、創造的行為です。ユニファにおいても保育に関する様々な制約、慣習、課題を、スキルやチームワーク、保育に関する知見によって能動的、創造的に解決していく、またその過程を楽しむということを意識していきたいと思うのです。

まとめ

 ユニファの開発チームも人数が増えてきて、今年中にはさらにチーム規模が大きくなっていきます。そうした時に、いかに少人数だった時の良い空気を残していけるか、また、改善すべきだったところはこのタイミングで改善していけるかが今後の課題となってきます。その為にも様々な施策等も行なっていきたいと思っていますので、また機会があればどんなことをやっているかは紹介できればと思います。

 また、ユニファでは一緒に世界一を目指してくれるエンジニアも募集中ですので、興味のある方はぜひご連絡ください。

ユニファ 株式会社の採用/求人 | 転職サイトGreen(グリーン)

深海へと

こんにちは。 カメラマンからデザインチームへと部署異動をした三好です。

世間でいうアラフォーと呼ばれる部類に属してしまった今日この頃、ようやく自分自身のことが正確に見え始めてきていると感じています。

何に対して生きている実感を得られるのか、奥深くに潜む欲求が満たされる瞬間、どの種類の苦痛であれば耐えられるのかなど、ひっくるめて適正というものを見極めた結果デザインという道に辿り着きました。

部署異動について

異動をしてまず思ったことは、見る角度が変わるだけで会社がまるで違う生き物に見えてくるということに驚きました。

「井の中の蛙大海を知らず」というたとえが正しいかどうかはわかりませんが、日々の業務に追われその慌しさを盾に、自ら心地の良い壁を作り出して生活していたような気がします。

その無意味な壁を壊すことができただけでも異動した価値はあったと思います。

もちろん周囲の方の理解や後押しがあったからこそなのですが。

写真とデザインについて

写真とデザインの少し似ているところを話したいと思います。

フォトグラファーを大きく分けると2種類存在します。ビジネスサイドのフォトグラファーと自己表現の手段としてのフォトグラファーです。

この2種類は全く性質が異なります。そしてデザイナーに関してもやはりアーティストと比較されることがあります。

社会に属している限り、利益に携わる限りはデザイナーもフォトグラファーも純粋な美を追い求めることはできません。自由に飛び回ることはできず、常に制限のある世界で生きています。

当たり前ですが他の職種と同じようにユーザーの視点、客観的視点を最優先に仕事を全うしますが、この世界の住人は美に対して敏感であるがゆえに自分自身の中に絶対の正義を持っています。明確な正解の無い世界では大きくブレないための指針としてこの正義は必要なものですが、そのために常に主観と客観の狭間で葛藤しています。

しかしその葛藤が決してネガティブな意味だけではないことは強く主張したい。

デザインに関しては特に社会という制限の中に放り出され、多くの人の思考を巻き込み、制作者の予測しない方向へ飛んでいき大きく形を変えていく。

そうすることによって1人の人間の内面だけでは生み出されないような強靭な創作物が姿を現すこともあると信じています。

と、偉そうなことを話しましたが私は駆け出しのデザイナーなので実際のところは何もわかっていないのかもしれません。

感性だけでは生きていけないのもデザインの世界。

凄まじい数のツール、まるで深海のように底の見えない情報量。

今はその海を手探りで潜っているような状態です。

ただし、気持ちの良い苦痛ではありますが。

ミリ単位の世界

f:id:unifa_tech:20190328114911j:plain f:id:unifa_tech:20190328114948j:plain f:id:unifa_tech:20190328115136j:plain

最後にテキストだけではつまらないので以前私が撮影した写真を掲載します。

普段見慣れている動物をほんの少し角度を変えた視点から切り取り、”creature”としての姿を浮かび上がらせようと意識したものです。

ミリ単位で表現は姿を変える。

そういう意味では、写真もデザインと同じかもしれません。

深層学習で馬を見分ける(その2)

R&Dエンジニアの浅野です。前回の記事で写真から馬の顔を検出することができるようになりました。今回は切り出した顔からどの馬なのかを判定するモデルを作成します。全3回のシリーズ記事の2回目です。

  1. 顔の検出器の作成 ← 前回
  2. 分類器の作成 ← 今ここ
  3. スマホで動くようにする

データセット

馬を識別するモデルを学習するにあたり、公開されている馬の顔データセットを使用することにします。このデータは、47頭の馬を様々な天候や光の当たり具合で撮影したビデオから各頭30枚ずつの顔部分(正面と左右からのアングル)を切り出したものです。こうやって時間をかけてデータセットを整備して公開してくれるのは本当にありがたいことです。

f:id:unifa_tech:20190322133407j:plainf:id:unifa_tech:20190322133357j:plain:w110f:id:unifa_tech:20190322133354j:plain
データセットに含まれる写真の例。すべて違う馬です。

モデルの学習

シンプルな4層のCNNを用いて47頭の馬の顔を分類するモデルを作ります。47*30=1,410枚の画像のうち、1,118枚を学習用、141枚をバリデーション用、そして残り141枚をテスト用に分割して学習を行いました。100エポック回したときの様子を下に図示しています。順調に学習がすすんでいます。テスト用画像で精度を測ったところ95.03%と思ったより良い結果がでました。4層のCNNとはいえ最適化すべきパラメータ数は300万以上あるのに、1,000枚ちょっとの画像でそれなりの結果がでてしまうのが面白いところです。

f:id:unifa_tech:20190322143954j:plain:w500
学習の様子

ちなみに、ImageNetで学習済みのResNet50から転移学習(全結合層のみの学習)を行った際の精度は96.45%, Convolution層も含めてFine Tuningを行った場合の精度は98.58%とさらに高い数値が出ています。ImageNetの画像には馬が写っている画像が含まれていることもありますが、さすがに様々な画像系のタスクの転移学習で利用されるだけあって、馬の顔認証というニッチな用途でも効果を発揮してくれました。

今後に向けて

今回は画像分類として馬の認識を行いましたが、この方法だと追加で認識させたい馬がでてきたときに、その馬の顔データを十分な数だけ集めてモデルを学習しなおす必要があります。一方、人間の顔認証でやっているように、Triplet lossArcFaceなどの手法を用いて顔画像からembeddingを計算するモデルを作成しておくと、その後の運用を楽にすることができます。ただしそのモデル作成のためには今回使ったものよりもはるかに多数の正解データが必要なので実現はまだ先になりそうです。

前回と今回の記事で、画像から馬の顔を切り出し、その顔からどの馬なのか判定することができるようになりました。これまではクラウド環境でGPUを使用して学習および推論を実行してきましたが、次回はスマホで推論を実行できるようにしたいと思います。

保育の世界を変えるエンジニア募集中

(今回の内容も保育とは全く関係ありませんでしたが)ユニファではテクノロジーで保育を変えていく仲間を大募集中です。少しでも興味を持たれた方はぜひ声をかけてください!

【AI系技術サーバサイドエンジニア】R&Dチームにて機械学習やAIを用いたサービス開発を担う(東京)の採用情報 | ユニファ株式会社

そのコマンドにロマンはあるか

こんにちは。ICT開発の柿本です。

全てのエンジニアに声を大にして問いたいのですが、 コマンドを覚えるのってツラくないですか?

自分の記憶力の問題なのか、この世にコマンドが多すぎるのかわかりませんが、『あのコマンドってどうやって使うんだっけ?』と思ってググると、検索結果のほとんどがアクセス済みだったりして、『そういえば最近調べたなぁ』となります。

しかも、Googleはご丁寧に『〇〇/〇〇/〇〇 にこのページにアクセスしました。』と表示してくれますが、『あれ?これ今日じゃない?』となるとパソコンを閉じたくなります。(そもそもあの表示に何の意味があるんだろう。。)

そんなこんなで私の作業メモにはつらつらと色々なコマンドが書いてあるわけですが、今日はその中の一部を恥を忍んで紹介します。

環境

OS: Mac OS 10.14  
シェル: zsh

『シェルは何を使ってますか?』
『zshです。』
『すごいですね!!』

はエンジニアトークあるあるですが、zshは補完機能が優れているのでどちらかというと すごくない人 こそ使うべきだと思います。

そして 適度にすごい人.zshrc をGitHubから拝借すれば、労せずに楽できます。
超絶すごい人.zshrc は逆に混乱するので避けたほうが良いです。

ただ、ネットで見つけたコマンドがなぜか動かなくて4時間くらい潰した結果『パラメータ文字列の をエスケープしないとダメだった〜!!』みたいなことはままあるので、その辺は気をつけたほうがいいです。

まあその4時間の学びが大事だったりしますが。

それではさっそく紹介していきます!!

curlしたい

% curl -X POST -H 'Content-Type:application/json' http://localhost:3000/api/v1/users -d '{"name":"Taro"}'

APIサーバーの動作確認をする時によく使います。

『これを覚えるのがツラいの?』てレベルですが、 Content-Type だったか ContentType だったかいつも間違えるので、メモしておくことにしました。

認証を通しつつcurlしたい

% curl http://localhost:3000/api/v1/users/1 -H "Authorization:Bearer token_string"

これもAPIサーバーの動作確認をする時によく使います。

これは Authorization だったか Authenticate だったか Authentication だったか混乱するので、メモしておくことにしました。

『AuthorizationとAuthenticationの違いを学び直してこいよ!』というツッコミもありそうですが、そもそもそれが頭に入るのであればこのコマンドはとっくに覚えられています。

データをたくさん作りたい

% i=0; while [ $i -le 9 ]; do curl -X POST -H 'Content-Type:application/json' http://localhost:3000/api/v1/users -d '{"name":"Taro_0'"$i"'"}'; i=$(expr $i + 1);done

API経由でデータをたくさん作る時のワンライナーです。

単純にダミーデータが欲しかったり、負荷をかけてみたかったり、データをたくさん欲しいことがあったので、メモしました。

大量に投入したい時はDBに直接入れたほうがパフォーマンスはいいですが、リレーションなども気にしながら入れるのであれば、API経由が一番気が楽だったりします。

nginxのaccess.logを解析したい

% find . -name 'access.log.201901011[0-3]_0.log' | xargs grep -h -o '\turi:/\S*' | sed -e 's/[0-9]\{1,\}/*/g' | sort | uniq -c | sort -r

ログを1時間でローテーションしていて『4時間分のログを解析したい!』といった時に、複数ファイルをまたがってややこしくなるので、メモしました。

やっていることは、

  1. 対象のログファイルをfind(ここでは10時から13時まで)
  2. uri: xxx の部分だけgrepで抽出
  3. /users/1 /users/2 は同じuriとして扱いたいので数字を * にsedで置換
  4. uniq -c するために sort
  5. 最後にもう1回降順ソートしてアクセスランキング的な並びにする

これはエンドポイントごとのアクセス数を見たい時でしたが、 grep の対象を変えればいろいろな切り口の解析ができます。

ログファイルを解析するツールもあるのでしょうが、大事なのはロマンなのです。

RailsアプリをDockerでサクッと動かしたい

% docker run --name ruby-tmp -it -v `pwd`:/var/ruby -w /var/ruby -p 3000:3000 ruby:2.6.1 /bin/bash
# bundle install
# apt-get update && apt-get install -y nodejs
# rails db:migrate
# rails s

ちょっとこのRailsアプリを動かしてみたいな、という時に使います。
PostgreSQLなど使っているともう少し工夫が必要ですが、SQLiteなら大丈夫です。

普段の開発でdocker-composeを使っていると、 docker-compose updocker-compose down くらいしか使わず、 docker コマンドは忘れ去ってしまうのでメモしました。

弊社ではサーバーサイドエンジニアの採用時に簡単なRailsアプリを課題(これが結構面白い)として作っていただくのですが、それを動かしてみる時に便利です。

終わりに

私のメモにはもっとたくさんコマンドが書いてあって、

『それくらい覚えろよ』

もしくは

『わざわざコマンドでやる必要ないよね』

のどちらかに分類できるのですが、コマンドにはロマンがあると信じてこれからもどんどんメモしていこうと思います。

おしまい

DataBindingでLoading

こんにちは、スマホチームのあいばです。
最近思い立ってオンライン英会話に入会しました。リモートの時には自宅でせっせとランチ英会話しているのですがまだまだ効果は見えません。。
最近お仕事の方はiOS中心だったのですが、Android案件も盛り上がって来たのでおさらいしています。
そこで今回はGithubBrowserSample を読み、処理中のProgressBarの表示についてどんなことしてるか見てみようと思います。

GithubBrowserSample概要

リポジトリ検索、リポジトリ詳細、ユーザ情報の3画面からなるアプリです。
今回はリポジトリ詳細のコードから引用して説明したいと思います。(この機能を選んだ理由は特にないです)
図は各モジュールが相互にどのようなやり取りをしているかを示しています。公式より f:id:unifa_tech:20190307100655p:plain

build.gradle

app/build.gradle
DataBindingを利用してProgressBarの表示を更新しているため設定を有効にする必要があります。

dataBinding {  
    enabled = true  
}

レイアウト

loading_state.xml
ProgressBarとリトライボタン、エラーテキストを持つレイアウトです。
変数resourceの値を参照し各Viewのvisibilityを切り替えています。
ほとんどJavaと同じようにコードを書くこともできます。詳細は公式のドキュメント でどうぞ。

    <layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
        <data>
            <import type="com.android.example.github.vo.Resource" />
            <import type="com.android.example.github.vo.Status" />
            <!-- 変数を宣言しレイアウトファイル内のバインディング式で使えるようにする -->
            <variable
                name="resource"
                type="Resource" />
            <variable
                name="callback"
                type="com.android.example.github.ui.common.RetryCallback" />
        </data>

        <LinearLayout
            android:orientation="vertical"
            <!-- カスタムセッターでvisibilityを設定 -->
            <!-- データがセットされたら非表示になる -->
            app:visibleGone="@{resource.data == null}"
            android:layout_width="wrap_content"
            android:gravity="center"
            android:padding="@dimen/default_margin"
            android:layout_height="wrap_content">
            <ProgressBar
                <!-- ローディング中は表示する -->
                app:visibleGone="@{resource.status == Status.LOADING}"
                style="?android:attr/progressBarStyle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:id="@+id/progress_bar"
                android:layout_margin="8dp" />
            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/retry"
                android:id="@+id/retry"
                <!-- リスナーバインディングを利用してリトライを実行する -->
                android:onClick="@{() -> callback.retry()}"
                <!-- エラー時にリトライボタンを表示する -->
                app:visibleGone="@{resource.status == Status.ERROR}" />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:id="@+id/error_msg"
                android:text="@{resource.message ?? @string/unknown_error}"
                <!-- エラー時にTextViewを表示する -->
                app:visibleGone="@{resource.status == Status.ERROR}" />
        </LinearLayout>
    </layout>

BindingAdapters.kt
BindingAdapterを利用してカスタム セッターを作成しています。
xmlに直接バインディング式を書いても実現できますが、レイアウトがややこしくなるのは辛いですよね。

    object BindingAdapters {
        @JvmStatic
        @BindingAdapter("visibleGone")
        fun showHide(view: View, show: Boolean) {
            view.visibility = if (show) View.VISIBLE else View.GONE
        }
    }

ちなみにカスタムセッターを使わない場合の実装はこんな感じでしょうか。 loading_state.xmlより

    <layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
        <data>
            <!-- Viewクラス追加 -->
            <import type="android.view.View"/>
             ...
                <LinearLayout
                android:orientation="vertical"
                <!-- 書き換え -->
                <!-- app:visibleGone="@{resource.data == null}" -->
                android:visibility="@{resource.data == null ? View.VISIBLE : View.GONE"

                android:layout_width="wrap_content"
                android:gravity="center"
                android:padding="@dimen/default_margin"
                android:layout_height="wrap_content">
                ...

repo_fragment.xml
loading_state.xmlをインクルードしています。
アプリの名前空間と変数名を使用するとインクルードされたレイアウトに変数をセットできます。

    <layout xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:android="http://schemas.android.com/apk/res/android">
        <data>
            ...
            <variable
                name="retryCallback"
                type="com.android.example.github.ui.common.RetryCallback" />
        </data>
        <androidx.constraintlayout.widget.ConstraintLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <include
                layout="@layout/loading_state"
                <!-- loading_state.xmlに宣言したresource, callbackに値をセット -->
                app:resource="@{(Resource) repo}"
                app:callback="@{() -> retryCallback.retry()}"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintStart_toStartOf="parent"
                android:layout_marginStart="8dp"
                app:layout_constraintEnd_toEndOf="parent"
                android:layout_marginEnd="8dp"
                app:layout_constraintBottom_toBottomOf="parent"
                android:layout_marginBottom="8dp"
                android:layout_marginTop="8dp"
                app:layout_constraintTop_toTopOf="parent" />
        </androidx.constraintlayout.widget.ConstraintLayout>
    </layout>

Repository

NetworkBoundResource.kt
sqlite DBとネットワーク共通でリソースを提供するクラスです。 このクラスでResourceの状態を管理しています。

    init {  
        // 生成時に処理中ステータスをセット
        result.value = Resource.loading(null)  
        @Suppress("LeakingThis")  
        // ローカルDBから読み込み
        val dbSource = loadFromDb()  
        result.addSource(dbSource) { data ->  
            result.removeSource(dbSource)  
            if (shouldFetch(data)) {  
                // DBから取得できなかった場合ネットワークから取得する
                fetchFromNetwork(dbSource)  
            } else {  
                result.addSource(dbSource) { newData ->  
                //DBから読み込めた場合はステータスをSuccessへ変更
                    setValue(Resource.success(newData))  
            }  
        }  
    }

コードは省略しますが、ネットワークからの取得成功/失敗のタイミングでもステータスを変更しています。 またNetworkBoundResourceはabstractになっていて、各機能のRepositoryクラスで匿名クラスを実装しカスタマイズしています。

RepoFragment.kt
FragmentではBindingクラスを生成し、値を適用しています。 RepoFragmentBindingクラスは自動生成されるクラスなのですが、名前はレイアウトファイル名から決まるようです。

    var binding by autoCleared<RepoFragmentBinding>()
    
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {  
        val dataBinding = DataBindingUtil.inflate<RepoFragmentBinding>(  
            inflater,
            R.layout.repo_fragment,
            container,
            false
        )
        // リトライボタンのイベント処理を実装
        dataBinding.retryCallback = object : RetryCallback {  
            override fun retry() {  
                repoViewModel.retry()  
            }  
        }  
        binding = dataBinding  
        return dataBinding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {  
        repoViewModel = ViewModelProviders.of(this, viewModelFactory).get(RepoViewModel::class.java)  
        repoViewModel.setId(params.owner, params.name)
        
        // LiveDataの変更を監視するためのLifecycleOwnerをセットします
        binding.setLifecycleOwner(viewLifecycleOwner)  
        binding.repo = repoViewModel.repo  
        val adapter = ContributorAdapter(dataBindingComponent, appExecutors) { contributor ->
            navController().navigate(
                RepoFragmentDirections.showUser(contributor.login) 
            )  
        } 
        this.adapter = adapter  
        binding.contributorList.adapter = adapter  
        initContributorList(repoViewModel)  
    }

最後に

以前からLoadingの表示ってストレスフルだなぁと感じていたこともあり今回調べてみました。
"リクエスト投げる前にProgressBar/Dialogを表示してコールバック受けたら消す"みたいな実装をよくみる(やってた)と思いますが、バグの宝庫なんですよね…
成功、失敗、キャンセル、画面回転…さらにリクエスト投げるまでのシーケンスも次第に複雑になっていくのでとても面倒見きれない。
自分のプロダクトに適用できるかはまだわかりませんが、このサンプルではステータスが変わればDatabindingで表示が切り替わるので余計なことを考えなくて済むのではないでしょうか。
また、LiveDataを使えばライフサイクルとも連動してくれるので組み合わせて使えば何かと便利そうです。
DataBindingでできることを色々調査できてよかったです!