├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml └── vcs.xml ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── dev │ │ └── roshana │ │ └── jetpackcomposebasearch │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── dev │ │ │ └── roshana │ │ │ └── jetpackcomposebasearch │ │ │ ├── BaseContent.kt │ │ │ ├── MainActivity.kt │ │ │ ├── app │ │ │ └── JetpackComposeBaseApplication.kt │ │ │ ├── di │ │ │ └── AppModule.kt │ │ │ └── navigation │ │ │ ├── BottomNavigationBar.kt │ │ │ ├── MainNavigation.kt │ │ │ └── NavigationItem.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_baseline_add_location.xml │ │ ├── ic_baseline_settings.xml │ │ ├── ic_launcher_background.xml │ │ └── logo.png │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── dev │ └── roshana │ └── jetpackcomposebasearch │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── buildSrc ├── .gitignore ├── build.gradle.kts ├── setting.gradle.kts └── src │ └── main │ └── java │ └── dev │ └── roshana │ └── buildsrc │ ├── BuildPlugins.kt │ ├── BuildTypes.kt │ ├── ConfigData.kt │ ├── Dependencies.kt │ └── Versions.kt ├── data ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── dev │ │ └── roshana │ │ └── data │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── dev │ │ └── roshana │ │ └── data │ │ ├── db │ │ ├── AppDatabase.kt │ │ ├── ArticleDao.kt │ │ ├── OperatorTypeConverters.kt │ │ └── di │ │ │ └── DatabaseModule.kt │ │ ├── network │ │ ├── api │ │ │ └── ApiService.kt │ │ ├── di │ │ │ └── NetworkModule.kt │ │ ├── models │ │ │ └── dto │ │ │ │ ├── ArticleDto.kt │ │ │ │ └── SourceDto.kt │ │ └── utils │ │ │ ├── ConnectivityInterceptor.kt │ │ │ └── Constants.kt │ │ ├── preference │ │ └── Preference.kt │ │ └── repository │ │ ├── ArticleRepositoryImpl.kt │ │ ├── DataStoreRepositoryImpl.kt │ │ ├── base │ │ └── BaseRepository.kt │ │ ├── di │ │ └── RepositoryModule.kt │ │ ├── pagingSource │ │ └── ArticlePagingSource.kt │ │ └── utils │ │ └── Utils.kt │ └── test │ └── java │ └── dev │ └── roshana │ └── data │ └── ExampleUnitTest.kt ├── domain ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── dev │ │ └── roshana │ │ └── domain │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── dev │ │ │ └── roshana │ │ │ └── domain │ │ │ ├── di │ │ │ └── UseCaseModule.kt │ │ │ ├── models │ │ │ ├── DataState.kt │ │ │ ├── ResponseWrapper.kt │ │ │ ├── article │ │ │ │ └── Article.kt │ │ │ └── dialogs │ │ │ │ └── Item.kt │ │ │ ├── repositories │ │ │ ├── ArticleRepository.kt │ │ │ └── DataStoreRepository.kt │ │ │ ├── usecases │ │ │ ├── ArticleUseCase.kt │ │ │ └── DataStoreUseCase.kt │ │ │ └── utils │ │ │ ├── ConfigHelper.kt │ │ │ └── Headers.kt │ └── res │ │ ├── drawable │ │ ├── first.png │ │ ├── food1.jpg │ │ ├── food10.jpg │ │ ├── food11.webp │ │ ├── food12.webp │ │ ├── food13.webp │ │ ├── food14.jpg │ │ ├── food15.webp │ │ ├── food16.jpg │ │ ├── food2.jpg │ │ ├── food3.jpg │ │ ├── food4.jpg │ │ ├── food5.webp │ │ ├── food6.jpg │ │ ├── food7.webp │ │ ├── food8.webp │ │ ├── food9.jpg │ │ ├── second.png │ │ └── third.png │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── dev │ └── roshana │ └── domain │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── presentation ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── dev │ │ └── roshana │ │ └── presentation │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── dev │ │ └── roshana │ │ └── presentation │ │ ├── articleUi │ │ ├── ArticleListColumn.kt │ │ ├── ArticleListState.kt │ │ ├── ArticleListViewModel.kt │ │ ├── ArticleUi.kt │ │ ├── ArticlesListScreen.kt │ │ └── articleNavigationGraph.kt │ │ ├── locationUi │ │ ├── LocationListScreen.kt │ │ └── locationNavigationGraph.kt │ │ ├── navigation │ │ ├── NavigationDestinations.kt │ │ └── Screens.kt │ │ ├── uiComponent │ │ ├── dialogs │ │ │ ├── DialogState.kt │ │ │ ├── DialogType.kt │ │ │ └── Dialogs.kt │ │ ├── progressbar │ │ │ └── DialogCircularProgressBar.kt │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ └── welcomeUi │ │ ├── OnBoardingPage.kt │ │ ├── WelcomeScreen.kt │ │ ├── WelcomeState.kt │ │ └── WelcomeViewModel.kt │ └── test │ └── java │ └── dev │ └── roshana │ └── presentation │ └── ExampleUnitTest.kt └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 34 | 35 | 36 | 37 | 38 | 39 | 41 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import dev.roshana.buildsrc.BuildTypes 2 | import dev.roshana.buildsrc.ConfigData 3 | import dev.roshana.buildsrc.Dependencies 4 | 5 | plugins { 6 | id("com.android.application") 7 | id("org.jetbrains.kotlin.android") 8 | id("kotlin-android") 9 | id("dagger.hilt.android.plugin") 10 | id("kotlin-kapt") 11 | } 12 | 13 | android { 14 | compileSdk = ConfigData.compileSdkVersion 15 | 16 | defaultConfig { 17 | applicationId = ConfigData.applicationId 18 | minSdk = ConfigData.minSdkVersion 19 | targetSdk = ConfigData.targetSdkVersion 20 | versionCode = ConfigData.versionCode 21 | versionName = ConfigData.versionName 22 | 23 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 24 | vectorDrawables { 25 | useSupportLibrary = true 26 | } 27 | } 28 | 29 | buildFeatures { 30 | dataBinding = true 31 | viewBinding = true 32 | } 33 | 34 | buildTypes { 35 | getByName(BuildTypes.RELEASE) { 36 | isMinifyEnabled = false 37 | proguardFiles( 38 | getDefaultProguardFile("proguard-android-optimize.txt"), 39 | "proguard-rules.pro" 40 | ) 41 | } 42 | } 43 | compileOptions { 44 | sourceCompatibility = JavaVersion.VERSION_1_8 45 | targetCompatibility = JavaVersion.VERSION_1_8 46 | } 47 | kotlinOptions { 48 | jvmTarget = JavaVersion.VERSION_1_8.toString() 49 | } 50 | buildFeatures { 51 | compose = true 52 | } 53 | composeOptions { 54 | kotlinCompilerExtensionVersion = rootProject.extra["compose_version"] as String 55 | } 56 | packagingOptions { 57 | resources { 58 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 59 | } 60 | } 61 | } 62 | 63 | dependencies { 64 | 65 | dependencies { 66 | implementation(project(":domain")) 67 | implementation(project(":presentation")) 68 | 69 | implementation(Dependencies.ComposeLibs.coreKtx) 70 | 71 | /** compose 72 | **/ 73 | implementation(Dependencies.ComposeLibs.composeUi) 74 | implementation(Dependencies.ComposeLibs.composeMaterial) 75 | implementation(Dependencies.ComposeLibs.composeUiPreview) 76 | implementation(Dependencies.ComposeLibs.androidxLifeCycle) 77 | implementation(Dependencies.ComposeLibs.composeActivity) 78 | implementation(Dependencies.ComposeLibs.composeNav) 79 | implementation(Dependencies.ComposeLibs.composeHilt) 80 | implementation(Dependencies.ComposeLibs.composePaging) 81 | implementation(Dependencies.ComposeLibs.composeAnimNavigation) 82 | implementation(Dependencies.ComposeLibs.coreSplash) 83 | 84 | 85 | 86 | /** hilt - dagger 87 | **/ 88 | implementation(Dependencies.CommonLibs.daggerAndroid) 89 | kapt(Dependencies.CommonLibs.daggerCompiler) 90 | kapt(Dependencies.CommonLibs.daggerAndroidCompiler) 91 | implementation(Dependencies.CommonLibs.hilt) 92 | //implementation(Dependencies.CommonLibs.hiltViewModel) 93 | kapt(Dependencies.CommonLibs.hiltCompiler) 94 | kapt(Dependencies.CommonLibs.hiltViewModelKapt) 95 | 96 | /** testing 97 | **/ 98 | testImplementation(Dependencies.TestLibs.junit) 99 | androidTestImplementation(Dependencies.TestLibs.extJunit) 100 | androidTestImplementation(Dependencies.TestLibs.espresso) 101 | androidTestImplementation(Dependencies.TestLibs.composeUiJunit) 102 | debugImplementation(Dependencies.TestLibs.composeUiTooling) 103 | debugImplementation(Dependencies.TestLibs.composeUiTestManifest) 104 | } 105 | 106 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/dev/roshana/jetpackcomposebasearch/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.jetpackcomposebasearch 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("dev.roshana.jetpackcomposebasearch", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/dev/roshana/jetpackcomposebasearch/BaseContent.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.jetpackcomposebasearch 2 | 3 | import androidx.compose.animation.ExperimentalAnimationApi 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material.ExperimentalMaterialApi 6 | import androidx.compose.material.Scaffold 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.hilt.navigation.compose.hiltViewModel 10 | import androidx.navigation.compose.currentBackStackEntryAsState 11 | import com.google.accompanist.navigation.animation.rememberAnimatedNavController 12 | import dev.roshana.jetpackcomposebasearch.navigation.BottomNavigationBar 13 | import dev.roshana.jetpackcomposebasearch.navigation.MainNavigation 14 | import dev.roshana.presentation.navigation.Articles 15 | import dev.roshana.presentation.navigation.Locations 16 | import dev.roshana.presentation.welcomeUi.WelcomeViewModel 17 | 18 | /** PariSa; 19 | coding and smoking ;) 20 | **/ 21 | 22 | @ExperimentalMaterialApi 23 | @ExperimentalAnimationApi 24 | @Composable 25 | fun BaseContent() { 26 | val navController = rememberAnimatedNavController() 27 | val bottomScreens = listOf( 28 | Articles.ARTICLESLIST, 29 | Locations.LOCATIONLIST 30 | ) 31 | val showNavBar = navController 32 | .currentBackStackEntryAsState().value?.destination?.route in bottomScreens 33 | 34 | val welcomeViewModel: WelcomeViewModel = hiltViewModel() 35 | val startScreen = welcomeViewModel.startDestination 36 | 37 | 38 | Scaffold( 39 | bottomBar = { 40 | if (showNavBar) { 41 | BottomNavigationBar(navController) 42 | } 43 | } 44 | ) { padding -> 45 | 46 | MainNavigation( 47 | navHostController = navController, 48 | modifier = Modifier.padding(padding), 49 | startDestination = startScreen.value 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/dev/roshana/jetpackcomposebasearch/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.jetpackcomposebasearch 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.animation.ExperimentalAnimationApi 7 | import androidx.compose.material.ExperimentalMaterialApi 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.tooling.preview.Preview 11 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 12 | import androidx.hilt.navigation.compose.hiltViewModel 13 | import androidx.paging.compose.collectAsLazyPagingItems 14 | import dagger.hilt.android.AndroidEntryPoint 15 | import dev.roshana.presentation.articleUi.ArticleListViewModel 16 | import dev.roshana.presentation.uiComponent.ui.theme.JetpackComposeBaseArchTheme 17 | import dev.roshana.presentation.welcomeUi.WelcomeViewModel 18 | import javax.inject.Inject 19 | 20 | /** PariSa; 21 | coding and smoking ;) 22 | **/ 23 | 24 | @AndroidEntryPoint 25 | class MainActivity : ComponentActivity() { 26 | 27 | @OptIn( 28 | ExperimentalMaterialApi::class, 29 | ExperimentalAnimationApi::class 30 | ) 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | 34 | setContent { 35 | JetpackComposeBaseArchTheme { 36 | BaseContent() 37 | } 38 | } 39 | } 40 | } 41 | 42 | @Composable 43 | fun Greeting(name: String) { 44 | val viewModel: ArticleListViewModel = hiltViewModel() 45 | val state = viewModel.articleListState.value 46 | val list = state.dataList?.collectAsLazyPagingItems() 47 | val size = list?.itemSnapshotList?.items 48 | Text(text = "Hello ${size}!") 49 | } 50 | 51 | @Preview(showBackground = true) 52 | @Composable 53 | fun DefaultPreview() { 54 | JetpackComposeBaseArchTheme { 55 | Greeting("Android") 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/dev/roshana/jetpackcomposebasearch/app/JetpackComposeBaseApplication.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.jetpackcomposebasearch.app 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | import dev.roshana.domain.utils.initPreferenceUtils 6 | 7 | /** PariSa; 8 | coding and smoking ;) 9 | **/ 10 | 11 | @HiltAndroidApp 12 | class JetpackComposeBaseApplication : Application(){ 13 | override fun onCreate() { 14 | super.onCreate() 15 | initPreferenceUtils(applicationContext) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/dev/roshana/jetpackcomposebasearch/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.jetpackcomposebasearch.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.android.qualifiers.ApplicationContext 8 | import dagger.hilt.components.SingletonComponent 9 | import javax.inject.Singleton 10 | 11 | /** PariSa; 12 | coding and smoking ;) 13 | **/ 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | class AppModule() { 18 | 19 | @Provides 20 | @Singleton 21 | fun providesContext(@ApplicationContext context: Context) = context.applicationContext 22 | } -------------------------------------------------------------------------------- /app/src/main/java/dev/roshana/jetpackcomposebasearch/navigation/BottomNavigationBar.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.jetpackcomposebasearch.navigation 2 | 3 | import androidx.compose.animation.ExperimentalAnimationApi 4 | import androidx.compose.animation.core.Spring 5 | import androidx.compose.animation.core.animateDpAsState 6 | import androidx.compose.animation.core.animateFloatAsState 7 | import androidx.compose.animation.core.spring 8 | import androidx.compose.foundation.background 9 | import androidx.compose.foundation.clickable 10 | import androidx.compose.foundation.layout.* 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material.Card 13 | import androidx.compose.material.Icon 14 | import androidx.compose.material.MaterialTheme 15 | import androidx.compose.material.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.draw.alpha 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.res.painterResource 23 | import androidx.compose.ui.unit.dp 24 | import androidx.navigation.NavGraph.Companion.findStartDestination 25 | import androidx.navigation.NavHostController 26 | import androidx.navigation.compose.currentBackStackEntryAsState 27 | 28 | /** PariSa; 29 | coding and smoking ;) 30 | **/ 31 | 32 | @OptIn(ExperimentalAnimationApi::class) 33 | @Composable 34 | fun BottomNavigationBar(navHostController: NavHostController) { 35 | 36 | val bottomScreens = listOf( 37 | NavigationItem.ArticlesScreen, 38 | NavigationItem.LocationsScreen, 39 | ) 40 | 41 | Card( 42 | modifier = Modifier 43 | .fillMaxWidth() 44 | .height(80.dp), 45 | backgroundColor = Color.LightGray 46 | 47 | ) { 48 | Row( 49 | modifier = Modifier.fillMaxSize(), 50 | horizontalArrangement = Arrangement.SpaceAround, 51 | verticalAlignment = Alignment.CenterVertically 52 | ) { 53 | 54 | bottomScreens.map { 55 | val isSelected = navHostController 56 | .currentBackStackEntryAsState().value?.destination?.route == it.screen_route 57 | val animatedWeight by animateFloatAsState(targetValue = if (isSelected) 1.5f else 1f) 58 | 59 | CustomBottomNavItem( 60 | screen = it, 61 | isSelected = isSelected, 62 | modifier = Modifier 63 | .weight(animatedWeight) 64 | .clickable { 65 | navHostController.navigate( 66 | it.screen_route 67 | ) { 68 | restoreState = true 69 | launchSingleTop = true 70 | 71 | val destination = navHostController.graph.findStartDestination().id 72 | popUpTo(destination) { saveState = true } 73 | } 74 | } 75 | ) 76 | } 77 | 78 | } 79 | } 80 | } 81 | 82 | @ExperimentalAnimationApi 83 | @Composable 84 | private fun CustomBottomNavItem( 85 | modifier: Modifier = Modifier, 86 | screen: NavigationItem, 87 | isSelected: Boolean, 88 | ) { 89 | val animatedIconSize by animateDpAsState( 90 | targetValue = if (isSelected) 30.dp else 25.dp, 91 | animationSpec = spring( 92 | stiffness = Spring.StiffnessLow, 93 | dampingRatio = Spring.DampingRatioMediumBouncy 94 | ) 95 | ) 96 | 97 | Box( 98 | modifier = modifier, 99 | contentAlignment = Alignment.Center, 100 | ) { 101 | Row( 102 | modifier = Modifier 103 | .height(if (isSelected) 50.dp else 50.dp) 104 | .background( 105 | color = MaterialTheme.colors.onBackground, 106 | shape = RoundedCornerShape(5.dp) 107 | ) 108 | .padding(horizontal = 5.dp, vertical = 10.dp), 109 | verticalAlignment = Alignment.CenterVertically, 110 | horizontalArrangement = Arrangement.Center, 111 | ) { 112 | Icon( 113 | painter = painterResource(id = screen.icon), contentDescription = "bottom Bar Icon", 114 | 115 | modifier = Modifier 116 | .align(Alignment.CenterVertically) 117 | .fillMaxHeight() 118 | .padding(horizontal = 5.dp) 119 | .alpha(if (isSelected) 1f else .7f) 120 | .size(animatedIconSize), 121 | tint = if (isSelected) MaterialTheme.colors.primary else Color.Gray 122 | ) 123 | 124 | if (isSelected) { 125 | Text( 126 | text = screen.title, 127 | modifier = Modifier.padding(start = 8.dp, end = 10.dp), 128 | maxLines = 1, 129 | color = MaterialTheme.colors.primary 130 | ) 131 | } 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /app/src/main/java/dev/roshana/jetpackcomposebasearch/navigation/MainNavigation.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.jetpackcomposebasearch.navigation 2 | 3 | import androidx.compose.animation.ExperimentalAnimationApi 4 | import androidx.compose.material.ExperimentalMaterialApi 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.navigation.NavHostController 8 | import androidx.navigation.compose.NavHost 9 | import androidx.navigation.compose.composable 10 | import dev.roshana.presentation.articleUi.ArticlesListScreen 11 | import dev.roshana.presentation.locationUi.LocationScreen 12 | import dev.roshana.presentation.navigation.Screens 13 | import dev.roshana.presentation.welcomeUi.WelcomeScreen 14 | import kotlinx.coroutines.ExperimentalCoroutinesApi 15 | 16 | /** PariSa; 17 | coding and smoking ;) 18 | **/ 19 | 20 | @OptIn(ExperimentalCoroutinesApi::class) 21 | @ExperimentalMaterialApi 22 | @ExperimentalAnimationApi 23 | @Composable 24 | fun MainNavigation( 25 | navHostController: NavHostController, 26 | modifier: Modifier = Modifier, 27 | startDestination: String 28 | ) { 29 | 30 | NavHost( 31 | navController = navHostController, 32 | startDestination = startDestination, modifier = modifier 33 | ) { 34 | /*articleGraph(navHostController = navHostController) 35 | locationGraph(navHostController = navHostController)*/ 36 | 37 | composable(Screens.WelcomeScreen.route) { 38 | WelcomeScreen(navHostController = navHostController) 39 | } 40 | 41 | composable(Screens.ArticleListScreen.route) { 42 | ArticlesListScreen() 43 | } 44 | 45 | composable(Screens.LocationScreen.route) { 46 | LocationScreen() 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/dev/roshana/jetpackcomposebasearch/navigation/NavigationItem.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.jetpackcomposebasearch.navigation 2 | 3 | import androidx.annotation.DrawableRes 4 | import dev.roshana.jetpackcomposebasearch.R 5 | import dev.roshana.presentation.navigation.Articles 6 | import dev.roshana.presentation.navigation.Locations 7 | 8 | /** PariSa; 9 | coding and smoking ;) 10 | **/ 11 | 12 | sealed class NavigationItem( 13 | var title: String, 14 | var screen_route: String, 15 | @DrawableRes var icon: Int 16 | ) { 17 | object ArticlesScreen : NavigationItem( 18 | title = "Article", 19 | screen_route = Articles.ARTICLESLIST, 20 | icon = R.drawable.ic_baseline_settings 21 | ) 22 | 23 | object LocationsScreen : NavigationItem( 24 | title = "Locations", 25 | screen_route = Locations.LOCATIONLIST, 26 | R.drawable.ic_baseline_add_location 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_add_location.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/app/src/main/res/drawable/logo.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | JetpackComposeBaseArch 3 | اینترنت خود را بررسی نمایید و دوباره امتحان کنید 4 | متاسفانه ارتباط با سرور دچار اخلال شده است لطفا مجددا تلاش کنید 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/test/java/dev/roshana/jetpackcomposebasearch/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.jetpackcomposebasearch 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import dev.roshana.buildsrc.* 2 | 3 | plugins { 4 | id("com.android.application") version "7.2.2" apply false 5 | id("com.android.library") version "7.2.2" apply false 6 | id("org.jetbrains.kotlin.android") version "1.7.10" apply false 7 | id("org.jetbrains.kotlin.jvm") version "1.7.10" apply false 8 | 9 | } 10 | 11 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 12 | buildscript { 13 | val compose_version by extra("1.3.0") 14 | val kotlin_version by extra("1.7.10") 15 | 16 | dependencies { 17 | classpath("com.google.dagger:hilt-android-gradle-plugin:2.42") 18 | // NOTE: Do not place your application dependencies here; they belong 19 | // in the individual module build.gradle files 20 | } 21 | } 22 | 23 | 24 | 25 | /*buildscript { 26 | val compose_version by extra("1.1.0-beta01") 27 | val kotlin_version by extra("1.5.31") 28 | 29 | repositories { 30 | mavenCentral() 31 | google() 32 | } 33 | dependencies { 34 | //classpath(BuildPlugins.hiltPlugin) 35 | // NOTE: Do not place your application dependencies here; they belong 36 | // in the individual module build.gradle files 37 | } 38 | } 39 | 40 | plugins { 41 | id("com.android.application") version "7.2.2" apply false 42 | id("com.android.library") version "7.2.2" apply false 43 | id("org.jetbrains.kotlin.android") version "1.5.31" apply false 44 | id("org.jetbrains.kotlin.jvm") version "1.5.31" apply false 45 | }*/ 46 | 47 | tasks.register("clean", Delete::class) { 48 | delete(rootProject.buildDir) 49 | } -------------------------------------------------------------------------------- /buildSrc/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | repositories { 5 | google() 6 | mavenCentral() 7 | gradlePluginPortal() 8 | } -------------------------------------------------------------------------------- /buildSrc/setting.gradle.kts: -------------------------------------------------------------------------------- 1 | // settings.gradle.kts 2 | import de.fayard.refreshVersions.bootstrapRefreshVersionsForBuildSrc 3 | 4 | buildscript { 5 | repositories { gradlePluginPortal() } 6 | dependencies.classpath("de.fayard.refreshVersions:refreshVersions:0.9.5") 7 | } 8 | 9 | bootstrapRefreshVersionsForBuildSrc() -------------------------------------------------------------------------------- /buildSrc/src/main/java/dev/roshana/buildsrc/BuildPlugins.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.buildsrc 2 | 3 | object BuildPlugins { 4 | const val androidGradle = "com.android.tools.build:gradle:${Versions.gradle}" 5 | const val kotlinGradlePlugin = 6 | "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}" 7 | const val hiltPlugin = "com.google.dagger:hilt-android-gradle-plugin:${Versions.hilt}" 8 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/dev/roshana/buildsrc/BuildTypes.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.buildsrc 2 | 3 | 4 | object BuildTypes { 5 | const val RELEASE = "release" 6 | const val DEBUG = "debug" 7 | } 8 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/dev/roshana/buildsrc/ConfigData.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.buildsrc 2 | 3 | object ConfigData { 4 | const val minSdkVersion = 21 5 | const val targetSdkVersion = 33 6 | const val compileSdkVersion = 33 7 | const val applicationId = "dev.roshana.jetpackcomposebasearch" 8 | const val versionCode = 1 9 | const val versionName = "0.1" 10 | const val versionMajor = 1 11 | const val versionMinor = 1 12 | const val versionPatch = 1 13 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/dev/roshana/buildsrc/Dependencies.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.buildsrc 2 | 3 | object Dependencies { 4 | 5 | object CommonLibs { 6 | const val material = "com.google.android.material:material:${Versions.material}" 7 | const val coroutines = 8 | "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}" 9 | const val coroutinesAndroid = 10 | "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}" 11 | const val coroutineAdapter = 12 | "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-experimental-adapter:${Versions.coroutinesAdapter}" 13 | const val arrowCore = "io.arrow-kt:arrow-core:${Versions.arrow}" 14 | const val arrowSyntax = "io.arrow-kt:arrow-syntax:${Versions.arrow}" 15 | const val arrowMeta = "io.arrow-kt:arrow-meta:${Versions.arrow}" 16 | const val dagger = "com.google.dagger:dagger:${Versions.dagger}" 17 | const val daggerCompiler = "com.google.dagger:dagger-compiler:${Versions.dagger}" 18 | const val daggerAndroid = "com.google.dagger:dagger-android-support:${Versions.dagger}" 19 | const val daggerAndroidCompiler = 20 | "com.google.dagger:dagger-android-processor:${Versions.dagger}" 21 | const val hilt = "com.google.dagger:hilt-android:${Versions.hilt}" 22 | const val hiltCompiler = "com.google.dagger:hilt-android-compiler:${Versions.hilt}" 23 | const val hiltViewModel = "androidx.hilt:hilt-lifecycle-viewmodel:${Versions.hiltViewModel}" 24 | const val hiltViewModelKapt = "androidx.hilt:hilt-compiler:${Versions.hiltViewModel}" 25 | 26 | const val gson = "com.google.code.gson:gson:${Versions.gson}" 27 | const val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit}" 28 | const val retrofitGson = "com.squareup.retrofit2:converter-gson:${Versions.retrofit}" 29 | const val okHttpInterceptor = "com.squareup.okhttp3:logging-interceptor:${Versions.okHttp}" 30 | const val kotlinSerialConverter = 31 | "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:${Versions.kotlinConverter}" 32 | const val jsonSerialization = 33 | "org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.jsonSerialization}" 34 | 35 | const val stetho = "com.facebook.stetho:stetho:${Versions.stetho}" 36 | const val stetho_OkHttp = "com.facebook.stetho:stetho-okhttp3:${Versions.stetho}" 37 | const val glide = "com.github.bumptech.glide:glide:${Versions.glide}" 38 | const val glideCompiler = "com.github.bumptech.glide:compiler:${Versions.glide}" 39 | 40 | const val picasso = "com.squareup.picasso:picasso:${Versions.picasso}" 41 | const val coil = "io.coil-kt:coil-compose:${Versions.coil}" 42 | const val coilImage = "com.github.skydoves:landscapist-coil:${Versions.coilImage}" 43 | const val accompanistPager = 44 | "com.google.accompanist:accompanist-pager:${Versions.accompanist}" 45 | const val accompanistIndicator = 46 | "com.google.accompanist:accompanist-pager-indicators:${Versions.accompanist}" 47 | const val dataStore = "androidx.datastore:datastore-preferences:${Versions.dataStore}" 48 | 49 | const val room = "androidx.room:room-runtime:${Versions.room}" 50 | const val roomCompiler = "androidx.room:room-compiler:${Versions.room}" 51 | const val roomPagingCompose = "androidx.room:room-paging:${Versions.room}" 52 | const val roomKtx = "androidx.room:room-ktx:${Versions.room}" 53 | const val roomGuava = "androidx.room:room-guava:${Versions.room}" 54 | const val roomTest = "androidx.room:room-testing:${Versions.room}" 55 | 56 | } 57 | 58 | object ComposeLibs { 59 | const val coreKtx = "androidx.core:core-ktx:${Versions.coreKtx}" 60 | const val composeUi = "androidx.compose.ui:ui:${Versions.composeVersion}" 61 | const val composeMaterial = "androidx.compose.material:material:${Versions.composeVersion}" 62 | const val composeUiPreview = 63 | "androidx.compose.ui:ui-tooling-preview:${Versions.composeVersion}" 64 | const val composeActivity = "androidx.activity:activity-compose:${Versions.composeActivity}" 65 | const val composeAnimation = 66 | "androidx.compose.animation:animation:${Versions.composeVersion}" 67 | const val composeViewModel = 68 | "androidx.lifecycle:lifecycle-viewmodel-compose:${Versions.composeViewModel}" 69 | const val androidxLifeCycle = 70 | "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifeCycle}" 71 | const val composeNav = "androidx.navigation:navigation-compose:${Versions.composeNav}" 72 | const val composePaging = "androidx.paging:paging-compose:${Versions.composePaging}" 73 | const val composeHilt = "androidx.hilt:hilt-navigation-compose:${Versions.composeHilt}" 74 | const val composeAnimNavigation = 75 | "com.google.accompanist:accompanist-navigation-animation:${Versions.composeAnimNav}" 76 | const val coreSplash = "androidx.core:core-splashscreen:${Versions.coreSplash}" 77 | } 78 | 79 | object TestLibs { 80 | const val junit = "junit:junit:${Versions.junit}" 81 | const val extJunit = "androidx.test.ext:junit:${Versions.extJunit}" 82 | const val testRunner = "androidx.test:runner:${Versions.testRunner}" 83 | const val mockitoKotlin = "com.nhaarman:mockito-kotlin-kt1.1:${Versions.mockitoKotlin}" 84 | const val archTesting = "androidx.arch.core:core-testing:${Versions.archTest}" 85 | const val extJUnit = "androidx.test.ext:junit:1.1.2" 86 | const val espresso = "androidx.test.espresso:espresso-core:${Versions.espresso}" 87 | const val composeUiJunit = "androidx.compose.ui:ui-test-junit4:${Versions.composeVersion}" 88 | const val composeUiTooling = "androidx.compose.ui:ui-tooling:${Versions.composeVersion}" 89 | const val composeUiTestManifest = 90 | "androidx.compose.ui:ui-test-manifest:${Versions.composeVersion}" 91 | } 92 | 93 | 94 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/dev/roshana/buildsrc/Versions.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.buildsrc 2 | 3 | object Versions { 4 | const val composeVersion = "1.1.0-beta01" 5 | const val kotlin = "1.5.31" 6 | const val gradle = "7.2.2" 7 | const val activityCompose = "1.4.0" 8 | const val composeViewModel = "2.4.1" 9 | const val composePaging = "1.0.0-alpha16" 10 | const val composeHilt = "1.0.0" 11 | const val composeNav = "2.5.1" 12 | const val composeAnimNav = "0.20.2" 13 | const val arrow = "0.10.4" 14 | const val kt_lint = "0.37.2" 15 | const val coroutines = "1.3.3" 16 | const val coroutinesAdapter = "1.0.0" 17 | const val gradleVersions = "0.27.0" 18 | const val hilt = "2.42" 19 | const val hiltViewModel = "1.0.0-alpha03" 20 | const val arch = "2.2.0" 21 | const val archTest = "2.1.0" 22 | const val material = "1.6.1" 23 | const val constraintLayout = "1.1.3" 24 | const val room = "2.4.2" 25 | const val dagger = "2.25.4" 26 | const val okHttp = "4.10.0" 27 | const val gson = "2.8.6" 28 | const val retrofit = "2.7.1" 29 | const val stetho = "1.5.1" 30 | const val glide = "4.11.0" 31 | const val junit = "4.13.2" 32 | const val extJunit = "1.1.3" 33 | const val testRunner = "1.2.0" 34 | const val espresso = "3.4.0" 35 | const val mockitoKotlin = "1.6.0" 36 | const val navigationComponent = "2.3.0" 37 | const val fragment = "1.2.5" 38 | const val coreKtx = "1.7.0" 39 | const val lifeCycle = "2.5.1" 40 | const val composeActivity = "1.4.0" 41 | const val kotlinConverter = "0.8.0" 42 | const val jsonSerialization = "1.3.3" 43 | const val picasso = "2.8" 44 | const val coil = "2.2.1" 45 | const val coilImage = "1.6.1" 46 | const val accompanist = "0.24.2-alpha" 47 | const val dataStore = "1.0.0" 48 | const val coreSplash = "1.0.0" 49 | } -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import dev.roshana.buildsrc.BuildTypes 2 | import dev.roshana.buildsrc.ConfigData 3 | import dev.roshana.buildsrc.Dependencies 4 | 5 | plugins { 6 | id("com.android.library") 7 | id("org.jetbrains.kotlin.android") 8 | id("kotlin-kapt") 9 | id("kotlin-parcelize") 10 | 11 | } 12 | 13 | android { 14 | compileSdk = ConfigData.compileSdkVersion 15 | 16 | defaultConfig { 17 | minSdk = ConfigData.minSdkVersion 18 | targetSdk = ConfigData.targetSdkVersion 19 | 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | consumerProguardFiles("consumer-rules.pro") 22 | } 23 | 24 | buildTypes { 25 | getByName(BuildTypes.RELEASE) { 26 | isMinifyEnabled = false 27 | proguardFiles( 28 | getDefaultProguardFile("proguard-android-optimize.txt"), 29 | "proguard-rules.pro" 30 | ) 31 | } 32 | } 33 | compileOptions { 34 | sourceCompatibility = JavaVersion.VERSION_1_8 35 | targetCompatibility = JavaVersion.VERSION_1_8 36 | } 37 | kotlinOptions { 38 | jvmTarget = JavaVersion.VERSION_1_8.toString() 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation(project(":domain")) 44 | 45 | /** compose 46 | **/ 47 | implementation(Dependencies.ComposeLibs.coreKtx) 48 | implementation(Dependencies.ComposeLibs.composePaging) 49 | implementation(Dependencies.CommonLibs.dataStore) 50 | 51 | /** hilt - dagger 52 | **/ 53 | implementation(Dependencies.CommonLibs.daggerAndroid) 54 | kapt(Dependencies.CommonLibs.daggerCompiler) 55 | kapt(Dependencies.CommonLibs.daggerAndroidCompiler) 56 | implementation(Dependencies.CommonLibs.hilt) 57 | kapt(Dependencies.CommonLibs.hiltCompiler) 58 | 59 | /** retrofit, okHttp 60 | **/ 61 | implementation(Dependencies.CommonLibs.retrofit) 62 | implementation(Dependencies.CommonLibs.retrofitGson) 63 | implementation(Dependencies.CommonLibs.okHttpInterceptor) 64 | implementation(Dependencies.CommonLibs.gson) 65 | implementation(Dependencies.CommonLibs.kotlinSerialConverter) 66 | implementation(Dependencies.CommonLibs.jsonSerialization) 67 | 68 | /** Coroutine 69 | **/ 70 | implementation(Dependencies.CommonLibs.coroutines) 71 | implementation(Dependencies.CommonLibs.coroutinesAndroid) 72 | implementation(Dependencies.CommonLibs.coroutineAdapter) 73 | 74 | /** Room 75 | **/ 76 | implementation(Dependencies.CommonLibs.room) 77 | implementation(Dependencies.CommonLibs.roomPagingCompose) 78 | implementation(Dependencies.CommonLibs.roomKtx) 79 | kapt(Dependencies.CommonLibs.roomCompiler) 80 | 81 | 82 | } -------------------------------------------------------------------------------- /data/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/data/consumer-rules.pro -------------------------------------------------------------------------------- /data/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /data/src/androidTest/java/dev/roshana/data/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("dev.roshana.data.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/db/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.db 2 | 3 | import android.content.Context 4 | import androidx.annotation.VisibleForTesting 5 | import androidx.room.Database 6 | import androidx.room.Room 7 | import androidx.room.RoomDatabase 8 | import androidx.room.TypeConverters 9 | import dev.roshana.data.network.models.dto.ArticleDto 10 | 11 | /** PariSa; 12 | coding and smoking ;) 13 | **/ 14 | 15 | /** 16 | * Main database description. 17 | */ 18 | @Database( 19 | entities = [ 20 | ArticleDto::class 21 | ], 22 | version = 1, exportSchema = false 23 | ) 24 | @TypeConverters(OperatorTypeConverters::class) 25 | abstract class AppDatabase : RoomDatabase() { 26 | 27 | companion object { 28 | 29 | @VisibleForTesting 30 | private const val DATABASE_NAME = "cache.db" 31 | 32 | @Volatile 33 | private var INSTANCE: AppDatabase? = null 34 | 35 | @JvmStatic 36 | fun getInstance(context: Context, testMode: Boolean = false): AppDatabase { 37 | return INSTANCE ?: synchronized(this) { 38 | INSTANCE ?: buildDatabase(context, testMode).also { 39 | INSTANCE = it 40 | } 41 | } 42 | } 43 | 44 | @JvmStatic 45 | private fun buildDatabase(context: Context, testMode: Boolean): AppDatabase { 46 | return if (testMode) { 47 | Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) 48 | .fallbackToDestructiveMigration() 49 | .allowMainThreadQueries() 50 | .build() 51 | } else { 52 | Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) 53 | .allowMainThreadQueries() 54 | .fallbackToDestructiveMigration() 55 | .build() 56 | } 57 | } 58 | } 59 | 60 | abstract fun articleDao(): ArticleDao 61 | 62 | } 63 | -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/db/ArticleDao.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.db 2 | 3 | import androidx.room.* 4 | import dev.roshana.data.network.models.dto.ArticleDto 5 | 6 | /** PariSa; 7 | coding and smoking ;) 8 | **/ 9 | 10 | @Dao 11 | interface ArticleDao { 12 | 13 | @Query("delete from article") 14 | fun removeArticles() 15 | 16 | @Insert(onConflict = OnConflictStrategy.IGNORE) 17 | fun insertArticles(config: ArticleDto): Long 18 | 19 | /* @Transaction 20 | fun insert(config: ArticleDto): Long { 21 | removeArticles() 22 | return insertArticles(config) 23 | }*/ 24 | 25 | @Query("select * from article") 26 | fun fetchAll(): List 27 | 28 | } -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/db/OperatorTypeConverters.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.db 2 | 3 | import android.util.Log 4 | import androidx.room.TypeConverter 5 | import com.google.gson.Gson 6 | import com.google.gson.reflect.TypeToken 7 | import dev.roshana.data.network.models.dto.ArticleDto 8 | import dev.roshana.data.network.models.dto.SourceDto 9 | 10 | /** PariSa; 11 | coding and smoking ;) 12 | **/ 13 | 14 | object OperatorTypeConverters { 15 | 16 | @TypeConverter 17 | @JvmStatic 18 | fun stringToIntList(data: String?): List? { 19 | return data?.let { 20 | it.split(",").map { 21 | try { 22 | it.toInt() 23 | } catch (ex: NumberFormatException) { 24 | Log.e("OperatorTypeConverters", "Cannot convert ${ex.message} to number") 25 | null 26 | } 27 | } 28 | }?.filterNotNull() 29 | } 30 | 31 | @TypeConverter 32 | @JvmStatic 33 | fun listIntToString(ints: List?): String? { 34 | return ints?.joinToString(",") 35 | } 36 | 37 | @TypeConverter 38 | @JvmStatic 39 | fun stringToMap(value: String): Map = 40 | fromJson(value) 41 | 42 | @TypeConverter 43 | @JvmStatic 44 | fun mapToString(value: Map?): String = 45 | toJson(value) 46 | 47 | @TypeConverter 48 | @JvmStatic 49 | fun stringToSource(value: String): SourceDto = 50 | fromJson(value) 51 | 52 | @TypeConverter 53 | @JvmStatic 54 | fun sourceToString(value: SourceDto): String = toJson(value) 55 | 56 | @TypeConverter 57 | @JvmStatic 58 | fun stringToArticle(value: String): ArticleDto = 59 | fromJson(value) 60 | 61 | @TypeConverter 62 | @JvmStatic 63 | fun articleToString(value: ArticleDto): String = toJson(value) 64 | 65 | 66 | @TypeConverter 67 | @JvmStatic 68 | fun stringToStringList(value: String?): List { 69 | if (value.isNullOrEmpty()) { 70 | return emptyList() 71 | } 72 | return fromJson(value) 73 | } 74 | 75 | @TypeConverter 76 | @JvmStatic 77 | fun stringListToString(items: List?): String = toJson(items) 78 | 79 | inline fun toJson(value: T): String { 80 | return if (value == null) "" else Gson().toJson(value) 81 | } 82 | 83 | inline fun fromJson(value: String): T { 84 | return Gson().fromJson(value, object : TypeToken() {}.type) 85 | } 86 | 87 | } 88 | 89 | -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/db/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.db.di 2 | 3 | import android.app.Application 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.components.SingletonComponent 8 | import dev.roshana.data.db.AppDatabase 9 | import dev.roshana.data.preference.Preference 10 | import dev.roshana.data.preference.PreferenceImpl 11 | import javax.inject.Singleton 12 | 13 | /** PariSa; 14 | coding and smoking ;) 15 | **/ 16 | 17 | @Module 18 | @InstallIn(SingletonComponent::class) 19 | class DatabaseModule { 20 | 21 | @Singleton 22 | @Provides 23 | fun provideDb(app: Application): AppDatabase { 24 | return AppDatabase.getInstance(app) 25 | } 26 | 27 | @Provides 28 | fun providePreference(app: Application): Preference { 29 | return PreferenceImpl(app) 30 | } 31 | } -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/network/api/ApiService.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.network.api 2 | 3 | import dev.roshana.data.network.models.dto.ArticleDto 4 | import dev.roshana.data.network.utils.TOP_HEAD_LINES 5 | import dev.roshana.domain.models.ResponseWrapper 6 | import retrofit2.http.GET 7 | import retrofit2.http.Query 8 | 9 | /** PariSa; 10 | coding and smoking ;) 11 | **/ 12 | 13 | interface ApiService { 14 | 15 | @GET(TOP_HEAD_LINES) 16 | suspend fun getTechCrunchNews( 17 | @Query("page") page: Int, 18 | @Query("sources") sources: String, 19 | @Query("apiKey") apiKey: String 20 | ): ResponseWrapper> 21 | 22 | 23 | } -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/network/di/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.network.di 2 | 3 | import android.app.Application 4 | import com.google.gson.GsonBuilder 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import dev.roshana.data.BuildConfig 10 | import dev.roshana.data.network.api.ApiService 11 | import dev.roshana.data.network.utils.BASE_URL 12 | import dev.roshana.domain.utils.getToken 13 | import dev.roshana.domain.utils.globalHeaders 14 | import dev.roshana.domain.R.string 15 | import okhttp3.Interceptor 16 | import okhttp3.OkHttpClient 17 | import okhttp3.Request 18 | import okhttp3.Response 19 | import okhttp3.logging.HttpLoggingInterceptor 20 | import retrofit2.Retrofit 21 | import retrofit2.converter.gson.GsonConverterFactory 22 | import java.io.IOException 23 | import java.net.UnknownHostException 24 | import java.util.concurrent.TimeUnit 25 | import javax.inject.Singleton 26 | 27 | /** PariSa; 28 | coding and smoking ;) 29 | **/ 30 | 31 | @Module 32 | @InstallIn(SingletonComponent::class) 33 | object NetworkModule { 34 | 35 | 36 | /* @Singleton 37 | @Provides 38 | fun provideOkHttp(): OkHttpClient { 39 | 40 | val trustAllCerts = arrayOf( 41 | object : X509TrustManager { 42 | @Throws(CertificateException::class) 43 | override fun checkClientTrusted( 44 | chain: Array?, 45 | authType: String? 46 | ) { 47 | } 48 | 49 | @Throws(CertificateException::class) 50 | override fun checkServerTrusted( 51 | chain: Array?, 52 | authType: String? 53 | ) { 54 | } 55 | 56 | override fun getAcceptedIssuers(): Array? { 57 | return arrayOf() 58 | } 59 | } 60 | ) 61 | val sslContext = SSLContext.getInstance("SSL") 62 | sslContext.init(null, trustAllCerts, SecureRandom()) 63 | val sslSocketFactory = sslContext.socketFactory 64 | 65 | val builder = OkHttpClient.Builder() 66 | builder.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager) 67 | builder.hostnameVerifier { _, _ -> true } 68 | //builder.addInterceptor(connectivityInterceptor) 69 | builder.addInterceptor(HttpLoggingInterceptor().apply { 70 | this.setLevel(HttpLoggingInterceptor.Level.BODY) 71 | }) 72 | builder.connectTimeout(1, TimeUnit.MINUTES) 73 | builder.readTimeout(1, TimeUnit.MINUTES) 74 | builder.writeTimeout(1, TimeUnit.MINUTES) 75 | 76 | return builder.build() 77 | 78 | }*/ 79 | 80 | 81 | /*@ExperimentalSerializationApi 82 | @Singleton 83 | @Provides 84 | fun providesAPI(client: OkHttpClient): ApiService { 85 | 86 | return Retrofit.Builder() 87 | .addConverterFactory(GsonConverterFactory.create()) 88 | .baseUrl(BASE_URL) 89 | .client(client) 90 | .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create())) 91 | .build() 92 | .create(ApiService::class.java) 93 | }*/ 94 | @Provides 95 | @Singleton 96 | internal fun provideOkHttpClient(application: Application) = OkHttpClient.Builder() 97 | .connectTimeout(30, TimeUnit.SECONDS) 98 | .writeTimeout(30, TimeUnit.SECONDS) 99 | .readTimeout(60, TimeUnit.SECONDS) 100 | .addRoshanaInterceptor(application) 101 | .addLoggerInterceptor() 102 | .build() 103 | 104 | @Provides 105 | @Singleton 106 | internal fun provideRetrofit(application: Application, okHttpClient: OkHttpClient) = 107 | Retrofit.Builder() 108 | .baseUrl(BASE_URL) 109 | .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create())) 110 | .client(okHttpClient) 111 | .build() 112 | 113 | @Provides 114 | @Singleton 115 | internal fun provideApiService(retrofit: Retrofit) = 116 | retrofit.create(ApiService::class.java) 117 | 118 | 119 | private fun OkHttpClient.Builder.addLoggerInterceptor() = apply { 120 | if (BuildConfig.DEBUG) { 121 | addNetworkInterceptor(HttpLoggingInterceptor().apply { 122 | level = HttpLoggingInterceptor.Level.BODY 123 | }) 124 | } 125 | } 126 | 127 | private fun OkHttpClient.Builder.addRoshanaInterceptor(application: Application) = apply { 128 | addInterceptor { chain -> 129 | try { 130 | handleChain(chain) 131 | } catch (ioException: UnknownHostException) { 132 | throw IOException(application.getString(string.connectionError), ioException) 133 | } catch (ioException: IOException) { 134 | throw IOException(application.getString(string.socketError), ioException) 135 | } 136 | } 137 | } 138 | 139 | private fun handleChain(chain: Interceptor.Chain): Response { 140 | return chain.request() 141 | .newBuilder() 142 | .addHeader("X-Api-Key", "de196ac120164019a0911ea8191f85e4") 143 | .addGlobalHeaders() 144 | .addToken() 145 | .build().let { 146 | chain.proceed(it) 147 | } 148 | } 149 | 150 | private fun Request.Builder.addGlobalHeaders() = apply { 151 | globalHeaders.forEach { (key, value) -> 152 | addHeader(key, value) 153 | } 154 | } 155 | 156 | private fun Request.Builder.addToken() = apply { 157 | getToken().takeIf { it.isNotEmpty() }?.let { 158 | addHeader("Authorization", "Token $it") 159 | } 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/network/models/dto/ArticleDto.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.network.models.dto 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.Keep 5 | import androidx.room.Entity 6 | import androidx.room.PrimaryKey 7 | import kotlinx.parcelize.Parcelize 8 | 9 | /** PariSa; 10 | coding and smoking ;) 11 | **/ 12 | 13 | @Keep 14 | @Parcelize 15 | @Entity(tableName = "article") 16 | data class ArticleDto( 17 | @PrimaryKey(autoGenerate = true) 18 | @Keep 19 | val localId: Long = 0, 20 | @Keep 21 | var author: String? = null, 22 | @Keep 23 | var title: String? = null, 24 | @Keep 25 | var description: String? = null, 26 | @Keep 27 | var url: String? = null, 28 | @Keep 29 | var urlToImage: String? = null, 30 | @Keep 31 | var publishedAt: String? = null, 32 | @Keep 33 | var content: String? = null, 34 | @Keep 35 | var source: SourceDto? = null 36 | ) : Parcelable 37 | -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/network/models/dto/SourceDto.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.network.models.dto 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.Keep 5 | import kotlinx.parcelize.Parcelize 6 | 7 | /** PariSa; 8 | coding and smoking ;) 9 | **/ 10 | 11 | @Parcelize 12 | data class SourceDto( 13 | @Keep val id: String? = null, 14 | @Keep val name: String? = null, 15 | ) : Parcelable 16 | -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/network/utils/ConnectivityInterceptor.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.network.utils 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.net.ConnectivityManager 6 | import android.net.NetworkCapabilities 7 | import android.os.Build 8 | import okhttp3.Interceptor 9 | import okhttp3.Request 10 | import okhttp3.Response 11 | import java.io.IOException 12 | 13 | /** PariSa; 14 | coding and smoking ;) 15 | **/ 16 | 17 | class NoConnectivityException : IOException() { 18 | override val message: String 19 | get() = "عدم اتصال به اینترنت" 20 | } 21 | 22 | class ConnectivityInterceptor( 23 | val app: Application 24 | ) : Interceptor { 25 | 26 | @Suppress("DEPRECATION") 27 | private fun isNetworkAvailable(context: Context): Boolean { 28 | val connectivityManager = 29 | context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 30 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 31 | val nw = connectivityManager.activeNetwork ?: return false 32 | val actNw = connectivityManager.getNetworkCapabilities(nw) ?: return false 33 | return when { 34 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true 35 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true 36 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true 37 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true 38 | else -> false 39 | } 40 | } else { 41 | return connectivityManager.activeNetworkInfo?.isConnected ?: false 42 | } 43 | } 44 | 45 | override fun intercept(chain: Interceptor.Chain): Response { 46 | synchronized(this) { 47 | val originalRequest = chain.request().newBuilder() 48 | if (!isNetworkAvailable(app)) { 49 | throw NoConnectivityException() 50 | } 51 | val request = requestHeader(originalRequest) 52 | return chain.proceed(request) 53 | } 54 | } 55 | 56 | companion object { 57 | fun requestHeader(request: Request.Builder): Request { 58 | return request 59 | .addHeader("Accept", "application/json") 60 | .addHeader("Content-type", "application/json") 61 | .build() 62 | 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/network/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.network.utils 2 | 3 | /** PariSa; 4 | coding and smoking ;) 5 | **/ 6 | 7 | const val BASE_URL = "https://newsapi.org/v2/" 8 | const val TOP_HEAD_LINES = "top-headlines" 9 | 10 | -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/preference/Preference.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.preference 2 | 3 | import android.app.Application 4 | import android.content.SharedPreferences 5 | import android.preference.PreferenceManager 6 | 7 | /** PariSa; 8 | coding and smoking ;) 9 | **/ 10 | 11 | interface Preference { 12 | fun put(key: String, value: String) 13 | fun put(key: String, value: Boolean) 14 | fun put(key: String, value: Long) 15 | fun put(key: String, value: Int) 16 | 17 | fun getString(key: String): String 18 | fun getBoolean(key: String): Boolean 19 | fun getLong(key: String): Long 20 | fun getInt(key: String): Int 21 | } 22 | 23 | class PreferenceImpl( 24 | application: Application 25 | ) : Preference { 26 | 27 | private val stringValues: HashMap = hashMapOf() 28 | private val longValues: HashMap = hashMapOf() 29 | private val intValues: HashMap = hashMapOf() 30 | private val booleanValues: HashMap = hashMapOf() 31 | 32 | var preferences: SharedPreferences = 33 | PreferenceManager.getDefaultSharedPreferences(application.applicationContext) 34 | 35 | override fun put(key: String, value: String) { 36 | synchronized(this) { 37 | stringValues[key] = value 38 | preferences.edit().putString(key, value).apply() 39 | } 40 | } 41 | 42 | override fun put(key: String, value: Boolean) { 43 | synchronized(this) { 44 | booleanValues[key] = value 45 | preferences.edit().putBoolean(key, value).apply() 46 | } 47 | } 48 | 49 | override fun put(key: String, value: Long) { 50 | synchronized(this) { 51 | longValues[key] = value 52 | preferences.edit().putLong(key, value).apply() 53 | } 54 | } 55 | 56 | override fun put(key: String, value: Int) { 57 | synchronized(this) { 58 | intValues[key] = value 59 | preferences.edit().putInt(key, value).apply() 60 | } 61 | } 62 | 63 | override fun getString(key: String): String { 64 | synchronized(this) { 65 | return if (stringValues.containsKey(key)) 66 | stringValues[key] ?: "" 67 | else 68 | preferences.getString(key, "") ?: "" 69 | } 70 | } 71 | 72 | override fun getBoolean(key: String): Boolean { 73 | synchronized(this) { 74 | return if (booleanValues.containsKey(key)) 75 | booleanValues[key] ?: false 76 | else 77 | preferences.getBoolean(key, false) 78 | } 79 | } 80 | 81 | override fun getLong(key: String): Long { 82 | synchronized(this) { 83 | return if (longValues.containsKey(key)) 84 | longValues[key] ?: 0 85 | else 86 | preferences.getLong(key, 0) 87 | } 88 | } 89 | 90 | override fun getInt(key: String): Int { 91 | synchronized(this) { 92 | return if (intValues.containsKey(key)) 93 | intValues[key] ?: 0 94 | else 95 | preferences.getInt(key, 0) 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/repository/ArticleRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.repository 2 | 3 | import androidx.paging.Pager 4 | import androidx.paging.PagingConfig 5 | import androidx.paging.PagingData 6 | import dev.roshana.data.network.api.ApiService 7 | import dev.roshana.data.repository.pagingSource.ArticlePagingSource 8 | import dev.roshana.domain.models.article.Article 9 | import dev.roshana.domain.repositories.ArticleRepository 10 | import kotlinx.coroutines.flow.Flow 11 | import javax.inject.Inject 12 | import javax.inject.Singleton 13 | 14 | /** PariSa; 15 | coding and smoking ;) 16 | **/ 17 | 18 | @Singleton 19 | class ArticleRepositoryImpl @Inject constructor( 20 | val apiService: ApiService 21 | ) : ArticleRepository { 22 | override suspend fun getTechCrunchNews(source: String?): Flow> { 23 | 24 | return Pager(PagingConfig(pageSize = 50)) { 25 | ArticlePagingSource(apiService = apiService) 26 | }.flow 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/repository/DataStoreRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.repository 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.booleanPreferencesKey 7 | import androidx.datastore.preferences.core.edit 8 | import androidx.datastore.preferences.core.emptyPreferences 9 | import androidx.datastore.preferences.preferencesDataStore 10 | import dev.roshana.domain.repositories.DataStoreRepository 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.catch 13 | import kotlinx.coroutines.flow.map 14 | import java.io.IOException 15 | import javax.inject.Inject 16 | import javax.inject.Singleton 17 | 18 | /** PariSa; 19 | coding and smoking ;) 20 | **/ 21 | 22 | @Singleton 23 | class DataStoreRepositoryImpl @Inject constructor(val context: Context) : DataStoreRepository { 24 | 25 | private object PreferencesKey { 26 | val onBoardingKey = booleanPreferencesKey(name = "on_boarding_completed") 27 | } 28 | 29 | private val dataStore = context.dataStore 30 | 31 | 32 | override suspend fun saveOnOnBoardingState(completed: Boolean) { 33 | dataStore.edit { preferences -> 34 | preferences[PreferencesKey.onBoardingKey] = completed 35 | } 36 | } 37 | 38 | override suspend fun readOnBoardingState(): Flow { 39 | return dataStore.data 40 | .catch { exception -> 41 | if (exception is IOException) { 42 | emit(emptyPreferences()) 43 | } else { 44 | throw exception 45 | } 46 | } 47 | .map { preferences -> 48 | val onBoardingState = preferences[PreferencesKey.onBoardingKey] ?: false 49 | onBoardingState 50 | } 51 | } 52 | } 53 | 54 | val Context.dataStore: DataStore by preferencesDataStore( 55 | name = "on_boarding_pref" 56 | ) 57 | -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/repository/base/BaseRepository.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.repository.base 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | 6 | /** PariSa; 7 | coding and smoking ;) 8 | **/ 9 | 10 | open class BaseRepository { 11 | 12 | suspend fun safeApiCall( 13 | apiCall: suspend () -> T 14 | ): Result { 15 | return withContext(Dispatchers.IO) { 16 | try { 17 | Result.success(apiCall.invoke()) 18 | } catch (throwable: Throwable) { 19 | Result.failure(throwable) 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/repository/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.repository.di 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import dagger.hilt.InstallIn 7 | import dagger.hilt.components.SingletonComponent 8 | import dev.roshana.data.network.api.ApiService 9 | import dev.roshana.data.repository.ArticleRepositoryImpl 10 | import dev.roshana.data.repository.DataStoreRepositoryImpl 11 | import dev.roshana.domain.repositories.ArticleRepository 12 | import dev.roshana.domain.repositories.DataStoreRepository 13 | import retrofit2.Retrofit 14 | 15 | /** PariSa; 16 | coding and smoking ;) 17 | **/ 18 | 19 | @Module 20 | @InstallIn(SingletonComponent::class) 21 | object RepositoryModule { 22 | 23 | /*@Provides 24 | @Singleton 25 | internal open fun providesArticleRepository( 26 | application: Application, 27 | apiService: ApiService 28 | ) = ArticleRepositoryImpl(application, apiService) as ArticleRepository*/ 29 | 30 | /*@Binds 31 | abstract fun providesArticleRepository(impl: ArticleRepositoryImpl): ArticleRepository*/ 32 | 33 | @Provides 34 | fun providesArticleRepository(retrofit: Retrofit): ArticleRepository { 35 | return ArticleRepositoryImpl(retrofit.create(ApiService::class.java)) 36 | } 37 | 38 | @Provides 39 | fun providesDataStoreRepository( 40 | context: Context 41 | ): DataStoreRepository = DataStoreRepositoryImpl(context = context) 42 | } 43 | -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/repository/pagingSource/ArticlePagingSource.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.repository.pagingSource 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import dev.roshana.data.network.api.ApiService 6 | import dev.roshana.domain.models.article.Article 7 | 8 | /** PariSa; 9 | coding and smoking ;) 10 | **/ 11 | 12 | class ArticlePagingSource( 13 | private val apiService: ApiService 14 | ) : PagingSource() { 15 | override fun getRefreshKey(state: PagingState): Int? { 16 | return state.anchorPosition?.let { anchorposition -> 17 | state.closestPageToPosition(anchorposition)?.prevKey?.plus(1) 18 | } 19 | } 20 | 21 | override suspend fun load(params: LoadParams): LoadResult { 22 | return try { 23 | val page = params.key ?: 1 24 | val response = apiService.getTechCrunchNews( 25 | page = page, 26 | sources = "techcrunch", 27 | apiKey = "de196ac120164019a0911ea8191f85e4" 28 | ) 29 | val items = response.articles?.map { 30 | Article(author = it.author, title = it.title, url = it.url) 31 | } 32 | val prevKey = if (page > 1) page - 1 else null 33 | val nextKey = if (items?.isNotEmpty() == true) page + 1 else null 34 | LoadResult.Page( 35 | data = items!!, 36 | nextKey = nextKey, 37 | prevKey = prevKey 38 | ) 39 | } catch (e: Exception) { 40 | LoadResult.Error(e) 41 | } 42 | /* return try { 43 | val page = params.key ?: 1 // set page 1 as default 44 | val pageSize = params.loadSize 45 | val response = apiService.getTechCrunchNews( 46 | page = page, 47 | sources = "techcrunch", 48 | apiKey = "de196ac120164019a0911ea8191f85e4" 49 | ) 50 | val items = response.articles?.map { 51 | Article(author = it.author, title = it.title, url = it.url) 52 | } 53 | val prevKey = if (page > 1) page - 1 else null 54 | val nextKey = if (items?.isNotEmpty() == true) page + 1 else null 55 | LoadResult.Page(items!!, prevKey, nextKey) 56 | } catch (e: Exception) { 57 | e.printStackTrace() 58 | LoadResult.Error(e) 59 | }*/ 60 | } 61 | 62 | 63 | } -------------------------------------------------------------------------------- /data/src/main/java/dev/roshana/data/repository/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data.repository.utils 2 | 3 | import android.util.Log 4 | import java.lang.Exception 5 | 6 | /** PariSa; 7 | coding and smoking ;) 8 | **/ 9 | 10 | fun getPageIntFromUrl(url: String): Int? { 11 | return try { 12 | url.substringAfterLast("=").toInt() 13 | } catch (e: Exception) { 14 | e.message?.let { Log.d("converting url", it) } 15 | null 16 | } 17 | } 18 | 19 | 20 | 21 | fun getEpisodeIntFromUrl(url: String): Int? { 22 | return try { 23 | url.substringAfterLast("/").toInt() 24 | } catch (e: Exception) { 25 | e.message?.let { Log.d("converting url", it) } 26 | null 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /data/src/test/java/dev/roshana/data/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.data 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import dev.roshana.buildsrc.BuildTypes.RELEASE 2 | import dev.roshana.buildsrc.ConfigData 3 | import dev.roshana.buildsrc.Dependencies 4 | 5 | plugins { 6 | id("com.android.library") 7 | id("org.jetbrains.kotlin.android") 8 | id("kotlin-kapt") 9 | id("kotlin-parcelize") 10 | } 11 | 12 | android { 13 | compileSdk = ConfigData.compileSdkVersion 14 | 15 | defaultConfig { 16 | minSdk = ConfigData.minSdkVersion 17 | targetSdk = ConfigData.targetSdkVersion 18 | 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | consumerProguardFiles("consumer-rules.pro") 21 | } 22 | 23 | buildTypes { 24 | getByName(RELEASE) { 25 | isMinifyEnabled = false 26 | proguardFiles( 27 | getDefaultProguardFile("proguard-android-optimize.txt"), 28 | "proguard-rules.pro" 29 | ) 30 | } 31 | } 32 | compileOptions { 33 | sourceCompatibility = JavaVersion.VERSION_1_8 34 | targetCompatibility = JavaVersion.VERSION_1_8 35 | } 36 | kotlinOptions { 37 | jvmTarget = JavaVersion.VERSION_1_8.toString() 38 | } 39 | } 40 | 41 | dependencies { 42 | 43 | implementation(Dependencies.ComposeLibs.coreKtx) 44 | 45 | /** compose 46 | **/ 47 | implementation(Dependencies.ComposeLibs.composePaging) 48 | 49 | /** hilt - dagger 50 | **/ 51 | implementation(Dependencies.CommonLibs.daggerAndroid) 52 | kapt(Dependencies.CommonLibs.daggerCompiler) 53 | kapt(Dependencies.CommonLibs.daggerAndroidCompiler) 54 | implementation(Dependencies.CommonLibs.hilt) 55 | kapt(Dependencies.CommonLibs.hiltCompiler) 56 | 57 | /** retrofit, okHttp 58 | **/ 59 | implementation(Dependencies.CommonLibs.retrofit) 60 | implementation(Dependencies.CommonLibs.retrofitGson) 61 | implementation(Dependencies.CommonLibs.okHttpInterceptor) 62 | implementation(Dependencies.CommonLibs.gson) 63 | implementation(Dependencies.CommonLibs.kotlinSerialConverter) 64 | implementation(Dependencies.CommonLibs.jsonSerialization) 65 | 66 | /** Room 67 | **/ 68 | /*implementation(Dependencies.CommonLibs.room) 69 | implementation(Dependencies.CommonLibs.roomPagingCompose) 70 | kapt(Dependencies.CommonLibs.roomCompiler)*/ 71 | 72 | /** testing 73 | **/ 74 | testImplementation(Dependencies.TestLibs.junit) 75 | androidTestImplementation(Dependencies.TestLibs.extJunit) 76 | androidTestImplementation(Dependencies.TestLibs.espresso) 77 | 78 | 79 | } -------------------------------------------------------------------------------- /domain/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/consumer-rules.pro -------------------------------------------------------------------------------- /domain/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /domain/src/androidTest/java/dev/roshana/domain/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.domain 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("dev.roshana.domain.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /domain/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/roshana/domain/di/UseCaseModule.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.domain.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import dev.roshana.domain.repositories.ArticleRepository 8 | import dev.roshana.domain.repositories.DataStoreRepository 9 | import dev.roshana.domain.usecases.ArticleUseCase 10 | import dev.roshana.domain.usecases.DataStoreUseCase 11 | import javax.inject.Singleton 12 | 13 | /** PariSa; 14 | coding and smoking ;) 15 | **/ 16 | 17 | @Module 18 | @InstallIn(SingletonComponent::class) 19 | object UseCaseModule { 20 | 21 | /*@Provides 22 | @Singleton 23 | internal fun providesArticleUseCase( 24 | articleRepository: ArticleRepository 25 | ) = ArticleUseCase(articleRepository)*/ 26 | 27 | @Provides 28 | @Singleton 29 | fun providesArticlesUseCase(articleRepository: ArticleRepository) = 30 | ArticleUseCase(articleRepository) 31 | 32 | @Provides 33 | @Singleton 34 | fun providesDataStoreUseCase(dataStoreRepository: DataStoreRepository) = 35 | DataStoreUseCase(dataStoreRepository) 36 | 37 | } -------------------------------------------------------------------------------- /domain/src/main/java/dev/roshana/domain/models/DataState.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.domain.models 2 | 3 | /** PariSa; 4 | coding and smoking ;) 5 | **/ 6 | 7 | open class DataState( 8 | var isLoading: Boolean = false, 9 | val data: T? = null, 10 | val errorMessage: String = "" 11 | ) -------------------------------------------------------------------------------- /domain/src/main/java/dev/roshana/domain/models/ResponseWrapper.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.domain.models 2 | 3 | import androidx.annotation.Keep 4 | 5 | /** PariSa; 6 | coding and smoking ;) 7 | **/ 8 | 9 | @Keep 10 | class ResponseWrapper { 11 | @Keep 12 | var status = "" 13 | 14 | @Keep 15 | var totalResults: String? = null 16 | 17 | @Keep 18 | var message: String? = null 19 | 20 | @Keep 21 | var articles: T? = null 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/roshana/domain/models/article/Article.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.domain.models.article 2 | 3 | /** PariSa; 4 | coding and smoking ;) 5 | **/ 6 | 7 | data class Article( 8 | val author: String? = null, 9 | val title: String? = null, 10 | val description: String? = null, 11 | val url: String? = null, 12 | val urlToImage: String? = null, 13 | val publishedAt: String? = null, 14 | val content: String? = null 15 | ) -------------------------------------------------------------------------------- /domain/src/main/java/dev/roshana/domain/models/dialogs/Item.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.domain.models.dialogs 2 | 3 | /** PariSa; 4 | coding and smoking ;) 5 | **/ 6 | 7 | data class Item( 8 | val id: Int, 9 | val title: String, 10 | val subtitle: String, 11 | val imageId: Int, 12 | val source: String = "demo source" 13 | ) -------------------------------------------------------------------------------- /domain/src/main/java/dev/roshana/domain/repositories/ArticleRepository.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.domain.repositories 2 | 3 | import androidx.paging.PagingData 4 | import dev.roshana.domain.models.article.Article 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | /** PariSa; 8 | coding and smoking ;) 9 | **/ 10 | 11 | interface ArticleRepository { 12 | suspend fun getTechCrunchNews(source: String? = null): Flow> 13 | } -------------------------------------------------------------------------------- /domain/src/main/java/dev/roshana/domain/repositories/DataStoreRepository.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.domain.repositories 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | /** PariSa; 6 | coding and smoking ;) 7 | **/ 8 | 9 | interface DataStoreRepository { 10 | 11 | suspend fun saveOnOnBoardingState(completed: Boolean) 12 | suspend fun readOnBoardingState(): Flow 13 | 14 | } 15 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/roshana/domain/usecases/ArticleUseCase.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.domain.usecases 2 | 3 | import androidx.paging.PagingData 4 | import dev.roshana.domain.models.article.Article 5 | import dev.roshana.domain.repositories.ArticleRepository 6 | import kotlinx.coroutines.flow.Flow 7 | import javax.inject.Inject 8 | 9 | /** PariSa; 10 | coding and smoking ;) 11 | **/ 12 | 13 | class ArticleUseCase @Inject constructor( 14 | private val articleRepository: ArticleRepository 15 | ) { 16 | suspend operator fun invoke(source: String? = null): Flow> { 17 | return articleRepository.getTechCrunchNews(source) 18 | } 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/roshana/domain/usecases/DataStoreUseCase.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.domain.usecases 2 | 3 | import dev.roshana.domain.repositories.DataStoreRepository 4 | import kotlinx.coroutines.flow.Flow 5 | import javax.inject.Inject 6 | 7 | /** PariSa; 8 | coding and smoking ;) 9 | **/ 10 | 11 | class DataStoreUseCase @Inject constructor( 12 | private val dataStoreRepository: DataStoreRepository 13 | ) { 14 | suspend operator fun invoke(completed: Boolean) { 15 | return dataStoreRepository.saveOnOnBoardingState(completed) 16 | } 17 | 18 | suspend operator fun invoke(): Flow { 19 | return dataStoreRepository.readOnBoardingState() 20 | } 21 | } -------------------------------------------------------------------------------- /domain/src/main/java/dev/roshana/domain/utils/ConfigHelper.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.domain.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import dev.roshana.domain.BuildConfig 7 | 8 | /** PariSa; 9 | coding and smoking ;) 10 | **/ 11 | 12 | fun isDebugMode(): Boolean { 13 | if (BuildConfig.DEBUG) 14 | return true 15 | return false 16 | } 17 | 18 | 19 | const val AUTH_CODE = "auth2" 20 | private const val TEST_PREFERENCE_NAME = "testPreference" 21 | lateinit var sharedPreferences: SharedPreferences 22 | 23 | lateinit var testPreferences: SharedPreferences 24 | 25 | private val authorization: String? 26 | get() = getStringPreference(AUTH_CODE) 27 | 28 | fun initPreferenceUtils(context: Context) { 29 | sharedPreferences = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) 30 | testPreferences = 31 | context.getSharedPreferences(TEST_PREFERENCE_NAME, Context.MODE_PRIVATE) 32 | } 33 | 34 | fun getStringPreference(tag: String): String? { 35 | return sharedPreferences.getString(tag, null) 36 | } 37 | 38 | @SuppressLint("ApplySharedPref") 39 | fun putStringPreference(tag: String, value: String) { 40 | sharedPreferences.edit().putString(tag, value).commit() 41 | } 42 | 43 | fun putIntTestPreference(tag: String, value: Int) { 44 | testPreferences.edit().putInt(tag, value).apply() 45 | } 46 | 47 | fun getIntTestPreference(tag: String, defaultValue: Int = -1): Int { 48 | return testPreferences.getInt(tag, defaultValue) 49 | } 50 | 51 | 52 | fun putLongTesPreference(tag: String, value: Long) { 53 | testPreferences.edit().putLong(tag, value).apply() 54 | } 55 | 56 | fun getLongTestPreference(tag: String): Long { 57 | return testPreferences.getLong(tag, -1L) 58 | } 59 | 60 | fun putLongPreferenceTagStartWith(tag: String, value: Long) { 61 | val editor = sharedPreferences.edit() 62 | for (key in sharedPreferences.all.keys) 63 | if (key.startsWith(tag)) 64 | editor.putLong(key, value) 65 | editor.apply() 66 | } 67 | 68 | fun getLongPreference(tag: String): Long { 69 | return sharedPreferences.getLong(tag, -1L) 70 | } 71 | 72 | fun getIntPreference(tag: String): Int { 73 | return sharedPreferences.getInt(tag, -1) 74 | } 75 | 76 | fun putIntPreference(tag: String, value: Int) { 77 | sharedPreferences.edit().putInt(tag, value).apply() 78 | } 79 | 80 | fun getBooleanPreference(pref: String): Boolean { 81 | return sharedPreferences.getBoolean(pref, false) 82 | } 83 | 84 | fun putBooleanPreference(pref: String, value: Boolean) { 85 | sharedPreferences.edit().putBoolean(pref, value).apply() 86 | } 87 | 88 | fun saveAuthorization(authorization: String) { 89 | putStringPreference( 90 | AUTH_CODE, 91 | authorization 92 | ) 93 | } 94 | 95 | fun getShownIqTests() = sharedPreferences.getStringSet("shownIqTests", mutableSetOf()) 96 | fun isShownIqTests(id: Long) = 97 | sharedPreferences.getStringSet("shownIqTests", mutableSetOf())?.contains(id.toString()) ?: false 98 | 99 | fun putIqTestAsShown(id: Long) = getShownIqTests() 100 | ?.apply { 101 | add(id.toString()) 102 | sharedPreferences.edit().putStringSet("shownIqTests", this).apply() 103 | } 104 | 105 | fun removeIqTestAsShown(id: Long) = getShownIqTests() 106 | ?.apply { 107 | remove(id.toString()) 108 | sharedPreferences.edit().putStringSet("shownIqTests", this).apply() 109 | } 110 | 111 | fun getShownMBTITests() = sharedPreferences.getStringSet("shownMBTITests", mutableSetOf()) 112 | fun isShownMBTITests(id: Long) = 113 | sharedPreferences.getStringSet("shownMBTITests", mutableSetOf())?.contains(id.toString()) 114 | ?: false 115 | 116 | fun putMBTITestAsShown(id: Long) = getShownMBTITests() 117 | ?.apply { 118 | add(id.toString()) 119 | sharedPreferences.edit().putStringSet("shownMBTITests", this).apply() 120 | } 121 | 122 | fun removeMBTITestAsShown(id: Long) = getShownMBTITests() 123 | ?.apply { 124 | remove(id.toString()) 125 | sharedPreferences.edit().putStringSet("shownMBTITests", this).apply() 126 | } 127 | 128 | @Synchronized 129 | fun getToken(): String { 130 | return if (authorization == null) "" else getStringPreference( 131 | AUTH_CODE 132 | ).toString() 133 | } 134 | 135 | @Synchronized 136 | fun removeAuthorization() = sharedPreferences.edit().remove( 137 | AUTH_CODE 138 | ).apply() 139 | 140 | 141 | fun isUserSignedIn() = getToken().isNotEmpty() 142 | 143 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/roshana/domain/utils/Headers.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.domain.utils 2 | 3 | import java.util.* 4 | 5 | /** PariSa; 6 | coding and smoking ;) 7 | **/ 8 | 9 | val globalHeaders by lazy { 10 | mapOf( 11 | Pair("X-CHERAGH-CLIENT", "Android"), 12 | Pair("Accept-Language", Locale.getDefault().country) 13 | ) 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /domain/src/main/res/drawable/first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/first.png -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food1.jpg -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food10.jpg -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food11.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food11.webp -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food12.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food12.webp -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food13.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food13.webp -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food14.jpg -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food15.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food15.webp -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food16.jpg -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food2.jpg -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food3.jpg -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food4.jpg -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food5.webp -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food6.jpg -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food7.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food7.webp -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food8.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food8.webp -------------------------------------------------------------------------------- /domain/src/main/res/drawable/food9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/food9.jpg -------------------------------------------------------------------------------- /domain/src/main/res/drawable/second.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/second.png -------------------------------------------------------------------------------- /domain/src/main/res/drawable/third.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/domain/src/main/res/drawable/third.png -------------------------------------------------------------------------------- /domain/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | اینترنت خود را بررسی نمایید و دوباره امتحان کنید 4 | متاسفانه ارتباط با سرور دچار اخلال شده است لطفا مجددا تلاش کنید 5 | -------------------------------------------------------------------------------- /domain/src/test/java/dev/roshana/domain/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.domain 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | android.enableJetifier=true 19 | # Kotlin code style for this project: "official" or "obsolete": 20 | kotlin.code.style=official 21 | # Enables namespacing of each library's R class so that its R class includes only the 22 | # resources declared in the library itself and none from the library's dependencies, 23 | # thereby reducing the size of the R class for that library 24 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Aug 28 11:54:06 IRDT 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /presentation/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /presentation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import dev.roshana.buildsrc.BuildTypes 2 | import dev.roshana.buildsrc.ConfigData 3 | import dev.roshana.buildsrc.Dependencies 4 | import org.jetbrains.kotlin.config.JvmAnalysisFlags.useIR 5 | 6 | plugins { 7 | id("com.android.library") 8 | id("org.jetbrains.kotlin.android") 9 | id("dagger.hilt.android.plugin") 10 | id("kotlin-kapt") 11 | id("kotlin-parcelize") 12 | } 13 | 14 | android { 15 | compileSdk = ConfigData.compileSdkVersion 16 | 17 | defaultConfig { 18 | minSdk = ConfigData.minSdkVersion 19 | targetSdk = ConfigData.targetSdkVersion 20 | 21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 22 | consumerProguardFiles("consumer-rules.pro") 23 | } 24 | 25 | buildTypes { 26 | getByName(BuildTypes.RELEASE) { 27 | isMinifyEnabled = false 28 | proguardFiles( 29 | getDefaultProguardFile("proguard-android-optimize.txt"), 30 | "proguard-rules.pro" 31 | ) 32 | } 33 | } 34 | 35 | buildFeatures { 36 | compose = true 37 | } 38 | composeOptions { 39 | kotlinCompilerVersion = rootProject.extra["kotlin_version"] as String 40 | kotlinCompilerExtensionVersion = rootProject.extra["compose_version"] as String 41 | } 42 | compileOptions { 43 | sourceCompatibility = JavaVersion.VERSION_1_8 44 | targetCompatibility = JavaVersion.VERSION_1_8 45 | } 46 | kotlinOptions { 47 | jvmTarget = JavaVersion.VERSION_1_8.toString() 48 | useIR 49 | } 50 | } 51 | 52 | dependencies { 53 | 54 | implementation(project(":domain")) 55 | implementation(project(":data")) 56 | 57 | implementation(Dependencies.ComposeLibs.coreKtx) 58 | implementation(Dependencies.CommonLibs.material) 59 | implementation(Dependencies.CommonLibs.accompanistPager) 60 | implementation(Dependencies.CommonLibs.accompanistIndicator) 61 | 62 | /** compose 63 | **/ 64 | implementation(Dependencies.ComposeLibs.composeUi) 65 | implementation(Dependencies.ComposeLibs.composeMaterial) 66 | implementation(Dependencies.ComposeLibs.composeUiPreview) 67 | implementation(Dependencies.ComposeLibs.androidxLifeCycle) 68 | implementation(Dependencies.ComposeLibs.composeActivity) 69 | implementation(Dependencies.ComposeLibs.composeHilt) 70 | implementation(Dependencies.ComposeLibs.composeNav) 71 | implementation(Dependencies.ComposeLibs.composePaging) 72 | implementation(Dependencies.ComposeLibs.composeAnimNavigation) 73 | implementation(Dependencies.ComposeLibs.coreSplash) 74 | 75 | /** hilt - dagger 76 | **/ 77 | implementation(Dependencies.CommonLibs.daggerAndroid) 78 | kapt(Dependencies.CommonLibs.daggerCompiler) 79 | kapt(Dependencies.CommonLibs.daggerAndroidCompiler) 80 | implementation(Dependencies.CommonLibs.hilt) 81 | kapt(Dependencies.CommonLibs.hiltCompiler) 82 | //implementation(Dependencies.CommonLibs.hiltViewModel) 83 | kapt(Dependencies.CommonLibs.hiltViewModelKapt) 84 | 85 | /** Coroutine 86 | **/ 87 | implementation(Dependencies.CommonLibs.coroutines) 88 | implementation(Dependencies.CommonLibs.coroutinesAndroid) 89 | implementation(Dependencies.CommonLibs.coroutineAdapter) 90 | 91 | /** retrofit, okHttp 92 | **/ 93 | implementation(Dependencies.CommonLibs.retrofit) 94 | implementation(Dependencies.CommonLibs.retrofitGson) 95 | implementation(Dependencies.CommonLibs.okHttpInterceptor) 96 | implementation(Dependencies.CommonLibs.gson) 97 | implementation(Dependencies.CommonLibs.kotlinSerialConverter) 98 | implementation(Dependencies.CommonLibs.jsonSerialization) 99 | 100 | /** Room 101 | **/ 102 | /* implementation(Dependencies.CommonLibs.room) 103 | implementation(Dependencies.CommonLibs.roomPagingCompose) 104 | kapt(Dependencies.CommonLibs.roomCompiler)*/ 105 | 106 | /** testing 107 | **/ 108 | testImplementation(Dependencies.TestLibs.junit) 109 | androidTestImplementation(Dependencies.TestLibs.extJunit) 110 | androidTestImplementation(Dependencies.TestLibs.espresso) 111 | androidTestImplementation(Dependencies.TestLibs.composeUiJunit) 112 | debugImplementation(Dependencies.TestLibs.composeUiTooling) 113 | debugImplementation(Dependencies.TestLibs.composeUiTestManifest) 114 | } -------------------------------------------------------------------------------- /presentation/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttyCat/ComposeCleanArch/46eabaf49033cbbced61d07ad34370aa164cbcd8/presentation/consumer-rules.pro -------------------------------------------------------------------------------- /presentation/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /presentation/src/androidTest/java/dev/roshana/presentation/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("dev.roshana.presentation.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /presentation/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/articleUi/ArticleListColumn.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.articleUi 2 | 3 | import androidx.compose.animation.ExperimentalAnimationApi 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.lazy.LazyColumn 6 | import androidx.compose.foundation.lazy.LazyListState 7 | import androidx.compose.foundation.lazy.rememberLazyListState 8 | import androidx.compose.material.Button 9 | import androidx.compose.material.CircularProgressIndicator 10 | import androidx.compose.material.ExperimentalMaterialApi 11 | import androidx.compose.material.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.unit.dp 16 | import androidx.paging.LoadState 17 | import androidx.paging.compose.LazyPagingItems 18 | import androidx.paging.compose.items 19 | import dev.roshana.domain.models.article.Article 20 | 21 | /** PariSa; 22 | coding and smoking ;) 23 | **/ 24 | 25 | @OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class) 26 | @Composable 27 | fun ArticlesListColumn( 28 | modifier: Modifier = Modifier, 29 | items: LazyPagingItems
, 30 | listState: LazyListState = rememberLazyListState(), 31 | navigate: (Int) -> Unit = {} 32 | ) { 33 | LazyColumn( 34 | state = listState, 35 | modifier = modifier 36 | 37 | ) { 38 | 39 | items(items) { article -> 40 | if (article != null) { 41 | ArticleUI(article) {} 42 | } 43 | } 44 | 45 | items.apply { 46 | when { 47 | loadState.refresh is LoadState.Loading -> { 48 | item { 49 | Box( 50 | modifier = Modifier 51 | .fillMaxSize() 52 | .padding( 53 | top = 50.dp, 54 | bottom = 50.dp 55 | ), 56 | contentAlignment = Alignment.Center 57 | ) { 58 | CircularProgressIndicator( 59 | modifier = Modifier.height(30.dp) 60 | ) 61 | } 62 | } 63 | } 64 | 65 | loadState.append is LoadState.Loading -> { 66 | 67 | item { 68 | Box( 69 | modifier = Modifier.fillMaxWidth() 70 | .padding(bottom = 56.dp), 71 | contentAlignment = Alignment.Center 72 | ) { 73 | CircularProgressIndicator(modifier = Modifier.height(30.dp)) 74 | } 75 | } 76 | } 77 | 78 | loadState.refresh is LoadState.Error -> { 79 | val errorMessage = items.loadState.refresh as LoadState.Error 80 | item { 81 | Box( 82 | modifier = Modifier.fillMaxWidth() 83 | .padding(bottom = 56.dp), 84 | contentAlignment = Alignment.BottomCenter 85 | ) { 86 | Column( 87 | modifier = Modifier.fillMaxWidth(), 88 | horizontalAlignment = Alignment.CenterHorizontally 89 | ) { 90 | val errorText = 91 | if (errorMessage.error.localizedMessage!! 92 | .contains("404") 93 | ) "Article not Found" 94 | else 95 | errorMessage.error.localizedMessage 96 | Text(errorText) 97 | /* Button(onClick = { retry() }) { 98 | Text(text = "Try Again") 99 | }*/ 100 | } 101 | } 102 | } 103 | } 104 | 105 | loadState.append is LoadState.Error -> { 106 | val errorMessage = items.loadState.append as LoadState.Error 107 | 108 | item { 109 | Box( 110 | modifier = Modifier.fillMaxWidth() 111 | .padding(bottom = 56.dp), 112 | contentAlignment = Alignment.BottomCenter 113 | ) { 114 | Column( 115 | modifier = Modifier.fillMaxWidth(), 116 | horizontalAlignment = Alignment.CenterHorizontally 117 | ) { 118 | Text(text = errorMessage.error.localizedMessage!!) 119 | Button(onClick = { retry() }) { 120 | Text(text = "Try Again") 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | } 128 | /** WARNING adding this to a lazy column makes iit lose state 129 | item { 130 | Box(modifier = Modifier.height(60.dp) 131 | .padding(56.dp)) 132 | }*/ 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/articleUi/ArticleListState.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.articleUi 2 | 3 | import androidx.paging.PagingData 4 | import dev.roshana.domain.models.DataState 5 | import dev.roshana.domain.models.article.Article 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | /** PariSa; 9 | coding and smoking ;) 10 | **/ 11 | 12 | class ArticleListState( 13 | var loading: Boolean? = null, 14 | var dataList: Flow>? = null, 15 | var errorMsg: String? = "" 16 | ) : DataState>>( 17 | isLoading = loading == false, 18 | data = dataList, 19 | errorMessage = errorMsg!! 20 | ) 21 | 22 | 23 | /* var isLoading: Boolean = false, 24 | val dataList: Flow>? = null, 25 | val errorMessage: String = ""*/ 26 | 27 | -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/articleUi/ArticleListViewModel.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.articleUi 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.State 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import androidx.paging.cachedIn 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import dev.roshana.domain.usecases.ArticleUseCase 11 | import kotlinx.coroutines.CoroutineExceptionHandler 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | 15 | /** PariSa; 16 | coding and smoking ;) 17 | **/ 18 | 19 | @HiltViewModel 20 | class ArticleListViewModel @Inject constructor( 21 | private val articleUseCase: ArticleUseCase 22 | ) : ViewModel() { 23 | 24 | private val _articleListState: MutableState = mutableStateOf( 25 | ArticleListState() 26 | ) 27 | val articleListState: State 28 | get() = _articleListState 29 | 30 | private val handler = CoroutineExceptionHandler { _, exception -> 31 | _articleListState.value.loading = false 32 | _articleListState.value = ArticleListState( 33 | errorMsg = exception.message!! 34 | ) 35 | } 36 | 37 | init { 38 | getArticles() 39 | } 40 | 41 | private fun getArticles() = viewModelScope.launch(handler) { 42 | val response = articleUseCase().cachedIn(viewModelScope) 43 | _articleListState.value = ArticleListState( 44 | dataList = response 45 | ) 46 | 47 | } 48 | } -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/articleUi/ArticleUi.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.articleUi 2 | 3 | import android.widget.Toast 4 | import androidx.compose.animation.ExperimentalAnimationApi 5 | import androidx.compose.animation.animateContentSize 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.material.Card 10 | import androidx.compose.material.ExperimentalMaterialApi 11 | import androidx.compose.material.MaterialTheme 12 | import androidx.compose.material.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.text.font.FontWeight 18 | import androidx.compose.ui.text.style.TextOverflow 19 | import androidx.compose.ui.unit.dp 20 | import dev.roshana.domain.models.article.Article 21 | import kotlinx.coroutines.ExperimentalCoroutinesApi 22 | 23 | /** PariSa; 24 | coding and smoking ;) 25 | **/ 26 | 27 | @OptIn(ExperimentalCoroutinesApi::class) 28 | @ExperimentalMaterialApi 29 | @ExperimentalAnimationApi 30 | @Composable 31 | fun ArticleUI(article: Article, modifier: Modifier = Modifier, onClick: (Int) -> Unit) { 32 | 33 | val context = LocalContext.current 34 | Card( 35 | modifier = modifier 36 | .animateContentSize() 37 | .padding(8.dp) 38 | .clickable { 39 | Toast 40 | .makeText(context, "${article.title}", Toast.LENGTH_LONG) 41 | .show() 42 | }, 43 | shape = RoundedCornerShape(8.dp), 44 | elevation = 8.dp 45 | ) { 46 | Row { 47 | 48 | 49 | /*ImageCard( 50 | imageLink = article.url!!, modifier = Modifier 51 | .fillMaxWidth(0.35f) 52 | )*/ 53 | 54 | ArticleInfo( 55 | article = article, 56 | modifier = Modifier 57 | .fillMaxSize() 58 | .padding(4.dp) 59 | ) 60 | } 61 | } 62 | } 63 | 64 | 65 | @Composable 66 | fun ArticleInfo( 67 | article: Article, 68 | modifier: Modifier = Modifier, 69 | showExtraInfo: Boolean = true, 70 | alignment: Alignment.Horizontal = Alignment.Start 71 | ) { 72 | 73 | Column( 74 | modifier = modifier 75 | .fillMaxHeight() 76 | .fillMaxWidth(), 77 | horizontalAlignment = alignment 78 | ) { 79 | Text( 80 | text = "Article Title : ${article.author}", 81 | fontWeight = FontWeight.Bold, 82 | style = MaterialTheme.typography.h6, 83 | maxLines = 1, 84 | overflow = TextOverflow.Ellipsis 85 | ) 86 | Spacer(modifier = Modifier.height(8.dp)) 87 | 88 | } 89 | } -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/articleUi/ArticlesListScreen.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.articleUi 2 | 3 | import androidx.compose.animation.ExperimentalAnimationApi 4 | import androidx.compose.animation.core.animateDpAsState 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.lazy.rememberLazyListState 10 | import androidx.compose.material.ExperimentalMaterialApi 11 | import androidx.compose.material.Scaffold 12 | import androidx.compose.material.Text 13 | import androidx.compose.material.TopAppBar 14 | import androidx.compose.runtime.* 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.platform.LocalContext 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.unit.max 21 | import androidx.compose.ui.unit.min 22 | import androidx.hilt.navigation.compose.hiltViewModel 23 | import androidx.paging.compose.collectAsLazyPagingItems 24 | import kotlinx.coroutines.ExperimentalCoroutinesApi 25 | 26 | /** PariSa; 27 | coding and smoking ;) 28 | **/ 29 | 30 | @OptIn(ExperimentalCoroutinesApi::class) 31 | @ExperimentalMaterialApi 32 | @ExperimentalAnimationApi 33 | @Composable 34 | fun ArticlesListScreen() { 35 | 36 | val context = LocalContext.current 37 | val viewModel: ArticleListViewModel = hiltViewModel() 38 | val state = viewModel.articleListState.value 39 | val lazyListState = rememberLazyListState() 40 | //val lazyGridState = rememberLazyGridState() 41 | 42 | val showColumn by remember { 43 | mutableStateOf(true) 44 | } 45 | val scope = rememberCoroutineScope() 46 | val toolBarHeight by animateDpAsState( 47 | targetValue = min(56.dp, 100.dp), 48 | ) 49 | 50 | 51 | val articles = state.dataList?.collectAsLazyPagingItems() 52 | Scaffold(topBar = { 53 | TopAppBar( 54 | modifier = Modifier 55 | .height(toolBarHeight), 56 | title = { 57 | Text( 58 | text = "Articles", 59 | color = Color.Black 60 | ) 61 | }, 62 | backgroundColor = Color.White, 63 | ) 64 | }) { 65 | 66 | Box(modifier = Modifier.padding(it)) { 67 | 68 | if (state.errorMessage.isNotEmpty()) { 69 | Box( 70 | modifier = Modifier.fillMaxSize(), 71 | contentAlignment = Alignment.Center 72 | ) { 73 | Text(text = state.errorMessage) 74 | } 75 | } else { 76 | articles?.let { items -> 77 | 78 | if (showColumn) { 79 | ArticlesListColumn(items = items, listState = lazyListState) { articleId -> 80 | 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/articleUi/articleNavigationGraph.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.articleUi 2 | 3 | import androidx.compose.animation.ExperimentalAnimationApi 4 | import androidx.compose.material.ExperimentalMaterialApi 5 | import androidx.navigation.NavGraphBuilder 6 | import androidx.navigation.NavHostController 7 | import com.google.accompanist.navigation.animation.composable 8 | import com.google.accompanist.navigation.animation.navigation 9 | import dev.roshana.presentation.navigation.Articles 10 | import kotlinx.coroutines.ExperimentalCoroutinesApi 11 | 12 | /** PariSa; 13 | coding and smoking ;) 14 | **/ 15 | 16 | @OptIn(ExperimentalAnimationApi::class) 17 | @ExperimentalMaterialApi 18 | @ExperimentalCoroutinesApi 19 | @ExperimentalAnimationApi 20 | fun NavGraphBuilder.articleGraph( 21 | navHostController: NavHostController 22 | ) { 23 | navigation( 24 | startDestination = Articles.ARTICLESLIST, 25 | route = Articles.ARTICLEGRAPH 26 | ) { 27 | 28 | composable( 29 | route = Articles.ARTICLESLIST 30 | ) { 31 | ArticlesListScreen() 32 | } 33 | 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/locationUi/LocationListScreen.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.locationUi 2 | 3 | import androidx.compose.animation.ExperimentalAnimationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.material.ExperimentalMaterialApi 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.text.font.FontWeight 11 | import androidx.compose.ui.text.style.TextAlign 12 | import androidx.compose.ui.unit.sp 13 | import kotlinx.coroutines.ExperimentalCoroutinesApi 14 | 15 | /** PariSa; 16 | coding and smoking ;) 17 | **/ 18 | 19 | @OptIn(ExperimentalCoroutinesApi::class) 20 | @ExperimentalMaterialApi 21 | @ExperimentalAnimationApi 22 | @Composable 23 | fun LocationScreen() { 24 | 25 | Text( 26 | text = "Location Screen", 27 | fontWeight = FontWeight.Bold, 28 | color = Color.White, 29 | textAlign = TextAlign.Center, 30 | fontSize = 20.sp, 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/locationUi/locationNavigationGraph.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.locationUi 2 | 3 | import androidx.compose.animation.ExperimentalAnimationApi 4 | import androidx.compose.animation.core.spring 5 | import androidx.compose.animation.slideInVertically 6 | import androidx.compose.material.ExperimentalMaterialApi 7 | import androidx.navigation.NavGraphBuilder 8 | import androidx.navigation.NavHostController 9 | import com.google.accompanist.navigation.animation.composable 10 | import com.google.accompanist.navigation.animation.navigation 11 | import dev.roshana.presentation.navigation.Articles 12 | import dev.roshana.presentation.navigation.Locations 13 | import kotlinx.coroutines.ExperimentalCoroutinesApi 14 | 15 | /** PariSa; 16 | coding and smoking ;) 17 | **/ 18 | 19 | @ExperimentalMaterialApi 20 | @ExperimentalCoroutinesApi 21 | @ExperimentalAnimationApi 22 | @OptIn(ExperimentalAnimationApi::class) 23 | fun NavGraphBuilder.locationGraph( 24 | navHostController: NavHostController 25 | ) { 26 | navigation( 27 | startDestination = Locations.LOCATIONLIST, 28 | route = Locations.LOCATIONGRAPH 29 | ) { 30 | 31 | composable(route = Locations.LOCATIONLIST){ 32 | LocationScreen() 33 | } 34 | 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/navigation/NavigationDestinations.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.navigation 2 | 3 | /** PariSa; 4 | coding and smoking ;) 5 | **/ 6 | 7 | object Welcome { 8 | const val WELCOMEGRAPH = "welcome" 9 | } 10 | 11 | object Articles { 12 | const val ARTICLEGRAPH = "article" 13 | const val ARTICLESLIST = "articlelist" 14 | const val ARTICLEDETAILS = "articledetails" 15 | } 16 | 17 | object Locations { 18 | const val LOCATIONGRAPH = "location" 19 | const val LOCATIONLIST = "locationList" 20 | } 21 | 22 | object SETTINGS { 23 | const val SETTINGSGRAPH = "settings" 24 | } 25 | -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/navigation/Screens.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.navigation 2 | 3 | import dev.roshana.presentation.navigation.Articles.ARTICLESLIST 4 | import dev.roshana.presentation.navigation.Locations.LOCATIONLIST 5 | import dev.roshana.presentation.navigation.Welcome.WELCOMEGRAPH 6 | 7 | /** PariSa; 8 | coding and smoking ;) 9 | **/ 10 | 11 | sealed class Screens(val route: String) { 12 | object WelcomeScreen : Screens(route = WELCOMEGRAPH) 13 | object ArticleListScreen : Screens(route = ARTICLESLIST) 14 | object LocationScreen : Screens(route = LOCATIONLIST) 15 | } 16 | -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/uiComponent/dialogs/DialogState.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.uiComponent.dialogs 2 | 3 | /** PariSa; 4 | coding and smoking ;) 5 | **/ 6 | 7 | data class DialogState(var showDialog: Boolean, var dialogType: DialogType) -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/uiComponent/dialogs/DialogType.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.uiComponent.dialogs 2 | 3 | /** PariSa; 4 | coding and smoking ;) 5 | **/ 6 | 7 | enum class DialogType { 8 | SIMPLE, TITLE, VERTICALBUTTON, IMAGE, LONGDIALOG, ROUNDED, DATEPICKER, TIMEPICKER 9 | } -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/uiComponent/dialogs/Dialogs.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.uiComponent.dialogs 2 | 3 | import androidx.compose.animation.ExperimentalAnimationApi 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material.* 11 | import androidx.compose.material.MaterialTheme.typography 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.platform.LocalContext 18 | import androidx.compose.ui.res.painterResource 19 | import androidx.compose.ui.unit.dp 20 | import androidx.fragment.app.FragmentActivity 21 | import com.google.android.material.datepicker.MaterialDatePicker 22 | import com.google.android.material.timepicker.MaterialTimePicker 23 | import dev.roshana.domain.R 24 | import dev.roshana.domain.models.dialogs.Item 25 | import kotlinx.coroutines.ExperimentalCoroutinesApi 26 | 27 | /** PariSa; 28 | coding and smoking ;) 29 | **/ 30 | 31 | @OptIn(ExperimentalCoroutinesApi::class) 32 | @ExperimentalMaterialApi 33 | @ExperimentalAnimationApi 34 | @Composable 35 | fun ShowDialog(type: DialogType, onDismiss: () -> Unit) { 36 | val item = remember { item } 37 | 38 | when (type) { 39 | DialogType.SIMPLE -> 40 | AlertDialog( 41 | text = { 42 | Text(item.subtitle) 43 | }, 44 | backgroundColor = Color.White, 45 | confirmButton = { 46 | TextButton( 47 | onClick = onDismiss, 48 | modifier = Modifier.padding(8.dp) 49 | ) { 50 | Text(text = "Ok") 51 | } 52 | }, 53 | onDismissRequest = onDismiss 54 | ) 55 | DialogType.TITLE -> 56 | AlertDialog( 57 | title = { Text(text = item.title, style = typography.h6) }, 58 | text = { 59 | Text(item.subtitle) 60 | }, 61 | confirmButton = { 62 | Row(modifier = Modifier) { 63 | TextButton( 64 | onClick = onDismiss, 65 | modifier = Modifier.padding(4.dp) 66 | ) { 67 | Text(text = "Cancel", color = Color.Gray) 68 | } 69 | TextButton( 70 | onClick = onDismiss, 71 | modifier = Modifier.padding(4.dp) 72 | ) { 73 | Text(text = "Ok") 74 | } 75 | } 76 | }, 77 | onDismissRequest = onDismiss 78 | ) 79 | DialogType.VERTICALBUTTON -> 80 | AlertDialog( 81 | title = { Text(text = item.title, style = typography.h6) }, 82 | text = { 83 | Text(item.subtitle) 84 | }, 85 | confirmButton = { 86 | OutlinedButton( 87 | onClick = onDismiss, 88 | modifier = Modifier 89 | .padding(8.dp) 90 | .width(100.dp) 91 | ) { 92 | Text(text = "Cancel") 93 | } 94 | Button( 95 | onClick = onDismiss, 96 | modifier = Modifier 97 | .padding(8.dp) 98 | .width(100.dp) 99 | ) { 100 | Text(text = "Ok") 101 | } 102 | }, 103 | onDismissRequest = onDismiss 104 | ) 105 | DialogType.IMAGE -> 106 | AlertDialog( 107 | title = { Text(text = item.title, style = typography.h6) }, 108 | text = { 109 | Text(item.subtitle, modifier = Modifier.padding(bottom = 8.dp)) 110 | Image( 111 | painter = painterResource(item.imageId), 112 | contentDescription = null 113 | ) 114 | }, 115 | confirmButton = { 116 | TextButton( 117 | onClick = onDismiss, 118 | modifier = Modifier.padding(8.dp) 119 | ) { 120 | Text(text = "Ok") 121 | } 122 | }, 123 | onDismissRequest = onDismiss 124 | ) 125 | DialogType.LONGDIALOG -> 126 | AlertDialog( 127 | title = { Text(text = item.title, style = typography.h6) }, 128 | text = { 129 | Column { 130 | Text(item.subtitle, modifier = Modifier.padding(bottom = 8.dp)) 131 | Image( 132 | painter = painterResource(item.imageId), 133 | contentDescription = null 134 | ) 135 | Text( 136 | item.subtitle + item.title + item.subtitle + item.title, 137 | style = typography.subtitle2 138 | ) 139 | } 140 | }, 141 | confirmButton = { 142 | TextButton( 143 | onClick = onDismiss, 144 | modifier = Modifier.padding(8.dp) 145 | ) { 146 | Text(text = "Ok") 147 | } 148 | }, 149 | onDismissRequest = onDismiss, 150 | ) 151 | DialogType.ROUNDED -> 152 | AlertDialog( 153 | title = { Text(text = item.title, style = typography.h6) }, 154 | text = { 155 | Column { 156 | Text(item.subtitle, modifier = Modifier.padding(bottom = 8.dp)) 157 | Image( 158 | painter = painterResource(item.imageId), 159 | contentDescription = null, 160 | modifier = Modifier.clip(RoundedCornerShape(16.dp)) 161 | ) 162 | } 163 | }, 164 | confirmButton = { 165 | TextButton( 166 | onClick = onDismiss, 167 | modifier = Modifier.padding(8.dp) 168 | ) { 169 | Text(text = "Ok") 170 | } 171 | }, 172 | onDismissRequest = onDismiss, 173 | shape = RoundedCornerShape(16.dp) 174 | ) 175 | DialogType.DATEPICKER -> { 176 | val context = LocalContext.current 177 | (context as? FragmentActivity)?.supportFragmentManager?.let { manager -> 178 | 179 | val builder = MaterialDatePicker.Builder.datePicker() 180 | .build() 181 | 182 | builder.addOnPositiveButtonClickListener { selectedDate -> 183 | 184 | } 185 | builder.addOnDismissListener { 186 | onDismiss() 187 | } 188 | builder.show(manager, "DatePicker") 189 | } 190 | } 191 | 192 | DialogType.TIMEPICKER -> { 193 | val context = LocalContext.current 194 | (context as? FragmentActivity)?.supportFragmentManager?.let { manager -> 195 | val builder = MaterialTimePicker.Builder() 196 | .build() 197 | builder.addOnPositiveButtonClickListener { 198 | 199 | } 200 | builder.addOnDismissListener { 201 | onDismiss.invoke() 202 | } 203 | builder.show(manager, "TimePicker") 204 | } 205 | } 206 | 207 | } 208 | } 209 | 210 | val item = Item( 211 | 1, 212 | "Awesome List Item", 213 | "Very awesome list item has very awesome subtitle. This is bit long", 214 | R.drawable.food6 215 | ) 216 | -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/uiComponent/progressbar/DialogCircularProgressBar.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.uiComponent.progressbar 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material.CircularProgressIndicator 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.unit.dp 13 | import androidx.compose.ui.window.Dialog 14 | import androidx.compose.ui.window.DialogProperties 15 | 16 | /** PariSa; 17 | coding and smoking ;) 18 | **/ 19 | 20 | @Composable 21 | fun DialogCircularProgressBar( 22 | modifier: Modifier = Modifier 23 | ) { 24 | Dialog( 25 | onDismissRequest = { }, 26 | DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) 27 | ) { 28 | Box( 29 | contentAlignment = Alignment.Center, 30 | modifier = Modifier 31 | .size(100.dp) 32 | .background(Color.Transparent, shape = RoundedCornerShape(8.dp)) 33 | ) { 34 | CircularProgressIndicator( 35 | color = Color.Cyan 36 | ) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/uiComponent/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.uiComponent.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple200 = Color(0xFFBB86FC) 6 | val Purple500 = Color(0xFF6200EE) 7 | val Purple700 = Color(0xFF3700B3) 8 | val Teal200 = Color(0xFF03DAC5) 9 | val lightBlue = Color(0xff2ec7e6) -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/uiComponent/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.uiComponent.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/uiComponent/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.uiComponent.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = darkColors( 10 | primary = Purple200, 11 | primaryVariant = Purple700, 12 | secondary = Teal200 13 | ) 14 | 15 | private val LightColorPalette = lightColors( 16 | primary = Purple500, 17 | primaryVariant = Purple700, 18 | secondary = Teal200 19 | 20 | /* Other default colors to override 21 | background = Color.White, 22 | surface = Color.White, 23 | onPrimary = Color.White, 24 | onSecondary = Color.Black, 25 | onBackground = Color.Black, 26 | onSurface = Color.Black, 27 | */ 28 | ) 29 | 30 | @Composable 31 | fun JetpackComposeBaseArchTheme( 32 | darkTheme: Boolean = isSystemInDarkTheme(), 33 | content: @Composable () -> Unit 34 | ) { 35 | val colors = if (darkTheme) { 36 | DarkColorPalette 37 | } else { 38 | LightColorPalette 39 | } 40 | 41 | MaterialTheme( 42 | colors = colors, 43 | typography = Typography, 44 | shapes = Shapes, 45 | content = content 46 | ) 47 | } -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/uiComponent/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.uiComponent.ui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/welcomeUi/OnBoardingPage.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.welcomeUi 2 | 3 | import androidx.annotation.DrawableRes 4 | import dev.roshana.domain.R 5 | 6 | /** PariSa; 7 | coding and smoking ;) 8 | **/ 9 | 10 | sealed class OnBoardingPage( 11 | @DrawableRes 12 | val image: Int, 13 | val title: String, 14 | val description: String 15 | ) { 16 | object First : OnBoardingPage( 17 | image = R.drawable.first, 18 | title = "Meeting", 19 | description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod." 20 | ) 21 | 22 | object Second : OnBoardingPage( 23 | image = R.drawable.second, 24 | title = "Coordination", 25 | description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod." 26 | ) 27 | 28 | object Third : OnBoardingPage( 29 | image = R.drawable.third, 30 | title = "Dialogue", 31 | description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod." 32 | ) 33 | } -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/welcomeUi/WelcomeScreen.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.welcomeUi 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.ExperimentalAnimationApi 5 | import androidx.compose.foundation.Image 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.material.* 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.platform.LocalContext 13 | import androidx.compose.ui.res.painterResource 14 | import androidx.compose.ui.text.font.FontWeight 15 | import androidx.compose.ui.text.style.TextAlign 16 | import androidx.compose.ui.tooling.preview.Preview 17 | import androidx.compose.ui.unit.dp 18 | import androidx.hilt.navigation.compose.hiltViewModel 19 | import androidx.navigation.NavHostController 20 | import com.google.accompanist.pager.* 21 | import dev.roshana.presentation.navigation.Screens 22 | import dev.roshana.presentation.uiComponent.ui.theme.lightBlue 23 | import kotlinx.coroutines.ExperimentalCoroutinesApi 24 | 25 | /** PariSa; 26 | coding and smoking ;) 27 | **/ 28 | 29 | @OptIn(ExperimentalCoroutinesApi::class, ExperimentalPagerApi::class) 30 | @ExperimentalMaterialApi 31 | @ExperimentalAnimationApi 32 | @Composable 33 | fun WelcomeScreen(navHostController: NavHostController) { 34 | val context = LocalContext.current 35 | val viewModel: WelcomeViewModel = hiltViewModel() 36 | 37 | val pages = listOf( 38 | OnBoardingPage.First, 39 | OnBoardingPage.Second, 40 | OnBoardingPage.Third, 41 | ) 42 | 43 | val pagerState = rememberPagerState() 44 | 45 | Column( 46 | modifier = Modifier.fillMaxSize(), 47 | ) { 48 | 49 | HorizontalPager( 50 | modifier = Modifier.weight(10f), 51 | count = 3, 52 | state = pagerState, 53 | verticalAlignment = Alignment.Top, 54 | ) { position -> 55 | PagerScreen(onBoardingPage = pages[position]) 56 | } 57 | 58 | HorizontalPagerIndicator( 59 | pagerState = pagerState, 60 | modifier = Modifier 61 | .align(Alignment.CenterHorizontally) 62 | .weight(1f) 63 | ) 64 | 65 | FinishButton(modifier = Modifier.weight(1f), pagerState = pagerState) { 66 | // Note : need to pop back to close app from home page 67 | // otherwise it will return to last onBoarding page when you click back. 68 | viewModel.saveOnBoardingState(completed = true) 69 | navHostController.popBackStack() 70 | navHostController.navigate(Screens.ArticleListScreen.route) 71 | } 72 | } 73 | 74 | } 75 | 76 | @Composable 77 | fun PagerScreen(onBoardingPage: OnBoardingPage) { 78 | Column( 79 | modifier = Modifier 80 | .fillMaxWidth(), 81 | horizontalAlignment = Alignment.CenterHorizontally, 82 | verticalArrangement = Arrangement.Top 83 | ) { 84 | Image( 85 | modifier = Modifier 86 | .fillMaxWidth(0.5f) 87 | .fillMaxHeight(0.7f), 88 | painter = painterResource(id = onBoardingPage.image), 89 | contentDescription = "Pager Image" 90 | ) 91 | Text( 92 | modifier = Modifier 93 | .fillMaxWidth(), 94 | text = onBoardingPage.title, 95 | fontSize = MaterialTheme.typography.h4.fontSize, 96 | fontWeight = FontWeight.Bold, 97 | textAlign = TextAlign.Center 98 | ) 99 | Text( 100 | modifier = Modifier 101 | .fillMaxWidth() 102 | .padding(horizontal = 40.dp) 103 | .padding(top = 20.dp), 104 | text = onBoardingPage.description, 105 | fontSize = MaterialTheme.typography.subtitle1.fontSize, 106 | fontWeight = FontWeight.Medium, 107 | textAlign = TextAlign.Center 108 | ) 109 | } 110 | } 111 | 112 | @ExperimentalAnimationApi 113 | @ExperimentalPagerApi 114 | @Composable 115 | fun FinishButton( 116 | modifier: Modifier, 117 | pagerState: PagerState, 118 | onClick: () -> Unit 119 | ) { 120 | Row( 121 | modifier = modifier 122 | .padding(horizontal = 40.dp), 123 | verticalAlignment = Alignment.Top, 124 | horizontalArrangement = Arrangement.Center 125 | ) { 126 | AnimatedVisibility( 127 | modifier = Modifier.fillMaxWidth(), 128 | visible = pagerState.currentPage == 2 129 | ) { 130 | Button( 131 | onClick = onClick, 132 | colors = ButtonDefaults.buttonColors( 133 | backgroundColor = lightBlue, 134 | contentColor = Color.White 135 | ) 136 | ) { 137 | Text(text = "Finish") 138 | } 139 | } 140 | } 141 | } 142 | 143 | @Composable 144 | @Preview(showBackground = true) 145 | fun FirstOnBoardingScreenPreview() { 146 | Column(modifier = Modifier.fillMaxSize()) { 147 | PagerScreen(onBoardingPage = OnBoardingPage.First) 148 | } 149 | } 150 | 151 | @Composable 152 | @Preview(showBackground = true) 153 | fun SecondOnBoardingScreenPreview() { 154 | Column(modifier = Modifier.fillMaxSize()) { 155 | PagerScreen(onBoardingPage = OnBoardingPage.Second) 156 | } 157 | } 158 | 159 | @Composable 160 | @Preview(showBackground = true) 161 | fun ThirdOnBoardingScreenPreview() { 162 | Column(modifier = Modifier.fillMaxSize()) { 163 | PagerScreen(onBoardingPage = OnBoardingPage.Third) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/welcomeUi/WelcomeState.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.welcomeUi 2 | 3 | import dev.roshana.domain.models.DataState 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | /** PariSa; 7 | coding and smoking ;) 8 | **/ 9 | 10 | class WelcomeState( 11 | var loading: Boolean? = true, 12 | var dataList: Flow? = null, 13 | var errorMsg: String? = "" 14 | ) : DataState>( 15 | isLoading = loading == false, 16 | data = dataList, 17 | errorMessage = errorMsg!! 18 | ) 19 | -------------------------------------------------------------------------------- /presentation/src/main/java/dev/roshana/presentation/welcomeUi/WelcomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation.welcomeUi 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.State 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import dev.roshana.domain.usecases.DataStoreUseCase 10 | import dev.roshana.presentation.navigation.Screens 11 | import kotlinx.coroutines.CoroutineExceptionHandler 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.launch 14 | import javax.inject.Inject 15 | 16 | /** PariSa; 17 | coding and smoking ;) 18 | **/ 19 | 20 | @HiltViewModel 21 | class WelcomeViewModel @Inject constructor( 22 | private val dataStoreUseCase: DataStoreUseCase 23 | ) : ViewModel() { 24 | 25 | private val _onBoardingState: MutableState = mutableStateOf(false) 26 | val onBoardingState: State 27 | get() = _onBoardingState 28 | 29 | private val _welcomeState: MutableState = mutableStateOf(WelcomeState()) 30 | val welcomeState: State 31 | get() = _welcomeState 32 | 33 | private val _startDestination: MutableState = 34 | mutableStateOf(Screens.WelcomeScreen.route) 35 | val startDestination: State 36 | get() = _startDestination 37 | 38 | 39 | private val handler = CoroutineExceptionHandler { _, exception -> 40 | _welcomeState.value.loading = false 41 | _welcomeState.value = WelcomeState( 42 | errorMsg = exception.message!! 43 | ) 44 | } 45 | 46 | 47 | init { 48 | readOnBoardingState() 49 | } 50 | 51 | 52 | fun saveOnBoardingState(completed: Boolean) { 53 | viewModelScope.launch { 54 | dataStoreUseCase.invoke(completed) 55 | } 56 | } 57 | 58 | private fun readOnBoardingState() { 59 | viewModelScope.launch(handler) { 60 | dataStoreUseCase.invoke().collect { completed -> 61 | if (completed) { 62 | _startDestination.value = Screens.ArticleListScreen.route 63 | } else { 64 | _startDestination.value = Screens.WelcomeScreen.route 65 | } 66 | } 67 | _welcomeState.value.loading = false 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /presentation/src/test/java/dev/roshana/presentation/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package dev.roshana.presentation 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "JetpackComposeBaseArch" 16 | include(":app") 17 | 18 | include(":data") 19 | include(":domain") 20 | include(":presentation") 21 | --------------------------------------------------------------------------------