$30 off During Our Annual Pro Sale. View Details »

外部デバイスと密に連携するAndroidアプリに最適なアーキテクチャとは?

yurihondo
February 06, 2019

 外部デバイスと密に連携するAndroidアプリに最適なアーキテクチャとは?

ユーザーが画面操作しなくても頻繁に外部からのイベントで画面遷移が発生するアプリ、
あなたならどのようなアーキテクチャを採用しますか?

弊社ではタクシー配車サービスを提供しています。
そのため、タクシーメーターと連携するアプリを開発し、各タクシーに設置しています。

このアプリは画面操作はもちろんタクシーメーターの操作、またプッシュ通知をトリガーに画面遷移する必要があります。

このように外部デバイスと密に連携し、様々なイベントを処理するアプリに最適なアーキテクチャとは何なのでしょうか。

アーキテクチャはそのアプリごとに最適なものが異なる、と考えています。
少々特殊な事例ですが、我々の試行錯誤の結果、またどのように改善を進めていこうと考えているか、をお話しできればと思います。

キーワード

- MVVM
- Redux
- Flux
- StateMachine
- multi module

yurihondo

February 06, 2019
Tweet

More Decks by yurihondo

Other Decks in Programming

Transcript

  1.   Android   DroidKaigi 2019 Tomoya Miwa, Yuri

    Hondo
  2. $# tomoya0x00 '  ( ) Kiosk " ! &%RX-8(

     *) Twitter : @tomoya0x00 Pokemon GO (%Lv.40): 1024 1615 6009
  3.  URI   (   ) Android @DeNA

    - MOV    Twitter : URI - @yuyuyuyuyuri Pokemon GO : 05URI28 - 0188 7196 1789
  4.    "!    $ # $

     $ 4 1 4 3 2
  5.  

  6. None
  7. •   +*. ◦ -) OK • *%&!" ◦

      (,* $#'
  8. #    ("&)   $ !%/% !%$

     '
  9. c #     $ !%/% !%$ 

    '  ("&)
  10.     (Android) BLE Logger  

  11.   ➡    

  12.  

  13.      

  14. OK     

  15.       

  16.          

    
  17.    310  

  18.  310  

  19.        310  

     
  20.     1000   

  21.   

  22.   •     •  

     •  
  23.   •     •  

     •  
  24. https://dena.com/jp/press/003459

  25. • 20179"2% ◦  $  • "200 #!

  26. 

  27.     

  28.  '* • !+ )" • +$#(%&   •

      • !+ 
  29.   BLE Logger      

    BLE  
  30.  ,+, #  BLE Logger   )!%/%'(* $

    BLE !% &% %  "   
  31.  +*+ "   ( $/$&') # BT Classic

    !     $ %$ $ 
  32.  +. • !%!/-& • !/(',)*  •  

    # • "BLE BT Classic • !%!/$ #
  33.  

  34.  • ➡ • OK • ➡ • NG •

     
  35.      ⬇  

  36.    •  ➡  • OK •

     ➡  •  • ” ➡ ”   
  37. #  "  ! ⬇  

  38. 

  39.     

  40. •     • Rx ! ◦ !

  41.  14 • #*#53+ • #5.,2/0  •  

     ' • &BLE LoggerBT Classic • #*#5( ' • $"!%)-
  42. 

  43.  

  44.  •   ◦ // / •  

  45.        

  46. 

  47.        

  48.  → 

  49.   /  →   “ ”

  50.  !14 ✔%-%5 3. →'   ✔ #$* →)2"&(

    ✔%-%5+$* →' / ,0
  51.     

  52. 

  53.  

  54. 

  55. https://dena.com/jp/press/004421

  56.   

  57. https://dena.com/jp/press/004421

  58.    +  

  59. MOV or     

  60. Before

  61. After    

  62. 

  63.       

  64. 

  65.      "! 

  66. 

  67.     

  68. • Singleton & mutableclass •   •  

    
  69.    !

  70. MOV   

  71.  (  ) 

  72. ('* -  - % !& - ,#+  $)"&

    -   & '* 
  73.   

  74.  " •  • $# ! •  

  75.  " •  • $# ! •  

  76.    ! <https://github.com/tomoya0x00/statek>

  77. None
  78. None
  79. 

  80. None
  81. None
  82. None
  83. LED

  84. stateMachine(initial = NOT_LOANED) { state(NOT_LOANED) { edge<PressRental>(LOCK) } state(ON_LOAN, entry

    = { /* LED ON */ }, exit = { /* LED OFF */ }) { state(LOCK) { edge<PressUnLock>(UNLOCK) edge<PressLock>(NOT_LOANED, guard = { it.isLongPress }) } state(UNLOCK) { edge<PressLock>(LOCK, guard = { !it.isLongPress }) } } }
  85. stateMachine(initial = NOT_LOANED) { state(NOT_LOANED) { edge<PressRental>(LOCK) } state(ON_LOAN, entry

    = { /* LED ON */ }, exit = { /* LED OFF */ }) { state(LOCK) { edge<PressUnLock>(UNLOCK) edge<PressLock>(NOT_LOANED, guard = { it.isLongPress }) } state(UNLOCK) { edge<PressLock>(LOCK, guard = { !it.isLongPress }) } } }
  86. stateMachine(initial = NOT_LOANED) { state(NOT_LOANED) { edge<PressRental>(LOCK) } state(ON_LOAN, entry

    = { /* LED ON */ }, exit = { /* LED OFF */ }) { state(LOCK) { edge<PressUnLock>(UNLOCK) edge<PressLock>(NOT_LOANED, guard = { it.isLongPress }) } state(UNLOCK) { edge<PressLock>(LOCK, guard = { !it.isLongPress }) } } }
  87. stateMachine(initial = NOT_LOANED) { state(NOT_LOANED) { edge<PressRental>(LOCK) } state(ON_LOAN, entry

    = { /* LED ON */ }, exit = { /* LED OFF */ }) { state(LOCK) { edge<PressUnLock>(UNLOCK) edge<PressLock>(NOT_LOANED, guard = { it.isLongPress }) } state(UNLOCK) { edge<PressLock>(LOCK, guard = { !it.isLongPress }) } } }
  88. stateMachine(initial = NOT_LOANED) { state(NOT_LOANED) { edge<PressRental>(LOCK) } state(ON_LOAN, entry

    = { /* LED ON */ }, exit = { /* LED OFF */ }) { state(LOCK) { edge<PressUnLock>(UNLOCK) edge<PressLock>(NOT_LOANED, guard = { it.isLongPress }) } state(UNLOCK) { edge<PressLock>(LOCK, guard = { !it.isLongPress }) } } }     ✌
  89.  " •  • $# ! •  

  90.       

  91. 

  92.     

  93. Tablet         

      Phone   Maps API   A  B
  94. Tablet         

      Phone   Maps API  
  95. Tablet         

      Phone   Maps API  
  96. Tablet         

      Phone   Maps API  UI
  97.  UI

  98. Tablet         

      Phone   Maps API   A  B
  99. Tablet( )0 ! '* .% &+ Maps API .% Phone(

    )0 !  /-&+ "".% "".% A B  &+ "  $ # ,
  100.  (&$ ' ' Maps API ' # ' $

    Tablet! ")  Phone! ")  A B UI %
  101.  " •  • $# ! •  

  102.    

  103. !  '" -  $  - #&( 

    %  '" )
  104. ... 

  105.    

  106. ,'!  3. 8(1 1 UI(/2)5#    !0"

    2 &7 6+%$ -4 )* 
  107. Logic / Data UI / External 1 UI(#%) ' 

     $ 2 ) (! "&   
  108. Logic / Data UI / External ! ! 1 UI(!#)

    %   " 2 ' & $ 
  109. Logic / Data UI / External ! GUI  !

    Sytem  1 2
  110. GUI System MVP MVVM Flux Redux MVI Hexagonal Onion Clean

    
  111.     !

  112. MVP GUI MVI Redux MVVM Flux 

  113. GUI" Flux vs MVVM • View !%7 3. MVVM*1 •

    Flux"-#*' 1  • ActionStore4&+/ ,0)( "-26$5 
  114. System Hexagonal Onion Clean 

  115. System( % Onion( % vs Clean( % • /> =2!$'

    ,? 5< #*-4 • 0;7."&( 6:Clean ⊃ Onion719 • Onion3+)7817 $( !
  116. .6 (# • Flux →'"%(4+  →38 &'! • Onion(#

    →)19*,: 5;20  →/&$(7- 
  117.  

  118. 

  119.  - UI

  120. - UI View    

  121.  - external

  122.  - external   

  123.  - infrastructure

  124.  - infrastructure API DataBase  

  125.  - service

  126.  - service     

  127.  - model

  128.  - model UIService   Action 

  129. View Action Dispatcer Store Flux     

     
  130. Infra Service Model      UI External

  131.      

  132. StateMachine Dispatcher     Action

  133. View IF Event   Service IF ActionDispatcher  StateMachine

    Flux 
  134. 

  135. ①    UI

  136. ① 1.    1.  

  137.    1.  

  138.  A

  139.   State ↓ State

  140.     

  141.  State     focus = SELF

  142.     SELF

  143.  310    2.  

  144.  ②   

  145. ② 1.  1. 

  146.  ’ .   B  IF  

      
  147.   StateMachine  new 2. 

  148. ...!

  149.    ...

  150.       

  151. Core Sub StateMachine Sub Event Event StateMachine   

     
  152.    ✏

  153. None
  154.    1.  

  155. None
  156. Device (TaxiMeter) interface TaxiMeter { val status: Observable<Status> } EventHandler

    private val sm: StateMachine<MapState> by lazy {...} init { meter.status.subscribe { handle(MapEvent.ChangeMeterStatus(it)) }.addTo(disposable) } fun handle(event: MapEvent) { sm.dispatch(event) }
  157. Device (TaxiMeter) interface TaxiMeter { val status: Observable<Status> } private

    val sm: StateMachine<MapState> by lazy {...} init { meter.status.subscribe { handle(MapEvent.ChangeMeterStatus(it)) }.addTo(disposable) } fun handle(event: MapEvent) { sm.dispatch(event) } EventHandler     Event#handle
  158. Device (TaxiMeter) interface TaxiMeter { val status: Observable<Status> } EventHandler

    private val sm: StateMachine<MapState> by lazy {...} init { meter.status.subscribe { handle(MapEvent.ChangeMeterStatus(it)) }.addTo(disposable) } fun handle(event: MapEvent) { sm.dispatch(event) }      
  159. None
  160. stateMachine(initial = PickupState.ON_ACCEPT) { state(PickupState.ON_ACCEPT) { edge<MapEvent.ChangeMeterStatus>( next = PickupState.ON_PICKING

    ) } state(PickupState.ON_PICKING, entry = { dispatcher.dispatch(MapAction.ChangeCameraFocus(MapAction.Focus.SELF)) launch { repository.command(ChangeTaxiBusinessStatus.PICK_UP) } } ) { edge<MapEvent.ArrivePickupArea> { dispatcher.dispatch(TaxiAction.ArrivePickupArea) } } } StateMachine
  161. stateMachine(initial = PickupState.ON_ACCEPT) { state(PickupState.ON_ACCEPT) { edge<MapEvent.ChangeMeterStatus>( next = PickupState.ON_PICKING

    ) } state(PickupState.ON_PICKING, entry = { dispatcher.dispatch(MapAction.ChangeCameraFocus(MapAction.Focus.SELF)) launch { repository.command(ChangeTaxiBusinessStatus.PICK_UP) } } ) { edge<MapEvent.ArrivePickupArea> { dispatcher.dispatch(TaxiAction.ArrivePickupArea) } } } StateMachine Event  State 
  162. stateMachine(initial = PickupState.ON_ACCEPT) { state(PickupState.ON_ACCEPT) { edge<MapEvent.ChangeMeterStatus>( next = PickupState.ON_PICKING

    ) } state(PickupState.ON_PICKING, entry = { launch { repository.command(ChangeTaxiBusinessStatus.PICK_UP) dispatcher.dispatch(MapAction.ChangeCameraFocus(MapAction.Focus.SELF)) } } ) { edge<MapEvent.ArrivePickupArea> { dispatcher.dispatch(TaxiAction.ArrivePickupArea) } } } StateMachine   Repository  Action Dispatcher 
  163. None
  164. Dispatcher private val subject = PublishSubject.create<Action>().toSerialized() fun dispatch(action: Action) {

    subject.onNext(action) } fun <T : Action> on(clazz: Class<T>): Observable<T> = subject.ofType(clazz) .observeOn(AndroidSchedulers.mainThread()) .hide() Store val focusObservable: Observable<Focus> = dispatcher.on(MapAction.ChangeCameraFocus::class. java ) .scan(Focus.UNKNOWN) { _, action -> when (action.focus) { MapAction.Focus.SELF -> Focus.SELF else -> Focus.DESTINATION } }
  165. Dispatcher private val subject = PublishSubject.create<Action>().toSerialized() fun dispatch(action: Action) {

    subject.onNext(action) } fun <T : Action> on(clazz: Class<T>): Observable<T> = subject.ofType(clazz) .observeOn(AndroidSchedulers.mainThread()) .hide() Store val focusObservable: Observable<Focus> = dispatcher.on(MapAction.ChangeCameraFocus::class. java ) .scan(Focus.UNKNOWN) { _, action -> when (action.focus) { MapAction.Focus.SELF -> Focus.SELF else -> Focus.DESTINATION } } #dispatch   Action  subject  
  166. Dispatcher private val subject = PublishSubject.create<Action>().toSerialized() fun dispatch(action: Action) {

    subject.onNext(action) } fun <T : Action> on(clazz: Class<T>): Observable<T> = subject.ofType(clazz) .observeOn(AndroidSchedulers.mainThread()) .hide() Store val focusObservable: Observable<Focus> = dispatcher.on(MapAction.ChangeCameraFocus::class. java ) .scan(Focus.UNKNOWN) { _, action -> when (action.focus) { MapAction.Focus.SELF -> Focus.SELF else -> Focus.DESTINATION } } Store  Action     
  167. None
  168. UI // For example store.focusObservable.toLiveData() //  .observe(this, Observer {

    moveCameraToDriver() }) private fun moveCameraToDriver() { ownPosition.value?.let { val latlng = LatLng(it.location.latitude, it.location.longitude) val cameraPosition = CameraPosition(latlng, ZOOM_LEVEL_DEFAULT, TILT, 0f) val update = CameraUpdateFactory.newCameraPosition(cameraPosition) map.moveCamera(update) } }
  169. UI // For example store.focusObservable.toLiveData() //  .observe(this, Observer {

    moveCameraToDriver() }) private fun moveCameraToDriver() { ownPosition.value?.let { val latlng = LatLng(it.location.latitude, it.location.longitude) val cameraPosition = CameraPosition(latlng, ZOOM_LEVEL_DEFAULT, TILT, 0f) val update = CameraUpdateFactory.newCameraPosition(cameraPosition) map.moveCamera(update) } } LiveData  
  170.  310    2.  

  171.  !     

  172. 

  173.   - "#,8 ); :.(& /9 $  -

    #!13-%  *70  6<3-4' +5   2 
  174.     !

  175.