diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/MainActivity.kt b/app/src/main/java/com/ericampire/android/androidstudycase/MainActivity.kt index 223a95e..5e63be9 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/MainActivity.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/MainActivity.kt @@ -8,31 +8,20 @@ import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview -import com.ericampire.android.androidstudycase.ui.theme.AndroidStudyCaseTheme +import com.ericampire.android.androidstudycase.presentation.screen.main.ui.MainScreen +import com.ericampire.android.androidstudycase.presentation.theme.AndroidStudyCaseTheme +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - AndroidStudyCaseTheme { - // A surface container using the 'background' color from the theme + AndroidStudyCaseTheme(darkTheme = true) { Surface(color = MaterialTheme.colors.background) { - Greeting("Android") + MainScreen() } } } } -} - -@Composable -fun Greeting(name: String) { - Text(text = "Hello $name!") -} - -@Preview(showBackground = true) -@Composable -fun DefaultPreview() { - AndroidStudyCaseTheme { - Greeting("Android") - } } \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/app/AppNavigation.kt b/app/src/main/java/com/ericampire/android/androidstudycase/app/AppNavigation.kt new file mode 100644 index 0000000..c87680e --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/app/AppNavigation.kt @@ -0,0 +1,38 @@ +package com.ericampire.android.androidstudycase.app + +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.business.HomeViewModel +import com.ericampire.android.androidstudycase.presentation.screen.home.ui.HomeScreen +import com.ericampire.android.androidstudycase.presentation.screen.preview.ui.PreviewScreen +import com.ericampire.android.androidstudycase.util.Destination + +fun NavGraphBuilder.addHomeScreen(navController: NavController) { + composable(Destination.Home.route) { + HomeScreen( + navController = navController, + viewModel = hiltViewModel() + ) + } +} + +fun NavGraphBuilder.addExploreScreen(navController: NavController) { + composable(Destination.Explore.route) { + ExploreScreen( + navController = navController, + viewModel = hiltViewModel() + ) + } +} + +fun NavGraphBuilder.addPreviewScreen(navController: NavController) { + composable(Destination.Preview.route) { + PreviewScreen( + navController = navController, + viewModel = hiltViewModel() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/app/di/RoomModule.kt b/app/src/main/java/com/ericampire/android/androidstudycase/app/di/RoomModule.kt new file mode 100644 index 0000000..2a543b2 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/app/di/RoomModule.kt @@ -0,0 +1,42 @@ +package com.ericampire.android.androidstudycase.app.di + +import android.content.Context +import androidx.room.Room +import com.ericampire.android.androidstudycase.app.room.AppDatabase +import com.ericampire.android.androidstudycase.data.room.AnimatorDao +import com.ericampire.android.androidstudycase.data.room.BlogDao +import com.ericampire.android.androidstudycase.data.room.LottieFilesDao +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object RoomModule { + + @Provides + fun provideRoomDatabase(@ApplicationContext context: Context): AppDatabase { + val db = Room.databaseBuilder( + context, + AppDatabase::class.java, "database-name" + ) + return db.build() + } + + @Provides + fun provideBlogDao(appDatabase: AppDatabase): BlogDao { + return appDatabase.blogDao + } + + @Provides + fun provideAnimatorDao(appDatabase: AppDatabase): AnimatorDao { + return appDatabase.animatorDao + } + + @Provides + fun provideLottieFilesDao(appDatabase: AppDatabase): LottieFilesDao { + return appDatabase.lottieFileDao + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/app/di/ViewModelModule.kt b/app/src/main/java/com/ericampire/android/androidstudycase/app/di/ViewModelModule.kt index c1c5952..3bdceee 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/app/di/ViewModelModule.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/app/di/ViewModelModule.kt @@ -1,6 +1,11 @@ package com.ericampire.android.androidstudycase.app.di +import com.ericampire.android.androidstudycase.domain.usecase.* +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.Module +import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ViewModelComponent @@ -8,4 +13,35 @@ import dagger.hilt.android.components.ViewModelComponent @InstallIn(ViewModelComponent::class) object ViewModelModule { + @Provides + fun provideHomeViewModel( + findFeaturedAnimatorUseCase: FindFeaturedAnimatorUseCase, + findFeaturedBlogUseCase: FindFeaturedBlogUseCase + ): HomeViewModel { + return HomeViewModel( + findFeaturedAnimatorUseCase = findFeaturedAnimatorUseCase, + findFeaturedBlogUseCase = findFeaturedBlogUseCase + ) + } + + @Provides + 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 + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/app/room/AppDatabase.kt b/app/src/main/java/com/ericampire/android/androidstudycase/app/room/AppDatabase.kt new file mode 100644 index 0000000..ad42854 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/app/room/AppDatabase.kt @@ -0,0 +1,23 @@ +package com.ericampire.android.androidstudycase.app.room + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.ericampire.android.androidstudycase.data.room.AnimatorDao +import com.ericampire.android.androidstudycase.data.room.BlogDao +import com.ericampire.android.androidstudycase.data.room.LottieFilesDao +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.util.room.DateConverter + +@Database( + entities = [Blog::class, Animator::class, Lottiefile::class], + version = 2, + exportSchema = false +) +abstract class AppDatabase : RoomDatabase() { + abstract val blogDao: BlogDao + abstract val animatorDao: AnimatorDao + abstract val lottieFileDao: LottieFilesDao +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreAction.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreAction.kt new file mode 100644 index 0000000..3995086 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreAction.kt @@ -0,0 +1,7 @@ +package com.ericampire.android.androidstudycase.presentation.screen.explore.business + +sealed interface ExploreAction { + object FindRecentFile : ExploreAction + object FindFeaturedFile : ExploreAction + object FindPopularFile : ExploreAction +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreEffect.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreEffect.kt new file mode 100644 index 0000000..c491e86 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreEffect.kt @@ -0,0 +1,7 @@ +package com.ericampire.android.androidstudycase.presentation.screen.explore.business + +sealed interface ExploreEffect { + data class ShowErrorMessage(val message: String) : ExploreEffect + object Loading : ExploreEffect + object Success : ExploreEffect +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreViewModel.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreViewModel.kt new file mode 100644 index 0000000..e352ec0 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreViewModel.kt @@ -0,0 +1,79 @@ +package com.ericampire.android.androidstudycase.presentation.screen.explore.business + +import androidx.lifecycle.viewModelScope +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.mvi.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.collectLatest +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( + private val findPopularLottieFileUseCase: FindPopularLottieFileUseCase, + private val findRecentLottieFileUseCase: FindRecentLottieFileUseCase, + private val findFeaturedLottieFileUseCase: FindFeaturedLottieFileUseCase +) : BaseViewModel() { + + override val container: Container + get() = container(initialState = ExploreViewState()) + + init { + viewModelScope.launch { + pendingAction.collectLatest { action -> + when(action) { + ExploreAction.FindFeaturedFile -> findFeaturedFile() + ExploreAction.FindPopularFile -> findPopularFile() + ExploreAction.FindRecentFile -> findRecentFile() + } + } + } + } + + private fun Flow>>.fetchData() = intent { + collect { result -> + when(result) { + is Result.Error -> { + val errorMessage = result.exception.localizedMessage ?: "Unknown Error" + postSideEffect(ExploreEffect.ShowErrorMessage(errorMessage)) + } + Result.Loading -> { + postSideEffect(ExploreEffect.Loading) + } + is Result.Success -> { + postSideEffect(ExploreEffect.Success) + reduce { state.copy(files = result.data) } + } + } + } + } + + private fun findRecentFile() { + viewModelScope.launch { + findRecentLottieFileUseCase(Unit).fetchData() + } + } + + private fun findPopularFile() { + viewModelScope.launch { + findPopularLottieFileUseCase(Unit).fetchData() + } + } + + private fun findFeaturedFile() { + viewModelScope.launch { + findFeaturedLottieFileUseCase(Unit).fetchData() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreViewState.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreViewState.kt new file mode 100644 index 0000000..3406c77 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/business/ExploreViewState.kt @@ -0,0 +1,7 @@ +package com.ericampire.android.androidstudycase.presentation.screen.explore.business + +import com.ericampire.android.androidstudycase.domain.entity.Lottiefile + +data class ExploreViewState( + val files: List = emptyList() +) diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/ui/ExploreScreen.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/ui/ExploreScreen.kt new file mode 100644 index 0000000..cb196ae --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/explore/ui/ExploreScreen.kt @@ -0,0 +1,13 @@ +package com.ericampire.android.androidstudycase.presentation.screen.explore.ui + +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import com.ericampire.android.androidstudycase.presentation.screen.explore.business.ExploreViewModel + +@Composable +fun ExploreScreen( + navController: NavController, + viewModel: ExploreViewModel +) { + +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeAction.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeAction.kt new file mode 100644 index 0000000..d285d5d --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeAction.kt @@ -0,0 +1,3 @@ +package com.ericampire.android.androidstudycase.presentation.screen.home.business + +sealed interface HomeAction \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeEffect.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeEffect.kt new file mode 100644 index 0000000..5eafec2 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeEffect.kt @@ -0,0 +1,3 @@ +package com.ericampire.android.androidstudycase.presentation.screen.home.business + +sealed interface HomeEffect \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeViewModel.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeViewModel.kt new file mode 100644 index 0000000..8e61ae6 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeViewModel.kt @@ -0,0 +1,30 @@ +package com.ericampire.android.androidstudycase.presentation.screen.home.business + +import androidx.lifecycle.viewModelScope +import com.ericampire.android.androidstudycase.domain.usecase.FindFeaturedAnimatorUseCase +import com.ericampire.android.androidstudycase.domain.usecase.FindFeaturedBlogUseCase +import com.ericampire.android.androidstudycase.util.mvi.BaseViewModel +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 HomeViewModel @Inject constructor( + private val findFeaturedBlogUseCase: FindFeaturedBlogUseCase, + private val findFeaturedAnimatorUseCase: FindFeaturedAnimatorUseCase +) : BaseViewModel() { + + override val container: Container + get() = container(initialState = HomeViewState()) + + init { + viewModelScope.launch { + pendingAction.collectLatest { action -> + + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeViewState.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeViewState.kt new file mode 100644 index 0000000..3d7e4d0 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/business/HomeViewState.kt @@ -0,0 +1,7 @@ +package com.ericampire.android.androidstudycase.presentation.screen.home.business + +import com.ericampire.android.androidstudycase.domain.entity.Blog + +data class HomeViewState( + val blog: List = emptyList() +) diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/ui/HomeScreen.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/ui/HomeScreen.kt new file mode 100644 index 0000000..b028dd8 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/home/ui/HomeScreen.kt @@ -0,0 +1,13 @@ +package com.ericampire.android.androidstudycase.presentation.screen.home.ui + +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import com.ericampire.android.androidstudycase.presentation.screen.home.business.HomeViewModel + +@Composable +fun HomeScreen( + navController: NavController, + viewModel: HomeViewModel +) { + +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/main/ui/MainScreen.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/main/ui/MainScreen.kt new file mode 100644 index 0000000..5ca0dfa --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/main/ui/MainScreen.kt @@ -0,0 +1,92 @@ +package com.ericampire.android.androidstudycase.presentation.screen.main.ui + +import androidx.compose.runtime.Composable +import androidx.navigation.compose.rememberNavController +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Explore +import androidx.compose.material.icons.rounded.Home +import androidx.compose.material.icons.rounded.Scanner +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.compose.NavHost +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.compose.currentBackStackEntryAsState +import com.ericampire.android.androidstudycase.app.addExploreScreen +import com.ericampire.android.androidstudycase.app.addHomeScreen +import com.ericampire.android.androidstudycase.app.addPreviewScreen +import com.ericampire.android.androidstudycase.util.Destination + +@Composable +fun MainScreen() { + val navController = rememberNavController() + + val items = listOf( + Destination.Home, + Destination.Preview, + Destination.Explore, + ) + + Scaffold( + bottomBar = { + BottomNavigation( + backgroundColor = MaterialTheme.colors.background, + content = { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + items.forEachIndexed { index, screen -> + BottomNavigationItem( + selectedContentColor = MaterialTheme.colors.primary, + unselectedContentColor = Color.Gray, + icon = { + Icon( + imageVector = getIconByIndex(index), + contentDescription = null + ) + }, + label = { + Text(text = stringResource(screen.resourceId)) + }, + selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true, + onClick = { + navController.navigate(screen.route) { + launchSingleTop = true + restoreState = true + } + } + ) + } + } + ) + }, + content = { innerPadding -> + NavHost( + navController = navController, + modifier = Modifier.padding(innerPadding), + startDestination = Destination.Home.route, + builder = { + addHomeScreen(navController = navController) + addPreviewScreen(navController = navController) + addExploreScreen(navController = navController) + } + ) + }, + ) +} + +fun getIconByIndex(index: Int): ImageVector { + return when (index) { + 0 -> Icons.Rounded.Home + 1 -> Icons.Rounded.Scanner + 2 -> Icons.Rounded.Explore + else -> Icons.Rounded.Home + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewAction.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewAction.kt new file mode 100644 index 0000000..ef9f402 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewAction.kt @@ -0,0 +1,3 @@ +package com.ericampire.android.androidstudycase.presentation.screen.preview.business + +sealed interface PreviewAction \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewEffect.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewEffect.kt new file mode 100644 index 0000000..77f62df --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewEffect.kt @@ -0,0 +1,3 @@ +package com.ericampire.android.androidstudycase.presentation.screen.preview.business + +sealed interface PreviewEffect \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewViewModel.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewViewModel.kt new file mode 100644 index 0000000..99a60d1 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewViewModel.kt @@ -0,0 +1,27 @@ +package com.ericampire.android.androidstudycase.presentation.screen.preview.business + +import androidx.lifecycle.viewModelScope +import com.ericampire.android.androidstudycase.util.mvi.BaseViewModel +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() { + + override val container: Container + get() = container(initialState = PreviewViewState()) + + init { + viewModelScope.launch { + pendingAction.collectLatest { action -> + + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewViewState.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewViewState.kt new file mode 100644 index 0000000..44c7253 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/business/PreviewViewState.kt @@ -0,0 +1,7 @@ +package com.ericampire.android.androidstudycase.presentation.screen.preview.business + +import com.ericampire.android.androidstudycase.domain.entity.Lottiefile + +data class PreviewViewState( + val lottieFile: Lottiefile? = null +) diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/ui/PreviewScreen.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/ui/PreviewScreen.kt new file mode 100644 index 0000000..2d1e8d2 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/screen/preview/ui/PreviewScreen.kt @@ -0,0 +1,13 @@ +package com.ericampire.android.androidstudycase.presentation.screen.preview.ui + +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import com.ericampire.android.androidstudycase.presentation.screen.preview.business.PreviewViewModel + +@Composable +fun PreviewScreen( + navController: NavController, + viewModel: PreviewViewModel +) { + +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Color.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Color.kt new file mode 100644 index 0000000..584e942 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Color.kt @@ -0,0 +1,12 @@ +package com.ericampire.android.androidstudycase.presentation.theme + +import androidx.compose.ui.graphics.Color + + + +object AppColor { + val Purple200 = Color(0xFFBB86FC) + val Purple500 = Color(0xFF6200EE) + val Purple700 = Color(0xFF3700B3) + val Teal200 = Color(0xFF03DAC5) +} \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/ui/theme/Shape.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Shape.kt similarity index 79% rename from app/src/main/java/com/ericampire/android/androidstudycase/ui/theme/Shape.kt rename to app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Shape.kt index e36e512..b835bbe 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/ui/theme/Shape.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Shape.kt @@ -1,4 +1,4 @@ -package com.ericampire.android.androidstudycase.ui.theme +package com.ericampire.android.androidstudycase.presentation.theme import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Shapes diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/ui/theme/Theme.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Theme.kt similarity index 76% rename from app/src/main/java/com/ericampire/android/androidstudycase/ui/theme/Theme.kt rename to app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Theme.kt index e7e83fc..d5eb658 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/ui/theme/Theme.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Theme.kt @@ -1,4 +1,4 @@ -package com.ericampire.android.androidstudycase.ui.theme +package com.ericampire.android.androidstudycase.presentation.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.MaterialTheme @@ -8,15 +8,15 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color private val DarkColorPalette = darkColors( - primary = Purple200, - primaryVariant = Purple700, - secondary = Teal200 + primary = AppColor.Purple200, + primaryVariant = AppColor.Purple700, + secondary = AppColor.Teal200 ) private val LightColorPalette = lightColors( - primary = Purple500, - primaryVariant = Purple700, - secondary = Teal200, + primary = AppColor.Purple500, + primaryVariant = AppColor.Purple700, + secondary = AppColor.Teal200, background = Color.White, surface = Color.White, onPrimary = Color.White, diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/ui/theme/Type.kt b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Type.kt similarity index 90% rename from app/src/main/java/com/ericampire/android/androidstudycase/ui/theme/Type.kt rename to app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Type.kt index 80a2370..1d632b7 100644 --- a/app/src/main/java/com/ericampire/android/androidstudycase/ui/theme/Type.kt +++ b/app/src/main/java/com/ericampire/android/androidstudycase/presentation/theme/Type.kt @@ -1,4 +1,4 @@ -package com.ericampire.android.androidstudycase.ui.theme +package com.ericampire.android.androidstudycase.presentation.theme import androidx.compose.material.Typography import androidx.compose.ui.text.TextStyle diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/ui/theme/Color.kt b/app/src/main/java/com/ericampire/android/androidstudycase/ui/theme/Color.kt deleted file mode 100644 index 9aa9a9b..0000000 --- a/app/src/main/java/com/ericampire/android/androidstudycase/ui/theme/Color.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.ericampire.android.androidstudycase.ui.theme - -import androidx.compose.ui.graphics.Color - -val Purple200 = Color(0xFFBB86FC) -val Purple500 = Color(0xFF6200EE) -val Purple700 = Color(0xFF3700B3) -val Teal200 = Color(0xFF03DAC5) \ No newline at end of file diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/util/Destination.kt b/app/src/main/java/com/ericampire/android/androidstudycase/util/Destination.kt new file mode 100644 index 0000000..a953ca4 --- /dev/null +++ b/app/src/main/java/com/ericampire/android/androidstudycase/util/Destination.kt @@ -0,0 +1,10 @@ +package com.ericampire.android.androidstudycase.util + +import androidx.annotation.StringRes +import com.ericampire.android.androidstudycase.R + +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) +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index 4e0c74c..d604f28 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -32,6 +32,7 @@ object Libs { const val compose_ui_tooling = "androidx.compose.ui:ui-tooling:_" const val compose_ui_tooling_preview = "androidx.compose.ui:ui-tooling-preview:_" + const val androidx_lifecycle_viewmodel_ktx = "androidx.lifecycle:lifecycle-viewmodel-ktx:_" const val core_ktx = "androidx.core:core-ktx:_" const val lifecycle_runtime_ktx = "androidx.lifecycle:lifecycle-runtime-ktx:_" const val androidx_test_ext_junit = "androidx.test.ext:junit:_" @@ -131,6 +132,7 @@ object Libs { const val room_compiler = "androidx.room:room-compiler:_" const val room_runtime = "androidx.room:room-runtime:_" + const val room_ktx = "androidx.room:room-ktx:_" const val room_testing = "androidx.room:room-testing:_" const val koin_core = "io.insert-koin:koin-core:_" @@ -143,4 +145,6 @@ object Libs { const val orbit_mvi_viewmodel = "org.orbit-mvi:orbit-viewmodel:4.2.0" const val orbit_mvi_core = "org.orbit-mvi:orbit-core:4.2.0" const val orbit_mvi_test = "org.orbit-mvi:orbit-test:4.2.0" + + const val joda_time = "net.danlew:android.joda:2.10.9" } diff --git a/data/src/main/java/com/ericampire/android/androidstudycase/data/di/RepositoryModule.kt b/data/src/main/java/com/ericampire/android/androidstudycase/data/di/RepositoryModule.kt index 2953553..c2a6aad 100644 --- a/data/src/main/java/com/ericampire/android/androidstudycase/data/di/RepositoryModule.kt +++ b/data/src/main/java/com/ericampire/android/androidstudycase/data/di/RepositoryModule.kt @@ -1,6 +1,5 @@ package com.ericampire.android.androidstudycase.data.di -import com.ericampire.android.androidstudycase.data.datasource.animator.AnimatorDataSource import com.ericampire.android.androidstudycase.data.datasource.animator.LocalAnimatorDataSource import com.ericampire.android.androidstudycase.data.datasource.animator.RemoteAnimatorDataSource import com.ericampire.android.androidstudycase.data.datasource.blog.LocalBlogDataSource diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 5bfe32e..e1eddfb 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -3,7 +3,7 @@ import de.fayard.refreshVersions.core.versionFor plugins { id("com.android.library") id("kotlin-android") - kotlin("plugin.serialization") version "1.5.21" + kotlin("plugin.serialization") version "1.5.20" kotlin("kapt") } @@ -41,8 +41,7 @@ dependencies { api(Libs.ktor_client_core) api(Libs.ktor_serialization) - - api(Libs.room_runtime) + api(Libs.joda_time) testImplementation(Libs.junit_jupiter_api) testImplementation(Libs.junit_jupiter_engine) diff --git a/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Animator.kt b/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Animator.kt index 471a298..cd0fb26 100644 --- a/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Animator.kt +++ b/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Animator.kt @@ -10,8 +10,8 @@ import kotlinx.serialization.Serializable @Entity data class Animator( @PrimaryKey - val name: String = "", - val avatarUrl: String = "", + var name: String = "", + var avatarUrl: String = "", ) @Serializable diff --git a/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/BlogPage.kt b/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/BlogPage.kt index 43021d9..dea6a32 100644 --- a/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/BlogPage.kt +++ b/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/BlogPage.kt @@ -2,6 +2,9 @@ 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 @@ -9,29 +12,31 @@ import java.util.* @Serializable data class BlogPage( - val currentPage: Int, - val from: Int, - val perPage: Int, - val blogs: List, - val to: Int, - val total: Int, - val totalPages: Int + val currentPage: Int, + val from: Int, + val perPage: Int, + val blogs: List, + val to: Int, + val total: Int, + val totalPages: Int ) @Serializable data class BlogData( - @SerialName("blogs") val blogPage: BlogPage + @SerialName("blogs") val blogPage: BlogPage ) @Serializable data class BlogApiResponse( - @SerialName("data") val blogBlogData: BlogData + @SerialName("data") val blogBlogData: BlogData ) @Serializable @Entity data class Blog( - @Contextual val postedAt: Date, - @PrimaryKey val imageUrl: String, - val title: String +// @TypeConverters(DateConverter::class) +// @Serializable(with = DateSerializer::class) + var postedAt: String = "", + @PrimaryKey val imageUrl: String = "", + var title: String ) \ No newline at end of file diff --git a/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Lottiefile.kt b/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Lottiefile.kt index 21ab7f8..3f4635b 100644 --- a/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Lottiefile.kt +++ b/domain/src/main/java/com/ericampire/android/androidstudycase/domain/entity/Lottiefile.kt @@ -1,14 +1,14 @@ package com.ericampire.android.androidstudycase.domain.entity -import androidx.room.Entity -import androidx.room.PrimaryKey +import androidx.room.* +import com.ericampire.android.androidstudycase.domain.util.DateSerializer +import com.ericampire.android.androidstudycase.util.room.DateConverter import kotlinx.serialization.Contextual import kotlinx.serialization.ExperimentalSerializationApi import java.util.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonNames -import java.util.* @Serializable data class LottieFilesApiResponse( @@ -16,10 +16,9 @@ data class LottieFilesApiResponse( val lottieFilesLottieFilesData: LottieFilesData ) -@ExperimentalSerializationApi @Serializable data class LottieFilesData( - @JsonNames("recent", "popular") + @SerialName("recent") val page: LottieFilesPage ) @@ -39,15 +38,17 @@ data class LottieFilesPage( @Serializable @Entity class Lottiefile( - @PrimaryKey val id: Long, - val bgColor: String, - val lottieUrl: String, - val gifUrl: String, - val videoUrl: String, - val imageUrl: String, - val name: String, - @Contextual val createdAt: Date, - val createdBy: Animator + @PrimaryKey var id: Long? = null, + var bgColor: String = "", + var lottieUrl: String = "", + var gifUrl: String = "", + var videoUrl: String = "", + var imageUrl: String = "", + var name: String = "", +// @TypeConverters(DateConverter::class) +// @Serializable(with = DateSerializer::class) + var createdAt: String = "", + @Ignore var createdBy: Animator? = null ) diff --git a/domain/src/main/java/com/ericampire/android/androidstudycase/domain/util/DataSerializer.kt b/domain/src/main/java/com/ericampire/android/androidstudycase/domain/util/DataSerializer.kt new file mode 100644 index 0000000..93bd6f5 --- /dev/null +++ b/domain/src/main/java/com/ericampire/android/androidstudycase/domain/util/DataSerializer.kt @@ -0,0 +1,23 @@ +package com.ericampire.android.androidstudycase.domain.util + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import org.joda.time.DateTime +import java.util.* + +object DateSerializer : KSerializer { + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Date) { + encoder.encodeString(DateTime(value).toString()) + } + + override fun deserialize(decoder: Decoder): Date { + return DateTime(decoder.decodeString()).toDate() + } +} \ No newline at end of file diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 26e041a..dc14493 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -2,6 +2,10 @@ Lottiefile App - - + Home + Explore + Preview + Featured + Recent + Popular \ No newline at end of file diff --git a/util/build.gradle.kts b/util/build.gradle.kts index cbe6396..e82a728 100644 --- a/util/build.gradle.kts +++ b/util/build.gradle.kts @@ -32,10 +32,16 @@ dependencies { api(project(":i18n")) implementation(Libs.core_ktx) + api(Libs.androidx_lifecycle_viewmodel_ktx) + + api(Libs.room_runtime) + api(Libs.room_ktx) api(platform(Libs.kotlin_coroutine_bom)) api(Libs.kotlin_coroutine_core) + api(Libs.orbit_mvi_core) + testImplementation(Libs.junit_jupiter_api) testImplementation(Libs.junit_jupiter_engine) diff --git a/util/src/main/java/com/ericampire/android/androidstudycase/util/mvi/BaseViewModel.kt b/util/src/main/java/com/ericampire/android/androidstudycase/util/mvi/BaseViewModel.kt new file mode 100644 index 0000000..72ee087 --- /dev/null +++ b/util/src/main/java/com/ericampire/android/androidstudycase/util/mvi/BaseViewModel.kt @@ -0,0 +1,17 @@ +package com.ericampire.android.androidstudycase.util.mvi + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import org.orbitmvi.orbit.ContainerHost + +abstract class BaseViewModel : ContainerHost, ViewModel() { + protected val pendingAction = MutableSharedFlow() + + protected fun submitAction(action: A) { + viewModelScope.launch { + pendingAction.emit(action) + } + } +} \ No newline at end of file diff --git a/util/src/main/java/com/ericampire/android/androidstudycase/util/room/DateConverter.kt b/util/src/main/java/com/ericampire/android/androidstudycase/util/room/DateConverter.kt new file mode 100644 index 0000000..3c52b2b --- /dev/null +++ b/util/src/main/java/com/ericampire/android/androidstudycase/util/room/DateConverter.kt @@ -0,0 +1,18 @@ +package com.ericampire.android.androidstudycase.util.room + +import androidx.room.TypeConverter +import java.util.* + + +object DateConverter { + + @TypeConverter + fun fromTimestamp(value: Long): Date { + return Date(value) + } + + @TypeConverter + fun dateToTimestamp(date: Date): Long { + return date.time + } +} \ No newline at end of file diff --git a/util/src/main/java/com/ericampire/android/androidstudycase/util/usecase/FlowUseCase.kt b/util/src/main/java/com/ericampire/android/androidstudycase/util/usecase/FlowUseCase.kt index 5aedac4..8b97919 100644 --- a/util/src/main/java/com/ericampire/android/androidstudycase/util/usecase/FlowUseCase.kt +++ b/util/src/main/java/com/ericampire/android/androidstudycase/util/usecase/FlowUseCase.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart /** * Executes business logic in its execute method and keep posting updates to the result as @@ -13,6 +14,7 @@ import kotlinx.coroutines.flow.flowOn */ abstract class FlowUseCase(private val coroutineDispatcher: CoroutineDispatcher) { operator fun invoke(parameters: P): Flow> = execute(parameters) + .onStart { emit(Result.Loading) } .catch { e -> emit(Result.Error(Exception(e))) } .flowOn(coroutineDispatcher)