Upgrade to Pro — share decks privately, control downloads, hide ads and more …

EightにおけるAndroidのリアーキテクチャ/Android's re-architecture at Eight

Sansan
November 12, 2018

EightにおけるAndroidのリアーキテクチャ/Android's re-architecture at Eight

■イベント
Sansan Builders Box 2018
https://jp.corp-sansan.com/sbb2018/

■登壇概要
タイトル:「EightにおけるAndroidのリアーキテクチャ」
登壇者:Eight事業部 Mobile App Group エンジニア 山本 純平

▼Sansan Builders Box
https://buildersbox.corp-sansan.com/

Sansan

November 12, 2018
Tweet

More Decks by Sansan

Other Decks in Technology

Transcript

  1. Eightにおける
    Androidアプリのリアーキテクチャ

    View Slide

  2. ⼭本純平(Jumpei Yamamoto)
    ೥ ݄ 4BOTBOגࣜձࣾ ೖࣾ
    &JHIU"OESPJEΞϓϦέʔγϣϯ։ൃ
    σʔλ෼ੳ
    ʮ,PUMJOΠϯΞΫγϣϯʯͷ຋༁ʹࢀՃ
    Eight事業部 モバイル・アプリケーショングループ

    View Slide

  3. Sansan Builders Box
    質問などはsli.doで
    https://app2.sli.do/event/qlew0bqo
    sli.do -> 「#SBB1-1」

    View Slide

  4. Sansan Builders Box
    本Sessionについて
    EightのV9リリースを前にAndroid版で⾏っている⼤規模なアー
    キテクチャの刷新について、どのような⽅向を⽬指しているの
    かについて紹介したいと思います。

    View Slide

  5. Sansan Builders Box
    Agenda
    - データの流れを整理する
    - ドメインオブジェクトを定義する
    - マルチモジュール化
    - まとめ

    View Slide

  6. Sansan Builders Box
    前提とする構成
    • 基本的にはサーバのデータを取
    得 or 更新するアプリケーション
    • ビジネスロジックはサーバ側で
    実装
    • データをサーバから取得
    • ローカルのデータベースに
    キャッシュ
    • UIで表⽰
    データベース
    UI
    サーバ

    View Slide

  7. Sansan Builders Box
    Agenda
    - σʔλͷྲྀΕΛ੔ཧ͢Δ
    - ドメインオブジェクトを定義する
    - マルチモジュール化
    - まとめ
    ࣭໰ͳͲ͸sli.do → ʮ#SBB1-1ʯ

    View Slide

  8. データの流れを整理する
    ࣭໰ͳͲ͸sli.do → ʮ#SBB1-1ʯ

    View Slide

  9. Sansan Builders Box
    ケース: データの更新を考える
    • 初回データをロードして表⽰
    • メッセージ送信時に、表⽰を更新
    • サーバからのPushをトリガーにし
    てメッセージを受信
    クラウド
    දࣔΛߋ৽͢ΔτϦΨ͕ෳ਺͋
    ΓɺҰͭͷActivity্ͰॲཧΛ͢
    Δͷ͸େม
    Pushで表⽰の
    更新
    ⾃分でメッセージを送信
    したときに更新

    View Slide

  10. Sansan Builders Box
    解決⽅法
    • 扱うデータを⼀箇所で管理する
    • データの流れを統⼀する

    View Slide

  11. Sansan Builders Box
    データの流れを整理するために
    1. UIはモデルの変更を受け取ってそれを反映する
    2. モデルの変更とUIの更新は分離する
    ࢀߟ: FluxΞʔΩςΫνϟ

    View Slide

  12. データの流れに関する原則1
    UIはモデルの変更を受け取ってそれを反映する

    View Slide

  13. Sansan Builders Box
    • モデルはデータを⼀箇所で管理
    • UIはモデルの変更をObserveする
    • UIの更新はモデルの変更通知からのみ⾏う
    UIはモデルの変更を受け取ってそれを反映する
    UI UI
    データベース
    サーバ

    View Slide

  14. Sansan Builders Box
    UIにデータを供給するためのInterface
    • UIからみたモデルのレイヤをStoreと
    名付ける。
    • UIはStoreからデータを取得する
    • Storeはデータベースに変更があった
    場合にUIに対して変更通知を送る
    UI
    Store
    データベース

    View Slide

  15. Sansan Builders Box
    Storeインターフェイス
    interface ObjectStore{
    val value: T
    fun updates(): Observable
    }
    interface ArrayStore{
    val size: Int
    fun get(index: Int): T
    fun updates(): Observable
    }
    0CKFDU4UPSF୯ҰͷΦϒ
    δΣΫτ
    "SSBZ4UPSFΦϒδΣΫτ
    ͷ$PMMFDUJPO
    3Y+BWBΛ͔ͭͬͯ7JFXʹ
    มߋΛ௨஌͢Δ

    View Slide

  16. Sansan Builders Box
    Storeのデータベースでの実装
    • データベースライブラリにて、Storeイン
    ターフェイスを実装
    • Realm
    • SQLite
    • SharedPreferences
    Store
    データベース

    View Slide

  17. Sansan Builders Box
    データベースでの変更を通知する
    • Realm, SharedPreferencesにはデータベース⾃
    ⾝に変更を通知する機能を持つ
    • SQLiteの場合は保存時にRxJavaのRxRelayを
    使って、変更があったことをStoreに伝える
    Store
    データベース

    View Slide

  18. データの流れに関する原則2
    モデルの変更とUIの更新は分離する

    View Slide

  19. Sansan Builders Box
    UIでサーバから値を取得し、更新する
    // ඇಉظͷ௨৴ίʔϧόοΫ
    view.setOnClickListener{
    api.updateData(data){
    saveData(it.response)
    textView.text = it.response
    }
    }
    Activityͷίʔυ
    UI
    サーバから取
    得したデータ
    でUIを更新

    View Slide

  20. Sansan Builders Box
    UseCaseクラス
    • UIでアクションがあった場合
    は、UseCaseを実⾏する
    • UseCaseは通信等を⾏い、
    データベースを書き換える
    UI
    データベース
    ΞΫγϣϯ
    UseCase
    อଘ

    View Slide

  21. Sansan Builders Box
    UIからのアクション
    val usecase: updateDataUseCase = ...
    view.setOnClickListener {
    useCase.execute()
    } UIでのアクションに
    よってUseCaseを実⾏

    View Slide

  22. Sansan Builders Box
    UseCaseの実装例
    class GetPostUseCase (....): UseCase1 {
    override fun buildSingle(postId: PostId): Single =
    apiProvider.get().getPost(postId)
    .doOnNext { postRepository.savePost(it) }
    .single(Unit)
    }

    View Slide

  23. Sansan Builders Box
    UseCaseの実装例
    class GetPostUseCase (....): UseCase1 {
    override fun buildSingle(postId: PostId): Single =
    apiProvider.get().getPost(postId)
    .doOnNext { postRepository.savePost(it) }
    .single(Unit)
    }
    RxJavaのSingleを使い
    ioスレッドで実⾏

    View Slide

  24. Sansan Builders Box
    UseCaseの実装例
    class GetPostUseCase (....): UseCase1 {
    override fun buildSingle(postId: PostId): Single =
    apiProvider.get().getPost(postId)
    .doOnNext { postRepository.savePost(it) }
    .single(Unit)
    }
    サーバから
    データを取得

    View Slide

  25. Sansan Builders Box
    UseCaseの実装例
    class GetPostUseCase (....): UseCase1 {
    override fun buildSingle(postId: PostId): Single =
    apiProvider.get().getPost(postId)
    .doOnNext { postRepository.savePost(it) }
    .single(Unit)
    }
    データベースに保存

    View Slide

  26. Sansan Builders Box
    UIアクションからのデータの流れを統⼀する
    UI
    データベース
    ΞΫγϣϯ
    UseCase
    อଘ

    View Slide

  27. データの流れまとめ

    View Slide

  28. Sansan Builders Box
    データの流れまとめ
    • UIはStoreからデータを
    取得し表⽰を⾏う
    UI
    Store
    データベース
    UseCase
    දࣔ

    View Slide

  29. Sansan Builders Box
    データの流れまとめ
    • Viewからデータの更新を
    ⾏う場合はUseCaseを利
    ⽤してデータベースを更
    新する
    UI
    Store
    データベース
    ΞΫγϣϯ
    UseCase
    อଘ

    View Slide

  30. Sansan Builders Box
    データの流れまとめ
    • データベースの更新は
    Store経由でUIに通知さ
    れる
    UI
    Store
    データベース
    UseCase
    ௨஌

    View Slide

  31. Sansan Builders Box
    データの流れまとめ
    • UIはStoreからの更新の
    通知を受け取ってViewを
    更新する
    UI
    Store
    データベース
    UseCase
    ߋ৽

    View Slide

  32. Sansan Builders Box
    ケース: データの更新を考える
    データ更新のトリガが複数
    あっても、UIはつねにStore
    からの更新のみを⾒ていれば
    よい
    クラウド
    Store
    データベース
    Push௨஌
    ʹΑΔߋ৽
    UseCase
    ViewͷτϦΨ
    ʹΑΔߋ৽

    View Slide

  33. Sansan Builders Box
    データの流れについて、まとめ
    アプリ内で扱うデータの流れを整理することで、複数
    の画⾯やバックグラウンドの通信など、複数の状況で
    更新されるデータを簡単に管理することができる

    View Slide

  34. Sansan Builders Box
    Agenda
    - データの流れを整理する
    - υϝΠϯΦϒδΣΫτΛఆٛ͢Δ
    - マルチモジュール化
    - まとめ
    ࣭໰ͳͲ͸sli.do → ʮ#SBB1-1ʯ

    View Slide

  35. ドメインオブジェクトを定義する
    ࣭໰ͳͲ͸sli.do → ʮ#SBB1-1ʯ

    View Slide

  36. Sansan Builders Box
    ドメインオブジェクトを定義する
    • 画⾯の表⽰項⽬に合わせたドメインオブジェクト
    • プリミティブな型をラップする
    • ドメインオブジェクトとライブラリの依存関係

    View Slide

  37. 画⾯の表⽰項⽬に合わせた
    ドメインオブジェクト

    View Slide

  38. Sansan Builders Box
    これまでの実装
    • ローカルのデータベースに
    は基本的にサーバから取得
    したJSONの構造がそのま
    ま保存される
    • UIではローカルのデータ
    ベースに保存されたデータ
    をそのまま使⽤していた
    UI
    Store
    データベース

    View Slide

  39. Sansan Builders Box
    保存データと表⽰項⽬のミスマッチ
    UIの表⽰ロジックが複雑化
    σʔλϕʔε্Ͱͷఆٛ
    ໊લʹfirstCardͷfullName
    kindΛݟͯiconʹछผΛબ୒
    ໊ࢗը૾ͷURL͸cardId͔Βߏங
    etc.

    View Slide

  40. Sansan Builders Box
    Eightシステム全体から⾒た
    モバイル・アプリケーションの役割とは?
    • システムのドメインロジックはサーバ
    で実装
    • モバイル・アプリケーションの⼤部分
    の実装はサーバのデータを取得して表
    ⽰、更新
    • モバイル・アプリケーション側でドメ
    インロジックを持つことはほぼない
    モバイル
    アプリケーション
    サーバ
    ϞόΠϧɾΞϓϦέʔγϣϯ͸
    ը໘දࣔʹಛԽͨ͠γεςϜͰ͋Δ

    View Slide

  41. Sansan Builders Box
    ドメインオブジェクト
    ドメインオブジェクトは
    画⾯上の表⽰項⽬に
    特化したものとして定義

    View Slide

  42. Sansan Builders Box
    表⽰項⽬に特化したドメインオブジェクト
    σʔλϕʔε্Ͱͷఆٛ υϝΠϯΦϒδΣΫτ
    表⽰⽤のiconリソースIDや
    URLを直接含む

    View Slide

  43. Sansan Builders Box
    表⽰項⽬に特化したドメインオブジェクト
    • データベースなどの外部ライブラリの影響を受けない
    • UI(ViewModelやPresenter等)で複雑な変換ロジックをも
    つ必要がない
    • ドメインオブジェクトへの変換処理とUIと切り離すことがで
    きる
    • アプリの表⽰仕様をドメインロジックとしてまとめることが
    できる

    View Slide

  44. プリミティブ型をラップする

    View Slide

  45. Sansan Builders Box
    従来のオブジェクト定義
    型だけでは何を表しているかわからないし、他のLong型の値も
    代⼊できてしまうので、独⾃の型を定義したい。
    でもアプリのいたるところでこの値が使われているし、これを置
    き換えるのは⼤変なのでは
    idがLong型で定義されている!

    View Slide

  46. Sansan Builders Box
    とりあえずdata classを定義してしまう
    • ⼀度にすべてのidを置き換えることは考えない
    • いつでもプリミティブ型の値が取り出せるので、元のソース
    と互換をとるのは簡単
    • 置き換えやすいところから少しずつ置き換えていく
    • 置き換えたところでは確実に効果が出る!
    data class CardId(val rawValue: Long)

    View Slide

  47. ドメインオブジェクトと
    ライブラリの依存関係

    View Slide

  48. Sansan Builders Box
    これまでの実装
    • UIからはStore経由でデータベースに
    依存しているため、UIでもデータベー
    スの変更などの影響をうけてしまう
    UI
    Store
    データベース

    View Slide

  49. Sansan Builders Box
    ドメインオブジェクトの導⼊
    • UIはStoreインターフェイ
    スとドメインオブジェク
    トのみを参照
    UI
    Store
    Interface
    ドメイン
    オブジェクト

    View Slide

  50. Sansan Builders Box
    ドメインオブジェクトの導⼊
    • UIはStoreインターフェイ
    スとドメインオブジェク
    トのみを参照
    • データベースレイヤにて
    Storeインターフェイスを
    実装
    UI
    Store
    Interface
    ドメイン
    オブジェクト
    StoreImpl
    データベース

    View Slide

  51. Sansan Builders Box
    ドメインオブジェクトの導⼊
    • UIはStoreインターフェイ
    スとドメインオブジェク
    トのみを参照
    • データベースレイヤにて
    Storeインターフェイスを
    実装
    • UIへはDIを使ってInject
    UI
    Store
    Interface
    ドメイン
    オブジェクト
    StoreImpl
    データベース
    Inject
    Component

    View Slide

  52. Sansan Builders Box
    ドメインオブジェクトまとめ
    • イミュータブルなドメインオブジェクトを定義することに
    よって、アプリはデータベースなど個別のライブラリの影響
    を最⼩限にとどめることができる
    • ビジネスロジックをサーバに持つモバイル・アプリケーショ
    ンはデータの閲覧に特化したシステムとしてドメインオブ
    ジェクトを作成する

    View Slide

  53. Sansan Builders Box
    Agenda
    - データの流れを整理する
    - ドメインオブジェクトを定義する
    - ϚϧνϞδϡʔϧԽ
    - まとめ
    ࣭໰ͳͲ͸sli.do → ʮ#SBB1-1ʯ

    View Slide

  54. マルチモジュール化
    ࣭໰ͳͲ͸sli.do → ʮ#SBB1-1ʯ

    View Slide

  55. Sansan Builders Box
    なぜマルチモジュールにするか?
    • ビルドの⾼速化
    • ⼤規模なコードを分割してわかりやすくしたい
    • モジュール間の循環依存は許されないため、依存関係を強制した

    View Slide

  56. Sansan Builders Box
    モジュール分割⽅針
    • ⽔平⽅向(レイヤごと)に分ける
    • 垂直⽅向(機能別)に分ける

    View Slide

  57. Sansan Builders Box
    ドメインオブジェクトを導⼊したライブラリの依存関係
    UI
    Store
    Interface
    ドメイン
    オブジェクト
    StoreImpl
    データベース
    Component

    View Slide

  58. Sansan Builders Box
    ドメインオブジェクトを導⼊したライブラリの依存関係
    UI
    Store
    Interface
    ドメイン
    オブジェクト
    StoreImpl
    データベース
    Component
    app: モジュール
    ui_component:
    モジュール
    repository:
    モジュール
    domain:
    モジュール

    View Slide

  59. Sansan Builders Box
    ⽔平⽅向でのモジュール分割
    ui_component:
    libraries:
    network:
    repository:
    domain:
    utils:
    app:

    View Slide

  60. Sansan Builders Box
    ⽔平⽅向でのモジュール分割
    • app: アプリケーションのエ
    ントリポイント、DIによる
    依存関係の解決を⾏う
    ui_component:
    libraries:
    network:
    repository:
    domain:
    utils:
    app:

    View Slide

  61. Sansan Builders Box
    ⽔平⽅向でのモジュール分割
    • ui_component: Activityに始
    まるUIのコード
    ui_component:
    libraries:
    network:
    repository:
    domain:
    utils:
    app:

    View Slide

  62. Sansan Builders Box
    ⽔平⽅向でのモジュール分割
    • libraries: Viewの共通ライブ
    ラリとUseCase
    ui_component:
    libraries:
    network:
    repository:
    domain:
    utils:
    app:

    View Slide

  63. Sansan Builders Box
    ⽔平⽅向でのモジュール分割
    • network: 通信系ライブラリ

    ui_component:
    libraries:
    network:
    repository:
    domain:
    utils:
    app:

    View Slide

  64. Sansan Builders Box
    ⽔平⽅向でのモジュール分割
    • repository: データベース系
    ライブラリ群。
    ui_component:, libraries:か
    らは直接参照されない
    ui_component:
    libraries:
    network:
    repository:
    domain:
    utils:
    app:

    View Slide

  65. Sansan Builders Box
    ⽔平⽅向でのモジュール分割
    • domain: ドメインオブジェ
    クト、Storeインターフェイ

    ui_component:
    libraries:
    network:
    repository:
    domain:
    utils:
    app:

    View Slide

  66. Sansan Builders Box
    ⽔平⽅向でのモジュール分割
    • utils: 共通ユーティリティ
    ui_component:
    libraries:
    network:
    repository:
    domain:
    utils:
    app:

    View Slide

  67. Sansan Builders Box
    ⽔平⽅向でのモジュール分割のメリット
    • 下位のレイヤが上位のレイヤ
    に依存していないことを保証
    • 特にdomainモジュールがDB
    などの外部ライブラリに依存
    しないこと
    • uiのモジュールがrepositoryモ
    ジュールを直接参照しないこ
    とを強制できる
    ui_component:
    libraries:
    network
    repository
    domain
    utils
    app:

    View Slide

  68. Sansan Builders Box
    垂直⽅向(機能ごと)のモジュール分割
    メイン
    画⾯
    プロフィール
    表⽰画⾯
    カメラ
    機能

    View Slide

  69. Sansan Builders Box
    機能別モジュール分割例
    component
    main:
    component
    profile:
    component
    camera:
    library
    profile:
    camera
    device:
    domain:
    repository:
    app:

    View Slide

  70. Sansan Builders Box
    機能別モジュール分割例
    • app: アプリケーションのエントリ
    ポイント、DIによる依存関係の解
    決を⾏う
    component
    main:
    component
    profile:
    component
    camera:
    library
    profile:
    camera
    device:
    domain:
    repository:
    app:

    View Slide

  71. Sansan Builders Box
    機能別モジュール分割例
    • component_xxx:はそれぞれの機
    能毎のUI関連のコードが⼊るモ
    ジュール
    component
    main:
    component
    profile:
    component
    camera:
    library
    profile:
    camera
    device:
    domain:
    repository:
    app:

    View Slide

  72. Sansan Builders Box
    機能別モジュール分割例
    • library_xxx: 機能毎のライブラリ
    とUseCase
    component
    main:
    component
    profile:
    component
    camera:
    library
    profile:
    camera
    device:
    domain:
    repository:
    app:

    View Slide

  73. Sansan Builders Box
    機能別モジュール分割例
    • camera_device: cameraからしか
    使⽤されないデバイス関連モ
    ジュール
    component
    main:
    component
    profile:
    component
    camera:
    library
    profile:
    camera
    device:
    domain:
    repository:
    app:

    View Slide

  74. Sansan Builders Box
    機能別モジュール分割例
    • 機能毎にUI上のモジュールはわか
    れるが、各画⾯でドメインオブ
    ジェクトは共通に使⽤される
    component
    main:
    component
    profile:
    component
    camera:
    library
    profile:
    camera
    device:
    domain:
    repository:
    app:

    View Slide

  75. Sansan Builders Box
    機能別モジュール分割例
    • データベースも、アプリ全体で
    relationを保っているため⼀つの
    モジュールとして実装される
    component
    main:
    component
    profile:
    component
    camera:
    library
    profile:
    camera
    device:
    domain:
    repository:
    app:

    View Slide

  76. Sansan Builders Box
    機能別モジュール分割例
    component
    main:
    component
    profile:
    component
    camera:
    library
    profile:
    camera
    device:
    domain:
    repository:
    app:
    おそらくこの部
    分も機能別に分
    割できるはず

    View Slide

  77. Sansan Builders Box
    機能別モジュール分割のメリット
    component
    main:
    component
    profile:
    component
    camera:
    library
    profile:
    camera
    device:
    domain:
    repository:
    app:
    • 機能別にコードがまとまっている
    ので理解しやすい
    • 修正の影響範囲を最⼩限の抑える
    ことができる
    • ビルドを並列に実⾏することがで
    きる

    View Slide

  78. Sansan Builders Box
    循環依存するケース
    画⾯遷移上、異なるモ
    ジュールのUIでも循環依存
    んするケースが発⽣する
    ϓϩϑΟʔϧը໘ ϝοηʔδը໘
    メッセージ
    を送る
    プロフィー
    ルを表⽰

    View Slide

  79. Sansan Builders Box
    循環依存するケース
    component
    profile:
    プロフィール表⽰
    component
    message:
    メッセージ画⾯
    app:
    ϝοηʔδը໘Λىಈ
    ϓϩϑΟʔϧը໘Λىಈ

    View Slide

  80. Sansan Builders Box
    共通のインターフェイスを定義
    • 相互の画⾯の
    Intentを取得する
    ためのInterfaceを
    準備
    interface IntentResolver{
    fun getProfileIntent(): Intent
    fun getMessageIntent(): Intent
    }
    component
    profile:
    プロフィール表⽰
    component
    message:
    メッセージ画⾯
    app:

    View Slide

  81. Sansan Builders Box
    DI経由で解決
    • 相互の画⾯の
    Intentを取得する
    ためのInterfaceを
    準備
    • app:モジュールに
    てその実装を⾏い、
    各モジュールに
    Injectする
    interface IntentResolver{
    fun getProfileIntent(): Intent
    fun getMessageIntent(): Intent
    }
    component
    profile:
    プロフィール表⽰
    component
    message:
    メッセージ画⾯
    class IntentResolverImpl: IntentResolver{
    override fun getProfileIntent() = Intent(...)
    override fun getMessageIntent() = Intent(...)
    }
    app:

    View Slide

  82. Sansan Builders Box
    機能別モジュール分割のメリット
    • 機能別にコードがまとまっている
    ので理解しやすい
    • 修正の影響範囲を抑えることがで
    きる
    • ビルドを並列に実⾏することがで
    きる
    component
    main:
    component
    profile:
    component
    camera:
    library
    profile:
    camera
    device:
    domain:
    repository:
    app:

    View Slide

  83. Sansan Builders Box
    マルチモジュールに移⾏するデメリット
    • モノリシックなアプリから移⾏する場合、依存関係を解決す
    るのは⾮常に⼤変
    • singletonを多⽤
    • staticにApplicationを参照
    • ビルドの⾼速化の効果を計測するのは困難
    • 検証する環境が同じになるとは限らない
    • モジュール分割とコードの追加が同時に⾏われることも
    • ビルド⾼速化だけを⽬的とするなら良いマシンを!

    View Slide

  84. Sansan Builders Box
    マルチモジュール化まとめ
    • 機能毎にモジュールをまとめることにより、コードの影響範
    囲を限定できる
    • マルチモジュール化によりモジュールの依存関係を強制する
    ことができる
    • 特にdomainモジュールをpureに保つことができる
    • 依存関係が循環する場合はDIにて解決する
    • マルチモジュール化は⼤変

    View Slide

  85. Sansan Builders Box
    Agenda
    - データの流れを整理する
    - ドメインオブジェクトを定義する
    - マルチモジュール化
    - ·ͱΊ
    ࣭໰ͳͲ͸sli.do → ʮ#SBB1-1ʯ

    View Slide

  86. まとめ
    ࣭໰ͳͲ͸sli.do → ʮ#SBB1-1ʯ

    View Slide

  87. Sansan Builders Box
    まとめ
    • データの流れを整理することによってActivityを始めとする
    UIの実装をシンプルにすることができます。
    • モバイル・アプリケーションの場合、画⾯表⽰に特化したド
    メインオブジェクトを定義することによって実装がわかりや
    すくなります。
    • アプリケーションのコードをモジュールに分割してその依存
    関係を強制することで、全体的に理解しやすいコードにする
    ことができます。

    View Slide

  88. Sansan Builders Box
    質問などはsli.doで
    https://app2.sli.do/event/qlew0bqo
    sli.do -> 「#SBB1-1」

    View Slide

  89. View Slide