Update App Architecture

This commit is contained in:
2021-09-07 05:36:04 +02:00
parent 1367955396
commit abb21c994f
53 changed files with 867 additions and 245 deletions
+4 -4
View File
@@ -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)
+3
View File
@@ -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
@@ -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
)
}
} }
@@ -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
}
@@ -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<*, *>>
}
@@ -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<*>>)
@@ -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)
}
}
@@ -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() {
@@ -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
)
}
)
}
}
@@ -1,5 +0,0 @@
package com.ericampire.android.androidstudycase.presentation.screen.explore.business
sealed interface ExploreEffect {
data class ShowErrorMessage(val message: String) : ExploreEffect
}
@@ -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()
} }
@@ -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
)
@@ -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)
)
} }
} }
) )
@@ -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,
) )
} }
) )
@@ -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
}
@@ -1,3 +0,0 @@
package com.ericampire.android.androidstudycase.presentation.screen.home.business
sealed interface HomeEffect
@@ -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()
} }
@@ -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
@@ -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
),
) )
} }
) )
@@ -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()
}
}
)
}
@@ -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() {
}
@@ -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,
) {
}
@@ -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)
} }
) )
}, },
@@ -1,3 +0,0 @@
package com.ericampire.android.androidstudycase.presentation.screen.preview.business
sealed interface PreviewEffect
@@ -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 ->
}
}
}
} }
@@ -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
View File
@@ -1,2 +1,3 @@
<resources> <resources>
<string name="txt_featured_animation">Featured Animations</string>
</resources> </resources>
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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:_"
-2
View File
@@ -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)
@@ -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))
} }
@@ -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) {
@@ -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)
} }
} }
@@ -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) {
@@ -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
) )
} }
} }
@@ -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 ->
@@ -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 ->
@@ -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
} }
} }
@@ -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>>
} }
+4 -3
View File
@@ -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)
@@ -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
) )
@@ -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
) )
@@ -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 = "",
+2
View File
@@ -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>
+1 -1
View File
@@ -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)
@@ -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
View File
@@ -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