Slide 1

Slide 1 text

ΞϓϦΛϦϦʔεͰ͖Δঢ়ଶʹอͬͨ·· ஈ֊తʹϦϑΝΫλϦϯά͢ΔͨΊͷ ઓུͱઓज़ :VLJ"O[BJ !ZBO[N %SPJE,BJHJ Strategies and tactics for incremental refactoring 1

Slide 2

Slide 2 text

:VLJ"O[BJ w (PPHMF%FWFMPQFS&YQFSUGPS"OESPJE w 9 UXJUUFS !ZBO[N w CMPHZBO[NCMPHTQPUDPN w V1IZDB*OD 2

Slide 3

Slide 3 text

ϦϑΝΫλϦϯά 3

Slide 4

Slide 4 text

ϦϑΝΫλϦϯά $PEFSFGBDUPSJOH JTUIFQSPDFTTPG SFTUSVDUVSJOHFYJTUJOH TPVSDFDPEFXJUIPVU DIBOHJOHJUTFYUFSOBMCFIBWJPS https://en.wikipedia.org/wiki/Code_refactoring 4

Slide 5

Slide 5 text

ͳͥϦϑΝΫλϦϯάͨ͘͠ͳΔͷ͔ʁ 5

Slide 6

Slide 6 text

ͳͥϦϑΝΫλϦϯάͨ͘͠ͳΔͷ͔ʁ w ݹ͍ϥΠϒϥϦΛஔ͖׵͍͑ͨ 6

Slide 7

Slide 7 text

ͳͥϦϑΝΫλϦϯάͨ͘͠ͳΔͷ͔ʁ w ݹ͍ϥΠϒϥϦΛஔ͖׵͍͑ͨˡ֎తཁҼ 7

Slide 8

Slide 8 text

ͳͥϦϑΝΫλϦϯάͨ͘͠ͳΔͷ͔ʁ w $PSPVUJOF w )JMU w 7JFX.PEFM w +FUQBDL$PNQPTF w ެ͕ࣜਪ঑͍ͯ͠Δߏ੒ʹ͍ͨ͠ w ʜ 8

Slide 9

Slide 9 text

ͳͥϦϑΝΫλϦϯάͨ͘͠ͳΔͷ͔ʁ w $PSPVUJOF w )JMU w 7JFX.PEFM w +FUQBDL$PNQPTF w ެ͕ࣜਪ঑͍ͯ͠Δߏ੒ʹ͍ͨ͠ w ʜ ͜ΕΒͰԿ͔Λ ղܾɾվળ ͍ͨ͠ ˡ 9

Slide 10

Slide 10 text

ϦϑΝΫλϦϯάͰԿΛղܾ͍ͨ͠ͷ͔ 10

Slide 11

Slide 11 text

ϦϑΝΫλϦϯάͰԿΛղܾ͍ͨ͠ͷ͔ w Θ͔Γʹ͘͞ 11

Slide 12

Slide 12 text

Θ͔Γʹ͍ͬͯ͘ͳΜͩΖ͏ʁ 12

Slide 13

Slide 13 text

Θ͔Γʹ͍ͬͯ͘ͳΜͩΖ͏ʁ w લఏ஌͕ࣝ࢖͑ͳ͍ 13

Slide 14

Slide 14 text

લఏ஌ࣝ w Ұൠతͳ஌ࣝ w ιϑτ΢ΣΞ։ൃͱͯ͠ͷ஌ࣝ w "OESPJE1MBUGPSNͷ஌ࣝ w "OESPJEΞϓϦ։ൃͷ஌ࣝ w ϓϩδΣΫτಛ༗ͷ஌ࣝ 14

Slide 15

Slide 15 text

લఏ஌ࣝ w Ұൠతͳ஌ࣝ w ୯ޠͷҙຯ w ιϑτ΢ΣΞ։ൃͱͯ͠ͷ஌ࣝ w "OESPJE1MBUGPSNͷ஌ࣝ w "OESPJEΞϓϦ։ൃͷ஌ࣝ w ϓϩδΣΫτಛ༗ͷ஌ࣝ 15

Slide 16

Slide 16 text

લఏ஌ࣝ w Ұൠతͳ஌ࣝ w ιϑτ΢ΣΞ։ൃͱͯ͠ͷ஌ࣝ w "OESPJE1MBUGPSNͷ஌ࣝ w "DUJWJUZ 'SBHNFOU ʜ w "OESPJEΞϓϦ։ൃͷ஌ࣝ w ϓϩδΣΫτಛ༗ͷ஌ࣝ 16

Slide 17

Slide 17 text

લఏ஌ࣝ w Ұൠతͳ஌ࣝ w ιϑτ΢ΣΞ։ൃͱͯ͠ͷ஌ࣝ w "OESPJE1MBUGPSNͷ஌ࣝ w "OESPJEΞϓϦ։ൃͷ஌ࣝ w Α͘࢖ΘΕΔϥΠϒϥϦ΍ެ͓ࣜ͢͢Ίͷߏ੒ w ϓϩδΣΫτಛ༗ͷ஌ࣝ 17

Slide 18

Slide 18 text

લఏ஌ࣝ w Ұൠతͳ஌ࣝ w ιϑτ΢ΣΞ։ൃͱͯ͠ͷ஌ࣝ w "OESPJE1MBUGPSNͷ஌ࣝ w "OESPJEΞϓϦ։ൃͷ஌ࣝ w ϓϩδΣΫτಛ༗ͷ஌ࣝ 18

Slide 19

Slide 19 text

લఏ஌ࣝ w Ұൠతͳ஌ࣝ w ιϑτ΢ΣΞ։ൃͱͯ͠ͷ஌ࣝ w "OESPJE1MBUGPSNͷ஌ࣝ w "OESPJEΞϓϦ։ൃͷ஌ࣝ w ϓϩδΣΫτಛ༗ͷ஌ࣝ 19

Slide 20

Slide 20 text

Θ͔Γʹ͍ͬͯ͘ͳΜͩΖ͏ʁ w લఏ஌͕ࣝ࢖͑ͳ͍ w ೝ஌ೳྗͷݶք w ϚδΧϧφϯόʔ 20

Slide 21

Slide 21 text

Θ͔Γʹ͍ͬͯ͘ͳΜͩΖ͏ʁ w લఏ஌͕ࣝ࢖͑ͳ͍ w ೝ஌ೳྗͷݶք w ϚδΧϧφϯόʔ w άϧʔϓԽ w ֊૚Խ 21

Slide 22

Slide 22 text

Ϟδϡʔϧ͕ଟ͗͢Δ໰୊ w <*.0>ʢτοϓϨϕϧͷʣϞδϡʔϧ਺ΛݸҎԼʹ཈͍͑ͨ 22

Slide 23

Slide 23 text

Ϟδϡʔϧ͕ଟ͗͢Δ໰୊ w <*.0>ʢτοϓϨϕϧͷʣϞδϡʔϧ਺ΛݸҎԼʹ཈͍͑ͨ w άϧʔϓԽ w DPSFNPEFM ʜ w GFBUVSFʜ 23

Slide 24

Slide 24 text

Ϟδϡʔϧ͕ଟ͗͢Δ໰୊ w <*.0>ʢτοϓϨϕϧͷʣϞδϡʔϧ਺ΛݸҎԼʹ཈͍͑ͨ w άϧʔϓԽ w Ϟδϡʔϧͷඞཁੑ w Ϗϧυͷߴ଎Խ w ґଘํ޲ͷڧ੍ w JOUFSOBMΛ࢖࣮ͬͨ૷ͷΧϓηϧԽ 24

Slide 25

Slide 25 text

ϦϑΝΫλϦϯάͷઓུ

Slide 26

Slide 26 text

ϦϑΝΫλϦϯάͷઓུ w ઓུ·ͣ؀ڥΛ੔͑Α w ઓུॱ൪ΛۛຯͤΑɺ຤୺͔Β߈ུͤΑ w ઓུ͍ͭͰ΋தஅͰ͖Δ୯ҐͰ͢͢ΊΑ 26

Slide 27

Slide 27 text

ઓུ·ͣ؀ڥΛ੔͑Α w $* গͳ͘ͱ΋ϏϧυͱϢχοτςετ͸ʜ w WFSTJPODBUBMPHT w GPSNBUDIFDLUPPM w CVJMEMPHJD ͢ͰʹϞδϡʔϧ͕ͨ͘͞Μ͋ΔͳΒ w ❌࠷ॳʹඞཁʹͳΓͦ͏ͳϞδϡʔϧΛશ෦༻ҙ͢Δ 27

Slide 28

Slide 28 text

ઓུॱ൪ΛۛຯͤΑɺ຤୺͔Β߈ུͤΑ w ͳͥॱ൪͕ॏཁͳͷ͔ʁ w ΞϓϦΛϦϦʔεͰ͖Δঢ়ଶʹอͬͨ··ϦϑΝΫλϦϯά͢Δʹ͸ςετ ͕ඞཁ 28

Slide 29

Slide 29 text

ઓུॱ൪ΛۛຯͤΑɺ຤୺͔Β߈ུͤΑ w ͳͥॱ൪͕ॏཁͳͷ͔ʁ w ΞϓϦΛϦϦʔεͰ͖Δঢ়ଶʹอͬͨ··ϦϑΝΫλϦϯά͢Δʹ͸ςετ ͕ඞཁ w "ͷϦϑΝΫλϦϯάʹ͸"ͷVOJUUFTU͕ඞཁ w "͕ґଘ͍ͯ͠Δ#ͷϦϑΝΫλϦϯάΛ͠ͳ͍ͱ"ͷVOJUUFTU͕ॻ͚ ͳ͍ w #ˠ"ͷॱ൪ͰϦϑΝΫλϦϯά͠ͳ͍ͱ͍͚ͳ͍ 29

Slide 30

Slide 30 text

ઓུॱ൪ΛۛຯͤΑɺ຤୺͔Β߈ུͤΑ w ຤୺ͱ͸ w ґଘ͍ͯ͠Δ΋ͷ͕ͳ͍ˠςετͰ͖Δ w ྫʣαʔόʔ΍σʔλϕʔεͱ௨৴͢Δ෦෼ w ґଘ͞Ε͍ͯͳ͍ˠӨڹൣғ͕খ͍͞ˠखಈςετͰ֬ೝ͢Δ͜ͱ͕Մ ೳ w ྫʣ6*ͷಛఆͷύʔπ 30

Slide 31

Slide 31 text

ઓུ͍ͭͰ΋தஅͰ͖Δ୯ҐͰ͢͢ΊΑ w ͍ͭͰ΋தஅͰ͖Δ୯ҐͰஈ֊తʹਐΊΔ 31

Slide 32

Slide 32 text

.PEFM1SPKFDU

Slide 33

Slide 33 text

.PEFM1SPKFDU w Ұ෦,PUMJOͰ΄΅+BWB w 7PMMFZ %FQSFDBUFEMJCSBSJFT w ΄ͱΜͲͷϩδοΫ͕"DUJWJUZ΍'SBHNFOUɺΧελϜ7JFXʹॻ͔Ε͍ͯ Δ w 7JFX.PEFMT )JMU $PNQPTFͳ͠ w 5FTUͳ͠ w 4JOHMFNPEVMF 33

Slide 34

Slide 34 text

ϦϑΝΫλϦϯά͍ͨ͠ͱ͜Ζ w +BWBˠ,PUMJO w 7PMMFZˠ3FUSP fi U w ڊେͳ"DUJWJUZ 'SBHNFOUͷ෼ׂ w ϩδοΫΛద੾ͳΫϥεʹ੾Γ ग़͢ w 7JFX.PEFMTಋೖ w 3FQPTJUPSJFTಋೖ w .VMUJNPEVMF w WFSTJPODBUBMPHTಋೖ w )JMUಋೖ w $PNQPTFಋೖ w %FTJHO4ZTUFN w 5FTUTಋೖ 34

Slide 35

Slide 35 text

ϦϑΝΫλϦϯάͷॱ൪ ؀ڥͷઃఆ $* HSBEMF "(1WFSTJPOͷߋ৽ 7FSTJPODBUBMPHT )JMU 7PMMFZˠ3FUSP fi U .PEVMF௥Ճ 3FQPTJUPSZ 7JFX.PEFMಋೖ $PNQPTF %FTJHOTZTUFN੔උ 35

Slide 36

Slide 36 text

ઓུ·ͣ؀ڥΛ੔͑Α 4USBUFHZTFUVQ&OWJSPONFOUGJSTU

Slide 37

Slide 37 text

$*

Slide 38

Slide 38 text

$*؀ڥͷηοτΞοϓ w (JUIVCBDUJPOT w IUUQTHJUIVCDPNBOESPJEOPXJOBOESPJECMPCNBJOHJUIVC XPSL fl PXT#VJMEZBNM • run: ./gradlew :app:testDebug • run: ./gradlew :app:assemble w #JUSJTF w ʜ 38

Slide 39

Slide 39 text

steps: - name: Checkout uses: actions/checkout@… - name: Set up JDK 17 uses: actions/setup-java@… with: distribution: 'zulu' java-version: 17 - name: Setup Gradle uses: gradle/actions/setup-gradle@… with: validate-wrappers: true gradle-home-cache-cleanup: true - name: Run local tests if: always() run: ./gradlew :app:testDebug - name: Build all build type and flavor permutations run: ./gradlew :app:assemble ࢀߟ :app:assembleDebug for only debug build type 39

Slide 40

Slide 40 text

HSBEMFͱ"(1WFSTJPOͷߋ৽

Slide 41

Slide 41 text

HSBEMFͱ"(1WFSTJPOͷߋ৽ w (SBEMF w "OESPJE(SBEMF1MVHJO 41

Slide 42

Slide 42 text

"(1WFSTJPOͷߋ৽ w Yˠ w OBNFTQBDF w ˠˠY w IUUQTEFWFMPQFSBOESPJEDPNCVJMESFMFBTFTQBTUSFMFBTFT BHQSFMFBTFOPUFTEFGBVMUDIBOHFT • android.enableR8.fullMode = false • android.nonTransitiveRClass = false # if uses resources defined other modules 42

Slide 43

Slide 43 text

7FSTJPODBUBMPHT

Slide 44

Slide 44 text

7FSTJPODBUBMPHT w IUUQTEFWFMPQFSBOESPJEDPNCVJMENJHSBUFUPDBUBMPHT w IUUQTHJUIVCDPNBOESPJEOPXJOBOESPJECMPCNBJOHSBEMF MJCTWFSTJPOTUPNM w ఆ໊ٛͷΞϧϑΝϕοτॱʹฒ΂Δͷ͕͓͢͢Ί androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" } androidx-ktx = { module = "androidx.core:core-ktx", version.ref = "ktx" } androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androi …

Slide 45

Slide 45 text

"EEGPSNBUDIFDLUPPM

Slide 46

Slide 46 text

"EEGPSNBUDIFDLUPPM w TQPUMFTT w IUUQTHJUIVCDPNEJ ff QMVHTQPUMFTT w ࢖༻͢Δ,PUMJOGPSNBUUFSMJCSBSZΛબ΂ΔʢLUGNU LUMJOU ʜʣ w LUMJOU w ʜ 46

Slide 47

Slide 47 text

"EETQPUMFTTUPWFSTJPODBUBMPHT [versions] … ktlint = "1.3.1" … spotless = "6.25.0" … [plugins] … spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } 47

Slide 48

Slide 48 text

CVJMEHSBEMF QSPKFDUMFWFM plugins { … alias(libs.plugins.spotless) } spotless { kotlin { … } kotlinGradle { … } format("xml") { … } } 48

Slide 49

Slide 49 text

spotless { kotlin { target("**/*.kt") targetExclude( "**/build/**/*.kt", "app/src/main/java/com/sample/existing/**/*.kt", ) ktlint(libs.versions.ktlint.get()) .editorConfigOverride( … ) } kotlinGradle { target("*.gradle.kts") ktlint(libs.versions.ktlint.get()) } format("xml") { target("**/*.xml") targetExclude( "**/build/**/*.xml", "app/src/main/res/layout/*.xml", ) } } 49

Slide 50

Slide 50 text

spotless { kotlin { … ktlint(libs.versions.ktlint.get()) .editorConfigOverride( mapOf( "ktlint_standard_function-signature" to "disabled", ), ) } … } [*.{kt,kts}] … ktlint_standard_class-signature=disabled # not applied with spotless .editorcon fi g 50

Slide 51

Slide 51 text

"EEGPSNBUDIFDLUP$* steps: … - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 with: validate-wrappers: true gradle-home-cache-cleanup: true - name: Check spotless run: ./gradlew spotlessCheck - name: Run local tests if: always() run: ./gradlew :app:testDebug 51

Slide 52

Slide 52 text

"EEIJMU

Slide 53

Slide 53 text

)JMU w ͢ͰʹIJMUΛಋೖ͍ͯͨ͠ΓɺDPJOͱ͔ଞͷ%*ϥΠϒϥϦΛಋೖ͍ͯ͠Δ ৔߹ˠઌʹਐΉ w ͢ͰʹIJMUΛ࢖͍ͬͯΔˠઌʹਐΉ w EBHHFS΋IJMU΋࢖ͬͯͳ͍ˠIJMUΛಋೖ͢Δ w EBHHFSΛ࢖͍ͬͯΔ͕·ͩIJMUʹҠߦ͍ͯ͠ͳ͍ˠஈ֊తҠߦ͸Մೳ w ଞͷϦϑΝΫλϦϯάͷલʹIJMU΁ͷҠߦΛ׬ྃ͢Δ͜ͱΛ͓͢͢Ί͠· ͢ 53

Slide 54

Slide 54 text

"EEIJMUUPUIFQSPKFDU w IUUQTEFWFMPQFSBOESPJEDPNUSBJOJOHEFQFOEFODZJOKFDUJPOIJMU BOESPJE 54

Slide 55

Slide 55 text

[versions] … hilt = “2.52" kotlin = "2.0.10" ksp = "2.0.10-1.0.24" … [libraries] … hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } … [plugins] … hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } … libs.versions.toml 55

Slide 56

Slide 56 text

plugins { … alias(libs.plugins.hilt) apply false alias(libs.plugins.ksp) apply false } plugins { … alias(libs.plugins.hilt) alias(libs.plugins.ksp) } dependencies { … implementation(libs.hilt.android) ksp(libs.hilt.compiler) } project level build.gradle module level build.gradle 56

Slide 57

Slide 57 text

@HiltAndroidApp class MyApplication : Application() { … } … AndroidManifest.xml 57

Slide 58

Slide 58 text

ઓུॱ൪ΛۛຯͤΑɺ຤୺͔Β߈ུͤΑ 4UBSUSFGBDUPSJOHGSPNUIFFEHF

Slide 59

Slide 59 text

app module αʔόʔͱ ௨৴͢Δ෦෼ JSON UI { “name” : “…”, … } ItemResponse As-Is 59

Slide 60

Slide 60 text

app module αʔόʔͱ ௨৴͢Δ෦෼ JSON UI Repository (Old) ItemResponse ViewModel api module Item core module Item Item To-Be 60

Slide 61

Slide 61 text

public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { … VolleyUtil.getInstance(this).request( new ItemsRequest( response -> { recyclerViewAdapter.submitList(response); }, error -> { … } ) ); } … } As-Is 61

Slide 62

Slide 62 text

BQJ DPSFNPEVMF

Slide 63

Slide 63 text

app module api module core module dependencies { implementation(project(":core")) … } dependencies { implementation(project(":core")) implementation(project(":api")) … } 63

Slide 64

Slide 64 text

BQJNPEVMF JOUFSOBM3FTQPOTFDMBTTFT JOUFSOBM"QJ$MJFOU 3FUSP fi U "QJJOUFSGBDFBOEJOUFSOBMJNQMFNFOUBUJPOPG"QJJOUFSGBDF XSBQQFS PG"QJ$MJFOU 64

Slide 65

Slide 65 text

internal interface MyApiClient { @GET("/items") suspend fun getItems(): List } @Serializable internal data class ItemResponse( @SerialName("id") val id: Int, @SerialName("name") val name: String, ) JSON MyApiClient ItemResponse api module 65

Slide 66

Slide 66 text

interface MyApi { suspend fun getItems(): List } internal class DefaultMyApi( private val apiClient: MyApiClient ) : MyApi { override suspend fun getItems(): List { val response = apiClient.getItems() return response.map { ItemMapper(it) } } } core module Item JSON api module DefaultMyApi MyApiClient Item ItemResponse MyApi implements 66

Slide 67

Slide 67 text

e.g. { "status" : "NG", "message" : "invalid request parameter" } { "status" : "OK", "data" : { ... } } success error 67

Slide 68

Slide 68 text

{ "status" : "NG", "message" : "invalid request parameter" } { "status" : "OK", "data" : { ... } } success error @Serializable internal data class DataResponse( @SerialName("status") val status: String, @SerialName("message") val message: String?, @SerialName("data") val data: Data?, ) { @Serializable data class Data( @SerialName("id") val id: Int, … ) } e.g. 68

Slide 69

Slide 69 text

internal class DefaultMyApi( private val apiClient: MyApiClient ) : MyApi { override suspend fun getData(): Data { val response = apiClient.getData() if (response.status != "OK") { throw MyApiException.InvalidRequest( message = "status = ${response.status}, message = ${response.mess ) } val data = response.data ?: throw MyApiException.InvalidResponse( message = "data was null", ) return DataMapper(data) } } e.g. de fi ned in core module 69

Slide 70

Slide 70 text

.Z"QJΠϯελϯεͷੜ੒ api module MyApi MyApiClient DefaultMyApi implements uses 70

Slide 71

Slide 71 text

.Z"QJΠϯελϯεͷੜ੒ MyApi MyApiClient DefaultMyApi implements uses MyApiFactory instantiate MyApi api module 71

Slide 72

Slide 72 text

object MyApiFactory { fun create( baseUrl: String, tokenProvider: () -> String?, … ): MyApi { val apiClient = Retrofit.Builder() .baseUrl(baseUrl) .client( OkHttpClient.Builder() .addInterceptor { chain -> … } .build(), ) … .build() .create(MyApiClient::class.java) return DefaultMyApi(apiClient) } } api module 72

Slide 73

Slide 73 text

.Z"QJͷΠϯελϯεΛIJMUͰ؅ཧ͢Δ MyApi app module Hilt Module MyApi DefaultMyApi implements MyApiFactory instantiate api module 73

Slide 74

Slide 74 text

app module @InstallIn(SingletonComponent::class) @Module object AppModule { @Singleton @Provides fun provideMyApi( … ): MyApi { return MyApiFactory.create( tokenProvider = { … }, … ) } } object MyApiFactory { … fun create( … ): MyApi { … } } api module 74

Slide 75

Slide 75 text

JOUFSOBM.Z"QJ'BDUPSZ app module MyApi DefaultMyApi implements MyApiFactory instantiate Hilt Module MyApi api module 75

Slide 76

Slide 76 text

interface TokenProvider { fun provide(): String? } MyApi DefaultMyApi implements MyApiFactory instantiate app module Hilt TokenProvider MyApi api module api module uses 76

Slide 77

Slide 77 text

internal object MyApiFactory { … } @InstallIn(SingletonComponent::class) @Module object ApiModule { @Singleton @Provides fun provideMyApi( tokenProvider: TokenProvider, … ): MyApi { return MyApiFactory.create( tokenProvider::provide, … ) } } api module interface TokenProvider { fun provide(): String? } 77

Slide 78

Slide 78 text

class DefaultTokenProvider( … ) : TokenProvider { override fun provide(): String? { return … } } MyApi DefaultMyApi implements MyApiFactory instantiate app module Hilt TokenProvider implements of TokenProvider MyApi implements api module app module uses 78

Slide 79

Slide 79 text

MyApi DefaultMyApi implements MyApiFactory instantiate app module Hilt TokenProvider implements of TokenProvider MyApi implements Hilt Module api module uses app module @InstallIn(SingletonComponent::class) @Module object AppModule { @Singleton @Provides fun provideTokenProvider( … ): TokenProvider { return DefaultTokenProvider( … ) } } 79

Slide 80

Slide 80 text

5FTU w *UFN3FTQPOTF w *UFN.BQQFS w %FGBVMU.Z"QJ w .Z"QJ'BDUPSZ .Z"QJXJUI.PDL8FC4FSWFSPG0L)UUQ 80

Slide 81

Slide 81 text

api/src/test/resources/items.json [ { "id": "item id", "name": "item name" } ] internal object TestResourceReader { fun readFileAsString( fileName: String, charset: Charset = Charset.defaultCharset(), ): String { return javaClass.classLoader!!.getResource(fileName).readText(charset) } } api/src/test/kotlin/… 81

Slide 82

Slide 82 text

@Test fun items_success() = runTest { val server = MockWebServer().apply { enqueue( MockResponse().setBody( TestResourceReader.readFileAsString("items.json") ) ) start() } val baseUrl = server.url("/").toString() val token = UUID.randomUUID().toString() val api = MyApiFactory.create( baseUrl = baseUrl, tokenProvider = { token } ) val items = api.getItems() 82

Slide 83

Slide 83 text

val items = api.getItems() assertEquals( listOf( Item( id = ItemId("item id"), name = "item name" ) ), items ) val request = server.takeRequest() assertEquals("/items", request.path) assertEquals( "Bearer $token", request.getHeader("Authorization") ) server.shutdown() } 83

Slide 84

Slide 84 text

BQQNPEVMF

Slide 85

Slide 85 text

3FQPTJUPSZ interface ItemRepository { suspend fun getItems(): ApiResult> } class DefaultItemRepository @Inject constructor( private val myApi: MyApi ) : ItemRepository { override suspend fun getItems(): ApiResult> { return try { val result = myApi.getItems() ApiResult.Success(result) } catch (e: MyApiException) { ApiResult.Error(e) } } } 85

Slide 86

Slide 86 text

5FTU class DefaultItemRepositoryTest { private lateinit var repository: ItemRepository private lateinit var api: MyApi @Before fun setup() { api = mockk() repository = DefaultItemRepository(api) } @Test fun success() = runTest { coEvery { api.getItems() } returns listOf(…) val result = repository.getItems() assertEquals(ApiResult.Success(listOf(…)), result) } @Test fun error() = runTest { … } } 86

Slide 87

Slide 87 text

@InstallIn(SingletonComponent::class) @Module interface BindModule { @Singleton @Binds fun bindItemRepository( defaultImplementation: DefaultItemRepository ): ItemRepository } class DefaultItemRepository @Inject constructor( private val myApi: MyApi ) : ItemRepository { … } 87

Slide 88

Slide 88 text

MyApi JSON Repository Item core module Item app module api module 88

Slide 89

Slide 89 text

MyApi JSON Repository Item core module Item app module api module UI (Old) ItemResponse 89

Slide 90

Slide 90 text

MyApi JSON Repository Item core module Item app module api module UI (Old) ItemResponse ViewModel Item convert Item to (Old) ItemResponse 90

Slide 91

Slide 91 text

fun Item.toItemResponse(): ItemResponse { return ItemResponse( id = id.value, iconUrl = iconUrl, name = title, ) } class ItemTest { @Test fun toItemResponse() { val item = Item(…) val itemResponse = item.toItemResponse() assertEquals( ItemResponse(…), itemResponse ) } } @Deprecated("use Item in core module") data class ItemResponse( … ) 91

Slide 92

Slide 92 text

@HiltViewModel class MainViewModel @Inject constructor( private val itemRepository: ItemRepository, ) : ViewModel() { fun request( onSuccess: (List) -> Unit, onError: (Exception) -> Unit, ) { viewModelScope.launch { when (val result = itemRepository.getItems()) { is ApiResult.Error -> { onError(result.e) } is ApiResult.Success -> { onSuccess(result.data.map { it.toItemResponse() }) } } } } } ❌ 92

Slide 93

Slide 93 text

❌ @AndroidEntryPoint public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { … MainViewModel viewModel = new ViewModelProvider(this).get(MainViewModel viewModel.request( VolleyUtil.getInstance(this).request( new ItemsRequest( response -> { recyclerViewAdapter.submitList(response); return Unit.INSTANCE; }, error -> { … return Unit.INSTANCE; } ) ); } … 93

Slide 94

Slide 94 text

@HiltViewModel class MainViewModel @Inject constructor( private val itemRepository: ItemRepository, ) : ViewModel() { fun request( onSuccess: (List) -> Unit, onError: (Exception) -> Unit, ) { viewModelScope.launch { when (val result = itemRepository.getItems()) { is ApiResult.Error -> { onError(result.e) } is ApiResult.Success -> { onSuccess(result.data.map { it.toItemResponse() }) } } } } } ❌ 94

Slide 95

Slide 95 text

@HiltViewModel class MainViewModel @Inject constructor( private val itemRepository: ItemRepository, ) : ViewModel() { private val _items = MutableLiveData>>() val items: LiveData>> get() = _items fun request() { viewModelScope.launch { when (val result = itemRepository.getItems()) { is ApiResult.Error -> _items.postValue(result) is ApiResult.Success -> _items.postValue( ApiResult.Success(result.data.map { it.toItemResponse() }) ) } } } } 95

Slide 96

Slide 96 text

@AndroidEntryPoint public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { … MainViewModel viewModel = new ViewModelProvider(this).get(MainViewModel.class); viewModel.getItems().observe( this, result -> { if (result instanceof ApiResult.Success> response) { adapter.submitList(response.getData()); } else if (result instanceof ApiResult.Error error) { … } } ); viewModel.request(); } … 96

Slide 97

Slide 97 text

,PUMJOԽ

Slide 98

Slide 98 text

,PUMJOԽͷઓུ w େ͖͍ϑΝΠϧͷ··,PUMJOԽ͠ͳ͍ w dߦ͙Β͍ʹݮΒ͔ͯ͠Β 98

Slide 99

Slide 99 text

,PUMJOԽͷॱ൪ TUBUJDͳΫϥεΛUPQMFWFMʹҠಈ public class ItemAdapter extends ListAdapter { … public static class ItemHolder extends RecyclerView.ViewHolder { … } } public class ItemHolder extends RecyclerView.ViewHolder { … } 99

Slide 100

Slide 100 text

,PUMJOԽͷॱ൪ TUBUJDͳΫϥεΛUPQMFWFMʹҠಈ TUBUJD͡Όͳ͍JOOFSDMBTTΛTUBUJDʹͯ͠UPQMFWFMʹҠಈ 100

Slide 101

Slide 101 text

public class MainActivity extends AppCompatActivity { … private void onClickItem(Item item) { … } class ItemAdapter extends ListAdapter { … @Override public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { holder.itemView.setOnClickListener(view -> { onClickItem(getItem(position)); }); } } } 101

Slide 102

Slide 102 text

public class MainActivity extends AppCompatActivity { … private void onClickItem(Item item) { … } static class ItemAdapter extends ListAdapter { private final OnClickListener onClickItemListener; protected ItemAdapter3(OnClickListener listener) { … this.onClickItemListener = listener; } @Override public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { holder.itemView.setOnClickListener(view -> { onClickItemListener.onClick(getItem(position)); }); } } } 102

Slide 103

Slide 103 text

,PUMJOԽͷॱ൪ TUBUJDͳΫϥεΛUPQMFWFMʹҠಈ TUBUJD͡Όͳ͍JOOFSDMBTTΛTUBUJDʹͯ͠UPQMFWFMʹҠಈ ͜ͷΫϥεͷ੹຿Ͱ͸ͳ͍ॲཧΛɺผΫϥεΛ࡞ͬͯҕৡ 103

Slide 104

Slide 104 text

,PUMJOԽͷॱ൪ TUBUJDͳΫϥεΛUPQMFWFMʹҠಈ TUBUJD͡Όͳ͍JOOFSDMBTTΛTUBUJDʹͯ͠UPQMFWFMʹҠಈ ͜ͷΫϥεͷ੹຿Ͱ͸ͳ͍ॲཧΛɺผΫϥεΛ࡞ͬͯҕৡ͢Δ )FMQFSతͳDMBTTPCKFDUΛLPUMJOͰ༻ҙ͠ɺߦ͘Β͍ʹͳΔ·Ͱϝ ιουΛҠಈ͢Δ 104

Slide 105

Slide 105 text

,PUMJOԽͷॱ൪ TUBUJDͳΫϥεΛUPQMFWFMʹҠಈ TUBUJD͡Όͳ͍JOOFSDMBTTΛTUBUJDʹͯ͠UPQMFWFMʹҠಈ ͜ͷΫϥεͷ੹຿Ͱ͸ͳ͍ॲཧΛɺผΫϥεΛ࡞ͬͯҕৡ͢Δ )FMQFSతͳDMBTTPCKFDUΛLPUMJOͰ༻ҙ͠ɺߦ͘Β͍ʹͳΔ·Ͱϝ ιουΛҠಈ͢Δ LPUMJOԽ͠ɺIFMQFSΫϥεͱ߹ମ 105

Slide 106

Slide 106 text

,PUMJOԽͷDPNNJU த਎͕+BWBͷ··֦ுࢠΛKBWB͔ΒLUʹม͑ͯDPNNJU͢Δ ֦ுࢠΛLU͔ΒKBWBʹ໭͢ʢDPNNJU͠ͳ͍ʣ ,PUMJOԽͯ͠DPNNJU͢Δ % mv ItemAdapter.java ItemAdapter.java.kt % git add -A % git commit -m 'rename ItemAdapter.java to ItemAdapter.kt' % mv ItemAdapter.kt ItemAdapter.java % git add -A % git commit -m 'Kotlinize ItemAdapter' 106

Slide 107

Slide 107 text

,PUMJOԽͷDPNNJU 107

Slide 108

Slide 108 text

$PNQPTF΁ͷҠߦ

Slide 109

Slide 109 text

"CTUSBDU$PNQPTF7JFXΛܧঝͨ͠$VTUPN7JFX class DetailActivityComposeView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : AbstractComposeView(context, attrs, defStyleAttr) { @Composable override fun Content() { MyTheme { } } } 109

Slide 110

Slide 110 text

$PNQPTF7JFXͷݶք class DetailActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_detail) val composeView = findViewById(R.id.compose_view) composeView.setContent { MyTheme { Scaffold { … } } } } } only with kotlin 110

Slide 111

Slide 111 text

؆୯ͳ7JFXΛ$VTUPN7JFXʹஔ͖׵͑Δ 111

Slide 112

Slide 112 text

@Composable fun DetailContent( title: String, ) { Text( text = title, … ) } @Preview @Composable private fun Preview() { MyTheme { Surface { DetailContent( title = "title", ) } } } 112

Slide 113

Slide 113 text

class DetailActivityComposeView @JvmOverloads constructor( … ) : AbstractComposeView(context, attrs, defStyleAttr) { var title by mutableStateOf("") @Composable override fun Content() { MyTheme { Surface { DetailContent( title = title, ) } } } } 113

Slide 114

Slide 114 text

public class DetailActivity extends AppCompatActivity { private TextView titleView; private DetailActivityComposeView composeView; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { … titleView = findViewById(R.id.title_view); composeView = findViewById(R.id.compose_view); } private void update() { titleView.setText(…); composeView.setTitle(…); } } 114

Slide 115

Slide 115 text

$PNQPTF΁ͷҠߦεςοϓ "CTUSBDU$PNQPTF7JFXΛܧঝͨ͠$VTUPN7JFXΛ࡞Δ ؆୯ͳ7JFXΛ$VTUPN7JFXʹஔ͖׵͑Δ ྡ઀͢Δ7JFXΛ$VTUPN7JFXʹऔΓࠐΉ શͯͷ7JFX͕औΓࠐ·ΕΔ·ͰεςοϓΛ܁Γฦ͢ 115

Slide 116

Slide 116 text

3FDZDMFS7JFXˠ$PNQPTF

Slide 117

Slide 117 text

3FDZDMFS7JFXˠ$PNQPTF JUFNMBZPVUJO3FDZDMFS7JFXˠ$PNQPTF 3FDZDMFS7JFXˠ-B[Z$PMVNO-B[Z3PX 117

Slide 118

Slide 118 text

JUFNMBZPVUJO3FDZDMFS7JFXˠ$PNQPTF 7JFX)PMEFSͷϦϑΝΫλϦϯά 7JFX)PMEFSˠ$PNQPTF 7JFX)PMEFSͷϨΠΞ΢τʹରԠ͢ΔDPNQPTBCMFΛ࡞Δ ͷDPNQPTBCMFΛར༻͢Δ$VTUPN7JFXΛ "CTUSBDU$PNQPTF7JFXͰ࡞Δ ͷ$VTUPN7JFXΛ7JFX)PMEFSͰ࢖͏ શͯͷ7JFX)PMEFSʹରͯ͠εςοϓΛߦ͏ 118

Slide 119

Slide 119 text

7JFX)PMEFSͷϦϑΝΫλϦϯά w ੜ੒ϝιου w CJOEϝιου 119

Slide 120

Slide 120 text

public class ItemViewHolder extends RecyclerView.ViewHolder { … public static ItemViewHolder create(@NonNull ViewGroup parent) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.list_item, parent, false); return new ItemViewHolder(view); } } public class ItemAdapter extends ListAdapter { … @NonNull @Override public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) return ItemViewHolder.create(parent); } } 120

Slide 121

Slide 121 text

public class ItemViewHolder extends RecyclerView.ViewHolder { private final TextView titleView; … public void bind(@NonNull String iconUrl, @NonNull String title) { … titleView.setText(title); } … } public class ItemAdapter extends ListAdapter { … @Override public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { Item item = getItem(position); holder.bind(item.iconUrl, item.title); } } 121

Slide 122

Slide 122 text

7JFX)PMEFSʹରԠ͢ΔDPNQPTBCMFΛ࡞Δ @Composable fun ItemContent( iconUrl: String, title: String, modifier: Modifier = Modifier ) { Row(…) { AsyncImage( model = iconUrl, contentDescription = null, modifier = Modifier.size(40.dp) ) Text( text = title, … ) } } 122

Slide 123

Slide 123 text

"CTUSBDU$PNQPTF7JFXͰ$VTUPN7JFXΛ࡞Δ class ItemView @JvmOverloads constructor( … ) : AbstractComposeView(context, attrs, defStyleAttr) { var iconUrl by mutableStateOf("") var title by mutableStateOf("") @Composable override fun Content() { MyTheme { Surface { ItemContent( iconUrl = iconUrl, title = title, ) } } } } 123

Slide 124

Slide 124 text

7JFX)PMEFSͰ$VTUPN7JFXΛ࢖͏ public class ItemViewHolder extends RecyclerView.ViewHolder { private final ItemView itemView; private ItemViewHolder(@NonNull ItemView itemView) { super(itemView); this.itemView = itemView; } public void bind(@NonNull String iconUrl, @NonNull String title) { itemView.setIconUrl(iconUrl); itemView.setTitle(title); } public static ItemViewHolder create(@NonNull ViewGroup parent) { return new ItemViewHolder(new ItemView(parent.getContext())); } } 124

Slide 125

Slide 125 text

3FDZDMFS7JFXˠ-B[Z$PMVNO-B[Z3PX 3FDZDMFS7JFXʹରԠ͢ΔDPNQPTBCMFΛ࡞Δʢ-B[Z$PMVNO-B[Z3PX ͷDPNQPTBCMFΛར༻͢Δ$VTUPN7JFXΛ"CTUSBDU$PNQPTF7JFX Ͱ࡞Δ "DUJWJUZ'SBHNFOUͰͷDPNQPTBCMFΛ࢖͏ 125

Slide 126

Slide 126 text

3FDZDMFS7JFXʹରԠ͢ΔDPNQPTBCMFΛ༻ҙ @Composable fun ItemList( items: List ) { LazyColumn { items( items = items, key = { it.id.value }, contentType = { "item" } ) { item -> ItemContent( iconUrl = item.iconUrl, title = item.title ) } } } 126

Slide 127

Slide 127 text

"CTUSBDU$PNQPTF7JFXͰ$VTUPN7JFXΛ࡞Δ class ItemListView @JvmOverloads constructor( … ) : AbstractComposeView(context, attrs, defStyleAttr) { var items by mutableStateOf>(emptyList()) @Composable override fun Content() { MyTheme { Surface { ItemList(items) } } } } 127

Slide 128

Slide 128 text

"DUJWJUZ'SBHNFOUͰ$VTUPN7JFXΛ࢖͏ … 128

Slide 129

Slide 129 text

"DUJWJUZ'SBHNFOUͰ$VTUPN7JFXΛ࢖͏ public class MainActivity extends AppCompatActivity { private RecyclerView recyclerView; private ItemAdapter adapter; private ItemListView itemListView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); recyclerView = findViewById(R.id.recycler_view); itemListView = findViewById(R.id.item_list_view); } … private void update(List items) { adapter.submitList(items); itemListView.setItems(items); } } 129

Slide 130

Slide 130 text

·ͱΊ

Slide 131

Slide 131 text

·ͱΊ w "OESPJEͷڞ௨ͷ஌ࣝͱਓؒͷೝ஌ͷݶքΛ׆༻ͯ͠ɺϓϩδΣΫτ͕ΑΓ Θ͔Γ΍͘͢ͳΔΑ͏ͳϦϑΝΫλϦϯάΛ͢Δ w ϦϑΝΫλϦϯάͷॱ൪͕ॏཁɺςετͰ͖Δ຤୺෦෼͔Β͸͡ΊΔ w "CTUSBDU$PNQPTF7JFXΛܧঝͨ͠$VTUPN7JFXΛ׆༻͠ɺஈ֊తʹ DPNQPTFԽ͍ͯ͘͠ 131

Slide 132

Slide 132 text

5IBOLZPV 132