Setup Presentation Module

This commit is contained in:
2021-09-04 15:36:05 +02:00
parent e532ae454c
commit c00c3db51c
39 changed files with 602 additions and 68 deletions
@@ -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")
}
}
@@ -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
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
)
}
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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()
}
}
}
@@ -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()
)
@@ -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
) {
}
@@ -0,0 +1,3 @@
package com.ericampire.android.androidstudycase.presentation.screen.home.business
sealed interface HomeAction
@@ -0,0 +1,3 @@
package com.ericampire.android.androidstudycase.presentation.screen.home.business
sealed interface HomeEffect
@@ -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 ->
}
}
}
}
@@ -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()
)
@@ -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
) {
}
@@ -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
}
}
@@ -0,0 +1,3 @@
package com.ericampire.android.androidstudycase.presentation.screen.preview.business
sealed interface PreviewAction
@@ -0,0 +1,3 @@
package com.ericampire.android.androidstudycase.presentation.screen.preview.business
sealed interface PreviewEffect
@@ -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 ->
}
}
}
}
@@ -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
)
@@ -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,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
@@ -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,
@@ -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
@@ -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)
}