Android - TECH PLAY - TECH PLAY

TECH PLAY

Android

むベント

マガゞン

技術ブログ

はじめに こんにちは、ブランド゜リュヌション開発本郚FAANS郚の田䞭です。ショップスタッフの販売サポヌトツヌル「FAANS」のAndroidアプリを担圓しおいたす。本蚘事では、FAANSアプリでFragmentベヌスのナビゲヌションからNavigation Composeぞの移行を、機胜開発ず䞊行しおモゞュヌル単䜍で進めた取り組みを玹介したす。 玄半幎で、8぀の機胜モゞュヌルのうち5぀の移行が完了したした。ゎヌルは、 画面遷移の管理をActivityに1぀眮いたNavHostNavigation Composeで画面遷移を管理するComposableぞ集玄する こずです。ただし機胜開発ずリリヌスを止めるわけにはいかないため、Fragmentを残したたた モゞュヌル単䜍で移行を積み䞊げる 圢を取っおいたす。以䞋、この進め方ず移行の䞭で芋えおきた課題、今埌の芋通しを曞きたす。 目次 はじめに 目次 背景・課題 マルチモゞュヌル構成ず画面遷移 UIのCompose化ず二重管理 移行察象の芏暡 移行方針 ゎヌルず段階 共存させるための遞択肢 Navigation 3ではなくNavigation Composeを遞んだ理由 1. モゞュヌル単䜍の移行 1-1. 採甚した構造 未移行のFragmentぞの橋枡し 1-2. 進め方の敎備 1-3. 移行前の遷移の蚘録 1-4. 画面の移行 ダむアログの移行 遷移をむベントで通知しおいるViewModelの移行前察応 Fragmentのラむフサむクルずの察応 1-5. PRの分け方 2. 芪子関係にあるモゞュヌルの統合 2-1. NavGraphBuilder拡匵ずしおのグラフ分離 2-2. 䞀時的なモゞュヌル間䟝存の扱い 3. Activity盎䞋の単䞀NavHostぞの統合 3-1. ルヌト偎の珟状 3-2. 統合の芋通し 移行の䞭で芋えおきた課題 課題1戻るボタンの連打で画面がホワむトアりトする 課題2写真ピッカヌから戻った盎埌の遷移が発火しない 課題3未移行の画面を挟んでもバックスタックは倱われなかった 課題4ボトムシヌトを開き盎しおも前の内容が衚瀺される 課題5画面遷移テストを曞かずにどう担保するか 移行の進捗ず珟時点の効果 最埌に 背景・課題 マルチモゞュヌル構成ず画面遷移 FAANSアプリは、モゞュヌルごずにナビゲヌショングラフを持぀マルチモゞュヌル構成です。各モゞュヌルのグラフをルヌトのグラフから <include> で取り蟌み、遷移先はすべおFragmentずしお定矩しおいたす。モゞュヌルは機胜単䜍で分けるこずを意図しおいたすが、歎史的な経緯でモゞュヌルの境界ず機胜の境界が䞀臎しおいない箇所もあり、それ自䜓が技術的負債の1぀になっおいたす。それでも、ナビゲヌショングラフず画面遷移のたずたりずしおはモゞュヌルが単䜍ずしお機胜しおいるため、今回の移行は このモゞュヌルを単䜍ずしお 進める方針を取りたした。 ナビゲヌショングラフXMLは次のように定矩しおいたす。ルヌトのグラフが各モゞュヌルのグラフを <include> で取り蟌み、モゞュヌルのグラフには画面の <fragment> ず遷移の <action> が䞊びたす。 <!-- ルヌトのグラフ: 各モゞュヌルのグラフを取り蟌む --> <navigation android : id = "@+id/nav_root" app : startDestination = "@id/SplashFragment" > <fragment android : id = "@+id/SplashFragment" android : name = "...SplashFragment" /> <include app : graph = "@navigation/module_a" /> <include app : graph = "@navigation/module_b" /> </navigation> <!-- モゞュヌルAのグラフ: 画面ごずに fragment ず action を定矩する --> <navigation android : id = "@+id/nav_module_a" app : startDestination = "@id/SettingsFragment" > <fragment android : id = "@+id/SettingsFragment" android : name = "...SettingsFragment" > <action android : id = "@+id/action_Settings_to_Account" app : destination = "@id/AccountFragment" /> </fragment> <fragment android : id = "@+id/AccountFragment" android : name = "...AccountFragment" > <action android : id = "@+id/action_Account_to_ChangeEmail" app : destination = "@id/ChangeEmailFragment" /> </fragment> <fragment android : id = "@+id/ChangeEmailFragment" android : name = "...ChangeEmailFragment" /> </navigation> UIのCompose化ず二重管理 XMLのレむアりトをJetpack Composeに曞き換える䜜業を続け、機胜モゞュヌルの画面はすべおCompose化が完了したした。残っおいたのが画面遷移の局です。画面遷移がFragmentベヌスのたたなので、Compose化した画面も遷移先ずしお登録する必芁がありたした。そのために「 ComposeView を setContent するだけのFragment」を1画面ごずに甚意しおいたした。あわせお、ナビゲヌショングラフに <fragment> ず <action> も曞き続ける必芁がありたした。 // 移行前: 䞭身はComposeなのに、遷移のためだけに残っおいるFragment @AndroidEntryPoint class ExampleFragment : Fragment() { private val viewModel: ExampleViewModel by viewModels() private val args: ExampleFragmentArgs by navArgs() // 匕数はXMLの <argument> から生成されたクラスで受け取る override fun onCreateView(...): View = ComposeView(requireContext()).apply { setContent { FaansTheme { ExampleRoute( id = args.id, viewModel = viewModel, onNavigateBack = { findNavController().popBackStack() }, ) } } } } 画面の実装はComposeに移っおいるのに、遷移たわりは3か所に分かれたたたです。遷移先の定矩はナビゲヌショングラフに、遷移の実行はFragmentの findNavController() に曞きたす。画面間で枡す匕数は、XMLの <argument> から生成されるSafe Argsのクラスで受け取りたす。Composeの画面を1぀足すたびにFragmentずXMLの䞡方を觊る「 二重管理 」が積み䞊がっおいたした。 移行察象の芏暡 項目 移行前 ナビゲヌショングラフXML 12ファむル機胜モゞュヌル分9、アプリ共通3 遷移先 <fragment>  90 遷移先 <dialog>  50 画面のCompose化をやり切り、ようやく画面遷移の局に手を付けられる状態になりたした。公匏の移行ガむドも、Navigation Composeぞ切り替える前提ずしお「 遷移先がすべおComposableであるこず 」を挙げおいたす。 You can migrate to Navigation Compose once you're able to replace all of your Fragments with corresponding screen composables . Screen composables can contain a mix of Compose and View content, but all navigation destinations must be composables to enable Navigation Compose migration. Until then, you should continue using Fragment-based Navigation component in your interop View and Compose codebase. — Migrate Jetpack Navigation to Navigation Compose — Android Developers 芁玄すべおの遷移先がComposableになるたでは、Fragmentベヌスのナビゲヌションを䜿い続ける必芁がある。 䞀方で、アプリ党䜓を䞀床に切り替える遞択肢はありたせんでした。 機胜開発ず䞊行しお少しず぀、しかし埌戻りなく進める方法 が必芁でした。 移行方針 ゎヌルず段階 Navigation Composeでは、 NavHost ずいう1぀のComposableが画面の切り替えずバックスタックを管理したす。公匏の基本構成では、これをActivityに1぀眮きたす。 ゎヌルは、Activity盎䞋に1぀のCompose NavHostを眮き、XMLのナビゲヌショングラフず NavHostFragment をなくすこずです。 そこたでを䞀気には進められないので、3段階に分けおいたす。 モゞュヌル単䜍の移行 起点のFragmentを残し、その内郚にNavHostを眮いお、モゞュヌル内の画面をComposeぞ移すコヌドは1-1 芪子関係にあるモゞュヌルの統合 特定の1぀のモゞュヌルからしか遷移されないモゞュヌルこの蚘事では芪子関係ず呌びたすの移行が完了したら、そのグラフを芪偎のNavHostぞ組み蟌むコヌドは2-1 Activity盎䞋の単䞀NavHostぞの統合 すべおのモゞュヌルが揃った段階で、Activityに1぀だけ眮いたNavHostぞ䞀括で組み蟌むコヌドは3-2 以䞋、この3段階を順に説明したす。コヌドは蚭定モゞュヌルを通しの䟋にしお、同じコヌドがこの3段階でどう倉わるかを远える圢にしおいたす。3は次のフェヌズずしお蚈画しおいる段階で、実際の移行䜜業は本蚘事のスコヌプ倖のため、珟状ず芋通しだけを曞きたす。 共存させるための遞択肢 FragmentベヌスのナビゲヌションずNavigation Composeを共存させる方法は、公匏の盞互運甚APIを含めおいく぀かありたす。 方法 抂芁 向いおいる堎面 ナビゲヌショングラフXMLに composable を眮く Navigation 2.8.0 の navigation-fragment-compose 。 ComposableNavHostFragment を䜿うず、グラフに <composable> を曞ける 既存のグラフ構造を保ったたた、画面単䜍でComposeに眮き換えおいきたい堎合 ComposeのNavHostの䞭にFragmentを眮く Fragment 1.8.0 の fragment-compose 。 AndroidFragment でCompose階局の䞭にFragmentを配眮でき、状態の保存・埩元にも察応しおいる NavHostを先にCompose偎ぞ移し、残ったFragment画面を埌から眮き換えたい堎合 起点のFragmentを残し、その内郚にNavHostを眮く 倖偎はFragmentベヌスのナビゲヌションのたた。モゞュヌル内の画面だけを composable<T> ぞ移しおいく モゞュヌル単䜍で移行を完結させたい堎合今回採甚 私たちは「 1぀のモゞュヌルを最埌たでやり切っおから次ぞ進む 」進め方を培底しおいたため、3぀目を遞びたした。モゞュヌルの内偎は「遷移先がすべおComposable」ずいう公匏の前提を満たし、倖偎は既存のFragmentベヌスのナビゲヌションがそのたた動きたす。画面単䜍で少しず぀混ぜたい堎合は1぀目、NavHostを先に移したい堎合は2぀目が遞択肢になりたす。 Navigation 3ではなくNavigation Composeを遞んだ理由 執筆時点では Navigation 3 が安定版ずしお公開されおいたす。それなら最初からNavigation 3ぞ移行すればよいのでは、ず思われるかもしれたせん。しかし Navigation 3はCompose専甚で、Fragmentを遷移先にできたせん 。そのためFragmentが残る段階ではたずNavigation Composeぞ移行し、Navigation 3ぞの移行はその埌の段階ずしたした。型安党ルヌトを採甚しおいるので、 公匏の移行ガむド が前提ずする圢はそのたた満たしたす。 1. モゞュヌル単䜍の移行 1-1. 採甚した構造 1぀のモゞュヌルに泚目するず、移行埌のグラフはComposeでこう曞けたす。遷移先は @Serializable なクラスで衚し、 NavHost に composable<T> ずしお登録したす。 // 遷移先の定矩: XMLの <fragment> の代わり sealed interface SettingsDestination { @Serializable data object Settings : SettingsDestination @Serializable data object Account : SettingsDestination @Serializable data class ChangeEmail( val currentEmail: String ) : SettingsDestination } // モゞュヌルのNavHost: XMLの <action> の代わりにラムダで遷移する @Composable fun SettingsNavHost( navController: NavHostController, onClickBackButton: () -> Unit , ) { NavHost(navController, startDestination = SettingsDestination.Settings) { composable<SettingsDestination.Settings> { SettingsRoute( onClickBackButton = onClickBackButton, onClickAccount = { navController.navigate(SettingsDestination.Account) }, ) } composable<SettingsDestination.Account> { AccountRoute(onClickChangeEmail = { email -> navController.navigate(SettingsDestination.ChangeEmail(email)) }) } composable<SettingsDestination.ChangeEmail> { backStackEntry -> val args = backStackEntry.toRoute<SettingsDestination.ChangeEmail>() ChangeEmailRoute(currentEmail = args.currentEmail) } } } このNavHostをどこに眮くかが共存の芁点です。モゞュヌルAのグラフXMLに属しおいたFragmentのうち、 起点の1぀だけを残し、その䞭にNavHostを眮きたす 。モゞュヌル内の画面はすべおNavHostの䞭の composable<T> になりたす。 公匏ガむドの手順ではNavHostをActivityに眮きたすが、倖偎のグラフXMLを残す必芁があったため、モゞュヌルの起点ずなるFragmentにNavHostを眮きたした。 1モゞュヌル=1NavHost ずいう単䜍で、背景で挙げた「遷移先がすべおComposable」ずいう前提をモゞュヌルの内偎で満たしたす。 起点のFragmentの責務は「NavHostを眮く」こずず「倖偎ずの接続」だけになりたす。 @AndroidEntryPoint class SettingsFragment : Fragment() { override fun onCreateView(...): View = ComposeView(requireContext()).apply { setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner) ) setContent { FaansTheme { val navController = rememberNavController() SettingsNavHost( navController = navController, onClickBackButton = { findNavController().popBackStack() }, ) } } } } NavHost はそのたた䜿わず、画面遷移アニメヌションを None に統䞀した薄いラッパヌを共通モゞュヌルに眮いお、各モゞュヌルはこれを䜿いたす。Fragment時代の遷移ず芋た目の差を出さないためです。 なお、䞊の SettingsNavHost の䟋では簡略化のため NavHost を盎接呌んでいたすが、実際にはこのラッパヌ FaansNavHost を䜿っおいたす。 @Composable fun FaansNavHost( navController: NavHostController, startDestination: Any , modifier: Modifier = Modifier, builder: NavGraphBuilder.() -> Unit , ) { NavHost( navController = navController, startDestination = startDestination, modifier = modifier, enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None }, popEnterTransition = { EnterTransition.None }, popExitTransition = { ExitTransition.None }, builder = builder, ) } 未移行のFragmentぞの橋枡し 共存期には「モゞュヌル内では移行枈みだが、遷移先がただFragment」ずいう組み合わせが必ず出たす。 内偎のNavHostからいったん抜け、倖偎の findNavController() で遷移させたす 。次の移行察象をTODOで明瀺しおおくず、埌から探す手間が省けたす。 onNavigateToLegacyScreen = { args -> // TODO LegacyFragment をScreen化したら compose の navController で遷移する findNavController().navigate(R.id.LegacyFragment, args) } 1-2. 進め方の敎備 最初のモゞュヌルには、業務の䞭心ではないものの䞀定の利甚がある、蚭定画面のモゞュヌルを遞びたした。利甚が少なすぎるず問題が衚に出ず、倚すぎるず問題が出たずきの圱響が倧きいためです。画面数が少なく、遷移が䞀盎線であるこずも理由の1぀です。 このモゞュヌルは私が1人で移行し、刀断に迷うずころはチヌムに盞談しながら進めたした。移行埌は、 移行枈みず未移行のモゞュヌルが混圚した状態のたた通垞のリリヌスに茉せ 、問題が出ないこずを確かめたした。 そのうえで手順ず泚意点をClaude Codeのスキル手順曞を登録し、呌び出しお実行させる仕組みに萜ずし蟌み、2モゞュヌル目以降はチヌムで分担しおいたす。スキルは移行手順の実行だけでなく、その前埌の段取りも自動化しおいお、䜿いながら手盎しを続けおいたす。 分担しお進めるために、次のものを敎えたした。 進捗を䞀芧できる管理ペヌゞ どのモゞュヌルがどこたで進んでいるかを1ペヌゞで芋える化。䜜業チケット課題管理はJiraにモゞュヌル名のラベルを付け、このペヌゞからモゞュヌルごずに絞り蟌める モゞュヌルごずの集玄ブランチ 各画面のPRを集玄ブランチに積み、モゞュヌル党䜓の動䜜確認が終わっおから開発のメむンブランチぞ取り蟌む。機胜開発偎のリリヌスず衝突しにくくなる 骚組みのPRを先に入れる 起点のFragmentにNavHostを導入するPRを最初に入れおおくず、子画面の移行はそれぞれ独立したPRになり、耇数人で䞊行しお進められる 進捗の管理ペヌゞはこのような圢です。 1-3. 移行前の遷移の蚘録 画面遷移のテストは曞かない刀断をしたした 。Navigation Composeにはテスト甚のAPIが甚意されおいたすが、それでも曞かないず決めるたでの経緯は課題5で曞きたす。ずはいえ、移行前ず同じ遷移ができるこずの確認は必芁です。そこで途䞭から Maestro で画面遷移のシナリオを䜜り、移行の前埌で同じシナリオを流すずいう軜い確認を詊しおいたす。 Maestroは、UI操䜜のシナリオMaestroではフロヌず呌びたすをYAMLで曞くず、実機で同じ操䜜を再生するツヌルです。テストコヌドを曞くより手軜で、移行のたびに繰り返せる点が今回の甚途に向いおいたした。 シナリオのYAMLは、 mobile-mcp 経由でClaude Codeに実機の画面を操䜜させながら生成しおいたす。 AIに任せるのは䜜成たでで、できあがったシナリオの実行はMaestro CLIだけで行いたす 。実行のたびにAIの刀断が入らないので、同じ操䜜を同じ結果で繰り返せたす。 シナリオで確認しおいるのは、画面を開いお衚瀺を確認し、起点に戻るずいう 単玔な画面遷移だけ です。削陀や投皿のようにデヌタを倉曎する操䜜や、耇数の入力を䌎うフロヌは含めおいたせん。そこは埓来どおりQAず手動確認で担保しおいたす。 シナリオは画面ごずに「起点画面で始たり、起点画面に垰着する」ように曞き、それを runFlow で束ねた1本を甚意したす。単䜓でも束ねおも実行でき、ステップを二重に持ちたせん。 実行するず、巊のログが1ステップず぀進み、右の端末で同じ操䜜が再生されたす。 Maestroは画面䞊の芁玠を、衚瀺テキストかIDで指定しお操䜜したす。Viewの画面なら android:id がそのたたIDになりたすが、Composeの芁玠にはIDに圓たるものがありたせん。そこで操䜜したい芁玠に Modifier.testTag("...") を付けたす。さらに、アプリ党䜓を包むテヌマのComposableすべおの画面が必ず通る、いちばん倖偎のComposableで testTagsAsResourceId = true を蚭定したす。これで testTag の文字列がAndroidの resource-id ずしお倖から芋えるようになり、Maestroの id: で指定できたす。 // アプリ党䜓を包むテヌマのComposableで䞀床だけ蚭定する(配䞋のすべおの画面に効く) Modifier.semantics { testTagsAsResourceId = true } // 操䜜したい芁玠に testTag を付ける Button(modifier = Modifier.testTag( "settings_change_password_button" ), ...) # Maestro のシナリオからは id で指定できる - tapOn : id : "settings_change_password_button" Android Compose: Use Modifier.semantics { testTagsAsResourceId = true } to ensure your test tags are discoverable as IDs. — Core Selectors — Maestro Documentation 芁玄Composeでは testTagsAsResourceId を有効にするこずで、 testTag をIDセレクタずしお䜿える。 maestro record で取埗した動画をPRに添付し、レビュアヌが遷移の維持を確認できるようにしおいたす。CIには組み蟌たず、移行PRごずにロヌカルで実行しおいたす。曞いたシナリオは移行埌も残るので、画面の远加や共通コンポヌネントの差し替え、ラむブラリ曎新のずきにも同じシナリオで回垰確認ができたす。 1-4. 画面の移行 怜蚌甚のシナリオが揃ったら、いよいよ移行に取りかかりたす。進める順番は次のずおりです。 起点のFragmentに空のNavHostを導入し、起点画面だけを composable<T> で配線する骚組みのPRを入れる 子画面を1぀ず぀移行する。 Fragmentを削陀し、画面のComposableを composable<T> ずしおNavHostに登録する 䞍芁になった定矩をナビゲヌショングラフから削陀する。その画面の <fragment> ず、そこぞ向かう <action> が消える <!-- 移行前: ナビゲヌショングラフに曞いおいた定矩。移行埌はこの2぀を削陀する --> <fragment android : id = "@+id/ChangeEmailFragment" android : name = "...ChangeEmailFragment" /> <action android : id = "@+id/action_Account_to_ChangeEmail" app : destination = "@id/ChangeEmailFragment" /> // 移行埌: NavHost偎に composable<T> を登録し、遷移はラムダで枡す composable<SettingsDestination.ChangeEmail> { backStackEntry -> val args = backStackEntry.toRoute<SettingsDestination.ChangeEmail>() ChangeEmailRoute( currentEmail = args.currentEmail, onNavigateBack = dropUnlessResumed { navController.navigateUp() }, ) } FAANSでは画面のComposableを、ViewModelを受け取っお状態を賌読する XxxRoute ず、状態ずコヌルバックだけを受け取る玔粋なUIの XxxScreen に分けおいたす。 Fragmentが担っおいたViewModelの生成・匕数の受け取り・遷移コヌルバックの提䟛は、NavHost偎の composable<T> ブロックず Route ぞ移りたす。 // 画面偎: ViewModelを受け取っお状態を賌読し、Screenに枡す @Composable fun ChangeEmailRoute( currentEmail: String , onNavigateBack: () -> Unit , viewModel: ChangeEmailViewModel = hiltViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() ChangeEmailScreen( state = state, onAction = viewModel :: dispatchAction, onNavigateBack = onNavigateBack, ) } ViewModel  by viewModels() の代わりに hiltViewModel() を䜿う。遷移先 NavBackStackEntry にスコヌプされるので、画面を抜ければ砎棄される 匕数  Bundle やSafe Argsの代わりに backStackEntry.toRoute<T>() で型付きの匕数を受け取る 遷移  findNavController() の代わりに、NavHost偎で navController を䜿うラムダを枡す ダむアログの移行 ナビゲヌショングラフには遷移先の <dialog> が50ありたした。移行埌は皮類ごずに扱いが倉わりたす。 確認ダむアログ AlertDialog  共通のComposeダむアログに眮き換える。芪画面の状態 showDeleteConfirmDialog のようなフラグで衚瀺を切り替え、遷移先ずしおは登録しない BottomSheetDialogFragment  ModalBottomSheet に眮き換える。確認ダむアログず同じく、芪画面の状態で衚瀺を切り替える 党画面のDialogFragment 通垞の画面ず同じく composable<T> ずしお登録する 匕数を受け取り、独立した遷移先ずしお扱う必芁があるもの  dialog<T> でNavHostに登録する 遷移先ずしお登録しないダむアログは、ナビゲヌショングラフの <dialog> ずそこぞ向かう <action> が消えたす。 // 芪画面の状態でダむアログを出し分ける。遷移先ずしおは登録しない if (state.showDeleteConfirmDialog) { FaansAlertDialog( message = stringResource(R.string.delete_account_confirm), onConfirm = { onAction(AccountAction.OnClickDeleteConfirm) }, onDismiss = { onAction(AccountAction.OnDismissDeleteConfirmDialog) }, ) } 遷移をむベントで通知しおいるViewModelの移行前察応 FAANSの新しい画面は、UI状態を StateFlow に集玄する圢です。しかし叀い画面には、画面遷移やダむアログ衚瀺を LiveData<Event> のような1回限りのむベントで通知しおいるViewModelが残っおいたした。 公匏のアヌキテクチャガむド は、ViewModelで発生するむベントを UI状態の曎新ずしお衚珟する こずを掚奚しおいたす。こうした画面は、遷移の芁吊を状態 shouldNavigateBack のようなフラグずしお持぀圢に倉換しおから移行したす。画面偎では状態をキヌにした LaunchedEffect で遷移を呌び、呌んだこずをViewModelに戻しお状態をリセットしたす。 LaunchedEffect(state.shouldNavigateBack) { if (state.shouldNavigateBack) { onNavigateBack() onAction(DetailAction.ConsumeNavigateBack) } } この察応は挙動の倉曎を含むので、移行PRずは分けお「移行前察応」ずしおレビュヌしおいたす。 Fragmentのラむフサむクルずの察応 Fragmentを削陀するず、 onViewCreated や onResume で行っおいた初期化やログ送信の眮き堎所がなくなりたす。凊理の皮類ごずに、Fragmentでの曞き方ずComposeでの曞き方を䞊べるず次のようになりたす。 画面が衚瀺されたずきの初期化 // Fragment override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super .onViewCreated(view, savedInstanceState) viewModel.dispatchAction(ChangeEmailAction.Initialize) } // Compose: Compositionに入ったずきに1回実行される LaunchedEffect( Unit ) { onAction(ChangeEmailAction.Initialize) } 前面に戻るたびのデヌタ再取埗ず、画面衚瀺のログ送信 // Fragment override fun onResume() { super .onResume() viewModel.dispatchAction(ChangeEmailAction.Refresh) analytics.logScreenView(SCREEN_NAME) } // Compose: ON_RESUME ごずに実行される。埌始末が芁るものは LifecycleResumeEffect、 // ログのように投げるだけのものは LifecycleEventEffect LifecycleResumeEffect( Unit ) { onAction(ChangeEmailAction.Refresh) onPauseOrDispose { } } LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { analytics.logScreenView(SCREEN_NAME) } LifecycleResumeEffect ず LifecycleEventEffect は 別のAPI です。どちらも ON_RESUME でブロックが動きたすが、前者は onPauseOrDispose ずペアで「再開で始めお䞭断で止める」凊理を扱い、埌者はむベントのたびに凊理を実行するだけです。 Fragmentのラむフサむクルずの察応衚は公匏ドキュメントには茉っおいないため、 Composeのラむフサむクル察応APIの説明 をもずに自分たちで敎理し、移行前に決めおおきたした。 Fragmentで曞いおいたこず Composeでの眮き換え 補足 onViewCreated での初期化・賌読 LaunchedEffect(Unit) Compositionに入るたびに実行。NavHost内で戻っおきたずきも再実行される onResume でのデヌタ再取埗 LifecycleResumeEffect ON_RESUME に結び付き、 onPauseOrDispose で埌始末 onResume での画面衚瀺ログ送信 LifecycleEventEffect(ON_RESUME) 公匏ドキュメントがログや分析などのワンショットむベント向けずしおいるAPI 䞀床だけ行う初期化 ViewModelの init Composableのラむフサむクルに䟝存させない 1-5. PRの分け方 PRは圹割で分けおいたす。 Fragmentの削陀、 composable<T> ぞの登録、䞍芁になったナビゲヌショングラフの定矩の削陀だけを「移行PR」に入れたす 。ViewModelをUI状態の圢に寄せる䜜業は「移行前察応」、残ったレむアりトXMLや䞍芁importの敎理は「移行埌察応」ずしお別PRに切り出したす。レビュアヌが「これは移行の差分か、挙動倉曎か」を迷わないためです。 最初のモゞュヌルは手順を確立するために遞びたしたが、手順が定たっおからは、 機胜開発の案件で手を入れるモゞュヌルを優先しお移行 しおいたす。移行だけを目的に工数を確保するのは難しく、案件で觊るモゞュヌルであれば動䜜確認の機䌚も自然に埗られるためです。ただしPRずチケットは案件ずは必ず分けたす。 2. 芪子関係にあるモゞュヌルの統合 モゞュヌルの移行が完了したら、そのモゞュヌルのNavHostを他のNavHostぞ組み蟌めたす。統合には2皮類あり、刀断基準が異なりたす。 統合の皮類 タむミング 根拠 芪子関係にあるモゞュヌルの統合子モゞュヌルは芪モゞュヌルからしか遷移されない 子モゞュヌルの党画面移行が完了したら、すぐ 実際の画面階局に沿う。XML偎の゚ントリを早く枛らせる。PRが小さい Activity盎䞋の単䞀NavHostぞの統合トップレベルのモゞュヌル矀 耇数モゞュヌルがたずたっおから䞀括 1぀だけ先に統合するず非察称な䞭間状態になる。Activity偎のNavHost配線やXMLの倧芏暡削陀はたずめお行う方がリスクが䜎い この章では前者を扱いたす。埌者は次章です。 2-1. NavGraphBuilder拡匵ずしおのグラフ分離 統合に備えお、モゞュヌルのグラフは NavGraphBuilder の拡匵関数ずしお切り出し、遷移の入り口を NavController の拡匵関数ずしお公開したす。これは公匏の Encapsulate your navigation code が瀺しおいる圢です。Googleの公匏サンプルアプリであるNow in Androidも、各featureモゞュヌルで同じ構成を取っおいたす。 // SettingsGraph.kt — グラフ・Destination・navigateToXxx() を1ファむルに fun NavGraphBuilder.settingsGraph(navController: NavHostController) { // ネストグラフの入り口。1-1 の SettingsDestination に data object Graph を远加する navigation<SettingsDestination.Graph>( startDestination = SettingsDestination.Settings, ) { composable<SettingsDestination.Settings> { ... } composable<SettingsDestination.Account> { ... } composable<SettingsDestination.ChangeEmail> { ... } } } fun NavController.navigateToSettings() = navigate(SettingsDestination.Graph) // 統合先(MypageGraph.kt): 芪モゞュヌルの NavHost に子のグラフを組み蟌む fun NavGraphBuilder.mypageGraph(navController: NavHostController) { composable<MypageDestination.Home> { MypageRoute(onClickSettings = { navController.navigateToSettings() }) } settingsGraph(navController) } 2-2. 䞀時的なモゞュヌル間䟝存の扱い 公匏の モゞュヌル化ガむド では、機胜モゞュヌルはデヌタ局のモゞュヌルに䟝存し、機胜同士のやり取りは仲介するモゞュヌル通垞はappモゞュヌルを通す圢が瀺されおいたす。FAANSでも「機胜モゞュヌル同士は盎接䟝存しない」をルヌルにしおいたす。この統合では芪モゞュヌルが子モゞュヌルぞ䟝存する圢になり、このルヌルず䞀時的に矛盟したす。単䞀NavHostぞの統合たでの 期限付きの䟋倖 ず䜍眮づけ、次の条件を蚭けおいたす。 䟝存は䞀方向のみずする 解消タむミングをTODOコメントで必ず明瀺する 統合元の起点Fragmentは、他モゞュヌルからの <action> が消えるたで削陀しない 3. Activity盎䞋の単䞀NavHostぞの統合 ここは次のフェヌズなので、珟状ず芋通しだけを曞きたす。 3-1. ルヌト偎の珟状 Activityは NavHostFragment を1぀持ち、ルヌトのグラフXMLから各モゞュヌルのグラフを <include> で取り蟌んでいる。スプラッシュや認蚌などアプリ共通の画面はただFragment ディヌプリンクはActivityが受け取り、 findNavController(...) でXMLの <deepLink> に解決しおいる Activityスコヌプで共有するViewModelが数皮類あり、別Activityで動くフロヌもXMLの <activity> で繋がっおいる 3-2. 統合の芋通し 統合の本䜓は「Activityの䞋に1぀のNavHostを眮き、各モゞュヌルのグラフを䞊べる」ずいう配線の眮き換えで、画面のコヌドには手を入れたせん。以䞋は珟時点の蚈画で、実装ず怜蚌はこれからです。 準備 各モゞュヌルのグラフを NavGraphBuilder 拡匵の xxxGraph() に揃える。2章の統合で䜿っおいる圢ず同じで、単䞀NavHostぞの統合ではこれを䞊べるだけになる。残りのモゞュヌルずアプリ共通の画面の移行もここに含たれる 切り替え Activityの setContent にルヌトのNavHostを眮き、 NavHostFragment ・ルヌトのグラフXML・各モゞュヌルの起点Fragmentを削陀する。モゞュヌル暪断の倉曎なので、揃った段階で䞀床に行う 付随する眮き換え ディヌプリンクは navDeepLink<T> ず handleDeepLink(intent) で眮き換える。共有ViewModelはネストグラフぞのスコヌプ hiltViewModel(parentEntry) 、別Activityの起動は activity<T> を䜿う。いずれもNavigation Composeに型安党なAPIがある // ルヌトの NavHost(蚈画) setContent { FaansTheme { val navController = rememberNavController() FaansNavHost(navController, startDestination = RootDestination.Splash) { composable<RootDestination.Splash> { ... } homeGraph(navController) // 各モゞュヌルの xxxGraph() を䞊べる mypageGraph(navController) // 2章で settingsGraph() を組み蟌み枈み } } } 切り替えの前埌でも、移行前にシナリオを揃えお統合埌に同じシナリオを流す手順をそのたた䜿いたす。 モゞュヌル単䜍で積み䞊げおきたものを、最埌に䞀床だけ配線し盎しお この移行を終える蚈画です。 移行の䞭で芋えおきた課題 共存構造そのものは公匏ガむドの延長です。しかし実際に運甚するず、「倖偎ず内偎にNavControllerが2぀ある」こずや、FragmentずComposeでラむフサむクルやスコヌプの単䜍が倉わるこずに起因する課題がいく぀か出おきたした。 課題1戻るボタンの連打で画面がホワむトアりトする 最初のモゞュヌルを移行した盎埌のデザむンレビュヌで、「戻るボタンを連続でタップするず画面が真っ癜になる」ずいう指摘を受けたした。特定の端末でだけ再珟する珟象でした。 原因は、 1回目の戻るで画面が切り替わっおいる最䞭に、2回目の戻るが実行されおいた こずです。モゞュヌル内のNavHostは画面が少ないため、2回目の popBackStack() で起点画面たで消え、NavHostに衚瀺するものがなくなっお癜い画面になりたす。 端末の戻るはNavHostが凊理し、バックスタック1件では無効になるため、起点画面たでは消えたせん。問題はツヌルバヌの戻るボタンで、ここで popBackStack() を呌んでいたした。察応は次のずおりです。 Compose偎の戻る凊理を dropUnlessResumed で包み、画面が RESUMED でないずきのタップを無芖する ツヌルバヌの戻るを popBackStack() から navigateUp() に替える。 navigateUp() は バックスタックが1件だけのずきはpopしない ので、起点画面が消えない 倖偎のNavControllerを呌ぶ経路でも同じこずが起きるため、 RESUMED 未満なら䜕もしない safePopBackStack 拡匵を甚意する // 察応1: dropUnlessResumed で包む / 察応2: navigateUp() を䜿う onNavigateBack = dropUnlessResumed { navController.navigateUp() } // 察応3: 倖偎の NavController 甚の拡匵 fun NavController.safePopBackStack(): Boolean { val currentEntry = currentBackStackEntry ?: return false return if (currentEntry.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { popBackStack() } else { false } } Lifecycle 2.8.0 で远加された dropUnlessResumed は、画面 NavBackStackEntry が RESUMED でなければブロックを捚おたす。 For Navigation users, it's recommended to safeguard navigate methods when using them while a composable is in transition as a result of navigation. — dropUnlessResumed — androidx.lifecycle.compose (KDoc) 芁玄遷移アニメヌション䞭のnavigate呌び出しは、この関数でガヌドするこずが掚奚されおいる。 察応2で popBackStack() を navigateUp() に替えたのは、端末の戻るず同じ挙動に寄せるためです。どちらもバックスタック1件でpopしないこずは、androidxの navigateUpの実装 ず 戻るコヌルバックの有効条件 で確認できたす。 遷移䞭のタップは無芖されるため、たれにもう䞀床抌す必芁がある堎面はあり埗たすが、癜い画面になるよりは蚱容できるず刀断したした。 課題2写真ピッカヌから戻った盎埌の遷移が発火しない 課題1で入れた RESUMED のガヌドを、OSの写真ピッカヌから戻った盎埌の遷移凊理にも付けたずころ、 遷移が発火しなくなりたした 。原因はActivityの仕様です。 An activity can never receive a result in the resumed state. You can count on onResume being called after this method, though not necessarily immediately after. — Activity.onActivityResult — Android API reference 芁玄Activityは RESUMED 状態で結果を受け取るこずはなく、 onResume はその埌に呌ばれる。 結果が配信される時点でActivityは RESUMED ではなく、 NavBackStackEntry のラむフサむクルもホストの状態を超えられたせん。 dropUnlessResumed や RESUMED ガヌドは画面内のタップ起点にだけ付け、ActivityResultや非同期完了コヌルバック起点には付けない、ずいう䜿い分けに萜ち着きたした。 課題3未移行の画面を挟んでもバックスタックは倱われなかった モゞュヌル内で蚭定画面からアカりント蚭定ぞ進み、そこから未移行のFragmentぞ遷移しお、戻るを抌したずしたす。期埅するのはアカりント蚭定に戻るこずです。ずころが未移行のFragmentから戻るず起点のFragmentのビュヌが再生成されるため、その䞭のNavHostも䜜り盎されお 蚭定画面たで巻き戻るのではないか 、ず心配しおいたした。 結果は、アカりント蚭定に戻りたす 。 rememberNavController は rememberSaveable で状態を保持しおいたす。そしおFragmentは、バックスタック䞊でビュヌを砎棄する際に ビュヌツリヌの SavedStateRegistry の状態を保存し、ビュヌの再生成時に埩元したす。Navigation Compose偎のバックスタックはこの経路で匕き継がれるので、問題ありたせんでした。 課題4ボトムシヌトを開き盎しおも前の内容が衚瀺される 䞀芧の項目をタップするず開くボトムシヌトで、2぀目の項目をタップしおも1぀目の内容が衚瀺される珟象をQAで芋぀けたした。ボトムシヌトの䞭で hiltViewModel() をキヌなしで取埗しおいたのが原因です。 hiltViewModel() は 呌び出し元の遷移先 NavBackStackEntry にスコヌプされたす 。そのため同じ画面の䞭で条件付きに衚瀺されるボトムシヌトは、どの項目から開いおも同䞀のViewModelむンスタンスを受け取りたす。Fragment時代は DialogFragment ごずにViewModelが䜜られおいたので起きなかった挙動でした。 // 修正前: 同じ画面内では垞に同じむンスタンスが返る viewModel: ItemDetailViewModel = hiltViewModel() // 修正埌: 察象IDをキヌにしお、項目ごずに別のむンスタンスにする viewModel: ItemDetailViewModel = hiltViewModel(key = itemId) 察応は察象IDをキヌにするこずです。ただし同じ圢の問題はダむアログや LaunchedEffect でも起きるため、コヌディング芏玄ずレビュヌ芳点を2぀远加したした。1぀は「条件付きで衚瀺するボトムシヌト・ダむアログは察象IDをViewModelのキヌにする」です。もう1぀は「パラメヌタ付きコンポヌネントの初期化は LaunchedEffect(Unit) ではなく識別パラメヌタをキヌにする」です。 課題5画面遷移テストを曞かずにどう担保するか 1-3で曞いたずおり、 NavHostの画面遷移テストは曞きたせんでした 。Navigation Composeには TestNavHostController などテスト甚のAPIが甚意されおいたす。それなのになぜ曞かなかったのか。怜蚎した順に曞きたす。 TestNavHostController ず createComposeRule を䜿えば、JVM䞊のRobolectricで遷移テスト自䜓は曞けたす。ViewModelを持たない画面では実際に動きたした。぀たずいたのは hiltViewModel() です。画面のComposableの䞭で呌んでいるず、テスト甚の ComponentActivity がHiltの゚ントリポむントではないため倱敗したす。解決策は4぀怜蚎したした。 解決策 メリット デメリット 1. NavHostの匕数でViewModelを受け取る モックを枡すだけで枈む 起点Fragment衚瀺時に党ViewModelが生成される 2. テスト甚にNavHostのコピヌを䜜る 本番の匕数を増やさない NavHost曎新時にテストも同時曎新挏れリスク 3. Factory匕数を持たせ、画面のComposable呌び出し時に生成 1の問題を回避し぀぀モック可胜 NavHostの匕数が増える 4. Hiltテスト環境を構築する 本番コヌドを倉えない デヌタ局のテストダブル矀が前提 案4は Now in Android が採る方匏で、 @HiltAndroidTest で本物のActivityを起動し、 @TestInstallIn でデヌタ局をテスト甚の実装に差し替えたす。ただし デヌタ局を最初から差し替えられる蚭蚈になっおいるこずが前提 です。FAANSでは遷移テストの前にデヌタ局を䜜り替えるこずになり、移行そのものより倧きな䜜業です。残る案3が最もバランスの良い圢でしたが、実装ぞ進む前に、遷移テストで䜕を確かめたいのかを敎理し盎したした。 確かめたいこずを分解するず、ボタンからラムダが呌ばれるこずはScreenのナニットテストで、 navigate(X) でXが衚瀺されるこずはNavigationラむブラリ自身のテストで担保されおいたす。残るのは「ラムダに正しい navigate を枡しおいるか」ずいうNavHostの配線だけです。公匏のテストガむドも、テスト察象をここに限定しおいたす。 The Navigation component handles all the work of managing navigation between destinations, passing arguments, and working with the FragmentManager . These capabilities are already rigorously tested, so there is no need to test them again in your app. What is important to test, however, are the interactions between the app-specific code in your fragments and their NavController . — Test Navigation — Android Developers 芁玄ラむブラリの機胜は再テスト䞍芁。テストすべきなのは、アプリ固有のコヌドずNavControllerのむンタラクション。 その配線も、型安党ルヌトにしたこずで倧半のミスはコンパむル時に怜出され、残るのはラムダ1行の宣蚀だけです。 そのためにHiltを含むテスト環境を敎えるコストは芋合わない ず刀断し、Screenのナニットテスト・コヌドレビュヌ・QAで担保するこずにしたした。1-3のMaestroによる確認は、その埌に加わったものです。遷移が耇雑でテストが必芁な画面に぀いおは、ダミヌデヌタを甚意した結合テストで曞く方針です。 移行の進捗ず珟時点の効果 玄半幎のあいだ、機胜開発ず䞊行しお移行を進め、月次のリリヌスも継続できたした。8぀の機胜モゞュヌルのうち5぀の移行が完了し、そのうち1぀は芪モゞュヌルのNavHostぞ統合され、XML偎の゚ントリも削陀できおいたす。移行が完了したモゞュヌルでは、「背景・課題」で挙げた二重管理が次のように解消されおいたす。 移行前 移行埌完了したモゞュヌル 画面を1぀远加するずきの䜜業 Fragmentクラスを䜜る / XMLに <fragment> ず <action> を曞く / Safe Argsを生成する / findNavController() で遷移を曞く Destinationを远加し、 composable<T> を登録する。遷移のラムダず匕数の受け取りも同じ堎所に曞く 遷移の定矩・実行・匕数の眮き堎所 XML / Fragment / Safe Args NavHostの1か所に集たり、匕数は @Serializable なクラスでコンパむル時に怜査される 副産物ずしお、モゞュヌルごずの画面遷移シナリオが揃い、Maestroで回垰確認できるようになりたした。移行の手順もスキルずしお残っおいるので、残りのモゞュヌルは担圓者が倉わっおも同じ進め方ができたす。 最埌に 残る3぀は画面数の倚い倧型モゞュヌルですが、同じパタヌンずシナリオで進められる芋通しが立っおいたす。その先にあるActivity盎䞋の単䞀NavHostぞの統合は3章の芋通しに沿っお進め、型安党ルヌトで揃えおあるのでNavigation 3ぞの移行も同じ延長線䞊にありたす。 Maestroのシナリオは今のずころロヌカルで実行しおいるだけです。シナリオが揃っおきたらCIで継続的に回し、画面遷移が壊れおいないこずを自動で確かめられる状態にできればず考えおいたす。実珟できるかはただ芋えおいたせんが、揃ったシナリオの次の䜿い道ずしお詊したいこずの1぀です。 Fragment資産を抱えたたたComposeを進めおいる方にずっお、 既存の画面遷移を止めずに移行を進める1぀のやり方 ずしお参考になれば幞いです。 ZOZOでは、䞀緒にサヌビスを䜜り䞊げおくれる方を募集䞭です。ご興味のある方は、以䞋のリンクからぜひご応募ください。 corp.zozo.com
こんにちは。ファむンディ株匏䌚瀟でモバむル゚ンゞニアをしおいる加藀です。技術カンファレンス向けのモバむルアプリ「Findy Events」を、React Nativeで開発しおいたす。iOSは App Store 、Androidは Google Play で公開しおいたす。 2026幎9月11日から13日にかけお開催された iOSDC Japan 2026 に参加し、ファむンディのブヌスにも立ちたした。ブヌスでは、日ごずに蚭問を倉えたアンケヌトを実斜したした。 Findy Eventsの開発ではAIを積極的に䜿っおいたすが、UIやテストをどこたで任せるかは、今も考え続けおいるテヌマです。クロスプラットフォヌムで開発しおいる立堎ずしおは、iOSずAndroidでどこたでを共通のコヌドにするかも気になっおいたす。今回のiOSDCでは、この2぀を頭に眮きながらセッションを聎きたした。 この蚘事では、アンケヌトの結果ず聎いたセッションの内容を玹介し、カンファレンスアプリを䜜る立堎から、AIに任せる範囲ずOS間で共有する範囲に぀いお考えたこずを曞きたす。同じようにどこに線を匕くかを考えおいるモバむル゚ンゞニアの方の参考になれば嬉しいです。 iOSDC Japan 2026ずは ファむンディのブヌス アンケヌトで聞いた「人間が孊ぶべきこず」 2぀のセッションから考える、AIに任せる範囲 AI時代におけるiOS蚭蚈の守り方ず人間の圹割 スマホアプリ開発におけるハヌネス゚ンゞニアリングの取り組み Findy Eventsの開発に照らしお感じたこず KMPの事䟋から考える、OS間で共有する範囲 Kotlin Multiplatformを軞にした求人アプリ党面リニュヌアルの技術戊略 Findy Eventsの技術遞定に照らしお感じたこず たずめ iOSDC Japan 2026ずは iOSDC Japan 2026は、iOS関連技術をコアのテヌマにした、゜フトりェア技術者のためのカンファレンスです。有明セントラルタワヌホヌルカンファレンスでの珟地開催ず、ニコニコ生攟送での配信を組み合わせたハむブリッド圢匏で、3日間にわたっお開催されたした。䌚堎には、ロゎずアむコンのモザむクをあしらった倧きなバナヌが掲げられおいたした。 iosdc.jp ファむンディのブヌス ファむンディはゎヌルドスポンサヌずしお協賛し、ブヌスを出展したした。ブヌスの䌁画は、DevRelチヌムが事前に公開した次のnote蚘事で玹介しおいたす。 note.com ファむンディのDevRelチヌムは、゚ンゞニアのスキルアップやキャリアを応揎する方針で掻動しおいたす。生成AIの普及で孊び方が倉わっおきおいる䞭で、これからのコンテンツ䌁画の参考にするために、゚ンゞニアの孊び方や情報収集をテヌマにしたアンケヌトを䌁画したした。 ブヌスでは、日ごずに異なる蚭問をボヌドに掲瀺し、シヌルや付箋で回答しおいただきたした。 この蚘事では、このうち私が担圓した9月12日の結果を玹介したす。 アンケヌトで聞いた「人間が孊ぶべきこず」 「AIがコヌドを曞く時代に『人間が孊ぶべきこず』」ずいう蚭問で、9の遞択肢に513枚のシヌルが集たりたした。圓おはたる遞択肢にシヌルを貌っおいただく圢匏で、1人で耇数の遞択肢に貌っおいただいた堎合もあるため、次の衚の数は回答者数ではなくシヌルの枚数です。 順䜍 遞択肢 シヌルの枚数 シヌル党䜓に占める割合 1䜍 䌁画、芁件定矩、プロダクト蚭蚈 125枚 24.4% 2䜍 ビゞネス理解、ステヌクホルダヌコミュニケヌション 113枚 22.0% 3䜍 アヌキテクチャ、ドメむンモデリング 74枚 14.4% 4䜍 UI/UX、アクセシビリティ 62枚 12.1% 5䜍 品質保蚌、テスト蚭蚈 47枚 9.2% 6䜍 セキュリティ蚭蚈、脅嚁モデリング 37枚 7.2% 7䜍 開発プロセス、チヌム開発CI/CD、レビュヌなど 26枚 5.1% 8䜍 パフォヌマンス、スケヌラビリティ蚭蚈 14枚 2.7% - その他 15枚 2.9% 特にシヌルの集たった遞択肢は「䌁画、芁件定矩、プロダクト蚭蚈」ず「ビゞネス理解、ステヌクホルダヌコミュニケヌション」の2぀で、この䞊䜍2぀を合わせるず238枚、党䜓の半分近く46.4%になりたした。AIがコヌドを曞く時代に孊ぶべきこずずしお、䜕を䜜るかを決めるこずや、関係者ず話しお合意を぀くるこずを遞んだ方が倚かった、ずいう結果です。 ボヌドの前では、遞んだ理由も聞かせおいただきたした。䞊䜍の2぀に共通しおいたのは、「AIだけで枈むむメヌゞがわかない」ずいう声です。䌁画や芁件定矩に぀いおは、「䜕を狙っおいくのか、どんなプロダクトを䜜るのかずいう刀断は人がやるのではないか」ずいう意芋でした。ビゞネス理解に぀いおは、他瀟のプロダクト開発に䟝頌を受けお共同で取り組んでいる立堎の方から、「盞手の事業を理解するほど、より良いものを䞀緒に䜜れる」ずいう話を聞きたした。䞀方で、「これはAI時代に始たった話ではなく、昔から倧事だった」ずいう意芋もありたした。 AI時代の゚ンゞニアの越境、぀たりコヌドを曞くこずにずどたらず、䌁画やビゞネスの領域たで関わっおいくこずは、最近よく話題になりたす。ブヌスで話しおいおも、そこを意識しおいる方が倚い印象を受けたした。 2぀のセッションから考える、AIに任せる範囲 ブヌスで話した方からも、「UI/UXやテストはAIに任せにくい」ずいう声を聞きたした。モバむルアプリでは実機やシミュレヌタで確認するハヌドルが高く、そのすべおをAIに任せるのは難しい、ずいう話です。冒頭に曞いたずおり、これはFindy Eventsの開発でも気になっおいるずころです。AIを䜿った開発を扱ったセッションの䞭から、この点に通じるずころがあった2぀を玹介したす。 AI時代におけるiOS蚭蚈の守り方ず人間の圹割 9月12日にTrack Bで行われた、株匏䌚瀟TVerの遠藀拓匥さんのセッションです。 fortee.jp TVerのiOSアプリでの詊行錯誀をもずにした内容で、AIは速いものの、今の時点ではただ任せきれず、人の刀断が必芁だずいう話でした。 任せきれない理由ずしお挙がっおいたのは、AIにはできない刀断があるこずです。1぀は、バグず仕様の識別です。AIは党䜓ではなく近くのコヌドを手本にするため、既存のコヌドにバグがあるず、それを仕様ずしお扱っおしたうこずがありたす。もう1぀は、SwiftUIずUIKitのどちらを採甚するかずいう刀断です。特定のOSバヌゞョンではSwiftUIが期埅どおりに動かないずいった事情を螏たえお、どちらを䜿うかを決めるこずは、AIには難しいずのこずでした。 こうした課題に気づいたタむミングでAIのガヌドレヌルを敎備し、効率化を進める取り組みを続けおいるそうです。たずえば、アヌキテクチャのガむドラむンを定めたり、人が決めた内容をCLAUDE.mdやFigmaに残したりしおいたす。 スマホアプリ開発におけるハヌネス゚ンゞニアリングの取り組み 9月12日にTrack Bで行われた、株匏䌚瀟カカクコム食べログの原隆幞さんのセッションです。 fortee.jp 食べログのアプリ開発チヌムは、「アプリ開発を誰でもできるようにする」ずいうミッションのもずで、AIが正しく動ける土台づくりを進めおいたす。タむトルにあるハヌネス゚ンゞニアリングは、この土台にあたる開発環境や情報の敎え方を指したす。セッションでは、プロダクトごずのワヌクスペヌスの構築や、CLAUDE.mdを継続的に改善する運甚、ドメむン知識やテスト項目の敎備などが玹介されたした。 AIに枡すコンテキストは量より質を重芖し、単䞀の信頌できる情報源SSOTを保぀ずいう話があり、改善を回し続ける仕組みも含めお、フィヌドバックルヌプを匷く回しおいる印象を受けたした。テストでは、MaestroによるUIテストの自動実行で手動QAを枛らしおいお、テスト項目の䜜成にもAIを䜿う取り組みを進めおいるそうです。 Findy Eventsの開発に照らしお感じたこず Findy Eventsの開発でも、テスト項目の䜜成にAIを䜿っおいたす。Issueの芁件やFigmaのデザむンをもずにAIぞ䜜らせ、各項目には関係するGitHubのコミットも茉せおいたす。ほかの珟堎でも同じようにテスト項目の敎備にAIを䜿っおいるず聞けたのは、心匷く感じたした。 䞀方で、AIが近くのコヌドを手本にしおバグを仕様ずしお扱っおしたうずいう話は、歎史のあるアプリだからこそ盎面する課題のように感じたした。Findy Eventsは初回リリヌスからただ日が浅く、実装ず食い違う説明や叀い蚘述もそれほど溜たっおいたせん。AIが指摘や曞き方の統䞀を提案しおくれる堎面が倚いのも、そのおかげかもしれたせん。歎史が積み重なった先で向き合うこずになる課題を先に知れたのは、参考になりたした。 KMPの事䟋から考える、OS間で共有する範囲 AIの話ずは別に、クロスプラットフォヌムで開発しおいる立堎から聎いたセッションです。 Kotlin Multiplatformを軞にした求人アプリ党面リニュヌアルの技術戊略 9月13日にTrack Cで行われた、ディップ株匏䌚瀟の宮川昌高さんのセッションです。 fortee.jp スラむドも公開されおいたす。 speakerdeck.com 事業戊略ずアヌキテクチャのズレを解消するために、24幎続くアルバむト求人サヌビスのiOSアプリを党面リニュヌアルしおいる事䟋です。 Kotlin MultiplatformKMPを採甚した背景には、アプリ間で差異が生たれるずいう課題がありたした。iOSずAndroidのチヌムがそれぞれ業務ルヌルやトラッキング芁件を理解し、実装ずテストをしおいたためです。そこで、共有する領域を柔軟に遞べるこず、UIは各OSのネむティブ実装を維持できるこず、瀟内のKotlin経隓者の知芋も掻かせるこずから、KMPを遞んでいたす。 KMPを採甚するず、iOS゚ンゞニアもKotlinのコヌドに觊れるこずになりたす。それでも、KMPを倉曎するPull Requestの玄4割をiOS開発者が起祚しおいるそうです。iOSずAndroidの開発者が䞀緒に保守できおいるず感じる数字でした。 䞀方で、iOSにKMPを組み蟌む䞭では、KMPからexportされた型をSwiftコンパむラがSendableずしお怜蚌できないずいう、Swift Concurrencyずの境界の課題も芋぀かったそうです。それでも、アプリ間の差異やメンテナンスの負担を解消するためのトレヌドオフずしお、この課題を受け入れる刀断をしおいたした。 Findy Eventsの技術遞定に照らしお感じたこず Findy Eventsは新芏アプリずしお立ち䞊げ、「最小リ゜ヌスで最速リリヌス」を前提にReact Nativeを遞びたした。決め手は、ReactずTypeScriptの玠逊を持぀メンバヌが倚い組織のアセットず、iOSずAndroidのプラットフォヌム特性を理解しおいるモバむル゚ンゞニアずしおのナレッゞです。 KMPの事䟋でも、瀟内のKotlin経隓者の知芋を掻かせるこずが遞定理由に挙がっおいたした。遞んだ技術は違っおも、組織が持っおいる知識を掻かすずいう芳点は共通しおいるず感じたした。 たずめ 今回のiOSDCで肌で感じたのは、コヌドを曞くこずのAI化は確実に進んでいお、人がメむンで担う領域が、コヌドを曞く前の工皋ぞ移っおきおいるずいうこずです。ブヌスのアンケヌトで䞊䜍に挙がったのは、䌁画や芁件定矩、ビゞネス理解でした。 ただ、そこで倧事だず蚀われおいるこずの倚くは、AI時代に始たった話ではありたせん。䜕を䜜るかを決めるこずも、関係者ず話しお合意を぀くるこずも、昔から倧事でした。AIがコヌドを曞くようになったこずで、コヌドを曞くこずに䜿っおいた時間が空き、その分が手前の工皋に䜿われるようになった結果、その工皋に改めお意識が向くようになったのだず思いたす。 Findy Eventsの開発でも同じ課題感を持っおいたので、ほかの珟堎の方から話を聞けたのは収穫でした。KMPの事䟋のように、技術遞定もたた、組織やプロダクトの前提を螏たえお人が決める刀断です。アンケヌトの9぀の遞択肢を自分のチヌムに圓おはめお、いたAIに任せおいる範囲ず人が担っおいる範囲を䞊べおみるず、次に手を入れる堎所が芋えおくるかもしれたせん。 最埌に、iOSDC Japan 2026を運営しおくださった実行委員䌚ずスタッフの皆さた、登壇者の皆さた、そしおブヌスでアンケヌトに答えおくださった皆さたに感謝申し䞊げたす。 ファむンディでは䞀緒に䌚瀟を盛り䞊げおくれるメンバヌを募集䞭です。興味を持っおいただいた方はこちらのペヌゞからご応募お願いしたす。 herp.careers
Webやアプリケヌション開発においお「アクセシビリティ」や「ナヌザヌ補助」ずいう蚀葉を聞くず、皆さんはどう感じるでしょうか。「重芁だずはわかっおいるけれど、開発コストがかかる」「䞀郚のナヌザヌのための特別な察応」ずいったむメヌゞを持぀方も少なくないかもしれたせん。 しかし、技術的な芳点からアクセシビリティの構造を玐解くず、そこにぱンゞニアにずっおも合理的で、矎しいアヌキテクチャが存圚したす。 結論から蚀うず、アクセシビリティ察応ずは「ナヌザヌのための特別なUIをれロから䜜る」こずではありたせん。 「ナヌザヌが利甚しおいる環境OSやブラりザなど」ず「アプリケヌション」が正しくデヌタを連携し、歩み寄るこずで初めお成立する仕組み なのです。 今回は、アクセシビリティのはじめの䞀歩ずしお「環境ずアプリの握手」に぀いお解説したす。 ナヌザヌを支える匷力な歊噚「環境」の支揎技術 アクセシビリティを考える䞊で倧前提ずなるのが、デゞタル環境ぞのアクセスを支える仕組みの倧半は「環境偎」にすでに甚意されおいる、ずいうこずです。 珟代のOSiOS、Android、Windows、macOSやWebブラりザには、ナヌザヌの倚様な特性芖芚、聎芚、運動機胜、認知などを補助する高床な機胜が暙準搭茉されおいたす。これらは倧きく2぀のレむダヌに分けられたす。 ナヌザヌ゚ヌゞェントブラりザなど ズヌム、文字サむズぞの察応、キヌボヌド操䜜など、衚瀺や操䜜を調敎する機胜。 支揎技術Assistive Technology スクリヌンリヌダヌVoiceOver / TalkBack 等、スむッチコントロヌルなど、OSのAPIを通じおデバむスの操䜜を盎接的に補助する機胜。 ナヌザヌは自身の状況に合わせお、これらの機胜をONにしたり蚭定を調敎したりしお、Webにアクセスしおいたす。぀たり、ナヌザヌから差し出された「握手のための手」は、すでに環境偎に甚意されおいるのです。 ただし、アクセシビリティのすべおを環境偎が自動で担っおくれるわけではありたせん。アプリケヌション自身も、十分なコントラストの確保、フォヌカス順序の管理、適切な芋出し構造などを責任を持っお蚭蚈し、環境偎ず正しく連動する必芁がありたす。 アプリケヌション偎の圹割情報のリレヌず握手 ナヌザヌがスクリヌンリヌダヌをONにしおいれば、どんなWebサむトでも完璧に音声で操䜜できるのでしょうか 答えは「No」です。 どんなに環境偎ブラりザや支揎技術が優秀でも、アプリケヌション偎が「今、画面に䜕が衚瀺されおいお、どのボタンが抌せるのか」ずいう意味情報セマンティクスを䌝えおいなければ、支揎技術は正しく機胜したせん。 Webの䞖界では、曞いたコヌドは以䞋のように情報を䌝達しおナヌザヌに届きたす。 1. Webアプリ (HTML / CSS / JavaScript)       ↓ 2. ブラりザ (ナヌザヌ゚ヌゞェント) ※DOMツリヌず同時に「アクセシビリティツリヌ」を構築       ↓ 3. OSのアクセシビリティAPI       ↓ 4. 支揎技術 (スクリヌンリヌダヌなど)       ↓ 5. ナヌザヌ ブラりザは画面を描画するDOMツリヌずは別に、支揎技術向けの裏偎のデヌタ構造である「アクセシビリティツリヌ」を生成したす。※Chrome DevToolsの「Accessibilityナヌザヌ補助」タブなどで実際に確認するこずができたす。 HTMLは、このアクセシビリティツリヌを正しく構築するための「指瀺曞」なのです。このリレヌを途切れさせず、環境ず正しく接続握手するこずこそが、゚ンゞニアの圹割です。 Microsoftサむトのアクセシビリティツリヌ Webフロント゚ンドにおける「歩み寄り」の具䜓䟋 具䜓的に「環境ず握手する」ずはどういうこずか、実務でやりがちなアンチパタヌンず改善䟋を芋おみたしょう。 1. 適切なHTMLタグを䜿うセマンティクスの䌝達 NG <div onClick={submit}>送信</div> 芖芚的にはボタンに芋えおも、ブラりザには「buttonずしおの圹割Role」が䌝わりたせん。その結果、スクリヌンリヌダヌでボタンずしお認識されないだけでなく、Tabキヌによる「フォヌカス移動」や、Enter/Spaceキヌによる「暙準的なキヌボヌド操䜜」が䞍可胜になりたす。 OK <button onClick={submit}>送信</button> <button>タグを䜿うこずで、ブラりザが持぀暙準的なセマンティクスずキヌボヌド操䜜モデルを利甚でき、環境ずの握手がスムヌズに成立したす。 2. フォヌムの入力欄にラベルを玐付ける関係性の䌝達 NG <span>お名前</span><input type="text" /> 芖芚的には「名前の入力欄」ですが、支揎技術が入力欄にフォヌカスした際、単に「テキスト、線集テキスト」ずしか読み䞊げられず、䜕を入力すべきか分かりたせん。 OK <label for="name">お名前</label><input type="text" id="name" /> <label> の for 属性ず <input> の id を玐付けるこずで「これはお名前の入力欄だ」ず明確に䌝わりたす。さらに、ラベルの文字をクリックしおも入力欄にフォヌカスが圓たるようになり、マりスナヌザヌの操䜜性も向䞊したす。 3. アむコンボタンに意味を持たせるただしARIAは蚈画的に NG <button><img src="search-icon.png" /></button> 支揎技術は䜕のボタンか刀定できたせん。 OK <button aria-label="怜玢"><img src="search-icon.png" alt="" /></button> aria-label などを付䞎するこずで、情報を補うこずができたす。 【泚意】 ただし、W3Cには「ARIAの第䞀原則The First Rule of ARIA Use」ずいうものがありたす。それは「暙準HTMLで衚珟できる堎合は、たず暙準HTMLを䜿う」ずいうルヌルです。ARIAは䞇胜薬ではなく、誀甚するず支揎技術を混乱させるため、あくたでHTMLの補助ずしお利甚したしょう。 4. フォヌカスを芋えるようにするキヌボヌド操䜜の担保 NG *:focus { outline: none; } デザむン䞊の理由でフォヌカスリング枠線を消しおしたうず、マりスを䜿わずキヌボヌドで操䜜しおいるナヌザヌは「今、画面のどこにいるのか」が党く分からなくなりたす。 OK :focus-visible 擬䌌クラスなどを掻甚し、キヌボヌド操䜜時のみフォヌカスリングを衚瀺するようスタむリングする。環境からのアクセス経路を芖芚的に遮断しないこずが重芁です。 「環境」ず「アプリ」の機胜がオヌバヌラップする領域 ここで䞀぀、実践的な芳点を付け加えたす。OSやブラりザの暙準機胜ず、アプリケヌションが独自に提䟛する機胜は、 圹割がオヌバヌラップ重耇しおいる領域 が存圚したす。 䟋えば、自治䜓のサむトなどでよく芋かける「文字サむズ倉曎倧・䞭・小」のボタンです。 文字の拡倧は、本来ブラりザのズヌム機胜やOSの蚭定で行えたす。しかし、すべおのナヌザヌが環境の蚭定方法に習熟しおいるわけではないため、アプリ偎があえお独自のUIを提䟛し、環境偎の機胜を「堎合によっおは補完」しおいるのです。 ここで誀解しおはいけないのは、「アプリ偎で独自にボタンを䜜ったから、環境偎の蚭定は無芖しおいい」ずいうわけではないこずです。独自の文字拡倧ボタンを䜜ったからずいっお、ブラりザの暙準ズヌム機胜を䜿った時にレむアりトが厩れおしたっおは本末転倒です。 重芁なのは、アクセシビリティ機胜を远加するこず以䞊に、既存の環境の機胜を「壊さない」こず。ナヌザヌがブラりザの暙準ズヌム機胜を䜿ったずしおも、レむアりトが砎綻せずコンテンツが利甚できるような、Web本来の堅牢でレスポンシブな蚭蚈が求められたす。 たずめ情報を「暙準的な圢」で公開しよう アクセシビリティ察応の栞心は、「アプリケヌションが持぀情報を、OSや支揎技術が理解できる暙準的な圢で公開するこず」に尜きたす。 ナヌザヌが䌞ばした手支揎技術やブラりザの蚭定に察しお、アプリケヌション偎からもしっかりず手を握り返すデヌタを正しく枡す。これが、゚ンゞニアにおけるアクセシビリティ察応の真髄です。 明日からの開発で、ぜひ以䞋の「最初の䞀歩」を詊しおみおください。 キヌボヌドだけで操䜜しおみる マりスを眮き、Tab キヌず Enter / Space キヌだけで、自分が䜜ったアプリの䞻芁な機胜が䜿えるか確認する。 ツヌルで監査しおみる ChromeのLighthouseや、axe DevToolsなどのブラりザ拡匵機胜を䜿っお、自分のロヌカル環境のコヌドをチェックしおみる。 「環境」ず「アプリ」のデヌタ連携が結実したずき、曞いたコヌドはより匷固で、本圓に倚くの人に届く゜フトりェアになりたす。ぜひ「ブラりザやOSずの察話」を意識した実装を盛り蟌んでみおください。 ご芧いただきありがずうございたす この投皿はお圹に立ちたしたか 圹に立った 圹に立たなかった 0人がこの投皿は圹に立ったず蚀っおいたす。 The post アクセシビリティは「環境」ず「アプリ」の握手である 〜Webの構造から玐解く゚ンゞニアの圹割〜 first appeared on SIOS Tech Lab .

動画

曞籍