TECH PLAY

KINTOテクノロジーズ

KINTOテクノロジーズ の技術ブログ

969

この記事は KINTOテクノロジーズアドベントカレンダー2024 の18日目の記事です🎅🎄 はじめまして。KINTOテクノロジーズのAndroidエンジニア 山田 剛 です。 本記事では、Android View で構築されたアプリを Jetpack Compose を用いたUI記述へ移行させる、もしくは、Android View ベースのUIに徐々に Jetpack Compose ベースのUIを導入する際の第一歩となるような、ちょっとしたテクニックの一部を紹介します。 1. はじめに 宣言的UI のAndroid版として Jetpack Compose の開発開始が数年前にアナウンスされ、2021年7月にバージョン 1.0 がリリースされました。 多くのAndroid開発者が、その以前からJavaに続きAndroid開発の公式サポート言語となっていた Kotlin の柔軟性・拡張性を活用した Jetpack Compose のコンセプトを受け入れ、以降のAndroidアプリ開発において Jetpack Compose の採用が徐々に拡がっているようです。 宣言的UIの導入によって、従来のViewベースのUIと比べて少ないコード量で直感的な記述が可能になり、開発効率/生産性が向上すると考えられています。 最近になって Compose Multiplatform がリリースされ、Android 以外にも Jetpack Compose のスキルを活用できる領域が拡がってきました。 今後は魅力的なライブラリが Jetpack Compose のみに対応される、といったことも増えてくるかもしれません。 その魅力を既存アプリにも取り入れるべく、弊社でも当初 Android View ベースで構築されていたアプリのUIを Jetpack Compose ベースのUIに移行する取り組みを一部の開発プロジェクトで進めています。 しかし従来のViewベースのUIは 手続き的 な要素が多く、Jetpack Compose の 宣言的 なスタイルの導入は簡単というわけにはいきません。 この記事では、Jetpack Compose によって簡潔になったことで削ぎ落とされた、あるいは見えにくくなった要素を補完するためのちょっとしたテクニック、とりわけ Composable の位置やデバッグ情報の確認、 View と Composable の相互運用などについて紹介し、ViewベースからJetpack ComposeベースのUIにできるだけスムーズに移行するための一助となることを目指します。 2. Composable の位置合わせとデバッグ View の表現を今までになかった Composable に替えるという作業をするにあたって、同じ表示内容をどのように表すのか、という課題と同等以上に、正しい位置に配置できるのだろうか、という点が、 Android View に馴染んだ筆者にとっても大きな不安要素でした。まずは、 Composable の表示位置を View と同じに揃えること、そのために必要なデバッグのための情報を取得・確認する方法を紹介します。 2.1. Composable と View の位置を確かめる Android API level 1 から提供されている View は、Java のオブジェクト指向の考え方に基づき、矩形の画面要素としての表現のほか、View 同士の包含関係や相互作用、サブクラスによる機能拡張などもうまく表現しています。 たとえば、単一の View や ViewGroup が内包した多くの View の位置をログに出力することは、以下のようなコードでできます: private fun Resources.findResourceName(@IdRes resId: Int): String = try { getResourceName(resId) } catch (e: Resources.NotFoundException) { "?" } fun View.logCoordinators(logger: (String) -> Unit, res: Resources, outLocation: IntArray, rect: Rect, prefix: String, density: Float) { getLocationInWindow(outLocation) rect.set(outLocation[0], outLocation[1], outLocation[0] + width, outLocation[1] + height) var log = "$prefix${this::class.simpleName}(${res.findResourceName(id)})${rect.toShortString()}(${rect.width().toFloat() / density}dp, ${rect.height().toFloat() / density}dp)" if (this is TextView) { log += "{${if (text.length <= 10) text else text.substring(0, 7) + "..."}}" } logger(log) if (this is ViewGroup) { val nextPrefix = "$prefix " repeat(childCount) { getChildAt(it).logCoordinators(logger, res, outLocation, rect, nextPrefix, density) } } } fun View.logCoordinators(logger: (String) -> Unit = { Log.d("ViewLogUtil", it) }) = logCoordinators(logger, resources, IntArray(2), Rect(), "", resources.displayMetrics.density) ここで View$getLocationInWindow(IntArray) は、View が属する Activity のウィンドウ上での左上の座標を取得する関数です。Android View に慣れている開発者なら、このようなコードで動作を確認するのは簡単でしょう。 一点 Activity$onResume() などから直接これらの関数を呼び出してもまだ View の配置は完了しておらず意味のある情報が取得できないため、多くの場合は View$addOnLayoutChangeListener(OnLayoutChangeListener) などのコールバックから呼び出す必要があることに注意が必要です。 同じようなことを Jetpack Compose ではどうするかよく分からず、 Composable で同じように動作しているか確かめにくく移行作業がおっくう、という開発者も少なくないのではないでしょうか。 Jetpack Compose では、以下のように Modifier の拡張関数 onGloballyPositioned((LayoutCoordinator) -> Unit) を使って Composable の配置を取得できます: @Composable fun BoundsInWindowExample() { Text( modifier = Modifier .padding(16.dp) .background(Color.White) .onGloballyPositioned { Log.d("ComposeLog", "Target boundsInWindow: ${it.boundsInWindow()}") }, text = "Hello, World!", style = TextStyle( color = MaterialTheme.colorScheme.onSecondary, fontSize = 24.sp ) ) } onGloballyPositioned(...) の引数にコールバックを与えることで、Composable の位置が更新されるたびにコールバックで位置の座標を取得できます。 ここで LayoutCoordinator.boundsInWindow() は Composable の矩形の上下左右を Activity 座標系で取得する拡張関数です。 ひとつの Composable に内包されるすべての Composable の位置を一気に取得するのは今のところ難しそうですが、その代わりに単一の Composable の位置は簡単に取得できます。 しかも多くの場合、Activity などの複雑なライフサイクルを気にせずに位置情報を取得できそうです。 onGloballyPositioned(...) に似たコールバックとして onPositioned(...) もあり、こちらは親となる Composable の内部での相対的な位置が決定された後にコールバックが呼ばれるようです。 boundsInWindow() のほかにも boundsInRoot() 、 boundsInParent() などがあり、場合に応じて使い分けることができますが、今回は割愛します。 2.2. 画面表示確認用Composableを作ってみる Composable でも View$getLocationInWindow(IntArray) と互換性のある boundsInWindow() で位置を取得する方法がわかったので、テスト動作させているときの位置の動きをログに出力して動作を確かめながら Composable を作り込んでいけば、徐々に compose に慣れながら View と同じものを作れそうです。 それは正しく、お手軽な方法なのですが、 LogCat は大量の文字が流れていくので、特に情報量が多いときには見づらいことも多々あります。 そこで、画面表示確認用の Composable を別に作ってみることを考えます。 テストのときに限ってアプリの一部にデバッグ用表示領域を作って、常に更新させるようにすれば、流れていく大量の LogCat の中から決定的なログを必死に探す必要もなくなります。 …そんなことは Android View の時代からわかっていたのだがそれを作り込むのがおっくうで…という嘆息が聞こえてきそうですね。 宣言的UI たる Jetpack Compose では、そのようなデバッグ用表示領域を少ない手間で作りやすくなっています: class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK != Configuration.UI_MODE_NIGHT_YES setContentView(R.layout.activity_main) findViewById<WebView>(R.id.webView).let { webView -> webView.loadUrl("https://blog.kinto-technologies.com/") } val targetRect = mutableStateOf(Rect.Zero) // androidx.compose.ui.geometry.Rect findViewById<ComposeView>(R.id.composeTargetContainer).let { containerComposeView -> containerComposeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) containerComposeView.setContent { KtcAdventCalendar2024Theme { ScrollComposable(targetRect) } } } val visibleRect = mutableStateOf(Rect.Zero) val outLocation = IntArray(2) findViewById<View>(R.id.layoutMain).addOnLayoutChangeListener { v, left, top, right, bottom, _, _, _, _ -> v.getLocationInWindow(outLocation) visibleRect.value = Rect(outLocation[0].toFloat(), outLocation[1].toFloat(), outLocation[0].toFloat() + (right - left), outLocation[1].toFloat() + (bottom - top)) } findViewById<ComposeView>(R.id.composeTargetWatcher).let { watcherComposeView -> watcherComposeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) watcherComposeView.setContent { KtcAdventCalendar2024Theme { TargetWatcher(visibleRect.value, targetRect.value) } } } } } サンプルコードで作っているアプリは Jetpack Compose 化作業の途中で、まだ Activity$setContentView(Int) で View を使っており、その View の中で ComposeView を使って Composable を導入しようとしています。 ここでは mutableStateOf(...) で View および Composable の矩形の位置の情報を共有し、画面上での振る舞いを確かめようとしています。 画面構成は下のようになります。HorizontalScrollView の部分を Composable化しようとしており、そのために画面下部をデバッグ情報の表示に利用します: ![画面構成](/assets/blog/authors/tsuyoshi_yamada/advent-calendar_sample_screen-area.png =252x) MainActivity のレイアウトXMLファイルは以下のようになっています: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:layout_marginVertical="48dp"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/layoutMain" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <WebView android:id="@+id/webView" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginHorizontal="16dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/scrollView" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:ignore="NestedWeights" /> <HorizontalScrollView android:id="@+id/scrollView" android:layout_width="0dp" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/webView" app:layout_constraintTop_toTopOf="parent"> <androidx.compose.ui.platform.ComposeView android:id="@+id/composeTargetContainer" android:layout_width="wrap_content" android:layout_height="match_parent" /> </HorizontalScrollView> </androidx.constraintlayout.widget.ConstraintLayout> <androidx.compose.ui.platform.ComposeView android:id="@+id/composeTargetWatcher" android:layout_width="match_parent" android:layout_height="300dp" android:paddingTop="16dp" /> </LinearLayout> MainActivity.kt と activity_main.xml は Jetpack Compose 化作業の途中で、View と Composable が混在しています。 WebView などは、本記事執筆時点ではまだ Composable が提供されていないため、現時点では混在させながらうまく運用する実装も必要です。[^1] activity_main.xml において、 HorizontalScrollView の内部に含まれる ComposeView は既存の View を Composable で置き換えようとするもの、もう一方の ComposeView は上の Composable の位置を監視するために設置したものです。 View にとって代わる Composable は以下のようになっており、 "Target" と表示される部分の配置をチェックするべく、 onGloballyPositioned(...) を実装しています。 @Composable fun ScrollComposable(targetRect: MutableState<Rect>) { val textStyle = TextStyle( textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSecondary, fontSize = 24.sp, fontWeight = FontWeight.W600 ) Row( modifier = Modifier .fillMaxSize(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(Color.Red), contentAlignment = Alignment.Center ) { Text("1", style = textStyle) } Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(Color.Magenta), contentAlignment = Alignment.Center ) { Text("2", style = textStyle) } Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(MaterialTheme.colorScheme.primary) .onGloballyPositioned { targetRect.value = it.boundsInWindow() }, contentAlignment = Alignment.Center ) { Text("Target", style = textStyle) } Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(Color.Cyan), contentAlignment = Alignment.Center ) { Text("4", style = textStyle) } } } これを監視する Composable が以下です。ここではこの Composable 自身も onGloballyPositioned(...) を使って、自身のサイズを取得しています。 @Composable fun TargetWatcher(visibleRect: Rect, targetRect: Rect) { if (visibleRect.width <= 0f || visibleRect.height <= 0f) return val rootAspectRatio = visibleRect.width / visibleRect.height val density = LocalDensity.current // For calculating toDp() val targetColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.25F) var size by remember { mutableStateOf(IntSize.Zero) } Box( modifier = Modifier .fillMaxSize() .onGloballyPositioned { coordinates -> size = coordinates.size } ) { if (size.width <= 0F || size.height <= 0F) return@Box val watchAspectRatio = size.width.toFloat() / size.height val (paddingH: Float, paddingV: Float) = if (rootAspectRatio < watchAspectRatio) { (size.width - size.height * rootAspectRatio) / 2 to 0F } else { 0F to (size.height - size.width / rootAspectRatio) / 2 } with(density) { Box( modifier = Modifier .padding(horizontal = paddingH.toDp(), vertical = paddingV.toDp()) .fillMaxSize() .background(Color.Gray) ) } if (targetRect.width <= 0f || targetRect.height <= 0f) return@Box with(density) { Box( modifier = Modifier .padding( // Caution: exception is thrown if padding is negative start = max( 0F, marginOf( size.width, paddingH, visibleRect.left, visibleRect.right, targetRect.left ) ).toDp(), end = max( 0F, size.width - marginOf( size.width, paddingH, visibleRect.left, visibleRect.right, targetRect.right ) ).toDp(), top = max( 0F, marginOf( size.height, paddingV, visibleRect.top, visibleRect.bottom, targetRect.top ) ).toDp(), bottom = max( 0F, size.height - marginOf( size.height, paddingV, visibleRect.top, visibleRect.bottom, targetRect.bottom ) ).toDp() ) .fillMaxSize() .background(targetColor) ) } } } private fun marginOf( sizePx: Int, halfPx: Float, visibleFrom: Float, visibleTo: Float, px: Float ): Float { val alpha = (px - visibleFrom) / (visibleTo - visibleFrom) return (1 - alpha) * halfPx + alpha * (sizePx - halfPx) } このComposable関数で、Activityの画面と "Target" と表示される Composable の位置関係を図示しています。 padding の設定が少しわずらわしいですが、難しい算術ではありません。 Composable は形式的には関数ですが、 MutableState や remember を用いて状態を継続的に保持することでUIの表現を成り立たせています。同時に、継続的な情報が変更されたとき Composable の再描画 (recomposition) が行われるようになっており、わずらわしいイベント処理の記述を減らすことができ、 宣言的 なコーディングを可能にしています。 画面右上部分の HorizontalScrollView を左右にスクロールすると、画面下の Composable がスクロールに追従して位置を表示してくれます。ScrollView の内部に存在する View や Composable の位置を View$getLocationInWindow(IntArray) や LayoutCoordinator.boundsInWindow() で取得すると画面外の座標も得られますので、画面の内外を正常に行き来できるかどうかを確かめられます(Composable で Modifier.horizontalScroll(...) や  Modifier.verticalScroll(...) などを用いてスクロールさせる場合はこの限りではありません)。 従来からの View でこのようなデバッグ情報を表示するには、レイアウトXMLファイルと Java/Kotlin のコードをそれぞれ書き換える必要があり、リリース後のアプリに反映されない作業としてはちょっとわずらわしいものでした。変数の更新に追随するレイアウトXMLファイルを作成できるデータバインディングも提供されましたが、直感的とはいいがたいものでした。Jetpack Compose は情報をデザインに直接書き込むような感覚で画面を設計でき、時間のかからない作業で実装の成果を確認できます。 なんでもグラフィカルに表示するのがよいわけではありませんが、表現の選択肢が増え、コーディング作業に入っていきやすくなると思います。 [^1]: WebView の機能を Composable として利用するラッパー が accompanist のライブラリで提供されていますが、現在は 非推奨 となっています。View の Composable化はご本家でも簡単ではないようですし、一般のアプリ開発で View と Composable の同居が長く続くのは特におかしなことではありませんので、安心してComposable化に踏み切りましょう。 2.3. 宣言的にデバッグ情報を書く Composable によるデバッグ情報の表現は、さらに少ない手数で情報の追加を行なえます: @Composable fun TargetWatcher(visibleRect: Rect, targetRect: Rect) { // ... // ... Text( text = when { targetRect in visibleRect -> "Target: inside screen" visibleRect.overlaps(targetRect) -> "Target: crossing edge of screen" else -> "Target: outside screen" }, modifier = Modifier.align(Alignment.TopStart), style = TextStyle( textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurface, fontSize = 24.sp, fontWeight = FontWeight.W600 ) ) } } /** * "operator fun receiver.contains" defines in and !in (syntax: other in(!in) receiver) */ private operator fun Rect.contains(other: Rect) = left <= other.left && top <= other.top && right >= other.right && bottom >= other.bottom 上記では Text(...) を使ってデバッグ情報を追加しています。 Text(...) の引数 style は、デフォルト設定で充分なら省略できます。 Log.d(...) や println(...) などとほとんど変わらないタイプ数で情報を表示できます。しかもそれらと違ってスクロールで流れていってしまうこともありません。 「print文デバッグ」のような手軽さでUI構築ができるのも、 宣言的UI の意義の1つでしょう。 アプリをビルドして実行し、右上のスクロールビューを横スクロールすると、以下のように画面下部でアプリの動作する様子が確認できます: 画面外に表示中 画面の境界上にかかっている 画面内に表示中 3. LaunchedEffect で手続き的な処理 これまで 宣言的UI の意義を説明してきましたが、状態が変わったときのイベント処理や、状態が変わったことを示すアニメーションなどを実装するには、 手続き的 な処理も必要です。従来からの View で行なってきたイベント処理など手続き的な記述もできなければ、Jetpack Compose へは移行できません。 Jetpack Compose では LaunchedEffect を使って状態の変化に応じた処理を書くのが一般的です: @Composable fun TargetWatcher(visibleRect: Rect, targetRect: Rect) { // ... // ... var currentState by remember { mutableStateOf(TargetState.INSIDE) } var nextState by remember { mutableStateOf(TargetState.INSIDE) } var stateText by remember { mutableStateOf("") } var isTextVisible by remember { mutableStateOf(true) } nextState = when { targetRect in visibleRect -> TargetState.INSIDE visibleRect.overlaps(targetRect) -> TargetState.CROSSING else -> TargetState.OUTSIDE } LaunchedEffect(key1 = nextState) { if (stateText.isNotEmpty()) { if (currentState == nextState) return@LaunchedEffect stateText = when (nextState) { TargetState.INSIDE -> "Target: entered screen" TargetState.OUTSIDE -> "Target: exited screen" TargetState.CROSSING -> if (currentState == TargetState.INSIDE) "Target: exiting screen" else "Target: entering screen" } currentState = nextState repeat(3) { isTextVisible = true delay(250) isTextVisible = false delay(250) } } stateText = when (nextState) { TargetState.INSIDE -> "Target: inside screen" TargetState.CROSSING -> "Target: crossing edge of screen" TargetState.OUTSIDE -> "Target: outside screen" } isTextVisible = true } if (isTextVisible) { Text( text = stateText, modifier = Modifier.align(Alignment.TopStart), ) } } } enum class TargetState { INSIDE, CROSSING, OUTSIDE } LaunchedEffect の key は複数個の設定も可能です。また、Composable が呼び出される最初の1回だけ処理を行ないたい場合は、 LaunchedEffect(Unit) { ... } として実現できます。 ここで key に指定された nextState が変化するたびに、その変化に応じた処理が行なわれます。上記のコードは、状態が変化した直後の1.5秒間はテキストが点滅しながら直前の状態と比較した現在の状態を表示し、その後は静的な状態を表示します。 LaunchedEffect の key に状態の変数を指定し、それが変化したときの処理をブロック内に記述することでイベント処理が可能です。 LaunchedEffect の ブロック内では delay(...) などの suspend関数 を使った時間のかかる処理の記述が可能です。ブロック内の処理が終わる前に次の状態の変化があった場合、ブロック内の処理はキャンセルされ、次の状態の変化に応じた処理が最初から行なわれます。 LaunchedEffect の ブロック内では 手続き的 な処理を書いていますが、その後の Text(...) は手続き的に与えられた変数の値にしたがって 宣言的 に記述されていることに注目してください。UIの変化に応じた処理は LaunchedEffect を利用するのがよいでしょう。このほか、 SideEffect や、Activity および Fragment のライフサイクルに応じた処理を行なう DisposableEffect など、さまざまな状況への備えが用意されています。 一方、必要以上に複雑にならないよう、このような処理を多く書きすぎないことも重要です。たとえばインターネットからのレスポンスやNFCなど各種センサーの入力を契機としたイベント処理は ViewModel などに記述して、Composable 内の手続き的な記述はUIに関する要素のみにとどめることをお勧めします。 4. ComposeView と AndroidView の相互運用 Activity や Fragment の中で Composable を使う場合、上記のように ComposeView を使って Composable を表示できます。 逆に先述の WebView などを Composable の中で使うために AndroidView または AndroidViewBinding を使って View を Composable に埋め込むこともできます。方法は こちら(Compose でビューを使用する) をご参照ください。 本記事では詳細は省きますが、 AndroidView Composable の存在によって、Composable化の作業を進めたものの一部の View は Composable への置き換えが困難、もしくは非常に時間を要する、といった場合にも、Compose と View を混在させながら開発を進めることができます。 この相互運用は非常に強力で、 ComposeView で呼び出している Composable の中で AndroidView を呼び出し、その中で再度 ComposeView を設置して Composable を呼び出し、さらにその中で AndroidView を…という階層構造も可能です。Composable の中に View を使う余地を残すことで、Composable化作業が思ったように進展しない、あるいは Composable への置き換え作業に開発スプリントの期間を大きく超える時間を要する、といった場合にも、作業に費やした時間を無駄にするリスクが抑えられています。 5. Preview関数 Composable も View と遜色ない水準で位置合わせやデバッグ情報取得のテクニックが使え、手続き的な処理も書くことができ、あるいは Composable化の障害に直面しても強力な相互運用によって一部を View に戻す選択肢がある、と、これまで来た道を振り返ることのできる柔軟さについて主に述べました。ここでは Composable化によって新たに得られる果実、Preview関数の簡単で強力なプレビュー機能について述べます。 5.1. Preview関数を作成する レイアウトXMLファイルによる View の表現も Android Studio のプレビュー機能が利用できましたが、Jetpack Compose ではさらに強力なプレビュー関数を書けます。 @Preview アノテーションで修飾した関数を作成するだけで、Composable がプレビュー表示されます: // ... private class TargetWatcherParameterProvider : PreviewParameterProvider<TargetWatcherParameterProvider.TargetWatcherParameter> { class TargetWatcherParameter( val visibleRect: Rect, val targetRect: Rect ) override val values: Sequence<TargetWatcherParameter> = sequenceOf( TargetWatcherParameter( visibleRect = Rect(0f, 0f, 100f, 300f), targetRect = Rect(90f, 80f, 110f, 120f) ), TargetWatcherParameter( visibleRect = Rect(0f, 0f, 300f, 100f), targetRect = Rect(80f, 90f, 120f, 110f) ) ) } @Preview @Composable private fun PreviewTargetWatcher( @PreviewParameter(TargetWatcherParameterProvider::class) params: TargetWatcherParameterProvider.TargetWatcherParameter ) { KtcAdventCalendar2024Theme { TargetWatcher(params.visibleRect, params.targetRect) } } 上記は PreviewParameterProvider を利用して、ひとつのプレビュー関数で複数のパラメータを与えてプレビュー表示する例を示しています。 Android View のレイアウトXMLファイルで tools:??? 属性を使ってプレビューを設定する方式では、このようなことはできません。 PreviewParameterProvider を使わなくても、 @Preview と @Composable の2つのアノテーションで修飾された関数内で Composable関数を呼び出す記述を書くだけでプレビュー表示が可能です。 筆者といたしましては、新しい Composable を作り始めたときにはすぐにプレビュー関数を作成することをお勧めします。Composable の新規作成時に最初から強力なプレビュー機能を使える長所だけでも、Jetpack Compose への移行の動機として充分であると考えます。 デバッグ情報の表示の確認がプレビュー関数で行なえることも、Composable を活用してデバッグする利点の1つでしょう。 また最近では、 roborazzi などのライブラリを利用してプレビュー関数をUIテストに利用する手法が注目されており、テスト効率化の観点からもプレビュー関数の作成は有用です。 5.2. Preview関数を実行してみる こちら(プレビューを実行) に書かれている通り、Android Studio の左側の**Run '...'**アイコンをクリックすると、Preview関数をAndroid実機またはエミュレータで実行してみることができます。従来からの特定のActivityを実行する機能と同じようなものですが、Preview関数が簡単に書けること、およびインテントなどの要素がなく単純化された状況での実行が可能であることから、より強力になっているといえます。ボタンタップなどのUIアクションのコールバックも実行されるので、Preview関数をあたかも簡易なアプリであるかのようにテストに使えます。 ただし、Preview関数でテストができることを重視するあまりに Composable に多くの機能を持たせすぎることはお勧めしません。ビジネスロジックはできるだけ ViewModel や Circuit における Presenter などの別のクラスないしは関数に分離し、Composable はあくまでも 宣言的UI であり続けるべく、極力UIの表現のみを記述することをお勧めします。 6. おわりに 本記事が、ひとりでも多くの開発者にとって Android View から Jetpack Compose に踏み出すきっかけとなれれば幸いです。考え方の異なるUIシステムへの移行は一筋縄ではいかず、挫折の不安がつきまといます。その第一歩の足取りができるだけ軽やかになること、かつ徒労に終わるリスクができるだけ抑えられることを願っています。 7. 参考文献 Android API reference [Android]View の位置を取得する関数たち Jetpack Compose Modifier 徹底解説 What can Advanced / Lesser Known Modifiers do for your UI? — A Comprehensive Exploration in Jetpack Compose A Journey Through Advanced and 備考 Android ロボットは、Google が作成および提供している作品から複製または変更したものであり、 クリエイティブ・コモンズ 表示 3.0 ライセンスに記載された条件に従って使用しています。[^2] [^2]: https://developer.android.com/distribute/marketing-tools/brand-guidelines#android_robot
アバター
Hello, this is Hiroya (@___TRAsh) 🎅 This year, our Mobile App Development Group has focused extensively on sharing our work with the broader community. We have engaged in various initiatives, such as sponsoring and presenting at events like iOSDC and DroidKaigi, as well as contributing to this tech blog. This has provided us with more opportunities to connect with all of you throughout the year. To wrap up 2024, we’re excited to launch a series of posts as part of the KINTO Technologies Advent Calendar, focusing on Android/Flutter/iOS 🎉 https://qiita.com/advent-calendar/2024/kinto-technologies Our Android/Flutter/iOS engineers are working hard to publish the entire series, so please check them out 🎅 Today, I would like to introduce the Mobile App Development Group, the group behind our Android, Flutter, and iOS app development. What does the Mobile App Development Group do? We develop mobile apps for KINTO Technologies across iOS, Android, and Flutter platforms. The main products we develop are as follows. https://kinto-jp.com/entry_app/ https://kinto-jp.com/unlimited/app/ https://top.myroute.fun/ https://ppap.kinto-jp.com/prismjapan In addition to the above, we also take on development projects in response to PoC requests. Moreover, by leveraging the benefits of being a cross-functional team, we frequently host in-house study sessions to actively share new technologies and knowledge among team members. A survey of our team members This time, we conducted a survey with our engineers. We were able to gather insights that are not usually visible in our daily work, and I would like to share them here. 1. What is your development environment? (Up to 2 responses allowed) ![Development Environment Pie Chart](/assets/blog/authors/HiroyaHinomori/mobile_advent_calendar_2024_12_01_01.png =450x) One reason for the high percentage of Android in our development environments is the large number of Android engineers on our team. This is quite rare in Japan, I believe. Also, the Flutter team has been formed this year! We hope to gradually share more about Flutter-related development in the future. 2. How many years have you been developing? ![Development Experience Pie Chart] (/assets/blog/authors/HiroyaHinomori/mobile_advent_calendar_2024_12_01_02.png =450x) Since our company primarily hires mid-career professionals, many of our engineers have extensive experience. I was surprised to see so many with over 10 years of experience. It’s something that I hadn’t realized before. I look forward to learning from these experienced engineers as we continue to grow together. 3. Where are you from? ![Hometowns Pie Chart] (/assets/blog/authors/HiroyaHinomori/mobile_advent_calendar_2024_12_01_03.png =450x) I had a feeling this might be the case, but it turns out that less than half of our team members are Japanese. This is quite a unique workplace! While most communication happens in Japanese, we also hear English, Chinese, and Korean, creating a truly global environment. 4. What do you like about the Mobile App Development Group? We generated a word cloud from the comments we received to showcase the strengths of our team. It’s clear that atmosphere and technology stand out prominently. This seems to reflect the results of our efforts to prioritize communication across product teams and actively share knowledge—something that can be overlooked in daily work. We will continue to do our best next year as well 💪 :::details Summary Technology and learning environment Openness to new technologies: A flexible environment that encourages the adoption and use of the latest technologies. Support for skill development: A learning-friendly setting with active study sessions and knowledge sharing. High skill level: Members with strong technical skills and passion for growth. Focus on output: A dedication to delivering results and achieving goals. Communication and atmosphere Friendly atmosphere: A supportive environment where members can easily ask questions and share comments. Collaborative teamwork: Smooth collaboration both within and across project teams. Diversity and humor: A multicultural and diverse team that embraces individuality and enjoys cultural differences. Flat communication: Minimal barriers between members, regardless of age or background. Work environment Flexible and free work styles: A work culture that respects individual approaches and autonomy. Good team spirit: A cohesive team where everyone works well together. Supportive Environment: A welcoming and safe space where members feel comfortable working. It can be said that these qualities create an ideal team environment where learning, growth, diversity, and collaboration are valued and truly enjoyed. ::: 5. What technology are you most interested in right now? iOS Android We also created a word cloud for this topic using the comments we received. For both Android and iOS, KMP seems to be attracting attention. We can also see mentions of Flutter and Compose Multiplatform, which suggests that many team members are interested in cross-platform technologies. From my perspective, this year has truly been a breakthrough for cross-platform development. Additionally, it appears that many are keeping an eye on the technical evolution of programming languages specific to each platform. Moreover, the attention given to AI-related technologies reflects current industry trends, showing the high level of technical interest among our team members. :::details Summary iOS Top 3 Technologies Swift / SwiftUI Core technologies for Apple platforms, with particular interest in UI development. KMP (Kotlin Multiplatform) Utilized for multiplatform development, including integration on the iOS side. AI (MLC LLM、Apple Intelligence) Growing interest in machine learning and Apple’s AI technologies. Android Top 3 Technologies Jetpack Compose The core UI development technology, with active exploration of efficient coding practices and mastery. KMP (Kotlin Multiplatform) Notable for its use in Android app development and its integration with Compose Multiplatform. Flutter A popular choice for cross-platform development. ::: Summary I feel that the Mobile App Development Group is creating an environment where members can enjoy diversity and collaboration while learning. This is achieved through aspects such as a focus on technology and learning, effective communication and atmosphere, and ease of working. As mobile technology trends evolve rapidly, I aim to stay updated and contribute to the group’s growth. Lastly, I hope this article has conveyed the appeal of the Mobile App Development Group to you 🎅 Stay tuned for our Advent Calendar kicking off tomorrow 🎄
アバター
自己紹介 こんにちは。KINTOテクノロジーズの小山( @_koyasoo )です。今年からアジャイル推進に注力し始め、普段はスクラムマスター専任でチーム作りに励む毎日を送っています。今回はその中の活動を紹介したいと思います。 レトロスペクティブといえば・・? みなさん、ふりかえり(レトロスペクティブ)していますか? レトロスペクティブといえばKPT。ふりかえり手法のもはや代名詞と言われるようになってきましたね。KPTを使っている現場はものすごく多いと思いますが、果たしてそれは機能しているでしょうか? 私は今年の6月にスクラムフェス大阪に参加し、色々なセッションを受ける中で1つ強烈に印象に残ったセッションがありました。 OODA!!!!! (わかる人にはわかる) はい、いくおさん( @dora_e_m )ですね。いくおさんのふりかえりに関するセッションがとても印象に残りました。それまでふりかえりにはKPTしか使っていなかった私に 「ふりかえりが実りある場であり続けるために マンネリ化を避けたい」 という言葉がとても刺さりました。そこで「ふりかえりならいつもやっているし、すぐに行動に移せるのでは?」と思いました。 @ card 特にセッションの22ページ目に書いてあるように、KPTをYWT(KPTと似ている)に変更しただけで、めちゃくちゃ意見が出たというところが目から鱗でした。発想を変えるだけで意見って出るのか・・。 ![いくおさん資料22ページより引用](/assets/blog/authors/tkoyama/retro/ikuo-22.png =600x) いくおさん資料22ページより引用 そこで、本記事では私が実際に実践した5つのふりかえり手法をご紹介します。 ふりかえり手法の使い分け方まとめ いくおさんのセッションにもあるように、状況にあったふりかえり手法を切り替えることができると、より良いチームの成長につながると考えています。以下に各手法の特徴をまとめてみたのでぜひ参考にしてください。 こんな時に使える! 気をつけること KPT いつでも使えるオールマイティ こればっかりにならないこと 熱気球 チームの未来を考えたいとき 現状ある課題より過去のふりかえり要素が小さくなる LeanCoffee いろんな話題をしたいとき, ふりかえりじゃなくても使える 時間に追われてディスカッションするので少々疲れる Celebration Grid 事実に基づいてディスカッションしたいとき 実施したことが少なく、感じたことが多いタイミングだと意見が出ずらい FunDoneLearn ポジティブにふりかえりたいとき ネガティブが無視されがち 象、死んだ魚、嘔吐 チームに不満が溜まっていそうなとき チームが崩壊しないようにファシリテーションする 以降では、それぞれの振り返り手法について具体的にご紹介します。 ふりかえり紹介! この記事を読んでくれた方(特に今までKPTしかやっていない方)にぜひ、 別手法を試す第一歩を踏んでほしい! と考え、実施の仕方をできるだけ具体的に紹介したいと思います。やりやすいもので構わないので、ぜひやってみてください。意外とやってみたらKPTとそんなに変わらないです! なお、紹介する例では基本的にオンラインホワイトボードツールの Miro を使用しています。 @ card 1. 熱気球 中央に描かれる「熱気球」を自身のプロダクトと置き換え、それに対しどんな「荷物」があったか、「上昇気流」は何か、今後妨げとなりうる「雲」は何だろうか、という考え方でふりかえりを進める手法となります。 熱気球のイメージ画像と、3種類の見分けられる付箋を準備しておけば実施可能です。我々のチームではこのような熱気球が作成できました。 熱気球の図 実施方法は以下のとおりです。 まず「上昇気流」について書き(5分)、ディスカッションを実施(8分)。 次に「荷物」について書き(5分)、ディスカッションを実施(8分)。 同様に「雲」について書き(5分)、ディスカッションを実施(8分)。 最後に「より熱気球を高く飛ばすために重要なことは?」という観点で議論(10分)。 この手法では現在を見つめ直し、未来を想像することにフォーカスが当たる議論が生まれやすいです。KPTと比べるとProblemがProblem(現在)、Problem(予想)に分解されるため、より課題点をディスカッションしやすい手法とも言えます。 2. LeanCoffee 話題の洗い出しから始まり、さらにそれらに対して短いタイムボックスに区切って様々な会話を実施する手法です。 話題洗い出し用の付箋を編集できるエリア、ディスカッションに選出された付箋を順番に処理するエリアを準備しておけば実施可能です。こちらはMiroにテンプレートがありましたのでそちらを使ってみました。 LeanCoffeeの図 実施方法です。 はじめに参加者にお題を挙げてもらいます(8分)。このときテーマを指定してあげると参加者が意見を出しやすいです。 投票機能などを使い参加者の興味のあるお題を特定します。 票数の多いものから順番に以下のサイクルでディスカッションします。 最初は5分間、お題の説明するところを含めてディスカッションを開始します。 5分経った時点で一旦議論を止めます。そしてこのお題でまだ続けるかを参加者に問います。投票機能を使っても良いです。 続ける場合は追加で3分、終わる場合は次のお題に移ります。 3分経った時点でも同様に議論を止め、再度続けるか問います。 続ける場合は追加で1分、終わる場合は次のお題に移ります。 最後の1分が終わったらそのお題は終了です。追加で話したい場合は別途時間を設けるようにして、その時間でのディスカッションは終了させましょう。 この手法はとにかく色々な話題が次々に移り変わりますので、たくさんのことについてディスカッションができます。また、チームメンバーのその時興味のある方向性が傾向としてみれます。さらに、メンバーのタイムボックスへの意識付けにも使えるかもしれません。 ファシリテーション時には特に議論を止めるのが難しいですが、タイマーで音を鳴らしたり、会話の切れ目などで何とか止めるようにしましょう。この手法はタイムボックスが守れないと崩壊してしまいますので注意です。 3. Celebration Grid 実施した事柄に対して「成功」「失敗」の軸と「間違ったやり方」「実験的なやり方」「既に知っているやり方」の軸で、六象限に分けてディスカッションを進める手法です。名前の通り、成功・失敗に関わらず起きたことをお祝いするポジティブなマインドで進めます。 こちらのサイトの図を使用して進めることが多いみたいです。 @ card ![CelebrationGridのテンプレート](/assets/blog/authors/tkoyama/retro/celebration-grid-completed.jpg =600x) CelebrationGridのテンプレート 図の通り、それぞれのエリアは事象が発生する確率に合わせて大小サイズが異なっており、以下のような意味合いがあります。これらに沿ってディスカッションを進めました。 間違ったやり方 実験的なやり方 既に知っているやり方 成功 ラッキーだったね! いい体験をしたね! 正しいことをしたね! 失敗 なるべくしてなった 大丈夫、学びはあったさ 運がなかった Celebration Gridをやってみた図 実施方法です。 期間を指定して「やったこと」を挙げてもらいます(5分)。それぞれどこに属すかを考えながら挙げてもらうように誘導します。 それぞれについて深掘ります。 最後に多くの気づきがあったことを全員で祝って終了します。 ぱっと見、難しそうな手法かと思いきや結構シンプルです。「発生した事象・事実」に基づいて会話が進むので、思想に捉われず事実と分離しながらディスカッションできます。逆に「事実」をまず挙げなければいけないので、参加者によっては意見が出ずらい場合があります。メンバーを集める際にできるだけ実働している人を選定するとうまくいきそうです。 4. FunDoneLearn 文字通り、Fun(楽しかったこと)、Done(やったこと)、Learn(学んだこと)を挙げていく手法になります。それぞれを包含する要素は円が重なるところに書いてもらいます。 それぞれの円を重ねたベン図のようなテンプレートを用意しておけば実施可能です。重なるエリアを大きくしておくと付箋が貼りやすいです。 FunDoneLearnの図 記載するほどではないですが、実施方法です。 期間を指定して、付箋を貼ってもらう(5分)。 それぞれに対してディスカッションする Funの要素があるように、ポジティブにフォーカスする手法です。全体的にハッピーな気持ちで振り返りが実施できると思います。逆にProblemのようなネガティブ要素が少ないので、それらが多そうな場合はあまり向かないかもしれないです。 5. 象、死んだ魚、嘔吐 3つの角度から課題を洗い出す手法です。象(みんな知っている課題)、死んだ魚(放置するとまずい課題)、嘔吐(心の中にある課題)といった具合で、メンバーから普段言いづらいことをぶっちゃけてもらいます。 象と魚と嘔吐の図があれば実施可能ですが、メンバーの個人間の対立を防ぐために大きめにルールを書いておくことを推奨します。 象、死んだ魚、嘔吐の図 実施方法です。 まずルールの説明をします。この手法でチーム内を対立させたいわけではなく、今ある課題に対して打ち手を考えたいだけであることを説明します。この手法では大事なポイントだと思っています。 それぞれに沿って付箋を挙げてもらいます(8分)。私のチームではネガティブに寄りすぎないように、反転してポジティブな意見になればピンクの付箋で追記してもらうよう案内しました。 特にこの手法では意見出しの最中に他のメンバーの内容が見えてしまうと、場合によってはモヤモヤしてしまうかもしれないため、編集中の付箋は公開しないようにします。MiroであればPrivate Modeを使うと良いです。 Private Modeを解除し、それぞれに対してディスカッションします。 ネガティブに向き合う手法のため、他の手法に比べると少々気を遣うことが多いです。ですが、そこまでネガティブな雰囲気にはならず、「そんなこと気にしてたのか〜」「同じこと思っていた!」などの意見が出ると予想されます。チームでの課題解決の方向性を合わせるのに非常に有効だと思います。 どのふりかえりにも言えること KPTを含め6つのふりかえりを実施した私の所感ですが、共通項は意外と多いです。 最終的なゴールはいつも「ネクストアクションをチームで合意する」 大きめにルールを書いておくと参加者が迷わない 意見は会の中で挙げてもらえばOK!ベースは5分、考えさせる内容なら8〜10分 ディスカッションではまず付箋を説明してもらい、次に「自分で感想を言うか」「感想ありそうな人に雑に振る」!(雑な方がフラットに意見が出やすいと思ってます笑) 自分も意見を出す場合は、事前に付箋を用意しておいた方がファシリテーションに集中できる 特に1番目の「ネクストアクションを合意」が意識できていれば、どの手法も回すことができます。それさえ決まればふりかえりを実施した価値になります。他のことは全て忘れても良いくらいです。 参加者からの感想 新しい手法を新しいメンバーで実施した後はいつも不安になります。ぶっちゃけKPTで良かったんじゃないか・・、色々指示しすぎて果たして振り返れていたか・・。 もしこんな思いをしていたら(自分のように)、恐れずにメンバーに感想を聞いてみましょう!きっとポジティブな意見しか返ってこないですよ。 いつもと違うふりかえりで新鮮だった。楽しかった。(熱気球) タイムボックスを意識して話ができたので、いつもは同じ話題で終わってしまうが色々な話ができて良かった。課題点が明確になった。(LeanCoffee) 失敗が多くなってしまったが、それがいい失敗なのか悪い失敗なのか分けて考えることができた。(Celebration Grid) チームメンバーが何に楽しさを感じて仕事をしているのかわかって良かった。シンプルに楽しかったことをシェアできたのも良かった。(FunDoneLearn) 思っていた課題がチームの共通認識にできて良かった。ぶっ込んでくれて助かった。(象、死んだ魚、嘔吐) まとめ どれかの手法に固定するのではなく、シーンに合ったふりかえり手法を選んで実施できるようになると、より良いチームになっていくと考えます。私もまだ6手法しか試せていないので、これからどんどん増やしていきたいと思います! 冒頭でも記載した通り、まだKPTしかやっていない方がいればぜひどれかの手法を試してみてください!まずは本記事記載の流れで。慣れてきたらアレンジしてチームに合わせてみるなんてどうでしょうか。 この記事がレトロスペクティブで悩めるスクラムマスターの後押しになったら嬉しいです。
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の17日目の記事です🎅🎄 学びの道の駅の中西です。今年は学びの道の駅プロジェクトが立ち上がり組織化されました。そんな中、社内Podcastの運営も行っており、今年のアドベントカレンダーではその内容もお届けしたいと思います。 「学びの道の駅」とは? 「学びの道の駅」は、社内で頻繁に開催される勉強会をもっと便利に、そして効果的に活用するために立ち上げられたプロジェクトです。社内の有志が中心となり勉強会の開催を支援し社内の知識共有を促進することを目的としています。 セキュリティ&プライバシー勉強会 KTC学びの道の駅ポッドキャストでは、社内の勉強会を開催している方にインタビューを行っています。その名も「突撃!となりの勉強会」。本日のポッドキャストのゲストは「セキュリティ&プライバシー勉強会」を開催している桑原さん、森野さん、笠井さんにインタビューします。みなさん、どうぞよろしくお願いいたします。 インタビュー 森野さん: セキュリティ・プライバシーグループの森野です。よろしくお願いします。 笠井さん: データ&プライバシーガバナンスチームの笠井と申します。よろしくお願いします。 桑原さん: セキュリティCoEグループの桑原です。よろしくお願いします。 HOKAさん: では、まず皆さんの普段のお仕事について教えていただけますか? 森野さん: 私たちのチームは三つに分かれています。データ&プライバシーガバナンスチーム、インフォメーションセキュリティチーム、そしてサイバーセキュリティディフェンスチームです。データ&プライバシーガバナンスチームは、データとプライバシーに関するルールの整備やガバナンスの効かせ方を担当しています。インフォメーションセキュリティチームは、情報セキュリティのスタンダードに基づいてアセスメント活動を行います。そして、サイバーセキュリティディフェンスチームは、脆弱性の管理や脅威情報の収集・調査を担当しています。 笠井さん: 私のチーム、データ&プライバシーガバナンスチームは、データに関するセキュリティ管理や個人情報の管理に関するルールを制定し、リスク評価を行い、それを経営に報告しています。 桑原さん: セキュリティCoEグループは、AWS、Azure、Google Cloudなどのクラウド環境のセキュリティを担当しています。安全にクラウドを利用するためのガードレールの設定や、危険な兆候の監視・サポートを行っています。 HOKAさん: 次に、セキュリティ&プライバシー勉強会について教えてください。どのような勉強会ですか? 桑原さん: この勉強会は、社員の皆さんにセキュリティに関連する知識を広めるために開催しています。昨年度から具体的なガイドラインを作成し、それを周知させるために勉強会を始めました。 森野さん: この勉強会を通じて、セキュリティバイデザインやプライバシーバイデザインが当たり前の要件として検討され、実装されることを目指しています。 笠井さん: 私たちの目的は、社員の皆さんがエンジニアリングに集中できるようにし、プライバシーやセキュリティのリスクについては我々がサポートすることです。 HOKAさん: 勉強会の反響はいかがですか? 桑原さん: 今月までに計4回実施しました。反響は良く、勉強になったという声や改善要望もいただいています。 森野さん: 参加者からは、説明がわかりやすくなったという声もあり、工夫を重ねています。 HOKAさん: この勉強会を通じて、KTCにどのような変化を期待していますか? 森野さん: セキュリティやプライバシーの機能が標準として実装される状態を目指しています。 桑原さん: 私たちの仕事が不要になるくらい、セキュリティが当たり前になることが理想です。 笠井さん: エンジニアの皆さんが安心して開発に集中できる環境を作りたいです。 HOKAさん: 最後に、皆さんがこの分野に進んだきっかけを教えてください。 森野さん: 私はソフトウェア開発からセキュリティに興味を持ち、学び続けています。 笠井さん: もともとセキュリティやプライバシーには関心がありませんでしたが、ガバナンスの面白さに引かれました。 桑原さん: 私はインフラエンジニアとしてキャリアを始めましたが、自分の管理するメールサーバーが侵害された経験からセキュリティに取り組むようになりました。 HOKAさん: 本日はありがとうございました。セキュリティ&プライバシー勉強会がKTCの皆さんにとって有益なものになることを期待しています。 皆さん、どうもありがとうございました。 今回はセキュリティ&プライバシー勉強会の詳細と、その運営の背景、今後の展望についてお届けしました。次回の勉強会も楽しみにしてください!
アバター
Flutterアプリとネイティブ機能の連携 〜Android専用のカメラ解析ライブラリを組み込むために検討したこと〜 こんにちは。Toyota Woven City Payment 開発グループの大杉です。 私たちのチームでは、 Woven by Toyota の Toyota Woven City で使用される決済システムの開発をしており、バックエンドからWebフロントエンド、そして、モバイルアプリケーションまで決済関連の機能を幅広く担当しています。 これまで、私たちはFlutterを使ってProof of Concept (PoC) 用のモバイルアプリの開発を行なって来ました。今回は、そのPoC用アプリに新たにAndroid/iOSネイティブでしか提供されていないカメラ解析ライブラリを組み込み、新機能を開発した際に直面した課題に対して試行錯誤したことをまとめました。 はじめに Flutterアプリにネイティブ機能を組み込むことは、単純な開発工数だけでなくメンテナンスのコストも増大するので、開発のハードルが高くなります。 私たちのプロジェクトでは、開発期間とリソースを鑑みてFlutterアプリにネイティブ機能を組み込まず、PoC用アプリとカメラ解析用のネイティブアプリを別々に開発し、それらを連携させることでPoCを実施しました。PoC完了後、Flutterアプリとカメラ解析アプリの統合を検討した際、Flutterのネイティブ連携機能に関して設計指針や実装方法などの情報が断片的であり、特にAndroidの複雑なUI構成に対して体系的な指針が少ないと感じました。 この記事では、Androidにフォーカスを当て、FlutterアプリにネイティブUIを組み込むための設計指針と具体的な実現方法を紹介します。 同じような境遇に直面しているエンジニアの皆さんの参考になれば幸いです。 :::message 執筆時のサンプルコード作成には Flutter v3.24.3 / Dart v3.5.3 を使用しています。 ::: アプリの概要 実際のPoCで開発したアプリを今回の記事に合わせて簡略化すると、以下のような仕様のアプリとなります。 仕様 startボタンを押下すると、カメラプレビューが表示される カメラプレビューの画像に対してカメラ解析機能が実行され、解析結果が通知される この記事では、このアプリをベースに話を進めていきたいと思います。 FlutterとAndroidネイティブのデータ連携 まず、Flutterからカメラを操作したり、Androidネイティブから解析結果を通知するなど、FlutterとAndroidネイティブ間のデータのやりとりでは、 MethodChannel と EventChannel を使用して実装しました。 特に、カメラの起動・停止などの命令については MethodChannel 、解析結果のイベントの通知については EventChannel を使用しています。 シーケンス図に表すと以下のようになります。 sequenceDiagram actor u as User participant f as Flutter participant mc as MethodChannel participant ec as EventChannel participant an as Android Native u ->> f: press start button activate f f ->> mc: start camera mc ->> an: set up camera an -->> mc: mc -->> f: result deactivate f loop an ->> ec: analyzed result ec ->> f: send analyzed data f ->> f: show data end u ->> f: press stop button activate f f ->> mc: stop camera mc ->> an: reset camera an -->> mc: mc -->> f: result deactivate f 次に、AndroidネイティブのカメラプレビューのUIをFlutter側に表示させる方法について話をしたいと思います。 FlutterアプリでAndroidのネイティブUIを表示する方法 FlutterアプリでAndroidネイティブのUIを表示する方法は、大きく分けて3つあります。 AndroidネイティブのSurfaceに描画された画像をFlutterのWidget tree内で表示する Texture widget AndroidネイティブUIをFlutterのWidget tree内に組み込み、表示・制御できる PlatformView 新たにActivityを起動する Intent それぞれの特徴と実装方法について説明します。 Texture widget Texture widgetは、AndroidネイティブのSurfaceに描画された画像をFlutter側のWidget tree内に表示します。つまり、ネイティブのUIの画像をFlutterからGPUに直接描画するものです。 この機能は、カメラプレビューや動画の再生などレイテンシーがあまり問題にならないユースケースには適していますが、リアルタイム性が求められるUIアニメーションなどではネイティブ側で調整する必要があり、FlutterとAndroidネイティブについて習熟している必要があると言えます。 また、 Texture widget自体にはタッチイベントなどのユーザーインタラクションを検知する機能もないため、Flutter側で GestureDetector などを使って実装する必要があります。 ただし、要件がマッチしていれば以下の実現方法のように比較的簡単に実装できます。 実装方法 まずは、 TextureRegistry というものを取得します。 Flutterアプリの場合は、TextureRegistryの実装である FlutterEngine.FlutterRenderer を取得できます。 Flutterプラグインの場合は、FlutterPluginBindingから取得できます。 // Flutterアプリの場合 val textureRegistry = this.flutterEngine.renderer // Flutterプラグインの場合 val textureRegistry = this.flutterPluginBinding.textureRegistry 次に、 textureRegistry から SurfaceTexture である textureEntry を生成し、 CameraXのプレビューインスタンスに対して Surface を提供する SurfaceProvider を設定します。これで準備は完了です。この Surface が、前述した描画バッファとなります。 val textureEntry = textureRegistry.createSurfaceTexture() val surfaceProvider = Preview.SurfaceProvider { request -> val texture = textureEntry?.surfaceTexture() texture?.setDefaultBufferSize( request.resolution.width, request.resolution.height ) val surface = Surface(texture) request.provideSurface(surface, cameraExecutor) { } } val preview = Preview.Builder().build().apply { setSurfaceProvider(surfaceProvider) } // 記事冒頭のカメラを解析する要件を満たすには、 // cameraProviderを用意し、Previewと解析処理をここでカメラに設定することで実現できます。 try { camera = cameraProvider?.bindToLifecycle( this, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis, // ここにカメラ映像の解析処理を設定 ) } catch(e: Exception) { Log.e(TAG, "Exception!!!", e) } その後、 Surface と関連づけられた TextureEntry のIDを MethodChannel の戻り値としてFlutter側に返してあげるだけです。 fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when(call.method) { "startCamera" -> { result.success(textureEntry.id()) } "stopCamera" -> { stopCamera() } else -> result.notImplemented() } } ネイティブの SurfaceTexture をFlutter側で描画するには、 MethodChannel から取得したtextureIdを Texture widgetに設定するだけで、カメラプレビューがFlutterアプリ上に表示されます。 static const platform = MethodChannel('com.example.camera_preview_texture/method'); int? _textureId; Future<void> onPressed() async { try { final result = await platform.invokeMethod<int>('startCamera'); if (result != null) { setState(() { _textureId = result; }); } } on PlatformException catch (e) { print(e.message); } } Widget build(BuildContext context) { if (_textureId == null) { return const SizedBox(); } return SizedBox.fromSize( size: MediaQuery.of(context).size, child: Texture( textureId: _textureId!, ), ); } この Texture widgetを使用した実装については、 mobile_scanner の実装が非常に参考になります。 PlatformView PlatformView は、AndroidネイティブUIをFlutterのWidget tree内に組み込み、表示・制御できるようにしたものです。 PlatformView には、 Virtual Display ( VD ), Hybrid Composition ( HC ), TextureLayerHybridComposition ( TLHC )という描画モードがあります[^1]。 PlatformView のAPIを利用すると、基本的には TLHC が選択されますが、AndroidネイティブのUIツリーに SurfaceView が含まれる場合は VD または HC にフォールバックします[^2]。 なお、 Texture widgetでは対応できなかったFlutterとAndroidネイティブのフレームレートの同期が改善され、ユーザーのインタラクションを制御でき、カメラプレビューや動画以外のUIも表示できます。 実装方法 この PlatformView を使ったサンプルコードでは、カメラプレビュー画面をJetpack Composeで実装しています。 FlutterアプリでJetpack Composeを使用するには、以下の依存関係や設定を app/build.gradle に追加する必要があります。 android { ~ ~ buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.4.8" } } dependencies { implementation("androidx.activity:activity-compose:1.9.3") implementation(platform("androidx.compose:compose-bom:2024.04.01")) implementation("androidx.compose.material3:material3") } それでは、具体的な実装の説明に移ります。 PlatformView を実装するには次の3つのステップが必要です。 PlatformView を継承したNativeViewを実装する PlatformViewFactory を継承したNativeViewFactoryを実装する FlutterEngine に PlatformViewFactory を登録する まずは、1. NativeViewの実装です。大まかな実装は 公式 を参照してください。 公式との差分として、ここではJetpack Composeを使用しており、Jetpack Composeである CameraPreview を ComposeView を使用してAndroidネイティブのViewのツリーに埋め込んでいます。 class NativeView(context: Context, id: Int, creationParams: Map<String?, Any?>?, methodChannel: MethodChannel, eventChannel: EventChannel) : PlatformView { private var nativeView: ComposeView? = null override fun getView(): View { return nativeView!! } override fun dispose() {} init { nativeView = ComposeView(context).apply { setContent { CameraPreview(methodChannel, eventChannel) } } } } Jetpack Composeの実装では、 View であるCameraXの PreviewView を AndroidView を使用してComposeにしています。 余談ですが、 AndroidView は Fragment に対しても使用できます。 @Composable fun CameraPreview(methodChannel: MethodChannel, eventChannel: EventChannel) { val context = LocalContext.current val preview = Preview.Builder().build() val previewView = remember { PreviewView(context) } suspend fun startCamera(context: Context) { val cameraProvider = context.getCameraProvider() cameraProvider.unbindAll() // 記事冒頭のカメラを解析する要件を満たすには、 // cameraProviderを用意し、Previewと解析処理をここでカメラに設定することで実現できます。 cameraProvider.bindToLifecycle( LocalLifecycleOwner.current, CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build(), preview, analysis, // ここにカメラ映像の解析処理を設定 ) preview.surfaceProvider = previewView.surfaceProvider } suspend fun stopCamera(context: Context) { val cameraProvider = context.getCameraProvider() cameraProvider.unbindAll() } LaunchedEffect(Unit) { fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when(call.method) { "startCamera" -> { runBlocking { CoroutineScope(Dispatchers.Default).launch { withContext(Dispatchers.Main) { startCamera(context) } } } result.success("ok") } "stopCamera" -> { runBlocking { CoroutineScope(Dispatchers.Default).launch { withContext(Dispatchers.Main) { stopCamera(context) } } } } else -> result.notImplemented() } } methodChannel.setMethodCallHandler(::onMethodCall) } AndroidView(factory = { previewView }, modifier = Modifier.fillMaxSize()) } 次に、2. NativeViewFactoryの実装と3. FlutterEngineへの登録は以下の通りです。 class MainActivity: FlutterFragmentActivity() { ~ ~ override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) val methodChannel = MethodChannel( flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL ) val eventChannel = EventChannel( flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL ) flutterEngine .platformViewsController .registry .registerViewFactory(VIEW_TYPE, NativeViewFactory(methodChannel, eventChannel)) } } class NativeViewFactory( private val methodChannel: MethodChannel, private val eventChannel: EventChannel ) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { override fun create(context: Context, viewId: Int, args: Any?): PlatformView { val creationParams = args as Map<String?, Any?>? return NativeView( context, viewId, creationParams, methodChannel, eventChannel ) } } 最後に、Flutter側の実装です。 PlatformViewsService.initSurfaceAndroidView() は、 TLHC / HC のいずれかを使用するためのAPIです。 PlatformViewsService.initAndroidView() を使用すれば TLHC / VD のいずれかを使用でき、 PlatformViewsService.initExpensiveAndroidView() を使用すると強制的に HC となります。 class CameraPreviewView extends StatelessWidget { final String viewType = 'camera_preview_compose'; final Map<String, dynamic> creationParams = <String, dynamic>{}; CameraPreviewView({super.key}); @override Widget build(BuildContext context) { return PlatformViewLink( viewType: viewType, surfaceFactory: (context, controller) { return AndroidViewSurface( controller: controller as AndroidViewController, hitTestBehavior: PlatformViewHitTestBehavior.opaque, gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{}, ); }, onCreatePlatformView: (params) { return PlatformViewsService.initSurfaceAndroidView( id: params.id, viewType: viewType, layoutDirection: TextDirection.ltr, creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), onFocus: () { params.onFocusChanged(true); }, ) ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) ..create(); }, ); } } このように PlatformView を使用することで、FlutterアプリにAndroidネイティブのUIを組み込むことができます。 Intent Intent は、FlutterではなくAndroidの機能で、Flutterが動作するMainActivityとは別のActivityを起動することできます。 これを使用すると、アプリ内の別の画面に遷移させたり、外部のアプリを起動させることができ、Activity間でデータのやりとりもできます。 前述した2つの方法 ( Texture widgetと PlatformView )にはパフォーマンスの課題があることが報告されています[^3]。 これらの課題を解決するためには、FlutterとAndroidネイティブへの深い造詣が求められるため、場合によってはAndroidアプリを別で作成してしまった方が開発コストを抑えられるかもしれません。 ただし、その場合は別の観点での課題があります。 チームにFlutterエンジニアしかいない場合は、Android開発のキャッチアップが必要となる 外部アプリとして開発した場合は、アプリ間のインターフェースに何らかのセキュリティ対策やライフサイクルを考慮した実装をする必要がある たとえば、以下のような対応が求められます。 Activity間でやりとりするデータに対してバリデーションをする 特定のアプリ以外からは呼び出されないようにする 呼び出されたアプリは、呼び出し元アプリがkillされてしまっている場合でも正しく動作することを保証する それでは、Flutterで Intent を使用する方法を見ていきたいと思います。まずは、Flutterアプリから別のActivityを呼び出す方法についてです。 呼び出し元のActivity(Flutterアプリが動作するMainActivity) override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { if (call.method!!.contentEquals("startCamera")) { val dummyData = call.argument<String>("dummy_data") ?: return result.error( "ERROR", "data is invalid", null ) // 画面遷移の場合 val intent = Intent(this, SubActivity::class.java) // 外部アプリの場合 val packageName = "com.example.camera_preview_intent" val intent = activity.packageManager.getLaunchIntentForPackage(packageName) ?: return result.error( "ERROR", "unexpected error", null ) intent.setClassName(packageName, ".SubActivity") // 送信データの格納 intent.putExtra("EXTRA_DUMMY_DATA", dummyData) intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) activity.startActivityForResult(intent, REQUEST_CODE) } } override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) { eventSink = sink } override fun onCancel(arguments: Any?) { eventSink = null } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) { val result = data.getStringExtra("RESULT_DATA") ?: "", eventSink?.success(result) return true } return false } 続いて、Flutterアプリから呼び出されたActivityの実装です。以下のように、特定の処理が完了したら Intent を使用してデータを返却できます。 呼び出し先のActivity val intent = Intent() intent.putExtra("RESULT_DATA", resultData) activity.setResult(Activity.RESULT_OK, intent) finish() このように Intent を使用することでFlutterとAndroidネイティブ側の複雑なUI制御を考慮しないで済み、さらには、FlutterとAndroidネイティブのActivity間でデータのやりとりができます。 ただし、その場合はセキュリティやデータの整合性については考慮する必要があります。 まとめ この記事では、Flutterアプリにネイティブ機能を組み込む方法について、Androidにフォーカスして説明しました。 FlutterとAndroidネイティブ間のデータ連携は、 MethodChannel と EventChannel を用いて実現しました FlutterへAndroidネイティブのUIを組み込む方法は以下 Texture widget カメラプレビューや動画表示に適しており、比較的簡単に実装できる ユーザーインタラクション制御の実装が必要で、パフォーマンスの課題もある PlatformView ネイティブUIをFlutterのWidget treeに組み込んでくれ、ユーザーインタラクション制御も実現できる View, Fragment, Jetpack Composeが組み込み可能 パフォーマンスの課題もある Intent 画面を遷移や別アプリを起動してAndroidのUIを直接表示でき、データのやりとりもできる セキュリティやデータのやり取りに注意が必要 以上、FlutterアプリにAndroidネイティブ機能を組み込む際はそれぞれの方法にメリットとデメリットがあるため、プロジェクトの要件に応じて適切な選択をすることが求められます。 参照 [^1]: Hosting native Android views in your Flutter app with Platform Views [^2]: Android Platform Views [^3]: Performance 備考 サムネイルのドロイド君は、Googleが作成および提供している作品から複製または変更したものであり、クリエイティブ・コモンズ表示 3.0 ライセンスに記載された条件に従って使用しています。
アバター
This article is day 1 of the KINTO Technologies Advent Calendar 2024 . 🎅🎄 Hello! Rina ( @chimrindayo ) here. I am an engineer at KINTO Technologies, where I am involved in the development and operation of Mobility Market and part of the Developer Relations (DevRel) Group as well. Today, I would like to tell you about a study session called “Mobility Night” that we have formed together with t-kurimura-san at Luup, Inc. 🙌 What is Mobility Night? Source: Documents created by t-kurimura-san Mobility Night is a collaborative study session aimed at advancing the mobility industry. It provides companies and organizations in this field with a platform to share their software technologies and expertise 🚀 We believe that many technical challenges in mobility-related software are shared across the industry, including areas such as GPS, IoT, quality assurance, and product design. This study session was created with the hope that sharing unique insights from the mobility industry on such topics will drive the advancement of software technologies and contribute to overall product improvement across the entire industry. The name "Mobility Night" was chosen to reflect our vision of creating a casual and inclusive space where everyone can come together to share and exchange information freely. Holding a Closed Event For the first event, we held a closed study session called “ Mobility Night #0 .” We invited Luup, Inc., Charichari, Inc., GO Inc., newmo, Inc. (all of which gave presentations), along with many other mobility companies, to join us. Participants shared mobility-related insights, with a focus on introducing their businesses and products to one another. We initially decided to hold a closed event to gauge interest in a technical study session focused on the mobility industry. This approach allowed us to assess whether such an event would resonate with participants and to identify common themes that could make future open study sessions meaningful and valuable for the community. Results from Holding a Closed Event Thankfully, the closed event was a big hit and a huge success. The event was so lively that some attendees continued the excitement by heading straight to an after-party 🍻 It felt inspiring to see people from different companies in the mobility industry engage in passionate discussions about current trends and what the near future holds 🔥 Here are some of the impressions shared by attendees in the study session questionnaire: Gathering insights about the mobility industry was highly beneficial. Industry-specific study sessions are great, aren’t they? With everyone focused on mobility, every discussion was incredibly engaging and fascinating to listen to! The information shared was closely related to my field, making me think, "I want to connect with these people again in the future." It had a warm and welcoming vibe, almost like being at home. Mobility Night Going Forward To ensure the continuity of these study sessions, we plan to hold them every other month (during even-numbered months) to start with. Currently, as the event is still in its early stages, only companies invited by the organizing members are giving talks. However, in the future, we aim to foster an environment where anyone feels comfortable stepping up to share, regardless of whether they were specifically invited by the organizers! If you’re interested in giving a talk, feel free to submit your entry via Connpass 🙌 (If we receive a large number of entries, speakers may be selected through a lottery.) We’re also sharing the latest updates about Mobility Night on Discord. If you’d like to participate in Mobility Night, are interested in giving a talk but want to discuss it with us beforehand, want to join as an organizing member, or are curious about it in any other way, please feel free to join us on Discord! Personally, I hope it becomes a space not only for sharing information about Mobility Night but also for initiatives like finding collaborators to co-host mobility-related study sessions. https://discord.gg/nn7QW5pn8B Notification about the Next Session We are going to hold Mobility Night #1! The schedule is as follows: https://mobility-night.connpass.com/event/334400/ Date and time December 5, 2024 (Thu), starting at 6:30 p.m. Theme GPS and location information Venue KINTO Technologies Muromachi Office We welcome everyone interested in the mobility industry to join us, whether or not they are currently part of mobility-related companies or organizations! Spots are limited, so if you’re interested, be sure to apply soon! We genuinely look forward to seeing you there.
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の16日目の記事です🎅🎄 学びの道の駅の中西です。今年は学びの道の駅プロジェクトが立ち上がり組織化されました。そんな中、社内Podcastの運営も行っており、今年のアドベントカレンダーではその内容もお届けしたいと思います。 「学びの道の駅」とは? 「学びの道の駅」は、社内で頻繁に開催される勉強会をもっと便利に、そして効果的に活用するために立ち上げられたプロジェクトです。社内の有志が中心となり勉強会の開催を支援し社内の知識共有を促進することを目的としています。 10X innovation Culture program KTC学びの道の駅ポッドキャストでは、社内の勉強会を開催している方にインタビューを行っています。その名も「突撃!となりの勉強会」。本日のポッドキャストのゲストはGoogleが提供している 10X innovation Culture programご担当の粟田さんとHOKAさんにお越しいただきました。いつもはHOKAさんがインタビューをされているのですが、今日は私、明田がインタビューをします。では早速ですが、お二人にインタビューを進めていきます。 インタビュー 粟田さん: よろしくお願いします。普段の業務は、データベースを軸にオペレーションを誰でもできるようにするプラットフォームエンジニアリングを担当しています。それとは別に、企業カルチャーに興味があるので、いろいろな活動をしています。 HOKAさん: 普段は人事グループの組織人事チームで仕事をしています。研修の企画や実行、入社後面談や全社員面談を通じて、研修のニーズや課題を把握し、企画を行っています。 明田さん: 今回の勉強会の開催のきっかけを教えてください。 粟田さん: Google Cloudのエンタープライズユーザー会「 Jagu'e'r 」の一環で、企業カルチャーとイノベーションを考える分科会に参加していました。その活動の中で10X innovation Culture programをやってみようと考え、有志で15人ほど集めて実施しました。その中にHOKAさんもいて、これがきっかけで広がっていきました。 HOKAさん: その通りです。Googleのオフィスで初めて開催したとき、参加者の反応は非常に良かったです。普段接点がない方々とも一緒にワークショップを行うことで、新たなコミュニケーションの場が生まれました。 明田さん: 次に、開催の詳細についてお伺いします。最初の開催からどのように広がっていったのでしょうか? 粟田さん: 最初はGoogleオフィスで開催し、その後社内での開催に移行しました。社内での開催でも多くの参加者が集まり、非常に前向きな反応をいただきました。 HOKAさん: KTCの社員が前向きに取り組む姿勢を見て、Googleの方々からも好評をいただきました。今後もこのプログラムを社内外に広げていければと考えています。 明田さん: 将来の展望について教えてください。 粟田さん: 将来的には、認定ファシリテーター資格を取得し、他の企業にも10Xを広めていけるようにしたいと考えています。 HOKAさん: まずは社内の他のグループにも展開し、KTC全体でイノベーションカルチャーを醸成していきたいと思っています。 明田さん: KTCをどんな組織にしていきたいですか? 粟田さん: 枠にとらわれない柔軟な発想とコミュニケーション、コラボレーションが活発な組織にしていきたいです。 HOKAさん: 失敗を恐れずに挑戦できるカルチャーを作りたいですね。そのために10Xのメソッドを活用していきたいです。 明田さん: 最後に、これを聞いている皆さんにメッセージをお願いします。 粟田さん: カルチャーは押し付けられるものではなく、自分の行動から生まれるものです。興味がある方はぜひ一緒にやりましょう。 HOKAさん: 興味がある方は見学からでもいいので、ぜひご連絡ください。 10X innovation Culture programを通じて、KTCがより働きやすい組織になることを期待しています。興味を持った方は粟田さんやHOKAさんにご連絡ください。 今回は10X innovation Culture programの詳細と、その運営の背景、今後の展望についてお届けしました。次回の勉強会も楽しみにしてください!
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の16日目の記事です🎅🎄 1. はじめに こんにちは。 モバイルアプリ開発グループ 沖田です。 師走ですね!年の瀬ですね! 思い返せばいろんな出来事がありました。 強く印象に残っていることの1つは、 Osaka Tech Lab で、Developers Summit 2024 KANSAI に参加したことです。 というわけで、今回は、 Developers Summit 2024 KANSAI 参加し、登壇したこと Osaka Tech Labの近況 について、お届けします♪ 2. 登壇前の私の気持ち 弊社は、スポンサーとして、ブース出展と登壇をする機会をいただきました。 ※ 詳しくはこちらのテックブログをご覧ください^^ Developers Summit 2024 KANSAI 振り返り 一緒に未来を描く仲間を募りたい! そのために、KINTOテクノロジーズ株式会社を知ってもらいたい! Osaka Tech Labを知ってもらいたい! そんな想いから、スポンサーに応募しました。 Osaka Tech Lab にとって、初めての試みです。 そして、わたくし、なんと、 スポンサー枠の登壇という大役を仰せつかりました。 できるかな、、登壇とかしたことないんだけど、私にできるのかな、、 と不安がいっぱい。 でもでも、せっかく頂いた貴重な機会。 やれるだけやってみよう!と思い切ってチャレンジすることにしました。 3. 登壇の当日 - 緊張と感動の瞬間 そのような中、迎えた当日。 実は、ストーリーは当日の午前中まで直し、 ギリギリの時間まで、練習を重ねる事態となっておりました w そして、緊張のあまり、早めに会場入りし、うろうろして過ごす。 そうこうしているうちに、 刻々と時計の針は進み、出番がやってきました。 登壇のテーマは " チャレンジ " です。 いまさらジタバタしても仕方がない。 少しでも多くの人に届けるために、落ち着いてゆっくり丁寧に話そう。 そう思って、ゆっくりと話し始めました。 スピーチが終盤に差し掛かったころ、 ふと、顔を上げると、 黒のTシャツを着た人たちが視界に入りました。 なんと、弊社のメンバーが、 KINTOテクノロジーズのTシャツを着て、登壇を見守ってくれていたんです。 なんかもう感動しちゃって。 他社さんがオレンジやイエロー、ブルーなど、鮮やかなカラーのTシャツで参加されている中、 弊社は目立ちづらい黒。 それがまた、Osaka Tech Lab らしいなと、 クスっと笑えてリラックスすることができました。 登壇準備がなかなか捗らない様子を、そっと見守ってくれていたOsaka Tech Lab所属のメンバー 登壇未経験の私の立ち上がりをサポートし、プロフィールや登壇内容を一緒に考えてくれた他拠点所属のメンバー 登壇の数日前に「登壇準備がうまくできません」と駆け込まれ、突然フォローに追われた、モバイルアプリ開発グループのマネージャー陣やチームリーダー そうです。 語り手が私だったというだけで、いろんな人の想いが詰まって完成した登壇だったのです。 そのおかげもあり、 当日の登壇には、174名の方がご参加くださいました。 イベントの運営スタッフのみなさま、サポートしてくれた弊社のメンバー、ならびにセッションに参加してくださったみなさまに、 この場を借りて、御礼申し上げます。 4. 登壇後の私の気持ち 登壇を終えて振り返ったことをここに綴ろうと思います。 1ヶ月以上前から準備を始めたけど、直前までバタバタだった  → 良い経験にはなりましたが、少し悩みすぎてしまったため、もっと効率よくやる方法はあったと思います。 初めての登壇ということもあり、勝手がわからなかった  → 最初に、登壇の目的やゴール、伝えたいことを明確にしてから、登壇資料作成をすれば良かったと思います。 練習では何度やっても20分にしかならなかったけど、本番はちょうど30分で話すことができた リモコンの上下を逆に持っていて、ボタン押しても次のページに行かなくて、初っ端から焦った  資料は、pptで作成して、pdf出力したものを使用した  → こだわって選んだフォントが、pdfには反映されませんでした。悲しかったですがこれも良い教訓になりました。 上記のように、準備はとても大変でしたが、 結果として、後悔のない登壇をすることができました。 同じくスポンサーとして参加されていた他の企業様や、 Ask the Speaker でお声掛け下さった方々との新たな出会いもありました。 そして、なんと、 CodeZine編集部様がセッションレポートをリリースしてくださるという ご褒美までいただきました。 トヨタグループで挑戦するPM兼モバイルアプリエンジニア、Osaka Tech Labで働く魅力とは? 本当に、感無量であります。 5. 登壇のまとめ - 学びと成長 簡単にですが、今回の登壇のついてのまとめです。 ■良かったこと・成長できたこと 登壇がきっかけで、日頃の取り組みを整理・言語化することができた 結果として、所属プロジェクトの次のステップが明確になり、私自身もレベルアップすることができた   → "インプットとアウトプット"、"言語化すること"の意義を体感することができました。 ■改善点 人生で初めての登壇が30分枠というのは少々荷が重かった   → 5分から10分枠の外部登壇を先に経験しておくのがおすすめです♪ 想いを伝えることができた一方で、一歩踏み込んだ具体的な事例は乏しかったように思う   → ご参加いただいた方に何か1つでも持って帰って頂けるような登壇を目指していきたいです。 登壇資料・ストーリーをつくる前に、登壇のポイントを明確にすること   → 先に軸をつくっておくと、ブレないストーリーを描きやすく、効率的に準備を進めることができます。 (サンプル)登壇のポイント ■ 登壇の目的 ■ ゴール (今回の登壇で、なにが出来ればOKか) ■ 伝えたいメッセージ 6. Osaka Tech Labの近況 さてさて。 その後、あっという間に時は経ち、12月になりました。 Developers Summit 2024 KANSAI 後、 Osaka Tech Lab に新たな動きが生まれています。 それは、、 外部イベントの開催に チャレンジ です! 第1弾はこちらでした。 "関西フロントエンド忘年会2024 HACK.BAR × KINTOテクノロジーズ" 今後、モバイルアプリ開発グループのイベントも企画してみたいなと ココロの中で思っています。 他社さんとコラボ開催するのも、たのしそうです♪ チャレンジしたいことリストが、あふれ返って困ってしまいますね w 一歩ずつ(いや、半歩ずつかもしれないけどw)、 歩みを進めていきたく思います。 7. おわりに いかがでしたでしょうか。 いまはまだ見ぬ未来に向かって、 Osaka Tech Lab の 軌跡を一緒に描いていきませんか!? ご応募、お待ちしています! KINTOテクノロジーズ株式会社採用TOP wantedly
アバター
Prerequisites and Scope In this post, we will explore how to handle Android Compose navigation from an object-oriented perspective. We will cover methods to encapsulate underlying technologies and develop features that enhance development convenience and preventing lock-in to specific technologies. Through this, we can not only write high-quality navigation code for Android Compose apps but also acquire the chain-of-thought of object-oriented development. This document assumes an MVVM architecture using Dagger / Hilt. This document only delivers data through the navigation route → ViewModel → UI path, NOT navigation route → UI. This document requires prior knowledge of the Android Navigation back stack. This document does not consider UX issues such as rapid consecutive clicks on navigation buttons. This document does not cover deep links. This document does not cover handling back gestures supported at the Android OS level. This document does not cover strict argument validation or performance optimization. The terms and names used in this document are arbitrary and may differ from established technical or academic terminology. This article is the 16th day of the KINTO Technologies Advent Calendar 2024 . 🎅🎄 Navigation Types Detail When navigating to a screen that provides detailed information, the transition typically involves moving from a general list to a specific item screen, such as from a news feed to a news detail or from a menu to a menu item. Each navigation action increases the back stack by one. You can return to the previous screen by removing( pop ) the top of the back stack. You can call the NavController.navigate function with the route string, like navController.navigate("sub3_1") . @Composable fun NavigationGraph() { val navController = rememberNavController() NavHost(navController, "splash") { composable("main3") { val viewModel: Main3ViewModel = hiltViewModel() Main3Screen( id = viewModel.id, navMain1 = { /* ... */ }, navMain2 = { /* ... */ }, navMain3 = { /* ... */ }, navSub31 = { navController.navigate("sub3_1") }, navSub32 = { navController.navigate("sub3_2") }, navSub33 = { navController.navigate("sub3_3") } ) } } } Switching In this type of navigation, the user perceives the content change within the same screen rather than navigating to a different screen. This is typically used with tabs or a bottom navigation bar. In the case of a bottom navigation bar, the back stack height does not change. The bottom of the back stack must always be one of Main#1 , Main#2 , or Main#3 . To remove itself from the back stack, a popUpTo call is required, and saving and restoring the UI state may be necessary as needed. @Composable fun NavigationGraph() { val navController = rememberNavController() NavHost(navController, "splash") { composable("main3") { val viewModel: Main3ViewModel = hiltViewModel() Main3Screen( id = viewModel.id, navMain1 = { navController.navigate("main1") { popUpTo("main1") { inclusive = true saveState = true } launchSingleTop = true restoreState = true } }, navMain2 = { navController.navigate("main2") { popUpTo("main2") { inclusive = true saveState = true } launchSingleTop = true restoreState = true } }, navMain3 = { navController.navigate("main3") { popUpTo("main3") { inclusive = true saveState = true } launchSingleTop = true restoreState = true } }, navSub31 = { /* ... */ }, navSub32 = { /* ... */ }, navSub33 = { /* ... */ } ) } } } One-Way This type of navigation involves moving to a screen from which you cannot return to the previous screen. It removes( pop ) itself from the back stack and adds( push ) the destination screen. Examples include cases where you cannot return to the form screen after submitting a form or navigating away from a splash screen. If you only need to prevent returning to itself, you can simply handle it with popBackStack , but if necessary, you may need to use popUpTo to remove multiple screens from the back stack. @Composable fun NavigationGraph() { val navController = rememberNavController() NavHost(navController, "splash") { composable("splash") { val viewModel: SplashViewModel = hiltViewModel() SplashScreen( timeout = viewModel.timeout, navMain1 = { navController.popBackStack() navController.navigate("main1") } ) } composable("transactional3") { val viewModel: Transactional3ViewModel = hiltViewModel() Transactional3Screen( onClickSave = { /* ... */ }, onClickSubmit = { viewModel.onClickSubmit { navController.navigate("transactional1") { popUpTo("sub1") { inclusive = true } } } } ) } } } Transactional(Split) This type of navigation involves splitting a very complex or long single screen into multiple steps. It is used to improve UX by reducing user stress when there is a lot of information to convey or actions to request from the user. The user can exit the flow midway, but must start from the beginning when re-entering. If the user completes the task or navigates away from the flow, the entire flow is removed from the back stack. This approach can be combined with one-way navigation to prevent users from abandoning complex UI, include forms. Within the flow, you can freely navigate back and forth, but when exiting the flow, you need to popUpTo to remove all screens of the flow from the back stack. @Composable fun NavigationGraph() { val navController = rememberNavController() NavHost(navController, "splash") { composable( route = "transactional1", arguments = listOf( navArgument("draft") { type = NavType.IntType } ) ) { val viewModel: Transactional1ViewModel = hiltViewModel() Transactional1Screen( id = viewModel.id, onClickBack = { viewModel.onClickBack { navController.popBackStack() } }, onClickSave = { viewModel.onClickSave { navController.navigate("sub2") { popUpTo("transactional1") { inclusive = true } } } }, onClickNext = { navController.navigate("transactional2") } ) } } } Navigation Management We will explain how to manage navigation in an object-oriented manner using the Sub#1 screen and related transitions, highlighted in red. We will introduce object-oriented elements step by step. The Sub#1 screen has the following conditions: The Sub#1 screen requires either the draft parameter or the combination of param1 , param2 , param3 , and param4 parameters to open. To navigate from the Sub#1 screen to the Sub#2 screen, you must wait until the long-running save function completes. To navigate from the Sub#1 screen to the Transactional#1 screen, you must wait until the long-running start function completes. The ViewModel should be independent of the navigation graph or UI. It should use the arguments received from the navigation route directly or use them to fetch data and set properties required by the UI. @HiltViewModel class Sub1ViewModel @Inject constructor( handle: SavedStateHandle, private val draftModel: DraftModel ) : ViewModel() { val id: UUID = UUID.randomUUID() val draft: Draft? = handle.get<Int?>("draft")?.let { runBlocking { if (null != draft && 0 < draft) { draftModel.get(draft)!!.also { param1 = it.param1 param2 = it.param2 param3 = it.param3 param4 = it.param4 } } } } var param1: String? = handle["param1"] private set var param2: String? = handle["param2"] private set var param3: String? = handle["param3"] private set var param4: String? = handle["param4"] private set fun onClickSave(callback: () -> Unit) { viewModelScope.launch { // Long save task. delay(2_000) callback() } } fun onClickStart(callback: () -> Unit) { viewModelScope.launch { // Long start task. delay(2_000) callback() } } } Using Only the Basic Features of the Guide Document Based on the navigation written using the Design your navigation graph / Minimal example guide, the NavigationGraph has the following roles: Registering screens. Declaring and registering the list of parameters for screen routes. Creating route strings for screen navigation. Encapsulating NavController.navigate . Executing the screen's UI and passing the encapsulated NavController.navigate logic. /** * Guide document style navigation graph. */ @Composable fun NavigationGraph() { val navController = rememberNavController() NavHost(navController, "splash") { composable( route = "sub1?${ listOf( "draft={draft}", "param1={param1}", "param2={param2}", "param3={param3}", "param4={param4}" ).joinToString("&") }", arguments = listOf( navArgument("draft") { type = NavType.IntType defaultValue = 0 }, navArgument("param1") { type = NavType.StringType nullable = true }, navArgument("param2") { type = NavType.StringType nullable = true }, navArgument("param3") { type = NavType.StringType nullable = true }, navArgument("param4") { type = NavType.StringType nullable = true } ) ) { Sub1Screen( navBack = navController::popBackStack, navSub2 = { navController.navigate("sub2") { popUpTo("sub1") { inclusive = true } } }, navTransactional1 = { draft -> if (null == draft) { navController.navigate("transactional1") } else { navController.navigate("transactional1?draft=${draft.id}") } } ) } } } /** * Bridge between navigation(encapsule navigation), `ViewModel`(state hoisting) and UI. */ @Composable fun Sub1Screen( viewModel: Sub1ViewModel = hiltViewModel(), navBack: () -> Unit = {}, navSub2: () -> Unit = {}, navTransactional1: (Draft?) -> Unit = {} ) { Sub1Content( id = viewModel.id, param1 = viewModel.param1!!, param2 = viewModel.param2!!, param3 = viewModel.param3!!, param4 = viewModel.param4!!, onClickBack = navBack, onClickSave = { viewModel.onClickSave(callback = navSub2) }, onClickStart = { viewModel.onClickStart(callback = { navTransactional1(viewModel.draft) }) } ) } /** * Display state(arguments) only. */ @Composable private fun Sub1Content( id: UUID, param1: String, param2: String, param3: String, param4: String, onClickBack: () -> Unit = {}, onClickSave: () -> Unit = {}, onClickStart: () -> Unit = {} ) { IconButton(onClick = onClickBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, "back") } // ... Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { OutlinedButton( onClick = onClickSave, modifier = Modifier.padding(16.dp) ) { Text("SAVE", style = MaterialTheme.typography.labelLarge) } Button( onClick = onClickStart, modifier = Modifier.padding(16.dp) ) { Text("START", style = MaterialTheme.typography.labelLarge) } } } Ongoing Challenges Long Navigation Graph Code : NavigationGraph code becomes lengthy as the number of screens, arguments for each screen, connected screens and back stack manipulations increase. Mismatch Between Code Order and Execution Order : The order in which the code is written becomes the order in which it is read, which not match the execution order. This discrepancy forces developers to constantly determine whether the code they are reading is relevant to the current context, increasing cognitive overhead and making maintenance difficult. Too Many Parameters in UI Functions : The number of parameters in UI functions ( Sub1Screen ) increases with the number of connected screens. For example, in a settings screen with many detailed settings, the number of parameters grows accordingly. Introducing Navigator The purpose of introducing a navigator is to reduce the number of parameters in UI functions. Navigation-related parameters in UI functions are grouped into a navigator object. Roles of the Navigator: Grouping Screens Navigable from Sub#1 into an Object : The navigator object encapsulates the navigation logic for screens that can navigate from the Sub#1 screen. Creating Navigation route : The navigator object is responsible for generating the route strings required for navigation. Manipulating the Back Stack : The navigator object handles back stack operations such as popping and pushing screens to ensure proper navigation flow. @Immutable class Sub1Navigator( private val navController: NavController ) { fun back() { navController.popBackStack() } fun sub2() { navController.navigate("sub2") { popUpTo("sub1") { inclusive = true } } } fun transactional1(draft: Drift? = null) { if (null == draft) { navController.navigate("transactional1") } else { navController.navigate("transactional1?draft=${draft.id}") } } } The NavigationGraph has become simpler by introducing the Navigator , which separates the navigation route creation and back stack manipulation code. composable( route = "sub1?${ listOf( "draft={draft}", "param1={param1}", "param2={param2}", "param3={param3}", "param4={param4}" ).joinToString("&") }", arguments = listOf( navArgument("draft") { type = NavType.IntType defaultValue = 0 }, navArgument("param1") { type = NavType.StringType nullable = true }, navArgument("param2") { type = NavType.StringType nullable = true }, navArgument("param3") { type = NavType.StringType nullable = true }, navArgument("param4") { type = NavType.StringType nullable = true } ) ) { Sub1Screen(navigator = remember(navController) { Sub1Navigator(navController) }) } The Sub1Screen has fewer parameters by grouping navigation-related parameters into a single object, making the call code clearer by encapsulating it as object members. @Composable fun Sub1Screen( navigator: Sub1Navigator, viewModel: Sub1ViewModel = hiltViewModel() ) { Sub1Content( id = viewModel.id, param1 = viewModel.param1!!, param2 = viewModel.param2!!, param3 = viewModel.param3!!, param4 = viewModel.param4!!, onClickBack = navigator::back, onClickSave = { viewModel.onClickSave(callback = navigator::sub2) }, onClickStart = { viewModel.onClickStart(callback = { navigator.transactional1(viewModel.draft) }) } ) } Ongoing Challenges Although the UI functions have been simplified, the navigation graph remains complex and developers still experience continuous context switching due to the following roles: Registering screens. Declaring and registering the list of parameters for screen routes. Navigator + companion object To simplify the NavigationGraph function and centralize navigation information, you can use a companion object within the navigator class. This approach allows you to define routes and arguments in one place. Here's how you can do it : Define the routes and arguments in the companion object of the navigator class. Use these definitions in the NavigationGraph function. @Immutable class Sub1Navigator( private val navController: NavController ) { @Suppress("MemberVisibilityCanBePrivate") companion object { const val ARG_DRAFT = "draft" const val ARG_PARAM1 = "param1" const val ARG_PARAM2 = "param2" const val ARG_PARAM3 = "param3" const val ARG_PARAM4 = "param4" const val ROUTE = "sub1?" + "$ARG_DRAFT={draft}&" + "$ARG_PARAM1={param1}&" + "$ARG_PARAM2={param2}&" + "$ARG_PARAM3={param3}&" + "$ARG_PARAM4={param4}" val ARGUMENTS = listOf( navArgument(ARG_DRAFT) { type = NavType.LongType defaultValue = 0 }, navArgument(ARG_PARAM1) { type = NavType.StringType nullable = true }, navArgument(ARG_PARAM2) { type = NavType.StringType nullable = true }, navArgument(ARG_PARAM3) { type = NavType.StringType nullable = true }, navArgument(ARG_PARAM4) { type = NavType.StringType nullable = true } ) } // ... } By using the companion object in the navigator class, the navigation graph can be simplified to focus on screen registration and UI function calls. The UI function remains unchanged. composable( route = Sub1Navigator.ROUTE, arguments = Sub1Navigator.ARGUMENTS ) { Sub1Screen(navigator = remember(navController) { Sub1Navigator(navController) }) } Ongoing Challenges Considering only the Sub#1 screen, this is sufficient. However, expanding the scope to include the navigators for Main#1 and Sub#2 , which need to navigate to the Sub#1 screen, is as follows: @Immutable class Main1Navigator( private val navController: NavController ) { // ... fun sub1(item: Main1Item) { navController.navigate("sub1?param1=${item.param1}&param2=${item.param2}&param3=${item.param3}&param4=${item.param4}") } } @Immutable class Sub2Navigator( private val navController: NavController ) { // ... fun sub1(draft: Draft) { navController.navigate("sub1?draft=${draft.id}") { popUpTo(Main2Navigator.ROUTE) { inclusive = true } } } } Sub1Navigator manages valid route formats, but the actual route composition is handled by Main1Navigator and Sub2Navigator , resulting in an inconsistent state where the responsibility for the Sub#1 route is distributed to users rather than being centralized in Sub#1 . Shared Destination Handling It is reasonable to manage valid route formats and the logic for composing valid route values together. By moving the route creation logic to each companion object , it can be standardized. Encapsulate the route itself and composition. Implement object-based navigation. @Immutable class Sub1Navigator( private val navController: NavController ) { companion object { // ... const val ARG_DRAFT = "draft" const val ARG_PARAM1 = "param1" const val ARG_PARAM2 = "param2" const val ARG_PARAM3 = "param3" const val ARG_PARAM4 = "param4" fun route(item: Main1Item) = "sub1?$ARG_PARAM1=${item.param1}&$ARG_PARAM2=${item.param2}&$ARG_PARAM3=${item.param3}&$ARG_PARAM4=${item.param4}" fun route(draft: Draft) = "sub1?draft=${draft.id}" } // ... } @Immutable class Main1Navigator( private val navController: NavController ) { // ... fun sub1(item: Main1Item) { navController.navigate(Sub1Navigator.route(item)) } } @Immutable class Sub2Navigator( private val navController: NavController ) { // ... fun sub1(draft: Draft) { navController.navigate(Sub1Navigator.route(draft)) { popUpTo(Main2Navigator.ROUTE) { inclusive = true } } } } Ongoing Challenges When registering a screen, using the same class for route , arguments , and navigator instance creation is crucial. If a mistake occurs, it can lead to logical errors such as : Inconsistent Route Definitions : If the route and arguments are not defined consistently, the navigation may fail or behave unexpectedly. Incorrect Navigator Instance : Using a different navigator instance can lead to navigation logic errors, causing the app to navigate to incorrect screens or fail to navigate. composable( route = Main1Navigator.ROUTE, arguments = Transactional1Navigator.ARGUMENTS ) { Sub1Screen(navigator = remember(navController) { Sub1Navigator(navController) }) } Abstracting Navigator, companion object and Standardizing Destination Handling You can abstract the navigator and the navigator's companion object and define the following properties. interface Navigator { val destination: Destination } interface Destination { val routePattern: String val arguments: List<NamedNavArgument> fun route(varargs arguments: Any?): String } If the navigator and companion object each implement Navigator and Destination , the navigation graph configuration is standardized as follows. @Immutable class Sub1Navigator( private val navController: NavController ): Navigator { companion object: Destination { const val ARG_DRAFT = "draft" const val ARG_PARAM1 = "param1" const val ARG_PARAM2 = "param2" const val ARG_PARAM3 = "param3" const val ARG_PARAM4 = "param4" override val routePattern = "sub1?$ARG_DRAFT={draft}&$ARG_PARAM1={param1}&$ARG_PARAM2={param2}&$ARG_PARAM3={param3}&$ARG_PARAM4={param4}" override val arguments = listOf( navArgument(ARG_DRAFT) { type = NavType.LongType defaultValue = 0 }, navArgument(ARG_PARAM1) { type = NavType.StringType nullable = true }, navArgument(ARG_PARAM2) { type = NavType.StringType nullable = true }, navArgument(ARG_PARAM3) { type = NavType.StringType nullable = true }, navArgument(ARG_PARAM4) { type = NavType.StringType nullable = true } ) override fun route(varargs arguments: Any?): String = when { 1 == arguments.size && arguments[0] is Main1Item -> route(arguments[0] as Main1Item) 1 == arguments.size && arguments[0] is Draft -> route(arguments[0] as Draft) else -> throw IllegalArgumentException("Invalid arguments : arguments=$arguments") } fun route(item: Main1Item) = "sub1?$ARG_PARAM1=${item.param1}&$ARG_PARAM2=${item.param2}&$ARG_PARAM3=${item.param3}&$ARG_PARAM4=${item.param4}" fun route(draft: Draft) = "sub1?draft=${draft.id}" } override val destination = Companion } The responsibilities of NavigationGraph are summarized as follows : Creating navigator instances. Connecting the abstracted navigation object with the UI function. Main1Navigator(navController).let { navigator -> composable(navigator.destination.routePattern, navigator.destination.arguments) { Main1Screen(navigator) } } Sub1Navigator(navController).let { navigator -> composable(navigator.destination.routePattern, navigator.destination.arguments) { Sub1Screen(navigator) } } Sub2Navigator(navController).let { navigator -> composable(navigator.destination.routePattern, navigator.destination.arguments) { Sub2Screen(navigator) } } Improving Development Productivity Global Navigation Open a web browser, making a phone call, open app settings, restarting the app and reloading the UI are sometimes necessary regardless of the screen. It is more efficient to share a single implementation of code for these universal functions rather than implementing them individually on each screen where needed. @Immutable class Sub1Navigator( private val navController: NavController ): Navigator { fun web(uri: Uri) { /* Indivisual impl */ } fun call(phoneNumber: String) { /* Indivisual impl */ } } @Immutable class Sub31Navigator( private val navController: NavController ): Navigator { fun web(uri: Uri) { /* Indivisual impl */ } fun settings() { /* Indivisual impl */ } } Solution You can declare common functions in the Navigator interface, implement the universal functions, and then delegate them to the individual screen navigators to achieve commonality. /** * Define common navigation functions. */ interface Navigator { val destination: Destination fun web(uri: Uri) fun call(phoneNumber: String) fun settings() fun reopen() fun restart() } /** * Implement common navigation functions. */ open class BaseNavigator( private val activity: Activity, val navController: NavController ): Navigator { override fun web(uri: Uri) { activity.startActivity(Intent(ACTION_VIEW, uri)) } // ... override fun reopen(){ activity.finish() activity.startActivity(Intent(activity, activity::class.java)) } override fun restart() { activity.startActivity(Intent(activity, activity::class.java)) exitProcess(0) } } /** * Delegate common navigation functions to individual screen navigators. */ @Immutable class Sub1Navigator( private val baseNavigator: BaseNavigator ): Navigator by baseNavigagor { fun sub2() { baseNavigator.navController.navigate(Sub2Navigator.route()) { popUpTo(routePattern) { inclusive = true } } } } @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { NavigationGraph(this@MainActivity) } } } /** * Replace the `NavController` with the base navigator instance. * * @param activity The activity that owns the navigation graph. */ @Composable fun NavigationGraph(activity: Activity) { val baseNavigator = BaseNavigator(activity, rememberNavController()) NavHost(baseNavigator.navController, SplahNavigator.routePattern) { Sub1Navigator(baseNavigator).let { navigator -> // ... } } } Navigation Graph Configuration Utility There is still a risk of logical errors as follows. Sub1Navigator(baseNavigator).let { navigator -> composable(Main1Navigator.routePattern, Transactional1Navigator.arguments) { Sub1Screen(navigator) } } Solution By adding a utility function to handle navigator instances and screen registration, fun <N : Navigator> NavGraphBuilder.composable( navigator: N, content: @Composable AnimatedContentScope.(NavBackStackEntry, N) -> Unit ) { composable( route = navigator.destination.routePattern, arguments = navigator.destination.arguments ) { backStackEntry -> content(backStackEntry, navigator) } } the navigation graph configuration becomes simpler, eliminating the possibility of logical errors. composable(Main1Navigator(baseNavigator)) { _, navigator -> Main1Screen(navigator) } composable(Sub1Navigator(baseNavigator)) { backStackEntry, navigator -> Sub1Screen(navigator) } composable(Sub2Navigator(baseNavigator)) { _, navigator -> Sub2Screen(navigator) } @Preview Support For screens that display static resource in bundle, you can write navigation code by passing only the navigator instead of passing a separate event handler as an argument. Here is an example : @Composable fun StaticResourceListScreen(navigator: StaticResourceListNavigator) { Column { Button(onClick = navigator::static1) { Text("Static#1") } Button(onClick = navigator::static2) { Text("Static#2") } Button(onClick = navigator::static2) { Text("Static#2") } } } @Preview code repeats BaseNavigator(PreviewActivity(), rememberNavController()) for each navigator instance. As the types and number of previews increase, it becomes inconvenient to write previews. And this overhead force developer to skip the preview. @Composable @Preview(showSystemUi = true) fun PreviewStaticResourceListScreen() { MaterialTheme { StaticResourceListScreen(StaticResourceListNavigator(BaseNavigator(PreviewActivity(), rememberNavController()))) } } @Composable @Preview(showSystemUi = true) fun PreviewStatic1Screen() { MaterialTheme { Static1Screen(Static1Navigator(BaseNavigator(PreviewActivity(), rememberNavController()))) } } @Composable @Preview(showSystemUi = true) fun PreviewStatic2Screen() { MaterialTheme { Static2Screen(Static2Navigator(BaseNavigator(PreviewActivity(), rememberNavController()))) } } @Composable @Preview(showSystemUi = true) fun PreviewStatic3Screen() { MaterialTheme { Static3Screen(Static3Navigator(BaseNavigator(PreviewActivity(), rememberNavController()))) } } Solution Create a utility function to instantiate BaseNavigator and implement it to handle real apps and previews separately. @Composable fun baseNavigator( activity: Activity = if (LocalInspectionMode.current) { PreviewActivity() } else { LocalContext.current as Activity } ): BaseNavigator { val navHostController = rememberNavController() val base = remember(activity) { BaseNavigator(activity, navHostController) } return base } @Composable fun NavigationGraph(activity: Activity) { val baseNavigator = baseNavigator(activity) NavHost(navController, SplahNavigator.routePattern) { // ... } } @Composable @Preview(showSystemUi = true) fun PreviewStaticResourceListScreen() { MaterialTheme { StaticResourceListScreen(StaticResourceListNavigator(baseNavigator())) } } @Composable @Preview(showSystemUi = true) fun PreviewStatic1Screen() { MaterialTheme { Static1Screen(Static1Navigator(baseNavigator())) } } @Composable @Preview(showSystemUi = true) fun PreviewStatic2Screen() { MaterialTheme { Static2Screen(Static2Navigator(baseNavigator())) } } @Composable @Preview(showSystemUi = true) fun PreviewStatic3Screen() { MaterialTheme { Static3Screen(Static3Navigator(baseNavigator())) } } Custom Start Screen The NavigationGraph configuration function used so far can change the start screen but can only use a single navigation graph. This means that it is not possible to develop a demo application that utilizes only part of the existing functionality. @AndroidEntryPoint class DemoActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { NavigationGraph(this@DemoActivity) // FIXED start screen. } } } @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { NavigationGraph(this@MainActivity) } } } @Composable fun NavigationGraph(activity: Activity) { val baseNavigator = baseNavigator(activity) NavHost(navController, SplahNavigator.routePattern) { Sub1Navigator(baseNavigator).let { navigator -> composable(navigator.destination.routePattern, navigator.destination.arguments) { Sub1Screen(navigator) } } } } Solution Implement destination in BaseNavigator , then pass the hoisted BaseNavigator instance from the activity to the navigation graph. open class BaseNavigator( private val activity: Activity, val navController: NavController, override val destination: Desination ): Navigator { // ... } /** * @param startDestination Default value available only in preview. */ @Composable fun baseNavigator( activity: Activity = if (LocalInspectionMode.current) { PreviewActivity() } else { LocalContext.current as Activity }, startDestination: Destination = if(LocalInspectionMode.current || activity is PreviewActivity) { object: Destination { override val routePattern = "preview" // ... } } else { throw IllegalArgumentException("When running the app in real mode, you must provide a startDestination.") } ): BaseNavigator { val navHostController = rememberNavController() val base = remember(activity) { BaseNavigator(activity, navHostController, startDestination) } return base } @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { NavigationGraph(baseNavigator(destination = SplashNavigator.Companion)) } } } @Composable fun NavigationGraph(baseNavigator: BaseNavigator = baseNavigator()) { // Implement additional and common navigation features here. NavHost( // Encapsulate the `navigation-compose` dependency in activity(`MainActivity`) and UI(`UiRoot`). baseNavigator.navController, baseNavigator.destination.routePattern ) { // ... } } DEMO App Development Support The navigation graph configuration function( NavigationGraph ) can change the start screen but can only use a single navigation graph & start screen. This means that it is not possible to develop a demo application that use only part of the existing screen and functionality. @Composable fun NavigationGraph(baseNavigator: BaseNavigator = rememberBaseNavigator()) { NavHost( navController = baeNavigator.navController, startDestination = baseNavigator.destination.routePattern ) { // ... } } Solution Separate the navigation graph configuration code from the navigation-compose call. This allows for the separation of navigation graph building, the connection between navigators and UI functions, and common navigation features, while additionally encapsulating dependencies. @Composable fun NavigationGraph(baseNavigator: BaseNavigator = rememberBaseNavigator(), builder: NavGraphBuilder.() -> Unit) { NavHost( navController = baeNavigator.navController, startDestination = baseNavigator.destination.routePattern, builder = builder ) } Production App @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { UiRoot(baseNavigator(destination = SplashNavigator.Companion)) } } } @Composable fun UiRoot(baseNavigator: BaseNavigator) { NavigationGraph(baseNavigator) { // ... composable(Sub1Navigator(baseNavigator)) { _, navigator -> Sub1Screen(navigator) } // ... } } DEMO App @AndroidEntryPoint class DemoActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { DemoUiRoot(baseNavigator(destination = Transactional1Navigator.Companion)) } } } @Composable fun DemoUiRoot(baseNavigator: BaseNavigator) { NavigationGraph(baseNavigator) { composable(Transactional1Navigator(baseNavigator)) { _, navigator -> Transactional1Screen(navigator) } } } Terminology Unification In the guide document , the term screen is used to refer to navigation targets. However, many UI design systems—such as Atomic Design , Carbon Design System , Ant Design , Shopify Polaris — use page to refer to navigation targets, and screen is used to refer to the physical device display. Additionally, when registering a screen in the navigation graph, the function name composable is used to indicate that the @Composable function is called. Changing the developer-centric terminology to consider related fields outside of development can prevent confusion or the need to confirm meanings, ensuring smooth and quick communication. Therefore, it is advisable to unify the terminology. fun <N : Navigator> NavGraphBuilder.composable( navigator: N, content: @Composable AnimatedContentScope.(NavBackStackEntry, N) -> Unit ) { composable( route = navigator.destination.routePattern, arguments = navigator.destination.arguments ) { entry -> content(entry, navigator) } } @Composable fun UiRoot(baseNavigator: BaseNavigator) { NavigationGraph(baseNavigator) { composable(Sub1Navigator(baseNavigator)) { _, navigator -> Sub1Screen(navigator) } } } @Composable fun Sub1Screen(navigator: Sub1Navigator, viewModel: Sub1ViewModel = hiltViewModel()) { // ... } Solution Assuming the adoption of Atomic Design as the design system, unify the terminology by using page instead of screen for navigation targets. fun <N : Navigator> NavGraphBuilder.page( navigator: N, content: @Composable AnimatedContentScope.(NavBackStackEntry, N) -> Unit ) { composable( route = navigator.destination.routePattern, arguments = navigator.destination.arguments ) { entry -> content(entry, navigator) } } @Composable fun UiRoot(baseNavigator: BaseNavigator) { NavigationGraph(baseNavigator) { page(Sub1Navigator(baseNavigator)) { _, navigator -> Sub1Page(navigator) } } } @Composable fun Sub1Page(navigator: Sub1Navigator, viewModel: Sub1ViewModel = hiltViewModel()) { // ... } Conclusion At first, using only the basic features of the guide document, the structure was simple. Introducing object-oriented navigation design results in multiple generic types and functions that are not affected by specific navigation spec, each responsible for specialized functionality. Navigator : Groups navigation related to the UI, defines properties on how it is registered in the navigation graph. Destination : Defines properties for registering pages in the navigation graph. BaseNavigator : Defines the integration between Application , Activity , and the navigation graph, implements common/global navigation. NavGraphBuilder.page : Connects the navigation graph and each page( Navigator ). @Composable fun NavigationGraph : Encapsulates navigation-compose , implements common navigation features, separates common features and the concrete navigation graph. @Composable fun baseNavigator : Provides development convenience. The structure of the generic object is as follows. Using generic objects to construct the navigation graph creates complex structure. Instead, if there are already generic features, the application-level features require less development and fewer considerations when using them, allowing for faster development. Therefore, as more screens are added, the average development cost per screen decreases. Depending on the field, product, development environment and individuals, this design approach may not be necessary. Even if needed, it might have a lower priority and the solutions could differ. The object-oriented design applied in this document is merely one example. However, the recognized issues and their design directions could be applicable to other software as well. The Meaning of Object-Oriented Design Object-oriented design does not necessarily result in short code or simple structure. The code appears shorter or simpler only when looking at already divided code by role. Even when following the guide document, the function that build the entire navigation graph and each screen function are written separately. In an extreme case, all screens can be implemented within the NavigationGraph or without NavigationGraph function; MainActivity can implement all screens while calling setContent . Object-oriented design starts with analyzing what the code does and what responsibilities it requires to achieve that functionality. It then determines what should be grouped together and what should be separated into types or files. Each code connected through import or object references. Finally, it leads to which developer takes the responsibility of maintain the code and overhead of study to handle that code. In the IT industry, developers are very important means of production. Therefore, cost and productivity are determined by how developers spend their time. Thus, a developer's cognitive capacity becomes expensive resource. The developer who has higher technical skills, the deeper understanding of the product and the more knowledge of the work history, the more valuable the developer's focus becomes. The cognitive overhead of determining whether the code being viewed is necessary for the current task consumes the most focus (cognitive capacity) in the least valuable way. Lower focus leads to lower productivity. Applying object-oriented design inevitably results in complex structure and requires deep technical and historical knowledge to properly understand the code. Object-Oriented Design is a development methodology that involves a large amount of development and learning in advance to create an environment where surface-level functionality can be developed quickly and simply. Risks and Costs of Object-Oriented Design Object-oriented design is a development methodology that achieves high productivity based on a large amount of generic functionality and complex structure. However, generic functionality and structure has their limit. Some changes that exceed these limit can occur at any time. It is nearly impossible for planners, managers and designers, who are not developers regularly acquiring and utilizing tacit knowledge (or domain knowledge), to understand why some changes looks so simple but so difficult and time-consuming. Even among developers, those who lack an understanding of tacit knowledge will find it equally incomprehensible. It is not a solution that requires them to acquire tacit knowledge or to request developers to repeatedly explain until they understand. Instead, this problem requires a systematical and cultural solution by the organization or company, rather than an individual one. Especially when object-oriented design is introduced for high productivity in a company or organization, managing conflicts arising from differences in tacit knowledge between developers or between developers and other positions becomes important. And this is where risks (or costs) arise. Of course, this is a problem when object-oriented design is done well. Before that, the issues are who will take responsibility for object-oriented design and how the responsible person will be decided at the organization or company level. If this decision is wrong, the most valuable resource of the organization or company, teamwork will be sacrificed. References Design your navigation graph / Minimal example android/compose-samples > Jetnews > HomeScreens Atomic Design Carbon Design System Ant Design Shopify Polaris
アバター
This article is part of day 12 of the KINTO Technologies Advent Calendar 2024 Introduction Hello everyone! My name is Matsuno, and I am a member of the Platform Group MSP team at KINTO Technologies (my entry from when I first joined can be found here ). In my previous job, I worked for an SES operating company and was in charge of maintenance and operation of systems built on-premise and AWS as an infrastructure engineer. Just as I thought that I wanted to be a little more involved with the system and the people involved with it, I joined KINTO Technologies by chance, and now I am. The reason why I decided to write this article What do you think of when you suddenly hear the MSP? Do you know what it stands for? Embarrassingly, I didn't know the acronym MSP until the recruiter showed me the job posting.... So, in this article, I would like to share what I learned and struggled with since joining KINTO Technologies as a member of the MSP team, as well as what the MSP team is doing! What is MSP? First, let me give you a general description of MSP. MSP stands for "Managed Service Provider" and is introduced in the Gartner Glossary as follows: (Translated by DeepL) Managed Service Providers (MSPs) provide services such as network, application, infrastructure, and security through ongoing, regular support and active management at a customer location, an MSP data center (hosted), or a third-party data center. MSPs may also offer their own services in combination with the services of other providers (for example, Security MSPs provide systems management on top of third-party cloud IaaS). Pure Play MSPs specialize in a single vendor or technology and typically provide their core services. Many MSPs include services from other types of providers. The term MSP used to be applied to infrastructure- and device-centric types of services, but now it includes ongoing routine management, maintenance, and support. Gartner Glossary: Quoted from Managed Service Provider (MSP) "Provide through ongoing, regular support and active management." This is the core idea of MSP. If you look up the term MSP, it seems that there are some differences between businesses, but most of them refer to services that specialize in the maintenance, operation, and monitoring of running systems. MSP team efforts at KINTO Technologies The origins of the MSP team From here on, let’s take a look at what the MSP team is doing within KINTO Technologies! First, the mission of the MSP team at KINTO Technologies. “We will contribute to indirect development speed and quality improvement through application operation support.” In order to understand why this mission came about, I looked back at the time when the MSP team was established. When the concept of the MSP team was first launched, KINTO Technologies faced the following challenges. Since the developer and operator of the system under development are the same, the development speed cannot be increased due to the busy operation work. We are not able to provide support at the same time as the system uptime in response to problems. In order to solve these issues, the MSP team at KINTO Technologies is composed of the following two teams: MSP Service Desk (Outsourced) MSP In-house Team The MSP Service Desk is outsourced and will be a so-called 24h365d capable unit. On the other hand, the MSP in-house team is a relatively new team that was launched in May 2023 and handles the work taken over from each development team within KINTO Technologies during the day on weekdays. Specific responsibilities of the MSP team At KINTO Technologies, various in-house products are being developed every day, and the MSP team mainly handles the following tasks. Account management Account registration and deletion Password resets Account inventory Turnover response Compile and disseminate security reports Support for reruns when data integration batch fails System monitoring alert primary response Response to various inquiries Some of you may be able to imagine from the list above, but the current MSP team mainly deals with systems management and operation that can be handled regularly, those that need to be handled by multiple teams but the response method was not unified, or those that could not be handled. What are we actually doing? Since it might be hard to picture, I’m going to introduce some details of the work that the MSP team is working on. Currently, the MSP team responds to turnover based on HR information published internally every month. The task involves gathering information on employees who are leaving their jobs due to retirement or childcare leave. This includes managing all processes from confirming the existence of accounts on target systems (both in-house and SaaS) to deleting those accounts. As of November 2024, a total of seven systems developed and managed by two groups are responding to turnover. The benefits of having the MSP team handle this task all at once include the following for example. Business standardization Eliminate account management differences between groups and systems. Reduce operating costs and focus on development Reduce operational costs for the team responsible for system development and management. Prevention of work becoming dependent on an individual employee The MSP in-house team creates procedures to ensure that all team members are available. In terms of operational cost reduction, let’s make a specific calculation for the turnover response in the following example. For seven systems, let’s say that the person in charge handles the removal of turnover accounts on a monthly basis. And assume that each task takes about two hours. In that case, the monthly and annual operating costs required overall will be: 2 (monthly workloads) x 7 (number of target systems) = 14 (hours/month) 14 (hours/month) x 12 = 168 (hours/year) This is just an estimate, but it shows that the MSP team handles approximately 150 workloads a year, taking on responsibilities that would otherwise fall to the development team. When I joined KINTO Technologies, the MSP team was already handling employee turnover. Even mid-month, accounts were deleted the next working day after an employee left, which gave me the impression that the team was responding quite efficiently. While there are these advantages, there are also disadvantages of course. You might think of the following: High communication costs Depending on the work, there is an increase in interactions with the team that took over, so it can be difficult to feel the benefits of letting go of the work. Handover risk Due to operational errors by the MSP team, recovery measures are required. Based on these advantages and disadvantages, **how to minimize the disadvantages and increase the advantages at the time of taking over work is the time to show of your skills. ** So far, I have introduced the formation of the MSP team at KINTO Technologies and its specific operations. I wrote some cool things, but I'm still inexperienced, so I'm studying every day. The future of the MSP team What was requested I would like to close this article by talking about what I have been asked to do since joining KINTO Technologies, and about the future of the MSP team! First of all, when I joined KINTO Technologies, I was asked to do the following two things. To grow as an MSP in-house team leader and lead the in-house team To contribute to the expansion of the MSP in-house production team by utilizing my practical experience of system maintenance and operation. These were very challenging for me. This is because the work style up until now was that certain sets of tasks in each time span, such as daily, weekly, and monthly, were required to be output with the same quality without making mistakes. Most of the time, both as an organization and as a system, things were stable. On the other hand, KINTO Technologies is an organization that aims to expand both as an organization and as a business, and of course, the development of new systems is progressing accordingly. As an MSP team whose mission is to improve the speed and quality of development of the development team, we would like to expand our response operations. What I was conscious of as a team leader As I mentioned earlier, I thought I understood what I was asked to do, but it was completely different from doing the work in front of me as an engineer and being a team leader. Up until now, I only had to focus on my own output. However, to grow as a team leader, I need to pay attention to the output of all my team members. Up until now, I only had to focus on my own output. However, to grow as a team leader, I need to pay attention to the output of all my team members. I have gotten used to it with the support of those around me, but I feel that I am learning every day. Expanding the capabilities of the MSP in-house team Lastly, I will contribute to the business expansion of the MSP in-house team by utilizing my practical experience in system maintenance and operation. In the past, I was at a development site that was shrinking as an organization, but the work was becoming more dependent on individual employees. I remember feeling that it was quite difficult to get rid of that dependency, partly because of the aging of the people in charge and the ongoing overload situation. I thought I could do what I could, but there was a limit to what I could do because the workloads were also limited. Based on that experience, I feel that what the MSP team is working on now is useful for KINTO Technologies. I understand what the team is trying to do and the need for it very well, but I don't have the know-how. In order to expand the efforts of the MSP team, I believe that I need to be able to do and practice the following. Think not only from a system perspective but also from a business perspective in order to create an appropriate business design and flow. When taking over work, follow the steps to ensure that the output of the MSP team is consistent. Get to know the MSP team’s efforts within KINTO Technologies. Inherently, I don’t dislike creating documents such as procedure manuals, and I don’t have any opposition to doing routine work, so I am confident that this is appropriate to a certain extent, but I feel that the part of creating work is really difficult.... Up until now, I have been working in a system development-oriented way, so I naturally focus on system specifications and the AWS services we use internally. However, to build a successful business, I need to shift my focus beyond just system specifications and AWS services. “What is our business perspective?” Every day, I face documents while asking myself this question. From the perspective of aligning the output, I understand that the basic procedure is to prepare the workflow, raise a ticket, gather the necessary information, and respond to it. However, we are struggling to organize our business flow. It is difficult to judge which part should be considered properly and which part should not be considered much. I think that if you can practice the above after establishing your own style, you can minimize the risk of business handover and maximize the advantages, but this is quite difficult.... I feel that this is something that cannot be acquired overnight, so I am working on my day-to-day work with the help of my superiors and managers. However, if I can create good work through my work, it will lead to the expansion of the MSP team, so I will do my best. Conclusion In this article, I began by discussing MSP in general and then introduced the efforts of the MSP team from my perspective as an author who joined KINTO Technologies in April 2024. I highlighted its usefulness and shared my future aspirations. Unlike other tech blogs, I don't work on anything technically advanced in my daily work. Therefore, instead of discussing technology or introducing technical problems, I focused on introducing the MSP team at KINTO Technologies from my own subjective perspective. I would be happy if you have become even a little more interested in the MSP team’s efforts.
アバター
Introduction Hello! I'm Yao Xie from the Mobile Application Development Group at KINTO Technologies, where I develop the Android app of KINTO Kantan Moushikomi (KINTO Easy Application) . In this article, I'd like to share how we can use AGSL (Android Graphics Shading Language) to enhance custom UI components and perform advanced image processing in Android apps. What is AGSL AGSL (Android Graphics Shading Language) is a GPU-based shading language designed for Android. Built on Skia Shading Language (SKSL), it offers Android-specific optimizations for creating advanced graphics effects. Fully integrated with the Android rendering pipeline, AGSL enables efficient and seamless implementation of complex visual effects. From GLSL to SKSL to AGSL Graphics shading languages have evolved significantly to meet the increasing demand for high-quality graphics in modern applications. Here's a brief overview: GLSL (OpenGL Shading Language): The original shading language used with OpenGL for rendering 2D and 3D graphics. It allows developers to write custom shaders that run on the GPU. SKSL (Skia Shading Language): Introduced as part of the Skia graphics library, which is used for rendering 2D graphics on various platforms, including Android. AGSL (Android Graphics Shading Language): A shading language specifically designed for Android, building upon the capabilities of SKSL and tailored to integrate seamlessly with the Android rendering pipeline. Key Differences Between GLSL, SKSL, and AGSL AGSL is optimized for mobile devices, providing better performance and lower power consumption compared to GLSL. Its integration with the Android rendering pipeline allows for more efficient graphics rendering. GLSL: C-like syntax for OpenGL. Cross-platform but limited on Android due to OpenGL ES variations. SKSL: Similar to GLSL, optimized for Skia's 2D graphics. Primarily used internally within Skia, making it less accessible for direct Android development. AGSL: Based on SKSL with Android-specific enhancements. Fully integrated with Android's graphics pipeline for optimal performance. How does AGSL work? Below is a hierarchical diagram (from top to bottom) that illustrates where the AGSL shader string fits within Android's graphics rendering system and the data flow process (this diagram is a conceptual representation and not an exact system architecture) Get started Step 1: Define a Gradient Shader Create a shader file with a smooth gradient effect applied only to the text using AGSL. The composable input ensures the gradient respects the text's alpha mask. @Language("AGSL") val gradientTextShader = """ uniform float2 resolution; // Text size uniform float time; // Time for animation uniform shader composable; // Input composable (text mask) half4 main(float2 coord) { // Normalize coordinates to [0, 1] float2 uv = coord / resolution; // Hardcoded gradient colors half4 startColor = half4(1.0, 0.65, 0.15, 1.0); // Orange half4 endColor = half4(0.26, 0.65, 0.96, 1.0); // Blue // Linear gradient from startColor to endColor half4 gradientColor = mix(startColor, endColor, uv.x); // Optional: Add a subtle animation (gradient shifting) float shift = 0.5 + 0.5 * sin(time * 2.0); gradientColor = mix(startColor, endColor, uv.x + shift * 0.1); // Use the alpha from the input composable mask half4 textAlpha = composable.eval(coord); // Combine the gradient color with the composable alpha return gradientColor * textAlpha.a; } """.trimIndent() Step 2: Create a Modifier for the Shader Define a custom Modifier that applies the gradient shader to text. The shader leverages a dynamic time parameter to animate the gradient. fun Modifier.gradientTextEffect(): Modifier = composed { val shader = remember { RuntimeShader(gradientTextShader) } var time by remember { mutableStateOf(0f) } // Increment animation time LaunchedEffect(Unit) { while (true) { time += 0.016f // Simulate 60 FPS delay(16) } } this.graphicsLayer { shader.setFloatUniform("resolution", size.width, size.height) shader.setFloatUniform("time", time) renderEffect = RenderEffect .createRuntimeShaderEffect(shader, "composable") .asComposeRenderEffect() } } Step 3: Apply the Shader to a Text Component Use the Modifier.gradientTextEffect() in your UI to apply the gradient effect. @Composable fun GradientTextDemo() { Box( modifier = Modifier .fillMaxSize() .padding(16.dp), contentAlignment = Alignment.Center ) { Text( text = "Gradient Text", fontSize = 36.sp, fontWeight = FontWeight.Bold, color = Color.White, modifier = Modifier.gradientTextEffect() ) } } Result: Is That All? What Else Can AGSL Do? AGSL's capabilities extend far beyond the basics, empowering developers to craft dynamic, visually appealing, and high-performance app experiences. Let’s explore additional ways AGSL can elevate your app with real-world examples. 1. Enhance UI Components AGSL allows you to create captivating UI elements that engage users and reinforce your app’s purpose. Animated Borders: Create marquee or pulsating effects around cards, buttons, or images. Custom Gradients: Implement animated, GPU-accelerated gradients that flow dynamically. Dynamic Glow Effects: Add glowing highlights or halos to buttons and sliders. Example: Driving Skill Training App Imagine you’re building a Driving Skill Training App. The goal is to make the interface visually engaging to encourage users to interact with key elements, like a "Start Training" button. Here’s how AGSL can help on providing Dynamic Glow Effect: AGSL Shader Code: @Language("AGSL") val glowButtonShader = """ // Shader for a glowing rounded rectangle button uniform shader button; // Input texture or color for the button uniform float2 size; // Button size uniform float cornerRadius; // Corner radius of the button uniform float glowRadius; // Radius of the glow effect uniform float glowIntensity; // Intensity of the glow layout(color) uniform half4 glowColor; // Color of the glow // Signed Distance Function (SDF) for a rounded rectangle float calculateRoundedRectSDF(vec2 position, vec2 rectSize, float radius) { vec2 adjustedPosition = abs(position) - rectSize + radius; // Adjust for rounded corners return min(max(adjustedPosition.x, adjustedPosition.y), 0.0) + length(max(adjustedPosition, 0.0)) - radius; } // Function to calculate glow intensity based on distance float calculateGlow(float distance, float radius, float intensity) { return pow(radius / distance, intensity); // Glow falls off as distance increases } half4 main(float2 coord) { // Normalize coordinates and aspect ratio float aspectRatio = size.y / size.x; float2 normalizedPosition = coord.xy / size; normalizedPosition.y *= aspectRatio; // Define normalized rectangle size and center float2 normalizedRect = float2(1.0, aspectRatio); float2 normalizedRectCenter = normalizedRect / 2.0; normalizedPosition -= normalizedRectCenter; // Calculate normalized corner radius and distance float normalizedRadius = aspectRatio / 2.0; float distanceToRect = calculateRoundedRectSDF(normalizedPosition, normalizedRectCenter, normalizedRadius); // Get the button's color half4 buttonColor = button.eval(coord); // Inside the rounded rectangle, return the button's original color if (distanceToRect < 0.0) { return buttonColor; } // Outside the rectangle, calculate glow effect float glow = calculateGlow(distanceToRect, glowRadius, glowIntensity); half4 glowEffect = glow * glowColor; // Apply tone mapping to the glow for a natural look glowEffect = 1.0 - exp(-glowEffect); return glowEffect; } """.trimIndent() Result: The button can feature a glowing halo that pulsates, drawing attention and mimicking the feel of car lights. https://youtube.com/shorts/CW1yBgJyDo4?rel=0 2. Perform Advanced Image Processing AGSL excels in real-time image manipulation, enabling effects that are both dynamic and interactive. AGSL empowers developers to create high-performance, GPU-accelerated image processing effects. Custom Filters: Implement artistic effects like sepia, pixelation, or vignette. Dynamic Blur: Apply real-time blur effects like motion blur or depth-of-field effects. Color Adjustments: Adjust brightness, contrast, or saturation dynamically in the UI. Example: Ripple Effect for an Image Imagine an image of the moon in your app. You want to add a ripple effect to simulate the moon’s reflection on water, making the interface more interactive and visually interesting. AGSL Shader Code: @Language("AGSL") val rippleShader = """ // Uniform variables: inputs provided from the outside uniform float2 size; // The size of the canvas in pixels (width, height) uniform float time; // The elapsed time for animating the ripple effect uniform shader composable; // The shader applied to the composable content being rendered // Main function: calculates the final color at a given fragment (pixel) coordinate half4 main(float2 fragCoord) { // Scale factor based on the canvas width for normalization float scale = 1 / size.x; // Normalize fragment coordinates float2 scaledCoord = fragCoord * scale; // Calculate the center of the canvas in normalized coordinates float2 center = size * 0.5 * scale; // Calculate the distance from the current fragment to the center float dist = distance(scaledCoord, center); // Calculate the direction vector from the center to the fragment float2 dir = scaledCoord - center; // Apply a sinusoidal wave based on the distance and time float sin = sin(dist * 70 - time * 6.28); // Offset coordinates by applying the wave effect in the direction of the fragment float2 offset = dir * sin; // Calculate the texture coordinates with the ripple effect applied float2 textCoord = scaledCoord + offset / 30; // Sample the composable shader using the adjusted texture coordinates return composable.eval(textCoord / scale); } """.trimIndent() Result: With minimal performance cost, this shader adds depth and elegance to your app's imagery. https://www.youtube.com/shorts/80QOTzNUHLg?rel=0 3. Enable Procedural Graphics Procedural graphics are perfect for crafting visually engaging interfaces without relying on static assets. Pattern Generation: Create procedural textures like stripes, grids, or noise. Shape Animations: Design morphing shapes or moving patterns. 3D-Like Effects: Simulate depth and perspective without actual 3D rendering. Example: Animated Loading Screen Loading screens are often mundane, but AGSL can turn them into dynamic works of art. For instance, you can create a shining, animated sphere that captivates users while the app loads. AGSL Shader Code: @Language("AGSL") val lightBallShader = """ uniform float2 size; // The size of the canvas in pixels (width, height) uniform float time; // The elapsed time for animating the light effect uniform shader composable; // Shader for the composable content half4 main(float2 fragCoord) { // Initialize output color float4 o = float4(0.0); // Normalize coordinates relative to the canvas center float2 u = fragCoord.xy * 2.0 - size.xy; float2 s = u / size.y; // Loop to calculate the light ball effect for (float i = 0.0; i < 180.0; i++) { float a = i / 90.0 - 1.0; // Calculate a normalized angle float sqrtTerm = sqrt(1.0 - a * a); // Circular boundary constraint float2 p = cos(i * 2.4 + time + float2(0.0, 11.0)) * sqrtTerm; // Oscillation term // Compute position and adjust with distortion float2 c = s + float2(p.x, a) / (p.y + 2.0); // Calculate the distance factor (denominator) float denom = dot(c, c); // Add light intensity with color variation float4 cosTerm = cos(i + float4(0.0, 2.0, 4.0, 0.0)) + 1.0; o += cosTerm / denom * (1.0 - p.y) / 30000.0; } // Return final color with an alpha of 1.0 return half4(o.rgb, 1.0); } """.trimIndent() Result: This shader adds a futuristic, polished look to your app’s loading screen, making the wait feel shorter and more engaging. https://youtube.com/shorts/pUTU0KRmFek?rel=0 4. Boost App Performance AGSL shines in performance-heavy scenarios, offloading rendering tasks to the GPU for smooth, efficient animations. Efficient Animations: Handle complex, real-time effects smoothly. Battery Optimization: Achieve visually stunning effects with minimal power usage. Example: Weather Animation on a Map View Imagine your product manager asks you to create a weather animation overlay on a map view. Traditional methods are performance-intensive, but GSL achieves efficient GPU rendering by minimizing CPU overhead and leveraging Android's optimized rendering pipeline. AGSL Shader Code for rain: @Language("AGSL") val rainShader = """ uniform float time; // The elapsed time for animating the rain uniform float2 size; // The size of the canvas in pixels (width, height) uniform shader composable; // Shader for the composable content // Generate a pseudo-random number based on input float random(float st) { return fract(sin(st * 12.9898) * 43758.5453123); } half4 main(float2 fragCoord) { // Normalize fragment coordinates to the [0, 1] range float2 uv = fragCoord / size; // Rain parameters float speed = 1.0; // Speed of raindrops float t = time * speed; // Time-adjusted factor for animation float density = 200.0; // Number of rain "drops" per unit area float length = 0.1; // Length of a raindrop float angle = radians(30.0); // Angle of the rain (in degrees) float slope = tan(angle); // Slope of the rain's trajectory // Compute grid position and animated raindrop position float gridPosX = floor(uv.x * density); float2 pos = -float2(uv.x * density + t * slope, fract(uv.y - t)); // Calculate the raindrop visibility at this fragment float drop = smoothstep(length, 0.0, fract(pos.y + random(gridPosX))); // Background and rain colors half4 bgColor = half4(0.0, 0.0, 0.0, 0.0); // Black transparent background half4 rainColor = half4(0.8, 0.8, 1.0, 1.0); // Light blue raindrop color // Blend the background and raindrop color based on drop visibility half4 color = mix(bgColor, rainColor, drop); return color; // Output the final color for the fragment } """.trimIndent() Result: This shader effectively simulates rain, with support for clouds and snow as well (code for the latter two is excluded here for brevity), while maintaining smooth performance, even on low-end devices. https://youtube.com/shorts/l63i3mQ_n2Y?rel=0 Conclusion AGSL is a versatile tool empowering developers to create visually stunning, highly interactive, and performance-optimized effects in Android apps. Whether it’s enhancing UI components, performing advanced image processing, generating procedural graphics, or boosting performance in animation-heavy scenarios, AGSL ensures your app stands out. With AGSL, the possibilities are limited only by your creativity. Start experimenting and bring your app to life!
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の15日目の記事です🎅🎄 学びの道の駅の中西です。今年は学びの道の駅プロジェクトが立ち上がり組織化されました。そんな中、社内Podcastの運営も行っており、今年のアドベントカレンダーではその内容もお届けしたいと思います。 「学びの道の駅」とは? 「学びの道の駅」は、社内で頻繁に開催される勉強会をもっと便利に、そして効果的に活用するために立ち上げられたプロジェクトです。社内の有志が中心となり勉強会の開催を支援し社内の知識共有を促進することを目的としています。 Osaka Tech Lab情報共有会 KTC学びの道の駅ポッドキャストでは、社内の勉強会を開催している方にインタビューを行っています。その名も「突撃!となりの勉強会」。本日のポッドキャストのゲストは「Osaka Tech Lab情報共有会」のご担当、沖田さんと福田さんにお越しいただきました。早速ですが、お二人の自己紹介をお願いできますか? インタビュー 沖田さん: はい、沖田と申します。モバイルアプリ開発グループに所属しており、開発PMとして他のグループとモバイル開発チームをつなぐ役割を担っています。よろしくお願いします。 福田さん: 福田です。2020年7月にKTC(当時は株式会社KINTO)に入社し、プロデュースグループで業務を行っていました。途中、10ヶ月の育児休業を取りまして、2024年2月に復帰しました。現在はクリエイティブ室に異動し、KTCのコーポレートサイトの運用とリニューアルのディレクションを担当しています。よろしくお願いします。 HOKAさん: ありがとうございます。では、大阪の情報共有会を開催したきっかけについて教えてください。 福田さん: Osaka Tech Labは2022年4月に立ち上がりました。当初は分析グループのトモナガさんお1人でした。メンバーが増えるにつれて、「隣の人がどんな仕事をしているのかわからないね」という話題が上がるようになりました。そこで、コミュニケーションを活性化するために情報共有会を始めました。 沖田さん: Osaka Tech Lab情報共有会の創設者は、福田さんですよね。 最初は自己紹介から始まり、趣味を共有することで共通点を見つけて仲良くなることを目指していました。 HOKAさん: なるほど。沖田さんも第1回から参加し、推進されていったんですね。どのように変わっていったのでしょうか? 沖田さん: 最初は人数も少なかったので、みんなで集まってやろうという感じでした。今では17回目を迎え、メンバーが増えてきたことで、会の形式も少しずつ変化してきました。 HOKAさん: Osaka Tech Labが主役の会というのはどういう意味でしょうか? 沖田さん: Osaka Tech Labのコミュニケーションを活性化することが目的です。メンバー同士の仕事内容や取り組みを共有し、横のつながりを強化しています。また、テックブログでのアウトプットにもつなげています。 HOKAさん: 素晴らしいですね。実際に形になっていることを感じます。参加者の反応や雰囲気の変化もありますか? 福田さん: 最初は会話が多かったですが、今では課題を話し合う場として機能しています。オフィスをより良くするための話し合いも行っています。 沖田さん: 例えば、オフィスに時計がなかったので設置したり、本棚を増設したりしました。みんなで意見を出し合い、改善を進めています。 HOKAさん: 今後の展望について教えてください。 沖田さん: 和気あいあいとした雰囲気を保ちながら、組織が大きくなってもコミュニケーションを大切にしたいです。 福田さん: 大阪からKTCを盛り上げていきたいと思っています。情報共有会を継続するか、新しいスタイルで続けていくか、考えていきたいです。 HOKAさん: 他の部署の方が参加したい場合はどうすればいいでしょうか? 沖田さん: 毎月LT枠の募集をしているので、ぜひ参加していただきたいです。 Osaka Tech Lab メンバーにお気軽にお声掛けください。 福田さん: 情報共有会の後にはビアバッシュも開催しているので、コミュニケーションの機会としても活用していただければと思います。 HOKAさん: 最後に、聞いている皆さんに一言お願いします。 沖田さん: ぜひ大阪に来てください。お待ちしています。 福田さん: 情報共有会にLTを持って参加してください。お待ちしています。 HOKAさん: 今日はありがとうございました。大阪からの取り組みが全体に波及していく様子を感じました。沖田さん、福田さん、ありがとうございました。 以上がインタビュー記事のまとめです。Osaka Tech Lab情報共有会の魅力や役割について、読者に伝わる内容になっていますね。 今回はOsaka Tech Lab情報共有会の詳細と、その運営の背景、今後の展望についてお届けしました。次回の勉強会も楽しみにしてください!
アバター
はじめに こんにちは! KINTOテクノロジーズの新車サブスク開発グループに所属している劉(ユ)です。 完璧ではないかもしれませんが、少しずつ問題をより良い方向に改善していくことを目指して日々努力しています。 この記事ではSpring BootにおいてRedisのPub/Subを導入してシステム日付を変更する内容について共有したいと思います。 導入に至った背景 システムのQAやテストを実施する際、システムの日付を変更しないと確認できないケースが多くあります。 特に、サブスクリプションサービスにおいては特定の日付に依存するビジネスロジックをテストする必要があります。 例えば、期間開始日や期間終了日、月額料金、中途解約の精算金、メンテナンス点検・車検などに基づいた処理の検証が求められます。 これまではシステム日付が環境設定ファイルで定義されていたため、日付を変更するたびにコンテナの再デプロイが必要でした。 その結果、テストやQAを行うたびに再デプロイに5分以上の時間がかかるという問題が発生していました。 このような状況における課題を解決した内容を紹介したいと思います。 これを入れたことでどういうメリットがあったか? RedisのPub/Subの概念を導入することで、テスト環境でのシステム日付変更がより効率的になり、迅速な対応が可能となりました。 これにより、テストやQAなどの工数を削減できるようになり、作業効率が向上しました。 具体的にコンテナの再デプロイは不要になり、変更したい設定項目(トピック)に対するメッセージ(変更したい設定値)を発信するだけで、各コンテナはリアルタイムで変更内容を受信し、設定値の変更ができるようになりました。 また、複数のコンテナが構成されている場合でも、すべてのSubscriberがメッセージを受信しているため、複数のコンテナでも再起動せずに設定値を変更できます。 さらに、システム日付変更のログも出力できるため、変更履歴を追跡することが可能となりました。 他にもSpring BootのProfileの設定で、指定したテスト環境のみでこの機能を有効にし、本番環境や他の環境へ誤って適用することを防げます。 ※Profileについては こちら Redis Pub/Subとは Redis Pub/Subは、メッセージキューのメッセージングパターンの1つです。 メッセージキューとは、サーバーレスやマイクロサービスアーキテクチャにおける非同期通信の手法の1つで、分散システムにおいてリアルタイムのイベント通知を実現します。 この仕組みは、異なるソフトウェアモジュール間で拡張かつ安定した通信をサポートするため、データベースやキャッシュとしての利用に加え、メッセージブローカーとしても広く使用されています。 主な構成 Topic(主題):購読する対象となる主題やテーマです。 Publisher(発行者):特定のTopicに関するメッセージを発信します。 Subscriber(受信者):購読したTopicに対して、発行者のメッセージを受信します。 Keyspace Notifications 何らかの方法でRedisデータセットに影響を与えるイベントを受信し、Redisキーおよび値の変更内容をリアルタイムでモニタリングします。 ではどういう実装か? システム日付変更の仕組み トピックに対するメッセージを送信するPublisherとしてAPIを実装しました。 購読しているトピック(キー)に対するイベントが発生すると複数のコンテナ(Subscriber)がメッセージを受信し、リアルタイムで設定値を変更します。 システム構成 JavaとSpring Bootを使用して構築されています。アプリケーションはコンテナ化され、クラウド環境で稼働しています。 build.gradleにlibraryの追加 implementation 'org.springframework.data:spring-data-redis' RedisConfigクラスの実装 @AllArgsConstructor @Configuration public class RedisTemplateConfig { private final RedissonClient redissonClient; @Bean public RedisTemplate<String, String> redisTemplate() { RedisTemplate<String, String> template = new RedisTemplate<>(); template.setConnectionFactory(new RedissonConnectionFactory(redissonClient)); template.setDefaultSerializer(new StringRedisSerializer()); return template; } @Bean public RedisMessageListenerContainer redisContainer() { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(new RedissonConnectionFactory(redissonClient)); return container; } } Publisherの実装 トピックに対するメッセージを送信するAPIを実装します。 @RestController public class SystemTimeController { private final SomeService service; @PostMapping("/update") public void updateSystemTime(@RequestParam String specifiedDateTime) { service.publish(specifiedDateTime); } } @Service @RequiredArgsConstructor public class SomeService { private final RedisTemplate<String, String> redisTemplate; // Topicのキーを定義 private static final String FOO_TOPIC = "foo-key"; public void publish(String specifiedDateTime) { // Topicに対するメッセージを送信する redisTemplate.opsForValue().set(FOO_TOPIC, specifiedDateTime); } } Subscriberの実装 購読しているトピック(キー)に対するイベントが発生すると、メッセージを受信します。 @Slf4j @Component @Profile("develop1, develop2") // 指定されたテスト環境のプロファイルのみ有効 public class FooKeyspaceEventMessageListener extends KeyspaceEventMessageListener { private final RedisMessageListenerContainer listenerContainer; private final RedisTemplate<String, String> redisTemplate; private static final String FOO_TOPIC = "foo-key"; @Override public void init() { doRegister(listenerContainer); } public FooKeyspaceEventMessageListener( RedisMessageListenerContainer listenerContainer, RedisTemplate<String, String> redisTemplate) { super(listenerContainer); this.listenerContainer = listenerContainer; this.redisTemplate = redisTemplate; } @Override protected void doHandleMessage(Message message) { // Redisからシステム日付を取得する String systemTime = updateSystemTimeConfig(redisTemplate.opsForValue().get(FOO_TOPIC)); // システム日付を反映するメソッドを作成してコールする updateSystemTimeConfig(systemTime); log.info("Receive a message about FOO_TOPIC: {}", message); } } さいごに 今回の記事を最後までお読みいただき、ありがとうございます。 まだまだ至らない点が多いですが、毎回少しずつ問題を改善しながら成長していこうと努力しています。 完璧な構造や実装ではありませんが、少しずつより良い方向へ進んでいくことが重要だと思っています。 今後もこうした小さな進歩が集まり、より良い結果を生み出せるよう、引き続き学び続けていきます。 共に成長する旅路を歩んでいければと思います。ありがとうございました。
アバター
This article is part of day 6 of KINTO Technologies Advent Calendar 2024 . 🎅🎄 Introduction Hello, I'm Tada from the SCoE Group at KINTO Technologies (from now on referred to as, KTC). SCoE is an abbreviation for Security Center of Excellence, and some of you may not be very familiar with the term yet. At KTC, we restructured our CCoE team into the SCoE Group this past April. The story behind the reorganization is summarized in this blog article , so please do check it out. Also, I've written this blog article about the Osaka Tech Lab (our company's Osaka office), so feel free to check it out as well. KTC operates many production environments on Amazon Web Services (hereafter referred to as AWS), and with the leveraging of OpenAI, the use of Microsoft Azure (hereafter referred to as Azure) has been on the increase as well lately. One of the tasks of the SCoE is to provide an environment with preconfigured security settings based on group policies. In this blog, I would like to introduce some of the security settings we implement when providing an Azure subscription. Since Azure-specific terms will be used, please refer to the official website and other resources for more details. Designing the Azure Landing Zone and Management Groups When considering security settings, it is important to first understand landing zones and management groups. KTC’s subscription environment has been designed and constructed based on Azure landing zone design principles. However, rather than use Microsoft’s official landing zone as is, we have designed a lighter one of our own to go with KTC’s environment, referring to best practices as we did so. Within the landing zone, we have designed several management groups to organize subscriptions logically and manage them efficiently. The following figure gives an overview of this. By using these management groups, we apply appropriate policies to each subscription. Management group Overview KTC The root of the management groups, used to apply the policies that will be common to all of them Management For managing subscriptions used for security, such as ones used for consolidating the Activity Logs of all subscriptions Workload For managing subscriptions used for workloads Sandbox For managing subscriptions used for sandboxes PolicyStaging For managing subscriptions and management groups used for testing Azure policies The key point is that we have one, unified management group for workloads. This management group contains subscriptions used for products, and within a single subscription, has separate production, development, and staging environments on a per-resource-group basis. There are various approaches to environment separation, but at KTC, we started with this approach because the number of workloads is not large, it is limited to specific Azure services, and cost management at the subscription level is easier. We are also thinking of reviewing it if use of Azure increases in the future. Role of the “Management” Management Group The Management management group is a management group used to consolidate subscriptions for operational management and the deployment of security tools common to all subscriptions. Only members responsible for operations and monitoring have access, and for example, we manage a subscription here that aggregates and monitors the Activity Log for all subscriptions. Configuring Security Settings Using Azure Policies By using Azure policies , it is possible to create resources in accordance with security and governance requirements, and violations can be detected and remediated. We use Azure policies to automatically apply security settings when creating subscriptions here at KTC, too. We are only using built-in policies at the moment, and not yet gone as far as creating custom ones. I would like to consider doing that in the future if the environment changes (due to an increase in workloads, for example). The following is an example of a typical setup that utilizes Azure policies. Monitoring and storing Activity Logs Configuring and using Defender for CSPM At KTC, we do not adopt an approach of overly applying Azure policies as preventive guardrails. This decision is based on factors such as the relatively small number of workloads at KTC, the skill level of engineers, and operational costs. Rather than increasing the constraints with strict preventive guardrails, our approach is all about leaving the engineers a certain degree of freedom, and conducting kaizen (continuous improvement) to address issues detected through heuristic guardrails. The intention behind this is to enable our engineers to hone their skills through problem solving, get increasingly interested in the work, and grow. Monitoring and storing Activity Logs The Activity Logs from all the subscriptions are consolidated in the Log Analytics workspace of the Management subscription. We have set things up so that the Audit Logs get automatically consolidated by means of an Azure policy when new subscriptions are added, too. The Azure policy we are using is shown below. Configure the Azure Activity Logs to stream to the specified Log Analytics workspace Since Log Analytics has a default retention period of 90 days, we keep backups in a storage account. However, there is no setting for this in the Azure policy, so we are doing it manually. We have confirmed that it can be configured to happen automatically by creating a custom policy, but have not gone as far as doing it yet. Configuring and using Defender for CSPM These are called heuristic guardrails. If any risky configurations or actions are performed in the Azure environment, we use Cloud Security Posture Management (CSPM) solutions to detect these risks. In the case of Azure, Microsoft Defender for Cloud can be used for CSPM. Microsoft Defender for Cloud is a solution for Cloud Native Application Protection Platform (CNAPP), covering security solutions such as CSPM (Cloud Security Posture Management) and CWPP (Cloud Workload Protection Platform). Microsoft Defender for Cloud’s CSPM features include Foundational CSPM, which is free, and Defender CSPM, with which resources like servers, databases, and storage give rise to costs. In KTC’s case, we use Defender CSPM, because it enable you to do more detailed CSPM checking. Using the following Azure policy, we configure Defender CSPM automatically upon issuing subscriptions. Configure things so that Microsoft Defender CSPM will be enabled Once we have configured the settings, we periodically monitor the alert situation via Microsoft Defender for Cloud, and if there are any risky configurations, we work with the product-side staff using the subscription to do kaizen to address the risks. We are not implementing cloud workload protection at the moment, but would like to consider it in the future (as resources increase, for example). Threat Detection In order to discover security incidents and unauthorized access in the Azure environment at an early stage, we have introduced a threat detection mechanism. I think many companies that have introduced AWS are using Amazon GuardDuty to achieve this, and that is also what KTC is doing. For Azure, using Microsoft Sentinel is apparently the surefire approach, but given KTC’s environment, in view of the cost and effort involved in introducing that, we are achieving it through the CDR (cloud detection response) features of the third-party product sysdig instead. The CDR is actually handled by the OSS Falco . Falco detects and notifies you of abnormal behavior, potential security threats, and other violations regarding your hosts, containers, Kubernetes, and cloud environment as a whole. General threat detection rules are provided, and these can also be customized and tuned. This makes it very easy to use. KTC was already using sysdig for CSPM and threat detection for its Google Cloud environments, so we are applying the know-how from that to Azure as well. Summary In this article, I talked about some of the security settings we employ at KTC when we provide Azure subscriptions. To enhance security, we utilize Azure Policy, Microsoft Defender for Cloud, and Sysdig's CDR functionality. As I said in “Configuring Security Settings Using Azure Policies,” I think how strict you should make preventive guardrails will depend largely on your own company’s situation, so you should design and operate things in the best ways to suit that. I hope this content serves as a helpful reference for security settings when using Azure. Thank you for reading all the way to the end. Conclusion The SCoE Group is looking for new team members to work with us. We welcome those with practical experience in cloud security as well as those who are interested but have no prior experience. Please feel free to contact us. For additional details, please check here.
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の23日目の記事です🎅🎄 はじめに こんにちは!KINTO テクノロジーズでiOSアプリケーションを開発しているFelixです。今日は、最近見つけた設計ツールを紹介します: Play .開発の生産性を向上させる方法を模索する中で、UI設計をSwiftコードに変換できるツールを探していました。そんな時にPlayを見つけ、予想以上に多くの機能を持っていることがわかりました。Playを使うと、開発者や設計者は、SwiftUI コードを自動的に生成しながら、デバイス上で直接ユーザーインターフェースを作成、テスト、反復処理できます。このツールは、設計と開発を結び付け、より優れたコラボレーションを促進し、効率を高めます。 この投稿では、私が特に便利だと感じた機能をいくつか紹介します。 設計者はNative SwiftUIコンポーネントを使用できます KINTOでは、設計者と密に連携して、アプリケーションがAppleのヒューマンインターフェースガイドラインに沿うようにしています。ただし、これは難しい場合があります。設計者はiOSにおける詳細な実装を必ずしも完全に把握しているわけではないからです。Playの場合、設計者はネイティブ SwiftUI コンポーネントを使用して設計することができます。これにより、設計者はiOSですぐに利用できる機能をより深く理解できるようになり、開発者はiOSネイティブ ライブラリにすでに含まれている機能を再発明する必要がなくなります。このツールは、ネイティブコンポーネントを活用することで、設計者と開発者とのコミュニケーションを明確にし、Appleのヒューマンインターフェイスガイドラインの遵守を確保します。 相互作用の定義 UI設計のみに焦点を当てている多くの設計ツールとは異なり、Playを使用すると、 設計者はアクションとアニメーションをiOSで自然に設定することができます。これにより、コンポーネントがどのように動作するかがより明確になります。例えば、設計者は、ボタンを押すと何が起こるか、次のページへの移行がどのように見えるか、アニメーションがどのように流れるかを指定できます。この機能は、設計と開発のギャップを埋め、コンセプトから実装への移行を円滑にします。 ライブプレビュー 私たちの開発プロセスで特に時間のかかる部分は、UI設計を実装し、テストビルドの公開後にフィードバックを待つことです。Playを使用すると、このプロセスが円滑化し、待ち時間が大幅に短縮します。設計者がライブプレビューで動作を確認できるからです。インタラクションが設計内で設定されると、Playを使用することで、設計者は実際のデバイスで直接作業をテストできます。このツールはiOSのネイティブライブラリを使用してデモアプリを構築するため、設計者は製品版で期待される正確な動作を体験できます。この機能は、設計やインタラクションを開発者に引き渡す前に検証する際に非常に役立ちます。 他にも... もちろん、PlayはUI設計をSwiftUIコードに変換しますが、それだけでなく、ワークフローを向上させる追加機能も提供します。例えば、Playは既存のFigma設計のインポートをサポートしているため、移行が簡単になります。設計者は、UI がさまざまな画面サイズやデバイスでどのように表示されるかをプレビューできるため、レスポンシブで適応可能な設計を確保できます。まだ私が発見していない機能がたくさんあると思います!私たちのチームはこのツールの使用に非常に興味を持っていますが、これを試す前にiOSとAndroid用に設計を分ける必要があります。iOS用の独立した設計があり、チームの生産性を向上させ、設計者と開発者とのコミュニケーションを促進したいと考えている場合は、Playを試してみることを強くお勧めします。
アバター
はじめに こんにちは、KINTO テクノロジーズ ( 以下、KTC ) SCoE グループの桑原 @Osaka Tech Lab です。 SCoE は、Security Center of Excellence の略語で、まだ馴染みのない言葉かもしれません。KTC では、2024 年 4 月に CCoE チームを SCoE グループとして再編しました。SCoE グループについて知りたい方は、 クラウドセキュリティの進化をリードする SCoE グループ を参照ください。 また、KTC の関西拠点である Osaka Tech Lab については、 Osaka Tech Lab 紹介 をご参照ください。 SCoE グループでは、AWS や Google Cloud , Azure のクラウドにおける「ガードレール監視と改善活動をリアルタイムで実施する」をミッションに活動しています。具体的な活動の観点としては以下の 3 つです。 セキュリティリスクを発生させない セキュリティリスクを常に監視・分析する セキュリティリスクが発生したときに速やかに対応する 今回は「KTC のクラウドセキュリティエンジニアって何をやってるの?」を具体的に知っていただこうと思います。 クラウドセキュリティエンジニアのとある一日 具体的なイメージをしていただくために、クラウドセキュリティエンジニアの "とある一日" をご紹介します。(セキュリティという分野の都合上、詳細に語れない部分があることをご了解ください。) アラート確認 朝一番に行うのは、リスクの高いアラートが発生していないかの確認です。CSPM(Cloud Security Posture Management)や脅威検知サービスを使用して、クラウド環境全体のセキュリティ状況を把握し、即時対応が必要なアラートがないか確認します。 KTC では、CSPM や脅威検知サービスとして、 AWS Security Hub や Amazon GuardDuty 、 Sysdig Secure 等のサービスを活用しています。 アラートを確認する際は、以下の観点で対応します。 アラートの優先順位付け : 重大度や影響範囲に基づいてアラートを分類し、優先順位を付けます。 アラートのトリアージ : 発生したアラートの原因を特定し、必要な対応策を講じます。 偽陽性(過検知)の管理 : セキュリティツールは時折、偽陽性(False Positives)を発生させることがあります。これにより、実際には問題のないアクティビティがアラートとして報告されることがあります。これらの管理もアラート処理として対応します。 業務上必要なオペレーションの識別 : 偽陽性の管理に関係しますが、一部のアラートは、業務上必要なオペレーションによって引き起こされることがあります。例えば、各プロダクトの担当者が定期的に行うメンテナンス作業などです。これらのアクティビティを識別し、適切に対応します。 これにより、クラウド環境全体のセキュリティ状況を把握し、即時対応が必要なアラートがないか確認します。 情報収集、キャッチアップ 次に、サイバーセキュリティの動向や AWS などのクラウドサービスの最新情報をキャッチアップします。情報源としては以下のものを利用します。 X(旧:Twitter) : サイバーセキュリティの専門家や業界リーダーをフォローしています。彼らは最新の脅威情報や対策を共有しているため、リアルタイムでの情報収集が可能です。 AWS や Google Cloud の公式ニュースやブログ : クラウドサービスプロバイダーの公式情報は、新機能のリリースやセキュリティアップデートに関する重要な情報源です。これにより、新サービスのローンチ情報や最新の技術動向、ベストプラクティスを把握できます。 その他ニュースサイト : サイバーセキュリティに特化したニュースサイトやブログを定期的にチェックすることで、業界全体の動向を把握し、最新の脅威や攻撃手法についてキャッチアップします。 SIEMでの脅威検知 KTC では、SIEM(Security Information and Event Management)として、 Splunk Cloud Platform を使用しています。この Splunk にセキュリティ関連ログを集約し、ログの横断分析と監視を行える環境を整備しています。 この日は、Splunk のダッシュボードにて、違和感のあるログを発見しました。内容としては、「Google Cloud の組織ポリシーで制限されているサービスに対して、リソース作成を何度しようとしてオペレーションに失敗している」というものです。 Splunk にて独自に作成している Google Cloud Audit logs 用のダッシュボードの情報から、おおよそのアクティビティは判断できていましたが、より詳細に調査します。 まず、Google Cloud の組織ポリシーで制限をかけているサービスに対して、リソース作成を何度もリトライしているユーザーを特定します。 Google Cloud の Audit log (audit:policy_denied) ではユーザー情報はマスクされた状態でログ出力されるため、このログ単体ではユーザーは特定できません。端末系のログなどと一緒に横断的なログを分析することで、ユーザーを特定します。この分析に使用するクエリを作成し、該当のユーザーを特定しました。 次に特定したユーザーの行動を詳細に分析するためのクエリを作成し、ログを分析します。 どうやら、AI/ML のサービスである Vertex AI を使用しようとしている模様です。該当のプロジェクトでは、Compute 系の利用申請は出ていませんでしたので、組織ポリシーにて Compute 系サービスの使用を制限をしています。 Vertex AI は、Notebook を使用する際に、Compute Engine (GCE) インスタンスが起動します。よって、この部分で組織ポリシー違反となります。 結果的に、「Google Cloud プロジェクト新規発行申請時に利用予定サービスの記載漏れがあった」ということで、この件は、問題ないアクティビティであることを確認しました。 クラウドベンダーネイティブのセキュリティサービスに対するコスト最適化検討 クラウドベンダーが提供するセキュリティサービスは従量課金制であり、クラウドリソースが増加すると、それに伴ってセキュリティサービスの利用料も増加します。 私たちが考える「セキュリティ」は、 「ビジネスのためのセキュリティ」であり、「ビジネスを阻害するセキュリティ」は NG です。 そのため、「セキュリティとコストのバランス」も重要なポイントであり、セキュリティサービスのコスト最適化も SCoE グループのミッションに含まれます。 この日は、全体コストの中で割合が高いいくつかのセキュリティサービスについて、コスト最適化の可能性を調査しました。 上記のグラフは、今回の分析対象となったサービスを示しています。その中でも特に AWS Config に注目しました。(具体的な項目名はマスキングしています) AWS Config は、AWS リソースの構成を監査、評価、そして記録するためのサービスです。2023 年 11 月までは、AWS Config の記録方式として「リソース構成の変更が発生するたびに記録する方式」しか提供されていませんでした。この方式は、「記録頻度:継続的な記録」と呼ばれています。 つまり、リソースの変更頻度が高い場合、AWS Config の記録回数が増加し、それに比例して利用料金も増加する仕組みです。 例として、ネットワーク関連のイベントを確認してみましょう。以下のデータは、ある AWS アカウントにおける 1 週間分の VPC およびネットワーク関連の構成変更回数を示したグラフです。 Elastic Network Interface (ENI) の作成・削除に該当する  CreateNetworkInterface と DeleteNetworkInterface が一日あたり約 17,000 件発生していることがわかります。 KTC では、Amazon Elastic Container Service (ECS) の Fargate を活用しています。このため、ECS タスク (コンテナ) が起動・停止するたびに、ENI の作成・削除が発生します。このような状況下で AWS Config を「記録頻度:継続的な記録」に設定している場合、これらの変更に伴う AWS Config の記録回数が膨大になり、それに応じて課金額も増加します。 しかし、2023 年 11 月以降、AWS Config に「記録頻度:日次記録」を選択できる機能が追加されました。この新機能により、リソースタイプごとに記録頻度を調整することが可能となり、セキュリティとコストのバランスを柔軟に取ることができるようになりました。一般的には、この設定を活用することで AWS Config の利用コストを最適化できると考えられています。 ただし、これは AWS Control Tower を使用していない場合に限ります。AWS Control Tower は複数の AWS アカウントのガバナンスを一元管理するためのサービスです。 AWS Control Tower を利用して AWS アカウントの AWS Config を管理している場合は、 Guidance for creating and modifying AWS Control Tower resources を確認してください。 ガイダンスの冒頭に記載されている以下の一文に注目してください。 Do not modify or delete any resources created by AWS Control Tower, including resources in the management account, in the shared accounts, and in member accounts. If you modify these resources, you may be required to update your landing zone or re-register an OU, and modification can result in inaccurate compliance reporting. この記載が示すように、 AWS Control Tower によって作成されたリソースを AWS Control Tower 以外の方法で変更または削除することは非推奨 です。 具体的には、2024 年 12 月現在、AWS Control Tower では AWS Config の記録頻度を変更する機能が提供されていません。そのため、AWS Control Tower 管理下の AWS Config の記録頻度を変更することは非推奨とされており、公式ドキュメントにも問題が発生する可能性があると記されています。 公式ドキュメントの内容を踏まえつつ、念のため AWS サポートにも問い合わせを行ったところ、同様の見解を得ました。 このように、「設定そのものは可能であっても、問題が発生するリスクがある、または非推奨とされる」場合には、安定したクラウドセキュリティとガバナンスを維持することが難しくなります。その結果、 「ビジネスを阻害するセキュリティ」 となりかねません。 以上を踏まえ、AWS Config の記録頻度変更は現時点では見送ることとし、AWS サポートに改善要望を提出しました。クラウドサービスの利便性向上を目的としたこうした改善要望の起案は、地道ではありますが非常に重要な取り組みであると考えています。 セキュリティ勉強会の準備 最後に、定期的に実施している社内セキュリティ勉強会(セキュリティ&プライバシー勉強会)での登壇資料を作成しました。 SCoE グループでは、プロダクト開発時の「要件定義」「設計」「開発」フェーズにおけるクラウドセキュリティの勘所をまとめた、"クラウドセキュリティガイドライン" を策定し、社内向けに公開しています。 このガイドラインは、KTC が所属するグループ企業のセキュリティポリシーを遵守し、セキュリティリスクを最小限に抑えつつ、効率的な開発を支援するための重要なリソースです。 この "クラウドセキュリティガイドライン" の周知と理解を促進するために、勉強会でのセッションを受け持っています。勉強会では、具体的な事例や実践的なアドバイスを交えながら、ガイドラインの各項目について詳しく説明します。 この日は、IAM(Identity and Access Management)のベストプラクティスについて、持ち時間 20 分に収まるサイズの登壇資料を作成しました。 さいごに KTC のクラウドセキュリティエンジニアのとある一日をご紹介いたしました。業務のごく一部でしたが、業務内容をイメージできましたでしょうか? 我々 SCoE グループでは、一緒に働いてくれる仲間を募集しています。クラウドセキュリティの実務経験がある方も、経験はないけれど興味がある方も大歓迎です。お気軽にお問い合わせください。 詳しくは、 こちらをご確認ください 。
アバター
This article is the entry for day 14 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Konnichiwa! I am Felix, and I develop iOS applications at KINTO Technologies. Today, I would like to introduce a design tool I recently discovered: Play . While exploring ways to improve development productivity, I was searching for a tool that could translate UI designs into Swift code. That is when I found Play, and it turned out to offer far more features than I had expected. Play allows developers and designers to create, test, and iterate on user interfaces directly on their devices, all while automatically generating SwiftUI code. This tool connects design and development, fostering better collaboration and increasing efficiency. In this post, I will share some of the features I found particularly useful. Designers Can Use Native SwiftUI Components At KINTO, we collaborate closely with designers to ensure our applications align with Apple’s Human Interface Guidelines. However, achieving this can sometimes be challenging, as designers may not always be fully aware of the detailed implementation in iOS. Play allows designers to use native SwiftUI components in their designs. This helps designers better understand what features are readily available in iOS, saving developers from having to reinvent features that are already part of the iOS native library. By leveraging native components, the tool promotes clearer communication between designers and developers and ensures adherence to Apple’s Human Interface Guidelines. Defining Interactions Unlike many design tools that focus solely on UI design, Play enables designers to define actions and animations natively in iOS. This provides a clearer vision of how components are expected to behave. For instance, designers can specify what happens when a button is pressed, how transitions to the next page should look, and how animations should flow. This capability bridges the gap between design and development, making the transition from concept to implementation smoother. Live Preview A particularly time-consuming part of our development process is implementing UI designs and waiting for feedback after publishing a test build. Play helps streamline this process and significantly reduces the waiting time, as designers can check the behavior in the live preview. Once interactions are set up in the design, Play allows designers to test their work directly on a real device. Since the tool builds a demo app using iOS native libraries, designers can experience the exact behavior they would expect in the production version. This feature is incredibly valuable for validating designs and interactions before handing them off to developers. And More... Of course, Play converts UI designs into SwiftUI code, but it goes beyond that to offer additional features that can elevate your workflow. For example, it supports importing existing Figma designs, making migration effortless. Designers can also preview how the UI will look on different screen sizes and devices, ensuring responsive and adaptable designs. I’m sure there are plenty more features I haven’t discovered yet! Our team is highly interested in using this tool, however we need to separate the design for iOS and Android before we try this. If you have the independent designs for iOS and if you are looking to improve team productivity and foster better communication between designers and developers, I would highly recommend giving Play a try.
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の14日目の記事です🎅🎄 学びの道の駅の中西です。今年は学びの道の駅プロジェクトが立ち上がり組織化されました。そんな中、社内Podcastの運営も行っており、今年のアドベントカレンダーではその内容もお届けしたいと思います。 「学びの道の駅」とは? 「学びの道の駅」は、社内で頻繁に開催される勉強会をもっと便利に、そして効果的に活用するために立ち上げられたプロジェクトです。社内の有志が中心となり勉強会の開催を支援し社内の知識共有を促進することを目的としています。 部長会議事メモを読む会 KTC学びの道の駅ポッドキャストでは、KTCの勉強会を開催している方々にインタビューをしています。その名も「突撃!隣の勉強会」。今回は「部長会議事メモを読む会」を開催している大森さんと高木さんにお話を伺います。 インタビュー HOKAさん: では、早速お二人の自己紹介をお願いできますか? 大森さん: はい、コーポレートITの大森です。普段は室町16階センターでパソコンのキッティング作業をしています。アセットプラットフォームチームに所属しており、業務用デバイスやSaaSアカウントライセンスの管理を行っています。新入社員のデバイス準備や回収も担当しています。 高木さん: はい、同じくコーポレートITの高木です。私はテックサービスチームで勤務しており、神保町と室町を行き来しています。サービスデスクとして、社内からの問い合わせに対応したり、問題解決を行っています。具体的には自己サービスマネジメント(GSM)やOPITマネジメントを担当しています。 HOKAさん: ありがとうございます。それでは、部長会議事メモを読む会のきっかけについて教えていただけますか? 大森さん: きっかけは、名古屋にいるきんちゃんが発起人です。コーポレートITは普段の業務で事業の最前線の情報に触れる機会が少ないため、部長会の議事メモを共有し、みんなでインプットし議論することで生産性を向上させようという目的で始めました。 高木さん: 私も同じように感じています。議事メモを読むことで、事業の動きを先読みし、業務に役立てることができます。例えば、申請が来る前に準備を整えることができるので、業務の効率が上がります。 HOKAさん: 実際にこの会を通じてどのような効果がありましたか? 大森さん: 実際の業務に直結することは少ないですが、議事メモを読むことでプロジェクトの背景を理解し、適切な提案ができるようになります。これにより、業務の質が向上します。 高木さん: 同感です。議事メモを読むことで、事業の動きを把握し、突発的な依頼にも対応しやすくなります。議事メモを通じて得た情報は、業務の判断や提案に役立っています。 HOKAさん: これからの展望について教えてください。 高木さん: ファシリテーターのチャレンジの場としても使ってもらいたいです。新しい参加者も増え、より賑やかに、楽しく学べる場にしていきたいです。 大森さん: 同感です。事業理解を深めるために、議事メモを読み、議論することで、参加者全員が業務に役立てられるようにしたいです。情報を蓄積し、後からでもキャッチアップできるようにしていきたいです。 HOKAさん: 最後に、聞いている皆さんに一言お願いします。 大森さん: この会は誰でも参加可能です。興味がある方はぜひ参加してください。一緒に事業理解を深め、業務の質を向上させましょう。 高木さん: これからSlackチャンネルを作成し、告知を行いますので、興味のある方はぜひ参加してください。参加することで、事業理解が深まり、自分の仕事にも役立つと思います。 HOKAさん: 今日はありがとうございました。 今回は部長会議事メモを読む会の詳細と、その運営の背景、今後の展望についてお届けしました。次回の勉強会も楽しみにしてください!
アバター
Introduction Hello. I am Chris, a front-end developer in the Global Development Division at KINTO Technologies. I have written about Storybook and Vue.js , but today, I would like to move away from technology a little, and talk about management. Actually, I became the leader of the front-end team in July of last year, and it's been almost a year since then. It has been my first leadership experience and a many things happened, but I wanted to think about how I can become a better one for next year, look back on what I have done as one over the past year, and set it all down in writing. So, I decided to write this article about it. About the Team First, going back to when I became the team leader last year, the team itself was created when the department reorganized and we wanted a unit that specialized in front-end development. As I mentioned in a previous article, we are a multinational team with some members who are not that fluent in Japanese, so we communicate in basic English within the team. The main job of this team is to do the front-end development for each project that belongs to the Global Development Division, but since there are not many members in relation to the multiple products involved, it is not uncommon for one person to be responsible for several of them. Also, since there are no front-end tasks for products that have entered their maintenance period, we sometimes improve the code by refactoring it, and at the same time rotate team members to other products. Role as the Team Leader Before I became the team leader, I used to focus on front-end development work and develop the design system as a member of this team. However, since becoming the leader, I have mainly focused on the following tasks, and have often left the development work to the other members rather than doing it myself. Selecting technologies and implementation methods The first task is to select the technologies and implementation methods for the team as a whole. There are many frameworks and libraries to choose from for front-end development. In fact, there are so many to choose from that I imagine lots of people probably struggle to decide—and I am one of them, of course. When it comes to frameworks, you have to consider things like whether the team members are used to it, whether it meets the product requirements in terms of functionality, and whether the community support is adequate. However, another important factor is how to strike a balance with developers’ common tendency to want to try out trending frameworks, too. Currently, the front-end team in the Global Development Division is basically uniformly using Vue.js/Nuxt.js, but that does not mean that we will have to continue using this set indefinitely. The team always has an atmosphere of wanting to try new things, so recently, inspired by other front-end teams at in-house tech staff get-togethers, we have been trying out and assessing things like SvelteKit and Astro as well. Then, we decide what to use based on the results of our assessments. Communication and following up between PMs and team members, and reviewing product code The second task is to ensure that product managers (PdMs) / project managers (PjMs) and team members can communicate smoothly. Since the front-end team sometimes does development tasks for multiple products, I get the members to stay in communication with the respective PdM/PjM as they go. Through things like checking specifications, making suggestions, and giving feedback, I help ensure that product development can go smoothly. In addition, since there is often only one team member assigned to each current product, as the leader, I review all the products in order to understand their quality situation and other information about them. Serving as a bridge between senior managers and team members The third task is to serve as a bridge between senior managers and team members. In our company, becoming a manager can sometimes mean having more than 10 extra members under you. For example, in my case, my own direct manager also manages other teams, and is in charge of nearly 20 members in total. Consequently, management of team members is often done via their leader. Currently I mainly do the following: Set roles and missions for each member based on their abilities and aspirations (of course, ultimately coordinate with the manager) Based on the missions we have decided for them, I set up regular 1-on-1 with them, give them feedback, listen to their concerns and the things they want to discuss, and feed the on-site opinions back to the manager as necessary Motivating and mentoring Lastly, of course, managing the team members. As mentioned above, in addition to one-on-one communication, discovering each team member's strengths and making the most of them is also an important role. My team members not only have various nationalities but various skill levels as well. However, one thing they all share is they are highly ambitious. Some of them pay tremendous attention to detail and spot things that are difficult to notice, while others study in their private time then use the skills and knowledge thus gained in their work. It is also necessary to maintain motivation, and I want to help team members achieve their goals as much as possible, but even when that is not possible, communication to motivate them is necessary. Also, if team members need to use a technology that is new to them, I will, for example, give them a lecture on it, or—if it is new to me as well—study it together with them. What I Want to Be Mindful of as a Leader Appropriate communication and following up With working remotely on the rise since the COVID-19 pandemic, our company has also introduced a full-flextime system and a remote-work system. This in turn means more meetings held online. Compared to talking face-to-face, it is harder to pick up information from facial expressions and gestures, and there is a higher risk of communication errors. In particular, when I mainly want to talk to the team members as their leader and for one-on-ones requested by them, as far as possible, I choose times when everyone concerned is in the office. (Of course, we keep the meetings themselves to a minimum.) Also, I won the hackathon held in the department last year and am working with another team to turn my creation into a product. So, I also regret not being able to rapidly follow up with my own members due to doing two jobs, and am now looking for a way to balance things better. Delegating and nurturing well My team works on multiple products, so when assigning members to them, I try to take into account their skill levels and preferences, how difficult the product is, what the issues are, and so on. Then, even after assigning them, I regularly talk to them about the situation and provide them with support. Sometimes when I look at team members’ tasks, I feel a strong desire to do them myself as well. However, I do my best to put my own feelings aside and get the members to do them :->. On the other hand, even after leaving tasks to them, I still have to think about what I can do to help them grow as well. One of my answers is to encourage them to think for themselves as much as possible. For example, in the case of junior-level members, when I want to point out something in a review, first, I ask them questions to get them to think about the “why” part, rather than thinking about everything from scratch. Conclusion Being a leader for a year has brought me a lot of experiences, and I would like to wrap up this article by talking about what might lie in store for the Global Group’s front-end team this fiscal year. Updating our skills in the FE field As I mentioned above, in addition to the team still having some junior-level members, the technologies keep changing day after day. The past few years have seen lots of hot new frameworks in just the front-end field alone. Notable examples are Svelte, Astro, and Qwik, all of which other departments are using. Introducing a new framework or library entails thinking about more than just the technical factors, but studying up on those as well will prove useful in the future, and broaden your horizons to boot. So, I would like to set some vague annual goals for the members and update their skills as a team. Understanding the BE field A common tale from my own experience is that specializing solely in the front-end and having little understanding of the back-end can lead to mismatches in discussion about API integration. So, even though our company separates the front-end work from the back-end in terms of job descriptions, I would like my team to learn as much of the basics as possible in areas their own work does not cover, too. For example, one of the measures I am doing now is to coordinate with the project leaders after assigning members to projects, so that they get to experience handling simple back-end tasks as well, at a level that will not be a nuisance to anyone.
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の13日目の記事です🎅🎄 Impact Effort Matrix(インパクト・エフォートマトリックス)を使って社内交流を実践してみた こんにちは、KINTO テクノロジーズの技術広報グループに所属しているMayaと木下です。 はじめに 私たちは過去に神保町で社内交流会を開催し、オフィス内の交流不足を解消し、チーム間のつながりを強化することを目指しました。 実施したときの記事はこちら↓ https://blog.kinto-technologies.com/posts/2023-09-19-JimbochoISM/ このイベントでは、企画段階にImpact/Effort Matrix(インパクト・エフォートマトリックス)を活用して、催しの内容を決定しました。 これにより、イベントを盛り上げるためのアイデアを整理し、タスクの優先順位を明確にすることができました。 その結果、複数回のイベントをスムーズに運営することができました。 この記事では、その時の経験を基に、Impact/Effort Matrixを用いたイベントの企画・実践、振り返りまでのアプローチを紹介します。 本記事が、読者の皆さんのプロジェクトやイベント計画に役立てば幸いです。 ぜひ、最後までお楽しみください。 Impact/Effort Matrixについて Impact/Effort Matrix(インパクト・エフォートマトリックス)は、プロジェクトやタスクの優先順位を効率的に決定するためのシンプルかつ効果的なツールです。 このマトリックスは、タスクやアイデアを 「成果(Impact)」 と 「労力(Effort)」 の2軸で分類することで、どの項目にリソースを集中させるべきかを視覚的に判断できます。 基本構造 Impact/Effort Matrix は4つの象限で構成されます: Quick Wins(すぐに実行すべきタスク) 高い成果をもたらし、必要な労力が少ないタスク 優先的に取り組むべきです Major Projects(戦略的に取り組むべきタスク) 高い成果が期待できるが、労力も多く必要なタスク リソース計画が重要になります Fill-ins(余裕があるときに実施するタスク) 労力が少なく成果も小さいタスク 優先順位は低めです Parking Lot(避けるべきタスク) 成果が低く、労力が多いタスク 基本的に取り組むべきではありません さらに詳しくは Miroのテンプレート説明ページ をご参照ください。 なぜ導入したのか 第1回目の神保町共有会開催後、それなりに良い感想をいただくことができました。 しかし、参加者全員が一体感を持てているわけではなかったため、もっと「ワイワイ感」を出すにはどうすればいいのか?というブレインストーミングをしたかったのです。 運営メンバーで議論し合いながら、目指す目的を明確にし、やるべきタスクとやらないタスクを選定しました。 そして、優先順位をメンバーで共有するための適切な手法を探している中、メンバーが過去に利用経験のあるImpact/Effort Matrixを用いることにしました。 活用して感じたメリット 優先順位付けが明確になる: 4象限に相対的に全ての課題を配置するので、議論を進め、付箋を並べるにつれて、優先するべきタスクが視覚的に浮き上がってくる 容易に共有でき、全員の理解が一致しやすい: 上記の延長線ではありますが、何を優先すべきかを議論するきっかけにもなるため、認識をチームで合わせる時間になります 全員のコンセンサスを得ながら進めるため、コミットメントも高く、全員が自分ごとと捉えて進めやすい チームのリソースを最適化できる: 次のアクションへも全員が納得する落とし込みにもつながるため、出戻りの発生を防げます 苦労したところ・工夫したところ よくある話ではありますが、一番チームで苦労したところは意思決定の部分でした。 2回実施したImpact/Effort Matrixの初回では、意見や共通認識の課題が多く出たものの、その情報をどう整理し、次のイベントの改善に繋げるかのコンセンサスに至るまでたくさん議論を重ねる必要がありました。 整理した内容の粒度がバラバラで、やりたいことのスコープも広かったため、思うように成果を出せませんでした。 2回目では、上記の経験を踏まえて課題の粒度を揃え、目的を明確にし、スコープを絞ることで、より具体的な結果を得ることができました。 メンバーの理解が深まり、プロセスをスムーズに進めることができました。 イベントの方向性をより具体化することができて、二回目も無事に成功し、神保町共有会の三回目に向けたアクションプランを策定し、Jiraボードでタスクを見える化しました。 この時、新しいメンバーが数名加わり、初めてIEMを体験する人たちでした。 未経験者を多く含む状況ながらも、これまでの経験からスムーズに進行することができ、三回目の神保町共有会を盛り上げることができました。 Impact/Effort matrixやってみての感想 Impact/Effort Matrixの手法を理解しても、実際に自分たちの状況に当てはめて実践しようとすると、最初は本当にこれで良いのかと迷うことがあり、チーム全体に不安が広がることもありました。 しかし、違和感を覚える点や改善すべき点について、皆が意見を出し合い、それを尊重し合うことで、私たちは様々なアイデアを整理し、どのタスクを優先すべきかを明確にすることができました。 このImpact/Effort Matrixの手法が上手く機能するまでの過程では、意見の対立もありましたが、それぞれのアイデアを客観的に評価することで、全員が納得できる形で進めることができました。 イベントに向けて準備が進むにつれ、チームとしての一体感が高まり、最終的には社内交流会の参加者から高い評価を得ることができました。 終わりに 今回の記事では、神保町での社内交流会を例に、Impact/Effort Matrixを活用したイベントの企画・実践、振り返りのアプローチをご紹介しました。 初めは手探りで不安もあり、うまくできているのか自信が持てない部分もありましたが、何度か実践を重ねる中で次第に慣れ、運営メンバーが途中で増えたにもかかわらず、スムーズに運営できるようになりました。 この手法が、皆さんのプロジェクトやイベントの成功に役立つことを願っています。 皆さんのプロジェクトやイベントの参考になれば幸いです。 KINTO テクノロジーズでは、一緒に働く仲間を広く募集しています。 ご興味を持たれた方は、ぜひお気軽にご連絡ください。お待ちしております! https://hrmos.co/pages/kinto-technologies/jobs
アバター