LINE株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。 LINEヤフー Tech Blog

Blog


【DroidKaigi 2022】Code Review Challengeの概要と解説まとめ

2022年10月5日(水)〜7日(金)の3日間にわたって開催された、DroidKaigi 2022にてLINEはゴールドスポンサーを務めました。

会期中はLINEのブースにたくさんの方にお越し頂き、誠にありがとうございました。

本ブログでは、イベント当日2日間実施した「Code Review Challenge」の解説をはじめ、LINEのコードレビューの取り組みや文化についてご紹介いたします。

「Code Review Challenge」に参加してくださった方も、そうでない方も、ぜひ本ブログで再チャレンジしてみて下さい。

Code Review Challengeの概要

DroidKaigi 2022のイベント当日、「Bad Code」を「Good Code」に変える、Code Review Challengeを実施しました。
この問題は、LINEの社内でおこなっているレビュー会で紹介されたBad Codeの書き方を参考に、エンジニアが一から考えたオリジナルの問題※になります。

2日間、来場される方に何度も楽しんでいただけるように全部で5つの問題を出題しました。

Schedule

  • Day1

    • 10:00-12:30 出題1

    • 12:30-15:00 出題2

    • 15:00-18:00 出題3

  • Day2

    • 10:00-14:00 出題4

    • 14:00-18:00 出題5

※この企画のために、LINEのエンジニアが考えたコードです。LINEで提供されるサービスの実際のコードではありません。

Code Review Challenge 問題について

解説は後日順次公開とさせていただく予定です。解説ブログが出るまでは、「Bad Code」を「Good Code」に変える、Code Review Challenge に取り組んでみてください。

Reviewの内容はハッシュタグ #CodeReviewChallenge をつけてTwitterでぜひ呟いてください。みんなでReviewをしてより良いコードを世に増やしていきましょう。

【出題1】Day1 10:00-12:30

この問題は「 千北一期 / Chigita Kazuki」が作りました

所属:LINEコミュニケーションサービス開発チーム

※ 解説記事: 
https://engineering.linecorp.com/ja/blog/code_review_challenge_1

sealed class ContactType {
    object Friend : ContactType()
    object Bot : ContactType()
}
 
class friend_item_Presenter(
    private val friendNameProvider: FriendNameProvider,
    private val coroutineScope: CoroutineScope
) {
    fun updateViews(itemId: String) {
        coroutineScope.launch {
            nameTextView.text = friendNameProvider.getName(itemId, ContactType.Friend)
            // update other view component
        }
    }
}
 
class BotItemPresenter(
    private val nameProvider: NameProvider,
    private val coroutineScope: CoroutineScope
) {
    fun updateViews(itemId: String) {
        coroutineScope.launch {
            val name = nameProvider.getName(itemId, ContactType.Bot) ?: return@launch
            val displayName = name.ifBlank {
                "No name Bot"
            }
            nameTextView.text = displayName
            // update other view component
        }
    }
}
 
interface NameProvider {
    suspend fun getName(uuid: String, contactType: ContactType): String?
}
 
class FriendNameProvider(
    val friendRepository: FriendRepository
) : NameProvider {
    override suspend fun getName(uuid: String, contactType: ContactType): String {
        if (contactType == ContactType.Friend) {
            val name = friendRepository.getSuperCoolName(uuid)
            return name
        }
        return ""
    }
}
 
class BotNameProvider(
    private val botRepository: BotRepository
) : NameProvider {
    override suspend fun getName(uuid: String, contactType: ContactType): String? {
        if (contactType == ContactType.Friend) {
            return null
        }
        val name = botRepository.getSuperCoolBotName(uuid)
        return name
    }
}

【出題2】Day1 12:30-15:00

この問題は「 池永健一 / Ikenaga Kenichi」が作りました!

所属:LINEコミュニケーション基盤開発チーム

※ 解説記事: 
https://engineering.linecorp.com/ja/blog/code_review_challenge_2

class PlayerPresenter(
    private val loadingView: View,
    private val playButton: View,
    private val pauseButton: View,
    private val progressView: TextView,
    private val player: Player,
    private val lifecycleScope: CoroutineScope
) {
    private var isPlaying: Boolean = false
 
    init {
        showLoadingView()
        lifecycleScope.launch {
            withContext(Dispatchers.IO) {
                player.prepare()
            }
            showPausingView()
        }
    }
 
    private fun showLoadingView() {
        loadingView.isVisible = true
        playButton.isVisible = false
        pauseButton.isVisible = false
        progressView.isVisible = false
    }
 
    private fun showPausingView() {
        loadingView.isVisible = false
        playButton.isVisible = true
        playButton.setOnClickListener {
            player.play()
            showPLayingView()
        }
        pauseButton.isVisible = false
        progressView.isVisible = true
        isPlaying = false
    }
 
    private fun showPLayingView() {
        loadingView.isVisible = false
        playButton.isVisible = false
        pauseButton.isVisible = true
        pauseButton.setOnClickListener {
            player.pause()
            showPausingView()
        }
        progressView.isVisible = true
        isPlaying = true
        lifecycleScope.launch {
            while (isPlaying) {
                progressView.text = "${player.currentPosition / 1000}/${player.duration / 1000}"
                delay(100)
            }
        }
    }
}

【出題3】Day1 15:00-18:00

この問題は「 石川宗寿 / Ishikawa Munetoshi」が作りました!

所属:ディベロッパーエクスペリエンス開発チーム

※ 解説記事: 
https://engineering.linecorp.com/ja/blog/code_review_challenge_3

class ProfileImageRepository(
    private val context: Context,
    private val metadataDao: ProfileImageMetadataDao = ProfileImageMetadataDao(),
    private val apiClient: RemoteApiClient = RemoteApiClient(context),
    private val ioScheduler: CoroutineContext = Dispatchers.IO
) {
    private data class ProfileImageCache(
        val userId: UserId,
        val bitmap: Bitmap,
        val expirationMillis: Long
    )
 
    private val fileCacheDirectory: File = context.cacheDir.resolve(PROFILE_IMAGE_DIRECTORY_NAME)
    private val memoryCache: MutableMap<UserId, ProfileImageCache> = mutableMapOf()
 
    suspend fun getProfileImage(userId: UserId): Bitmap? = withContext(ioScheduler) {
        val memoryCachedData = memoryCache[userId]
            ?.takeIf { it.expirationMillis >= System.currentTimeMillis() }
        if (memoryCachedData != null) {
            return@withContext memoryCachedData.bitmap
        }
 
        val (bitmap, expirationMillis) = getProfileImageFromLocalCacheOrQuery(userId)
            ?: return@withContext null
        memoryCache[userId] = ProfileImageCache(userId, bitmap, expirationMillis)
        bitmap
    }
 
    private suspend fun getProfileImageFromLocalCacheOrQuery(
        userId: UserId
    ): Pair<Bitmap, Long>? = withContext(ioScheduler) {
        val cachedExpirationMillis =
            metadataDao.queryExpirationMillis(userId) ?: Long.MIN_VALUE
        val cacheFile = fileCacheDirectory.resolve(userId.stringValue)
        val fileCachedBitmap = cacheFile.takeIf(File::exists)
            ?.readAsBitmap()
            ?.takeIf { cachedExpirationMillis < System.currentTimeMillis() }
        if (fileCachedBitmap != null) {
            return@withContext fileCachedBitmap to cachedExpirationMillis
        }
        val (bitmap, expirationMillis) = queryProfileImage(userId) ?: return@withContext null
        metadataDao.storeExpirationMillis(userId, expirationMillis)
        cacheFile.writeAsBitmap(bitmap)
        bitmap to expirationMillis
    }
 
    private suspend fun queryProfileImage(userId: UserId): Pair<Bitmap, Long>? =
        withContext(ioScheduler) {
            val response = apiClient.queryProfileImage(userId.stringValue)
                ?: return@withContext null
            val bitmap =
                BitmapFactory.decodeByteArray(response.bitmapBytes, 0, response.bitmapBytes.size)
            bitmap to response.expirationMillis
        }
 
    companion object {
        private const val PROFILE_IMAGE_DIRECTORY_NAME = "profile_image"
    }
}

【出題4】Day2 10:00-14:00

この問題は「 安藤祐貴  / Ando Yuki」が作りました!

所属:iOS/Androidエクスペリエンス開発チーム

※ 解説記事: 
https://engineering.linecorp.com/ja/blog/code_review_challenge_4

data class StickerLayoutState(
    val stickerID: StickerID,
    val stickerName: String,
    val stickerImage: String,
    val stickerType: StickerType,
    val creatorName: String,
    val creatorImage: String,
    val reviewPageIndex: Int,
    val reviewPageTotalCount: Int,
    val currentPageReviews: List<ReviewModel>,
    val isDetailPageOpened: Boolean,
    val publicationDateText: String,
    val contentSummaryText: String,
)
private var layoutState: StickerLayoutState = StickerLayoutState.Empty
class StickerType(val isOfficial: Boolean, val isUserMade: Boolean, val pageUrl: PageUrl)
class PageUrl(val officialUrl: String?, val userPageUrl: String?)
fun setSticker(binding: StickerLayoutBinding, stickerModel: StickerModel) {
    layoutState = layoutState.copy(
        stickerID = stickerModel.id,
        stickerName = stickerModel.stickerName,
        creatorName = stickerModel.creatorName,
        stickerImage = stickerModel.stickerImage
    )
    updateViewByBinding(binding)
}
fun setDetailLayoutOpened(binding: StickerLayoutBinding, isOpened: Boolean) {
    layoutState = StickerLayoutState.Empty.copy(isDetailPageOpened = isOpened)
    updateViewByBinding(binding)
}
fun onReviewPageLoaded(
    binding: StickerLayoutBinding,
    reviewPageIndex: Int,
    reviewPageTotalCount: Int,
    currentPageReviews: List<ReviewModel>
) {
    layoutState = layoutState.copy(
        reviewPageIndex = reviewPageIndex,
        reviewPageTotalCount = reviewPageTotalCount,
        currentPageReviews = currentPageReviews
    )
    updateViewByBinding(binding)
}
fun onDetailLayoutLoaded(
    binding: StickerLayoutBinding,
    stickerModel: StickerModel,
    detaliModel: StickerDetailModel
) {
    layoutState = layoutState.copy(
        isDetailPageOpened = true,
        stickerType = stickerModel.stickerType,
        creatorName = stickerModel.creatorName,
        stickerImage = detaliModel.stickerImage,
        creatorImage = detaliModel.creatorImage,
        publicationDateText = detaliModel.publicationDateText,
        contentSummaryText = detaliModel.contentSummaryText
    )
    updateViewByBinding(binding)
}
private fun updateViewByBinding(binding: StickerLayoutBinding) { /*update view by `layoutState`*/ }

【出題5】Day2 14:00-18:00

この問題は「 玉木英嗣  / Tamaki Hidetsugu」が作りました!

所属:LINEコミュニケーションサービス開発チーム

※ 解説記事: 
https://engineering.linecorp.com/ja/blog/code_review_challenge_5

class MyContentProvider : ContentProvider() {
 
    private val singleThreadCoroutineDispatcher: CoroutineDispatcher =
        Executors.newSingleThreadExecutor().asCoroutineDispatcher()
 
    // Do not request again if succeeded once.
    private val getResultCache: MutableMap<Long, String> = mutableMapOf()
     
    private val apiClient: ApiClient = ApiClient()
 
    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor {
        val id: Long = TODO() // Get id value from `uri`.
        val result = runBlocking {
            get(id)
        }
        return MatrixCursor(TODO()) // Create MatrixCursor from `result`.
    }
 
    private suspend fun get(id: Long): String = withContext(singleThreadCoroutineDispatcher) {
        val resultFromCache = getResultCache[id]
        if (resultFromCache != null) {
            return@withContext resultFromCache
        }
        val result = try {
            apiClient.get(id)
        } catch (_: Throwable) {
            showErrorMessage()
            null
        }
        getResultCache[id] = result
        return@withContext result
    }
 
    private suspend fun showErrorMessage() = withContext(Dispatchers.Main) {
        Toast.makeText(this, R.string.error, Toast.LENGTH_SHORT).show()
    }
 
    private class ApiClient {
 
        @Throws(IOException::class)
        suspend fun get(id: Long): String = withContext(Dispatchers.IO) {
            // Access to the server with HTTP request.
            delay(100L)
            return TODO()
        }
    }
}

After DroidKaigi 2022 で解説します!

10月12日(水)19時よりDroidKaigi 2022に協賛している株式会社ZOZO、ヤフー株式会社、LINE株式会社の3社合同で「After DroidKaigi 2022」を開催いたします。

当日は、Code Review Challengeの作問を担当した安藤による、各社の社員による上記取り組みの解説とCode Review Challengeの裏側(問題準備など)とそれを支えたLINEのレビュー文化について紹介する予定です。

どんなふうに準備をして当日を迎えたのか。参考になる情報を盛り沢山でお届けする予定です。

エンジニアの方であればどなたでも参加可能です。ぜひお気軽にご参加ください。

Code Review Challengeの裏側とLINEのレビュー文化

安藤 祐貴 (Yuki Ando) / Twitter / Software(Android) Engineer / LINE株式会社 iOS/Androidエクスペリエンス開発チーム

自己紹介文: 2021年にLINEに新卒入社し、LINEアプリのAndroidクライアントについて、主にAndroid OSの新機能を適用する業務に携わっています。車を走らせることが大好きです。

イベント情報

採用情報

LINE株式会社では一緒に働くエンジニアを募集しています!
LINEには多くのサービスがありますので、ぜひ気になるものがあればチェックしてみてください。

https://linecorp.com/ja/career/ja/all?tag=35