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. ࣾ಺޲͚ϥΠϒϥϦΛઃܭɾӡ༻͢Δ࿩
    Otemachi.apk #01 2018/12/11 @ymnd

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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?
    )

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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
    }
    }
    }

    View Slide

  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
    }
    }
    }

    View Slide

  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?)
    }
    }

    View Slide

  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)
    })
    }
    }

    View Slide

  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)
    })
    }
    }

    View Slide

  26. ᶄ࣮૷͢Δɿ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)
    })
    }

    View Slide

  27. ᶄ࣮૷͢Δɿ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)
    })
    }

    View Slide

  28. ᶄ࣮૷͢Δɿ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)
    })
    }

    View Slide

  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)
    }
    }

    View Slide

  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

    View Slide

  31. ᶄ࣮૷͢Δɿ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)
    }
    }

    View Slide

  32. ᶄ࣮૷͢Δɿ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)
    }
    }

    View Slide

  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 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

    View Slide

  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())
    }
    }

    View Slide

  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) }
    )
    }
    }

    View Slide

  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) }
    )
    }
    }

    View Slide

  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

    View Slide

  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
    }

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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/

    View Slide

  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 [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ͱಉظͤͣϩʔΧϧϦϙδτϦΛݟΔ

    View Slide

  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’
    }
    }
    }

    View Slide

  45. ͍͞͝ʹ

    View Slide

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

    View Slide

  47. ͍͞͝ʹએ఻Ͱ͢

    View Slide

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

    View Slide

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

    View Slide

  50. ϥΠϒϥϦ΍͍͖ͬͯ
    50

    View Slide