diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a2968c5..72385b5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,5 @@ import de.fayard.refreshVersions.core.versionFor - plugins { id("com.android.application") kotlin("android") @@ -68,6 +67,7 @@ android { excludes += "META-INF/LICENSE.md" excludes += "META-INF/LICENSE-notice.md" excludes += "META-INF/LGPL2.1" + excludes += "META-INF/com/android/build/gradle/aar-metadata.properties" excludes += "win32-x86/attach_hotspot_windows.dll" excludes += "win32-x86-64/attach_hotspot_windows.dll" } @@ -119,9 +119,9 @@ dependencies { implementation(Libs.lottie_compose) - implementation(Libs.orbit_mvi_core) - implementation(Libs.orbit_mvi_viewmodel) - testImplementation(Libs.orbit_mvi_test) + implementation(Libs.mavericks_compose) + testImplementation(Libs.mavericks_testing) + testImplementation(Libs.mavericks_mocking) kapt(Libs.room_compiler) testImplementation(Libs.room_testing) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 90c6843..8b4f311 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -47,6 +47,9 @@ + diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/app/AppNavigation.kt b/app/src/main/java/com/ericampire/android/androidstudycase/app/AppNavigation.kt index 7b1f435..0bcb952 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/app/AppNavigation.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/app/AppNavigation.kt @@ -1,22 +1,20 @@ package com.ericampire.android.androidstudycase.app import androidx.compose.material.ExperimentalMaterialApi -import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.ericampire.android.androidstudycase.presentation.screen.explore.ui.ExploreScreen import com.ericampire.android.androidstudycase.presentation.screen.home.ui.HomeScreen +import com.ericampire.android.androidstudycase.presentation.screen.login.ui.LoginScreen import com.ericampire.android.androidstudycase.presentation.screen.preview.ui.PreviewScreen import com.ericampire.android.androidstudycase.util.Destination import com.google.accompanist.pager.ExperimentalPagerApi +@ExperimentalMaterialApi fun NavGraphBuilder.addHomeScreen(navController: NavController) { composable(Destination.Home.route) { - HomeScreen( - navController = navController, - viewModel = hiltViewModel() - ) + HomeScreen(navController = navController) } } @@ -24,18 +22,20 @@ fun NavGraphBuilder.addHomeScreen(navController: NavController) { @ExperimentalPagerApi fun NavGraphBuilder.addExploreScreen(navController: NavController) { composable(Destination.Explore.route) { - ExploreScreen( - navController = navController, - viewModel = hiltViewModel() - ) + ExploreScreen(navController = navController) + } +} + +@ExperimentalMaterialApi +@ExperimentalPagerApi +fun NavGraphBuilder.addLoginScreen(navController: NavController) { + composable(Destination.Login.route) { + LoginScreen(navController = navController) } } fun NavGraphBuilder.addPreviewScreen(navController: NavController) { composable(Destination.Preview.route) { - PreviewScreen( - navController = navController, - viewModel = hiltViewModel() - ) + PreviewScreen(navController = navController) } } \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/app/di/CoroutineModule.kt b/app/src/main/java/com/ericampire/android/androidstudycase/app/di/CoroutineModule.kt index 83f3cd9..5cc6dda 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/app/di/CoroutineModule.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/app/di/CoroutineModule.kt @@ -9,13 +9,18 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob @Module @InstallIn(SingletonComponent::class) object CoroutineModule { + @Provides + fun provideCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob()) + @Provides @IoDispatcher fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/app/di/ViewModelModule.kt b/app/src/main/java/com/ericampire/android/androidstudycase/app/di/ViewModelModule.kt index 3bdceee..1400e94 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/app/di/ViewModelModule.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/app/di/ViewModelModule.kt @@ -1,47 +1,30 @@ package com.ericampire.android.androidstudycase.app.di -import com.ericampire.android.androidstudycase.domain.usecase.* +import com.ericampire.android.androidstudycase.app.hilt.AssistedViewModelFactory +import com.ericampire.android.androidstudycase.app.hilt.MavericksViewModelComponent +import com.ericampire.android.androidstudycase.app.hilt.ViewModelKey import com.ericampire.android.androidstudycase.presentation.screen.explore.business.ExploreViewModel import com.ericampire.android.androidstudycase.presentation.screen.home.business.HomeViewModel -import com.ericampire.android.androidstudycase.presentation.screen.preview.business.PreviewViewModel +import dagger.Binds import dagger.Module -import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent +import dagger.multibindings.IntoMap @Module -@InstallIn(ViewModelComponent::class) -object ViewModelModule { +@InstallIn(MavericksViewModelComponent::class) +interface ViewModelModule { - @Provides + @Binds + @IntoMap + @ViewModelKey(HomeViewModel::class) fun provideHomeViewModel( - findFeaturedAnimatorUseCase: FindFeaturedAnimatorUseCase, - findFeaturedBlogUseCase: FindFeaturedBlogUseCase - ): HomeViewModel { - return HomeViewModel( - findFeaturedAnimatorUseCase = findFeaturedAnimatorUseCase, - findFeaturedBlogUseCase = findFeaturedBlogUseCase - ) - } + factory: HomeViewModel.Factory + ): AssistedViewModelFactory<*, *> - @Provides + @Binds + @IntoMap + @ViewModelKey(ExploreViewModel::class) fun provideExploreViewModel( - findFeaturedAnimatorUseCase: FindFeaturedAnimatorUseCase, - findFeaturedBlogUseCase: FindFeaturedBlogUseCase - ): PreviewViewModel { - return PreviewViewModel() - } - - @Provides - fun providePreview( - findPopularLottieFileUseCase: FindPopularLottieFileUseCase, - findRecentLottieFileUseCase: FindRecentLottieFileUseCase, - findFeaturedLottieFileUseCase: FindFeaturedLottieFileUseCase - ): ExploreViewModel { - return ExploreViewModel( - findPopularLottieFileUseCase = findPopularLottieFileUseCase, - findRecentLottieFileUseCase = findRecentLottieFileUseCase, - findFeaturedLottieFileUseCase = findFeaturedLottieFileUseCase - ) - } + factory: ExploreViewModel.Factory + ): AssistedViewModelFactory<*, *> } \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/app/hilt/AssistedViewModelFactory.kt b/app/src/main/java/com/ericampire/android/androidstudycase/app/hilt/AssistedViewModelFactory.kt new file mode 100644 index 0000000..831d8e5 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/app/hilt/AssistedViewModelFactory.kt @@ -0,0 +1,9 @@ +package com.ericampire.android.androidstudycase.app.hilt + +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModel + + +interface AssistedViewModelFactory, S : MavericksState> { + fun create(state: S): VM +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/app/hilt/HiltMavericksViewModelFactory.kt b/app/src/main/java/com/ericampire/android/androidstudycase/app/hilt/HiltMavericksViewModelFactory.kt new file mode 100644 index 0000000..f683dcd --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/app/hilt/HiltMavericksViewModelFactory.kt @@ -0,0 +1,59 @@ +package com.ericampire.android.androidstudycase.app.hilt + + +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import dagger.hilt.DefineComponent +import dagger.hilt.EntryPoint +import dagger.hilt.EntryPoints +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +inline fun , S : MavericksState> hiltMavericksViewModelFactory() = + HiltMavericksViewModelFactory(VM::class.java) + +class HiltMavericksViewModelFactory, S : MavericksState>( + private val viewModelClass: Class> +) : MavericksViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: S): VM { + // We want to create the ViewModelComponent. In order to do that, we need to get its parent: ActivityComponent. + val componentBuilder = + EntryPoints.get(viewModelContext.app(), CreateMavericksViewModelComponent::class.java) + .mavericksViewModelComponentBuilder() + val viewModelComponent = componentBuilder.build() + val viewModelFactoryMap = EntryPoints.get( + viewModelComponent, + HiltMavericksEntryPoint::class.java + ).viewModelFactories + val viewModelFactory = viewModelFactoryMap[viewModelClass] + + @Suppress("UNCHECKED_CAST") + val castedViewModelFactory = viewModelFactory as? AssistedViewModelFactory + return castedViewModelFactory?.create(state) as VM + } +} + + +@MavericksViewModelScoped +@DefineComponent(parent = SingletonComponent::class) +interface MavericksViewModelComponent + +@DefineComponent.Builder +interface MavericksViewModelComponentBuilder { + fun build(): MavericksViewModelComponent +} + +@EntryPoint +@InstallIn(SingletonComponent::class) +interface CreateMavericksViewModelComponent { + fun mavericksViewModelComponentBuilder(): MavericksViewModelComponentBuilder +} + +@EntryPoint +@InstallIn(MavericksViewModelComponent::class) +interface HiltMavericksEntryPoint { + val viewModelFactories: Map>, AssistedViewModelFactory<*, *>> +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/app/hilt/MavericksViewModelScoped.kt b/app/src/main/java/com/ericampire/android/androidstudycase/app/hilt/MavericksViewModelScoped.kt new file mode 100644 index 0000000..991e902 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/app/hilt/MavericksViewModelScoped.kt @@ -0,0 +1,6 @@ +package com.ericampire.android.androidstudycase.app.hilt + +import javax.inject.Scope + +@Scope +annotation class MavericksViewModelScoped \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/app/hilt/ViewModelKey.kt b/app/src/main/java/com/ericampire/android/androidstudycase/app/hilt/ViewModelKey.kt new file mode 100644 index 0000000..ac17f4f --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/app/hilt/ViewModelKey.kt @@ -0,0 +1,13 @@ +package com.ericampire.android.androidstudycase.app.hilt + +import com.airbnb.mvrx.MavericksViewModel +import dagger.MapKey +import kotlin.reflect.KClass + +/** + * A [MapKey] for populating a map of ViewModels and their factories. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +@MapKey +annotation class ViewModelKey(val value: KClass>) \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/app/hilt/ViewModelScopedClass.kt b/app/src/main/java/com/ericampire/android/androidstudycase/app/hilt/ViewModelScopedClass.kt new file mode 100644 index 0000000..7582f8d --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/app/hilt/ViewModelScopedClass.kt @@ -0,0 +1,13 @@ +package com.ericampire.android.androidstudycase.app.hilt + +import java.util.concurrent.atomic.AtomicInteger +import javax.inject.Inject + +@MavericksViewModelScoped +class ViewModelScopedClass @Inject constructor() { + val id = instanceId.incrementAndGet() + + companion object { + private val instanceId = AtomicInteger(0) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/app/initializer/MavericksInitializer.kt b/app/src/main/java/com/ericampire/android/androidstudycase/app/initializer/MavericksInitializer.kt new file mode 100644 index 0000000..5f8fb4d --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/app/initializer/MavericksInitializer.kt @@ -0,0 +1,15 @@ +package com.ericampire.android.androidstudycase.app.initializer + +import android.content.Context +import androidx.startup.Initializer +import com.airbnb.mvrx.Mavericks + +class MavericksInitializer : Initializer { + override fun create(context: Context) { + Mavericks.initialize(context) + } + + override fun dependencies(): MutableList>> { + return mutableListOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/app/room/AppDatabase.kt b/app/src/main/java/com/ericampire/android/androidstudycase/app/room/AppDatabase.kt index 429d1db..1937ed5 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/app/room/AppDatabase.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/app/room/AppDatabase.kt @@ -13,7 +13,7 @@ import com.ericampire.android.androidstudycase.domain.entity.User @Database( entities = [Blog::class, Animator::class, Lottiefile::class, User::class], - version = 2, + version = 1, exportSchema = false ) abstract class AppDatabase : RoomDatabase() { diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/custom/LoadingAnimation.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/custom/LoadingAnimation.kt new file mode 100644 index 0000000..0c36a6f --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/custom/LoadingAnimation.kt @@ -0,0 +1,199 @@ +package com.ericampire.android.androidstudycase.presentation.custom + +import androidx.compose.animation.core.* +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ericampire.android.androidstudycase.presentation.theme.AndroidStudyCaseTheme +import com.ericampire.android.androidstudycase.presentation.theme.AppColor + + +@Composable +fun LoadingAnimation( + modifier: Modifier = Modifier, + waveColor: Color = AppColor.WhiteTransparent, + arcColor: Color = Color.White +) { + var currentRotation by remember { mutableStateOf(0f) } + val rotation = remember { Animatable(currentRotation) } + + LaunchedEffect(true) { + rotation.animateTo( + targetValue = currentRotation + 360f, + animationSpec = infiniteRepeatable( + animation = tween(2000, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ), + block = { + currentRotation = value + } + ) + } + + Surface( + modifier = modifier.size(200.dp), + color = Color.Transparent, + content = { + Box( + contentAlignment = Alignment.Center, + content = { + Canvas(modifier = Modifier.size(100.dp)) { + drawCircle( + style = Stroke(width = 10f), + color = waveColor + ) + + drawArc( + color = arcColor, + startAngle = rotation.value, + sweepAngle = 30f, + useCenter = false, + style = Stroke(width = 10f), + ) + } + + Surface( + modifier = Modifier.size(90.dp), + shape = CircleShape, + color = Color.Transparent, + content = { + WaveView( + waveColor = waveColor, + timeSpec = 5000, + init = true, + initValue = 1f, + targetValue = 0f + ) + WaveView( + waveColor = waveColor, + timeSpec = 5000, + init = true, + initValue = 0f, + targetValue = 1f, + waveWidth = 200 + ) + WaveView( + waveColor = waveColor, + timeSpec = 2000, + init = true, + initValue = 0f, + targetValue = 1f, + waveWidth = 350, + dxTimeSpec = 2000 + ) + } + ) + } + ) + } + ) +} + +@Composable +fun WaveView( + modifier: Modifier = Modifier, + timeSpec: Long, + initValue: Float, + targetValue: Float, + init: Boolean, + waveWidth: Int = 250, + dxTimeSpec: Int = 4000, + waveColor: Color, +) { + + val deltaXAnim = rememberInfiniteTransition() + val dx by deltaXAnim.animateFloat( + initialValue = initValue, + targetValue = targetValue, + animationSpec = infiniteRepeatable( + animation = tween(dxTimeSpec, easing = LinearEasing) + ) + ) + + val dy by deltaXAnim.animateFloat( + initialValue = 100f, + targetValue = 0f, + animationSpec = infiniteRepeatable( + animation = tween(4000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ) + ) + + val screenWidthPx = with(LocalDensity.current) { + (LocalConfiguration.current.screenHeightDp * density) - 80.dp.toPx() + } + val animTranslate by animateFloatAsState( + targetValue = if (init) 0f else screenWidthPx, + animationSpec = TweenSpec(if (init) 0 else timeSpec.toInt(), easing = LinearEasing) + ) + + val waveHeight by animateFloatAsState( + targetValue = if (init) 80f else 0f, + animationSpec = TweenSpec(if (init) 0 else timeSpec.toInt(), easing = LinearEasing) + ) + + val path = Path() + + Canvas( + modifier = modifier.fillMaxSize(), + onDraw = { + translate(top = animTranslate) { + drawPath(path = path, color = waveColor) + path.reset() + val halfWaveWidth = waveWidth / 2 + path.moveTo(-waveWidth + (waveWidth * dx), dy.dp.toPx()) + + for (i in -waveWidth..(size.width.toInt() + waveWidth) step waveWidth) { + path.relativeQuadraticBezierTo( + halfWaveWidth.toFloat() / 2, + -waveHeight, + halfWaveWidth.toFloat(), + 0f + ) + path.relativeQuadraticBezierTo( + halfWaveWidth.toFloat() / 2, + waveHeight, + halfWaveWidth.toFloat(), + 0f + ) + } + + path.lineTo(size.width, size.height) + path.lineTo(0f, size.height) + path.close() + } + } + ) +} + +@Preview +@Composable +fun LoadingAnimationPreview() { + AndroidStudyCaseTheme() { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + content = { + LoadingAnimation( + waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f), + arcColor = MaterialTheme.colors.primaryVariant + ) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreEffect.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreEffect.kt deleted file mode 100644 index 36a85fb..0000000 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreEffect.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.ericampire.android.androidstudycase.presentation.screen.explore.business - -sealed interface ExploreEffect { - data class ShowErrorMessage(val message: String) : ExploreEffect -} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreViewModel.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreViewModel.kt index 7f97673..3f12713 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreViewModel.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreViewModel.kt @@ -1,33 +1,29 @@ package com.ericampire.android.androidstudycase.presentation.screen.explore.business -import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.MavericksViewModelFactory +import com.ericampire.android.androidstudycase.app.hilt.AssistedViewModelFactory +import com.ericampire.android.androidstudycase.app.hilt.hiltMavericksViewModelFactory import com.ericampire.android.androidstudycase.domain.entity.Lottiefile import com.ericampire.android.androidstudycase.domain.usecase.FindFeaturedLottieFileUseCase import com.ericampire.android.androidstudycase.domain.usecase.FindPopularLottieFileUseCase import com.ericampire.android.androidstudycase.domain.usecase.FindRecentLottieFileUseCase import com.ericampire.android.androidstudycase.util.Result +import com.ericampire.android.androidstudycase.util.data import com.ericampire.android.androidstudycase.util.mvi.BaseViewModel -import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import org.orbitmvi.orbit.Container -import org.orbitmvi.orbit.syntax.simple.intent -import org.orbitmvi.orbit.syntax.simple.postSideEffect -import org.orbitmvi.orbit.syntax.simple.reduce -import org.orbitmvi.orbit.viewmodel.container -import javax.inject.Inject -@HiltViewModel -class ExploreViewModel @Inject constructor( +class ExploreViewModel @AssistedInject constructor( + @Assisted initialState: ExploreViewState, private val findPopularLottieFileUseCase: FindPopularLottieFileUseCase, private val findRecentLottieFileUseCase: FindRecentLottieFileUseCase, private val findFeaturedLottieFileUseCase: FindFeaturedLottieFileUseCase -) : BaseViewModel() { - - override val container: Container - get() = container(initialState = ExploreViewState()) +) : BaseViewModel(initialState) { init { viewModelScope.launch { @@ -41,21 +37,21 @@ class ExploreViewModel @Inject constructor( } } - private fun Flow>>.fetchData() = intent(registerIdling = false) { - collect { result -> - when (result) { - is Result.Error -> { - val errorMessage = result.exception.localizedMessage ?: "Unknown Error" - reduce { state.copy(isLoading = false) } - postSideEffect(ExploreEffect.ShowErrorMessage(errorMessage)) - } - Result.Loading -> reduce { - state.copy(isLoading = true) - } - is Result.Success -> reduce { - state.copy(files = result.data, isLoading = false) - } + private fun Flow>>.fetchData() { + viewModelScope.launch { + map { + it.data ?: emptyList() + }.execute { + copy(files = it) } } } + + @AssistedFactory + interface Factory : AssistedViewModelFactory { + override fun create(state: ExploreViewState): ExploreViewModel + } + + companion object : MavericksViewModelFactory + by hiltMavericksViewModelFactory() } \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreViewState.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreViewState.kt index af0a64e..ec34fe9 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreViewState.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreViewState.kt @@ -1,8 +1,10 @@ package com.ericampire.android.androidstudycase.presentation.screen.explore.business +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.Uninitialized import com.ericampire.android.androidstudycase.domain.entity.Lottiefile data class ExploreViewState( - val files: List = emptyList(), - val isLoading: Boolean = false -) + val files: Async> = Uninitialized, +) : MavericksState diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/ui/ExploreScreen.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/ui/ExploreScreen.kt index 357c471..1263caa 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/ui/ExploreScreen.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/ui/ExploreScreen.kt @@ -1,6 +1,6 @@ package com.ericampire.android.androidstudycase.presentation.screen.explore.ui -import android.widget.Toast +import androidx.compose.animation.Crossfade import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -18,45 +18,43 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.compose.collectAsState +import com.airbnb.mvrx.compose.mavericksViewModel import com.ericampire.android.androidstudycase.R import com.ericampire.android.androidstudycase.domain.entity.Lottiefile -import com.ericampire.android.androidstudycase.presentation.custom.LoadingView +import com.ericampire.android.androidstudycase.presentation.custom.LoadingAnimation import com.ericampire.android.androidstudycase.presentation.custom.TopActionBar import com.ericampire.android.androidstudycase.presentation.screen.explore.business.ExploreAction -import com.ericampire.android.androidstudycase.presentation.screen.explore.business.ExploreEffect import com.ericampire.android.androidstudycase.presentation.screen.explore.business.ExploreViewModel +import com.ericampire.android.androidstudycase.presentation.screen.explore.business.ExploreViewState import com.ericampire.android.androidstudycase.presentation.theme.AppColor import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.rememberPagerState import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +import timber.log.Timber @ExperimentalMaterialApi @ExperimentalPagerApi @Composable fun ExploreScreen( navController: NavController, - viewModel: ExploreViewModel + viewModel: ExploreViewModel = mavericksViewModel() ) { val coroutineScope = rememberCoroutineScope() - val state by viewModel.container.stateFlow.collectAsState() + val state by viewModel.collectAsState(ExploreViewState::files) val context = LocalContext.current val tabItems = stringArrayResource(id = R.array.explore_item) val pagerState = rememberPagerState(pageCount = tabItems.size) - LaunchedEffect(viewModel) { - viewModel.container.sideEffectFlow.collect { - when (it) { - is ExploreEffect.ShowErrorMessage -> { - Toast.makeText(context, it.message, Toast.LENGTH_LONG).show() - } - } - } - } - LaunchedEffect(viewModel) { viewModel.submitAction(ExploreAction.FindRecentFile) } @@ -122,38 +120,70 @@ fun ExploreScreen( ) }, content = { contentPadding -> - Box( - modifier = Modifier - .padding(contentPadding) - .fillMaxSize(), - contentAlignment = Alignment.Center, - content = { - if (state.isLoading) { - LoadingView() + Crossfade(modifier = Modifier.padding(contentPadding), targetState = state) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + content = { + when (it) { + Uninitialized -> { + LoadingAnimation( + waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f), + arcColor = MaterialTheme.colors.primaryVariant + ) + } + is Loading -> { + LoadingAnimation( + waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f), + arcColor = MaterialTheme.colors.primaryVariant + ) + } + is Success -> { + val animations = it.invoke() + HorizontalPager(state = pagerState) { + ExploreContent(files = animations) + } + } + is Fail -> { + Timber.e(it.error.localizedMessage) + } + } } - if (state.files.isNotEmpty()) { - ExploreContent(files = state.files) - } - } - ) + ) + } } ) } @ExperimentalMaterialApi @Composable -fun ExploreContent( +private fun ExploreContent( modifier: Modifier = Modifier, files: List ) { LazyColumn( modifier = modifier.fillMaxSize(), content = { + + item { + Divider( + modifier = Modifier + .background(MaterialTheme.colors.surface) + .height(18.dp) + ) + } + items(items = files, key = { it.toString() }) { lottieFile -> LottieFileItemView( lottiefile = lottieFile, onClick = {} ) + + Divider( + modifier = Modifier + .background(MaterialTheme.colors.surface) + .height(18.dp) + ) } } ) diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/ui/LottieFileItem.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/ui/LottieFileItem.kt index 0d5a6d4..3dae165 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/ui/LottieFileItem.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/ui/LottieFileItem.kt @@ -6,14 +6,16 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CornerSize import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.* +import androidx.compose.material.icons.rounded.ChatBubble +import androidx.compose.material.icons.rounded.CreateNewFolder +import androidx.compose.material.icons.rounded.Favorite +import androidx.compose.material.icons.rounded.Share import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -72,12 +74,10 @@ fun LottieFileItemView( Text( text = lottiefile.name, style = MaterialTheme.typography.h6, - textAlign = TextAlign.Center, ) Text( text = lottiefile.createdBy?.name ?: "", style = MaterialTheme.typography.caption, - textAlign = TextAlign.Center, ) } ) diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeAction.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeAction.kt index d285d5d..0da54af 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeAction.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeAction.kt @@ -1,3 +1,5 @@ package com.ericampire.android.androidstudycase.presentation.screen.home.business -sealed interface HomeAction \ No newline at end of file +sealed interface HomeAction { + object FetchData : HomeAction +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeEffect.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeEffect.kt deleted file mode 100644 index 5eafec2..0000000 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeEffect.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.ericampire.android.androidstudycase.presentation.screen.home.business - -sealed interface HomeEffect \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeViewModel.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeViewModel.kt index 8e61ae6..8e7b8bf 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeViewModel.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeViewModel.kt @@ -1,30 +1,68 @@ package com.ericampire.android.androidstudycase.presentation.screen.home.business -import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.MavericksViewModelFactory +import com.ericampire.android.androidstudycase.app.hilt.AssistedViewModelFactory +import com.ericampire.android.androidstudycase.app.hilt.hiltMavericksViewModelFactory import com.ericampire.android.androidstudycase.domain.usecase.FindFeaturedAnimatorUseCase import com.ericampire.android.androidstudycase.domain.usecase.FindFeaturedBlogUseCase +import com.ericampire.android.androidstudycase.domain.usecase.FindFeaturedLottieFileUseCase +import com.ericampire.android.androidstudycase.domain.usecase.FindUsersUseCase +import com.ericampire.android.androidstudycase.util.data import com.ericampire.android.androidstudycase.util.mvi.BaseViewModel -import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch -import org.orbitmvi.orbit.Container -import org.orbitmvi.orbit.viewmodel.container -import javax.inject.Inject -@HiltViewModel -class HomeViewModel @Inject constructor( +class HomeViewModel @AssistedInject constructor( + @Assisted initialState: HomeViewState, private val findFeaturedBlogUseCase: FindFeaturedBlogUseCase, - private val findFeaturedAnimatorUseCase: FindFeaturedAnimatorUseCase -) : BaseViewModel() { + private val findFeaturedAnimatorUseCase: FindFeaturedAnimatorUseCase, + private val findUsersUseCase: FindUsersUseCase, + private val findFeaturedLottieFileUseCase: FindFeaturedLottieFileUseCase +) : BaseViewModel(initialState) { - override val container: Container - get() = container(initialState = HomeViewState()) init { viewModelScope.launch { pendingAction.collectLatest { action -> - + when (action) { + HomeAction.FetchData -> fetchData() + } } } } + + private fun fetchData() { + val userFlow = findUsersUseCase(Unit) + val storiesFlow = findFeaturedBlogUseCase(Unit) + val animatorsFlow = findFeaturedAnimatorUseCase(Unit) + val animationsFlow = findFeaturedLottieFileUseCase(Unit) + + combine( + userFlow, + storiesFlow, + animationsFlow, + animatorsFlow + ) { user, stories, anim, animators -> + HomeContentData( + user = user.data?.firstOrNull(), + blog = stories.data ?: emptyList(), + featuredAnimators = animators.data ?: emptyList(), + featuredLottieFile = anim.data ?: emptyList(), + ) + }.execute { + copy(contentData = it) + } + } + + @AssistedFactory + interface Factory : AssistedViewModelFactory { + override fun create(state: HomeViewState): HomeViewModel + } + + companion object : MavericksViewModelFactory + by hiltMavericksViewModelFactory() } \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeViewState.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeViewState.kt index 3d7e4d0..0422c7c 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeViewState.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeViewState.kt @@ -1,7 +1,20 @@ package com.ericampire.android.androidstudycase.presentation.screen.home.business +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.Uninitialized +import com.ericampire.android.androidstudycase.domain.entity.Animator import com.ericampire.android.androidstudycase.domain.entity.Blog +import com.ericampire.android.androidstudycase.domain.entity.Lottiefile +import com.ericampire.android.androidstudycase.domain.entity.User data class HomeViewState( - val blog: List = emptyList() -) + val contentData: Async = Uninitialized +) : MavericksState + +data class HomeContentData( + val blog: List = emptyList(), + val featuredAnimators: List = emptyList(), + val featuredLottieFile: List = emptyList(), + val user: User? = null, +) : MavericksState diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/ui/FeaturedLottieFileView.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/ui/FeaturedLottieFileView.kt index fec6709..72c53eb 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/ui/FeaturedLottieFileView.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/ui/FeaturedLottieFileView.kt @@ -45,7 +45,7 @@ fun FeaturedLottieFileView( Box( modifier = modifier .background(Color.White) - .height(200.dp) + .height(170.dp) .width(170.dp), content = { LottieAnimation( @@ -71,7 +71,9 @@ fun FeaturedLottieFileView( Text( text = lottiefile.createdBy?.name ?: "", maxLines = 1, - style = MaterialTheme.typography.caption, + style = MaterialTheme.typography.caption.copy( + color = Color.Gray + ), ) } ) diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/ui/HomeScreen.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/ui/HomeScreen.kt index b028dd8..59e7f9d 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/ui/HomeScreen.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/ui/HomeScreen.kt @@ -1,13 +1,206 @@ package com.ericampire.android.androidstudycase.presentation.screen.home.ui +import androidx.compose.animation.Crossfade +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.compose.collectAsState +import com.airbnb.mvrx.compose.mavericksViewModel +import com.ericampire.android.androidstudycase.R +import com.ericampire.android.androidstudycase.presentation.custom.CustomImageView +import com.ericampire.android.androidstudycase.presentation.custom.LoadingAnimation +import com.ericampire.android.androidstudycase.presentation.custom.TopActionBar +import com.ericampire.android.androidstudycase.presentation.screen.home.business.HomeAction +import com.ericampire.android.androidstudycase.presentation.screen.home.business.HomeContentData import com.ericampire.android.androidstudycase.presentation.screen.home.business.HomeViewModel +import com.ericampire.android.androidstudycase.presentation.screen.home.business.HomeViewState +import com.ericampire.android.androidstudycase.presentation.theme.AppColor +import com.ericampire.android.androidstudycase.util.Destination +import timber.log.Timber +@ExperimentalMaterialApi @Composable fun HomeScreen( navController: NavController, - viewModel: HomeViewModel + viewModel: HomeViewModel = mavericksViewModel() ) { -} \ No newline at end of file + val state by viewModel.collectAsState(HomeViewState::contentData) + + LaunchedEffect(viewModel) { + viewModel.submitAction(HomeAction.FetchData) + } + + Scaffold( + topBar = { + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = AppColor.Black001), + content = { + TopActionBar() + } + ) + }, + content = { contentPadding -> + Crossfade(modifier = Modifier.padding(contentPadding), targetState = state) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + content = { + when (it) { + Uninitialized -> { + LoadingAnimation( + waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f), + arcColor = MaterialTheme.colors.primaryVariant + ) + } + is Loading -> { + LoadingAnimation( + waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f), + arcColor = MaterialTheme.colors.primaryVariant + ) + } + is Success -> { + HomeContent( + state = it.invoke(), + onLoginClick = { + navController.navigate(Destination.Login.route) + } + ) + } + is Fail -> { + Timber.e(it.error.localizedMessage) + } + } + } + ) + } + } + ) +} + +@ExperimentalMaterialApi +@Composable +fun HomeContent( + modifier: Modifier = Modifier, + state: HomeContentData, + onLoginClick: () -> Unit +) { + LazyColumn( + state = rememberLazyListState(), + modifier = modifier + .fillMaxSize() + .animateContentSize(), + verticalArrangement = Arrangement.spacedBy(16.dp), + content = { + item { + if (state.user == null) { + UnLoggedUserHeaderView(onLoginClick = onLoginClick) + } else { + LoggedUserHeaderView(user = state.user) + } + } + + item { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + maxLines = 1, + text = stringResource(id = R.string.txt_featured_animation), + style = MaterialTheme.typography.h4.copy( + color = MaterialTheme.colors.onSurface + ), + ) + } + + item { + LazyRow( + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + content = { + items(items = state.featuredLottieFile, key = { it.toString() }) { animation -> + FeaturedLottieFileView( + lottiefile = animation, + onClick = { + // Todo: + } + ) + } + } + ) + } + item { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + maxLines = 1, + text = stringResource(id = R.string.txt_featured_animator), + style = MaterialTheme.typography.h4.copy( + color = MaterialTheme.colors.onSurface + ), + ) + } + + item { + LazyRow( + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + content = { + items(items = state.featuredAnimators) { animator -> + CustomImageView( + modifier = Modifier + .size(70.dp) + .clip(CircleShape), + data = animator.avatarUrl, + ) + } + } + ) + } + + item { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + maxLines = 1, + text = stringResource(id = R.string.txt_latest_story), + style = MaterialTheme.typography.h4.copy( + color = MaterialTheme.colors.onSurface + ), + ) + } + + items(items = state.blog) { blog -> + BlogItemView( + modifier = Modifier.padding(horizontal = 16.dp), + blog = blog, + onClick = { } + ) + } + + item { + BrowseAllItemView() + } + } + ) +} diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/login/business/LoginViewModel.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/login/business/LoginViewModel.kt new file mode 100644 index 0000000..6abead9 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/login/business/LoginViewModel.kt @@ -0,0 +1,10 @@ +package com.ericampire.android.androidstudycase.presentation.screen.login.business + +import androidx.lifecycle.ViewModel +import com.ericampire.android.androidstudycase.domain.usecase.SaveUserUseCase +import javax.inject.Inject + +class LoginViewModel @Inject constructor( + private val saveUserUseCase: SaveUserUseCase +) : ViewModel() { +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/login/ui/LoginScreen.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/login/ui/LoginScreen.kt new file mode 100644 index 0000000..8c863ee --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/login/ui/LoginScreen.kt @@ -0,0 +1,11 @@ +package com.ericampire.android.androidstudycase.presentation.screen.login.ui + +import androidx.compose.runtime.Composable +import androidx.navigation.NavController + +@Composable +fun LoginScreen( + navController: NavController, +) { + +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/main/ui/MainScreen.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/main/ui/MainScreen.kt index 86937f8..30d0794 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/main/ui/MainScreen.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/main/ui/MainScreen.kt @@ -19,6 +19,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.ericampire.android.androidstudycase.app.addExploreScreen import com.ericampire.android.androidstudycase.app.addHomeScreen +import com.ericampire.android.androidstudycase.app.addLoginScreen import com.ericampire.android.androidstudycase.app.addPreviewScreen import com.ericampire.android.androidstudycase.util.Destination import com.google.accompanist.insets.navigationBarsPadding @@ -84,6 +85,7 @@ fun MainScreen() { addHomeScreen(navController = navController) addPreviewScreen(navController = navController) addExploreScreen(navController = navController) + addLoginScreen(navController = navController) } ) }, diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewEffect.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewEffect.kt deleted file mode 100644 index 77f62df..0000000 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewEffect.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.ericampire.android.androidstudycase.presentation.screen.preview.business - -sealed interface PreviewEffect \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewViewModel.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewViewModel.kt index 99a60d1..c388032 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewViewModel.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewViewModel.kt @@ -1,27 +1,12 @@ package com.ericampire.android.androidstudycase.presentation.screen.preview.business -import androidx.lifecycle.viewModelScope -import com.ericampire.android.androidstudycase.util.mvi.BaseViewModel +import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch -import org.orbitmvi.orbit.Container -import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel class PreviewViewModel @Inject constructor( -) : BaseViewModel() { +) : ViewModel() { - override val container: Container - get() = container(initialState = PreviewViewState()) - - init { - viewModelScope.launch { - pendingAction.collectLatest { action -> - - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/ui/PreviewScreen.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/ui/PreviewScreen.kt index a48cea6..784740b 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/ui/PreviewScreen.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/ui/PreviewScreen.kt @@ -11,14 +11,10 @@ import com.budiyev.android.codescanner.CodeScanner import com.budiyev.android.codescanner.CodeScannerView import com.ericampire.android.androidstudycase.R import com.ericampire.android.androidstudycase.presentation.custom.LottiePreviewDialog -import com.ericampire.android.androidstudycase.presentation.screen.preview.business.PreviewViewModel @Composable -fun PreviewScreen( - navController: NavController, - viewModel: PreviewViewModel -) { +fun PreviewScreen(navController: NavController) { var codeScanner: CodeScanner? = null var lottieFileUrl by remember { mutableStateOf("") } diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Color.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Color.kt index a358ff4..2b16236 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Color.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Color.kt @@ -14,4 +14,5 @@ object AppColor { val Black001 = Color(0xFF222222) val BlackOverlay = Color(0x4D000000) val BlackOverlay001 = Color(0x1A000000) + val WhiteTransparent = Color(0x80FFFFFF) } \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/util/Destination.kt b/app/src/main/java/com/ericampire/android/androidstudycase/util/Destination.kt index a953ca4..32c95e0 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/util/Destination.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/util/Destination.kt @@ -7,4 +7,5 @@ sealed class Destination(val route: String, @StringRes val resourceId: Int) { object Home : Destination("home", R.string.txt_home) object Explore : Destination("explore", R.string.txt_explore) object Preview : Destination("preview", R.string.txt_preview) + object Login : Destination("login", R.string.txt_login) } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5f8fdc..1a664b8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,2 +1,3 @@ + Featured Animations \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Apps.kt b/buildSrc/src/main/kotlin/Apps.kt index 4672ac3..9e4b1d3 100644 --- a/buildSrc/src/main/kotlin/Apps.kt +++ b/buildSrc/src/main/kotlin/Apps.kt @@ -1,5 +1,5 @@ object Apps { - const val compileSdk = 30 + const val compileSdk = 31 const val buildToolsVersion = "30.0.3" const val applicationId = "com.ericampire.android.androidstudycase" diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index b917ee0..3c1315f 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -18,7 +18,7 @@ object Libs { const val activity_compose = "androidx.activity:activity-compose:_" const val startup_runtime = "androidx.startup:startup-runtime:_" - const val navigation_compose = "androidx.navigation:navigation-compose:2.4.0-alpha06" + const val navigation_compose = "androidx.navigation:navigation-compose:2.4.0-alpha08" const val appcompat = "androidx.appcompat:appcompat:_" const val preference_ktx = "androidx.preference:preference-ktx:_" diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 423e189..8e74804 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -36,8 +36,6 @@ dependencies { api(platform(Libs.kotlin_coroutine_bom)) api(Libs.kotlin_coroutine_core) - api(Libs.ktor_client_android) - testImplementation(Libs.junit_jupiter_api) testImplementation(Libs.junit_jupiter_engine) diff --git a/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/animator/RemoteAnimatorDataSource.kt b/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/animator/RemoteAnimatorDataSource.kt index 25cddfa..a58636b 100644 --- a/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/animator/RemoteAnimatorDataSource.kt +++ b/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/animator/RemoteAnimatorDataSource.kt @@ -17,8 +17,11 @@ class RemoteAnimatorDataSource @Inject constructor( override fun findAll(): Flow>> { return flow { try { - val data = httpClient.get(ApiUrl.Animator.featured) - emit(Result.Success(data.animatorAnimatorData.featuredAnimators.results)) + val result = httpClient.get(ApiUrl.Animator.featured) + val identifiableData = result.data.animators.results.mapIndexed { index, animator -> + animator.copy(id = index.toLong()) + } + emit(Result.Success(identifiableData)) } catch (e: Exception) { emit(Result.Error(e)) } diff --git a/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/blog/RemoteBlogDataSource.kt b/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/blog/RemoteBlogDataSource.kt index 5a5557c..f0a97b2 100644 --- a/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/blog/RemoteBlogDataSource.kt +++ b/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/blog/RemoteBlogDataSource.kt @@ -17,12 +17,14 @@ class RemoteBlogDataSource @Inject constructor( return flow { try { val data = httpClient.get(ApiUrl.Blog.latest) - emit(Result.Success(data.blogBlogData.blogPage.blogs)) + val identifiableData = data.data.blogs.results.mapIndexed { index, blog -> + blog.copy(id = index.toLong()) + } + emit(Result.Success(identifiableData)) } catch (e: Exception) { emit(Result.Error(e)) } } - } override suspend fun save(blog: Blog) { diff --git a/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/lottiefiles/LocalLottieFileDataSource.kt b/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/lottiefiles/LocalLottieFileDataSource.kt index 8df0b6d..3fd06eb 100644 --- a/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/lottiefiles/LocalLottieFileDataSource.kt +++ b/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/lottiefiles/LocalLottieFileDataSource.kt @@ -11,19 +11,19 @@ class LocalLottieFileDataSource @Inject constructor( private val lottieFileDao: LottieFilesDao ) : LottieFileDataSource { override fun findRecent(): Flow>> { - return lottieFileDao.findRecent().map { + return lottieFileDao.findByType("recent").map { Result.Success(it) } } override fun findPopular(): Flow>> { - return lottieFileDao.findPopular().map { + return lottieFileDao.findByType("popular").map { Result.Success(it) } } override fun findFeatured(): Flow>> { - return lottieFileDao.findFeatured().map { + return lottieFileDao.findByType("featured").map { Result.Success(it) } } diff --git a/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/lottiefiles/RemoteLottieFileDataSource.kt b/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/lottiefiles/RemoteLottieFileDataSource.kt index 94ac6af..8016cc7 100644 --- a/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/lottiefiles/RemoteLottieFileDataSource.kt +++ b/data/src/main/java/com/ericampire/android/androidstudycase/data/datasource/lottiefiles/RemoteLottieFileDataSource.kt @@ -7,6 +7,7 @@ import com.ericampire.android.androidstudycase.util.Result import io.ktor.client.* import io.ktor.client.request.* import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.flow import javax.inject.Inject @@ -14,11 +15,14 @@ class RemoteLottieFileDataSource @Inject constructor( private val httpClient: HttpClient ) : LottieFileDataSource { - private fun find(url: String): Flow>> { + private fun find( + url: String, + action: suspend FlowCollector>>.(LottieFilesApiResponse) -> Unit + ): Flow>> { return flow { try { val data = httpClient.get(url) - emit(Result.Success(data.lottieFilesLottieFilesData.page.results)) + action(data) } catch (e: Exception) { emit(Result.Error(e)) } @@ -26,15 +30,24 @@ class RemoteLottieFileDataSource @Inject constructor( } override fun findRecent(): Flow>> { - return find(ApiUrl.LottieFile.recent) + return find(ApiUrl.LottieFile.recent) { response -> + val animations = response.lottieFilesLottieFilesData.recent?.results ?: emptyList() + emit(Result.Success(animations)) + } } override fun findPopular(): Flow>> { - return find(ApiUrl.LottieFile.popular) + return find(ApiUrl.LottieFile.popular) { response -> + val animations = response.lottieFilesLottieFilesData.popular?.results ?: emptyList() + emit(Result.Success(animations)) + } } override fun findFeatured(): Flow>> { - return find(ApiUrl.LottieFile.featured) + return find(ApiUrl.LottieFile.featured) { response -> + val animations = response.lottieFilesLottieFilesData.featured?.results ?: emptyList() + emit(Result.Success(animations)) + } } override suspend fun save(lottiefile: Lottiefile) { diff --git a/data/src/main/java/com/ericampire/android/androidstudycase/data/di/RepositoryModule.kt b/data/src/main/java/com/ericampire/android/androidstudycase/data/di/RepositoryModule.kt index 10ba4d6..f87e61c 100644 --- a/data/src/main/java/com/ericampire/android/androidstudycase/data/di/RepositoryModule.kt +++ b/data/src/main/java/com/ericampire/android/androidstudycase/data/di/RepositoryModule.kt @@ -15,10 +15,13 @@ import com.ericampire.android.androidstudycase.domain.repository.AnimatorReposit import com.ericampire.android.androidstudycase.domain.repository.BlogRepository import com.ericampire.android.androidstudycase.domain.repository.LottieFileRepository import com.ericampire.android.androidstudycase.domain.repository.UserRepository +import com.ericampire.android.androidstudycase.util.IoDispatcher import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope @Module @@ -29,10 +32,14 @@ object RepositoryModule { fun provideAnimatorRepository( localDataSource: LocalAnimatorDataSource, remoteDataSource: RemoteAnimatorDataSource, + @IoDispatcher coroutineDispatcher: CoroutineDispatcher, + coroutineScope: CoroutineScope ): AnimatorRepository { return AnimatorRepositoryImpl( localDataSource = localDataSource, - remoteDataSource = remoteDataSource + remoteDataSource = remoteDataSource, + coroutineDispatcher = coroutineDispatcher, + coroutineScope = coroutineScope ) } @@ -49,10 +56,14 @@ object RepositoryModule { fun provideLottieFileRepository( localDataSource: LocalLottieFileDataSource, remoteDataSource: RemoteLottieFileDataSource, + @IoDispatcher coroutineDispatcher: CoroutineDispatcher, + coroutineScope: CoroutineScope ): LottieFileRepository { return LottieFileRepositoryImpl( localDataSource = localDataSource, - remoteDataSource = remoteDataSource + remoteDataSource = remoteDataSource, + coroutineDispatcher = coroutineDispatcher, + coroutineScope = coroutineScope ) } @@ -60,10 +71,14 @@ object RepositoryModule { fun provideBlogRepository( localDataSource: LocalBlogDataSource, remoteDataSource: RemoteBlogDataSource, + @IoDispatcher coroutineDispatcher: CoroutineDispatcher, + coroutineScope: CoroutineScope ) : BlogRepository { return BlogRepositoryImpl( localDataSource = localDataSource, - remoteDataSource = remoteDataSource + remoteDataSource = remoteDataSource, + coroutineDispatcher = coroutineDispatcher, + coroutineScope = coroutineScope ) } } \ No newline at end of file diff --git a/data/src/main/java/com/ericampire/android/androidstudycase/data/repository/AnimatorRepositoryImpl.kt b/data/src/main/java/com/ericampire/android/androidstudycase/data/repository/AnimatorRepositoryImpl.kt index d6699bc..74949c8 100644 --- a/data/src/main/java/com/ericampire/android/androidstudycase/data/repository/AnimatorRepositoryImpl.kt +++ b/data/src/main/java/com/ericampire/android/androidstudycase/data/repository/AnimatorRepositoryImpl.kt @@ -3,15 +3,21 @@ package com.ericampire.android.androidstudycase.data.repository import com.ericampire.android.androidstudycase.data.datasource.animator.AnimatorDataSource import com.ericampire.android.androidstudycase.domain.entity.Animator import com.ericampire.android.androidstudycase.domain.repository.AnimatorRepository +import com.ericampire.android.androidstudycase.util.IoDispatcher import com.ericampire.android.androidstudycase.util.Result +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import javax.inject.Inject class AnimatorRepositoryImpl @Inject constructor( private val remoteDataSource: AnimatorDataSource, private val localDataSource: AnimatorDataSource, + private val coroutineScope: CoroutineScope, + @IoDispatcher private val coroutineDispatcher: CoroutineDispatcher ) : AnimatorRepository { override fun findAll(): Flow>> { refreshData() @@ -19,7 +25,7 @@ class AnimatorRepositoryImpl @Inject constructor( } private fun refreshData() { - suspend { + coroutineScope.launch(coroutineDispatcher) { remoteDataSource.findAll().collect { if (it is Result.Success) { it.data.forEach { animator -> diff --git a/data/src/main/java/com/ericampire/android/androidstudycase/data/repository/BlogRepositoryImpl.kt b/data/src/main/java/com/ericampire/android/androidstudycase/data/repository/BlogRepositoryImpl.kt index 6253384..413e238 100644 --- a/data/src/main/java/com/ericampire/android/androidstudycase/data/repository/BlogRepositoryImpl.kt +++ b/data/src/main/java/com/ericampire/android/androidstudycase/data/repository/BlogRepositoryImpl.kt @@ -3,14 +3,20 @@ package com.ericampire.android.androidstudycase.data.repository import com.ericampire.android.androidstudycase.data.datasource.blog.BlogDataSource import com.ericampire.android.androidstudycase.domain.entity.Blog import com.ericampire.android.androidstudycase.domain.repository.BlogRepository +import com.ericampire.android.androidstudycase.util.IoDispatcher import com.ericampire.android.androidstudycase.util.Result +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import javax.inject.Inject class BlogRepositoryImpl @Inject constructor( private val remoteDataSource: BlogDataSource, private val localDataSource: BlogDataSource, + private val coroutineScope: CoroutineScope, + @IoDispatcher private val coroutineDispatcher: CoroutineDispatcher ) : BlogRepository { override fun findAll(): Flow>> { refreshData() @@ -18,7 +24,7 @@ class BlogRepositoryImpl @Inject constructor( } private fun refreshData() { - suspend { + coroutineScope.launch(coroutineDispatcher) { remoteDataSource.findAll().collect { if (it is Result.Success) { it.data.forEach { blog -> diff --git a/data/src/main/java/com/ericampire/android/androidstudycase/data/repository/LottieFileRepositoryImpl.kt b/data/src/main/java/com/ericampire/android/androidstudycase/data/repository/LottieFileRepositoryImpl.kt index 20ee702..6026b86 100644 --- a/data/src/main/java/com/ericampire/android/androidstudycase/data/repository/LottieFileRepositoryImpl.kt +++ b/data/src/main/java/com/ericampire/android/androidstudycase/data/repository/LottieFileRepositoryImpl.kt @@ -3,41 +3,44 @@ package com.ericampire.android.androidstudycase.data.repository import com.ericampire.android.androidstudycase.data.datasource.lottiefiles.LottieFileDataSource import com.ericampire.android.androidstudycase.domain.entity.Lottiefile import com.ericampire.android.androidstudycase.domain.repository.LottieFileRepository +import com.ericampire.android.androidstudycase.util.IoDispatcher import com.ericampire.android.androidstudycase.util.Result +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import javax.inject.Inject class LottieFileRepositoryImpl @Inject constructor( private val localDataSource: LottieFileDataSource, private val remoteDataSource: LottieFileDataSource, + private val coroutineScope: CoroutineScope, + @IoDispatcher private val coroutineDispatcher: CoroutineDispatcher ) : LottieFileRepository { override fun findRecent(): Flow>> { - val recentFiles = remoteDataSource.findRecent() - refreshData(recentFiles) - return recentFiles + refreshData(remoteDataSource.findRecent(), "recent") + return localDataSource.findRecent() } - private fun refreshData(data: Flow>>) { - suspend { + private fun refreshData(data: Flow>>, type: String) { + coroutineScope.launch(coroutineDispatcher) { data.collect { if (it is Result.Success) { - it.data.forEach { file -> localDataSource.save(file) } + it.data.forEach { file -> localDataSource.save(file.copy(type = type)) } } } } } override fun findPopular(): Flow>> { - val files = remoteDataSource.findPopular() - refreshData(files) - return files + refreshData(remoteDataSource.findPopular(), "popular") + return localDataSource.findPopular() } override fun findFeatured(): Flow>> { - val files = remoteDataSource.findFeatured() - refreshData(files) - return files + refreshData(remoteDataSource.findFeatured(), "featured") + return localDataSource.findFeatured() } } \ No newline at end of file diff --git a/data/src/main/java/com/ericampire/android/androidstudycase/data/room/LottieFilesDao.kt b/data/src/main/java/com/ericampire/android/androidstudycase/data/room/LottieFilesDao.kt index b148139..11eea80 100644 --- a/data/src/main/java/com/ericampire/android/androidstudycase/data/room/LottieFilesDao.kt +++ b/data/src/main/java/com/ericampire/android/androidstudycase/data/room/LottieFilesDao.kt @@ -12,12 +12,6 @@ interface LottieFilesDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(lottiefile: Lottiefile) - @Query("SELECT * FROM Lottiefile") - fun findPopular(): Flow> - - @Query("SELECT * FROM Lottiefile") - fun findFeatured(): Flow> - - @Query("SELECT * FROM Lottiefile") - fun findRecent(): Flow> + @Query("SELECT * FROM Lottiefile WHERE type = :type") + fun findByType(type: String): Flow> } \ No newline at end of file diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index e1eddfb..3fc0217 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -1,9 +1,7 @@ -import de.fayard.refreshVersions.core.versionFor - plugins { id("com.android.library") id("kotlin-android") - kotlin("plugin.serialization") version "1.5.20" + kotlin("plugin.serialization") version "1.5.21" kotlin("kapt") } @@ -41,6 +39,9 @@ dependencies { api(Libs.ktor_client_core) api(Libs.ktor_serialization) + api(Libs.ktor_client_android) + api(Libs.ktor_client_cio) + api(Libs.joda_time) testImplementation(Libs.junit_jupiter_api) diff --git a/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Animator.kt b/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Animator.kt index cd0fb26..998ba57 100644 --- a/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Animator.kt +++ b/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Animator.kt @@ -1,5 +1,6 @@ package com.ericampire.android.androidstudycase.domain.entity +import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import kotlinx.serialization.SerialName @@ -9,8 +10,10 @@ import kotlinx.serialization.Serializable @Serializable @Entity data class Animator( + @ColumnInfo(name = "id_animator") @PrimaryKey - var name: String = "", + val id: Long? = null, + val name: String = "", var avatarUrl: String = "", ) @@ -19,10 +22,11 @@ data class FeaturedAnimators(val results: List) @Serializable data class AnimatorData( - val featuredAnimators: FeaturedAnimators + @SerialName("featuredAnimators") + val animators: FeaturedAnimators ) @Serializable data class AnimatorApiResponse( - @SerialName("data") val animatorAnimatorData: AnimatorData + @SerialName("data") val data: AnimatorData ) \ No newline at end of file diff --git a/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/BlogPage.kt b/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/BlogPage.kt index dea6a32..a588d61 100644 --- a/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/BlogPage.kt +++ b/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/BlogPage.kt @@ -2,20 +2,14 @@ package com.ericampire.android.androidstudycase.domain.entity import androidx.room.Entity import androidx.room.PrimaryKey -import androidx.room.TypeConverters -import com.ericampire.android.androidstudycase.domain.util.DateSerializer -import com.ericampire.android.androidstudycase.util.room.DateConverter -import kotlinx.serialization.Contextual -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import java.util.* @Serializable data class BlogPage( val currentPage: Int, val from: Int, val perPage: Int, - val blogs: List, + val results: List, val to: Int, val total: Int, val totalPages: Int @@ -23,20 +17,19 @@ data class BlogPage( @Serializable data class BlogData( - @SerialName("blogs") val blogPage: BlogPage + val blogs: BlogPage ) @Serializable data class BlogApiResponse( - @SerialName("data") val blogBlogData: BlogData + val data: BlogData ) @Serializable @Entity data class Blog( -// @TypeConverters(DateConverter::class) -// @Serializable(with = DateSerializer::class) - var postedAt: String = "", - @PrimaryKey val imageUrl: String = "", - var title: String + @PrimaryKey val id: Long? = null, + val postedAt: String = "", + val imageUrl: String = "", + val title: String ) \ No newline at end of file diff --git a/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Lottiefile.kt b/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Lottiefile.kt index ff5cf7e..e3afd95 100644 --- a/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Lottiefile.kt +++ b/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Lottiefile.kt @@ -13,10 +13,14 @@ data class LottieFilesApiResponse( val lottieFilesLottieFilesData: LottieFilesData ) +/** + * You can avoid this duplication using @JsonNames annotation + */ @Serializable data class LottieFilesData( - @SerialName("recent") - val page: LottieFilesPage + val recent: LottieFilesPage? = null, + val featured: LottieFilesPage? = null, + val popular: LottieFilesPage? = null, ) @@ -40,6 +44,7 @@ data class Lottiefile( var lottieUrl: String = "", var gifUrl: String? = "", var videoUrl: String? = "", + var type: String = "", // For making difference between recent, popular and featured var imageUrl: String? = "", @ColumnInfo(name = "file_name") var name: String = "", var createdAt: String = "", diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index ee4289a..132e9f7 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -14,4 +14,6 @@ Go to Explore Login Hello Stranger + Featured Animators + Latest Stories \ No newline at end of file diff --git a/util/build.gradle.kts b/util/build.gradle.kts index e82a728..adeabf3 100644 --- a/util/build.gradle.kts +++ b/util/build.gradle.kts @@ -40,7 +40,7 @@ dependencies { api(platform(Libs.kotlin_coroutine_bom)) api(Libs.kotlin_coroutine_core) - api(Libs.orbit_mvi_core) + api(Libs.mavericks_core) testImplementation(Libs.junit_jupiter_api) testImplementation(Libs.junit_jupiter_engine) diff --git a/util/src/main/java/com/ericampire/android/androidstudycase/util/mvi/BaseViewModel.kt b/util/src/main/java/com/ericampire/android/androidstudycase/util/mvi/BaseViewModel.kt index d2acafc..71b8a77 100644 --- a/util/src/main/java/com/ericampire/android/androidstudycase/util/mvi/BaseViewModel.kt +++ b/util/src/main/java/com/ericampire/android/androidstudycase/util/mvi/BaseViewModel.kt @@ -1,12 +1,13 @@ package com.ericampire.android.androidstudycase.util.mvi -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch -import org.orbitmvi.orbit.ContainerHost -abstract class BaseViewModel : ContainerHost, ViewModel() { +abstract class BaseViewModel( + initialState: S +) : MavericksViewModel(initialState) { protected val pendingAction = MutableSharedFlow() fun submitAction(action: A) { diff --git a/versions.properties b/versions.properties index b8b7605..ff3305a 100644 --- a/versions.properties +++ b/versions.properties @@ -11,14 +11,13 @@ plugin.android=7.0.2 version.androidx.activity=1.3.0-alpha08 version.androidx.preference=1.1.1 version.androidx.appcompat=1.3.0 -version.androidx.compose.compiler=1.0.1 -version.androidx.compose.material-icons-extended=1.0.1 - -version.androidx.compose.material=1.0.1 -version.androidx.compose.runtime=1.0.1 -version.google.accompanist=0.16.1 -version.androidx.compose.ui=1.0.1 -version.androidx.compose.ui-viewbinding=1.0.1 +version.androidx.compose.compiler=1.0.2 +version.androidx.compose.material-icons-extended=1.0.2 +version.androidx.compose.material=1.0.2 +version.androidx.compose.runtime=1.0.2 +version.google.accompanist=0.18.0 +version.androidx.compose.ui=1.0.2 +version.androidx.compose.ui-viewbinding=1.0.2 version.androidx.core=1.5.0 version.androidx.room=2.3.0 version.androidx.startup=1.0.0