Update App Architecture
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -47,6 +47,9 @@
|
||||
<meta-data
|
||||
android:name="com.ericampire.android.androidstudycase.app.initializer.TimberInitializer"
|
||||
android:value="androidx.startup" />
|
||||
<meta-data
|
||||
android:name="com.ericampire.android.androidstudycase.app.initializer.MavericksInitializer"
|
||||
android:value="androidx.startup" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+17
-34
@@ -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<*, *>
|
||||
}
|
||||
+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(
|
||||
entities = [Blog::class, Animator::class, Lottiefile::class, User::class],
|
||||
version = 2,
|
||||
version = 1,
|
||||
exportSchema = false
|
||||
)
|
||||
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
|
||||
}
|
||||
+24
-28
@@ -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<ExploreViewState, ExploreEffect, ExploreAction>() {
|
||||
|
||||
override val container: Container<ExploreViewState, ExploreEffect>
|
||||
get() = container(initialState = ExploreViewState())
|
||||
) : BaseViewModel<ExploreViewState, ExploreAction>(initialState) {
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
@@ -41,21 +37,21 @@ class ExploreViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun Flow<Result<List<Lottiefile>>>.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<Result<List<Lottiefile>>>.fetchData() {
|
||||
viewModelScope.launch {
|
||||
map {
|
||||
it.data ?: emptyList()
|
||||
}.execute {
|
||||
copy(files = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
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<Lottiefile> = emptyList(),
|
||||
val isLoading: Boolean = false
|
||||
)
|
||||
val files: Async<List<Lottiefile>> = Uninitialized,
|
||||
) : MavericksState
|
||||
|
||||
+53
-23
@@ -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 ->
|
||||
Crossfade(modifier = Modifier.padding(contentPadding), targetState = state) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
content = {
|
||||
if (state.isLoading) {
|
||||
LoadingView()
|
||||
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<Lottiefile>
|
||||
) {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
+4
-4
@@ -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,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
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
|
||||
+49
-11
@@ -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<HomeViewState, HomeEffect, HomeAction>() {
|
||||
private val findFeaturedAnimatorUseCase: FindFeaturedAnimatorUseCase,
|
||||
private val findUsersUseCase: FindUsersUseCase,
|
||||
private val findFeaturedLottieFileUseCase: FindFeaturedLottieFileUseCase
|
||||
) : BaseViewModel<HomeViewState, HomeAction>(initialState) {
|
||||
|
||||
override val container: Container<HomeViewState, HomeEffect>
|
||||
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<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
|
||||
|
||||
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<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(
|
||||
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
|
||||
),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
+194
-1
@@ -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()
|
||||
) {
|
||||
|
||||
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 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)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
-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
|
||||
|
||||
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<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.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("") }
|
||||
|
||||
@@ -14,4 +14,5 @@ object AppColor {
|
||||
val Black001 = Color(0xFF222222)
|
||||
val BlackOverlay = Color(0x4D000000)
|
||||
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 Explore : Destination("explore", R.string.txt_explore)
|
||||
object Preview : Destination("preview", R.string.txt_preview)
|
||||
object Login : Destination("login", R.string.txt_login)
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
<resources>
|
||||
<string name="txt_featured_animation">Featured Animations</string>
|
||||
</resources>
|
||||
@@ -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"
|
||||
|
||||
@@ -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:_"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+5
-2
@@ -17,8 +17,11 @@ class RemoteAnimatorDataSource @Inject constructor(
|
||||
override fun findAll(): Flow<Result<List<Animator>>> {
|
||||
return flow {
|
||||
try {
|
||||
val data = httpClient.get<AnimatorApiResponse>(ApiUrl.Animator.featured)
|
||||
emit(Result.Success(data.animatorAnimatorData.featuredAnimators.results))
|
||||
val result = httpClient.get<AnimatorApiResponse>(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))
|
||||
}
|
||||
|
||||
+4
-2
@@ -17,12 +17,14 @@ class RemoteBlogDataSource @Inject constructor(
|
||||
return flow {
|
||||
try {
|
||||
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) {
|
||||
emit(Result.Error(e))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override suspend fun save(blog: Blog) {
|
||||
|
||||
+3
-3
@@ -11,19 +11,19 @@ class LocalLottieFileDataSource @Inject constructor(
|
||||
private val lottieFileDao: LottieFilesDao
|
||||
) : LottieFileDataSource {
|
||||
override fun findRecent(): Flow<Result<List<Lottiefile>>> {
|
||||
return lottieFileDao.findRecent().map {
|
||||
return lottieFileDao.findByType("recent").map {
|
||||
Result.Success(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findPopular(): Flow<Result<List<Lottiefile>>> {
|
||||
return lottieFileDao.findPopular().map {
|
||||
return lottieFileDao.findByType("popular").map {
|
||||
Result.Success(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findFeatured(): Flow<Result<List<Lottiefile>>> {
|
||||
return lottieFileDao.findFeatured().map {
|
||||
return lottieFileDao.findByType("featured").map {
|
||||
Result.Success(it)
|
||||
}
|
||||
}
|
||||
|
||||
+18
-5
@@ -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<Result<List<Lottiefile>>> {
|
||||
private fun find(
|
||||
url: String,
|
||||
action: suspend FlowCollector<Result<List<Lottiefile>>>.(LottieFilesApiResponse) -> Unit
|
||||
): Flow<Result<List<Lottiefile>>> {
|
||||
return flow {
|
||||
try {
|
||||
val data = httpClient.get<LottieFilesApiResponse>(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<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>>> {
|
||||
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>>> {
|
||||
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) {
|
||||
|
||||
+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.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
|
||||
)
|
||||
}
|
||||
}
|
||||
+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.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<Result<List<Animator>>> {
|
||||
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 ->
|
||||
|
||||
+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.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<Result<List<Blog>>> {
|
||||
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 ->
|
||||
|
||||
+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.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<Result<List<Lottiefile>>> {
|
||||
val recentFiles = remoteDataSource.findRecent()
|
||||
refreshData(recentFiles)
|
||||
return recentFiles
|
||||
refreshData(remoteDataSource.findRecent(), "recent")
|
||||
return localDataSource.findRecent()
|
||||
}
|
||||
|
||||
private fun refreshData(data: Flow<Result<List<Lottiefile>>>) {
|
||||
suspend {
|
||||
private fun refreshData(data: Flow<Result<List<Lottiefile>>>, 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<Result<List<Lottiefile>>> {
|
||||
val files = remoteDataSource.findPopular()
|
||||
refreshData(files)
|
||||
return files
|
||||
refreshData(remoteDataSource.findPopular(), "popular")
|
||||
return localDataSource.findPopular()
|
||||
}
|
||||
|
||||
override fun findFeatured(): Flow<Result<List<Lottiefile>>> {
|
||||
val files = remoteDataSource.findFeatured()
|
||||
refreshData(files)
|
||||
return files
|
||||
refreshData(remoteDataSource.findFeatured(), "featured")
|
||||
return localDataSource.findFeatured()
|
||||
}
|
||||
}
|
||||
+2
-8
@@ -12,12 +12,6 @@ interface LottieFilesDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun save(lottiefile: Lottiefile)
|
||||
|
||||
@Query("SELECT * FROM Lottiefile")
|
||||
fun findPopular(): Flow<List<Lottiefile>>
|
||||
|
||||
@Query("SELECT * FROM Lottiefile")
|
||||
fun findFeatured(): Flow<List<Lottiefile>>
|
||||
|
||||
@Query("SELECT * FROM Lottiefile")
|
||||
fun findRecent(): Flow<List<Lottiefile>>
|
||||
@Query("SELECT * FROM Lottiefile WHERE type = :type")
|
||||
fun findByType(type: String): Flow<List<Lottiefile>>
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
+7
-3
@@ -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<Animator>)
|
||||
|
||||
@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
|
||||
)
|
||||
+7
-14
@@ -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<Blog>,
|
||||
val results: List<Blog>,
|
||||
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
|
||||
)
|
||||
+7
-2
@@ -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 = "",
|
||||
|
||||
@@ -14,4 +14,6 @@
|
||||
<string name="txt_go_to_explore">Go to Explore</string>
|
||||
<string name="txt_login">Login</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>
|
||||
@@ -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)
|
||||
|
||||
+5
-4
@@ -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<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>()
|
||||
|
||||
fun submitAction(action: A) {
|
||||
|
||||
+7
-8
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user