はじめに
私は、普段Jetpack ComposeをAndroidアプリ開発で趣味程度に触ってきました。
最近stableになったAndroidのJetpack Composeですが、今年5月にJetpack Compose for WebのTechnology Previewが公開されました。
私の体感ではあるのですが、触ってHello worldなどしている記事は何件か見つけたのですが、何か作って公開して解説などしてる記事などは見かけなかったので今回触ってみようとなったのがきっかけです。
タイトルにある通り、私は今までiOSをほんのちょっとやったことがあって普段はAndroidアプリ開発とFlutterを使ったモバイルアプリ開発をしているので、HTMLやCSSを始めReactなどのWebフロントを開発する技術にとても疎いです。
なので、「あっているのかわからんがとりあえず動いたからこれでいこう」というスタンスで今回Jetpack Composeを使ってポートフォリオサイトを作りました。
色々詳しく調べたりしてないので間違ってることだらけだとは思いますが、ぜひWebフロント周りに知見のある方に気軽に指摘して頂けたら大変嬉しいです。
また、ComposeでAndroidアプリを作ったことはあるけど、for Web気になってる人に向けて最初の学習コストのハードルが下がって触るきっかけになると嬉しいなと思います。
今回作ったのはこちら。
軽く解説を入れながらどのように作ったのか書いていきたいと思います。
やっていき
Getting Started
まず最初何から始めればいいかわかないので、公式のGetting Startedを見ました。
これを見ればまずは、Hello world的な数字をカウントアップ・ダウンさせるページを作ることができます。
Hello worldを超えるために
Jetbrainsが公開しているこのページもComposeで作られているようで、GitHubにソースコードが公開されています。
ComposeでAndroidアプリ作るときみたいにColumnとRowやBoxがたくさん使われているのかと思いきや見たことない景色が広がっていてまずは軽くHTMLのタグ一覧を調べました。
http://www.fureai.or.jp/~irie/html-tag/
正直今も使い分けが全くわからず、雰囲気でレイアウトは作っています。
開発するときは実際のページのレイアウトとソースコードを比較して作りを見たりするために他に参考にするものがまだあまりなかったので、Jetbrainsのリポジトリはめちゃめちゃ参考にしました。
Webでレイアウトを作るときの初見殺し
私が今回作ったポートフォリオでは画面いっぱいにページを表示して中央にコンテンツを表示させ、縦と横もスクロールさせたくなかったです。
Composeでどう設定するかは後ほど説明するとして、そこで調べると、縦と横いっぱいに表示するにはCSSで100vh
と100vw
で縦と横の設定をするようです。
それをComposeで設定したのですが、なんかちょっとだけ縦と横にスクロールバーが表示されてスクロールできてしまう現象に陥りました。
知り合いのWebフロントに知見のあるエンジニアに尋ねると、どうやらデフォルトで当たるCSSのスタイルがあってそれが最初に勝手に設定されていてそれが原因そうとのこと。
それは初見殺しすぎると思いました。
Webフロントの開発はまずそのデフォルトで当たっているCSSのスタイルを無効化して更地に戻すところから始まるようで、その設定をしました。
更地に戻す方法はいくつかあるようなのですが、どれもそのデフォルトのCSSを無効にするリセットCSSと呼ばれるスタイルを設定するようです。
今回私は、こちらの記事でも紹介されているressというリセットCSSを使いました。
これを記事で紹介されている通りに設定すると、ページが画面いっぱいに表示されてちょっとスクロールされてしまう問題も解消しました。
index.html
に下記を追記しただけです。
<link href="https://unpkg.com/ress/dist/ress.min.css" rel="stylesheet">
めでたしめでたし。
ComposeでCSSを用意してフルスクリーンのコンテンツを中央に配置したページを作る
ではさきほど触れた、ページを全画面に表示して中央にコンテンツを表示させるやり方をComposeで設定していきます。
Composeでは、StyleSheet()
を継承したobject
を作ってその中にCSSの内容のようなものを書いていきます。
そこに、画面いっぱいにページを表示して、縦と横もスクロールさせずにコンテンツを中央に配置するためにこの記事を参考に実装しました。
CSSでは以下のような設定をしてHTMLのタグにそのスタイルを当てると要件を満たせるそう。
.box {
width: 100vw;
height: 100vh;
display: flex; /* 要素をflexboxに対応させる */
align-items: center; /* 縦方向の中央揃え */
justify-content: center; /* 横方向の中央揃え */
flex-direction: column; /* 子要素の並びを上から下にする(要素の改行に対応) */
}
それをComposeで書くとこんな感じです。StyleSheet()
を継承したAppStyleSheet
というobject
を定義しました。
ちょっと背景色を設定してるなどの違いがありますが、雰囲気でCSSとComposeでのスタイルの定義の違いがわかると思います。
今回ではCSSの.box
で定義している内容がcenterContainer
に定義している内容に当たります。
object AppStyleSheet : StyleSheet() {
val centerContainer by style {
display(DisplayStyle.Flex)
justifyContent(JustifyContent.Center)
flexDirection(FlexDirection.Column)
alignItems(AlignItems.Center)
width(100.vw)
height(100.vh)
background("#F1F1F1")
}
}
これをComposableに設定するのはこんな感じです。
これもHTMLにCSSを当てるのと同じような雰囲気で書けるのでなんとなくわかると思います。
@Composable
fun Layout(content: @Composable () -> Unit) {
Div(attrs = {
classes(AppStyleSheet.centerContainer)
}) {
content()
}
}
このLayout()
を使って根本のmain()
でページの内容をwrapするとページの内容が全画面かつ中央に配置されます。
fun main() {
renderComposable(rootElementId = "root") {
Style(AppStyleSheet)
Layout {
// ページの内容
}
}
}
レイアウトの組み方
Compose for WebにもColumn
とRow
はあります。HTMLタグのattrs
にflexDirection
とdisplay
を設定するとその中のコンテンツがColumn
になったりRow
になったりします。
HTMLのタグであるDiv
だったりP
だったりA
だったりのComposableが用意されていてそれらを使ってレイアウトを作っていけます。
Div(attrs = {
style {
flexDirection(FlexDirection.Column)
display(DisplayStyle.Flex)
}
}) {
// コンテンツの中身
}
スタイルを当てる際は、これは普通のHTMLとCSSと同じように、タグに直接style
でattrs
にスタイルを設定することもできますが、AppStyleSheet
に書いたスタイルをタグに設定することもできます。
上で書いた@Composable Div()
のstyle
に直接flexDirection
とdisplay
の設定をするのと、下のようにAppStyleSheet
に書いたスタイルを@Composable Div()
のclasses
で設定するのは同等のことです。
val columnContainer by style {
flexDirection(FlexDirection.Column)
display(DisplayStyle.Flex)
}
Div(attrs = {
classes(columnContainer)
}) {
// コンテンツの中身
}
Compose for Webではこんな感じでどんどんレイアウトを作っていきます。
テキストスタイルを設定する
テキストのフォントやフォントサイズを変更するスタイルはこんな感じで定義しました。
object TextStyleSheet : StyleSheet(AppStyleSheet) {
val text by style {
property(
"font-family",
"Lato, Noto Sans JP, system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,Arial,sans-serif",
)
}
}
使うときはこんな感じ。正直これもDiv
でwrapしていますがあっているのかはわかりません。
Div(attrs = {
classes(TextStyleSheet.text)
}) {
Text("あいうえお")
}
フォントはCSSでも同様に、読み込んでほしいフォントを優先順位付けして複数定義するようです。今回はLato
というフォントを最優先に表示するようにしています。
さきほどのレイアウト組むときのスタイルでは、flexDirection()
などCompose側で用意されている設定を使ってスタイルを定義していきましたが、今回出てきたproperty
を使えば、文字列でfont-family
などのCSSでも設定できる設定項目をComposeでも設定できます。色々試してみると面白いかもしれません。
Textにフォントサイズなどを変更させたいときはこんな感じです。
val text by style {
fontSize(24.px)
fontWeight(800)
property(
"font-family",
"Lato, Noto Sans JP, system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,Arial,sans-serif",
)
}
フッター
フッターを作るときは用意されている@Composable Footer()
を使って作れます。
今回のポートフォリオでもフッターを実装したのですが、スタイルはこのように定義してFooter()
に設定しました。
これで下にへばりついたコンテンツを画面に描画することができます。
val footer by style {
display(DisplayStyle.Flex)
position(Position.Fixed)
bottom(0.px)
justifyContent(JustifyContent.Center)
paddingBottom(16.px)
alignItems(AlignItems.Center)
width(100.vw)
}
Footer(attrs = { classes(AppStyleSheet.footer, TextStyleSheet.caption) }) {
Text("© 2021 · Powered by Jetpack Compose")
}
Composeで動くボタンを作る
今回実装したページにはボタンにカーソルを当てると沈み込む「Contact me」ボタンを実装してみました。
これはこのサイトでいい感じのボタンを見つけて、それぞれCSSの設定を見ていくつか組み合わせて作りました。
ここまでくると、CSS初見の私でもだんだん理解してきて適宜読み替えてComposeのStyleSheet
に書いて好きなように動くボタンを作ることができました。
少し定義の仕方が難しかったのはカーソルがボタンに当たったとき(ホバーされたとき)のスタイルの定義の仕方です。
これは、hover()
という設定項目が用意されていたので、そのstyle
の中にホバーされたときのレイアウト設定を書きました。
今回実装した、後ろにうっすら影があってカーソルを当てたときに沈み込むボタンは以下のようなコードで実装しました。
沈み込む動作に加えて、最初はうっすらついている影(property("box-shadow", "0px 5px 12px #CCCCCC, -6px -6px 12px #FFF")
)を沈み込んだときに影を濃くするようにしたりしてみました。(property("box-shadow", "0px 0px 4px #000000, -2px -2px 4px #FFF")
)
val bouncedButton by style {
display(DisplayStyle.Flex)
justifyContent(JustifyContent.Center)
alignItems(AlignItems.Center)
height(44.px)
width(240.px)
boxSizing("border-box")
color(Color("#000000"))
letterSpacing(0.1.em)
textDecoration("none")
position(Position.Relative)
}
val bouncedButtonSpan by style {
display(DisplayStyle.Flex)
justifyContent(JustifyContent.Center)
alignItems(AlignItems.Center)
height(44.px)
width(240.px)
background("#FFFFFF")
border {
width = 1.px
style = LineStyle.Solid
color = Color("#000000")
}
boxSizing("border-box")
top((-6).px)
left((-6).px)
property("transition-duration", "0.2s")
property("box-shadow", "0px 5px 12px #CCCCCC, -6px -6px 12px #FFF")
hover(self) style {
left((-1).px)
top((-1).px)
property("box-shadow", "0px 0px 4px #000000, -2px -2px 4px #FFF")
}
}
@Composable
fun BouncedButton(href: String, title: String) {
A(
attrs = {
classes(AppStyleSheet.bouncedButton, AppStyleSheet.bouncedButtonSpan)
},
href = href,
) {
Div(attrs = {
classes(TextStyleSheet.caption)
}) {
Text(title)
}
}
}
ホバーされる前のボタンの背後のデザインなど少しデザインを変えて作っているとこもありますが、参考にしたCSSはこんな感じでした。
これらをいい感じに読み替えてStyleSheet
に設定しました。
section {
max-width: 300px;
margin: 0 auto;
}
a.btn_06 {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 50px;
box-sizing: border-box;
background: repeating-linear-gradient(45deg, #ffffff, #ffffff 3px, #e7e7e7 3px, #e7e7e7 30px);
color: #333;
font-size: 14px;
letter-spacing: 0.1em;
text-decoration: none;
position: relative;
}
a.btn_06 span {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 50px;
background: #fff;
border: 1px solid #000;
box-sizing: border-box;
position: absolute;
top: -6px;
left: -6px;
transition-duration: 0.2s;
}
a.btn_06:hover span {
left: -1px;
top: -1px;
}
レスポンシブ対応
今回作ったポートフォリオは自分の名前などの情報とアイコンをRowで横並びにレイアウトしたことにより、スマートフォンのサイズなど横幅が狭くなったときのレイアウトがいまいちになる問題があったのでそれを解消すべくレスポンシブ対応もしてみました。
幅が640
になったらRowにしていた自分の情報のテキストとアイコンをColumnにして縦にしていい感じに見えるようにしました。
左から順番にテキスト→アイコンの順番にRowで設定していたので、そのままColumnにすると上から順番にテキスト→アイコンと表示され、個人的にアイコン→テキストの順番でレイアウトしてほしかったため、flexDirection(FlexDirection.ColumnReverse)
を使って上からアイコン→テキストの順番でレイアウトするようにしています。
val bioContainer by style {
flexDirection(FlexDirection.Row)
display(DisplayStyle.Flex)
alignItems(AlignItems.Center)
media(mediaMaxWidth(640.px)) {
self style {
flexDirection(FlexDirection.ColumnReverse)
display(DisplayStyle.Flex)
alignItems(AlignItems.Center)
}
}
}
@Composable
fun BioSection() {
Div(attrs = {
classes(bioContainer)
}) {
InfoSection()
Div(attrs = { style { marginLeft(32.px) } }) {
Icon()
}
}
}
おわりに
のセクションでソースコードを公開しているので、詳しくはそちらを見てもらえればと思うのですが、画面幅が狭くなったときにフォントサイズを小さくしたりなどもしています。
できたポートフォリオを公開する
さあ、Compose for Webでサイトができたら外部に公開してみましょう。
公開するときに必要なファイル群を生成するには、以下のコマンドで生成します。
$ ./gradlew jsBrowserProductionWebpack
生成されたファイルたちはbuild/distributions
配下にいます。
今回私は、GitHub Pagesを使って公開しました。無料で手軽にできるので最初はおすすめです。
私のGitHub Pages用のリポジトリはこちらです。これらは全てさきほどのコマンドで生成されたファイルたちです。
おわりに
今回作ったComposeのコードはここに置いてあります。少しでも参考になると嬉しいです。
今まで、スマホアプリしか作れなかった自分をWebサイトまでも作れるようにしてくれるComposeとKotlinは最高のツールだなと改めて思いました。iOSのUI部分は置いておいて、KMMも含めてこれでクライアントサイドは一通り作れるようになりそうです。
今回はシンプルな静的サイトを作ったのですが、よくあるGitHubクライアントやQiitaクライアントみたいなのもCompose for Webで次は作ってみたいですね。