Slide 1

Slide 1 text

ࣾ಺޲͚ϥΠϒϥϦΛઃܭɾӡ༻͢Δ࿩ Otemachi.apk #01 2018/12/11 @ymnd

Slide 2

Slide 2 text

ࠓճ͓࿩͠ͳ͍͜ͱ ࣾ֎޲͚ͷϥΠϒϥϦͷ࣮૷ʹ͍ͭͯ →ଞͷϥΠϒϥϦʹґଘ͠ͳ͍ઃܭ →Kotlinͷόʔδϣϯଞ 2

Slide 3

Slide 3 text

ࠓ೔ͷ͓඼ॻ͖ ᶃ ࣾ಺޲͚ϥΠϒϥϦͱ͸ ᶄ Կނ։ൃͨ͠ͷ͔ ᶅ Ͳ͏։ൃͨ͠ͷ͔ ᶃ ΞʔΩςΫνϟΛߟ͑Δ ᶄ ࣮૷ͯ͠ΈΔ ᶅ ΞϓϦʹಋೖ͢Δ ᶆ ϦϦʔεϑϩʔ 3

Slide 4

Slide 4 text

ᶃࣾ಺޲͚ϥΠϒϥϦͱ͸ • ࣾ಺ʹͷΈެ։͢ΔϥΠϒϥϦ • ࣾ֎Ͱ࢖ΘΕΔ͜ͱΛ૝ఆ͍ͯ͠ͳ͍ • ڞ௨ͷػೳΛ͘͘Γͩͯ͠ॏෳ࣮ͨ͠૷Λආ͚Δ 4

Slide 5

Slide 5 text

ᶃࣾ಺޲͚ϥΠϒϥϦͱ͸ • ࣾ಺ʹͷΈެ։͢ΔϥΠϒϥϦ • ࣾ֎Ͱ࢖ΘΕΔ͜ͱΛ૝ఆ͍ͯ͠ͳ͍ • ڞ௨ͷػೳΛ͘͘Γͩͯ͠ॏෳ࣮ͨ͠૷Λආ͚Δ ྫ͑͹ɺ ಡऀϑΥʔϜɺܭଌπʔϧɺσόοάϝχϡʔͳͲ 5

Slide 6

Slide 6 text

ᶃࣾ಺޲͚ϥΠϒϥϦͱ͸ • ࣾ಺ʹͷΈެ։͢ΔϥΠϒϥϦ • ࣾ֎Ͱ࢖ΘΕΔ͜ͱΛ૝ఆ͍ͯ͠ͳ͍ • ڞ௨ͷػೳΛ͘͘Γͩͯ͠ॏෳ࣮ͨ͠૷Λආ͚Δ ྫ͑͹ɺ ಡऀϑΥʔϜɺܭଌπʔϧɺσόοάϝχϡʔͳͲ →ΞϓϦ͝ͱʹ͕ࠩͳ͍ػೳΛ·ͱΊΔ 6

Slide 7

Slide 7 text

ᶃࣾ಺޲͚ϥΠϒϥϦͱ͸ɿྫ • ͝ҙݟ͝ཁ๬ϥΠϒϥϦ 7

Slide 8

Slide 8 text

ᶃࣾ಺޲͚ϥΠϒϥϦͱ͸ɿྫ • ϢʔβʔͷҙݟΛऩू͢ΔͨΊʹߏங͞Εͨ • ౤ߘ͞Εͨ͝ҙݟ͸SlackͱBacklogʹ௨஌͞ΕΔ • ো֐ΛૣظൃݟͰ͖Δ໾ׂ΋୲͏ • ৄ͘͠͸ฐٕࣾज़ϒϩάΛ͝ཡ͍ͩ͘͞ • https://hack.nikkei.com/blog 8

Slide 9

Slide 9 text

ᶄԿނ։ൃͨ͠ͷ͔ • ిࢠ൛ͱࢴ໘ϏϡʔΞʔͱ͍͏̎ͭͷΞϓϦΛఏڙ͢Δ • ͝ҙݟ͝ཁ๬͸ɺΞϓϦ͝ͱʹ͕ࠩ͋ΔػೳͰ͸ͳ͍ • ॏෳͨ͠։ൃΛආ͚ϝϯςφϯείετΛԼ͍͛ͨ 9

Slide 10

Slide 10 text

ᶄԿނ։ൃ͢΂͖͔ • ϥΠϒϥϦͮ͘ΓͷୈҰาͰ͋Δ • ࣾ಺ʹด͍ͯ͡ΔͨΊ๯ݥͰ͖Δ • ࣗ෼ͷߟ͑ͨ࠷ળͷߏ੒ΛͱΕΔ 10 →ීஈͷ։ൃ͔Β཭Εͯݕ౼Ͱ͖Δ

Slide 11

Slide 11 text

ᶅͲ͏։ൃͨ͠ͷ͔ • Կ͕ඞཁ͔Λߟ͑Δ • Ͳ͏͍͏ը໘͕ඞཁ͔ • Ͳ͏͍͏ػೳ͕ඞཁ͔ 11

Slide 12

Slide 12 text

ᶅͲ͏։ൃͨ͠ͷ͔ɿը໘Λߟ͑Δ ᶃ ొ࿥ϑΥʔϜʹϢʔβʔ͕ೖྗ͢Δ ᶄ ૹ৴ϘλϯΛԡԼͨ͠ͱ͖ʹ֬ೝը໘͕ग़Δ ᶅ OKԡԼޙʹૹ৴͠ɺૹ৴தͰ͋Δ͜ͱΛදࣔ͢Δ ᶆ ૹ৴ޙʹ׬ྃը໘Λग़͢ 12

Slide 13

Slide 13 text

ᶅͲ͏։ൃͨ͠ͷ͔ɿը໘Λߟ͑Δ 13

Slide 14

Slide 14 text

ᶅͲ͏։ൃͨ͠ͷ͔ɿػೳΛߟ͑Δ • ૹ৴લʹจࣈ਺ΛνΣοΫ͢Δ • αʔόʔʹσʔλΛૹ৴͢Δ • ֤छը໘Λग़͢ 14

Slide 15

Slide 15 text

ᶃΞʔΩςΫνϟΛߟ͑Δ • /model -> ϦΫΤετɾϨεϙϯεΛೖΕΔ • /api -> ௨৴ॲཧΛೖΕΔ • /ui -> ը໘ΛೖΕΔ 15

Slide 16

Slide 16 text

ᶃΞʔΩςΫνϟΛߟ͑Δ • /model -> ϦΫΤετɾϨεϙϯεΛೖΕΔ • /api -> ௨৴ॲཧΛೖΕΔ • /ui -> ը໘ΛೖΕΔ 16 ̍ɿͰ͖Δ͚ͩγϯϓϧʹ ̎ɿςετΛॻ͖΍͘͢

Slide 17

Slide 17 text

ᶄ࣮૷͢Δɿmodel 17 data class Request( @SerializedName("body") private val body: String, @SerializedName("to") private val to: String, @SerializedName("from") private val from: String ) data class Response( @SerializedName("posted_at") val postedAt: Long, @SerializedName("to") val to: String?, @SerializedName("message") val message: String? )

Slide 18

Slide 18 text

ᶄ࣮૷͢Δɿapi • Service -> RetrofitͷαʔϏε • Client -> OkHttp / Retrofit ͷΠϯελϯεΛฦ͢ • Api -> ࣮ࡍͷૹ৴ॲཧΛॻ͘ • Sender -> ApiΛwrap͢Δ 18

Slide 19

Slide 19 text

ᶄ࣮૷͢Δɿapi 19 Service Client Api Sender

Slide 20

Slide 20 text

ᶄ࣮૷͢Δɿapi 20 Service Client Api Sender DB

Slide 21

Slide 21 text

ᶄ࣮૷͢Δɿapi/client 21 class KaizenClient { companion object { @JvmStatic fun create(userAgent: String, debugMode: Boolean = false): HogeService { val retrofit = createRetrofit(userAgent, debugMode) return retrofit.create(AtlasService::class.java) } internal fun createRetrofit(userAgent: String, debugMode: Boolean = false): Retrofit { val gson = //… val httpLoggingInterceptor = //… val userAgentInterceptor = //… val client = //… return Retrofit.Builder() .client(client) .addConverterFactory(GsonConverterFactory.create(gson)) .baseUrl(getBaseUrl(debugMode)) .build() } internal fun getBaseUrl(debugMode: Boolean): String { return if (debugMode) DEBUG_API_BASE else RELEASE_API_BASE } } }

Slide 22

Slide 22 text

ᶄ࣮૷͢Δɿapi/client 22 class KaizenClient { companion object { @JvmStatic fun create(userAgent: String, debugMode: Boolean = false): HogeService { val retrofit = createRetrofit(userAgent, debugMode) return retrofit.create(AtlasService::class.java) } internal fun createRetrofit(userAgent: String, debugMode: Boolean = false): Retrofit { val gson = //… val httpLoggingInterceptor = //… val userAgentInterceptor = //… val client = //… return Retrofit.Builder() .client(client) .addConverterFactory(GsonConverterFactory.create(gson)) .baseUrl(getBaseUrl(debugMode)) .build() } internal fun getBaseUrl(debugMode: Boolean): String { return if (debugMode) DEBUG_API_BASE else RELEASE_API_BASE } } }

Slide 23

Slide 23 text

ᶄ࣮૷͢Δɿapi/api 23 interface KaizenApi { fun sendRequestAsync(request: Request, successCallback: OnSuccessCallback? = null, failureCallback: OnFailureCallback? = null ) suspend fun sendRequestAwait(request: Request, successCallback: OnSuccessCallback? = null, failureCallback: OnFailureCallback? = null) interface OnSuccessCallback { fun onSuccess(responseBody: ResponseBody?) } interface OnFailureCallback { fun onFailure(error: Throwable?) } }

Slide 24

Slide 24 text

ᶄ࣮૷͢Δɿapi/api 24 class KaizenApiImpl(private val service: KaizenService) : KaizenApi { override fun sendRequestAsync(request: Request, successCallback: KaizenApi.OnSuccessCallback?, failureCallback: KaizenApi.OnFailureCallback? ) { service.sendAction( //… ).enqueue({ _, response -> val isSuccess: Boolean = if (response?.isSuccessful != null) response.isSuccessful else false if (!isSuccess) { val e = Exception(“…“) failureCallback?.onFailure(e) } else { successCallback?.onSuccess(response?.body()) } }, { throwable -> failureCallback?.onFailure(throwable) }) } }

Slide 25

Slide 25 text

ᶄ࣮૷͢Δɿapi/api 25 class KaizenApiImpl(private val service: KaizenService) : KaizenApi { override fun sendRequestAsync(request: Request, successCallback: KaizenApi.OnSuccessCallback?, failureCallback: KaizenApi.OnFailureCallback? ) { service.sendAction( //… ).enqueue({ _, response -> val isSuccess: Boolean = if (response?.isSuccessful != null) response.isSuccessful else false if (!isSuccess) { val e = Exception(“…“) failureCallback?.onFailure(e) } else { successCallback?.onSuccess(response?.body()) } }, { throwable -> failureCallback?.onFailure(throwable) }) } }

Slide 26

Slide 26 text

ᶄ࣮૷͢Δɿapi/api 26 fun Call.enqueue( success: (call: Call?, response: Response?) -> Unit, failure: (throwable: Throwable?) -> Unit) { enqueue(object : Callback { override fun onResponse( call: Call?, response: Response?) = success(call, response) override fun onFailure( call: Call?, t: Throwable?) = failure(t) }) }

Slide 27

Slide 27 text

ᶄ࣮૷͢Δɿapi/api 27 fun Call.enqueue( success: (call: Call?, response: Response?) -> Unit, failure: (throwable: Throwable?) -> Unit) { enqueue(object : Callback { override fun onResponse( call: Call?, response: Response?) = success(call, response) override fun onFailure( call: Call?, t: Throwable?) = failure(t) }) }

Slide 28

Slide 28 text

ᶄ࣮૷͢Δɿapi/api 28 fun Call.enqueue( success: (call: Call?, response: Response?) -> Unit, failure: (throwable: Throwable?) -> Unit) { enqueue(object : Callback { override fun onResponse( call: Call?, response: Response?) = success(call, response) override fun onFailure( call: Call?, t: Throwable?) = failure(t) }) }

Slide 29

Slide 29 text

ᶄ࣮૷͢Δɿapi/api 29 override suspend fun sendRequestAwait(request: Request, successCallback: KaizenApi.OnSuccessCallback?, failureCallback: KaizenApi.OnFailureCallback?) { service.sendAction( //… ).awaitResult() .ifSucceeded { res -> successCallback?.onSuccess(res) } .ifFailed { exception -> failureCallback?.onFailure(exception) } }

Slide 30

Slide 30 text

ᶄ࣮૷͢Δɿapi/api 30 override suspend fun sendRequestAwait(request: Request, successCallback: KaizenApi.OnSuccessCallback?, failureCallback: KaizenApi.OnFailureCallback?) { service.sendAction( //… ).awaitResult() .ifSucceeded { res -> successCallback?.onSuccess(res) } .ifFailed { exception -> failureCallback?.onFailure(exception) } } https://github.com/gildor/kotlin-coroutines-retrofit

Slide 31

Slide 31 text

ᶄ࣮૷͢Δɿapi/api 31 https://github.com/gildor/kotlin-coroutines-retrofit public suspend fun Call.awaitResult(): Result { return suspendCancellableCoroutine { continuation -> enqueue(object : Callback { override fun onResponse(call: Call?, response: Response) { continuation.resume( if (response.isSuccessful) { val body = response.body() if (body == null) { Result.Exception(NullPointerException("Response body is null")) } else { Result.Ok(body, response.raw()) } } else { Result.Error(HttpException(response), response.raw()) } ) } //… }) registerOnCompletion(continuation) } }

Slide 32

Slide 32 text

ᶄ࣮૷͢Δɿapi/api 32 https://github.com/gildor/kotlin-coroutines-retrofit public suspend fun Call.awaitResult(): Result { return suspendCancellableCoroutine { continuation -> enqueue(object : Callback { override fun onResponse(call: Call?, response: Response) { continuation.resume( if (response.isSuccessful) { val body = response.body() if (body == null) { Result.Exception(NullPointerException("Response body is null")) } else { Result.Ok(body, response.raw()) } } else { Result.Error(HttpException(response), response.raw()) } ) } //… }) registerOnCompletion(continuation) } }

Slide 33

Slide 33 text

ᶄ࣮૷͢Δɿapi/api 33 override suspend fun sendRequestAwait(request: Request, successCallback: KaizenApi.OnSuccessCallback?, failureCallback: KaizenApi.OnFailureCallback?) { service.sendAction( //… ).awaitResult() .ifSucceeded { res -> successCallback?.onSuccess(res) } .ifFailed { exception -> failureCallback?.onFailure(exception) } } inline fun Result.ifSucceeded(handler: (data: T) -> Unit): Result { (this as? Result.Ok)?.getOrNull()?.let { handler(it) } return this } Ҏલ Kotlin Conf AppͰ࢖ΘΕ͍ͯͨॻ͖ํΛࢀߟʹ͠·ͨ͠
 https://github.com/JetBrains/kotlinconf-app/commit/3be7c4f50e0fb7bd08096ae25682536c7c5561dc#diff- c0f8fc435376ccc32a73f3834b6e3c7c

Slide 34

Slide 34 text

ᶄ࣮૷͢Δɿapi/api 34 class KaizenApiImplTest : MockWebServerTest() { override fun onSetUp() { kaizenApi = KaizenApiImpl(retrofit.create(KaizenService::class.java)) request = Request(…) successCallback = spy(object : KaizenApi.OnSuccessCallback { override fun onSuccess(responseBody: ResponseBody?) { latch.countDown() } }) //… } @Test fun sendRequestAsync() { latch = CountDownLatch(1) kaizenApi.sendRequestAsync(…) latch.await() verify(successCallback, times(1)).onSuccess(any()) verify(failureCallback, never()).onFailure(any()) } @Test fun sendRequestAsync_error() { latch = CountDownLatch(1) kaizenApi.sendRequestAsync(…) latch.await() verify(successCallback, never()).onSuccess(any()) verify(failureCallback, times(1)).onFailure(any()) } }

Slide 35

Slide 35 text

ᶄ࣮૷͢Δɿapi/sender 35 class RequestSenderImpl( private val kaizenApi: KaizenApi = KaizenApiImpl(…), private val onSuccessListener: OnSuccessListener?, private val onFailureListener: OnFailureListener? ) : RequestSender(onSuccessListener, onFailureListener) { override suspend fun sendRequest(request: Request) { kaizenApi.sendRequestAwait(request, { response -> successListenerWeakRef.get()?.onSuccess(response) }, { error -> failureListenerWeakRef.get()?.onFailure(error, request) } ) } }

Slide 36

Slide 36 text

ᶄ࣮૷͢Δɿapi/sender 36 class RequestSenderImpl( private val kaizenApi: KaizenApi = KaizenApiImpl(…), private val onSuccessListener: OnSuccessListener?, private val onFailureListener: OnFailureListener? ) : RequestSender(onSuccessListener, onFailureListener) { override suspend fun sendRequest(request: Request) { kaizenApi.sendRequestAwait(request, { response -> successListenerWeakRef.get()?.onSuccess(response) }, { error -> failureListenerWeakRef.get()?.onFailure(error, request) } ) } }

Slide 37

Slide 37 text

ᶄ࣮૷͢Δɿapi/sender 37 @Test fun processSend() { val api = mock(KaizenApi::class.java) val entity = mock(KaizenRequestEntity::class.java) val sender = RequestSenderImpl.Builder("") .apply { kaizenApi = api } .build() runBlocking { sender.processSend(entity) verify(api, never()).sendRequestAsync(any(), any(), any()) verify(api, times(1)).sendRequestAwait(any(), any(), any()) } } https://github.com/mockito/mockito/pull/1032

Slide 38

Slide 38 text

ᶄ࣮૷͢Δɿui 38 class RequestForm : RelativeLayout { constructor(context: Context?) : super(context) //… var requestSender: RequestSender? = null private fun init(context: Context) { val layoutInflater = LayoutInflater.from(context) layoutInflater.inflate(R.layout.form, this, true) editText = findViewById(R.id.edit_text) as EditText editText.addTextChangedListener(object : TextWatcher { override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) { val strLength = charSequence.length val isLengthValid = validateStrLength(strLength) sendBtn.isEnabled = isLengthValid bodyCounter.text = formatBodyCounter(strLength) } }) } private fun validateStrLength(strLength: Int): Boolean = strLength in minStrLength..maxStrLength }

Slide 39

Slide 39 text

ᶅΞϓϦʹಋೖ͢Δ ᶃ submodule ᶄ gradle-git-repo-plugin 39

Slide 40

Slide 40 text

ᶅsubmodule • ௚઀։ൃ؀ڥ͔ΒϦϙδτϦΛ৮ΕΔ • ϥΠϒϥϦΛ௥Ճ͢Δඞཁ͕ͳ͍ • ࣍ͷίϚϯυ͕େࣄ • git submodule update —init • ϥΠϒϥϦΞοϓσʔτ͢Δͱ͖ίϚϯυΛ๨Ε͕ͪ 40

Slide 41

Slide 41 text

ᶅgradle-git-repo-plugin • privateͳϦϙδτϦͰ΋ࢀরͰ͖ΔϓϥάΠϯ • ಺෦తʹ͸gitͷίϚϯυΛୟ͍͍ͯΔ • clone͞ΕͨϦϙδτϦ͸~/.gitRepos/ʹ֨ೲ͞ΕΔ • privateͳϦϙδτϦʹ͋ΔaarΛ࢖͑Δ • ίϚϯυΛଧͨͳͯ͘ྑ͍ͷͰ؆୯ • https://qiita.com/kgmyshin/items/87f560172c31c2fbd899 • https://qiita.com/shimada_takuya/items/ 9e8b14ae19540f7ad0a4 41

Slide 42

Slide 42 text

ᶅgradle-git-repo-pluginɿಋೖ 42 //build.gradle dependencies { classpath 'com.github.atsushi-ageet:gradle-git-repo-plugin:2.0.4' } //app/build.gradle apply plugin: ‘git-repo' repositories { // Username repository name branch directory github("Nikkei", "KaizenRequest_Android", "master", "repository") } dependencies { implementation ('com.nikkei:kaizenrequest:1.4.1') { } • forkઌͷϥΠϒϥϦΛ࢖͍ͬͯΔ • grgitͷόʔδϣϯ͕௿͍ͨΊGitHub΁઀ଓͰ͖ͳ͔ͬͨ • https://blog.github.com/2018-02-23-weak-cryptographic-standards-removed/

Slide 43

Slide 43 text

ᶅgradle-git-repo-pluginɿCircle CI 43 version: 2 jobs: build: working_directory: ~/code docker: - image: circleci/android:api-25-alpha environment: _JAVA_OPTIONS: -Xms512m -Xmx1024m steps: - checkout - add_ssh_keys: fingerprints: - “ssh_key” - run: cp ssh_config ~/.ssh/config - run: git clone [email protected]:ymnder/Hoge.git ~/.gitRepos/ymnder/hoge_library - run: name: Run Unit Test command: ./gradlew testReleaseUnitTest -Poffline • CIʹ͸ݸผͷDeploy KeysΛ౉͢Α͏ʹ͍ͯ͠Δ • add_ssh_keysΛ͢Δ͜ͱͰ伴Λऔಘ͢Δ • gitRepoͷgitΛ࢖Θͣɺࣄલʹclone͢Δ • TestͰ͸ -Poffline Λ͚ͭΔ͜ͱͰɺGitHubͱಉظͤͣϩʔΧϧϦϙδτϦΛݟΔ

Slide 44

Slide 44 text

ᶆϦϦʔεϑϩʔ ᶃ PR͕approved͞ΕͨΒ࣍ͷίϚϯυΛ࣮ߦ͢Δ • ./gradlew uploadArchives ᶄ aar͕࡞੒͞ΕΔͷͰɺcommitΛpush͢Δ ᶅ masterʹϚʔδɺgit tagΛ͚ͭΔ ᶆ ࢖༻ΞϓϦଆͰϥΠϒϥϦόʔδϣϯΛ্͛Δ 44 // app/build.gradle uploadArchives { repositories { mavenDeployer { repository url: "file://${repo.absolutePath}" pom.version = "${versionMajor}.${versionMinor}.${versionPatch}" pom.groupId = 'com.nikkei' pom.artifactId = ‘library_name’ } } }

Slide 45

Slide 45 text

͍͞͝ʹ

Slide 46

Slide 46 text

ࣾ಺޲͚ϥΠϒϥϦΛͭ͘Δͱྑ͍͜ͱ • ࣾ಺ϥΠϒϥϦ͸ٕज़ݕূͷ৔ͱͯ͠࢖͑Δ • Kotlin Coroutines • Room • ͋ΒΏΔτϥοϓΛ౿Έൈ͘͜ͱͰ͍֮͑ͯ͘ • ීஈ࢖͏ϥΠϒϥϦͷߏ੒ʹؔ৺͕޲͘ 46

Slide 47

Slide 47 text

͍͞͝ʹએ఻Ͱ͢

Slide 48

Slide 48 text

ʊਓਓਓਓਓਓਓਓਓਓਓਓਓਓਓʊ ʼɹDroidKaigi 2019Ͱొஃ͠·͢ɹʻ ʉY^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^ʉ ৄղఆظߪೖ 48

Slide 49

Slide 49 text

whoami • twitter:@ymnd, github:@ymnder • Application Engineer • Android ೔ܦిࢠ൛ΞϓϦ • Android ࢴ໘ϏϡʔΞʔΞϓϦ • noteͰٕज़ॻయͷຊΛ࠶ൢͯ͠·͢ https://note.mu/nikkei_staff/n/n44623c9b9ab4 49

Slide 50

Slide 50 text

ϥΠϒϥϦ΍͍͖ͬͯ 50