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

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

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

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

Avatar for Keishin Yokomaku

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