Setup Presentation Module
This commit is contained in:
@@ -8,31 +8,20 @@ import androidx.compose.material.Surface
|
|||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
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() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
AndroidStudyCaseTheme {
|
AndroidStudyCaseTheme(darkTheme = true) {
|
||||||
// A surface container using the 'background' color from the theme
|
|
||||||
Surface(color = MaterialTheme.colors.background) {
|
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
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.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.Module
|
||||||
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.components.ViewModelComponent
|
import dagger.hilt.android.components.ViewModelComponent
|
||||||
|
|
||||||
@@ -8,4 +13,35 @@ import dagger.hilt.android.components.ViewModelComponent
|
|||||||
@InstallIn(ViewModelComponent::class)
|
@InstallIn(ViewModelComponent::class)
|
||||||
object ViewModelModule {
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+7
@@ -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
|
||||||
|
}
|
||||||
+7
@@ -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
|
||||||
|
}
|
||||||
+79
@@ -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<ExploreViewState, ExploreEffect, ExploreAction>() {
|
||||||
|
|
||||||
|
override val container: Container<ExploreViewState, ExploreEffect>
|
||||||
|
get() = container(initialState = ExploreViewState())
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
pendingAction.collectLatest { action ->
|
||||||
|
when(action) {
|
||||||
|
ExploreAction.FindFeaturedFile -> findFeaturedFile()
|
||||||
|
ExploreAction.FindPopularFile -> findPopularFile()
|
||||||
|
ExploreAction.FindRecentFile -> findRecentFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Flow<Result<List<Lottiefile>>>.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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -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<Lottiefile> = emptyList()
|
||||||
|
)
|
||||||
+13
@@ -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
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
package com.ericampire.android.androidstudycase.presentation.screen.home.business
|
||||||
|
|
||||||
|
sealed interface HomeAction
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
package com.ericampire.android.androidstudycase.presentation.screen.home.business
|
||||||
|
|
||||||
|
sealed interface HomeEffect
|
||||||
+30
@@ -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<HomeViewState, HomeEffect, HomeAction>() {
|
||||||
|
|
||||||
|
override val container: Container<HomeViewState, HomeEffect>
|
||||||
|
get() = container(initialState = HomeViewState())
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
pendingAction.collectLatest { action ->
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -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<Blog> = emptyList()
|
||||||
|
)
|
||||||
+13
@@ -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
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
+92
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
package com.ericampire.android.androidstudycase.presentation.screen.preview.business
|
||||||
|
|
||||||
|
sealed interface PreviewAction
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
package com.ericampire.android.androidstudycase.presentation.screen.preview.business
|
||||||
|
|
||||||
|
sealed interface PreviewEffect
|
||||||
+27
@@ -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<PreviewViewState, PreviewEffect, PreviewAction>() {
|
||||||
|
|
||||||
|
override val container: Container<PreviewViewState, PreviewEffect>
|
||||||
|
get() = container(initialState = PreviewViewState())
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
pendingAction.collectLatest { action ->
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -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
|
||||||
|
)
|
||||||
+13
@@ -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
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
+1
-1
@@ -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.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.Shapes
|
import androidx.compose.material.Shapes
|
||||||
+7
-7
@@ -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.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
@@ -8,15 +8,15 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
private val DarkColorPalette = darkColors(
|
private val DarkColorPalette = darkColors(
|
||||||
primary = Purple200,
|
primary = AppColor.Purple200,
|
||||||
primaryVariant = Purple700,
|
primaryVariant = AppColor.Purple700,
|
||||||
secondary = Teal200
|
secondary = AppColor.Teal200
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorPalette = lightColors(
|
private val LightColorPalette = lightColors(
|
||||||
primary = Purple500,
|
primary = AppColor.Purple500,
|
||||||
primaryVariant = Purple700,
|
primaryVariant = AppColor.Purple700,
|
||||||
secondary = Teal200,
|
secondary = AppColor.Teal200,
|
||||||
background = Color.White,
|
background = Color.White,
|
||||||
surface = Color.White,
|
surface = Color.White,
|
||||||
onPrimary = Color.White,
|
onPrimary = Color.White,
|
||||||
+1
-1
@@ -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.material.Typography
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ object Libs {
|
|||||||
const val compose_ui_tooling = "androidx.compose.ui:ui-tooling:_"
|
const val compose_ui_tooling = "androidx.compose.ui:ui-tooling:_"
|
||||||
const val compose_ui_tooling_preview = "androidx.compose.ui:ui-tooling-preview:_"
|
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 core_ktx = "androidx.core:core-ktx:_"
|
||||||
const val lifecycle_runtime_ktx = "androidx.lifecycle:lifecycle-runtime-ktx:_"
|
const val lifecycle_runtime_ktx = "androidx.lifecycle:lifecycle-runtime-ktx:_"
|
||||||
const val androidx_test_ext_junit = "androidx.test.ext:junit:_"
|
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_compiler = "androidx.room:room-compiler:_"
|
||||||
const val room_runtime = "androidx.room:room-runtime:_"
|
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 room_testing = "androidx.room:room-testing:_"
|
||||||
|
|
||||||
const val koin_core = "io.insert-koin:koin-core:_"
|
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_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_core = "org.orbit-mvi:orbit-core:4.2.0"
|
||||||
const val orbit_mvi_test = "org.orbit-mvi:orbit-test: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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.ericampire.android.androidstudycase.data.di
|
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.LocalAnimatorDataSource
|
||||||
import com.ericampire.android.androidstudycase.data.datasource.animator.RemoteAnimatorDataSource
|
import com.ericampire.android.androidstudycase.data.datasource.animator.RemoteAnimatorDataSource
|
||||||
import com.ericampire.android.androidstudycase.data.datasource.blog.LocalBlogDataSource
|
import com.ericampire.android.androidstudycase.data.datasource.blog.LocalBlogDataSource
|
||||||
|
|||||||
@@ -3,7 +3,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.21"
|
kotlin("plugin.serialization") version "1.5.20"
|
||||||
kotlin("kapt")
|
kotlin("kapt")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,8 +41,7 @@ dependencies {
|
|||||||
|
|
||||||
api(Libs.ktor_client_core)
|
api(Libs.ktor_client_core)
|
||||||
api(Libs.ktor_serialization)
|
api(Libs.ktor_serialization)
|
||||||
|
api(Libs.joda_time)
|
||||||
api(Libs.room_runtime)
|
|
||||||
|
|
||||||
testImplementation(Libs.junit_jupiter_api)
|
testImplementation(Libs.junit_jupiter_api)
|
||||||
testImplementation(Libs.junit_jupiter_engine)
|
testImplementation(Libs.junit_jupiter_engine)
|
||||||
|
|||||||
+2
-2
@@ -10,8 +10,8 @@ import kotlinx.serialization.Serializable
|
|||||||
@Entity
|
@Entity
|
||||||
data class Animator(
|
data class Animator(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val name: String = "",
|
var name: String = "",
|
||||||
val avatarUrl: String = "",
|
var avatarUrl: String = "",
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
+17
-12
@@ -2,6 +2,9 @@ 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.Contextual
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -9,29 +12,31 @@ 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 blogs: List<Blog>,
|
||||||
val to: Int,
|
val to: Int,
|
||||||
val total: Int,
|
val total: Int,
|
||||||
val totalPages: Int
|
val totalPages: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BlogData(
|
data class BlogData(
|
||||||
@SerialName("blogs") val blogPage: BlogPage
|
@SerialName("blogs") val blogPage: BlogPage
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BlogApiResponse(
|
data class BlogApiResponse(
|
||||||
@SerialName("data") val blogBlogData: BlogData
|
@SerialName("data") val blogBlogData: BlogData
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@Entity
|
@Entity
|
||||||
data class Blog(
|
data class Blog(
|
||||||
@Contextual val postedAt: Date,
|
// @TypeConverters(DateConverter::class)
|
||||||
@PrimaryKey val imageUrl: String,
|
// @Serializable(with = DateSerializer::class)
|
||||||
val title: String
|
var postedAt: String = "",
|
||||||
|
@PrimaryKey val imageUrl: String = "",
|
||||||
|
var title: String
|
||||||
)
|
)
|
||||||
+15
-14
@@ -1,14 +1,14 @@
|
|||||||
package com.ericampire.android.androidstudycase.domain.entity
|
package com.ericampire.android.androidstudycase.domain.entity
|
||||||
|
|
||||||
import androidx.room.Entity
|
import androidx.room.*
|
||||||
import androidx.room.PrimaryKey
|
import com.ericampire.android.androidstudycase.domain.util.DateSerializer
|
||||||
|
import com.ericampire.android.androidstudycase.util.room.DateConverter
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonNames
|
import kotlinx.serialization.json.JsonNames
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LottieFilesApiResponse(
|
data class LottieFilesApiResponse(
|
||||||
@@ -16,10 +16,9 @@ data class LottieFilesApiResponse(
|
|||||||
val lottieFilesLottieFilesData: LottieFilesData
|
val lottieFilesLottieFilesData: LottieFilesData
|
||||||
)
|
)
|
||||||
|
|
||||||
@ExperimentalSerializationApi
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LottieFilesData(
|
data class LottieFilesData(
|
||||||
@JsonNames("recent", "popular")
|
@SerialName("recent")
|
||||||
val page: LottieFilesPage
|
val page: LottieFilesPage
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,15 +38,17 @@ data class LottieFilesPage(
|
|||||||
@Serializable
|
@Serializable
|
||||||
@Entity
|
@Entity
|
||||||
class Lottiefile(
|
class Lottiefile(
|
||||||
@PrimaryKey val id: Long,
|
@PrimaryKey var id: Long? = null,
|
||||||
val bgColor: String,
|
var bgColor: String = "",
|
||||||
val lottieUrl: String,
|
var lottieUrl: String = "",
|
||||||
val gifUrl: String,
|
var gifUrl: String = "",
|
||||||
val videoUrl: String,
|
var videoUrl: String = "",
|
||||||
val imageUrl: String,
|
var imageUrl: String = "",
|
||||||
val name: String,
|
var name: String = "",
|
||||||
@Contextual val createdAt: Date,
|
// @TypeConverters(DateConverter::class)
|
||||||
val createdBy: Animator
|
// @Serializable(with = DateSerializer::class)
|
||||||
|
var createdAt: String = "",
|
||||||
|
@Ignore var createdBy: Animator? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+23
@@ -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<Date> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<string name="app_name">Lottiefile App</string>
|
<string name="app_name">Lottiefile App</string>
|
||||||
|
<string name="txt_home">Home</string>
|
||||||
|
<string name="txt_explore">Explore</string>
|
||||||
|
<string name="txt_preview">Preview</string>
|
||||||
|
<string name="txt_featured">Featured</string>
|
||||||
|
<string name="txt_recent">Recent</string>
|
||||||
|
<string name="txt_popular">Popular</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -32,10 +32,16 @@ dependencies {
|
|||||||
|
|
||||||
api(project(":i18n"))
|
api(project(":i18n"))
|
||||||
implementation(Libs.core_ktx)
|
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(platform(Libs.kotlin_coroutine_bom))
|
||||||
api(Libs.kotlin_coroutine_core)
|
api(Libs.kotlin_coroutine_core)
|
||||||
|
|
||||||
|
api(Libs.orbit_mvi_core)
|
||||||
|
|
||||||
testImplementation(Libs.junit_jupiter_api)
|
testImplementation(Libs.junit_jupiter_api)
|
||||||
testImplementation(Libs.junit_jupiter_engine)
|
testImplementation(Libs.junit_jupiter_engine)
|
||||||
|
|
||||||
|
|||||||
@@ -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<S: Any, E: Any, A> : ContainerHost<S, E>, ViewModel() {
|
||||||
|
protected val pendingAction = MutableSharedFlow<A>()
|
||||||
|
|
||||||
|
protected fun submitAction(action: A) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
pendingAction.emit(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import kotlinx.coroutines.CoroutineDispatcher
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.flowOn
|
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
|
* 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<in P, R>(private val coroutineDispatcher: CoroutineDispatcher) {
|
abstract class FlowUseCase<in P, R>(private val coroutineDispatcher: CoroutineDispatcher) {
|
||||||
operator fun invoke(parameters: P): Flow<Result<R>> = execute(parameters)
|
operator fun invoke(parameters: P): Flow<Result<R>> = execute(parameters)
|
||||||
|
.onStart { emit(Result.Loading) }
|
||||||
.catch { e -> emit(Result.Error(Exception(e))) }
|
.catch { e -> emit(Result.Error(Exception(e))) }
|
||||||
.flowOn(coroutineDispatcher)
|
.flowOn(coroutineDispatcher)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user