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

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

ymnder
December 11, 2018

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

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

ymnder

December 11, 2018
Tweet

More Decks by ymnder

Other Decks in Programming

Transcript

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

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

    Retrofit ͷΠϯελϯεΛฦ͢ • Api -> ࣮ࡍͷૹ৴ॲཧΛॻ͘ • Sender -> ApiΛwrap͢Δ 18
  4. ᶄ࣮૷͢Δɿ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 } } }
  5. ᶄ࣮૷͢Δɿ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 } } }
  6. ᶄ࣮૷͢Δɿ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?) } }
  7. ᶄ࣮૷͢Δɿ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) }) } }
  8. ᶄ࣮૷͢Δɿ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) }) } }
  9. ᶄ࣮૷͢Δɿ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) }) }
  10. ᶄ࣮૷͢Δɿ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) }) }
  11. ᶄ࣮૷͢Δɿ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) }) }
  12. ᶄ࣮૷͢Δɿ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) } }
  13. ᶄ࣮૷͢Δɿ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
  14. ᶄ࣮૷͢Δɿ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) } }
  15. ᶄ࣮૷͢Δɿ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) } }
  16. ᶄ࣮૷͢Δɿ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
  17. ᶄ࣮૷͢Δɿ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()) } }
  18. ᶄ࣮૷͢Δɿ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) } ) } }
  19. ᶄ࣮૷͢Δɿ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) } ) } }
  20. ᶄ࣮૷͢Δɿ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
  21. ᶄ࣮૷͢Δɿ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 }
  22. ᶅgradle-git-repo-plugin • privateͳϦϙδτϦͰ΋ࢀরͰ͖ΔϓϥάΠϯ • ಺෦తʹ͸gitͷίϚϯυΛୟ͍͍ͯΔ • clone͞ΕͨϦϙδτϦ͸~/.gitRepos/ʹ֨ೲ͞ΕΔ • privateͳϦϙδτϦʹ͋ΔaarΛ࢖͑Δ •

    ίϚϯυΛଧͨͳͯ͘ྑ͍ͷͰ؆୯ • https://qiita.com/kgmyshin/items/87f560172c31c2fbd899 • https://qiita.com/shimada_takuya/items/ 9e8b14ae19540f7ad0a4 41
  23. ᶅ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/
  24. ᶅ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ͱಉظͤͣϩʔΧϧϦϙδτϦΛݟΔ
  25. ᶆϦϦʔεϑϩʔ ᶃ 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’ } } }
  26. whoami • twitter:@ymnd, github:@ymnder • Application Engineer • Android ೔ܦిࢠ൛ΞϓϦ

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