Skip to content

Commit

Permalink
Merge pull request #777 from DroidKaigi/mikan/profilecard/improve-per…
Browse files Browse the repository at this point in the history
…formance

ProfileCard: improve performance to first launch
  • Loading branch information
takahirom authored Aug 25, 2024
2 parents 0aca858 + cb935c3 commit 9e1ac60
Show file tree
Hide file tree
Showing 17 changed files with 615 additions and 424 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.github.droidkaigi.confsched.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import javax.inject.Qualifier

@Qualifier
public annotation class IoDispatcher

@InstallIn(SingletonComponent::class)
@Module
public class DispatcherModule {
@IoDispatcher
@Provides
public fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
}
1 change: 1 addition & 0 deletions core/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ kotlin {
implementation(libs.ktorKotlinxSerialization)
implementation(libs.ktorContentNegotiation)
implementation(libs.kermit)
implementation(libs.qrcodeKotlin)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import io.github.droidkaigi.confsched.data.di.RepositoryQualifier
import io.github.droidkaigi.confsched.di.IoDispatcher
import io.github.droidkaigi.confsched.model.ProfileCardRepository
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Singleton

@Module
Expand All @@ -25,8 +27,9 @@ internal abstract class ProfileCardRepositoryModule {
@Provides
fun provideProfileCardRepository(
profileCardDataStore: ProfileCardDataStore,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
): ProfileCardRepository {
return DefaultProfileCardRepository(profileCardDataStore)
return DefaultProfileCardRepository(profileCardDataStore, ioDispatcher)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@ import androidx.compose.runtime.remember
import io.github.droidkaigi.confsched.compose.safeCollectAsRetainedState
import io.github.droidkaigi.confsched.model.ProfileCard
import io.github.droidkaigi.confsched.model.ProfileCardRepository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.getDrawableResourceBytes
import org.jetbrains.compose.resources.getSystemResourceEnvironment
import qrcode.QRCode

public class DefaultProfileCardRepository(
private val profileCardDataStore: ProfileCardDataStore,
private val ioDispatcher: CoroutineDispatcher,
) : ProfileCardRepository {
@Composable
override fun profileCard(): ProfileCard {
Expand All @@ -22,4 +30,18 @@ public class DefaultProfileCardRepository(
override suspend fun save(profileCard: ProfileCard.Exists) {
profileCardDataStore.save(profileCard)
}

@OptIn(ExperimentalResourceApi::class)
override suspend fun loadQrCodeImageByteArray(link: String, centerLogoRes: DrawableResource): ByteArray {
return withContext(ioDispatcher) {
val logoImage = getDrawableResourceBytes(
environment = getSystemResourceEnvironment(),
resource = centerLogoRes,
)
QRCode.ofSquares()
.withLogo(logoImage, 400, 400)
.build(link)
.renderToBytes()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import org.koin.core.module.Module
import org.koin.core.module.dsl.singleOf
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module
import platform.Foundation.NSDocumentDirectory
Expand Down Expand Up @@ -165,6 +166,11 @@ public val dataModule: Module = module {
DefaultProfileCardDataStore(dataStore)
}

// Since Kotlin/Native doesn't support Dispatchers.IO, we use Dispatchers.Default instead.
single(named("IoDispatcher")) {
Dispatchers.Default
}

singleOf(::DefaultAuthApi) bind AuthApi::class
singleOf(::DefaultSessionsApiClient) bind SessionsApiClient::class
singleOf(::DefaultContributorsApiClient) bind ContributorsApiClient::class
Expand All @@ -179,7 +185,12 @@ public val dataModule: Module = module {
singleOf(::DefaultSponsorsRepository) bind SponsorsRepository::class
singleOf(::DefaultEventMapRepository) bind EventMapRepository::class
singleOf(::DefaultAboutRepository) bind AboutRepository::class
singleOf(::DefaultProfileCardRepository) bind ProfileCardRepository::class
single<ProfileCardRepository> {
DefaultProfileCardRepository(
profileCardDataStore = get(),
ioDispatcher = get(named("IoDispatcher")),
)
}
single<Repositories> {
DefaultRepositories(
mapOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package io.github.droidkaigi.confsched.model

import androidx.compose.runtime.Composable
import io.github.droidkaigi.confsched.model.compositionlocal.LocalRepositories
import org.jetbrains.compose.resources.DrawableResource

interface ProfileCardRepository {
@Composable
fun profileCard(): ProfileCard
suspend fun save(profileCard: ProfileCard.Exists)
suspend fun loadQrCodeImageByteArray(link: String, centerLogoRes: DrawableResource): ByteArray
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ package io.github.droidkaigi.confsched.testing.data

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import io.github.droidkaigi.confsched.di.DispatcherModule
import io.github.droidkaigi.confsched.di.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
@TestInstallIn(components = [SingletonComponent::class], replaces = [DispatcherModule::class])
class TestDispatcherModule {

@Provides
@Singleton
fun provideTestDispatcher(): TestDispatcher = StandardTestDispatcher()

@Provides
@IoDispatcher
fun provideIoDispatcher(): CoroutineDispatcher = StandardTestDispatcher()
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import io.github.droidkaigi.confsched.profilecard.ProfileCardLinkTextFieldTestTa
import io.github.droidkaigi.confsched.profilecard.ProfileCardNicknameTextFieldTestTag
import io.github.droidkaigi.confsched.profilecard.ProfileCardOccupationTextFieldTestTag
import io.github.droidkaigi.confsched.profilecard.ProfileCardScreen
import io.github.droidkaigi.confsched.profilecard.ProfileCardShareButtonTestTag
import io.github.droidkaigi.confsched.profilecard.component.ProfileCardFlipCardBackTestTag
import io.github.droidkaigi.confsched.profilecard.component.ProfileCardFlipCardFrontTestTag
import io.github.droidkaigi.confsched.profilecard.component.ProfileCardFlipCardTestTag
Expand Down Expand Up @@ -141,12 +140,6 @@ class ProfileCardScreenRobot @Inject constructor(
.assertTextEquals(link)
}

fun checkShareProfileCardButtonEnabled() {
composeTestRule
.onNode(hasTestTag(ProfileCardShareButtonTestTag))
.assertIsEnabled()
}

fun checkCardScreenDisplayed() {
composeTestRule
.onNode(hasTestTag(ProfileCardCardScreenTestTag))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ class ProfileCardScreenTest(
}
itShould("show card screen") {
captureScreenWithChecks {
checkShareProfileCardButtonEnabled()
checkCardScreenDisplayed()
checkProfileCardFrontDisplayed()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
Expand Down Expand Up @@ -52,12 +51,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
Expand All @@ -69,8 +68,6 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.rememberGraphicsLayer
import androidx.compose.ui.graphics.vector.VectorPainter
import androidx.compose.ui.graphics.vector.rememberVectorPainter
Expand All @@ -85,6 +82,7 @@ import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import co.touchlab.kermit.Logger
import coil3.compose.rememberAsyncImagePainter
import com.preat.peekaboo.image.picker.toImageBitmap
import conference_app_2024.feature.profilecard.generated.resources.add_image
import conference_app_2024.feature.profilecard.generated.resources.card_type
Expand Down Expand Up @@ -112,10 +110,10 @@ import io.github.droidkaigi.confsched.droidkaigiui.component.AnimatedTextTopAppB
import io.github.droidkaigi.confsched.droidkaigiui.component.resetScroll
import io.github.droidkaigi.confsched.model.ProfileCard
import io.github.droidkaigi.confsched.model.ProfileCardType
import io.github.droidkaigi.confsched.profilecard.component.CapturableCard
import io.github.droidkaigi.confsched.profilecard.component.FlipCard
import io.github.droidkaigi.confsched.profilecard.component.InvertSystemBarAppearance
import io.github.droidkaigi.confsched.profilecard.component.PhotoPickerButton
import io.github.droidkaigi.confsched.profilecard.component.ShareableCard
import io.ktor.util.decodeBase64Bytes
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource
Expand Down Expand Up @@ -195,6 +193,7 @@ internal data class ProfileCardScreenState(
val cardError: ProfileCardError,
val uiType: ProfileCardUiType,
val userMessageStateHolder: UserMessageStateHolder,
val qrCodeImageByteArray: ByteArray? = null,
)

@Composable
Expand Down Expand Up @@ -333,6 +332,7 @@ internal fun ProfileCardScreen(
},
contentPadding = padding,
isCreated = true,
qrCodeImageByte = uiState.qrCodeImageByteArray,
)
}
}
Expand Down Expand Up @@ -469,10 +469,10 @@ internal fun EditScreen(
}

@OptIn(ExperimentalEncodingApi::class)
private fun ByteArray.toBase64(): String = Base64.encode(this)
internal fun ByteArray.toBase64(): String = Base64.encode(this)

@OptIn(ExperimentalEncodingApi::class)
private fun String.decodeBase64Bytes(): ByteArray = Base64.decode(this)
internal fun String.decodeBase64Bytes(): ByteArray = Base64.decode(this)

@Composable
internal fun Label(label: String) {
Expand Down Expand Up @@ -662,7 +662,7 @@ private fun CardTypeImage(
)
}

fun Modifier.selectedBorder(
private fun Modifier.selectedBorder(
isSelected: Boolean,
selectedBorderColor: Color,
vectorPainter: VectorPainter,
Expand Down Expand Up @@ -705,13 +705,16 @@ internal fun CardScreen(
onClickEdit: () -> Unit,
onClickShareProfileCard: (ImageBitmap) -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
qrCodeImageByte: ByteArray?,
modifier: Modifier = Modifier,
isCreated: Boolean = false,
contentPadding: PaddingValues = PaddingValues(16.dp),
) {
val coroutineScope = rememberCoroutineScope()
val graphicsLayer = rememberGraphicsLayer()
var isShareReady by remember { mutableStateOf(false) }
val profileCardImagePainter = rememberProfileImagePainter(uiState.image)
val qrCodeImagePainter = rememberAsyncImagePainter(qrCodeImageByte)

// The background of this screen is light, contrasting any other screen in the app.
// Invert the content color of system bars to accommodate this unique property.
Expand All @@ -720,10 +723,11 @@ internal fun CardScreen(
ProvideProfileCardTheme(uiState.cardType.toString()) {
Box {
// Not for display, for sharing
ShareableProfileCard(
ShareableCard(
uiState = uiState,
graphicsLayer = graphicsLayer,
contentPadding = contentPadding,
profileImagePainter = profileCardImagePainter,
qrCodeImagePainter = qrCodeImagePainter,
onReadyShare = {
Logger.d { "Ready to share" }
isShareReady = true
Expand All @@ -747,6 +751,8 @@ internal fun CardScreen(
) {
FlipCard(
uiState = uiState,
profileImagePainter = profileCardImagePainter,
qrCodeImagePainter = qrCodeImagePainter,
isCreated = isCreated,
)
Spacer(Modifier.height(32.dp))
Expand All @@ -758,7 +764,10 @@ internal fun CardScreen(
onClickShareProfileCard(graphicsLayer.toImageBitmap())
}
},
colors = ButtonDefaults.buttonColors(containerColor = Color.White),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
disabledContainerColor = Color.White,
),
border = if (uiState.cardType == ProfileCardType.None) {
BorderStroke(
0.5.dp,
Expand Down Expand Up @@ -804,58 +813,8 @@ internal fun CardScreen(
}

@Composable
private fun ShareableProfileCard(
uiState: ProfileCardUiState.Card,
graphicsLayer: GraphicsLayer,
contentPadding: PaddingValues,
onReadyShare: () -> Unit,
) {
var frontImage: ImageBitmap? by remember { mutableStateOf(null) }
var backImage: ImageBitmap? by remember { mutableStateOf(null) }
CapturableCard(
uiState = uiState,
onCaptured = { front, back ->
frontImage = front
backImage = back
onReadyShare()
},
)
Box(
modifier = Modifier.padding(contentPadding)
.drawWithContent {
graphicsLayer.record {
this@drawWithContent.drawContent()
}
drawLayer(graphicsLayer)
},
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.background(LocalProfileCardTheme.current.primaryColor)
.padding(vertical = 50.dp),
) {
backImage?.let {
Image(
bitmap = it,
contentDescription = null,
modifier = Modifier
.offset(x = 70.dp, y = 15.dp)
.rotate(10f)
.size(150.dp, 190.dp),
)
}
frontImage?.let {
Image(
bitmap = it,
contentDescription = null,
modifier = Modifier
.offset(x = (-70).dp, y = (-15).dp)
.rotate(-10f)
.size(150.dp, 190.dp),
)
}
}
}
}
private fun rememberProfileImagePainter(
imageBase64String: String,
) = rememberAsyncImagePainter(
model = rememberSaveable { imageBase64String.decodeBase64Bytes() },
)
Loading

0 comments on commit 9e1ac60

Please sign in to comment.