Skip to content

Commit

Permalink
Merge pull request #109 from thomaskioko/feature/decompose-ios
Browse files Browse the repository at this point in the history
[iOS] Migrate to Decompose
  • Loading branch information
thomaskioko committed Dec 9, 2023
2 parents c004429 + 4339f79 commit 965a44f
Show file tree
Hide file tree
Showing 140 changed files with 1,960 additions and 1,650 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dependencies {
implementation(libs.appauth)

implementation(libs.decompose.decompose)
implementation(libs.decompose.extensions.compose)
implementation(libs.kotlinInject.runtime)
ksp(libs.kotlinInject.compiler)
}
Expand Down
24 changes: 8 additions & 16 deletions app/src/main/kotlin/com/thomaskioko/tvmaniac/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.thomaskioko.tvmaniac.compose.theme.TvManiacTheme
import com.thomaskioko.tvmaniac.datastore.api.Theme
import com.thomaskioko.tvmaniac.datastore.api.AppTheme
import com.thomaskioko.tvmaniac.inject.MainActivityComponent
import com.thomaskioko.tvmaniac.inject.create
import com.thomaskioko.tvmaniac.navigation.Loading
import com.thomaskioko.tvmaniac.navigation.ThemeLoaded
import com.thomaskioko.tvmaniac.navigation.ThemeState

class MainActivity : ComponentActivity() {
Expand All @@ -36,14 +34,11 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()

setContent {
val themeState by component.presenter.state.collectAsState()
val themeState by component.presenter.state.subscribeAsState()
val darkTheme = shouldUseDarkTheme(themeState)

splashScreen.setKeepOnScreenCondition {
when (themeState) {
Loading -> true
is ThemeLoaded -> false
}
themeState.isFetching
}

DisposableEffect(darkTheme) {
Expand Down Expand Up @@ -74,13 +69,10 @@ class MainActivity : ComponentActivity() {
@Composable
private fun shouldUseDarkTheme(
uiState: ThemeState,
): Boolean = when (uiState) {
Loading -> isSystemInDarkTheme()
is ThemeLoaded -> when (uiState.theme) {
Theme.LIGHT -> false
Theme.DARK -> true
Theme.SYSTEM -> isSystemInDarkTheme()
}
): Boolean = when (uiState.appTheme) {
AppTheme.LIGHT_THEME -> false
AppTheme.DARK_THEME -> true
AppTheme.SYSTEM_THEME -> isSystemInDarkTheme()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ package com.thomaskioko.tvmaniac.datastore.api
import kotlinx.coroutines.flow.Flow

interface DatastoreRepository {
fun saveTheme(theme: Theme)
fun observeTheme(): Flow<Theme>
fun saveTheme(appTheme: AppTheme)
fun observeTheme(): Flow<AppTheme>

fun clearAuthState()
fun observeAuthState(): Flow<AuthState>
suspend fun saveAuthState(authState: AuthState)
suspend fun getAuthState(): AuthState?
}

enum class Theme {
LIGHT,
DARK,
SYSTEM,
enum class AppTheme(val value: String) {
LIGHT_THEME("Light Theme"),
DARK_THEME("Light Theme"),
SYSTEM_THEME("Light Theme"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.thomaskioko.tvmaniac.datastore.api.AppTheme
import com.thomaskioko.tvmaniac.datastore.api.AuthState
import com.thomaskioko.tvmaniac.datastore.api.DatastoreRepository
import com.thomaskioko.tvmaniac.datastore.api.Theme
import com.thomaskioko.tvmaniac.util.model.AppCoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
Expand All @@ -21,19 +21,19 @@ class DatastoreRepositoryImpl(
private val dataStore: DataStore<Preferences>,
) : DatastoreRepository {

override fun saveTheme(theme: Theme) {
override fun saveTheme(appTheme: AppTheme) {
coroutineScope.io.launch {
dataStore.edit { preferences ->
preferences[KEY_THEME] = theme.name
preferences[KEY_THEME] = appTheme.name
}
}
}

override fun observeTheme(): Flow<Theme> = dataStore.data.map { preferences ->
override fun observeTheme(): Flow<AppTheme> = dataStore.data.map { preferences ->
when (preferences[KEY_THEME]) {
Theme.LIGHT.name -> Theme.LIGHT
Theme.DARK.name -> Theme.DARK
else -> Theme.SYSTEM
AppTheme.LIGHT_THEME.name -> AppTheme.LIGHT_THEME
AppTheme.DARK_THEME.name -> AppTheme.DARK_THEME
else -> AppTheme.SYSTEM_THEME
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import app.cash.turbine.test
import com.thomaskioko.tvmaniac.datastore.api.Theme
import com.thomaskioko.tvmaniac.datastore.api.AppTheme
import com.thomaskioko.tvmaniac.datastore.implementation.DatastoreRepositoryImpl
import com.thomaskioko.tvmaniac.datastore.implementation.DatastoreRepositoryImpl.Companion.KEY_THEME
import com.thomaskioko.tvmaniac.datastore.implementation.IgnoreIos
Expand Down Expand Up @@ -56,17 +56,17 @@ class DatastoreRepositoryImplTest {
@Test
fun default_theme_is_emitted() = runTest {
repository.observeTheme().test {
awaitItem() shouldBe Theme.SYSTEM
awaitItem() shouldBe AppTheme.SYSTEM_THEME
}
}

@IgnoreIos
@Test
fun when_theme_is_changed_correct_value_is_set() = runTest {
repository.observeTheme().test {
repository.saveTheme(Theme.DARK)
awaitItem() shouldBe Theme.SYSTEM // Default theme
awaitItem() shouldBe Theme.DARK
repository.saveTheme(AppTheme.DARK_THEME)
awaitItem() shouldBe AppTheme.SYSTEM_THEME // Default theme
awaitItem() shouldBe AppTheme.DARK_THEME
}
}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
package com.thomaskioko.tvmaniac.datastore.testing

import com.thomaskioko.tvmaniac.datastore.api.AppTheme
import com.thomaskioko.tvmaniac.datastore.api.AuthState
import com.thomaskioko.tvmaniac.datastore.api.DatastoreRepository
import com.thomaskioko.tvmaniac.datastore.api.Theme
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow

class FakeDatastoreRepository : DatastoreRepository {

private val themeFlow: Channel<Theme> = Channel(Channel.UNLIMITED)
private val appThemeFlow: Channel<AppTheme> = Channel(Channel.UNLIMITED)
private val authStateFlow: Channel<AuthState> = Channel(Channel.UNLIMITED)

suspend fun setTheme(theme: Theme) {
themeFlow.send(theme)
suspend fun setTheme(appTheme: AppTheme) {
appThemeFlow.send(appTheme)
}

suspend fun setAuthState(authState: AuthState) {
authStateFlow.send(authState)
}

override fun saveTheme(theme: Theme) {
override fun saveTheme(appTheme: AppTheme) {
// no -op
}

override fun observeTheme(): Flow<Theme> = themeFlow.receiveAsFlow()
override fun observeTheme(): Flow<AppTheme> = appThemeFlow.receiveAsFlow()

override fun clearAuthState() {
// no -op
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ package com.thomaskioko.tvmaniac.traktauth.implementation
import android.app.Application
import android.net.Uri
import androidx.core.net.toUri
import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthManager
import com.thomaskioko.tvmaniac.util.model.Configs
import com.thomaskioko.tvmaniac.util.scope.ActivityScope
import com.thomaskioko.tvmaniac.util.scope.ApplicationScope
import me.tatarka.inject.annotations.Provides
import net.openid.appauth.AuthorizationRequest
Expand Down Expand Up @@ -51,10 +49,3 @@ interface TraktAuthComponent {
application: Application,
): AuthorizationService = AuthorizationService(application)
}

interface TraktAuthManagerComponent {

@ActivityScope
@Provides
fun provideTraktAuthManager(bind: TraktAuthManagerImpl): TraktAuthManager = bind
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import net.openid.appauth.AuthorizationService
import net.openid.appauth.ClientAuthentication

@Inject
class TraktAuthManagerImpl(
actual class TraktAuthManagerImpl(
private val activity: ComponentActivity,
private val traktActivityResultContract: TraktActivityResultContract,
private val traktAuthRepository: TraktAuthRepository,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.thomaskioko.tvmaniac.traktauth.implementation

import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthManager
import com.thomaskioko.tvmaniac.util.scope.ActivityScope
import me.tatarka.inject.annotations.Provides

interface TraktAuthManagerComponent {

@ActivityScope
@Provides
fun provideTraktAuthManager(
bind: TraktAuthManagerImpl,
): TraktAuthManager = bind
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.thomaskioko.tvmaniac.traktauth.implementation

import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthManager
import me.tatarka.inject.annotations.Inject

@Inject
expect class TraktAuthManagerImpl : TraktAuthManager
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.thomaskioko.tvmaniac.traktauth.implementation

import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthManager
import me.tatarka.inject.annotations.Inject

// TODO:: Replace with actual typealias. See https://youtrack.jetbrains.com/issue/KT-61573
@Inject
actual class TraktAuthManagerImpl : TraktAuthManager {

override fun launchWebView() {
// NO OP
}

override fun registerResult() {
// NO OP
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.thomaskioko.tvmaniac.traktauth.implementation

import com.thomaskioko.tvmaniac.traktauth.api.TraktAuthManager
import me.tatarka.inject.annotations.Inject

@Inject
actual class TraktAuthManagerImpl : TraktAuthManager {
override fun launchWebView() {
// NO OP
}

override fun registerResult() {
// NO OP
}
}
1 change: 1 addition & 0 deletions core/util/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ kotlin {
api(libs.ktor.serialization)

implementation(libs.coroutines.core)
implementation(libs.decompose.decompose)
implementation(libs.kermit)
implementation(libs.napier)
implementation(libs.kotlinInject.runtime)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.thomaskioko.tvmaniac.util.decompose

import com.arkivanov.decompose.value.MutableValue
import com.arkivanov.decompose.value.Value
import com.arkivanov.essenty.lifecycle.Lifecycle
import com.arkivanov.essenty.lifecycle.LifecycleOwner
import com.arkivanov.essenty.lifecycle.doOnDestroy
import com.arkivanov.essenty.lifecycle.subscribe
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext

/**
* This helper implementation in from Cofetti Kmp App
* See https://github.com/joreilly/Confetti/blob/fb832c2131b2f3e5276a1a3a30666aa571e1e17e/shared/src/commonMain/kotlin/dev/johnoreilly/confetti/decompose/DecomposeUtils.kt#L27
*/

fun LifecycleOwner.coroutineScope(
context: CoroutineContext = Dispatchers.Main.immediate,
): CoroutineScope {
val scope = CoroutineScope(context + SupervisorJob())
lifecycle.doOnDestroy(scope::cancel)

return scope
}

fun <T : Any> StateFlow<T>.asValue(
lifecycle: Lifecycle,
context: CoroutineContext = Dispatchers.Main.immediate,
): Value<T> =
asValue(
initialValue = value,
lifecycle = lifecycle,
context = context,
)

fun <T : Any> Flow<T>.asValue(
initialValue: T,
lifecycle: Lifecycle,
context: CoroutineContext = Dispatchers.Main.immediate,
): Value<T> {
val value = MutableValue(initialValue)
var scope: CoroutineScope? = null

lifecycle.subscribe(
onStart = {
scope = CoroutineScope(context).apply {
launch {
collect { value.value = it }
}
}
},
onStop = {
scope?.cancel()
scope = null
},
)

return value
}
1 change: 1 addition & 0 deletions feature/discover/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ dependencies {
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui.util)
implementation(libs.androidx.compose.runtime)
implementation(libs.decompose.extensions.compose)
implementation(libs.snapper)
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
Expand All @@ -53,6 +52,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.thomaskioko.tvmaniac.category.api.model.Category
import com.thomaskioko.tvmaniac.compose.components.BoxTextItems
import com.thomaskioko.tvmaniac.compose.components.ErrorUi
Expand Down Expand Up @@ -89,7 +89,7 @@ fun DiscoverScreen(
discoverShowsPresenter: DiscoverShowsPresenter,
modifier: Modifier = Modifier,
) {
val discoverState by discoverShowsPresenter.state.collectAsState()
val discoverState by discoverShowsPresenter.state.subscribeAsState()
val pagerState = rememberPagerState(pageCount = {
(discoverState as? DataLoaded)?.recommendedShows?.size ?: 0
})
Expand Down
1 change: 1 addition & 0 deletions feature/library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ dependencies {
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.runtime)
implementation(libs.decompose.extensions.compose)
implementation(libs.kotlinx.collections)
}
Loading

0 comments on commit 965a44f

Please sign in to comment.