Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] Migrate to Decompose #109

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cd68fdf
Add platform component and fix scoping issue when building iOS framew…
thomaskioko Dec 3, 2023
49b95f9
Add decompose util class from Confetti
thomaskioko Dec 3, 2023
b57c3fb
Add decompose extension dependencies.
thomaskioko Dec 3, 2023
31de0f4
Replace StateFlow with Value and use helper function.
thomaskioko Dec 3, 2023
600d117
Replace collectAsState with decompose subscribeAsState
thomaskioko Dec 3, 2023
f151395
Delete viewModel implementation.
thomaskioko Dec 4, 2023
fd37d06
Minor cleanup: update state and actions.
thomaskioko Dec 5, 2023
75ab4c9
Remove internal keyword. Make it accessible to iOS project.
thomaskioko Dec 5, 2023
51bb9b2
Move framework setup to shared gradle file
thomaskioko Dec 5, 2023
1c5f7c0
Add util dependency
thomaskioko Dec 5, 2023
3cc0bdc
Remove turbine block.
thomaskioko Dec 6, 2023
76299d9
Minor cleanup. Rename theme enum. Fixes conflicts on iOS.
thomaskioko Dec 6, 2023
41fa569
Setup Decompose core navigation on ios 🥳
thomaskioko Dec 6, 2023
8ba30ec
Minor cleanup: Fix wierd naming issue on iOS
thomaskioko Dec 7, 2023
010ad88
Move font our of UI directory
thomaskioko Dec 8, 2023
340a264
Update extensions directory.
thomaskioko Dec 8, 2023
4a6402c
Update ui components directory.
thomaskioko Dec 9, 2023
1af4954
Minor UI improvements and update structure.
thomaskioko Dec 9, 2023
bd928ae
Minor UI tweaks.
thomaskioko Dec 9, 2023
397c82b
Add Library Ui implementation.
thomaskioko Dec 9, 2023
e0bab38
Bump up dependency versions.
thomaskioko Dec 9, 2023
4339f79
Update project configuration.
thomaskioko Dec 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading