Update App Architecture
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import de.fayard.refreshVersions.core.versionFor
|
import de.fayard.refreshVersions.core.versionFor
|
||||||
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
kotlin("android")
|
kotlin("android")
|
||||||
@@ -68,6 +67,7 @@ android {
|
|||||||
excludes += "META-INF/LICENSE.md"
|
excludes += "META-INF/LICENSE.md"
|
||||||
excludes += "META-INF/LICENSE-notice.md"
|
excludes += "META-INF/LICENSE-notice.md"
|
||||||
excludes += "META-INF/LGPL2.1"
|
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/attach_hotspot_windows.dll"
|
||||||
excludes += "win32-x86-64/attach_hotspot_windows.dll"
|
excludes += "win32-x86-64/attach_hotspot_windows.dll"
|
||||||
}
|
}
|
||||||
@@ -119,9 +119,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation(Libs.lottie_compose)
|
implementation(Libs.lottie_compose)
|
||||||
|
|
||||||
implementation(Libs.orbit_mvi_core)
|
implementation(Libs.mavericks_compose)
|
||||||
implementation(Libs.orbit_mvi_viewmodel)
|
testImplementation(Libs.mavericks_testing)
|
||||||
testImplementation(Libs.orbit_mvi_test)
|
testImplementation(Libs.mavericks_mocking)
|
||||||
|
|
||||||
kapt(Libs.room_compiler)
|
kapt(Libs.room_compiler)
|
||||||
testImplementation(Libs.room_testing)
|
testImplementation(Libs.room_testing)
|
||||||
|
|||||||
@@ -47,6 +47,9 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.ericampire.android.androidstudycase.app.initializer.TimberInitializer"
|
android:name="com.ericampire.android.androidstudycase.app.initializer.TimberInitializer"
|
||||||
android:value="androidx.startup" />
|
android:value="androidx.startup" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.ericampire.android.androidstudycase.app.initializer.MavericksInitializer"
|
||||||
|
android:value="androidx.startup" />
|
||||||
</provider>
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
package com.ericampire.android.androidstudycase.app
|
package com.ericampire.android.androidstudycase.app
|
||||||
|
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import com.ericampire.android.androidstudycase.presentation.screen.explore.ui.ExploreScreen
|
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.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.presentation.screen.preview.ui.PreviewScreen
|
||||||
import com.ericampire.android.androidstudycase.util.Destination
|
import com.ericampire.android.androidstudycase.util.Destination
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
|
||||||
|
@ExperimentalMaterialApi
|
||||||
fun NavGraphBuilder.addHomeScreen(navController: NavController) {
|
fun NavGraphBuilder.addHomeScreen(navController: NavController) {
|
||||||
composable(Destination.Home.route) {
|
composable(Destination.Home.route) {
|
||||||
HomeScreen(
|
HomeScreen(navController = navController)
|
||||||
navController = navController,
|
|
||||||
viewModel = hiltViewModel()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,18 +22,20 @@ fun NavGraphBuilder.addHomeScreen(navController: NavController) {
|
|||||||
@ExperimentalPagerApi
|
@ExperimentalPagerApi
|
||||||
fun NavGraphBuilder.addExploreScreen(navController: NavController) {
|
fun NavGraphBuilder.addExploreScreen(navController: NavController) {
|
||||||
composable(Destination.Explore.route) {
|
composable(Destination.Explore.route) {
|
||||||
ExploreScreen(
|
ExploreScreen(navController = navController)
|
||||||
navController = navController,
|
}
|
||||||
viewModel = hiltViewModel()
|
}
|
||||||
)
|
|
||||||
|
@ExperimentalMaterialApi
|
||||||
|
@ExperimentalPagerApi
|
||||||
|
fun NavGraphBuilder.addLoginScreen(navController: NavController) {
|
||||||
|
composable(Destination.Login.route) {
|
||||||
|
LoginScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavGraphBuilder.addPreviewScreen(navController: NavController) {
|
fun NavGraphBuilder.addPreviewScreen(navController: NavController) {
|
||||||
composable(Destination.Preview.route) {
|
composable(Destination.Preview.route) {
|
||||||
PreviewScreen(
|
PreviewScreen(navController = navController)
|
||||||
navController = navController,
|
|
||||||
viewModel = hiltViewModel()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,13 +9,18 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object CoroutineModule {
|
object CoroutineModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob())
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@IoDispatcher
|
@IoDispatcher
|
||||||
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
|
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
|
||||||
|
|||||||
+17
-34
@@ -1,47 +1,30 @@
|
|||||||
package com.ericampire.android.androidstudycase.app.di
|
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.explore.business.ExploreViewModel
|
||||||
import com.ericampire.android.androidstudycase.presentation.screen.home.business.HomeViewModel
|
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.Module
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.components.ViewModelComponent
|
import dagger.multibindings.IntoMap
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(ViewModelComponent::class)
|
@InstallIn(MavericksViewModelComponent::class)
|
||||||
object ViewModelModule {
|
interface ViewModelModule {
|
||||||
|
|
||||||
@Provides
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(HomeViewModel::class)
|
||||||
fun provideHomeViewModel(
|
fun provideHomeViewModel(
|
||||||
findFeaturedAnimatorUseCase: FindFeaturedAnimatorUseCase,
|
factory: HomeViewModel.Factory
|
||||||
findFeaturedBlogUseCase: FindFeaturedBlogUseCase
|
): AssistedViewModelFactory<*, *>
|
||||||
): HomeViewModel {
|
|
||||||
return HomeViewModel(
|
|
||||||
findFeaturedAnimatorUseCase = findFeaturedAnimatorUseCase,
|
|
||||||
findFeaturedBlogUseCase = findFeaturedBlogUseCase
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(ExploreViewModel::class)
|
||||||
fun provideExploreViewModel(
|
fun provideExploreViewModel(
|
||||||
findFeaturedAnimatorUseCase: FindFeaturedAnimatorUseCase,
|
factory: ExploreViewModel.Factory
|
||||||
findFeaturedBlogUseCase: FindFeaturedBlogUseCase
|
): AssistedViewModelFactory<*, *>
|
||||||
): PreviewViewModel {
|
|
||||||
return PreviewViewModel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
fun providePreview(
|
|
||||||
findPopularLottieFileUseCase: FindPopularLottieFileUseCase,
|
|
||||||
findRecentLottieFileUseCase: FindRecentLottieFileUseCase,
|
|
||||||
findFeaturedLottieFileUseCase: FindFeaturedLottieFileUseCase
|
|
||||||
): ExploreViewModel {
|
|
||||||
return ExploreViewModel(
|
|
||||||
findPopularLottieFileUseCase = findPopularLottieFileUseCase,
|
|
||||||
findRecentLottieFileUseCase = findRecentLottieFileUseCase,
|
|
||||||
findFeaturedLottieFileUseCase = findFeaturedLottieFileUseCase
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package com.ericampire.android.androidstudycase.app.hilt
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.MavericksState
|
||||||
|
import com.airbnb.mvrx.MavericksViewModel
|
||||||
|
|
||||||
|
|
||||||
|
interface AssistedViewModelFactory<VM : MavericksViewModel<S>, S : MavericksState> {
|
||||||
|
fun create(state: S): VM
|
||||||
|
}
|
||||||
+59
@@ -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 <reified VM : MavericksViewModel<S>, S : MavericksState> hiltMavericksViewModelFactory() =
|
||||||
|
HiltMavericksViewModelFactory<VM, S>(VM::class.java)
|
||||||
|
|
||||||
|
class HiltMavericksViewModelFactory<VM : MavericksViewModel<S>, S : MavericksState>(
|
||||||
|
private val viewModelClass: Class<out MavericksViewModel<S>>
|
||||||
|
) : MavericksViewModelFactory<VM, S> {
|
||||||
|
|
||||||
|
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<VM, S>
|
||||||
|
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<Class<out MavericksViewModel<*>>, AssistedViewModelFactory<*, *>>
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
package com.ericampire.android.androidstudycase.app.hilt
|
||||||
|
|
||||||
|
import javax.inject.Scope
|
||||||
|
|
||||||
|
@Scope
|
||||||
|
annotation class MavericksViewModelScoped
|
||||||
@@ -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<out MavericksViewModel<*>>)
|
||||||
+13
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -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<Unit> {
|
||||||
|
override fun create(context: Context) {
|
||||||
|
Mavericks.initialize(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dependencies(): MutableList<Class<out Initializer<*>>> {
|
||||||
|
return mutableListOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ import com.ericampire.android.androidstudycase.domain.entity.User
|
|||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Blog::class, Animator::class, Lottiefile::class, User::class],
|
entities = [Blog::class, Animator::class, Lottiefile::class, User::class],
|
||||||
version = 2,
|
version = 1,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|||||||
+199
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
-5
@@ -1,5 +0,0 @@
|
|||||||
package com.ericampire.android.androidstudycase.presentation.screen.explore.business
|
|
||||||
|
|
||||||
sealed interface ExploreEffect {
|
|
||||||
data class ShowErrorMessage(val message: String) : ExploreEffect
|
|
||||||
}
|
|
||||||
+25
-29
@@ -1,33 +1,29 @@
|
|||||||
package com.ericampire.android.androidstudycase.presentation.screen.explore.business
|
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.entity.Lottiefile
|
||||||
import com.ericampire.android.androidstudycase.domain.usecase.FindFeaturedLottieFileUseCase
|
import com.ericampire.android.androidstudycase.domain.usecase.FindFeaturedLottieFileUseCase
|
||||||
import com.ericampire.android.androidstudycase.domain.usecase.FindPopularLottieFileUseCase
|
import com.ericampire.android.androidstudycase.domain.usecase.FindPopularLottieFileUseCase
|
||||||
import com.ericampire.android.androidstudycase.domain.usecase.FindRecentLottieFileUseCase
|
import com.ericampire.android.androidstudycase.domain.usecase.FindRecentLottieFileUseCase
|
||||||
import com.ericampire.android.androidstudycase.util.Result
|
import com.ericampire.android.androidstudycase.util.Result
|
||||||
|
import com.ericampire.android.androidstudycase.util.data
|
||||||
import com.ericampire.android.androidstudycase.util.mvi.BaseViewModel
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
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 @AssistedInject constructor(
|
||||||
class ExploreViewModel @Inject constructor(
|
@Assisted initialState: ExploreViewState,
|
||||||
private val findPopularLottieFileUseCase: FindPopularLottieFileUseCase,
|
private val findPopularLottieFileUseCase: FindPopularLottieFileUseCase,
|
||||||
private val findRecentLottieFileUseCase: FindRecentLottieFileUseCase,
|
private val findRecentLottieFileUseCase: FindRecentLottieFileUseCase,
|
||||||
private val findFeaturedLottieFileUseCase: FindFeaturedLottieFileUseCase
|
private val findFeaturedLottieFileUseCase: FindFeaturedLottieFileUseCase
|
||||||
) : BaseViewModel<ExploreViewState, ExploreEffect, ExploreAction>() {
|
) : BaseViewModel<ExploreViewState, ExploreAction>(initialState) {
|
||||||
|
|
||||||
override val container: Container<ExploreViewState, ExploreEffect>
|
|
||||||
get() = container(initialState = ExploreViewState())
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -41,21 +37,21 @@ class ExploreViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Flow<Result<List<Lottiefile>>>.fetchData() = intent(registerIdling = false) {
|
private fun Flow<Result<List<Lottiefile>>>.fetchData() {
|
||||||
collect { result ->
|
viewModelScope.launch {
|
||||||
when (result) {
|
map {
|
||||||
is Result.Error -> {
|
it.data ?: emptyList()
|
||||||
val errorMessage = result.exception.localizedMessage ?: "Unknown Error"
|
}.execute {
|
||||||
reduce { state.copy(isLoading = false) }
|
copy(files = it)
|
||||||
postSideEffect(ExploreEffect.ShowErrorMessage(errorMessage))
|
|
||||||
}
|
|
||||||
Result.Loading -> reduce {
|
|
||||||
state.copy(isLoading = true)
|
|
||||||
}
|
|
||||||
is Result.Success -> reduce {
|
|
||||||
state.copy(files = result.data, isLoading = false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory : AssistedViewModelFactory<ExploreViewModel, ExploreViewState> {
|
||||||
|
override fun create(state: ExploreViewState): ExploreViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MavericksViewModelFactory<ExploreViewModel, ExploreViewState>
|
||||||
|
by hiltMavericksViewModelFactory()
|
||||||
}
|
}
|
||||||
+5
-3
@@ -1,8 +1,10 @@
|
|||||||
package com.ericampire.android.androidstudycase.presentation.screen.explore.business
|
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
|
import com.ericampire.android.androidstudycase.domain.entity.Lottiefile
|
||||||
|
|
||||||
data class ExploreViewState(
|
data class ExploreViewState(
|
||||||
val files: List<Lottiefile> = emptyList(),
|
val files: Async<List<Lottiefile>> = Uninitialized,
|
||||||
val isLoading: Boolean = false
|
) : MavericksState
|
||||||
)
|
|
||||||
|
|||||||
+59
-29
@@ -1,6 +1,6 @@
|
|||||||
package com.ericampire.android.androidstudycase.presentation.screen.explore.ui
|
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.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
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.R
|
||||||
import com.ericampire.android.androidstudycase.domain.entity.Lottiefile
|
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.custom.TopActionBar
|
||||||
import com.ericampire.android.androidstudycase.presentation.screen.explore.business.ExploreAction
|
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.ExploreViewModel
|
||||||
|
import com.ericampire.android.androidstudycase.presentation.screen.explore.business.ExploreViewState
|
||||||
import com.ericampire.android.androidstudycase.presentation.theme.AppColor
|
import com.ericampire.android.androidstudycase.presentation.theme.AppColor
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import com.google.accompanist.pager.HorizontalPager
|
||||||
import com.google.accompanist.pager.pagerTabIndicatorOffset
|
import com.google.accompanist.pager.pagerTabIndicatorOffset
|
||||||
import com.google.accompanist.pager.rememberPagerState
|
import com.google.accompanist.pager.rememberPagerState
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@ExperimentalMaterialApi
|
@ExperimentalMaterialApi
|
||||||
@ExperimentalPagerApi
|
@ExperimentalPagerApi
|
||||||
@Composable
|
@Composable
|
||||||
fun ExploreScreen(
|
fun ExploreScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
viewModel: ExploreViewModel
|
viewModel: ExploreViewModel = mavericksViewModel()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val state by viewModel.container.stateFlow.collectAsState()
|
val state by viewModel.collectAsState(ExploreViewState::files)
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val tabItems = stringArrayResource(id = R.array.explore_item)
|
val tabItems = stringArrayResource(id = R.array.explore_item)
|
||||||
val pagerState = rememberPagerState(pageCount = tabItems.size)
|
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) {
|
LaunchedEffect(viewModel) {
|
||||||
viewModel.submitAction(ExploreAction.FindRecentFile)
|
viewModel.submitAction(ExploreAction.FindRecentFile)
|
||||||
}
|
}
|
||||||
@@ -122,38 +120,70 @@ fun ExploreScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
content = { contentPadding ->
|
content = { contentPadding ->
|
||||||
Box(
|
Crossfade(modifier = Modifier.padding(contentPadding), targetState = state) {
|
||||||
modifier = Modifier
|
Box(
|
||||||
.padding(contentPadding)
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxSize(),
|
contentAlignment = Alignment.Center,
|
||||||
contentAlignment = Alignment.Center,
|
content = {
|
||||||
content = {
|
when (it) {
|
||||||
if (state.isLoading) {
|
Uninitialized -> {
|
||||||
LoadingView()
|
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
|
@ExperimentalMaterialApi
|
||||||
@Composable
|
@Composable
|
||||||
fun ExploreContent(
|
private fun ExploreContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
files: List<Lottiefile>
|
files: List<Lottiefile>
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
content = {
|
content = {
|
||||||
|
|
||||||
|
item {
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colors.surface)
|
||||||
|
.height(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
items(items = files, key = { it.toString() }) { lottieFile ->
|
items(items = files, key = { it.toString() }) { lottieFile ->
|
||||||
LottieFileItemView(
|
LottieFileItemView(
|
||||||
lottiefile = lottieFile,
|
lottiefile = lottieFile,
|
||||||
onClick = {}
|
onClick = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colors.surface)
|
||||||
|
.height(18.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-4
@@ -6,14 +6,16 @@ import androidx.compose.foundation.shape.CircleShape
|
|||||||
import androidx.compose.foundation.shape.CornerSize
|
import androidx.compose.foundation.shape.CornerSize
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
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.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
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.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -72,12 +74,10 @@ fun LottieFileItemView(
|
|||||||
Text(
|
Text(
|
||||||
text = lottiefile.name,
|
text = lottiefile.name,
|
||||||
style = MaterialTheme.typography.h6,
|
style = MaterialTheme.typography.h6,
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = lottiefile.createdBy?.name ?: "",
|
text = lottiefile.createdBy?.name ?: "",
|
||||||
style = MaterialTheme.typography.caption,
|
style = MaterialTheme.typography.caption,
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-1
@@ -1,3 +1,5 @@
|
|||||||
package com.ericampire.android.androidstudycase.presentation.screen.home.business
|
package com.ericampire.android.androidstudycase.presentation.screen.home.business
|
||||||
|
|
||||||
sealed interface HomeAction
|
sealed interface HomeAction {
|
||||||
|
object FetchData : HomeAction
|
||||||
|
}
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
package com.ericampire.android.androidstudycase.presentation.screen.home.business
|
|
||||||
|
|
||||||
sealed interface HomeEffect
|
|
||||||
+50
-12
@@ -1,30 +1,68 @@
|
|||||||
package com.ericampire.android.androidstudycase.presentation.screen.home.business
|
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.FindFeaturedAnimatorUseCase
|
||||||
import com.ericampire.android.androidstudycase.domain.usecase.FindFeaturedBlogUseCase
|
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 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.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.orbitmvi.orbit.Container
|
|
||||||
import org.orbitmvi.orbit.viewmodel.container
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
class HomeViewModel @AssistedInject constructor(
|
||||||
class HomeViewModel @Inject constructor(
|
@Assisted initialState: HomeViewState,
|
||||||
private val findFeaturedBlogUseCase: FindFeaturedBlogUseCase,
|
private val findFeaturedBlogUseCase: FindFeaturedBlogUseCase,
|
||||||
private val findFeaturedAnimatorUseCase: FindFeaturedAnimatorUseCase
|
private val findFeaturedAnimatorUseCase: FindFeaturedAnimatorUseCase,
|
||||||
) : BaseViewModel<HomeViewState, HomeEffect, HomeAction>() {
|
private val findUsersUseCase: FindUsersUseCase,
|
||||||
|
private val findFeaturedLottieFileUseCase: FindFeaturedLottieFileUseCase
|
||||||
|
) : BaseViewModel<HomeViewState, HomeAction>(initialState) {
|
||||||
|
|
||||||
override val container: Container<HomeViewState, HomeEffect>
|
|
||||||
get() = container(initialState = HomeViewState())
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
pendingAction.collectLatest { action ->
|
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<HomeViewModel, HomeViewState> {
|
||||||
|
override fun create(state: HomeViewState): HomeViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MavericksViewModelFactory<HomeViewModel, HomeViewState>
|
||||||
|
by hiltMavericksViewModelFactory()
|
||||||
}
|
}
|
||||||
+15
-2
@@ -1,7 +1,20 @@
|
|||||||
package com.ericampire.android.androidstudycase.presentation.screen.home.business
|
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.Blog
|
||||||
|
import com.ericampire.android.androidstudycase.domain.entity.Lottiefile
|
||||||
|
import com.ericampire.android.androidstudycase.domain.entity.User
|
||||||
|
|
||||||
data class HomeViewState(
|
data class HomeViewState(
|
||||||
val blog: List<Blog> = emptyList()
|
val contentData: Async<HomeContentData> = Uninitialized
|
||||||
)
|
) : MavericksState
|
||||||
|
|
||||||
|
data class HomeContentData(
|
||||||
|
val blog: List<Blog> = emptyList(),
|
||||||
|
val featuredAnimators: List<Animator> = emptyList(),
|
||||||
|
val featuredLottieFile: List<Lottiefile> = emptyList(),
|
||||||
|
val user: User? = null,
|
||||||
|
) : MavericksState
|
||||||
|
|||||||
+4
-2
@@ -45,7 +45,7 @@ fun FeaturedLottieFileView(
|
|||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(Color.White)
|
.background(Color.White)
|
||||||
.height(200.dp)
|
.height(170.dp)
|
||||||
.width(170.dp),
|
.width(170.dp),
|
||||||
content = {
|
content = {
|
||||||
LottieAnimation(
|
LottieAnimation(
|
||||||
@@ -71,7 +71,9 @@ fun FeaturedLottieFileView(
|
|||||||
Text(
|
Text(
|
||||||
text = lottiefile.createdBy?.name ?: "",
|
text = lottiefile.createdBy?.name ?: "",
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
style = MaterialTheme.typography.caption,
|
style = MaterialTheme.typography.caption.copy(
|
||||||
|
color = Color.Gray
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+194
-1
@@ -1,13 +1,206 @@
|
|||||||
package com.ericampire.android.androidstudycase.presentation.screen.home.ui
|
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.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 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.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
|
@Composable
|
||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
viewModel: HomeViewModel
|
viewModel: HomeViewModel = mavericksViewModel()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
+10
@@ -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() {
|
||||||
|
}
|
||||||
+11
@@ -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,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
+2
@@ -19,6 +19,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
|
|||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.ericampire.android.androidstudycase.app.addExploreScreen
|
import com.ericampire.android.androidstudycase.app.addExploreScreen
|
||||||
import com.ericampire.android.androidstudycase.app.addHomeScreen
|
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.app.addPreviewScreen
|
||||||
import com.ericampire.android.androidstudycase.util.Destination
|
import com.ericampire.android.androidstudycase.util.Destination
|
||||||
import com.google.accompanist.insets.navigationBarsPadding
|
import com.google.accompanist.insets.navigationBarsPadding
|
||||||
@@ -84,6 +85,7 @@ fun MainScreen() {
|
|||||||
addHomeScreen(navController = navController)
|
addHomeScreen(navController = navController)
|
||||||
addPreviewScreen(navController = navController)
|
addPreviewScreen(navController = navController)
|
||||||
addExploreScreen(navController = navController)
|
addExploreScreen(navController = navController)
|
||||||
|
addLoginScreen(navController = navController)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
package com.ericampire.android.androidstudycase.presentation.screen.preview.business
|
|
||||||
|
|
||||||
sealed interface PreviewEffect
|
|
||||||
+2
-17
@@ -1,27 +1,12 @@
|
|||||||
package com.ericampire.android.androidstudycase.presentation.screen.preview.business
|
package com.ericampire.android.androidstudycase.presentation.screen.preview.business
|
||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.ViewModel
|
||||||
import com.ericampire.android.androidstudycase.util.mvi.BaseViewModel
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class PreviewViewModel @Inject constructor(
|
class PreviewViewModel @Inject constructor(
|
||||||
|
|
||||||
) : BaseViewModel<PreviewViewState, PreviewEffect, PreviewAction>() {
|
) : ViewModel() {
|
||||||
|
|
||||||
override val container: Container<PreviewViewState, PreviewEffect>
|
|
||||||
get() = container(initialState = PreviewViewState())
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch {
|
|
||||||
pendingAction.collectLatest { action ->
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+1
-5
@@ -11,14 +11,10 @@ import com.budiyev.android.codescanner.CodeScanner
|
|||||||
import com.budiyev.android.codescanner.CodeScannerView
|
import com.budiyev.android.codescanner.CodeScannerView
|
||||||
import com.ericampire.android.androidstudycase.R
|
import com.ericampire.android.androidstudycase.R
|
||||||
import com.ericampire.android.androidstudycase.presentation.custom.LottiePreviewDialog
|
import com.ericampire.android.androidstudycase.presentation.custom.LottiePreviewDialog
|
||||||
import com.ericampire.android.androidstudycase.presentation.screen.preview.business.PreviewViewModel
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewScreen(
|
fun PreviewScreen(navController: NavController) {
|
||||||
navController: NavController,
|
|
||||||
viewModel: PreviewViewModel
|
|
||||||
) {
|
|
||||||
|
|
||||||
var codeScanner: CodeScanner? = null
|
var codeScanner: CodeScanner? = null
|
||||||
var lottieFileUrl by remember { mutableStateOf("") }
|
var lottieFileUrl by remember { mutableStateOf("") }
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ object AppColor {
|
|||||||
val Black001 = Color(0xFF222222)
|
val Black001 = Color(0xFF222222)
|
||||||
val BlackOverlay = Color(0x4D000000)
|
val BlackOverlay = Color(0x4D000000)
|
||||||
val BlackOverlay001 = Color(0x1A000000)
|
val BlackOverlay001 = Color(0x1A000000)
|
||||||
|
val WhiteTransparent = Color(0x80FFFFFF)
|
||||||
}
|
}
|
||||||
@@ -7,4 +7,5 @@ sealed class Destination(val route: String, @StringRes val resourceId: Int) {
|
|||||||
object Home : Destination("home", R.string.txt_home)
|
object Home : Destination("home", R.string.txt_home)
|
||||||
object Explore : Destination("explore", R.string.txt_explore)
|
object Explore : Destination("explore", R.string.txt_explore)
|
||||||
object Preview : Destination("preview", R.string.txt_preview)
|
object Preview : Destination("preview", R.string.txt_preview)
|
||||||
|
object Login : Destination("login", R.string.txt_login)
|
||||||
}
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
<string name="txt_featured_animation">Featured Animations</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
object Apps {
|
object Apps {
|
||||||
const val compileSdk = 30
|
const val compileSdk = 31
|
||||||
const val buildToolsVersion = "30.0.3"
|
const val buildToolsVersion = "30.0.3"
|
||||||
|
|
||||||
const val applicationId = "com.ericampire.android.androidstudycase"
|
const val applicationId = "com.ericampire.android.androidstudycase"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ object Libs {
|
|||||||
|
|
||||||
const val activity_compose = "androidx.activity:activity-compose:_"
|
const val activity_compose = "androidx.activity:activity-compose:_"
|
||||||
const val startup_runtime = "androidx.startup:startup-runtime:_"
|
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 appcompat = "androidx.appcompat:appcompat:_"
|
||||||
const val preference_ktx = "androidx.preference:preference-ktx:_"
|
const val preference_ktx = "androidx.preference:preference-ktx:_"
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ dependencies {
|
|||||||
api(platform(Libs.kotlin_coroutine_bom))
|
api(platform(Libs.kotlin_coroutine_bom))
|
||||||
api(Libs.kotlin_coroutine_core)
|
api(Libs.kotlin_coroutine_core)
|
||||||
|
|
||||||
api(Libs.ktor_client_android)
|
|
||||||
|
|
||||||
testImplementation(Libs.junit_jupiter_api)
|
testImplementation(Libs.junit_jupiter_api)
|
||||||
testImplementation(Libs.junit_jupiter_engine)
|
testImplementation(Libs.junit_jupiter_engine)
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -17,8 +17,11 @@ class RemoteAnimatorDataSource @Inject constructor(
|
|||||||
override fun findAll(): Flow<Result<List<Animator>>> {
|
override fun findAll(): Flow<Result<List<Animator>>> {
|
||||||
return flow {
|
return flow {
|
||||||
try {
|
try {
|
||||||
val data = httpClient.get<AnimatorApiResponse>(ApiUrl.Animator.featured)
|
val result = httpClient.get<AnimatorApiResponse>(ApiUrl.Animator.featured)
|
||||||
emit(Result.Success(data.animatorAnimatorData.featuredAnimators.results))
|
val identifiableData = result.data.animators.results.mapIndexed { index, animator ->
|
||||||
|
animator.copy(id = index.toLong())
|
||||||
|
}
|
||||||
|
emit(Result.Success(identifiableData))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emit(Result.Error(e))
|
emit(Result.Error(e))
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -17,12 +17,14 @@ class RemoteBlogDataSource @Inject constructor(
|
|||||||
return flow {
|
return flow {
|
||||||
try {
|
try {
|
||||||
val data = httpClient.get<BlogApiResponse>(ApiUrl.Blog.latest)
|
val data = httpClient.get<BlogApiResponse>(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) {
|
} catch (e: Exception) {
|
||||||
emit(Result.Error(e))
|
emit(Result.Error(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun save(blog: Blog) {
|
override suspend fun save(blog: Blog) {
|
||||||
|
|||||||
+3
-3
@@ -11,19 +11,19 @@ class LocalLottieFileDataSource @Inject constructor(
|
|||||||
private val lottieFileDao: LottieFilesDao
|
private val lottieFileDao: LottieFilesDao
|
||||||
) : LottieFileDataSource {
|
) : LottieFileDataSource {
|
||||||
override fun findRecent(): Flow<Result<List<Lottiefile>>> {
|
override fun findRecent(): Flow<Result<List<Lottiefile>>> {
|
||||||
return lottieFileDao.findRecent().map {
|
return lottieFileDao.findByType("recent").map {
|
||||||
Result.Success(it)
|
Result.Success(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findPopular(): Flow<Result<List<Lottiefile>>> {
|
override fun findPopular(): Flow<Result<List<Lottiefile>>> {
|
||||||
return lottieFileDao.findPopular().map {
|
return lottieFileDao.findByType("popular").map {
|
||||||
Result.Success(it)
|
Result.Success(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findFeatured(): Flow<Result<List<Lottiefile>>> {
|
override fun findFeatured(): Flow<Result<List<Lottiefile>>> {
|
||||||
return lottieFileDao.findFeatured().map {
|
return lottieFileDao.findByType("featured").map {
|
||||||
Result.Success(it)
|
Result.Success(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-5
@@ -7,6 +7,7 @@ import com.ericampire.android.androidstudycase.util.Result
|
|||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -14,11 +15,14 @@ class RemoteLottieFileDataSource @Inject constructor(
|
|||||||
private val httpClient: HttpClient
|
private val httpClient: HttpClient
|
||||||
) : LottieFileDataSource {
|
) : LottieFileDataSource {
|
||||||
|
|
||||||
private fun find(url: String): Flow<Result<List<Lottiefile>>> {
|
private fun find(
|
||||||
|
url: String,
|
||||||
|
action: suspend FlowCollector<Result<List<Lottiefile>>>.(LottieFilesApiResponse) -> Unit
|
||||||
|
): Flow<Result<List<Lottiefile>>> {
|
||||||
return flow {
|
return flow {
|
||||||
try {
|
try {
|
||||||
val data = httpClient.get<LottieFilesApiResponse>(url)
|
val data = httpClient.get<LottieFilesApiResponse>(url)
|
||||||
emit(Result.Success(data.lottieFilesLottieFilesData.page.results))
|
action(data)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emit(Result.Error(e))
|
emit(Result.Error(e))
|
||||||
}
|
}
|
||||||
@@ -26,15 +30,24 @@ class RemoteLottieFileDataSource @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun findRecent(): Flow<Result<List<Lottiefile>>> {
|
override fun findRecent(): Flow<Result<List<Lottiefile>>> {
|
||||||
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<Result<List<Lottiefile>>> {
|
override fun findPopular(): Flow<Result<List<Lottiefile>>> {
|
||||||
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<Result<List<Lottiefile>>> {
|
override fun findFeatured(): Flow<Result<List<Lottiefile>>> {
|
||||||
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) {
|
override suspend fun save(lottiefile: Lottiefile) {
|
||||||
|
|||||||
+18
-3
@@ -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.BlogRepository
|
||||||
import com.ericampire.android.androidstudycase.domain.repository.LottieFileRepository
|
import com.ericampire.android.androidstudycase.domain.repository.LottieFileRepository
|
||||||
import com.ericampire.android.androidstudycase.domain.repository.UserRepository
|
import com.ericampire.android.androidstudycase.domain.repository.UserRepository
|
||||||
|
import com.ericampire.android.androidstudycase.util.IoDispatcher
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@@ -29,10 +32,14 @@ object RepositoryModule {
|
|||||||
fun provideAnimatorRepository(
|
fun provideAnimatorRepository(
|
||||||
localDataSource: LocalAnimatorDataSource,
|
localDataSource: LocalAnimatorDataSource,
|
||||||
remoteDataSource: RemoteAnimatorDataSource,
|
remoteDataSource: RemoteAnimatorDataSource,
|
||||||
|
@IoDispatcher coroutineDispatcher: CoroutineDispatcher,
|
||||||
|
coroutineScope: CoroutineScope
|
||||||
): AnimatorRepository {
|
): AnimatorRepository {
|
||||||
return AnimatorRepositoryImpl(
|
return AnimatorRepositoryImpl(
|
||||||
localDataSource = localDataSource,
|
localDataSource = localDataSource,
|
||||||
remoteDataSource = remoteDataSource
|
remoteDataSource = remoteDataSource,
|
||||||
|
coroutineDispatcher = coroutineDispatcher,
|
||||||
|
coroutineScope = coroutineScope
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,10 +56,14 @@ object RepositoryModule {
|
|||||||
fun provideLottieFileRepository(
|
fun provideLottieFileRepository(
|
||||||
localDataSource: LocalLottieFileDataSource,
|
localDataSource: LocalLottieFileDataSource,
|
||||||
remoteDataSource: RemoteLottieFileDataSource,
|
remoteDataSource: RemoteLottieFileDataSource,
|
||||||
|
@IoDispatcher coroutineDispatcher: CoroutineDispatcher,
|
||||||
|
coroutineScope: CoroutineScope
|
||||||
): LottieFileRepository {
|
): LottieFileRepository {
|
||||||
return LottieFileRepositoryImpl(
|
return LottieFileRepositoryImpl(
|
||||||
localDataSource = localDataSource,
|
localDataSource = localDataSource,
|
||||||
remoteDataSource = remoteDataSource
|
remoteDataSource = remoteDataSource,
|
||||||
|
coroutineDispatcher = coroutineDispatcher,
|
||||||
|
coroutineScope = coroutineScope
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,10 +71,14 @@ object RepositoryModule {
|
|||||||
fun provideBlogRepository(
|
fun provideBlogRepository(
|
||||||
localDataSource: LocalBlogDataSource,
|
localDataSource: LocalBlogDataSource,
|
||||||
remoteDataSource: RemoteBlogDataSource,
|
remoteDataSource: RemoteBlogDataSource,
|
||||||
|
@IoDispatcher coroutineDispatcher: CoroutineDispatcher,
|
||||||
|
coroutineScope: CoroutineScope
|
||||||
) : BlogRepository {
|
) : BlogRepository {
|
||||||
return BlogRepositoryImpl(
|
return BlogRepositoryImpl(
|
||||||
localDataSource = localDataSource,
|
localDataSource = localDataSource,
|
||||||
remoteDataSource = remoteDataSource
|
remoteDataSource = remoteDataSource,
|
||||||
|
coroutineDispatcher = coroutineDispatcher,
|
||||||
|
coroutineScope = coroutineScope
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+7
-1
@@ -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.data.datasource.animator.AnimatorDataSource
|
||||||
import com.ericampire.android.androidstudycase.domain.entity.Animator
|
import com.ericampire.android.androidstudycase.domain.entity.Animator
|
||||||
import com.ericampire.android.androidstudycase.domain.repository.AnimatorRepository
|
import com.ericampire.android.androidstudycase.domain.repository.AnimatorRepository
|
||||||
|
import com.ericampire.android.androidstudycase.util.IoDispatcher
|
||||||
import com.ericampire.android.androidstudycase.util.Result
|
import com.ericampire.android.androidstudycase.util.Result
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
class AnimatorRepositoryImpl @Inject constructor(
|
class AnimatorRepositoryImpl @Inject constructor(
|
||||||
private val remoteDataSource: AnimatorDataSource,
|
private val remoteDataSource: AnimatorDataSource,
|
||||||
private val localDataSource: AnimatorDataSource,
|
private val localDataSource: AnimatorDataSource,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
@IoDispatcher private val coroutineDispatcher: CoroutineDispatcher
|
||||||
) : AnimatorRepository {
|
) : AnimatorRepository {
|
||||||
override fun findAll(): Flow<Result<List<Animator>>> {
|
override fun findAll(): Flow<Result<List<Animator>>> {
|
||||||
refreshData()
|
refreshData()
|
||||||
@@ -19,7 +25,7 @@ class AnimatorRepositoryImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshData() {
|
private fun refreshData() {
|
||||||
suspend {
|
coroutineScope.launch(coroutineDispatcher) {
|
||||||
remoteDataSource.findAll().collect {
|
remoteDataSource.findAll().collect {
|
||||||
if (it is Result.Success) {
|
if (it is Result.Success) {
|
||||||
it.data.forEach { animator ->
|
it.data.forEach { animator ->
|
||||||
|
|||||||
+7
-1
@@ -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.data.datasource.blog.BlogDataSource
|
||||||
import com.ericampire.android.androidstudycase.domain.entity.Blog
|
import com.ericampire.android.androidstudycase.domain.entity.Blog
|
||||||
import com.ericampire.android.androidstudycase.domain.repository.BlogRepository
|
import com.ericampire.android.androidstudycase.domain.repository.BlogRepository
|
||||||
|
import com.ericampire.android.androidstudycase.util.IoDispatcher
|
||||||
import com.ericampire.android.androidstudycase.util.Result
|
import com.ericampire.android.androidstudycase.util.Result
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class BlogRepositoryImpl @Inject constructor(
|
class BlogRepositoryImpl @Inject constructor(
|
||||||
private val remoteDataSource: BlogDataSource,
|
private val remoteDataSource: BlogDataSource,
|
||||||
private val localDataSource: BlogDataSource,
|
private val localDataSource: BlogDataSource,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
@IoDispatcher private val coroutineDispatcher: CoroutineDispatcher
|
||||||
) : BlogRepository {
|
) : BlogRepository {
|
||||||
override fun findAll(): Flow<Result<List<Blog>>> {
|
override fun findAll(): Flow<Result<List<Blog>>> {
|
||||||
refreshData()
|
refreshData()
|
||||||
@@ -18,7 +24,7 @@ class BlogRepositoryImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshData() {
|
private fun refreshData() {
|
||||||
suspend {
|
coroutineScope.launch(coroutineDispatcher) {
|
||||||
remoteDataSource.findAll().collect {
|
remoteDataSource.findAll().collect {
|
||||||
if (it is Result.Success) {
|
if (it is Result.Success) {
|
||||||
it.data.forEach { blog ->
|
it.data.forEach { blog ->
|
||||||
|
|||||||
+15
-12
@@ -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.data.datasource.lottiefiles.LottieFileDataSource
|
||||||
import com.ericampire.android.androidstudycase.domain.entity.Lottiefile
|
import com.ericampire.android.androidstudycase.domain.entity.Lottiefile
|
||||||
import com.ericampire.android.androidstudycase.domain.repository.LottieFileRepository
|
import com.ericampire.android.androidstudycase.domain.repository.LottieFileRepository
|
||||||
|
import com.ericampire.android.androidstudycase.util.IoDispatcher
|
||||||
import com.ericampire.android.androidstudycase.util.Result
|
import com.ericampire.android.androidstudycase.util.Result
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LottieFileRepositoryImpl @Inject constructor(
|
class LottieFileRepositoryImpl @Inject constructor(
|
||||||
private val localDataSource: LottieFileDataSource,
|
private val localDataSource: LottieFileDataSource,
|
||||||
private val remoteDataSource: LottieFileDataSource,
|
private val remoteDataSource: LottieFileDataSource,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
|
@IoDispatcher private val coroutineDispatcher: CoroutineDispatcher
|
||||||
) : LottieFileRepository {
|
) : LottieFileRepository {
|
||||||
|
|
||||||
override fun findRecent(): Flow<Result<List<Lottiefile>>> {
|
override fun findRecent(): Flow<Result<List<Lottiefile>>> {
|
||||||
val recentFiles = remoteDataSource.findRecent()
|
refreshData(remoteDataSource.findRecent(), "recent")
|
||||||
refreshData(recentFiles)
|
return localDataSource.findRecent()
|
||||||
return recentFiles
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshData(data: Flow<Result<List<Lottiefile>>>) {
|
private fun refreshData(data: Flow<Result<List<Lottiefile>>>, type: String) {
|
||||||
suspend {
|
coroutineScope.launch(coroutineDispatcher) {
|
||||||
data.collect {
|
data.collect {
|
||||||
if (it is Result.Success) {
|
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<Result<List<Lottiefile>>> {
|
override fun findPopular(): Flow<Result<List<Lottiefile>>> {
|
||||||
val files = remoteDataSource.findPopular()
|
refreshData(remoteDataSource.findPopular(), "popular")
|
||||||
refreshData(files)
|
return localDataSource.findPopular()
|
||||||
return files
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findFeatured(): Flow<Result<List<Lottiefile>>> {
|
override fun findFeatured(): Flow<Result<List<Lottiefile>>> {
|
||||||
val files = remoteDataSource.findFeatured()
|
refreshData(remoteDataSource.findFeatured(), "featured")
|
||||||
refreshData(files)
|
return localDataSource.findFeatured()
|
||||||
return files
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-8
@@ -12,12 +12,6 @@ interface LottieFilesDao {
|
|||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun save(lottiefile: Lottiefile)
|
suspend fun save(lottiefile: Lottiefile)
|
||||||
|
|
||||||
@Query("SELECT * FROM Lottiefile")
|
@Query("SELECT * FROM Lottiefile WHERE type = :type")
|
||||||
fun findPopular(): Flow<List<Lottiefile>>
|
fun findByType(type: String): Flow<List<Lottiefile>>
|
||||||
|
|
||||||
@Query("SELECT * FROM Lottiefile")
|
|
||||||
fun findFeatured(): Flow<List<Lottiefile>>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM Lottiefile")
|
|
||||||
fun findRecent(): Flow<List<Lottiefile>>
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import de.fayard.refreshVersions.core.versionFor
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.library")
|
id("com.android.library")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
kotlin("plugin.serialization") version "1.5.20"
|
kotlin("plugin.serialization") version "1.5.21"
|
||||||
kotlin("kapt")
|
kotlin("kapt")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +39,9 @@ dependencies {
|
|||||||
|
|
||||||
api(Libs.ktor_client_core)
|
api(Libs.ktor_client_core)
|
||||||
api(Libs.ktor_serialization)
|
api(Libs.ktor_serialization)
|
||||||
|
api(Libs.ktor_client_android)
|
||||||
|
api(Libs.ktor_client_cio)
|
||||||
|
|
||||||
api(Libs.joda_time)
|
api(Libs.joda_time)
|
||||||
|
|
||||||
testImplementation(Libs.junit_jupiter_api)
|
testImplementation(Libs.junit_jupiter_api)
|
||||||
|
|||||||
+7
-3
@@ -1,5 +1,6 @@
|
|||||||
package com.ericampire.android.androidstudycase.domain.entity
|
package com.ericampire.android.androidstudycase.domain.entity
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
@@ -9,8 +10,10 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
@Entity
|
@Entity
|
||||||
data class Animator(
|
data class Animator(
|
||||||
|
@ColumnInfo(name = "id_animator")
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
var name: String = "",
|
val id: Long? = null,
|
||||||
|
val name: String = "",
|
||||||
var avatarUrl: String = "",
|
var avatarUrl: String = "",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,10 +22,11 @@ data class FeaturedAnimators(val results: List<Animator>)
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AnimatorData(
|
data class AnimatorData(
|
||||||
val featuredAnimators: FeaturedAnimators
|
@SerialName("featuredAnimators")
|
||||||
|
val animators: FeaturedAnimators
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AnimatorApiResponse(
|
data class AnimatorApiResponse(
|
||||||
@SerialName("data") val animatorAnimatorData: AnimatorData
|
@SerialName("data") val data: AnimatorData
|
||||||
)
|
)
|
||||||
+7
-14
@@ -2,20 +2,14 @@ package com.ericampire.android.androidstudycase.domain.entity
|
|||||||
|
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
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 kotlinx.serialization.Serializable
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BlogPage(
|
data class BlogPage(
|
||||||
val currentPage: Int,
|
val currentPage: Int,
|
||||||
val from: Int,
|
val from: Int,
|
||||||
val perPage: Int,
|
val perPage: Int,
|
||||||
val blogs: List<Blog>,
|
val results: List<Blog>,
|
||||||
val to: Int,
|
val to: Int,
|
||||||
val total: Int,
|
val total: Int,
|
||||||
val totalPages: Int
|
val totalPages: Int
|
||||||
@@ -23,20 +17,19 @@ data class BlogPage(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BlogData(
|
data class BlogData(
|
||||||
@SerialName("blogs") val blogPage: BlogPage
|
val blogs: BlogPage
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BlogApiResponse(
|
data class BlogApiResponse(
|
||||||
@SerialName("data") val blogBlogData: BlogData
|
val data: BlogData
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@Entity
|
@Entity
|
||||||
data class Blog(
|
data class Blog(
|
||||||
// @TypeConverters(DateConverter::class)
|
@PrimaryKey val id: Long? = null,
|
||||||
// @Serializable(with = DateSerializer::class)
|
val postedAt: String = "",
|
||||||
var postedAt: String = "",
|
val imageUrl: String = "",
|
||||||
@PrimaryKey val imageUrl: String = "",
|
val title: String
|
||||||
var title: String
|
|
||||||
)
|
)
|
||||||
+7
-2
@@ -13,10 +13,14 @@ data class LottieFilesApiResponse(
|
|||||||
val lottieFilesLottieFilesData: LottieFilesData
|
val lottieFilesLottieFilesData: LottieFilesData
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can avoid this duplication using @JsonNames annotation
|
||||||
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LottieFilesData(
|
data class LottieFilesData(
|
||||||
@SerialName("recent")
|
val recent: LottieFilesPage? = null,
|
||||||
val page: LottieFilesPage
|
val featured: LottieFilesPage? = null,
|
||||||
|
val popular: LottieFilesPage? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -40,6 +44,7 @@ data class Lottiefile(
|
|||||||
var lottieUrl: String = "",
|
var lottieUrl: String = "",
|
||||||
var gifUrl: String? = "",
|
var gifUrl: String? = "",
|
||||||
var videoUrl: String? = "",
|
var videoUrl: String? = "",
|
||||||
|
var type: String = "", // For making difference between recent, popular and featured
|
||||||
var imageUrl: String? = "",
|
var imageUrl: String? = "",
|
||||||
@ColumnInfo(name = "file_name") var name: String = "",
|
@ColumnInfo(name = "file_name") var name: String = "",
|
||||||
var createdAt: String = "",
|
var createdAt: String = "",
|
||||||
|
|||||||
@@ -14,4 +14,6 @@
|
|||||||
<string name="txt_go_to_explore">Go to Explore</string>
|
<string name="txt_go_to_explore">Go to Explore</string>
|
||||||
<string name="txt_login">Login</string>
|
<string name="txt_login">Login</string>
|
||||||
<string name="txt_hello_stranger">Hello Stranger</string>
|
<string name="txt_hello_stranger">Hello Stranger</string>
|
||||||
|
<string name="txt_featured_animator">Featured Animators</string>
|
||||||
|
<string name="txt_latest_story">Latest Stories</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -40,7 +40,7 @@ dependencies {
|
|||||||
api(platform(Libs.kotlin_coroutine_bom))
|
api(platform(Libs.kotlin_coroutine_bom))
|
||||||
api(Libs.kotlin_coroutine_core)
|
api(Libs.kotlin_coroutine_core)
|
||||||
|
|
||||||
api(Libs.orbit_mvi_core)
|
api(Libs.mavericks_core)
|
||||||
|
|
||||||
testImplementation(Libs.junit_jupiter_api)
|
testImplementation(Libs.junit_jupiter_api)
|
||||||
testImplementation(Libs.junit_jupiter_engine)
|
testImplementation(Libs.junit_jupiter_engine)
|
||||||
|
|||||||
+5
-4
@@ -1,12 +1,13 @@
|
|||||||
package com.ericampire.android.androidstudycase.util.mvi
|
package com.ericampire.android.androidstudycase.util.mvi
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import com.airbnb.mvrx.MavericksState
|
||||||
import androidx.lifecycle.viewModelScope
|
import com.airbnb.mvrx.MavericksViewModel
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.orbitmvi.orbit.ContainerHost
|
|
||||||
|
|
||||||
abstract class BaseViewModel<S: Any, E: Any, A> : ContainerHost<S, E>, ViewModel() {
|
abstract class BaseViewModel<S : MavericksState, A>(
|
||||||
|
initialState: S
|
||||||
|
) : MavericksViewModel<S>(initialState) {
|
||||||
protected val pendingAction = MutableSharedFlow<A>()
|
protected val pendingAction = MutableSharedFlow<A>()
|
||||||
|
|
||||||
fun submitAction(action: A) {
|
fun submitAction(action: A) {
|
||||||
|
|||||||
+7
-8
@@ -11,14 +11,13 @@ plugin.android=7.0.2
|
|||||||
version.androidx.activity=1.3.0-alpha08
|
version.androidx.activity=1.3.0-alpha08
|
||||||
version.androidx.preference=1.1.1
|
version.androidx.preference=1.1.1
|
||||||
version.androidx.appcompat=1.3.0
|
version.androidx.appcompat=1.3.0
|
||||||
version.androidx.compose.compiler=1.0.1
|
version.androidx.compose.compiler=1.0.2
|
||||||
version.androidx.compose.material-icons-extended=1.0.1
|
version.androidx.compose.material-icons-extended=1.0.2
|
||||||
|
version.androidx.compose.material=1.0.2
|
||||||
version.androidx.compose.material=1.0.1
|
version.androidx.compose.runtime=1.0.2
|
||||||
version.androidx.compose.runtime=1.0.1
|
version.google.accompanist=0.18.0
|
||||||
version.google.accompanist=0.16.1
|
version.androidx.compose.ui=1.0.2
|
||||||
version.androidx.compose.ui=1.0.1
|
version.androidx.compose.ui-viewbinding=1.0.2
|
||||||
version.androidx.compose.ui-viewbinding=1.0.1
|
|
||||||
version.androidx.core=1.5.0
|
version.androidx.core=1.5.0
|
||||||
version.androidx.room=2.3.0
|
version.androidx.room=2.3.0
|
||||||
version.androidx.startup=1.0.0
|
version.androidx.startup=1.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user