Slide 1

Slide 1 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ DXΛࢧ͑Δٕज़ KeithYokoma (Keishin Yokomaku) /
 DroidKaigi 2020

Slide 2

Slide 2 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ About Me ▸ Keishin Yokomaku ▸ @KeithYokoma: GitHub / Twitter / Qiita / Stack Overflow ▸ Merpay, Inc. / Engineer ▸ Fun: Gymnastics / Cycling / Photography / Motorsport / Camping DroidKaigi 2020 2

Slide 3

Slide 3 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ͜ͷηογϣϯͷ໨త: Objectives ▸ ϞόΠϧΞϓϦͷ։ൃϓϩηεΛ࣋ଓతʹૉૣ͘ճ͚ͭͮ͠ΒΕΔΑ͏ʹͳΔ ▸ ΤϯδχΞϦϯάνʔϜͱͯ͠ɺΑ͍ DX Λ֫ಘ͢ΔͨΊɺ
 ։ൃʹணख͢Δલ͔Β࣮ࡍʹϦϦʔεͯ͠ӡ༻͍ͯ͘͠·Ͱͷؒʹɺ
 ͲΜͳ͜ͱʹ஫໨ͯ͠ࢪࡦʹऔΓ૊ΉͱΑ͍͔ɺࢦ਑Λࣔ͠·͢ DroidKaigi 2020 3

Slide 4

Slide 4 text

Developer Experience ։ൃମݧ DroidKaigi 2020

Slide 5

Slide 5 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ͳͥ DX ͕ॏཁ͔: Why DX matters? ▸ ϓϩμΫτ͸೔ʑ੒௕Λଓ͚Δ ▸ ৽ػೳͷ௥Ճ ▸ طଘػೳͷվम ▸ όάमਖ਼ ▸ ͳͲ DroidKaigi 2020 5

Slide 6

Slide 6 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ͳͥ DX ͕ॏཁ͔: Why DX matters? ▸ ιʔείʔυ΋ຖ೔มԽΛ͠ଓ͚Δ ▸ ৽͍͠Ϋϥεɾϝιουͷ௥Ճ ▸ طଘΫϥεɾϝιουͷৼΔ෣͍ͷมߋ ▸ ϥΠϒϥϦͷߋ৽ ▸ ͳͲ DroidKaigi 2020 6

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ DroidKaigi 2020 9

Slide 10

Slide 10 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Agenda ▸ ࣄલ४උ ▸ ઃܭ ▸ ։ൃϓϩηε ▸ ϦϦʔε DroidKaigi 2020 10

Slide 11

Slide 11 text

Preparation
 ࣄલ४උ DroidKaigi 2020

Slide 12

Slide 12 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ࣄલ४උ: Preparation ▸ ࣄલʹνʔϜͰ࿩͠߹͓ͬͯ͘ͱΑ͍͜ͱ ▸ ίʔσΟϯάن໿ ▸ ϒϥϯνઓུ DroidKaigi 2020 12

Slide 13

Slide 13 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ίʔσΟϯάن໿: Coding standards ▸ ίʔυͷॻ͖ํ (ελΠϧ) ʹ͍ͭͯೝࣝΛἧ͑Δ ▸ εϖʔεΠϯσϯτ vs λϒΠϯσϯτ ▸ ࠷ऴߦʹվߦΛ͍ΕΔ͔Ͳ͏͔ ▸ Ұߦͷ௕͞ ▸ etc… DroidKaigi 2020 13

Slide 14

Slide 14 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ίʔσΟϯάن໿: Coding standards ▸ ίʔσΟϯάن໿ΛϦϙδτϦͰڞ༗͍ͨ͠ ▸ .editorconfig ▸ IDE ʹίʔσΟϯάن໿ʹैͬͨϑΥʔϚοτΛͤ͞Δ ▸ AndroidStudio/IntelliJ IDEA ͳΒ௥Ճͷ Plugin ͳ͠Ͱ࢖͑Δ DroidKaigi 2020 14

Slide 15

Slide 15 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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

Slide 16

Slide 16 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϒϥϯνઓུ: Branch strategy ▸ ϒϥϯν͝ͱʹ໾ׂΛܾΊͯӡ༻͢ΔͨΊͷϧʔϧͮ͘Γ ▸ Git flow ▸ GitHub flow DroidKaigi 2020 16

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Git flow - Overview DroidKaigi 2020 18 master hotfix!/* release!/* bugfix"/* feature"/* develop v1.1.0 v1.1.1

Slide 19

Slide 19 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Git flow - Feature development DroidKaigi 2020 19 master hotfix!/* release!/* bugfix"/* feature"/* develop Feature A Feature B

Slide 20

Slide 20 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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

Slide 21

Slide 21 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Git flow DroidKaigi 2020 21 master hotfix!/* release!/* bugfix"/* feature"/* develop v1.1.0 v1.1.1 Bugfix

Slide 22

Slide 22 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ GitHub flow ▸ master ▸ ৗʹϦϦʔεՄೳͳࠩ෼Λอ͍࣋ͯ͠Δϒϥϯν DroidKaigi 2020 22

Slide 23

Slide 23 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ GitHub flow DroidKaigi 2020 23 master v1.1.0 v1.3.0 feature"/* Feature A Feature A v1.2.0 Bugfix v1.2.1 Feature C

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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!/*

Slide 29

Slide 29 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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

Slide 30

Slide 30 text

Architecture ઃܭ DroidKaigi 2020

Slide 31

Slide 31 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ઃܭ: Architecture ▸ ઃܭΛߟ͑Δͱ͖ͷϙΠϯτΛ཈͑Δ ▸ ઃܭͷ໨త ▸ ϢχοτςετͰ͔֬ΊΔ͜ͱ͕Β ▸ ઃܭΛॿ͚ΔϞδϡʔϧߏ੒ DroidKaigi 2020 31

Slide 32

Slide 32 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ઃܭ: Architecture ▸ ͲͷύλʔϯΛ࠾༻͢Δͷ͔ ▸ MVVM ▸ MVP ▸ MVC ▸ MVI DroidKaigi 2020 32

Slide 33

Slide 33 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ઃܭ: Architecture ▸ ͲͷύλʔϯΛ࠾༻͢Δͷ͔ ▸ MVVM ▸ MVP ▸ MVC ▸ MVI ▸ ͍ͬͺ͍͋Δ DroidKaigi 2020 33

Slide 34

Slide 34 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ઃܭ: Architecture ▸ ઃܭΛ͢Δ໨త ▸ Ϋϥε΍ϝιουʹద੾ͳ໋໊Λͯ͠ɺؔ৺Λ෼͔Γ΍͘͢͢Δ ▸ ؔ৺Λ෼཭ͯ͠ɺϝϯςφϯε΍ςετΛ͠΍͘͢͢Δ DroidKaigi 2020 34

Slide 35

Slide 35 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ઃܭ: Architecture ▸ ϞόΠϧΞϓϦͷઃܭͰϑΥʔΧε͍ͨ͜͠ͱ ▸ API௨৴΍ϏδωεϩδοΫͳͲΛը໘ͷ࣮૷͔Β͏·͘෼཭͍ͨ͠ ▸ ը໘͕΋ͭঢ়ଶͱͦͷભҠͷํ๏Λ͏·͘දݱ͍ͨ͠ ▸ ঢ়ଶʹԠͨ͡ UI ͷߋ৽ϩδοΫΛ੾Γ཭͍ͨ͠ ▸ ͜ΕΒ͕Ϣχοτςετ͠΍͍͢ܗͰ࣮ݱͰ͖Δ͜ͱ ▸ ඞཁ࠷௿ݶͷϞοΫΛ४උ͢Δ͚ͩͰࡁ·͍ͤͨ DroidKaigi 2020 35

Slide 36

Slide 36 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ઃܭ: Architecture ▸ ͏·͘ઃܭύλʔϯΛ࢖͍͜ͳ͢ (ྫ) ▸ UIɺϏδωεϩδοΫɺAPI ௨৴ͳͲϨΠϠʔΛ͏·͘෼཭͍ͨ͠ ➡ ϨΠϠʔυΞʔΩςΫνϟɺΫϦʔϯΞʔΩςΫνϟ ▸ UI ͕΋ͭঢ়ଶͱͦͷભҠΛ͏·͘දݱ͍ͨ͠ ➡ ReduxɺFluxɺ͋Δ͍͸ LiveData ʹΑΔঢ়ଶભҠͷ௨஌ ▸ UI ͷૢ࡞Λೖྗͱͯ͠ϏδωεϩδοΫΛಈ͔͠ɺͦͷ݁ՌΛ͏·͘ UI ʹ൓ө͍ͨ͠ ➡ MVVMɺDataBindingɺ͋Δ͍͸؆୯ͳҕৡύλʔϯ DroidKaigi 2020 36

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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)) }
 }) } }

Slide 46

Slide 46 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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 } }) } }

Slide 47

Slide 47 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ςετ: Test ▸ State ͷঢ়ଶભҠΛςετ͢Δ৔߹ ▸ ͋Δೖྗʹର͠ɺظ଴ͨ͠௨Γͷ State ͕ग़ྗ͞ΕΔ͜ͱ ▸ State ͷมԽͷॱ൪͕कΒΕ͍ͯΔ͜ͱ ▸ Initial → Loading → Success or Failure DroidKaigi 2020 47

Slide 48

Slide 48 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class SampleViewModel(private val dataSource: SampleDataSource) : ViewModel() { private val viewStatePublisher: MutableLiveData = MutableLiveData(SampleViewState()) val viewState: LiveData = 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)) }
 }) } }

Slide 49

Slide 49 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class SampleViewModel(private val dataSource: SampleDataSource) : ViewModel() { private val viewStatePublisher: MutableLiveData = MutableLiveData(SampleViewState()) val viewState: LiveData = 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

Slide 50

Slide 50 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class SampleViewModel(private val dataSource: SampleDataSource) : ViewModel() { private val viewStatePublisher: MutableLiveData = MutableLiveData(SampleViewState()) val viewState: LiveData = 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

Slide 51

Slide 51 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class SampleViewModel(private val dataSource: SampleDataSource) : ViewModel() { private val viewStatePublisher: MutableLiveData = MutableLiveData(SampleViewState()) val viewState: LiveData = 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

Slide 52

Slide 52 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class SampleViewModel(private val dataSource: SampleDataSource) : ViewModel() { private val viewStatePublisher: MutableLiveData = MutableLiveData(SampleViewState()) val viewState: LiveData = 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

Slide 53

Slide 53 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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() } }

Slide 54

Slide 54 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class SampleViewModelTest : Spek({ Feature("SampleViewModel#startLoading") { val dataSource: SampleDataSource by memoized(CachingMode.EACH_GROUP) { mockk(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 } } })

Slide 55

Slide 55 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ UI ͷঢ়ଶભҠͷςετ: Unit Test for UI state machine class SampleViewModelTest : Spek({ Scenario("Load data successfully") { lateinit var viewModel: SampleViewModel lateinit var observer: Observer lateinit var changedStateSlot: CapturingSlot 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) } } })

Slide 56

Slide 56 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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")))) } } })

Slide 57

Slide 57 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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")))) } } })

Slide 58

Slide 58 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Ϟδϡʔϧߏ੒ͱ໋໊نଇ: Module structure and naming convention ▸ γϯάϧϞδϡʔϧ ▸ ϚϧνϞδϡʔϧ ▸ Dynamic Feature Modules DroidKaigi 2020 58

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϚϧνϞδϡʔϧ: Multiple modules ▸ ϞδϡʔϧΛෳ਺ʹ෼཭͠ɺapk Λੜ੒͢ΔϞδϡʔϧ͕͢΂ͯΛू໿͢Δ ▸ Pros ▸ Ϟδϡʔϧͷ໋໊ʹΑͬͯ໾ׂΛ໌ࣔͰ͖Δ ▸ Gradle ͷ࢓૊ΈͰϏϧυ଎౓Λ্͛΍͍͢ ▸ Cons ▸ Ϟδϡʔϧͷ෼཭ํ๏ʹڞ௨ೝ͕ࣝඞཁ DroidKaigi 2020 60

Slide 61

Slide 61 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ μΠφϛοΫϑΟʔνϟʔϞδϡʔϧ: Dynamic Feature Modules ▸ Dynamic Delivery Λ͢ΔͳΒඞਢ ▸ Pros ▸ ϚϧνϞδϡʔϧ͕ڧ੍ʹͳΔͷͰɺಉ͡ϝϦοτΛಘΒΕΔ ▸ Play Store Ͱ഑෍͢ΔΞϓϦͷαΠζΛখ͘͞Ͱ͖Δ ▸ Cons ▸ Ϟδϡʔϧͷ෼཭ํ๏ʹڞ௨ೝ͕ࣝඞཁ DroidKaigi 2020 61

Slide 62

Slide 62 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Ϟδϡʔϧͷ෼ׂํ๏: Modularization strategy ▸ Ͳ͏͍͏୯ҐͰϞδϡʔϧΛͭ͘Δͷ͔ܾΊ͓ͯ͘ ▸ ϨΠϠʔυΞʔΩςΫνϟͷ૚Ͱ෼ׂ ▸ ػೳ͝ͱʹ෼ׂ ▸ ͜ΕΒͷ૊Έ߹Θͤ DroidKaigi 2020 62

Slide 63

Slide 63 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Ϟδϡʔϧͷ໋໊: Naming convention for module ▸ Ϟδϡʔϧ෼ׂͷ୯ҐʹԠ໋໊ͨ͡نଇΛ࡞͓ͬͯ͘ ▸ ΞϓϦέʔγϣϯຊମ: app ▸ ֤छػೳͷ UI ࣮૷: feature_** ▸ Ϣʔεέʔε: usecase_** ▸ σʔλΞΫηε: repository_** ▸ ڞ௨ॲཧ: common_** DroidKaigi 2020 63

Slide 64

Slide 64 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Ϟδϡʔϧͷґଘؔ܎: Dependencies between modules DroidKaigi 2020 64 feature_a repository_a usecase_a feature_b usecase_b repository_b feature_b usecase_c repository_c

Slide 65

Slide 65 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Ϟδϡʔϧͷґଘؔ܎: Dependencies between modules DroidKaigi 2020 65 feature_a repository_a usecase_a feature_b usecase_b repository_b feature_b usecase_c repository_c

Slide 66

Slide 66 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Ϟδϡʔϧͷґଘؔ܎: Dependencies between modules DroidKaigi 2020 66 feature_a repository_a usecase_a feature_b usecase_b repository_b feature_b usecase_c repository_c

Slide 67

Slide 67 text

Development Process ։ൃϓϩηε DroidKaigi 2020

Slide 68

Slide 68 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ։ൃϓϩηε: Development Process ▸ ೔ʑͷ։ൃϓϩηεΛεϜʔζʹճ͠ଓ͚ΔͨΊͷऔΓ૊Έ ▸ ຊ౰ʹਓͷྗΛඞཁͱ͢Δ৔ॴʹਓ͕ूதͯ͠औΓ૊ΊΔΑ͏ʹ͢Δ ▸ খ͞ͳࣦഊʹૉૣ͘ؾ෇͚Δ࢓૊ΈΛ࡞Δ ▸ ٕज़తͳϑΟʔυόοΫϧʔϓΛ࡞Δ ▸ ࣋ଓతʹऔΓ૊ΈΛճ͍ͯ͘͠࢓૊ΈΛ࡞Δ ▸ ࣗಈԽʂ DroidKaigi 2020 68

Slide 69

Slide 69 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ࣗಈԽ: Automation ▸ ࣗಈԽ͢Δͱ͖ʹߟྀ͢Δ͜ͱ ▸ ඞཁͳ͜ͱΛɺඞཁͳ͚࣮ͩߦ͢Δ͜ͱ ▸ Ϗϧυͷ੒Ռ෺Λ୭Ͱ΋ΞΫηεͰ͖Δ৔ॴʹอଘ͓ͯ͘͜͠ͱ ▸ ϓϩδΣΫτͷ੒௕ͱڞʹεέʔϧ͍͚ͯ͠Δ࢓૊ΈͰ͋Δ͜ͱ DroidKaigi 2020 69

Slide 70

Slide 70 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ࣗಈԽ: Automation ▸ CI (Continuous Integration) ΍ CD (Continuous Delivery) ▸ Ϗϧυ ▸ ςετ ▸ ੩తղੳ ▸ ΞϓϦͷ഑৴ ▸ ೔ʑͷϫʔΫϑϩʔ DroidKaigi 2020 70

Slide 71

Slide 71 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϏϧυͷࣗಈԽ: Build Automation ▸ ࠩ෼͕ਖ਼͘͠ίϯύΠϧͰ͖Δ͜ͱΛอূ ▸ ΞϓϦέʔγϣϯͷ഑৴Λ͢ΔͨΊʹඞཁෆՄܽ ▸ ػೳ։ൃϒϥϯν: debug ϏϧυͷΈੜ੒ ▸ master ΍ develop ͳͲ: release Ϗϧυ΋ੜ੒ DroidKaigi 2020 71

Slide 72

Slide 72 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ςετͷࣗಈԽ: Test Automation ▸ ࠩ෼͕ςετͰ୲อͰ͖Δൣғʹ͓͍ͯਖ਼͘͠ಈ࡞͢Δ͜ͱΛอূ ▸ σάϨΛݕ஌͢ΔͨΊͷୈҰา DroidKaigi 2020 72

Slide 73

Slide 73 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ςετͷࣗಈԽ: Test Automation ▸ ϓϩμΫτͷ੒௕ͱڞʹςετͷ࣮ߦ࣌ؒ΋૿͍͑ͯ͘ ▸ ࣮ߦ࣌ؒΛ୹͘͢Δʹ͸… ▸ Gradle ͕࢖͑ΔϓϩηεͱϞδϡʔϧΛ૿΍ͯ͠ฒྻੑΛ্͛Δ ▸ ςετ࣮ߦ༻ͷίϯςφΛෳ਺্ཱͪ͛ɺ࣮ߦ͢ΔςετΛ෼ࢄ͢Δ ▸ ࣌ؒʹґଘ͍ͯͯ͠଴ͭඞཁͷ͋ΔςετΛݮΒ͢ ▸ etc…… DroidKaigi 2020 73

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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
 …… ୯Ұίϯςφͷ৔߹ ෳ਺ίϯςφͷ৔߹

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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

Slide 78

Slide 78 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ੩తղੳͷࣗಈԽ: Static Analysis Automation ▸ ੩తղੳΛࣗಈԽ͠ίʔυϨϏϡʔʹ໾ཱͯΔ ▸ ಈ͔͢λΠϛϯά͸ Pull Request ͕Ͱ͖ͨ࣌ͳͲ ▸ ੩తղੳͷ݁ՌΛϨϙʔτ͢Δ ▸ xml ΍ html ͷϨϙʔτϑΝΠϧ΋ CI ͷ੒Ռ෺ͱଊ͑Δ ▸ Pull Request ͕͋Ε͹ɺϨϙʔτͷ಺༰Λίϝϯτ͢Δ DroidKaigi 2020 78

Slide 79

Slide 79 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ഑৴ͷࣗಈԽ: Delivery Automation ▸ มߋΛ͍ͭͰ΋ࢼͤΔΑ͏ʹͯ͠ɺϓϩμΫτͷϑΟʔυόοΫϧʔϓΛ࡞Δ ▸ ΞϓϦέʔγϣϯͷϏϧυ͕ऴΘͬͨΒ഑৴ ▸ Firebase App Distribution, DeployGate, HockeyApp, etc… DroidKaigi 2020 79

Slide 80

Slide 80 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϫʔΫϑϩʔͷࣗಈԽ: Automation ▸ ख࡞ۀͷखؒΛͳ͘͠ɺώϡʔϚϯΤϥʔͷ༨஍Λখ͘͢͞Δ DroidKaigi 2020 80

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϫʔΫϑϩʔͷࣗಈԽ: Automation ▸ QA ϑΣʔζͰͷόάमਖ਼Λ master ʹऔΓࠐΉ࡞ۀͷࣗಈԽ DroidKaigi 2020 82 master release"/* master release"/* releaseϒϥϯνӡ༻ͷ
 ࠷ޙʹϚʔδ͢Δ৔߹ releaseϒϥϯνΛఆظతʹ
 ࣗಈͰϚʔδ͢Δ৔߹

Slide 83

Slide 83 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϫʔΫϑϩʔͷࣗಈԽ: How to automate a workflow ▸ CLI ͰࣗಈԽͨ͠λεΫΛ࣮ߦͰ͖ΔΑ͏ʹ͢Δ ▸ Shell Script ▸ Gradle Custom Tasks ▸ etc… DroidKaigi 2020 83

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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

Slide 86

Slide 86 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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

Slide 87

Slide 87 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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

Slide 88

Slide 88 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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

Slide 89

Slide 89 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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

Slide 90

Slide 90 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ Gradle Custom Tasks ʹΑΔࣗಈԽ: Automation with Gradle Custom Tasks "// build.gradle.kts in the project root tasks.register("myTask") { parameter = "Hello, World" } DroidKaigi 2020

Slide 91

Slide 91 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ίʔυϨϏϡʔ: Code Review ▸ ઃܭํ਑΍໋໊ͷΘ͔Γ΍͢͞ɺߟྀ࿙ΕνΣοΫʹਓͷྗΛूத͍ͨ͠ ▸ ࣗಈԽͰ͖Δͱ͜Ζ͸ͲΜͲΜࣗಈԽ͢Δ ▸ ίʔσΟϯάن໿ͷڞ༗: .editorconfig Λ࡞͓ͬͯ͘ ▸ ίʔσΟϯάن໿ʹै͍ͬͯΔ͔νΣοΫ: CI Ͱ ktlint ͳͲΛಈ͔͢ ▸ Α͋͘Δؒҧ͍ΛνΣοΫ: CI Ͱ android lint ͳͲΛಈ͔͢ DroidKaigi 2020 91

Slide 92

Slide 92 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ίʔυϨϏϡʔ: Code Review ▸ υϝΠϯ஌ࣝΛ࣋ͬͨਓʹϨϏϡʔΛґཔ͠ɺίʔυϨϏϡʔͷ࣭Λ্͛Δ ▸ ͩΕʹϨϏϡʔͯ͠΄͍͔͠ઃఆ͢Δ ▸ CODEOWNERS ▸ σΟϨΫτϦ͝ͱʹઃఆ ▸ σΟϨΫτϦ഑ԼͷϑΝΠϧʹࠩ෼Λ࡞ΔͱɺࣗಈͰઃఆͨ͠ਓ͕ϨϏϡϫʔ ʹͳΔ DroidKaigi 2020 92

Slide 93

Slide 93 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ίʔυϨϏϡʔ: Code Review ▸ ίʔυϨϏϡʔʹૉૣ͘औΓ૊ΜͰ։ൃαΠΫϧΛͳΊΒ͔ʹճ͢ ▸ Pull Request ͷઆ໌Λ͏·͘ॻ͘ ▸ Ͳ͏͍͏໨తͷࠩ෼ͳͷ͔Λઆ໌ͯ͠΄͍͠ ▸ ࠩ෼ΛಡΉ্Ͱͷલఏ஌͕ࣝ͋Ε͹આ໌ͯ͠΄͍͠ DroidKaigi 2020 95

Slide 96

Slide 96 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ίʔυϨϏϡʔ: Code Review ▸ νʔϜͰڞ௨ͷϑΥʔϚοτ͕͋Δͱॻ͖΍͘͢ͳΔ ▸ ςϯϓϨʔτͷ׆༻ ▸ GitHub: .github/PULL_REQUEST_TEMPLATE.md ▸ GitLab: .gitlab/merge_request_templates/XXX.md DroidKaigi 2020 96

Slide 97

Slide 97 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ςϯϓϨʔτྫ: Example of pull request template "## Summary "## Kind "## Details "## Links - Task Ticket: - Design Doc: - Crashlytics Issue: "## Screenshot Before | After :——:|:——: | DroidKaigi 2020

Slide 98

Slide 98 text

Release ϦϦʔε DroidKaigi 2020

Slide 99

Slide 99 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϦϦʔε: Release ▸ ҆શͳϦϦʔεΛࢧ͑Δ࢓૊Έͮ͘Γ ▸ ϑΟʔνϟʔϑϥά ▸ ϦϦʔετϨΠϯ ▸ ϞχλϦϯά DroidKaigi 2020 99

Slide 100

Slide 100 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϑΟʔνϟʔϑϥά: Feature Flag ▸ ػೳͷग़͠෼͚Λίϯτϩʔϧͯ͠ૉૣ͍։ൃαΠΫϧΛอͭ ▸ ϑΟʔνϟʔϑϥάΛ࢖͏৔໘ ▸ ஈ֊తʹػೳΛެ։͍ͨ͠ͱ͖ ▸ 1౓ͷϦϦʔεαΠΫϧΛ௒͑ͯػೳ։ൃΛ͍ͨ͠ͱ͖ DroidKaigi 2020 100

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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 ͷϑϥάΛ
 ༗ޮԽ͢Δ

Slide 105

Slide 105 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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 ͋ͨΒ͍͠ઃఆը໘ͷ
 ϨΠΞ΢τ࡞੒ ͋ͨΒ͍͠ઃఆը໘ͷ
 Ϟσϧ࣮૷ Ϟσϧͱը໘Λͭͳ͗͜Ή ઃఆҰཡʹɺ৽͍͠ઃఆը໘ ΁ͷಋઢΛ଍͢

Slide 106

Slide 106 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϑΟʔνϟʔϑϥάͷ஫ҙ఺: Caveats of Feature Flag ▸ ϑΟʔνϟʔϑϥάͰ෼཭ͨ͠ػೳͷ։ൃ͕ଞͷطଘػೳʹӨڹΛ༩͑ͳ͍ DroidKaigi 2020 106 Existing Feature New Feature Existing Logic New Logic Shared Logic Existing Feature New Feature

Slide 107

Slide 107 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϑΟʔνϟʔϑϥάͷ஫ҙ఺: Caveats of Feature Flag ▸ "Release toggles are the last thing you should do" by Martin Fowler ▸ ϑϥάΛ΋ͭ͜ͱͷίετ͸গͳ͔Βͣ͋Δ ▸ ༗ޮԽ͢ΔλΠϛϯά ▸ ࢖Θͳ͘ͳͬͨΒ࡟আ͢Δ ▸ ϑϥάΛ࣋ͭ΄͔ʹऔΓ͏Δબ୒ࢶ ▸ UI ͷͭͳ͗͜ΈΛ࠷ޙʹ͢Δ ▸ ػೳͦͷ΋ͷΛࡉ͔͘Θ͚ɺখ͘͞ϦϦʔε͢Δ DroidKaigi 2020 107

Slide 108

Slide 108 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ϦϦʔετϨΠϯ: Release Train ▸ ϦϦʔεεέδϡʔϧΛݻఆͯ͠ܧଓతʹ҆શͳϦϦʔεΛ͢Δ ▸ ϦϦʔετϨΠϯӡ༻ͷେݪଇ ▸ ܾ·ͬͨεέδϡʔϧ͔Βٯࢉͯ͠։ൃΛ͍ͯ͘͠ ▸ ؒʹ߹Θͳ͍΋ͷ͸࣍ͷϦϦʔε೔Λ଴ͭ ▸ ن໛ͷେ͖ͳ։ൃͳΒͰ͸ͷ՝୊ղܾํ๏ ▸ ͨ͘͞ΜͷϓϩδΣΫτؒͷௐ੔͝ͱΛ෼͔Γ΍͍ͨ͘͢͠ DroidKaigi 2020 108

Slide 109

Slide 109 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ؂ࢹମ੍: Metrics monitoring ▸ ૉૣ͘ҟৗʹؾ͖ͮɺૉૣ͘ରࡦΛଧͭ ▸ ҟৗͷछྨ ▸ ΞϓϦͷΫϥογϡ ▸ ྫ֎έʔεͷසൃ ▸ ϦάϨογϣϯͷൃੜ ▸ ύϑΥʔϚϯεͷྼԽ DroidKaigi 2020 109

Slide 110

Slide 110 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ؂ࢹମ੍: Metrics monitoring ▸ ࢓૊ΈͰղܾ: Crashlytics ▸ Ϋϥογϡ݅਺͕୹࣌ؒͰஶ͘͠৳ͼͨͱ͖ʹ௨஌͢Δ DroidKaigi 2020 110

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ؂ࢹମ੍: Metrics monitoring ▸ ໰୊ͷཧղΛॿ͚Δϩάͷऔಘ ▸ ͲΜͳঢ়گԼͰ͓͖ͨ໰୊͔ ▸ Ͳ͏͍͏ܦ࿏Ͱ͓͖ͨ໰୊͔ ▸ σόΠε͝ͱͷ܏޲͸͋Δ͔ ▸ ΞϓϦόʔδϣϯ͝ͱͷ܏޲͸͋Δ͔ ▸ etc… DroidKaigi 2020 113

Slide 114

Slide 114 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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

Slide 115

Slide 115 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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

Slide 116

Slide 116 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ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

Slide 117

Slide 117 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ΧελϜΩʔͷ௥Ճ: Adding custom keys ▸ Crashlytics ͷϨϙʔτʹ௥Ճ৘ใΛ෇༩͢Δ͘͠Έ ▸ ΞϓϦͷ࣋ͭઃఆ஋ ▸ e.g. XXX ͷػೳ͕༗ޮ͔Ͳ͏͔ ▸ Ϣʔβͷঢ়ଶ ▸ e.g. YYY ͷνϡʔτϦΞϧ͸ऴΘ͔ͬͨͲ͏͔ DroidKaigi 2020 117

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ΧελϜΩʔͷ௥Ճ: Adding custom keys DroidKaigi 2020 120

Slide 121

Slide 121 text

Wrap up ·ͱΊ DroidKaigi 2020

Slide 122

Slide 122 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ·ͱΊ: Wrap up ▸ DX Λܧଓతʹվળ͍ͯ͘͜͠ͱͰ… ▸ ૉૣ͍ػೳ࣮૷͕͠΍͘͢ͳΔ ▸ ૉૣ͍ৼΓฦΓ͕͠΍͘͢ͳΔ ▸ ͦͷ݁ՌɺνʔϜ΋ϓϩμΫτ΋͙Μ͙Μ੒௕͍ͯ͘͠ ▸ ܧଓ͸ྗͳΓ DroidKaigi 2020 122

Slide 123

Slide 123 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ·ͱΊ: Wrap up ▸ ࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑ΔྖҬ ▸ ࣄલ४උ ▸ ઃܭ ▸ ։ൃϓϩηε ▸ ϦϦʔε DroidKaigi 2020 123

Slide 124

Slide 124 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷDXΛࢧ͑Δٕज़ ͍͞͝ʹ: Wrap up ▸ ΋͏૸Γ࢝Ί͍ͯΔϓϩδΣΫτͰ DX Λվળ͢Δʹ͸… ▸ ݱঢ়෼ੳΛͯ͠νʔϜͷ໨ઢΛ߹ΘͤΔ ▸ KPT ͰఆੑతʹνʔϜͷߟ͑Λ·ͱΊͯΈΔ ▸ ςετΧόϨοδ΍ܯࠂ਺ͷਪҠͳͲΛ࢖ͬͯఆྔతʹ࣭ΛଌͬͯΈΔ ▸ ՝୊ײΛἧ͑ΔͱɺԿ͔ΒऔΓ૊ΉͱΑ͍͔΋෼͔Γ΍͘͢ͳΔ DroidKaigi 2020 124

Slide 125

Slide 125 text

࣋ଓతͳΞϓϦ։ൃͷͨΊͷ DXΛࢧ͑Δٕज़ KeithYokoma (Keishin Yokomaku) /
 DroidKaigi 2020