こんにちは、マンガアプリチームの id:nabe1216 です。
2021年11月より、はてな開発のマンガビューワのアプリ版「GigaViewer for Apps」を提供開始し、最初の導入事例として「コミックガルド+(プラス)」がリリースされました。 GigaViewer for Appsは各社のマンガ配信サイトで採用されている「GigaViewer for Web」のアプリ版で、GigaViewer for Webと同様にさまざまな規模のマンガアプリが導入できるように開発しています。
GigaViewer for AppsのUI周りは全て、Androidで新しく登場した宣言的UIフレームワークであるJetpack Composeで実装しています。この記事ではJetpack Composeの使い方や、関連して採用した技術について紹介します。
- Jetpack Composeを採用した背景
- 画面遷移にNavigation Composeを使用
- 状態管理は公式ドキュメントと似た実装に
- 関連して採用した技術
- アプリの実装上で工夫した点
- Jetpack Composeでアプリを作ってみて
Jetpack Composeを採用した背景
Jetpack Composeを採用した理由としては、実装したUIコンポーネントの再利用のしやすさが、GigaViewer for Appsをさまざまなマンガアプリで導入できるビューワにする上で役に立ちそうだったことがあります。
開発をスタートした2021年1月時点で、Jetpack Composeはまだアルファ版の状態でしたが、リリース予定の2021年秋頃には安定版が出ているだろうと予想していました。
社内のエンジニアもJetpack Composeに関心があって、個々でキャッチアップしており、そういった背景からJetpack Composeで開発を進めていくことになりました。
画面遷移にNavigation Composeを使用
Jetpack Composeを採用して悩むポイントに、画面遷移があります。GigaViewer for Appsでは、Navigation Composeを使いました。
Navigation Composeを使うと、遷移や画面間で値を渡すコードが文字列ベースになってしまいます。同じチームのid:takuji31が、画面の定義と引数をコード生成してくれるnavigation-compose-screenというプラグインを開発してくれたので、組み合わせて使用しています。
@AutoScreenId("ExampleScreen") enum class ExampleScreenId { @Route("/") Home, @Route("/detail/?id={id}") Detail, ; } @Composable fun Main(navController: ScreenNavController) { val currentScreen by navController.currentScreen.collectAsState() ScreenNavHost( navController = navController, startScreen = ExampleScreen.Home, ) { exampleScreenComposable { home { Home() } detail { Detail() } } } } class DetailViewModel(): ViewModel() { private val screen: ExampleScreen.Detail by savedStateHandle.screen() val id = screen.id }
状態管理は公式ドキュメントと似た実装に
状態管理では、ViewModel
にUiState
を1つ持たせて、これを変更する形にしています。複雑な状態の表現がしやすい実装を模索して、この実装にたどり着きました。
data class UiState<T : Any>( val isLoading: Boolean = false, val error: Event<Exception>? = null, val data: T? = null, ) @HiltViewModel class ExampleViewModel @Inject constructor() : ViewModel() { private val _state: MutableStateFlow<UiState<*>> = MutableStateFlow(UiState()) val state: StateFlow<UiState<*>> = _state.asStateFlow() } @Composable fun Example() { val viewModel: ExampleViewModel = hiltViewModel() val uiState by viewModel.state.collectAsState() ... }
実は、この後に公式ドキュメントの「UI Layer」が同じような実装に刷新され、奇跡的に公式に則った形となりました。
関連して採用した技術
SplashScreen API
開発時期がちょうどAndroid 12のリリース時期と重なっていたこともあり、Android 12で登場したSplashScreen APIも取り入れました。
JetpackからSplashScreen APIをバックポートするライブラリが出たこともあり、このライブラリを採用してスプラッシュ画面を実装しました。
ライトテーマとダークテーマそれぞれにロゴを用意しています。
ライトテーマ | ダークテーマ |
---|---|
Apollo Client (GraphQL)
API周りには、Apollo Client(GraphQL)を採用しています。
UIコンポーネントが必要なデータをGraphQLのFragmentで宣言することで、コンポーネントで必要なデータだけを取得するFragment Colocationという考え方があり、これがJetpack Composeの宣言的UIとも相性がいいということもあります。
GraphQLのクエリは、UIコンポーネントに表示させたいデータに依存しています。そのためGigaViewer for Appsでは、Apollo ClientをREST使用時によくあるRepository層で隠蔽させるのではなく、View側に持たせて使うようにしています。
その他に採用したライブラリ
その他にも採用したライブラリをいくつか挙げていきます。
- Accompanist
Jetpack Composeの便利なライブラリ群 - Coil
画像ローダー - Dagger Hilt
DI - Room / DataStore
DB / データストレージ
アプリの実装上で工夫した点
キーボード操作
Androidアプリは、タブレットやフォルダブル端末だけでなく、Chrome OSのデスクトップ環境で動作することもあります。どの環境でも快適に使えるようにしたいので、アプリをキーボードで操作できるようにしたり、矢印キーでビューワーのページめくりができるようにしています。
Box( modifier = Modifier .onKeyEvent { when (it.key) { Key.DirectionLeft -> { ... true } Key.DirectionRight -> { ... true } else -> { false } } .focusable(), )
タブレットやフォルダブル端末の最適化
タブレットやフォルダブル端末での操作やアプリの使い心地を向上させるため、タブレットやフォルダブルで漫画が見開きで表示されるように実装しています。
フォルダブルかどうかは、JetpackにあるWindowManagerのライブラリで判断しています。
val foldingFeature = windowLayoutInfo?.displayFeatures?.filterIsInstance(FoldingFeature::class.java)?.firstOrNull() val isSpreadPage by derivedStateOf { foldingFeature?.let { (it.orientation == FoldingFeature.Orientation.VERTICAL && it.state == FoldingFeature.State.HALF_OPENED) || (it.orientation == FoldingFeature.Orientation.VERTICAL && it.state == FoldingFeature.State.FLAT && isLargeScreen) } ?: false }
Jetpack Composeでアプリを作ってみて
Jetpack Composeは登場したばかりの技術ということもあり、不十分な部分もあって頑張って実装するところも多々ありはしましたが、UIコンポーネントの状態の管理から解放される宣言的UIの恩恵があり、テストが書きやすいこともあって、実装はとても快適で楽しいものでした。
GigaViewer for Appsはまだ必要最低限の機能が揃ったところで、これからさまざまなメディアのアプリを作れるように機能を追加し、発展させていくところです。
はてなでは、一緒にGigaViewer for Appsを作り上げていく仲間を募集しています!
id:nabe1216
2019年11月入社。マンガアプリチームでAndroidエンジニアを務める。個人でもアプリを作成しストアで公開するなどの活動も。
Twitter: @NabeCott
GitHub: NUmeroAndDev
Qiita: @Nabe1216