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

外部デバイスと密に連携する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. $# tomoya0x00 '  ( ) Kiosk " ! &%RX-8(

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

    - MOV    Twitter : URI - @yuyuyuyuyuri Pokemon GO : 05URI28 - 0188 7196 1789
  3.  +. • !%!/-& • !/(',)*  •  

    # • "BLE BT Classic • !%!/$ #
  4.    •  ➡  • OK •

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

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

  7. 

  8. 

  9. 

  10. 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 }) } } }
  11. 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 }) } } }
  12. 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 }) } } }
  13. 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 }) } } }
  14. 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 }) } } }     ✌
  15. Tablet         

      Phone   Maps API   A  B
  16. Tablet         

      Phone   Maps API  
  17. Tablet         

      Phone   Maps API  
  18. Tablet         

      Phone   Maps API  UI
  19. Tablet         

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

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

    Tablet! ")  Phone! ")  A B UI %
  22. Logic / Data UI / External 1 UI(#%) ' 

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

    %   " 2 ' & $ 
  24. GUI" Flux vs MVVM • View !%7 3. MVVM*1 •

    Flux"-#*' 1  • ActionStore4&+/ ,0)( "-26$5 
  25. System( % Onion( % vs Clean( % • /> =2!$'

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

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

  28. 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) }
  29. 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
  30. 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) }      
  31. 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
  32. 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 
  33. 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 
  34. 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 } }
  35. 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  
  36. 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     
  37. 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) } }
  38. 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  
  39. 

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

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