社内向けライブラリを設計・運用する話

934a9e49edc3174d09ab2e09daed5062?s=47 ymnder
December 11, 2018

 社内向けライブラリを設計・運用する話

Otemachi.apk #01 2018/12/11 @ymnd
https://connpass.com/event/109177/

934a9e49edc3174d09ab2e09daed5062?s=128

ymnder

December 11, 2018
Tweet

Transcript

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

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

  3. ࠓ೔ͷ͓඼ॻ͖ ᶃ ࣾ಺޲͚ϥΠϒϥϦͱ͸ ᶄ Կނ։ൃͨ͠ͷ͔ ᶅ Ͳ͏։ൃͨ͠ͷ͔ ᶃ ΞʔΩςΫνϟΛߟ͑Δ ᶄ

    ࣮૷ͯ͠ΈΔ ᶅ ΞϓϦʹಋೖ͢Δ ᶆ ϦϦʔεϑϩʔ 3
  4. ᶃࣾ಺޲͚ϥΠϒϥϦͱ͸ • ࣾ಺ʹͷΈެ։͢ΔϥΠϒϥϦ • ࣾ֎Ͱ࢖ΘΕΔ͜ͱΛ૝ఆ͍ͯ͠ͳ͍ • ڞ௨ͷػೳΛ͘͘Γͩͯ͠ॏෳ࣮ͨ͠૷Λආ͚Δ 4

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

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

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

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

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

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

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

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

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

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

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

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

    /ui -> ը໘ΛೖΕΔ 16 ̍ɿͰ͖Δ͚ͩγϯϓϧʹ ̎ɿςετΛॻ͖΍͘͢
  17. ᶄ࣮૷͢Δɿ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? )
  18. ᶄ࣮૷͢Δɿapi • Service -> RetrofitͷαʔϏε • Client -> OkHttp /

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

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

  21. ᶄ࣮૷͢Δɿ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 } } }
  22. ᶄ࣮૷͢Δɿ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 } } }
  23. ᶄ࣮૷͢Δɿ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?) } }
  24. ᶄ࣮૷͢Δɿ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) }) } }
  25. ᶄ࣮૷͢Δɿ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) }) } }
  26. ᶄ࣮૷͢Δɿapi/api 26 fun <RESPONSE_BODY> Call<RESPONSE_BODY>.enqueue( success: (call: Call<RESPONSE_BODY>?, response: Response<RESPONSE_BODY>?)

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

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

    -> Unit, failure: (throwable: Throwable?) -> Unit) { enqueue(object : Callback<RESPONSE_BODY> { override fun onResponse( call: Call<RESPONSE_BODY>?, response: Response<RESPONSE_BODY>?) = success(call, response) override fun onFailure( call: Call<RESPONSE_BODY>?, t: Throwable?) = failure(t) }) }
  29. ᶄ࣮૷͢Δɿ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) } }
  30. ᶄ࣮૷͢Δɿ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
  31. ᶄ࣮૷͢Δɿapi/api 31 https://github.com/gildor/kotlin-coroutines-retrofit public suspend fun <T : Any> Call<T>.awaitResult():

    Result<T> { return suspendCancellableCoroutine { continuation -> enqueue(object : Callback<T> { override fun onResponse(call: Call<T>?, response: Response<T>) { 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) } }
  32. ᶄ࣮૷͢Δɿapi/api 32 https://github.com/gildor/kotlin-coroutines-retrofit public suspend fun <T : Any> Call<T>.awaitResult():

    Result<T> { return suspendCancellableCoroutine { continuation -> enqueue(object : Callback<T> { override fun onResponse(call: Call<T>?, response: Response<T>) { 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) } }
  33. ᶄ࣮૷͢Δɿ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 <T : Any> Result<T>.ifSucceeded(handler: (data: T) -> Unit): Result<T> { (this as? Result.Ok)?.getOrNull()?.let { handler(it) } return this } Ҏલ Kotlin Conf AppͰ࢖ΘΕ͍ͯͨॻ͖ํΛࢀߟʹ͠·ͨ͠
 https://github.com/JetBrains/kotlinconf-app/commit/3be7c4f50e0fb7bd08096ae25682536c7c5561dc#diff- c0f8fc435376ccc32a73f3834b6e3c7c
  34. ᶄ࣮૷͢Δɿ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()) } }
  35. ᶄ࣮૷͢Δɿ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) } ) } }
  36. ᶄ࣮૷͢Δɿ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) } ) } }
  37. ᶄ࣮૷͢Δɿ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
  38. ᶄ࣮૷͢Δɿ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 }
  39. ᶅΞϓϦʹಋೖ͢Δ ᶃ submodule ᶄ gradle-git-repo-plugin 39

  40. ᶅsubmodule • ௚઀։ൃ؀ڥ͔ΒϦϙδτϦΛ৮ΕΔ • ϥΠϒϥϦΛ௥Ճ͢Δඞཁ͕ͳ͍ • ࣍ͷίϚϯυ͕େࣄ • git submodule

    update —init • ϥΠϒϥϦΞοϓσʔτ͢Δͱ͖ίϚϯυΛ๨Ε͕ͪ 40
  41. ᶅgradle-git-repo-plugin • privateͳϦϙδτϦͰ΋ࢀরͰ͖ΔϓϥάΠϯ • ಺෦తʹ͸gitͷίϚϯυΛୟ͍͍ͯΔ • clone͞ΕͨϦϙδτϦ͸~/.gitRepos/ʹ֨ೲ͞ΕΔ • privateͳϦϙδτϦʹ͋ΔaarΛ࢖͑Δ •

    ίϚϯυΛଧͨͳͯ͘ྑ͍ͷͰ؆୯ • https://qiita.com/kgmyshin/items/87f560172c31c2fbd899 • https://qiita.com/shimada_takuya/items/ 9e8b14ae19540f7ad0a4 41
  42. ᶅ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/
  43. ᶅ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 git@github.com: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ͱಉظͤͣϩʔΧϧϦϙδτϦΛݟΔ
  44. ᶆϦϦʔεϑϩʔ ᶃ 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’ } } }
  45. ͍͞͝ʹ

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

    • ීஈ࢖͏ϥΠϒϥϦͷߏ੒ʹؔ৺͕޲͘ 46
  47. ͍͞͝ʹએ఻Ͱ͢

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

  49. whoami • twitter:@ymnd, github:@ymnder • Application Engineer • Android ೔ܦిࢠ൛ΞϓϦ

    • Android ࢴ໘ϏϡʔΞʔΞϓϦ • noteͰٕज़ॻయͷຊΛ࠶ൢͯ͠·͢ https://note.mu/nikkei_staff/n/n44623c9b9ab4 49
  50. ϥΠϒϥϦ΍͍͖ͬͯ 50