患者さんと病院をつないでいくシステムの今とこれから
本エントリはUbie Advent Calendar 2020の22日目です。 21日目はtoCプロダクトAI受診相談ユビーのプロダクトオーナーである@shikicheeのエンジニアの僕が強みを活かして施策推進したら、異次元の角度で数字が伸びちゃった話でした。
自分は現在toBプロダクトである、医療機関向けのAI問診ユビーのソフトウェアエンジニアをしています。普段はRails/Ruby、Spring Boot/Kotlin、React/TypeScriptを使ってバックエンドからフロントエンドまでプロダクトに関連することについて全般的に開発をしています。
またこのほかに最近は全社的な技術戦略に関するロールを持っており、サービス全体のカタチをどのようにしていくかについて考えたり手を動かしたりしています。
本エントリでは、Ubieの現在のシステムの状況と、起こり始めている現象などを紹介しつつ、今後どのようなことを行っていくかについてまとめます。
Ubieの今とサービス群
Ubieは今年で創業から4年目を迎えました。現在のサービス群の基となるコードは、共同創業者の一人である久保(@quvo_ubie)が大学院時代の研究で作り始めました*1。コードベースとしてはこの研究時代の2年間を足した6年物ということになります。
現在のUbieのソフトウェアエンジニアやサービス、プロダクトの数は次の通りです。
- ソフトウェアエンジニアの数: 24人
- サービスの数(デプロイの単位):15個
- プロダクトの数:4個
独立して動作するサービス群の関連は次のようになっています。
この図にプロダクトの境界を引くと...
結構こんがらがっているように見えますね。実際に結構こんがらがっています。
現在に至る変遷
Ubieでは創業から現在に至るまで、不確実性を下げるための検証
にフォーカスしてきました。それぞれの局面毎に、リソースや時間の制約を鑑みて最善手と考えられるものを重ねているものの*2、やはりどうしても複雑化していっています。
創業前 疾患推測エンジン
- 創業前(およそ2年)
- ソフトウェアエンジニアの数:1人
- サービスの数:1個
- プロダクトの数:0個
- 不確実性:症状群の入力による疾患推測が実現可能か
創業1年〜 toC 症状チェッカー Androidアプリ
- 創業1年~
- ソフトウェアエンジニアの数:2〜3人
- サービスの数:2個 (Androidアプリも1個と数える)
- プロダクトの数:2個
- 不確実性:症状に基づいた疾患推測にニーズがあるのか
創業2年〜 toB AI問診サービス 病院向け
- 創業2年~
- ソフトウェアエンジニアの数:3〜7人
- サービスの数:3個
- プロダクトの数:2個
- 不確実性:症状チェッカーを病院につなげることがそもそもできるのか
疾患推測エンジンの実現、症状チェッカーの価値検証を経た後、出口としては 実際に患者さんが病院に受診する/しない
ことが重要となってきます。この時点で医療機関にとっては、症状チェッカーは患者さんの受診行動に影響を与えるかもしれませんが、業務上の課題には特に貢献しません。病院に受診した患者さんは改めて1から問診を行うからです。そこで医療機関の業務上の課題解決にフィットするプロダクトの検証が始まりました。
創業2.5年〜 toC 症状チェッカーサービス(テスト版)
- 創業2.5年~
- ソフトウェアエンジニアの数:7〜12人
- サービスの数:8個 (toC, toB, 疾患推測の単位で分割開始)
- プロダクトの数:2個
- 不確実性:症状に基づいた疾患推測ニーズのスケーラビリティ
医療機関向けのプロダクトの形がある程度見えてきた頃、症状チェッカーのリプレースが始まりました。Androidアプリは廃止し、Webサービスとして作り直しています。アプリの場合、症状チェックをするために症状の発現 -> 症状で検索などをする -> アプリの認知 -> インストール -> チェック
という風にかなりの数の手続きを踏まなければなりません。症状があるユーザにとってはかなりの負担です。Webサービスにすることで症状の発現 -> 症状で検索などをする -> ランディング -> チェック
とスムーズに症状チェックができるようになりました。同時にその形態でスケールは可能なのかという点の検証が必要となります。
創業3年〜 toB AI問診サービス クリニック向け
- 創業3年~
- ソフトウェアエンジニアの数:12~18人
- サービスの数:12個
- プロダクトの数:3個
- 不確実性:クリニックでスケールするための特有の普遍的な課題が存在するか
病院
とは入院施設としてベッド数が20床以上、医師3名以上の機関を指します。それ以外は診療所
となります。街に沢山あるクリニックや医院は診療所
です。国内の病院
はおよそ8000件で、診療所
は100000件です。病院
と診療所
では来院患者数や医療従事者の規模が異なります。業務上の課題や関心事も大きく異なるのではないか、という仮説のもと、プロダクトを分けて検証をはじめました。
創業4年〜 toB AI問診サービス アジア向け、症状チェッカーと AI問診サービスの接続
- 創業4年~
- ソフトウェアエンジニアの数:18~24人
- サービスの数:15個
- プロダクトの数:4個
- 不確実性:
- AI問診サービスが、海外の医療環境においても課題解決に貢献できるのか
- 症状チェッカーをトリガーとした、受診をする/しないが起こるのか
国内の医療機関への導入が徐々に進んできたところで、医療制度が近しい国においてもAI問診サービスが役に立つのか検証をはじめました。また国内においては症状チェッカーで行った問診をAI問診サービスを導入している病院に直接送ることができるようになりました。2つのプロダクトが繋がり、サービスの関連図には現れてこない複雑さも生まれ始めています*3。
現在起こっている現象
ソフトウェアエンジニアの人数、サービスの個数、プロダクトの数が着々と増えていく中、開発において起こっている現象が沢山あるわけですが、代表的なものとしては次の通りです。
- 暗黙知の部分のキャッチアップや変更コストが増加
- 特に文脈を知らない新メンバーは大変そうです。新メンバーはみんな荒野でも生き抜けるパワーを持っていますがとはいえ力を発揮するリードタイムはだんだん増加していっています。
- プロダクト間で共用している箇所が存在し、壊れやすい
- 共通ではなく共用となっています。一言でいうと1ロジックに2プロダクトの知識が詰まっていたりします。即日壊れます。
- 新しいアイデアの検証をするときのインベストが大きい領域がある
- たとえば、問診を聴取していくフローは症状チェッカーと医療機関向けプロダクトの両方の知識を有していて、さらにそれぞれが複数のパターンを持っているので、分岐が複雑になっています。特定の診療科向けに柔軟に質問を追加したり省略するといったことを気軽に試すのは難しい状態となっています。
技術的負債という視線
個別に現象に対処して行ってもいいですが、現在の人数やプロダクトの増加速度を鑑みると、もう少し中長期目線で考えたほうがよさそうだなぁということで、技術的負債の観点でそれぞれの現象を見直してみました。
ここでいう技術的負債とは、技術的負債の言葉の生みの親であるウォード・カニンガム氏の次の定義に準じます。
「もしも自分たちが書いているプログラム(WyCash)を、金融の世界に関する正しい捉え方だと自分たちが理解した姿と一致させることができなくなれば、自分たちは絶えずその不一致につまずき続けることになり、開発スピードは遅くなっていくでしょう。それはまるで借金の利子を払い続けるかのようです」
それぞれ 正しい捉え方だと自分たちが理解した姿と一致していないのではないか
という観点で見直すと、
- 暗黙知の部分のキャッチアップや変更コストが増加
- システムが、正しい捉え方だと自分たちが理解した姿と一致していない形であることで、リバースエンジニアリング的なアプローチによる理解を困難にしているのではないか。本当に複雑なのか、一致していないのか見直す必要があるのではないか
- プロダクト間で共用している箇所が存在し、壊れやすい
- 共用している部分が、利用者にとって独立した共通のものなのか、似ている別のものなのかが曖昧。あるべき姿にできていないか、あるべき姿をまだ見いだせていない可能性があるのではないか。
- 新しいアイデアの検証をするときのインベストが大きい領域がある
- システムが、あるべき姿の解像度が低い段階の時のままになっていることで大きく結合してしまっているのではないか。そのために不確実性の高いものを部分的に足し合わせる時に必要な変更が大きくなるのではないか。
不確実性の高い部分を素早く検証するために、不一致を許容するのは重要である一方で、不確実性が下がった後もその部分を放置しておくと、やがて様々な症状が現れてくるのかなと思います。
逆にこれらを解決していけば、現象の解消や防止ができるのではないかと考えました。
ROIで考える
正しい捉え方だと自分たちが理解した姿と一致させ続けることはとても重要だと考えつつも、とはいえリソースと時間の制約があるので、どこまでなにをするかをどう決めていくかは大事です。Ubieでは皆2言目にはROIだROIだと言っています。
図はほとんど意味を持っていませんが、つまり、複雑度を上げるような追加変更は線形に増えていきますが、開発のインベストはいつの間にか指数関数的に増加していくので、インベストが爆発するまえになんとかしたい、ということです。同時にこれを予防できればある程度の負債は許容してもよさそうです。
なぜインベストの増加を予防したいのかというと、当然速度やコストに効いてくるからですが、それ以外にも意思決定の歪みを防ぐという観点があるかと思います。
技術的負債によってインベストが増加すると、質の高いissueであっても優先度が下がったりします。重要な検証の優先度が下がるのは時にクリティカルになります。
ということで、直近で解くべき課題は
- 現在すでにインベストが高くなっている箇所の解消
- 将来インベストが高くなると予見される箇所などの可視化
- 不確実性が高い場所と低い場所が共存できるカタチを見つける(機動的な検証が可能な状態)
かなと定義しました。これによって人数やサービスやプロダクトが増えても速度を落とさない、むしろ加速するような状態を作っていきます。
マイクロサービスアーキテクチャとDDD(ドメイン駆動設計)に入門する
守破離ということで、すでにややマイクロサービスぽく分割していることもあって、マイクロサービスアーキテクチャについて改めてしっかりと入門することにしました。
マイクロサービスアーキテクチャの序盤の章では、サービス分割の観点でDDDを参照しています。
DDDの考え方は、技術的負債のあるべき姿と一致させるという考え方と似ています。エリック・エヴァンスのドメイン駆動設計
では歯を食いしばって真のモデルとシステムを一致させる
といった言葉が何度も出てきます。
マイクロサービスアーキテクチャの各種ノウハウを取り入れる前に、まずは現在のシステムとあるべき姿のギャップを、DDDにおけるコンテキストとその境界を描くことで可視化していきました。
あるべき形をコンテキストマップで可視化する
コアドメインはなんだろうというところから初めて、徐々に現在のサービス群が何を提供しているかなどを考えながら付け足していきました。
実際の作業はmiroで複数人で行っています。
あんまり現実のシステムを意識せず、理想状態を考えるとスムーズに行くなぁという感想です。同時にあまりにギャップありすぎてちょっと引くみたいな時もあります。
コンテキスト境界と現実の乖離が分かってくる
コンテキストマップを書いていくと、実際のコンテキストとサービスが横断している場所が見えてきました。次の画像は症状チェッカーと医療機関向けプロダクトが連携する箇所ですが、複数のサービスが1つのコンテキストに対して同時に携わっていることがわかります。実際にこの部分は壊れやすく、慎重な変更が必要になってしまっています。
価値創出の単位と業務フロー
また、コンテキストマップを描いていく中で、1つの業務フローに対して、いくつかの独立したコンテキスト達が複数組み合わせられていることに気づきました。
それぞれのコンテキストは個別に価値を生み出せそうな単位でありますが、現状はプロダクトの特定の業務フローのなかに埋まっています。これらを価値創出の単位として切り出せそうです。
価値創出の単位を自由に組み合わせることができるようになれば、考えてもみなかった、あるいは実現が難しいと思っていたプロダクトをどんどん作り出せるのではないか、と期待できます。
不確実性を内包するためのモジュラモノリスの考え方を取り入れる
コンテキストマップによって現在すでにインベストが高くなっている箇所の解消
のためのアクションの洗い出しや、将来インベストが高くなると予見される箇所などの可視化
ができました。不確実性が高い場所と低い場所が共存できるカタチを見つける
についてはまだまだこれからですが、価値創出の単位がそれぞれ確からしいかどうかを判断したり、新しい不確実性をどのように追加していくかについて、モジュラモノリスの考え方が役に立つのではないかと思っています。
モジュラモノリスはモノリスの中でモジュールによって境界を分離していくものです。すでにUbieは複数のサービスが分かれているので、本当のモジュラモノリスにしていくぞというのは現実的ではありません。しかし複数のコンテキストを持っていそうなサービスにおいて、モジュールによる分離をまずは行うというアプローチは非常にコスパが良いのかなと思っています*4。
価値創出を掛け算可能にするシステムを作る
ここから先はまだこれからです。ということでおそらく大小様々な課題と直面していくと思いますが、一連の活動を価値創出を掛け算可能にするシステム
の第1歩目と定義し、医療の領域の課題解決を加速していきたいと考えています*5。プロダクト開発もエンジニアリングも好きという方は是非一緒にやっていきませんか!?
待ってます。
testing-library/reactでmaterial-uiのTextFieldの値をテストする
material-uiのTextFieldをtesting-library/reactでテストしようとすると、HTMLElementを取り出すところで苦労したのでメモを残す。
TextFieldに付与したaria-labelはinputのラッパー要素に付く
次のTextFieldがどこかのコンポーネントにあるとする。
<TextField name="name" label="name" aria-label="name" variant="outlined" value="title" />
実際にDOMにレンダリングすると次の構造になる。ルートの要素にaria-label
がついていることがわかる。
<div aria-label="name" class="MuiFormControl-root MuiTextField-root" > <label class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled" data-shrink="true" > name </label> <div class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-formControl" > <input aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input" name="name" type="text" value="title" /> <fieldset aria-hidden="true" class="PrivateNotchedOutline-root-1 MuiOutlinedInput-notchedOutline" > <legend class="PrivateNotchedOutline-legendLabelled-3 PrivateNotchedOutline-legendNotched-4" > <span> name </span> </legend> </fieldset> </div> </div>
testing-libraryのgetByLabelText
関数は、ラベル名でHTMLElementを取り出すので、input要素に直接アクセスできないことがわかる。解決策として2つの方法がある。
querySelector関数を使う
1つ目の方法はgetByLabelText
関数でルート要素を取り出したあと、querySelector
関数を使ってinput要素を探すというもの。
import { render, cleanup } from "@testing-library/react/pure"; import "@testing-library/jest-dom/extend-expect"; import { TextField } from "@material-ui/core"; describe("Material UIのTextFieldのテスト", () => { it("querySelectorを使ってvalueの値をチェックする", () => { const result = render( <TextField name="name" label="name" aria-label="name" variant="outlined" value="title" /> ); // input要素を取り出す。 const input = result.getByLabelText("name").querySelector("input"); expect(input?.value).toEqual("title"); }); });
querySelector("input")
の戻り値はHTMLInputElement | null
なので適宜nullチェックを加える必要があるが、値を検証するだけならinput?.value
とするだけで十分。
getByDisplayValue関数を使う
2つめの方法は、getByDisplayValue関数を使ってvalue値からinput要素を取り出す。
describe("Material UIのTextFieldのテスト", () => { it("getByDisplayValueを使ってvalueの値をチェックする", () => { const result = render( <div> <div>title</div> <TextField name="name" label="name" aria-label="name" variant="outlined" value="title" /> </div> ); const input = result.getByDisplayValue("title"); // elementが取り出せた時点で 'title' というvalueを持っていることがわかるので // このexpectは実際は不要 expect(input.getAttribute("value")).toEqual("title"); }); });
getByDisplayValue
関数はinput要素系のための関数なので便利なのだが、値を使って取り出すので同じ値のinput要素があったりするとエラーになってしまう。
おわりに
個人的にはラベルを指定した上でquerySelector
関数を使う方法が好きですが、しかしMaterial UIの内部構造を知った上で書く必要があるのでちょっとな〜という気はしますが仕方ない気もする。
bit全探索 in Kotlin
最近AtCoderなどに参加していて、すべての組み合わせを生成しつつ計算するといった機会になんどか遭遇し、毎回頑張って実装していたのだけど、bit全探索という方法があるらしいと知り、調べて、Kotlinでどう書くか考えた結果次のようになった。
import java.util.BitSet fun bitFullSearch(n: Int): List<BitSet> = (0 until (1 shl n)).map { bit -> BitSet(n).apply { repeat(n) { i -> set(i, bit and (1 shl i) > 0) } } }
たとえば bitFullSearch(4)
などと呼び出すと、それぞれ次のbitが立ったBitSetのリストが手に入る。
{} {0} {1} {0, 1} {2} {0, 2} {1, 2} {0, 1, 2} {3} {0, 3} {1, 3} {0, 1, 3} {2, 3} {0, 2, 3} {1, 2, 3} {0, 1, 2, 3}
bitSet.get(i)
でそのインデックスのビットが有効かBooleanが手に入るほか、bitSet.stream()
で有効なインデックスのストリームが手に入るので大体いい感じにできる。
Material-UIのHidden要素をテストする
個人のプロジェクトでMaterial-UIを使っているんですが、 コンポーネントがHiddenを含んでいると、テストがうまく動きません。
import * as React from 'react'; import {Hidden} from "@material-ui/core"; export const Hoge = (): JSX.Element => { return ( <div> <Hidden mdUp> <div>mdUp</div> </Hidden> <Hidden smDown> <div>smDown</div> </Hidden> </div> ) };
import React from "react"; import {render, cleanup} from '@testing-library/react' import {Hoge} from "../hoge"; afterEach(async () => { await cleanup(); }); describe('hoge', () => { describe('ある幅以上の時のレンダリング', () => { it('mdUpが表示される', () => { const {getByText} = render( <Hoge/> ); expect(getByText("mdUp")).toBeVisible() }); }); });
見つけられない...ていうかそもそもHidden要素がどっちもレンダリングされていない
TestingLibraryElementError: Unable to find an element with the text: mdUp. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. <body> <div> <div /> </div> </body> 13 | <Hoge/> 14 | ); > 15 | expect(getByText("mdUp")).toBeVisible() | ^ 16 | }); 17 | }); 18 | });
環境
"@material-ui/core": "^4.10.2", "react": "^16.13.1", "@testing-library/dom": "^7.16.3", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.3.0", "@testing-library/user-event": "^12.0.7"
原因など
Unable to test React with Material-UI Hidden element · Issue #2179 · enzymejs/enzyme · GitHub によると、jest-domとmaterial-uiの双方に課題があるようです。 -> https://github.com/enzymejs/enzyme/issues/2179#issuecomment-528973289
対応
対応方法は2つほどあるぽいです、ここではかんたんな方を書きます。
テスト側でrenderするときにcreateMuiTheme
を用いて初期の画面幅を指定するやりかたです。
import React from "react"; import {render, cleanup} from '@testing-library/react' import {createMuiTheme, MuiThemeProvider} from "@material-ui/core"; import {Hoge} from "../hoge"; afterEach(async () => { await cleanup(); }); describe('hoge', () => { describe('ある幅以上の時のレンダリング', () => { it('mdUpが表示される', () => { const theme = createMuiTheme({props: {MuiWithWidth: {initialWidth: 'sm'}}}) const {getByText} = render( <MuiThemeProvider theme={theme}> <Hoge/> </MuiThemeProvider> ); expect(getByText("mdUp")).toBeVisible() }); }); });
これで画面幅を指定して実行できます。
initialWidthをmd
にするとsmDownの方の要素が表示状態になります。
おわりに
ReactでTDDでやっていくぞと思って触り始めたけどやはりUI部分はなかなか大変だなと思いつつ、Androidよりは楽かもという気持ち
Jetpack Compose 0.1.0-dev05から0.1.0-dev06にしたときに変更が必要だったところ
前回に引き続いて。
left, rightがstart, endに
レイアウトのleft
, right
が start
, end
になりました。なるだろうなーと思ってたので想定どおり。
before
LayoutPadding( top = 16.dp left = 8.dp, right = 8.dp, bottom = 16.dp )
after
LayoutPadding( top = 16.dp start = 8.dp, end = 8.dp, bottom = 16.dp )
DrawImageを廃止
Image
を描画するDrawImageがなくなりました。代わりにSimpleImage
を使います。このあたりは今後も色々と変わりそうですね。
before
Container( width = 100.dp, height = 200.dp ) { DrawImage(image) }
after
Container( width = 100.dp, height = 200.dp ) { SimpleImage(image) }
androidx.compose.Contextを廃止
ContextAmbientはandroidx.compose.Context
を返してましたが、androidx.compose.Context
になりました。
before
import androidx.compose.Context
after
import android.content.Context
AppBarIconを廃止
TopAppBarのnavigationIconに使うAppBarIconがなくなり、代わりにIconButtonを使う形になりました。
before
TopAppBar( title = { Text(context.getString(R.string.app_name)) }, navigationIcon = { AppBarIcon( icon = ImagePainter(BitmapImage(context.getBitmap(R.drawable.ic_baseline_arrow_back_24))), onClick = { backStack.pop() } ) } )
IconButtonを使う場合のほうが冗長ですが、children: @Composable() () -> Unit
を受け取るのでより柔軟な表現が可能になってます(例えばTextを渡してもちゃんと動く)。
after
TopAppBar( title = { Text(context.getString(R.string.app_name)) }, navigationIcon = { IconButton( onClick = { backStack.pop() } ) { SimpleImage( BitmapImage(context.getBitmap(R.drawable.ic_baseline_arrow_back_24)) ) } } )
ArrangementにVertical, Hotizontalの概念を追加
Column, RowともにArrangement
が設定できますが、寄せる方向の設定がStart
, End
という名前でした。なので次のように同じ値でもColumnかRowかで意味が異なります。
before
Column( modifier = LayoutWidth.Fill, arrangement = Arrangement.End // 下寄せ ) { Row( modifier = LayoutWidth.Fill, arrangement = Arrangement.End // 右寄せ ) { // something } }
ArrangementにVertical, Hotizontalの概念を追加し、使える値を増やしつつ制限をかけています。これにより意味を理解しやすくなりました。
after
Column( modifier = LayoutWidth.Fill, arrangement = Arrangement.Bottom // Arrangement.Endは使えない ) { Row( modifier = LayoutWidth.Fill, arrangement = Arrangement.End ) { // something } }
おわりに
今までは1ヶ月に1リリースという感じでしたが、2月は2回ありました。I/Oに向けてガガガッとスパートかけてる感じなんでしょうか。楽しみですね。
Jetpack Compose 0.1.0-dev05で追加されたAdapterListを眺める
Jetpack Compose 0.1.0-dev05がリリースされましたね。0.1.0-dev05でui-foundationにAdapterList
というComposableが追加されました。
待望のAdapterList
AdapterListの説明は次のようになっています。
A vertically scrolling list that only composes and lays out the currently visible items.
今まではRecyclerViewのようなComposableが存在せず、VerticalScrollerを使ってそれっぽい動作をしていましたが、VerticalScrollerはScrollViewと同じものなので実用には限界がありました。
AdapterListはRecyclerViewと同じように、表示されている要素だけをレンダリングするので、大量の要素があってもサクサク動作します。
AdapterListの使い方
AdapterListのシグネチャは次の通りです。
@Composable fun <T> AdapterList( data: List<T>, modifier: Modifier = Modifier.None, itemCallback: @Composable() (T) -> Unit )
実際に使うには次のようになります。
import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.ui.core.Text import androidx.ui.core.setContent import androidx.ui.foundation.AdapterList import androidx.ui.layout.Container import androidx.ui.layout.LayoutPadding import androidx.ui.layout.LayoutWidth import androidx.ui.material.MaterialTheme import androidx.ui.material.surface.Card import androidx.ui.unit.dp class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { val data = 0.until(100).toList() AdapterList(data = data) { Card(modifier = LayoutPadding(top = 8.dp, bottom = 8.dp, left = 16.dp, right = 16.dp)) { Container(modifier = LayoutWidth.Fill + LayoutPadding(16.dp)) { Text("Hello ${it}") } } } } } } }
AdapterListはまだ実用できない
0.1.0-dev05の時点ではまだ2コミットしかないので、実用に足らないのは当然っちゃ当然かなと思います。 https://android.googlesource.com/platform/frameworks/support/+log/refs/heads/androidx-compose-release/ui/ui-foundation/src/main/java/androidx/ui/foundation/AdapterList.kt
具体的な問題としては 要素のクリックイベントが動作しない
という点があります。例えば次のコードのButtonはうまく動作しません。
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { val data = 0.until(100).toList() AdapterList(data = data) { value -> Card(modifier = LayoutPadding(top = 8.dp, bottom = 8.dp, left = 16.dp, right = 16.dp)) { Container(modifier = LayoutWidth.Fill + LayoutPadding(16.dp)) { Button(onClick = { println("click! $value") }) { Text("button ${value}") } } } } } } } }
いくらか実験してみたところ、Ripple要素があるとクリックイベントが実行されないようです。Buttonは Ripple + Clickableで構成されているので反応しなくなっているようです。Rippleを使わずClickableのみを使えば動作はするのですが、タッチフィードバックがなくなるので厳しいです。
もちろんこうした問題は今後どんどん改善されていくと思いますが、今すぐに使うというのはちょっと難しそうです。
おわりに
ついにAdapterListが登場して実用段階への光が射してきましたね。さすがにプロダクション投入はまだまだ難しいですが、一部のViewをComposeに置き換えるのは十分できそうだなぁと思います。
Jetpack Compose 0.1.0-dev04から0.1.0-dev05にしたときに変更が必要だったところ
Jetpack Compose 0.1.0-dev05が出ました。 リリースノートはこちら https://developer.android.com/jetpack/androidx/releases/compose#0.1.0-dev05
前回に引き続き、アップデートでエラーになる部分と対応方法を紹介していきます。
androidx.ui.layout.Paddingが廃止
androidx.ui.layout.Padding
が廃止され、androidx.ui.layout.LayoutPadding
になりました。PaddingはComposableだったのに対して、LayoutPaddingはModifierです。次のような使い方になります。
before
Padding( 8.dp ) { Text(text = "こんにちは") }
after
Text( modifier = LayoutPadding( 8.dp ), text = "こんにちは" )
FontFamilyのコンストラクタがprivateになった
カスタムフォントを使う時はres/font
にフォントファイルを置きつつ、FontFamilyを作って利用する形だったのですが、FontFamilyのコンストラクタがprivateになり、作り方が変わりました。
before
FontFamily( // res/font/ipam.ttf にファイルを置いておく Font(name = "ipam.ttf", weight = FontWeight.W400, style = FontStyle.Normal) )
after
font(R.font.ipam, weight = FontWeight.W400, style = FontStyle.Normal)
Ambient.ofが廃止
Ambient.ofの代わりにambientOf関数が生えました。Providerも独立して、Providersになりました。Provideする値増えたらネスト大変だなーと思ってたところなのでちょうどいいです。
before
val IpamFontAmbient = Ambient.of<FontFamily>() @Composable fun IpamFontProvider(children : @Composable() () -> Unit) { val fontFamily = remember { FontFamily( Font(name = "ipam.ttf", weight = FontWeight.W400, style = FontStyle.Normal) ) } IpamFontAmbient.Provider(value = fontFamily, children = children) }
after
val IpamFontAmbient = ambientOf<Font>() @Composable fun IpamFontProvider(children : @Composable() () -> Unit) { val font = remember { font(R.font.ipam, weight = FontWeight.W400, style = FontStyle.Normal) } Providers(IpamFontAmbient.provides(value = font), children = children) }
ambient関数が非推奨
上位のComposableからProvideされる値を取り出す時はambient関数を使ってましたが、非推奨になりました。代わりにambient関数にkeyとして渡しているAmbilentインスタンスのcurrent
を使います。
before
val font = ambient(IpamFontAmbient)
after
val font = IpamFontAmbient.current
androidx.ui.core.ambientDensity関数が廃止
Composable内でdensityを取り出せるandroidx.ui.core.ambientDensity関数がなくなりました。
before
import androidx.ui.core.ambientDensity val density = ambientDensity()
after
import androidx.ui.core.DensityAmbient val density = DensityAmbient.current
withDensity関数が廃止
withDensity関数は、DensityScope.() -> Unit
を受け取ることで、Dp.toPx
関数などの拡張関数を使えるスコープを提供するのですが、なくなりました。代わりにDensityScopeと同様の拡張関数を持つDensity
interface が用意されました。DensityAmbient.current
でDensityインスタンスを取り出せるので、kotlinのwith
関数を使って同じことができます。
before
withDensity(density) { Paint().apply { isAntiAlias = true style = PaintingStyle.stroke this.strokeWidth = strokeWidth.toPx().value } }
after
val density = DensityAmbient.current with(density) { Paint().apply { isAntiAlias = true style = PaintingStyle.stroke this.strokeWidth = strokeWidth.toPx().value } }
ImageがPainterに変更
画像の表示はImageを使ってましたがPainterという抽象クラスが使われるようになりました。VectorAssetはImageになれないので、Painterで抽象化するのかなと思ったらVectorPainterはまだないみたいです。
before
AppBarIcon( icon = imageResource(R.drawable.ic_baseline_arrow_back_24), onClick = { backStack?.pop() } )
after
AppBarIcon( icon = ImagePainter(imageResource(R.drawable.ic_baseline_arrow_back_24)), onClick = { backStack?.pop() } )
Buttonの引数が変更
styleが展開されたほか、textがなくなりchildrenになりました。
before
Button( "キャンセル", style = ButtonStyle( backgroundColor = MaterialTheme.colors().secondary, contentColor = MaterialTheme.colors().onSecondary, shape = MaterialTheme.shapes().button, elevation = 2.dp ), onClick = onCloseRequest )
after
Button( backgroundColor = MaterialTheme.colors().secondary, contentColor = MaterialTheme.colors().onSecondary, shape = MaterialTheme.shapes().button, elevation = 2.dp, onClick = onCloseRequest ) { Text("キャンセル") }
終わりに
結構大変だった...。でもだいぶ洗練されてきた印象がある。今後が楽しみです。