Composeのバケツリレーを救いたい
はじめに
スペースマーケットでAndroidエンジニアをしておりますseoです。
Jetpack ComposeになってUI作成のスピードが上がったものの、引数の多さには目を背けたくなるときがあります😣
そこで今回はCompositionLocal
を使って、いくつかの引数を消してバケツリレーを少しだけ解消する方法を考えたいと思います。
CompositionLocalについて
CompositionLocalは明示的にパラメーターを渡すことなく、下層のComposableにパラメーターを渡すことのできる方法です。
- CompositionLocalに値を指定する
- CompositionLocalの使用
これを使うことによって、中間のComposableに値を渡すことなく、必要な箇所で参照することができます。
イベント測定メソッドを例に挙げて
今回はCompositionLocalを使って、イベント計測をするメソッドを下層のComposableに伝える方法をしてみたいと思います。
CompositionLocalを使わない場合
まずは、明示的にパラメーターを渡す方法でやってみます。
HomeFragmentにComposable関数が乗っているViewです。
@AndroidEntryPoint
class HomeFragment : Fragment() {
private val homeViewModel: HomeViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
homeViewModel.fetchHomeData()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setContent {
val uiState by homeViewModel.uiState.collectAsState()
HomeScreen(
uiState = uiState,
sendErrorResult = homeViewModel::sendErrorResult,
sendTapReloadButton = homeViewModel::sendTapReloadButton,
fetchData = homeViewModel::fetchData,
sendHomeScreen = homeViewModel::sendHomeScreen,
)
}
}
}
}
HomeViewModelでTrackingHelperクラスをDIしています。
TrackingHelperクラスにFirebaseやAmplitudeといった計測イベントのメソッドが定義されています。
@HiltViewModel
class HomeViewModel @Inject constructor(
private val repository: Repository,
private val trackingHelper: TrackingHelper,
) {
private val _uiState: MutableStateFlow<HomeUiState> = MutableStateFlow(HomeUiState())
val uiState = _uiState.asStateFlow()
fun fetchHomeData() {
repositry.fetch().onEach {
_uiState.update { it }
}.launchIn(viewModelScope)
}
fun sendShowErrorResult() {
trackingHelper.showErrorResult()
}
fun sendTapReloadButton() {
trackingHelper.tapReloadButton()
}
fun sendShowHomeScreen() {
trackingHelper.showHomeScreen()
}
}
こちらが子Composeです。孫Composeで必要となるパラメーターを渡します。
@Composable
fun HomeScreen(
uiState: HomeUiState,
sendErrorResult: () -> Unit,
sendTapReloadButton: () -> Unit,
fetchData: () -> Unit,
sendHomeScreen: () -> Unit,
) {
Scaffold {
when (uiState) {
is Loading -> LoadingIndicatorView()
is Error -> ErrorView(
sendErrorResult = sendErrorResult,
sendTapReloadButton = sendTapReloadButton,
fetchData = fetchData,
)
is Success -> SuccessView(
sendHomeScreen = sendHomeScreen
)
}
}
}
こちらが孫composeです。
@Composable
fun ErrorView(
sendErrorResult: () -> Unit,
sendTapReloadButton: () -> Unit,
fetchData: () -> Unit
) {
LaunchedEffect(Unit) {
sendErrorResult()
}
Column {
Text('エラーが発生しました')
Button(
text = 'リロード',
onClick = {
fetchData()
sendTapReloadButton()
)
}
}
@Composable
fun SuccessView(
sendHomeScreen: () -> Unit
) {
LaunchedEffect(Unit) {
sendHomeScreen()
}
Column {
...
}
}
CompositionLocalを使ってイベント関数を渡す場合
次に、CompositionLocalを使って、必要な箇所でtrackingHelperインスタンスを参照できるようにしましょう。
はじめに、CompositionalLocalの定義を行い、孫まで渡したいパラメーターをCompositionLocalProviderを使ってCompositionalLocalに値を設定します。
+ val LocalTrackingHelper = staticCompositionLocalOf<TrackingHelper> { error("No TrackingHelper provided") }
@AndroidEntryPoint
class HomeFragment : Fragment() {
private val homeViewModel: HomeViewModel by viewModels()
+ @Inject
+ lateinit var trackingHelper: TrackingHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
homeViewModel.fetchHomeData()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setContent {
+ CompositionLocalProvider(LocalTrackingHelper provides trackingHelper) {
val uiState by homeViewModel.uiState.collectAsState()
HomeScreen(
uiState = uiState,
sendErrorResult = homeViewModel::sendErrorResult,
sendTapReloadButton = homeViewModel::sendTapReloadButton,
fetchData = homeViewModel::fetchData,
sendHomeScreen = homeViewModel::sendHomeScreen,
)
+ }
}
}
}
}
そうすることで孫composeでは直接trackingHelper
インスタンスを召喚することができます。
@Composable
fun ErrorView(
- sendErrorResult: () -> Unit,
- sendTapReloadButton: () -> Unit,
fetchData: () -> Unit
) {
+ val trackingHelper = LocalTrackingHelper.current
LaunchedEffect(Unit) {
trackingHelper.sendErrorResult()
}
Column {
Text('エラーが発生しました')
Button(
text = 'リロード',
onClick = {
fetchData()
- sendTapReloadButton()
+ trackingHelper.sendTapReloadButton()
)
}
}
@Composable
fun SuccessView(
- sendHomeScreen: () -> Unit
) {
+ val trackingHelper = LocalTrackingHelper.current
LaunchedEffect(Unit) {
- sendHomeScreen()
+ trackingHelper.sendHomeScreen()
}
Column {
...
}
}
子ComposeとViewModelについても、不要な引数は削除することができました!
@Composable
fun HomeScreen(
uiState: HomeUiState,
- sendErrorResult: () -> Unit,
- sendTapReloadButton: () -> Unit,
fetchData: () -> Unit,
- sendHomeScreen: () -> Unit,
) {
Scaffold {
when (uiState) {
is Loading -> LoadingIndicatorView()
is Error -> ErrorView(
- sendErrorResult = sendErrorResult,
- sendTapReloadButton = sendTapReloadButton,
fetchData = fetchData,
)
is Success -> SuccessView(
- sendHomeScreen = sendHomeScreen
)
}
}
}
@HiltViewModel
class HomeViewModel @Inject constructor(
private val repository: Repository,
- private val trackingHelper: TrackingHelper,
) {
private val _uiState: MutableStateFlow<HomeUiState> = MutableStateFlow(HomeUiState())
val uiState = _uiState.asStateFlow()
fun fetchHomeData() {
repositry.fetch().onEach {
_uiState.update { it }
}.launchIn(viewModelScope)
}
- fun sendShowErrorResult() {
- trackingHelper.showErrorResult()
- }
- fun sendTapReloadButton() {
- trackingHelper.tapReloadButton()
- }
- fun sendShowHomeScreen() {
- trackingHelper.showHomeScreen()
- }
}
だいぶスッキリしましたね😊
CompositionLocalの弊害
ただしCompositionLocalにはデメリットもあり、公式でも下記の通り、多様は勧めていないようです。
ただし、CompositionLocal が常に最適な解決手段だとは限りません。CompositionLocal の乱用はおすすめしません。次のようなデメリットがあるためです。
CompositionLocal を使用すると、コンポーザブルの動作を理解することが難しくなります。暗黙的な依存関係ができるため、それを使用するコンポーザブルの呼び出し元で、すべての CompositionLocal の値が満たされていることを確認する必要があります。
そのため、チーム内でこういった場合はCompositionLocalで渡して、それ以外は明示的にパラメーターを渡す、など約束事を決めて運用するのがいいのではないかと思います。
最後に
スペースマーケットでは一緒に働く仲間を募集しています!
カジュアルに話を聞きたいだけという方でも大歓迎ですので、ちょっとでも興味があれば以下からご応募お待ちしております!
▼インフラエンジニア
▼Webエンジニア
▼アプリシニアエンジニア(EM候補)
▼バックエンドエンジニア(EM候補)
▼エンジニア採用ページ(迷ったらこちらからどうぞ!)
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion