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

持続的なアプリ開発のためのDXを支える技術

 持続的なアプリ開発のためのDXを支える技術

Keishin Yokomaku

February 21, 2020
Tweet

More Decks by Keishin Yokomaku

Other Decks in Technology

Transcript

  1. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ About Me ▸ Keishin Yokomaku ▸ @KeithYokoma: GitHub /

    Twitter / Qiita / Stack Overflow ▸ Merpay, Inc. / Engineer ▸ Fun: Gymnastics / Cycling / Photography / Motorsport / Camping DroidKaigi 2020 2
  2. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ͳͥ DX ͕ॏཁ͔: Why DX matters? ▸ ϓϩμΫτ͸೔ʑ੒௕Λଓ͚Δ ▸

    ৽ػೳͷ௥Ճ ▸ طଘػೳͷվम ▸ όάमਖ਼ ▸ ͳͲ DroidKaigi 2020 5
  3. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ͳͥ DX ͕ॏཁ͔: Why DX matters? ▸ ιʔείʔυ΋ຖ೔มԽΛ͠ଓ͚Δ ▸

    ৽͍͠Ϋϥεɾϝιουͷ௥Ճ ▸ طଘΫϥεɾϝιουͷৼΔ෣͍ͷมߋ ▸ ϥΠϒϥϦͷߋ৽ ▸ ͳͲ DroidKaigi 2020 6
  4. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ͳͥ DX ͕ॏཁ͔: Why DX matters? ▸ ୹ظؒͷΠςϨʔγϣϯΛܧଓ͠ɺϓϩμΫτΛ੒௕ͤ͞Δϓϩηε͕ඞཁ ▸

    1 ~ 2 िؒ͝ͱͷϦϦʔεͰԾઆݕূΛͲΜͲΜճ͍ͯ͘͠ ▸ ૉૣ͍ػೳ࣮૷ʢࡉ͔͘࡞Γ্͍͛ͯ͘ϓϩηεͷ܁Γฦ͠ʣ ▸ ૉૣ͍ৼΓฦΓʢࡉֶ͔͘ͼΛൃݟ͠దԠ͍ͯ͘͠ϓϩηεͷ܁Γฦ͠ʣ DroidKaigi 2020 7
  5. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ͳͥ DX ͕ॏཁ͔: Why DX matters? ▸ ୹ظؒͷΠςϨʔγϣϯΛܧଓ͠ɺϓϩμΫτΛ੒௕ͤ͞Δϓϩηε͕ඞཁ ▸

    1 ~ 2 िؒ͝ͱͷϦϦʔεͰԾઆݕূΛͲΜͲΜճ͍ͯ͘͠ ▸ ૉૣ͍ػೳ࣮૷ʢࡉ͔͘࡞Γ্͍͛ͯ͘ϓϩηεͷ܁Γฦ͠ʣ ▸ ૉૣ͍ৼΓฦΓʢࡉֶ͔͘ͼΛൃݟ͠దԠ͍ͯ͘͠ϓϩηεͷ܁Γฦ͠ʣ ▸ Α͍ DX Λ࡞Δ͜ͱͰɺ͜ͷϓϩηεΛ͏·͘ճͤΔΑ͏ʹͳΔ ▸ DX ͸ϓϩμΫτͷ੒௕ʹඞཁෆՄܽ DroidKaigi 2020 8
  6. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ίʔσΟϯάن໿: Coding standards ▸ ίʔσΟϯάن໿ΛϦϙδτϦͰڞ༗͍ͨ͠ ▸ .editorconfig ▸ IDE

    ʹίʔσΟϯάن໿ʹैͬͨϑΥʔϚοτΛͤ͞Δ ▸ AndroidStudio/IntelliJ IDEA ͳΒ௥Ճͷ Plugin ͳ͠Ͱ࢖͑Δ DroidKaigi 2020 14
  7. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ .editorconfig ͷྫ: .editorconfig example root = true [*] "//

    apply the following styles to all files indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.{java,kt,kts,xml}] "// only apply to java/kt/kts/xml files max_line_length = 140 [*.md] "// only apply to markdown files trim_trailing_whitespace = false DroidKaigi 2020
  8. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Git flow ▸ master ▸ ϦϦʔε͝ͱʹ develop ΛϚʔδ͠ tag

    ΛଧͭͨΊͷϒϥϯν ▸ develop ▸ ීஈͷ։ൃʹ͓͍ͯɺϨϏϡʔࡁΈͷࠩ෼ΛऔΓࠐΈଓ͚Δϒϥϯν ▸ release/hotfix branches ▸ ϨϏϡʔࡁΈͷόάमਖ਼ࠩ෼ΛऔΓࠐΉϒϥϯν DroidKaigi 2020 17
  9. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Git flow - Feature development DroidKaigi 2020 19 master

    hotfix!/* release!/* bugfix"/* feature"/* develop Feature A Feature B
  10. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Git flow - Feature verification and release for v1.1.0

    DroidKaigi 2020 20 master hotfix!/* release!/* bugfix"/* feature"/* develop v1.1.0 Bugfix Bugfix Bugfix
  11. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϒϥϯνઓུͷൺֱ: Comparison between Git flow and GitHub flow ▸

    Git flow ▸ ϦϦʔεʹ޲͚ͨࠩ෼؅ཧͷͨΊͷϒϥϯνͷ෼͚ํΛ͢Δ ▸ ωΠςΟϒΞϓϦͱ૬ੑ͕͍͍ ▸ GitHub flow ▸ ΞϓϦέʔγϣϯΛࡉ͔͘σϓϩΠ͢Δ͜ͱʹϑΥʔΧε͍ͯ͠Δ ▸ සൟʹσϓϩΠ͢ΔαʔόʔΞϓϦέʔγϣϯͱ૬ੑ͕͍͍ DroidKaigi 2020 24
  12. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϒϥϯνઓུͷ࠷దԽ: Optimize branch strategy ▸ ࣗ෼ͨͪͷϫʔΫϑϩʔʹ࠷దԽͯ͠ OK ▸ Git

    flow ͸΍Δ͜ͱ͕ଟͯ͘؅ཧ͕໘౗ʹͳΓ͕ͪ ▸ GitHub flow Ͱ͸ QA ϑΣʔζͷͱ͖ɺ
 ฏߦͯ͠ػೳ։ൃϒϥϯνΛϚʔδͮ͠Β͘ͳΔ ➡ ྆ऀͷؒͷࢠͷΑ͏ͳઓུΛߟ͑ͯΈΔ DroidKaigi 2020 25
  13. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Combination of Git flow and GitHub flow DroidKaigi 2020

    26 master feature"/* release!/* Feature A Feature B bugfix"/* Bugfix Bugfix hotfix!/* v1.1.0 Bugfix v1.1.1
  14. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Combination of Git flow and GitHub flow DroidKaigi 2020

    27 master feature"/* release!/* Feature A Feature B bugfix"/* hotfix!/*
  15. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Combination of Git flow and GitHub flow DroidKaigi 2020

    28 master feature"/* release!/* Feature A Feature B bugfix"/* Bugfix Bugfix v1.1.0 hotfix!/*
  16. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Combination of Git flow and GitHub flow DroidKaigi 2020

    29 master feature"/* release!/* Feature A Feature B bugfix"/* Bugfix Bugfix hotfix!/* v1.1.0 Bugfix v1.1.1
  17. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ઃܭ: Architecture ▸ ϞόΠϧΞϓϦͷઃܭͰϑΥʔΧε͍ͨ͜͠ͱ ▸ API௨৴΍ϏδωεϩδοΫͳͲΛը໘ͷ࣮૷͔Β͏·͘෼཭͍ͨ͠ ▸ ը໘͕΋ͭঢ়ଶͱͦͷભҠͷํ๏Λ͏·͘දݱ͍ͨ͠ ▸

    ঢ়ଶʹԠͨ͡ UI ͷߋ৽ϩδοΫΛ੾Γ཭͍ͨ͠ ▸ ͜ΕΒ͕Ϣχοτςετ͠΍͍͢ܗͰ࣮ݱͰ͖Δ͜ͱ ▸ ඞཁ࠷௿ݶͷϞοΫΛ४උ͢Δ͚ͩͰࡁ·͍ͤͨ DroidKaigi 2020 35
  18. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ઃܭ: Architecture ▸ ͏·͘ઃܭύλʔϯΛ࢖͍͜ͳ͢ (ྫ) ▸ UIɺϏδωεϩδοΫɺAPI ௨৴ͳͲϨΠϠʔΛ͏·͘෼཭͍ͨ͠ ➡

    ϨΠϠʔυΞʔΩςΫνϟɺΫϦʔϯΞʔΩςΫνϟ ▸ UI ͕΋ͭঢ়ଶͱͦͷભҠΛ͏·͘දݱ͍ͨ͠ ➡ ReduxɺFluxɺ͋Δ͍͸ LiveData ʹΑΔঢ়ଶભҠͷ௨஌ ▸ UI ͷૢ࡞Λೖྗͱͯ͠ϏδωεϩδοΫΛಈ͔͠ɺͦͷ݁ՌΛ͏·͘ UI ʹ൓ө͍ͨ͠ ➡ MVVMɺDataBindingɺ͋Δ͍͸؆୯ͳҕৡύλʔϯ DroidKaigi 2020 36
  19. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶͱͦͷભҠΛ͏·͘දݱ͢Δ: Implement UI state machine ▸ ࢓૊Έͱ࣮ͯ͠૷ʹམͱ͠ࠐΜͩ΋ͷ ▸

    ReduxɺFlux ▸ ঢ়ଶΛද͢ΦϒδΣΫτ: State ▸ ঢ়ଶભҠͷ͖͔͚ͬΛͭ͘ΔΦϒδΣΫτ: Action ▸ ঢ়ଶભҠͷৄࡉΛ࣮૷͢Δ: Reducer (Redux)ɺStore (Flux) DroidKaigi 2020 37
  20. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶͱͦͷભҠΛ͏·͘දݱ͢Δ: Implement UI state machine ▸ Redux ΍

    Flux ͷߟ͑ํΛ LiveData + ViewModel Ͱ୅༻ͯ͠ΈΔ ▸ ঢ়ଶΛද͢ΦϒδΣΫτ: State ▸ LiveData Ͱ State ͷมߋΛ௨஌ ▸ ViewModel ͸ State ͷભҠํ๏Λఆٛͨ͠ϝιουΛ࣋ͭ DroidKaigi 2020 38
  21. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶͱͦͷભҠΛ͏·͘දݱ͢Δ: Implement UI state machine ▸ ঢ়ଶΛද͢ΦϒδΣΫτͷઃܭ ▸

    ඇಉظॲཧͷॲཧதɾ੒ޭɾࣦഊͷදݱ ▸ Either Λ΋͏গ֦͠ுͨ͠ sealed class ʹΑΔදݱ ▸ ॳظঢ়ଶɾॲཧதɾ੒ޭɾࣦഊΛද͢ ▸ RemoteDataK / Kotlin ͷ sealed class Λ࢖͍͜ͳ͢ by kikuchy DroidKaigi 2020 39
  22. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶͱͦͷભҠΛ͏·͘දݱ͢Δ: Implement UI state machine ▸ ঢ়ଶΛද͢ΦϒδΣΫτͷઃܭ ▸

    UI Ͱඞཁͳσʔλܕ΁ͷม׵ ▸ API Ϩεϙϯεͷܕ͕ͦͷ··ը໘࣮૷ʹ౰ͯ͸·Δͱ͸ݶΒͳ͍ ▸ ϨΠϠʹ߹ΘͤͨܕͷίϯόʔτΛͯ͋͛͠Δ ▸ e.g. ෳ਺ͷ API ϨεϙϯεΛ૊Έ߹Θͤͯ UI ʹ൓ө͢Δ ➡ ෳ਺ͷ API ϨεϙϯεΛ߹੒ͨ͠ܕΛ࡞Δ DroidKaigi 2020 40
  23. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶͱͦͷભҠΛ͏·͘දݱ͢Δ: Implement UI state machine ▸ ঢ়ଶΛද͢ΦϒδΣΫτͷઃܭ ▸

    ྫ֎ͷऔΓѻ͍ ▸ ϓϩάϥϛϯάͷϛεʹΑΔ΋ͷ: ແཧʹ catch ͠ͳ͍ ▸ I/O ͷࣦഊ: ࣦഊͨ͠ཧ༝Λઆ໌͢ΔΦϒδΣΫτʹม׵ ▸ see also: ந৅֓೦ʹదͨ͠ྫ֎Λεϩʔ͢Δ
 (Effective Java 3rd Edition: Item 73) DroidKaigi 2020 41
  24. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶͱͦͷભҠΛ͏·͘දݱ͢Δ: Implement UI state machine data class SampleViewState(

    "// RemoteData: sealed type describing data loading state
 "// Initial/Loading/Success/Failure val apiData: RemoteData<String, Exception> = Initial "// …… )
  25. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶͱͦͷભҠΛ͏·͘දݱ͢Δ: Implement UI state machine class SampleViewModel(
 private

    val dataSource: SampleDataSource
 ) : ViewModel() { "// never allow others to mutate the state private val viewStatePublisher: MutableLiveData<SampleViewState> = MutableLiveData(SampleViewState()) val viewState: LiveData<SampleViewState> = viewStatePublisher fun loadData() { "// …… implement state changes
 "// …… start loading, subscribe result from data source, emit success or failure } }
  26. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶͱͦͷભҠΛ͏·͘දݱ͢Δ: Implement UI state machine class SampleViewModel(private val

    dataSource: SampleDataSource) : ViewModel() { private val viewStatePublisher: MutableLiveData<SampleViewState> = MutableLiveData(SampleViewState()) fun loadData() { viewStatePublisher.value"?.let { state "-> "// notify apiData becomes loading viewStatePublisher.value = state.copy(apiData = Loading()) } } }
  27. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶͱͦͷભҠΛ͏·͘දݱ͢Δ: Implement UI state machine class SampleViewModel(private val

    dataSource: SampleDataSource) : ViewModel() { fun loadData() { "// …… dataSource.loadApiData() .observeOn(AndroidSchedulers.mainThread())
 .subscribe({ data "-> viewStatePublisher.value"?.let { state "-> "// notify apiData successfully loaded viewStatePublisher.value = state.copy(apiData = Success(data)) }
 }, { exception "-> viewStatePublisher.value"?.let { state "-> "// notify apiData loading failed viewStatePublisher.value = state.copy(apiData = Failure(exception)) }
 }) } }
  28. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶͱͦͷભҠΛ͏·͘දݱ͢Δ: Implement UI state machine class SampleFragment :

    Fragment() { private lateinit var viewModel: SampleViewModel override fun onCreate(savedInstanceState: Bundle?) { "// …… viewModel.loadData() viewModel.viewState.observe(this, Observer { state "-> when (it) { is Loading "-> "// show loading view is Success "-> "// apply data to UI is Failure "-> "// show error message } }) } }
  29. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ςετ: Test ▸ State ͷঢ়ଶભҠΛςετ͢Δ৔߹ ▸ ͋Δೖྗʹର͠ɺظ଴ͨ͠௨Γͷ State ͕ग़ྗ͞ΕΔ͜ͱ

    ▸ State ͷมԽͷॱ൪͕कΒΕ͍ͯΔ͜ͱ ▸ Initial → Loading → Success or Failure DroidKaigi 2020 47
  30. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class

    SampleViewModel(private val dataSource: SampleDataSource) : ViewModel() { private val viewStatePublisher: MutableLiveData<SampleViewState> = MutableLiveData(SampleViewState()) val viewState: LiveData<SampleViewState> = viewStatePublisher fun startLoading() { viewStatePublisher.value"?.let { state "-> "// notify apiData becomes loading viewStatePublisher.value = state.copy(apiData = Loading()) } dataSource.loadApiData() .observeOn(AndroidSchedulers.mainThread())
 .subscribe({ data "-> viewStatePublisher.value"?.let { state "-> "// notify apiData successfully loaded viewStatePublisher.value = state.copy(apiData = Success(data)) }
 }, { exception "-> viewStatePublisher.value"?.let { state "-> "// notify apiData loading failed viewStatePublisher.value = state.copy(apiData = Failure(exception)) }
 }) } }
  31. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class

    SampleViewModel(private val dataSource: SampleDataSource) : ViewModel() { private val viewStatePublisher: MutableLiveData<SampleViewState> = MutableLiveData(SampleViewState()) val viewState: LiveData<SampleViewState> = viewStatePublisher fun startLoading() { viewStatePublisher.value"?.let { state "-> "// notify apiData becomes loading viewStatePublisher.value = state.copy(apiData = Loading()) } dataSource.loadApiData() .observeOn(AndroidSchedulers.mainThread())
 .subscribe({ data "-> viewStatePublisher.value"?.let { state "-> "// notify apiData successfully loaded viewStatePublisher.value = state.copy(apiData = Success(data)) }
 }, { exception "-> viewStatePublisher.value"?.let { state "-> "// notify apiData loading failed viewStatePublisher.value = state.copy(apiData = Failure(exception)) }
 }) } } Observed as
 1st value
  32. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class

    SampleViewModel(private val dataSource: SampleDataSource) : ViewModel() { private val viewStatePublisher: MutableLiveData<SampleViewState> = MutableLiveData(SampleViewState()) val viewState: LiveData<SampleViewState> = viewStatePublisher fun startLoading() { viewStatePublisher.value"?.let { state "-> "// notify apiData becomes loading viewStatePublisher.value = state.copy(apiData = Loading()) } dataSource.loadApiData() .observeOn(AndroidSchedulers.mainThread())
 .subscribe({ data "-> viewStatePublisher.value"?.let { state "-> "// notify apiData successfully loaded viewStatePublisher.value = state.copy(apiData = Success(data)) }
 }, { exception "-> viewStatePublisher.value"?.let { state "-> "// notify apiData loading failed viewStatePublisher.value = state.copy(apiData = Failure(exception)) }
 }) } } Observed as
 2nd value
  33. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class

    SampleViewModel(private val dataSource: SampleDataSource) : ViewModel() { private val viewStatePublisher: MutableLiveData<SampleViewState> = MutableLiveData(SampleViewState()) val viewState: LiveData<SampleViewState> = viewStatePublisher fun startLoading() { viewStatePublisher.value"?.let { state "-> "// notify apiData becomes loading viewStatePublisher.value = state.copy(apiData = Loading()) } dataSource.loadApiData() .observeOn(AndroidSchedulers.mainThread())
 .subscribe({ data "-> viewStatePublisher.value"?.let { state "-> "// notify apiData successfully loaded viewStatePublisher.value = state.copy(apiData = Success(data)) }
 }, { exception "-> viewStatePublisher.value"?.let { state "-> "// notify apiData loading failed viewStatePublisher.value = state.copy(apiData = Failure(exception)) }
 }) } } Observed as
 3rd value if success
  34. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class

    SampleViewModel(private val dataSource: SampleDataSource) : ViewModel() { private val viewStatePublisher: MutableLiveData<SampleViewState> = MutableLiveData(SampleViewState()) val viewState: LiveData<SampleViewState> = viewStatePublisher fun startLoading() { viewStatePublisher.value"?.let { state "-> "// notify apiData becomes loading viewStatePublisher.value = state.copy(apiData = Loading()) } dataSource.loadApiData() .observeOn(AndroidSchedulers.mainThread())
 .subscribe({ data "-> viewStatePublisher.value"?.let { state "-> "// notify apiData successfully loaded viewStatePublisher.value = state.copy(apiData = Success(data)) }
 }, { exception "-> viewStatePublisher.value"?.let { state "-> "// notify apiData loading failed viewStatePublisher.value = state.copy(apiData = Failure(exception)) }
 }) } } Observed as
 3rd value if error
  35. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class

    SampleViewModelTest : Spek({ beforeEachTest { RxAndroidPlugins.setMainThreadSchedulerHandler { Scheduler.trampoline() } ArchTaskExecutor.getInstance().setDelegate(TestExecutor()) } afterEachTest { RxAndroidPlugins.reset() ArchTaskExecutor.getInstance().setDelegate(null) } }) class TestExecutor : TaskExecutor() { override fun executeOnDiskIO(runnable: Runnable) { runnable.run() } override fun isMainThread(): Boolean = true override fun postToMainThread(runnable: Runnable) { runnable.run() } }
  36. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class

    SampleViewModelTest : Spek({ Feature("SampleViewModel#startLoading") { val dataSource: SampleDataSource by memoized(CachingMode.EACH_GROUP) { mockk<SampleDataSource>(relaxed = true) } Scenario("Load data successfully") { "// Test cases verifying state changes in successful scenario } Scenario("Load data unsuccessfully") { "// Test cases verifying state changes in failure scenario } } })
  37. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class

    SampleViewModelTest : Spek({ Scenario("Load data successfully") { lateinit var viewModel: SampleViewModel lateinit var observer: Observer<SampleViewState> lateinit var changedStateSlot: CapturingSlot<SampleViewState> Given("ViewModel with initial state and state observer") { changedStateSlot = slot() observer = mockk(relaxed = true) { every { onChanged(capture(changedStateSlot)) } just Runs } every { dataSource.loadApiData() } returns Observable.just("foo") viewModel = NotificationViewModel(dataSource) } } })
  38. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class

    SampleViewModelTest : Spek({ When("Start observing states") { viewModel.viewState.observeForever(observer) } And("Start loading") { viewModel.startLoading() } Then("State changes observed in the specified order") { verifyOrder { observer.onChanged(eq(SampleViewState(apiData = Initial))) observer.onChanged(eq(SampleViewState(apiData = Loading()))) observer.onChanged(eq(SampleViewState(apiData = Success("foo")))) } } })
  39. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class

    SampleViewModelTest : Spek({ When("Start observing states") { viewModel.viewState.observeForever(observer) } And("Start loading") { viewModel.startLoading() } Then("State changes observed in the specified order") { verifyOrder { observer.onChanged(eq(SampleViewState(apiData = Initial))) observer.onChanged(eq(SampleViewState(apiData = Loading()))) observer.onChanged(eq(SampleViewState(apiData = Success("foo")))) } } })
  40. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ γϯάϧϞδϡʔϧ: Single module ▸ apk Λੜ੒͢ΔϞδϡʔϧʹ͢΂͕ͯ͋Δ ▸ Pros ▸

    ߏ੒ͷ͜ͱͰ೰·ͳͯ͘ࡁΉ ▸ Cons ▸ Ϗϧυ଎౓Λ্͛ͨ͘ͳͬͨͱ͖ʹɺGradle ͷ͘͠ΈͷԸܙΛड͚ͮΒ͍ ▸ Ͳͷύοέʔδ΋༰қʹࢀরՄೳͳͨΊɺҙਤ͠ͳ͍ґଘؔ܎Λ࡞Γ΍͍͢ DroidKaigi 2020 59
  41. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϚϧνϞδϡʔϧ: Multiple modules ▸ ϞδϡʔϧΛෳ਺ʹ෼཭͠ɺapk Λੜ੒͢ΔϞδϡʔϧ͕͢΂ͯΛू໿͢Δ ▸ Pros ▸

    Ϟδϡʔϧͷ໋໊ʹΑͬͯ໾ׂΛ໌ࣔͰ͖Δ ▸ Gradle ͷ࢓૊ΈͰϏϧυ଎౓Λ্͛΍͍͢ ▸ Cons ▸ Ϟδϡʔϧͷ෼཭ํ๏ʹڞ௨ೝ͕ࣝඞཁ DroidKaigi 2020 60
  42. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ μΠφϛοΫϑΟʔνϟʔϞδϡʔϧ: Dynamic Feature Modules ▸ Dynamic Delivery Λ͢ΔͳΒඞਢ ▸

    Pros ▸ ϚϧνϞδϡʔϧ͕ڧ੍ʹͳΔͷͰɺಉ͡ϝϦοτΛಘΒΕΔ ▸ Play Store Ͱ഑෍͢ΔΞϓϦͷαΠζΛখ͘͞Ͱ͖Δ ▸ Cons ▸ Ϟδϡʔϧͷ෼཭ํ๏ʹڞ௨ೝ͕ࣝඞཁ DroidKaigi 2020 61
  43. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Ϟδϡʔϧͷ໋໊: Naming convention for module ▸ Ϟδϡʔϧ෼ׂͷ୯ҐʹԠ໋໊ͨ͡نଇΛ࡞͓ͬͯ͘ ▸ ΞϓϦέʔγϣϯຊମ:

    app ▸ ֤छػೳͷ UI ࣮૷: feature_** ▸ Ϣʔεέʔε: usecase_** ▸ σʔλΞΫηε: repository_** ▸ ڞ௨ॲཧ: common_** DroidKaigi 2020 63
  44. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ࣗಈԽ: Automation ▸ CI (Continuous Integration) ΍ CD (Continuous

    Delivery) ▸ Ϗϧυ ▸ ςετ ▸ ੩తղੳ ▸ ΞϓϦͷ഑৴ ▸ ೔ʑͷϫʔΫϑϩʔ DroidKaigi 2020 70
  45. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ςετͷࣗಈԽ: Test Automation ▸ ϓϩμΫτͷ੒௕ͱڞʹςετͷ࣮ߦ࣌ؒ΋૿͍͑ͯ͘ ▸ ࣮ߦ࣌ؒΛ୹͘͢Δʹ͸… ▸ Gradle

    ͕࢖͑ΔϓϩηεͱϞδϡʔϧΛ૿΍ͯ͠ฒྻੑΛ্͛Δ ▸ ςετ࣮ߦ༻ͷίϯςφΛෳ਺্ཱͪ͛ɺ࣮ߦ͢ΔςετΛ෼ࢄ͢Δ ▸ ࣌ؒʹґଘ͍ͯͯ͠଴ͭඞཁͷ͋ΔςετΛݮΒ͢ ▸ etc…… DroidKaigi 2020 73
  46. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ςετͷฒྻԽ: Parallelize unit test execution DroidKaigi 2020 74 ▸

    CircleCI Ͱͷྫ ▸ ͻͱͭͷεςοϓ಺Ͱෳ਺ͷίϯςφΛىಈ͢Δ࢓૊ΈΛ࢖͏ ▸ ෳ਺ͷίϯςφͰςετΛಉ࣌ʹ࣮ߦ͢Δ ▸ ͦΕͧΕͷίϯςφͰผͷςετΛ࣮ߦ͠ɺશମͰ͔͔Δ࣌ؒΛ୹ॖ͢Δ
  47. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ςετͷฒྻԽ: Parallelize unit test execution DroidKaigi 2020 75 Container

    0 Container 1 Container 2 Container 3 :module_a:test
 :module_b:test
 …… :module_c:test
 :module_d:test
 …… :module_e:test
 :module_f:test
 …… :module_g:test
 :module_h:test
 …… Container 0 :module_a:test
 :module_b:test
 :module_c:test
 :module_d:test
 :module_e:test
 :module_f:test
 …… ୯Ұίϯςφͷ৔߹ ෳ਺ίϯςφͷ৔߹
  48. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ςετͷฒྻԽ: Parallelize unit test execution DroidKaigi 2020 76 ▸

    ୯ҰϞδϡʔϧͷ৔߹ ▸ CircleCI CLI Λ࢖֤ͬͯίϯςφ͝ͱʹϑΝΠϧΛৼΓ෼͚Δ
 circleci tests glob "**/test""/**"/*.kt" | circleci tests split ▸ ϑΝΠϧ໊ΛΫϥε໊ʹม׵ͯ͋͛͠Ε͹ɺGradle ͷύϥϝʔλʹ౉ͤΔ ▸ ෳ਺Ϟδϡʔϧͷ৔߹ ▸ ֤ίϯςφ͝ͱʹϞδϡʔϧΛৼΓ෼͚ΔॲཧΛࣗ෼Ͱॻ͘
  49. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ςετͷฒྻԽ: Parallelize unit test execution "// The index number

    of container this execution is on. val containerIndex = System.getenv("CIRCLE_NODE_INDEX")"?.toInt() "?: 0 "// Total number of containers running in parallel. val totalContainer = System.getenv("CIRCLE_NODE_TOTAL")"?.toInt() "?: 0 "// Select modules to run unit tests on the container val modules = project.subprojects .withIndex() .filter { it.index % totalContainer "== containerIndex } .map { it.value } "// Execute tests modules.forEach { module "-> "./gradlew :${module.name}:test".runCommand() } DroidKaigi 2020
  50. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ੩తղੳͷࣗಈԽ: Static Analysis Automation ▸ ੩తղੳΛࣗಈԽ͠ίʔυϨϏϡʔʹ໾ཱͯΔ ▸ ಈ͔͢λΠϛϯά͸ Pull

    Request ͕Ͱ͖ͨ࣌ͳͲ ▸ ੩తղੳͷ݁ՌΛϨϙʔτ͢Δ ▸ xml ΍ html ͷϨϙʔτϑΝΠϧ΋ CI ͷ੒Ռ෺ͱଊ͑Δ ▸ Pull Request ͕͋Ε͹ɺϨϙʔτͷ಺༰Λίϝϯτ͢Δ DroidKaigi 2020 78
  51. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϫʔΫϑϩʔͷࣗಈԽ: Automation ▸ e.g. QA ϑΣʔζͰͷόάमਖ਼Λ master ʹऔΓࠐΉ࡞ۀͷࣗಈԽ ▸

    Ϛʔδͷࠩ෼Λখͯ͘͞͠ɺίϯϑϦΫτͷϦεΫΛԼ͛Δ ▸ e.g. release ϒϥϯνͷόάमਖ਼Λຖ೔ master ΁औΓࠐΉ ▸ e.g. master ϒϥϯνͷߋ৽Λຖ೔֤։ൃϒϥϯν΁औΓࠐΉ ▸ ϒϥϯνઓུʹ߹Θ࣮ͤͯ૷ɾӡ༻͢Δ DroidKaigi 2020 81
  52. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϫʔΫϑϩʔͷࣗಈԽ: Automation ▸ QA ϑΣʔζͰͷόάमਖ਼Λ master ʹऔΓࠐΉ࡞ۀͷࣗಈԽ DroidKaigi 2020

    82 master release"/* master release"/* releaseϒϥϯνӡ༻ͷ
 ࠷ޙʹϚʔδ͢Δ৔߹ releaseϒϥϯνΛఆظతʹ
 ࣗಈͰϚʔδ͢Δ৔߹
  53. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Gradle Custom Tasks ʹΑΔࣗಈԽ: Automation with Gradle Custom Tasks

    ▸ Gradle Custom Tasks ͷ࡞Γํ ▸ project_root/buildSrc ϞδϡʔϧʹλεΫͷ࣮૷Λॻ͘ ▸ project_root/build.gradle.kts ͰλεΫͷొ࿥Λ͢Δ DroidKaigi 2020 84
  54. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Gradle Custom Tasks ʹΑΔࣗಈԽ: Automation with Gradle Custom Tasks

    "// buildSrc/build.gradle.kts repositories { jcenter() } plugins { `kotlin-dsl` `java-gradle-plugin` } dependencies { implementation(gradleApi()) } DroidKaigi 2020
  55. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Gradle Custom Tasks ʹΑΔࣗಈԽ: Automation with Gradle Custom Tasks

    "// buildSrc/src/main/kotlin/com/example/MyCustomTask.kt open class MyCustomTask : DefaultTask() { @Input lateinit var parameter: String @TaskAction fun doAction() { "// … do your job! } } DroidKaigi 2020
  56. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Gradle Custom Tasks ʹΑΔࣗಈԽ: Automation with Gradle Custom Tasks

    "// buildSrc/src/main/kotlin/com/example/MyCustomTask.kt open class MyCustomTask : DefaultTask() { @Input lateinit var parameter: String @TaskAction fun doAction() { "// … do your job! } } DroidKaigi 2020
  57. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Gradle Custom Tasks ʹΑΔࣗಈԽ: Automation with Gradle Custom Tasks

    "// buildSrc/src/main/kotlin/com/example/MyCustomTask.kt open class MyCustomTask : DefaultTask() { @Input lateinit var parameter: String @TaskAction fun doAction() { "// … do your job! } } DroidKaigi 2020
  58. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Gradle Custom Tasks ʹΑΔࣗಈԽ: Automation with Gradle Custom Tasks

    "// buildSrc/src/main/kotlin/com/example/MyCustomTask.kt open class MyCustomTask : DefaultTask() { @Input lateinit var parameter: String @TaskAction fun doAction() { "// … do your job! } } DroidKaigi 2020
  59. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Gradle Custom Tasks ʹΑΔࣗಈԽ: Automation with Gradle Custom Tasks

    "// build.gradle.kts in the project root tasks.register<MyCustomTask>("myTask") { parameter = "Hello, World" } DroidKaigi 2020
  60. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ίʔυϨϏϡʔ: Code Review ▸ ઃܭํ਑΍໋໊ͷΘ͔Γ΍͢͞ɺߟྀ࿙ΕνΣοΫʹਓͷྗΛूத͍ͨ͠ ▸ ࣗಈԽͰ͖Δͱ͜Ζ͸ͲΜͲΜࣗಈԽ͢Δ ▸ ίʔσΟϯάن໿ͷڞ༗:

    .editorconfig Λ࡞͓ͬͯ͘ ▸ ίʔσΟϯάن໿ʹै͍ͬͯΔ͔νΣοΫ: CI Ͱ ktlint ͳͲΛಈ͔͢ ▸ Α͋͘Δؒҧ͍ΛνΣοΫ: CI Ͱ android lint ͳͲΛಈ͔͢ DroidKaigi 2020 91
  61. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ίʔυϨϏϡʔ: Code Review ▸ υϝΠϯ஌ࣝΛ࣋ͬͨਓʹϨϏϡʔΛґཔ͠ɺίʔυϨϏϡʔͷ࣭Λ্͛Δ ▸ ͩΕʹϨϏϡʔͯ͠΄͍͔͠ઃఆ͢Δ ▸ CODEOWNERS

    ▸ σΟϨΫτϦ͝ͱʹઃఆ ▸ σΟϨΫτϦ഑ԼͷϑΝΠϧʹࠩ෼Λ࡞ΔͱɺࣗಈͰઃఆͨ͠ਓ͕ϨϏϡϫʔ ʹͳΔ DroidKaigi 2020 92
  62. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ίʔυϨϏϡʔ: Code Review # KeithYokoma is the owner of

    Module A /module_a/ @KeithYokoma # Anyone in team_a can review changes in Module B /module_b/ @team_a # Either KeithYokoma or someone in team_a can review /module_c/ @KeithYokoma @team_a DroidKaigi 2020
  63. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ίʔυϨϏϡʔ: Code Review ▸ ίʔυϨϏϡʔʹૉૣ͘औΓ૊ΜͰ։ൃαΠΫϧΛͳΊΒ͔ʹճ͢ ▸ ίʔυϨϏϡʔͷϦΫΤετΛ௨஌͢Δ ▸ Pull

    Reminders ▸ ௚઀ࣗ෼Ѽͯʹ௨஌Λ͢Δ ▸ ಛఆͷνϟϯωϧͰɺApproval ͷͳ͍ Pull Request Λ௨஌͢Δ DroidKaigi 2020 94
  64. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ίʔυϨϏϡʔ: Code Review ▸ ίʔυϨϏϡʔʹૉૣ͘औΓ૊ΜͰ։ൃαΠΫϧΛͳΊΒ͔ʹճ͢ ▸ Pull Request ͷઆ໌Λ͏·͘ॻ͘

    ▸ Ͳ͏͍͏໨తͷࠩ෼ͳͷ͔Λઆ໌ͯ͠΄͍͠ ▸ ࠩ෼ΛಡΉ্Ͱͷલఏ஌͕ࣝ͋Ε͹આ໌ͯ͠΄͍͠ DroidKaigi 2020 95
  65. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ςϯϓϨʔτྫ: Example of pull request template "## Summary <!——

    Describe the change set in short ——> "## Kind <!—— Describe what kind of change? e.g. Bugfix, New feature, Design adjustment, etc…… ——> "## Details <!—— e.g. What caused the issue? How you solved it? Any caveats? Anything out of scope? ——> "## Links <!—— Put links related to this PR if any ——> - Task Ticket: - Design Doc: - Crashlytics Issue: "## Screenshot <!—— Put screenshots indicating what has been changed with this PR if any ——> Before | After :——:|:——: <img src="" width="300" "/>|<img src="" width="300" "/> DroidKaigi 2020
  66. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϑΟʔνϟʔϑϥάΛ࢖͏ྫ: Example usage of Feature Flag ▸ ஈ֊తʹػೳΛެ։͍ͨ͠ͱ͖ ▸

    Firebase Remote Config ΍ɺࣗલͷ࢓૊ΈͳͲͰઃఆΛม͑Δ ▸ ༷ʑͳଐੑ஋͔ΒઃఆΛੜ੒Ͱ͖Δ ▸ ৽͍͠ػೳͷ։ൃ͕ऴΘΓɺঃʑʹ։์͢Δͱ͖ʹ༗ޮ DroidKaigi 2020 101
  67. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϑΟʔνϟʔϑϥάΛ࢖͏ྫ: Example usage of Feature Flag ▸ 1౓ͷϦϦʔεαΠΫϧΛ௒͑ͯػೳ։ൃΛ͍ͨ͠ͱ͖ ▸

    Git ͷϒϥϯνΛࡉ͔͘࡞ͬͯϚʔδࠩ͠෼ΛੵΈ্͍͛ͯ͘ ▸ ࣮૷ͷ޻෉Ͱ։ൃ్தͷػೳΛݟ͑ͳ͍ɾ࢖͑ͳ͍Α͏ʹ࠹͙ DroidKaigi 2020 102 master Merge
 Feature A-1 v1.1.0 Merge
 Feature A-2 v1.2.0 Merge
 Feature A-3 v1.2.1 Finalize
 Feature A v1.3.0
  68. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϑΟʔνϟʔϑϥάΛ࢖͏ྫ: Example usage of Feature Flag ▸ ࣮૷ͷ޻෉ͷ࢓ํ ▸

    BuildConfig ʹϑΟʔνϟʔϑϥάΛఆٛ͢Δ ▸ UI ͷͭͳ͗͜ΈΛ࠷ޙʹ࣮ࢪ ▸ ։ൃऀ޲͚ͷઃఆը໘Ͱ༗ޮɾແޮΛ੾Γସ͑Δ ▸ etc… DroidKaigi 2020 103
  69. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϑΟʔνϟʔϑϥάΛ࢖͏ྫ: Example usage of Feature Flag ▸ BuildConfig ʹϑΟʔνϟʔϑϥάΛఆٛ͢Δ৔߹

    ▸ e.g. طଘը໘ʹ৽͍͠ػೳΛ΋ͬͨίϯϙʔωϯτΛ௥Ճ͢Δ࣮૷ DroidKaigi 2020 104 master Merge
 Feature A-1 v1.1.0 Merge
 Feature A-2 v1.2.0 Merge
 Feature A-3 v1.2.1 Finalize
 Feature A v1.3.0 ৽͍͠ػೳ޲͚ͷ
 UI ίϯϙʔωϯτ࡞੒ ৽͍͠ػೳͷ
 Ϟσϧ࣮૷ Ϟσϧͱ UI ίϯϙʔωϯτ Λͭͳ͗͜Ή BuildConfig ͷϑϥάΛ
 ༗ޮԽ͢Δ
  70. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϑΟʔνϟʔϑϥάΛ࢖͏ྫ: Example usage of Feature Flag ▸ UI ͷͭͳ͗ࠐΈΛ࠷ޙʹ΍Δ৔߹

    ▸ e.g. ৽͍͠ػೳͷઃఆը໘Λ࡞ΓɺઃఆҰཡʹ߲໨Λ଍࣮͢૷ DroidKaigi 2020 105 master Merge
 Feature A-1 v1.1.0 Merge
 Feature A-2 v1.2.0 Merge
 Feature A-3 v1.2.1 Finalize
 Feature A v1.3.0 ͋ͨΒ͍͠ઃఆը໘ͷ
 ϨΠΞ΢τ࡞੒ ͋ͨΒ͍͠ઃఆը໘ͷ
 Ϟσϧ࣮૷ Ϟσϧͱը໘Λͭͳ͗͜Ή ઃఆҰཡʹɺ৽͍͠ઃఆը໘ ΁ͷಋઢΛ଍͢
  71. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϑΟʔνϟʔϑϥάͷ஫ҙ఺: Caveats of Feature Flag ▸ "Release toggles are

    the last thing you should do" by Martin Fowler ▸ ϑϥάΛ΋ͭ͜ͱͷίετ͸গͳ͔Βͣ͋Δ ▸ ༗ޮԽ͢ΔλΠϛϯά ▸ ࢖Θͳ͘ͳͬͨΒ࡟আ͢Δ ▸ ϑϥάΛ࣋ͭ΄͔ʹऔΓ͏Δબ୒ࢶ ▸ UI ͷͭͳ͗͜ΈΛ࠷ޙʹ͢Δ ▸ ػೳͦͷ΋ͷΛࡉ͔͘Θ͚ɺখ͘͞ϦϦʔε͢Δ DroidKaigi 2020 107
  72. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϦϦʔετϨΠϯ: Release Train ▸ ϦϦʔεεέδϡʔϧΛݻఆͯ͠ܧଓతʹ҆શͳϦϦʔεΛ͢Δ ▸ ϦϦʔετϨΠϯӡ༻ͷେݪଇ ▸ ܾ·ͬͨεέδϡʔϧ͔Βٯࢉͯ͠։ൃΛ͍ͯ͘͠

    ▸ ؒʹ߹Θͳ͍΋ͷ͸࣍ͷϦϦʔε೔Λ଴ͭ ▸ ن໛ͷେ͖ͳ։ൃͳΒͰ͸ͷ՝୊ղܾํ๏ ▸ ͨ͘͞ΜͷϓϩδΣΫτؒͷௐ੔͝ͱΛ෼͔Γ΍͍ͨ͘͢͠ DroidKaigi 2020 108
  73. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ؂ࢹମ੍: Metrics monitoring ▸ ࢓૊ΈͰղܾ: Crashlytics ▸ ΞϥʔτΛ Slack

    ʹ౤ߘ͢Δ ▸ ৽نͷΫϥογϡΛݕ஌ͨ͠Β
 Issue Tracker ʹνέοτΛ࡞Δ ▸ etc… DroidKaigi 2020 111
  74. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ؂ࢹମ੍: Metrics monitoring ▸ ࢓૊ΈͰղܾ: Crashlytics ▸ ΞϥʔτΛ Slack

    ʹ౤ߘ͢Δ ▸ ৽نͷΫϥογϡΛݕ஌ͨ͠Β
 Issue Tracker ʹνέοτΛ࡞Δ ▸ etc… DroidKaigi 2020 112
  75. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϩΪϯάͷઃܭ: Log collection architecture ▸ e.g. Timber + Crashlytics

    ▸ ϩάΛు͖ͩ͢ϝιουΛ Timber ʹू໿ ▸ Timber.d("debug log") ▸ Timber.i("info log") ▸ Timber.e(exception, "error log") ▸ ϩάΛు͘ϝιουݺͼग़͠ΛϑοΫͯ͠ Crashlytics Ͱूܭ DroidKaigi 2020 114
  76. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϩΪϯάͷઃܭ: Log collection architecture class CrashlyticsTree : Timber.Tree() {

    override fun log(priority: Int, tag: String?, message: String, throwable: Throwable?) { when (priority) { Log.INFO "-> Crashlytics.log(priority, tag, message) Log.WARN "-> Crashlytics.log(priority, tag, message) Log.ERROR "-> { Crashlytics.log(priority, tag, message) throwable"?.let { Crashlytics.logException(it) } } } } } DroidKaigi 2020
  77. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϩΪϯάͷઃܭ: Log collection architecture class CrashlyticsTree : Timber.Tree() {

    override fun log(priority: Int, tag: String?, message: String, throwable: Throwable?) { when (priority) { Log.INFO "-> Crashlytics.log(priority, tag, message) Log.WARN "-> Crashlytics.log(priority, tag, message) Log.ERROR "-> { Crashlytics.log(priority, tag, message) throwable"?.let { Crashlytics.logException(it) } } } } } DroidKaigi 2020
  78. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ΧελϜΩʔͷ௥Ճ: Adding custom keys ▸ Crashlytics ͷϨϙʔτʹ௥Ճ৘ใΛ෇༩͢Δ͘͠Έ ▸ ΞϓϦͷ࣋ͭઃఆ஋

    ▸ e.g. XXX ͷػೳ͕༗ޮ͔Ͳ͏͔ ▸ Ϣʔβͷঢ়ଶ ▸ e.g. YYY ͷνϡʔτϦΞϧ͸ऴΘ͔ͬͨͲ͏͔ DroidKaigi 2020 117
  79. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ΧελϜΩʔͷ௥Ճ: Adding custom keys class SettingsFragment : PreferenceFragmentCompat() {

    override fun onCreatePreferences( savedState: Bundle?, rootKey: String? ) { setPreferencesFromResource(R.xml.pref, rootKey) val notificationPreference = findPreference<SwitchPreference>("enable_notification") notificationPreference"?.setOnPreferenceChangeListener { _, newValue "-> "// Put a custom key for preference value Crashlytics.setBool("enable_notification", newValue) true } } } DroidKaigi 2020
  80. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ΧελϜΩʔͷ௥Ճ: Adding custom keys class SettingsFragment : PreferenceFragmentCompat() {

    override fun onCreatePreferences( savedState: Bundle?, rootKey: String? ) { setPreferencesFromResource(R.xml.pref, rootKey) val notificationPreference = findPreference<SwitchPreference>("enable_notification") notificationPreference"?.setOnPreferenceChangeListener { _, newValue "-> "// Put a custom key for preference value Crashlytics.setBool("enable_notification", newValue) true } } } DroidKaigi 2020
  81. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ·ͱΊ: Wrap up ▸ DX Λܧଓతʹվળ͍ͯ͘͜͠ͱͰ… ▸ ૉૣ͍ػೳ࣮૷͕͠΍͘͢ͳΔ ▸

    ૉૣ͍ৼΓฦΓ͕͠΍͘͢ͳΔ ▸ ͦͷ݁ՌɺνʔϜ΋ϓϩμΫτ΋͙Μ͙Μ੒௕͍ͯ͘͠ ▸ ܧଓ͸ྗͳΓ DroidKaigi 2020 122
  82. ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ͍͞͝ʹ: Wrap up ▸ ΋͏૸Γ࢝Ί͍ͯΔϓϩδΣΫτͰ DX Λվળ͢Δʹ͸… ▸ ݱঢ়෼ੳΛͯ͠νʔϜͷ໨ઢΛ߹ΘͤΔ

    ▸ KPT ͰఆੑతʹνʔϜͷߟ͑Λ·ͱΊͯΈΔ ▸ ςετΧόϨοδ΍ܯࠂ਺ͷਪҠͳͲΛ࢖ͬͯఆྔతʹ࣭ΛଌͬͯΈΔ ▸ ՝୊ײΛἧ͑ΔͱɺԿ͔ΒऔΓ૊ΉͱΑ͍͔΋෼͔Γ΍͘͢ͳΔ DroidKaigi 2020 124