KINTOテクノロジーズのブログ - TECH PLAY

TECH PLAY

KINTOテクノロジーズ

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

1124

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日目の記事です🎅🎄 学びの道の駅の中西です。今年は学びの道の駅プロジェクトが立ち上がり組織化されました。そんな中、社内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.
この記事は 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情報共有会の詳細と、その運営の背景、今後の展望についてお届けしました。次回の勉強会も楽しみにしてください!
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 テクノロジーズ ( 以下、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 グループでは、一緒に働いてくれる仲間を募集しています。クラウドセキュリティの実務経験がある方も、経験はないけれど興味がある方も大歓迎です。お気軽にお問い合わせください。 詳しくは、 こちらをご確認ください 。
この記事は 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テクノロジーズアドベントカレンダー2024 の14日目の記事です🎅🎄 学びの道の駅の中西です。今年は学びの道の駅プロジェクトが立ち上がり組織化されました。そんな中、社内Podcastの運営も行っており、今年のアドベントカレンダーではその内容もお届けしたいと思います。 「学びの道の駅」とは? 「学びの道の駅」は、社内で頻繁に開催される勉強会をもっと便利に、そして効果的に活用するために立ち上げられたプロジェクトです。社内の有志が中心となり勉強会の開催を支援し社内の知識共有を促進することを目的としています。 部長会議事メモを読む会 KTC学びの道の駅ポッドキャストでは、KTCの勉強会を開催している方々にインタビューをしています。その名も「突撃!隣の勉強会」。今回は「部長会議事メモを読む会」を開催している大森さんと高木さんにお話を伺います。 インタビュー HOKAさん: では、早速お二人の自己紹介をお願いできますか? 大森さん: はい、コーポレートITの大森です。普段は室町16階センターでパソコンのキッティング作業をしています。アセットプラットフォームチームに所属しており、業務用デバイスやSaaSアカウントライセンスの管理を行っています。新入社員のデバイス準備や回収も担当しています。 高木さん: はい、同じくコーポレートITの高木です。私はテックサービスチームで勤務しており、神保町と室町を行き来しています。サービスデスクとして、社内からの問い合わせに対応したり、問題解決を行っています。具体的には自己サービスマネジメント(GSM)やOPITマネジメントを担当しています。 HOKAさん: ありがとうございます。それでは、部長会議事メモを読む会のきっかけについて教えていただけますか? 大森さん: きっかけは、名古屋にいるきんちゃんが発起人です。コーポレートITは普段の業務で事業の最前線の情報に触れる機会が少ないため、部長会の議事メモを共有し、みんなでインプットし議論することで生産性を向上させようという目的で始めました。 高木さん: 私も同じように感じています。議事メモを読むことで、事業の動きを先読みし、業務に役立てることができます。例えば、申請が来る前に準備を整えることができるので、業務の効率が上がります。 HOKAさん: 実際にこの会を通じてどのような効果がありましたか? 大森さん: 実際の業務に直結することは少ないですが、議事メモを読むことでプロジェクトの背景を理解し、適切な提案ができるようになります。これにより、業務の質が向上します。 高木さん: 同感です。議事メモを読むことで、事業の動きを把握し、突発的な依頼にも対応しやすくなります。議事メモを通じて得た情報は、業務の判断や提案に役立っています。 HOKAさん: これからの展望について教えてください。 高木さん: ファシリテーターのチャレンジの場としても使ってもらいたいです。新しい参加者も増え、より賑やかに、楽しく学べる場にしていきたいです。 大森さん: 同感です。事業理解を深めるために、議事メモを読み、議論することで、参加者全員が業務に役立てられるようにしたいです。情報を蓄積し、後からでもキャッチアップできるようにしていきたいです。 HOKAさん: 最後に、聞いている皆さんに一言お願いします。 大森さん: この会は誰でも参加可能です。興味がある方はぜひ参加してください。一緒に事業理解を深め、業務の質を向上させましょう。 高木さん: これからSlackチャンネルを作成し、告知を行いますので、興味のある方はぜひ参加してください。参加することで、事業理解が深まり、自分の仕事にも役立つと思います。 HOKAさん: 今日はありがとうございました。 今回は部長会議事メモを読む会の詳細と、その運営の背景、今後の展望についてお届けしました。次回の勉強会も楽しみにしてください!
この記事は KINTOテクノロジーズアドベントカレンダー2024 の14日目の記事です🎅🎄 学びの道の駅の中西です。今年は学びの道の駅プロジェクトが立ち上がり組織化されました。そんな中、社内Podcastの運営も行っており、今年のアドベントカレンダーではその内容もお届けしたいと思います。 「学びの道の駅」とは? 「学びの道の駅」は、社内で頻繁に開催される勉強会をもっと便利に、そして効果的に活用するために立ち上げられたプロジェクトです。社内の有志が中心となり勉強会の開催を支援し社内の知識共有を促進することを目的としています。 部長会議事メモを読む会 KTC学びの道の駅ポッドキャストでは、KTCの勉強会を開催している方々にインタビューをしています。その名も「突撃!隣の勉強会」。今回は「部長会議事メモを読む会」を開催している大森さんと高木さんにお話を伺います。 インタビュー HOKAさん: では、早速お二人の自己紹介をお願いできますか? 大森さん: はい、コーポレートITの大森です。普段は室町16階センターでパソコンのキッティング作業をしています。アセットプラットフォームチームに所属しており、業務用デバイスやSaaSアカウントライセンスの管理を行っています。新入社員のデバイス準備や回収も担当しています。 高木さん: はい、同じくコーポレートITの高木です。私はテックサービスチームで勤務しており、神保町と室町を行き来しています。サービスデスクとして、社内からの問い合わせに対応したり、問題解決を行っています。具体的には自己サービスマネジメント(GSM)やOPITマネジメントを担当しています。 HOKAさん: ありがとうございます。それでは、部長会議事メモを読む会のきっかけについて教えていただけますか? 大森さん: きっかけは、名古屋にいるきんちゃんが発起人です。コーポレートITは普段の業務で事業の最前線の情報に触れる機会が少ないため、部長会の議事メモを共有し、みんなでインプットし議論することで生産性を向上させようという目的で始めました。 高木さん: 私も同じように感じています。議事メモを読むことで、事業の動きを先読みし、業務に役立てることができます。例えば、申請が来る前に準備を整えることができるので、業務の効率が上がります。 HOKAさん: 実際にこの会を通じてどのような効果がありましたか? 大森さん: 実際の業務に直結することは少ないですが、議事メモを読むことでプロジェクトの背景を理解し、適切な提案ができるようになります。これにより、業務の質が向上します。 高木さん: 同感です。議事メモを読むことで、事業の動きを把握し、突発的な依頼にも対応しやすくなります。議事メモを通じて得た情報は、業務の判断や提案に役立っています。 HOKAさん: これからの展望について教えてください。 高木さん: ファシリテーターのチャレンジの場としても使ってもらいたいです。新しい参加者も増え、より賑やかに、楽しく学べる場にしていきたいです。 大森さん: 同感です。事業理解を深めるために、議事メモを読み、議論することで、参加者全員が業務に役立てられるようにしたいです。情報を蓄積し、後からでもキャッチアップできるようにしていきたいです。 HOKAさん: 最後に、聞いている皆さんに一言お願いします。 大森さん: この会は誰でも参加可能です。興味がある方はぜひ参加してください。一緒に事業理解を深め、業務の質を向上させましょう。 高木さん: これからSlackチャンネルを作成し、告知を行いますので、興味のある方はぜひ参加してください。参加することで、事業理解が深まり、自分の仕事にも役立つと思います。 HOKAさん: 今日はありがとうございました。 今回は部長会議事メモを読む会の詳細と、その運営の背景、今後の展望についてお届けしました。次回の勉強会も楽しみにしてください!
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.
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日目の記事です🎅🎄 学びの道の駅の中西です。今年は学びの道の駅プロジェクトが立ち上がり組織化されました。そんな中、社内Podcastの運営も行っており、今年のアドベントカレンダーではその内容もお届けしたいと思います。 「学びの道の駅」とは? 「学びの道の駅」は、社内で頻繁に開催される勉強会をもっと便利に、そして効果的に活用するために立ち上げられたプロジェクトです。社内の有志が中心となり勉強会の開催を支援し社内の知識共有を促進することを目的としています。 Factory自動車勉強会 KTC学びの道の駅ポッドキャストでは、社内の勉強会を開催している方にインタビューを行っています。その名も「突撃!となりの勉強会」。今回はFactoryチームの三浦さんにインタビューしました。 インタビュー HOKAさん: 三浦さん、よろしくお願いします。普段どんなお仕事をされているのか、自己紹介を兼ねてお話しいただけますか? 三浦さん: よろしくお願いします。正式名称を言いますと、プロジェクト開発部プロジェクト推進グループKINTOファクトリーチームのチームリーダーをやっています。ファクトリーチームはKTCだけでなく、KINTOの総合企画部と一緒にプロダクトの開発を行っています。 HOKAさん: 今回のインタビューのきっかけでもある、ファクトリーの中で自動車の勉強をしているということについて教えてください。 三浦さん: まず、みんなに楽しく仕事をしてもらいたいという思いがあります。自分たちが売っているものがどんなものなのかを知ることで、仕事が楽しくなると思います。車に関わる商品を扱っているので、その知識を持っているとより開発が楽しくなります。元々自動車業界でのキャリアがあり、その知識をシェアすることで、もっと良い提案ができるようになると思い、勉強会を始めました。 HOKAさん: その知識をどうやってシェアすることにしたのですか? 三浦さん: オンラインで勉強会を開催しています。最初は1時間だったのですが、現在は30分に縮小して月に一度のペースで行っています。最近では車内のネットワークやナビの進化について話しています。 HOKAさん: 勉強会に参加しているメンバーの反応はどうですか? 三浦さん: アンケートを取ったところ、普段触れることのない情報が新鮮だという意見が多かったです。参加者も減ることなく、興味を持って聞いてくれています。 HOKAさん: どれぐらいの時間やっているのですか? 三浦さん: 最初は1時間でしたが、今は30分で月に一度のペースです。 HOKAさん: これまでの勉強会でどんなテーマを扱いましたか? 三浦さん: 最近では車内のネットワークやナビの進化について話しました。他にも、ラスベガスのCESから見える自動車の進化についても話しました。 HOKAさん: ラスベガスのCESに行かれたのですか? 三浦さん: 実際には行けなかったのですが、ネットで公開されている展示内容をもとに自分なりの考察を加えてシェアしました。 HOKAさん: 今後の勉強会の予定はありますか? 三浦さん: 次回は、車の取り付け工程について話そうと思っています。販売店向けのマニュアルを使って、車にどのように部品が取り付けられているのかを共有する予定です。 HOKAさん: 勉強会に参加したい場合はどうすればいいですか? 三浦さん: 基本的にはオンラインで行っているので、関心がある方は自由に参加できます。今後、勉強会の情報を可視化する仕組みも作ろうとしています。 HOKAさん: 勉強会を始めるきっかけは何でしたか? 三浦さん: チームビルディングの一環として始めました。もともとプロジェクトチームだったものがチームになったことで、チーム内のメンバーに自動車についてもっと知ってもらおうと思ったのがきっかけです。 HOKAさん: 最後に、社員の皆さんに一言お願いします。 三浦さん: 我々は自動車に関わる事業会社で働いているので、車について知識を深めることで仕事がより楽しくなると思います。興味がある方は、ぜひ勉強会に参加してください。 HOKAさん: 三浦さん、ありがとうございました。これからも勉強会を通じて、社員の皆さんに自動車の魅力を伝えていってください。 今回はFactoryの詳細と、その運営の背景、今後の展望についてお届けしました。次回の勉強会も楽しみにしてください!
この記事は KINTOテクノロジーズアドベントカレンダー2024 の13日目の記事です🎅🎄 はじめまして。 KINTOテクノロジーズでUnlimited(Android)を開発しているfabtと申します。 最近何かと話題のスクリーンショットテストを導入してみたので、導入の流れを追いながら、つまづいた点とその解決方法について紹介します。 スクリーンショットテストの導入を考えている方の参考になれば幸いです。 スクリーンショットテストとは 開発中のソースコードを基に画面のスクリーンショットを撮影し、過去のソースコードを基に同様の手順で撮影されたスクリーンショットと比較し、変更点を確認・検証するテスト手法です。(ざっくりですが・・・) 人間の目では認識しづらい1dpの差も検出してくれるため、UIに意図しない変更が含まれていないかを容易に判断できます。 DroidKaigi 2024などのカンファレンスでも多くのセッションがあり、最近話題になっていると感じます。 スクリーンショットテストライブラリの選定 カンファレンスなどでの発表も多く、弊社の他のAndroid向けアプリでも導入実績のある roborazzi を採用することにしました。 なにはともあれ実行してみる 公式 のセットアップ手順やインターネット上の情報をもとに導入し、ローカル環境で実行してみます。 ひとまず特殊なことはしていないので、さっくり進めていきます。 まずはスクリーンショットテスト実行に必要なライブラリを導入します。 バージョンカタログ(libs.versions.toml) [versions] robolectric = "4.13" roborazzi = "1.29.0" [libraries] androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi" } roborazzi-compose = { module = "io.github.takahirom.roborazzi:roborazzi-compose", version.ref = "roborazzi" } roborazzi-junit-rule = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi" } [plugins] roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } rootのbuild.gradle.ktsファイル plugins { alias(libs.plugins.roborazzi) version libs.versions.roborazzi apply false } moduleのbuild.gradle.ktsファイル plugins { alias(libs.plugins.roborazzi) } android { testOptions { unitTests { isIncludeAndroidResources = true all { it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" } } } } dependencies { // robolectric testImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.test.manifest) testImplementation(libs.robolectric) // roborazzi testImplementation(libs.roborazzi) testImplementation(libs.roborazzi.compose) testImplementation(libs.roborazzi.junit.rule) } これで必要なライブラリを導入できたはずです。 下記のようなテスト用クラスを作成してとりあえずローカルで実行してみましょう! import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH import com.github.takahirom.roborazzi.captureRoboImage import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.GraphicsMode @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class ScreenShotTestSample { @get:Rule val composeTestRule = createComposeRule() @Test fun sample() { composeTestRule.apply { setContent { MaterialTheme { Surface { Text(text = "screen shot test sample") } } } onRoot().captureRoboImage( filePath = "$DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH/sample.png" ) } } } 下記のスクリーンショット保存コマンドをAndroid Studioのターミナルから実行すれば Text(text = ・・・) のスクリーンショットがpngとして出力されるはずです。 ./gradlew recordRoborazziDebug ![](/assets/blog/authors/f.tsuji/2024-12-13/folder_empty_roborazzi.png =750x) なんと、何も出力されませんでした。 予想外の結果です。 テストがスキップされる コマンド実行は成功しているのに、なぜかスクリーンショットの結果が出力されませんでした。 調査しつつローカルでユニットテストを実行してみると、以下のような表示がされていることに気がつきました。 ![](/assets/blog/authors/f.tsuji/2024-12-13/Test_events_were_not_received.png =750x) なぜかテストイベントを受信できておらずスキップされているようです。 たしかに、それであればコマンド成功かつ結果出力なしも納得できます。 さらに調査を進めていった結果、JUnitバージョンの競合が原因のようでした。 弊アプリでの通常のユニットテストはkotestを使用しているのですが、そちらのテストランナーはJUnit5、 roborazzi(というよりもrobolectric)のテストランナーがJUnit4であることが問題でした。 なんと roborazziのissueにも似たような問題がありました。 解決策は、上記issueにもある通り junit-vintage-engine という ライブラリ を使用することでした。 上記ライブラリはざっくりいうとJUnit4とJUnit5を共存させることができ、移行にも使われることがあるとのことです。 それでは junit-vintage-engine を導入し、再度コマンドを実行してみることにします。 まずは依存関係を追加(前段で導入した部分は割愛) バージョンカタログ(libs.versions.toml) [versions] junit-vintage-engine = "5.11.2" [libraries] junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit-vintage-engine" } moduleのbuild.gradle.ktsファイル dependencies { testImplementation(libs.junit.vintage.engine) } 再度スクリーンショット保存コマンドを実行します。今度こそ Text(text = ・・・) のスクリーンショットがpngとして出力されるはずです。 ![](/assets/blog/authors/f.tsuji/2024-12-13/build_failure.png =750x) 実行結果 :::message alert junit-vintage-engine を導入し、JUnitバージョンを共存させたことでテストは実行できましたが、結果は失敗でした。 ::: なにかしらの初期化に失敗 なんと実行に失敗してしまったようです。 ライブラリを追加する前は実行すらできていなかったので、ポジティブに一歩前進と捉えて愚直に解決を目指しましょう。トライアンドエラーです。 問題解決の最初の一手ということでログを見てみると、以下が出力されていました。 ![](/assets/blog/authors/f.tsuji/2024-12-13/build_failure_log.png =750x) なにかしらの初期化に失敗しているようです。 出力されたログを確認していくと、どうも弊アプリのApplicationクラスを初期化しようとしているみたいです。 Robolectricの設定 を詳しく調査すると、以下の記載を見つけました。 Robolectric will attempt to create an instance of your Application class as specified in the AndroidManifest. Robolectricは、 AndroidManifest.xml で指定されたApplicationクラスのインスタンスを作成しようとするようです。 ログの内容と一致しました! 初期化に失敗しないよう、公式を参考に @Config アノテーションを使用しシンプルなApplicationクラスを使用するよう伝えてみることにします。 import android.app.Application import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config(application = Application::class) // 追加 @GraphicsMode(GraphicsMode.Mode.NATIVE) class ScreenShotTestSample { 期待を込めて、スクリーンショット保存コマンド(割愛)をAndroid Studioのターミナルから実行します。 さすがに今度こそText(text = ・・・)のスクリーンショットがpngとして出力されるはずです。 ![](/assets/blog/authors/f.tsuji/2024-12-13/build_success.png =250x) ![](/assets/blog/authors/f.tsuji/2024-12-13/folder_roborazzi.png =750x) ![](/assets/blog/authors/f.tsuji/2024-12-13/result_sample.png =250x) スクリーンショットが出力されました! :::message Robolectric の設定を調査し、 @Config アノテーションを用いて使用するApplicationクラスを明示することでスクリーンショットテストが成功しました。 ::: 比較(スクリーンショットテスト)も試してみるべく、テスト用クラスを少しだけ変更しコマンドを実行してみます。 // Text(text = "screen shot test sample") Text(text = "compare sample") // textを変更 ./gradlew compareRoborazziDebug ![](/assets/blog/authors/f.tsuji/2024-12-13/folder_compare_roborazzi.png =750x) 比較結果が出力されました! ![](/assets/blog/authors/f.tsuji/2024-12-13/result_sample_actual.png =250x) 比較画像 ![](/assets/blog/authors/f.tsuji/2024-12-13/result_sample_compare.png =750x) 比較結果 成功したのでここまでのまとめ 簡易的なテストクラスを作成し、無事にスクリーンショットテストを実行できました。 導入しているユニットテストやプロパティの指定など、プロジェクトによって細かい部分の調整が大変な印象ですが、 変更を目で確認できるのは明瞭かつ高速なため、可能であれば導入を検討するとアプリの品質を担保しやすくなるかと思います。 余談 弊アプリではこのタイミングでCIの実装も行っています。 GitHub Actions Workflowを使用し、スクリーンショットの保存・比較・コメントをPRにするところまで実現しています。 特別テクニカルなことをしているわけではないため割愛しますが、 公式 にあるように companion branch approach を用いて結果を保持する手法を選択しています。 (ymlファイルの統合などは行いましたが、導入する際の一般的なレベルの最適化程度です。) :::message ./gradlew recordRoborazziDebug を実行した際、同一モジュールに通常のUnitTestがあるとそちらも実行されてしまいます。 弊アプリでは独自のプロパティを定義し、UnitTestを実行するためのGradleタスクとRoborazziを用いたスクリーンショットテスト用のタスクとに分離し管理しています。 参考issue https://github.com/android/nowinandroid/issues/911 https://github.com/takahirom/roborazzi/issues/36 ::: 閑話休題。 Preview関数もテスト対象にする Composable関数を実装する際にPreview用関数も同時に作成することが多いと思います。 この記事の前半部分のように手動でテストも実装するとなると、 Composable実装 Preview実装 テスト実装 となかなか大変です・・・。 せっかくならばPreview関数をそのままスクリーンショットテストの対象にしてしまおう!というのが本項の内容です。 こちらも特に話題の中心になっている技術ですね。 なにはともあれ実行してみる 手順は概ね以下です。 Preview関数を収集する 収集した関数をスクリーンショットテストする シンプルですね。 Preview関数の収集にはさまざまな方法があると思いますが、今回は ComposablePreviewScanner を使用していこうと思います。 導入手順がまとめられていること(調査がしやすいこと)、将来的に公式でも連携・サポートされそうなこと( 執筆時点ではExperimental )が主な理由です。 では、必要なライブラリを導入していきましょう。 バージョンカタログ(libs.versions.toml) [versions] composable-preview-scanner = "0.3.2" [libraries] composable-preview-scanner = { module = "io.github.sergio-sastre.ComposablePreviewScanner:android", version.ref = "composable-preview-scanner" } moduleのbuild.gradle.ktsファイル dependencies { // screenshot testing(Composable Preview) testImplementation(libs.composable.preview.scanner) } 続いて、composable-preview-scannerのREADMEや先人たちの情報を参考にテスト用クラスを作成してみましょう。 import android.app.Application import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH import com.github.takahirom.roborazzi.captureRoboImage import com.kinto.unlimited.ui.compose.preview.annotation.DialogPreview import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.ParameterizedRobolectricTestRunner import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview @RunWith(ParameterizedRobolectricTestRunner::class) class ComposePreviewTest( private val preview: ComposablePreview<AndroidPreviewInfo> ) { @get:Rule val composeTestRule = createComposeRule() @Config(application = Application::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Test fun snapshot() { val fileName = AndroidPreviewScreenshotIdBuilder(preview).ignoreClassName().build() val filePath = "$DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH/$fileName.png" // Preview関数名.png composeTestRule.apply { setContent { preview() } onRoot().captureRoboImage(filePath = filePath) } } companion object { private val cachedPreviews: List<ComposablePreview<AndroidPreviewInfo>> by lazy { AndroidComposablePreviewScanner() .scanPackageTrees( include = listOf("・・・"), // Preview関数を探すパッケージを設定する exclude = listOf() ) .includePrivatePreviews() // PrivateなPreview関数も含める .getPreviews() } @JvmStatic @ParameterizedRobolectricTestRunner.Parameters fun values(): List<ComposablePreview<AndroidPreviewInfo>> = cachedPreviews } } では、スクリーンショット保存コマンドを実行します。 ・・・したのですが。 やたらと時間がかかり1時間経っても終わる気配がありません。Preview関数の数も200弱なのでそこまでかかるかな?といった感じです。さすがにすんなりとはいかないですね。 :::message alert Preview関数をスクリーンショットテストの対象に含めたところ、テスト実行にとても長い時間がかかるようになってしまいました。 ::: テスト実行に時間がかかる 色々調べていくうちに似たような症例を見つけました。 https://github.com/takahirom/roborazzi/issues/388 1つのテストを完了するのに約5分かかり CircularProgressIndicator を削除したところ高速で実行されたとのこと。 Issueのやりとりを深掘りしていくと、どうやら無限アニメーションを含むComposableをテストしようとすると時間がかかっている模様。 解決策としては mainClock.autoAdvance = false を設定し、Compose UIとの自動同期を停止した上で手動で時間を進める方法が紹介されていました。 手動で時間を操作することで任意のタイミングでスクリーンショットを撮影できるため無限アニメーションによる影響を受けない、ということのようです。 弊アプリでもまさに CircularProgressIndicator を使用しているため、これは試すしかない・・・!と早速実装してみます。 mainClock. ・・・ の箇所をテストクラスに追加しました。 時間を止め、 1_000ミリ秒(1秒)進め、 スクリーンショットを撮影し、 時間停止を解除する。 という流れですね。 composeTestRule.apply { mainClock.autoAdvance = false // 1 setContent { preview() } mainClock.advanceTimeBy(1_000) // 2 onRoot().captureRoboImage(filePath = filePath) // 3 mainClock.autoAdvance = true // 4 } (別件ですが、画像を非同期で読み込みための coil というライブラリがありますが、そちらも 正しくテストできない可能性があるとのこと。 ただ解決方法は同様そうなのでまとめて対応できそうです。) さあ、実行です!!! ![](/assets/blog/authors/f.tsuji/2024-12-13/build_failure_time.png =500x) 高速で実行できるようになったものの、失敗してしまいました。 :::message alert 無限アニメーションがボトルネックとなっていましたが、テスト時に手動で時間を操作することで解消。高速でテストを実行できるようになりましたが、結果は失敗でした。 ::: ダイアログを含んでいると失敗する 実行速度は上がったものの、失敗しては意味がありません。こういう時に頼れるのはやはりログですね。 ComposePreviewTest > [17] > snapshot[17] FAILED java.lang.AssertionError: fail to captureRoboImage Reason: Expected exactly '1' node but found '2' nodes that satisfy: (isRoot) Nodes found: 1) Node #534 at (l=0.0, t=0.0, r=0.0, b=0.0)px 2) Node #535 at (l=0.0, t=0.0, r=320.0, b=253.0)px Has 1 child at androidx.compose.ui.test.SemanticsNodeInteraction.fetchOneOrThrow(SemanticsNodeInteraction.kt:178) at androidx.compose.ui.test.SemanticsNodeInteraction.fetchOneOrThrow$default(SemanticsNodeInteraction.kt:150) at androidx.compose.ui.test.SemanticsNodeInteraction.fetchSemanticsNode(SemanticsNodeInteraction.kt:84) at com.github.takahirom.roborazzi.RoborazziKt.captureRoboImage(Roborazzi.kt:278) at com.github.takahirom.roborazzi.RoborazziKt.captureRoboImage(Roborazzi.kt:268) at com.github.takahirom.roborazzi.RoborazziKt.captureRoboImage$default(Roborazzi.kt:263) at com.kinto.unlimited.ui.compose.ComposePreviewTest.snapshot(ComposePreviewTest.kt:49) 上記のようなログが複数出力されていました。2つのnodeがあるとのこと。 ログの情報で検索すると 気になるIssue を見つけました。 compose-multiplatform についてなので原因は違うかもしれませんが、たしかに失敗しているのは Dialog()Composable を使用している箇所のようでした。 諦めるしかないのか・・・と悩んでいたところ、Experimentalとしてダイアログを含むイメージをキャプチャする 関数が追加されている とのことです! Experimentalのため全てのテストに使用するのは憚られますが、テスト対象がダイアログかどうかを判定できれば成功するかもしれません・・・。 ということで DialogPreview() というカスタムアノテーションを作成し、ダイアログを含むPreviewに付与、テストクラスで情報を取得し判定することにしました。 annotation class DialogPreview() @OptIn(ExperimentalRoborazziApi::class) // 追加 @Config(application = Application::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Test fun snapshot() { val isDialog = preview.getAnnotation<DialogPreview>() != null // 追加 composeTestRule.apply { mainClock.autoAdvance = false setContent { preview() } mainClock.advanceTimeBy(1_000) if (isDialog) { // 追加 captureScreenRoboImage(filePath = filePath) } else { onRoot().captureRoboImage(filePath = filePath) } mainClock.autoAdvance = true } } companion object { private val cachedPreviews: List<ComposablePreview<AndroidPreviewInfo>> by lazy { AndroidComposablePreviewScanner() .scanPackageTrees( include = listOf("・・・"), exclude = listOf() ) .includePrivatePreviews() .includeAnnotationInfoForAllOf(DialogPreview::class.java) // 追加 .getPreviews() } } previewに DialogPreviewアノテーション が付与されているか(nullではないか)を判定し、ダイアログの場合はcaptureScreenRoboImage()を使用するようにしました。 実行してみます。 ![](/assets/blog/authors/f.tsuji/2024-12-13/build_success.png =250x) ![](/assets/blog/authors/f.tsuji/2024-12-13/folder_roborazzi_preview.png =750x) Preview関数の性格上、画像やファイル名をマスクしています。 Preview関数を読み込み、それらのスクリーンショットを保存できました! :::message ダイアログを含むComposableは、Nodeを複数持ってしまうため正しくRootを判断できずにテスト実行で失敗していました。 Experimentalではありますが、ダイアログをキャプチャできる関数を使用することで正常にテストを実行できました。 ::: スクリーンショットの比較は前段で確認済なので、そのままで問題なさそうです。 長々とお付き合いいただきありがとうございました。 最後のまとめ この記事では、スクリーンショットテストの導入の流れを追いながら、つまづいた点とその解決方法について紹介しました。 当初はライブラリを追加して実行するだけだと思っていましたが、なかなかうまくいかず、試行錯誤を通じてなんとかスクリーンショットテストを実行できるようになりました。 つまづいた点の解決方法はライブラリの追加やアノテーションの付与など、対応としては単純でしたが、そこに辿り着くまでの情報が少なく、意外と大変でした。 この記事が少しでも皆さんのお役に立てば幸いです。