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のレビュー文化

自己紹介文: 2021年にLINEに新卒入社し、LINEアプリのAndroidクライアントについて、主にAndroid OSの新機能を適用する業務に携わっています。車を走らせることが大好きです。
イベント情報
- 名称 : After DroidKaigi 2022
- 開催日時: 10月12日(水)19時
- 場所:オンライン
- 申込URL: https://yj-meetup.connpass.com/event/261714/
採用情報
LINE株式会社では一緒に働くエンジニアを募集しています!
LINEには多くのサービスがありますので、ぜひ気になるものがあればチェックしてみてください。