diff --git a/.gitignore b/.gitignore index aa724b7..10cfdbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,7 @@ *.iml .gradle /local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml +/.idea .DS_Store /build /captures diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index b589d56..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index a9f4e52..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index e1eea1d..0000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 0ad17cb..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/benchmark-rules.pro b/app/benchmark-rules.pro new file mode 100644 index 0000000..0674e77 --- /dev/null +++ b/app/benchmark-rules.pro @@ -0,0 +1 @@ +-dontobfuscate \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 7586834..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,67 +0,0 @@ -plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' -} - -android { - namespace 'com.example.composecodechallenge' - compileSdk 33 - - defaultConfig { - applicationId "com.example.composecodechallenge" - minSdk 24 - targetSdk 33 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables { - useSupportLibrary true - } - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = '1.8' - } - buildFeatures { - compose true - } - composeOptions { - kotlinCompilerExtensionVersion '1.3.2' - } - packagingOptions { - resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' - } - } -} - -dependencies { - - implementation 'androidx.core:core-ktx:1.8.0' - implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' - implementation 'androidx.activity:activity-compose:1.5.1' - implementation platform('androidx.compose:compose-bom:2022.10.00') - implementation 'androidx.compose.ui:ui' - implementation 'androidx.compose.ui:ui-graphics' - implementation 'androidx.compose.ui:ui-tooling-preview' - implementation 'androidx.compose.material3:material3' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') - androidTestImplementation 'androidx.compose.ui:ui-test-junit4' - debugImplementation 'androidx.compose.ui:ui-tooling' - debugImplementation 'androidx.compose.ui:ui-test-manifest' -} \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..1cabc9a --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,108 @@ +import com.example.buildsrc.Android +import com.example.buildsrc.Libs + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("kotlin-kapt") + id ("dagger.hilt.android.plugin") +} + +android { + namespace = "com.example.composecodechallenge" + compileSdk = Android.compileSdkVersion + + defaultConfig { + applicationId = Android.applicationId + minSdk = Android.minSdkVersion + targetSdk = Android.targetSdkVersion + versionCode = Android.versionCode + versionName = Android.versionName + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + buildConfigField("String", "BASE_URL", "\"https://api.github.com\"") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + create("benchmark") { + signingConfig = signingConfigs.getByName("debug") + matchingFallbacks += listOf("release") + isDebuggable = false + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.4.6" + } + packaging { + resources { + excludes.add("/META-INF/{AL2.0,LGPL2.1}") + } + } +} + +dependencies { + implementation(project(Libs.Modules.data)) + implementation(project(Libs.Modules.domain)) + //--------------------------------------------------------------------------------------------- + implementation(Libs.Jetpack.androidxCore) + implementation (platform(Libs.Common.kotlinBom)) + implementation(Libs.Jetpack.lifecycleRuntime) + implementation(Libs.Jetpack.activityCompose) + implementation (platform(Libs.Jetpack.composeBom)) + implementation(Libs.Jetpack.composeUi) + implementation(Libs.Jetpack.composeUiGraphics) + implementation(Libs.Jetpack.composeUiToolingPreview) + implementation(Libs.Jetpack.composeMaterial3) + implementation(Libs.Jetpack.composeMaterial3WindowSzeClass) + implementation(Libs.Jetpack.lifecycleRuntimeCompose) + implementation(Libs.Jetpack.navigationCompose) + implementation(Libs.Jetpack.trace) + + testImplementation(Libs.Testing.junit) + testImplementation(Libs.Testing.mockitoKotlin) + testImplementation(Libs.Testing.coroutinesTest) + + androidTestImplementation(Libs.Testing.junitEx) + androidTestImplementation(Libs.Testing.espresso) + androidTestImplementation (platform(Libs.Jetpack.composeBom)) + androidTestImplementation(Libs.Testing.composeUiTestJunit4) + debugImplementation(Libs.Jetpack.composeTooling) + debugImplementation(Libs.Testing.composeUiTestManifest) + implementation (Libs.Jetpack.workRuntime) + //---------------------------------------------------------------------------------------------- + implementation(Libs.Jetpack.hiltAndroid) + kapt(Libs.Jetpack.hiltAndroidCompiler) + implementation(Libs.Jetpack.hiltWork) + kapt(Libs.Jetpack.hiltCompiler) + implementation(Libs.Jetpack.hiltNavCompose) + //---------------------------------------------------------------------------------------------- + implementation(Libs.Common.stetho) + implementation(Libs.Common.stetho_OkHttp) + implementation(Libs.Common.okHttpInterceptor) + //---------------------------------------------------------------------------------------------- + implementation(Libs.Common.retrofit) + implementation(Libs.Common.retrofitGson) + //---------------------------------------------------------------------------------------------- + implementation(Libs.Common.arrowCore) + //---------------------------------------------------------------------------------------------- + implementation(Libs.Common.coil) + //---------------------------------------------------------------------------------------------- + implementation(Libs.Common.material) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb43..ff59496 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0c9a0fa..1725c93 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ + + + + diff --git a/app/src/main/java/com/example/composecodechallenge/MainActivity.kt b/app/src/main/java/com/example/composecodechallenge/MainActivity.kt deleted file mode 100644 index bfde89d..0000000 --- a/app/src/main/java/com/example/composecodechallenge/MainActivity.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.composecodechallenge - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import com.example.composecodechallenge.ui.theme.ComposeCodeChallengeTheme - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - ComposeCodeChallengeTheme { - - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/app/App.kt b/app/src/main/java/com/example/composecodechallenge/app/App.kt new file mode 100644 index 0000000..83afb84 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/app/App.kt @@ -0,0 +1,7 @@ +package com.example.composecodechallenge.app + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class App : Application() \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/di/NetworkModule.kt b/app/src/main/java/com/example/composecodechallenge/di/NetworkModule.kt new file mode 100644 index 0000000..d2810b0 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/di/NetworkModule.kt @@ -0,0 +1,77 @@ +package com.example.composecodechallenge.di + +import com.example.composecodechallenge.BuildConfig +import com.example.data.di.Cloud +import com.example.data.di.Mock +import com.example.data.restful.API +import com.example.data.source.cloud.BaseCloudRepository +import com.example.data.source.cloud.CloudMockRepository +import com.example.data.source.cloud.CloudRepository +import com.facebook.stetho.okhttp3.StethoInterceptor +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import dagger.Module +import dagger.Provides +import dagger.Reusable +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + @Reusable + @Provides + fun provideGson(): Gson { + return GsonBuilder().create() + } + + @Reusable + @Provides + fun provideOkHttpClient(): OkHttpClient { + val builder = OkHttpClient.Builder() + if (BuildConfig.DEBUG) { + val loggingInterceptor = HttpLoggingInterceptor() + loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY + builder.addInterceptor(loggingInterceptor) + builder.addNetworkInterceptor(StethoInterceptor()) + } + builder.connectTimeout(20L, TimeUnit.SECONDS) + builder.readTimeout(20L, TimeUnit.SECONDS) + builder.writeTimeout(20L, TimeUnit.SECONDS) + return builder.build() + } + + @Reusable + @Provides + fun provideRetrofit(okHttpClient: OkHttpClient, gson: Gson): Retrofit { + return Retrofit.Builder().client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create(gson)) + .baseUrl(BuildConfig.BASE_URL) + .build() + } + + @Reusable + @Provides + fun provideService(retrofit: Retrofit): API { + return retrofit.create(API::class.java) + } + + @Cloud + @Provides + fun provideCloudRepository(apIs: API): BaseCloudRepository { + return CloudRepository(apIs) + } + + @Mock + @Provides + fun provideCloudMockRepository(): BaseCloudRepository { + return CloudMockRepository() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/di/RepoBuilder.kt b/app/src/main/java/com/example/composecodechallenge/di/RepoBuilder.kt new file mode 100644 index 0000000..515c0e0 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/di/RepoBuilder.kt @@ -0,0 +1,22 @@ +package com.example.composecodechallenge.di + +import com.example.data.repository.GetUserDetailRepoImpl +import com.example.data.repository.GetUsersRepoImpl +import com.example.domain.repository.GetUserDetailRepository +import com.example.domain.repository.GetUsersRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryBuilder { + + @Binds + abstract fun bindUserRepo(userRepoImpl: GetUsersRepoImpl): GetUsersRepository + + @Binds + abstract fun bindUserDetailRepo(userDetailRepoImpl: GetUserDetailRepoImpl): GetUserDetailRepository + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/model/UserDetailItem.kt b/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/model/UserDetailItem.kt new file mode 100644 index 0000000..0474927 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/model/UserDetailItem.kt @@ -0,0 +1,5 @@ +package com.example.composecodechallenge.features.feature_userdetail.model + +data class UserDetailItem( + val avatarUrl: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/model/mapper/UserDetailMapper.kt b/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/model/mapper/UserDetailMapper.kt new file mode 100644 index 0000000..9e897e0 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/model/mapper/UserDetailMapper.kt @@ -0,0 +1,10 @@ +package com.example.composecodechallenge.features.feature_userdetail.model.mapper + +import com.example.composecodechallenge.features.feature_userdetail.model.UserDetailItem +import com.example.domain.model.userdetail.UserDetailModel + +internal fun UserDetailModel.toUserDetailItem(): UserDetailItem { + return UserDetailItem( + avatarUrl.orEmpty() + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/navigation/UserDetailGraph.kt b/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/navigation/UserDetailGraph.kt new file mode 100644 index 0000000..1722f98 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/navigation/UserDetailGraph.kt @@ -0,0 +1,19 @@ +package com.example.composecodechallenge.features.feature_userdetail.navigation + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.navigation +import com.example.composecodechallenge.features.feature_userlist.navigation.USER_ROUTE_PREFIX + +const val USER_GRAPH_ROUTE_PATTERN = "user_graph" + +fun NavGraphBuilder.userDetailGraph( + navController: NavHostController +) { + navigation( + route = USER_GRAPH_ROUTE_PATTERN, + startDestination = USER_ROUTE_PREFIX, + ) { + userDetailsScreen(navController) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/navigation/UserDetailNavigation.kt b/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/navigation/UserDetailNavigation.kt new file mode 100644 index 0000000..69bfab8 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/navigation/UserDetailNavigation.kt @@ -0,0 +1,37 @@ +package com.example.composecodechallenge.features.feature_userdetail.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.example.composecodechallenge.features.feature_userdetail.ui.UserListRoute +import com.example.composecodechallenge.features.feature_userlist.navigation.USER_ROUTE_PREFIX + +private const val USER_DETAILS_ROUTE = "$USER_ROUTE_PREFIX/detail" +const val USER_NAME = "userName" +const val USER_NAME_ARG_SUFFIX = "/{$USER_NAME}" +const val DEFAULT_VALUE = "Koorosh" + +internal fun NavController.navigateToUserDetails( + searchQuery: String, + navOptions: NavOptions? = null +) { + navigate(route = "$USER_DETAILS_ROUTE/$searchQuery", navOptions = navOptions) +} + +fun NavGraphBuilder.userDetailsScreen( + navController: NavController, +) { + composable(route = USER_DETAILS_ROUTE + USER_NAME_ARG_SUFFIX, + arguments = listOf(navArgument(USER_NAME) { + type = NavType.StringType + defaultValue = DEFAULT_VALUE + }) + ) { + UserListRoute( + onBackClick = navController::popBackStack, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/ui/UserDetailRoute.kt b/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/ui/UserDetailRoute.kt new file mode 100644 index 0000000..8410999 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/ui/UserDetailRoute.kt @@ -0,0 +1,20 @@ +package com.example.composecodechallenge.features.feature_userdetail.ui + +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.example.composecodechallenge.features.feature_userdetail.viewmodel.UserDetailViewModel + +@Composable +internal fun UserListRoute( + viewModel: UserDetailViewModel = hiltViewModel(), + onBackClick: () -> Unit, +) { + + val userDetail = viewModel.userDetail.collectAsStateWithLifecycle() + + UserDetailsScreen( + userDetail = userDetail, + onBackClick = onBackClick + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/ui/UserDetailScreen.kt b/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/ui/UserDetailScreen.kt new file mode 100644 index 0000000..e3dd869 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/ui/UserDetailScreen.kt @@ -0,0 +1,60 @@ +package com.example.composecodechallenge.features.feature_userdetail.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import coil.compose.rememberAsyncImagePainter +import com.example.composecodechallenge.features.feature_userdetail.model.UserDetailItem +import com.example.composecodechallenge.main.ui.common.SimpleTopAppBar +import com.example.composecodechallenge.main.ui.theme.ThemePreview + +@Composable +internal fun UserDetailsScreen( + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + userDetail: State, +) { + + Scaffold( + topBar = { + SimpleTopAppBar(title = "", onBackClick = onBackClick) + }, + content = { + UserDetailScreenContent( + imageUrl = userDetail.value.avatarUrl, + modifier = modifier.padding(it), + ) + } + ) +} + +@Composable +internal fun UserDetailScreenContent( + imageUrl: String, + modifier: Modifier = Modifier +) { + Image( + painter = rememberAsyncImagePainter(model = imageUrl), + contentDescription = "", + modifier = Modifier + .fillMaxWidth() + .clip(CircleShape) + .then(modifier) + ) +} + +@ThemePreview +@Composable +internal fun UserDetailsScreenPreview() { + UserDetailsScreen( + onBackClick = {}, + userDetail = mutableStateOf(UserDetailItem("")) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/viewmodel/UserDetailViewModel.kt b/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/viewmodel/UserDetailViewModel.kt new file mode 100644 index 0000000..f3e1fb3 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/features/feature_userdetail/viewmodel/UserDetailViewModel.kt @@ -0,0 +1,57 @@ +package com.example.composecodechallenge.features.feature_userdetail.viewmodel + +import android.util.Log +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.composecodechallenge.features.feature_userdetail.model.UserDetailItem +import com.example.composecodechallenge.features.feature_userdetail.model.mapper.toUserDetailItem +import com.example.domain.model.error.Error +import com.example.domain.model.userdetail.UserDetailModel +import com.example.domain.usecase.GetUserDetailUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@HiltViewModel +class UserDetailViewModel @Inject constructor( + private val getUserDetailUseCase: GetUserDetailUseCase, + savedStateHandle: SavedStateHandle, +) : + ViewModel() { + + private val userName: String = checkNotNull(savedStateHandle["userName"]) + + private val _userDetail = MutableStateFlow(UserDetailItem("")) + val userDetail = _userDetail.asStateFlow() + + init { + getUserDetail() + } + + private fun getUserDetail() { + viewModelScope.launch { + getUserDetailUseCase.getUserDetail(userName).fold( + ifRight = ::onSuccessResponse, + ifLeft = ::onErrorResponse + ) + } + } + + private fun onErrorResponse(error: Error) { + Log.i("error", error.toString()) + } + + private fun onSuccessResponse(questionList: UserDetailModel) { + viewModelScope.launch { + withContext(Dispatchers.Default) { + _userDetail.value = questionList.toUserDetailItem() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/model/UserItem.kt b/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/model/UserItem.kt new file mode 100644 index 0000000..abcdf07 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/model/UserItem.kt @@ -0,0 +1,7 @@ +package com.example.composecodechallenge.features.feature_userlist.model + +data class UserItem( + val id: Int, + val avatarUrl: String, + val userName: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/model/mapper/UserListMapper.kt b/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/model/mapper/UserListMapper.kt new file mode 100644 index 0000000..5e2367f --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/model/mapper/UserListMapper.kt @@ -0,0 +1,12 @@ +package com.example.composecodechallenge.features.feature_userlist.model.mapper + +import com.example.composecodechallenge.features.feature_userlist.model.UserItem +import com.example.domain.model.userlist.UserModel + +internal fun UserModel.toUserItem(): UserItem { + return UserItem( + id, + avatarUrl.orEmpty(), + login.orEmpty(), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/navigation/UserListNavigation.kt b/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/navigation/UserListNavigation.kt new file mode 100644 index 0000000..2dd222f --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/navigation/UserListNavigation.kt @@ -0,0 +1,17 @@ +package com.example.composecodechallenge.features.feature_userlist.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.example.composecodechallenge.features.feature_userdetail.navigation.USER_NAME_ARG_SUFFIX +import com.example.composecodechallenge.features.feature_userdetail.navigation.navigateToUserDetails +import com.example.composecodechallenge.features.feature_userlist.ui.UserListRoute + +internal const val USER_ROUTE_PREFIX = "user" +internal const val USER_LIST_ROUTE = "${USER_ROUTE_PREFIX}_list_route$USER_NAME_ARG_SUFFIX" + +fun NavGraphBuilder.userListScreen(navController: NavController) { + composable(route = USER_LIST_ROUTE) { + UserListRoute(navigateToUserDetails = navController::navigateToUserDetails) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/ui/UserListScreen.kt b/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/ui/UserListScreen.kt new file mode 100644 index 0000000..45ecc51 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/ui/UserListScreen.kt @@ -0,0 +1,160 @@ +package com.example.composecodechallenge.features.feature_userlist.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import coil.compose.rememberAsyncImagePainter +import com.example.composecodechallenge.R +import com.example.composecodechallenge.features.feature_userlist.model.UserItem +import com.example.composecodechallenge.main.ui.common.SimpleTopAppBar +import com.example.composecodechallenge.main.ui.theme.ThemePreview +import com.example.composecodechallenge.main.ui.theme.space + +@Composable +internal fun UserListScreen( + modifier: Modifier = Modifier, + searchQueryTextState: State, + onSearchQueryChange: (String) -> Unit, + users: State>, + navigateToUserDetails: (String) -> Unit, +) { + Scaffold( + topBar = { + SimpleTopAppBar(title = stringResource(id = R.string.top_bar_title)) + }, + content = { + UserListScreenContent( + modifier = modifier.padding(it), + searchQueryTextState = searchQueryTextState, + onSearchQueryChange = onSearchQueryChange, + users = users, + navigateToUserDetails, + ) + } + ) +} + +@Composable +fun UserListScreenContent( + modifier: Modifier = Modifier, + searchQueryTextState: State, + onSearchQueryChange: (String) -> Unit, + users: State>, + navigateToUserDetails: (String) -> Unit, +) { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = MaterialTheme.space.small), + ) { + TextField(searchQueryTextState, onSearchQueryChange) + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .weight(1f) + .testTag("list_item"), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.space.sMedium), + + ) { + items(users.value, key = { + it.id + }) { + Card( + modifier = Modifier + .fillMaxSize() + .clickable { + navigateToUserDetails.invoke(it.userName) + }, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + + Spacer(Modifier.width(MaterialTheme.space.small)) + + Image( + painter = rememberAsyncImagePainter(model = it.avatarUrl), + contentDescription = it.userName, + modifier = Modifier + .size(MaterialTheme.space.xLarge) + .clip(CircleShape) + ) + + Spacer(Modifier.width(MaterialTheme.space.small)) + + Text( + text = it.userName, + modifier = Modifier + .fillMaxSize() + .padding(vertical = MaterialTheme.space.medium) + ) + } + } + + } + } + } + +} + +@Composable +internal fun TextField( + searchQueryTextState: State, + onSearchQueryChange: (String) -> Unit +) { + OutlinedTextField( + value = searchQueryTextState.value.orEmpty(), + onValueChange = { + onSearchQueryChange(it) + }, + placeholder = { Text(text = stringResource(id = R.string.hint_search)) }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + modifier = Modifier + .padding(MaterialTheme.space.small) + .fillMaxWidth() + .testTag("search_users") + ) +} + +@ThemePreview +@Composable +internal fun UserListPreview() { + UserListScreen( + searchQueryTextState = mutableStateOf("Search..."), + onSearchQueryChange = {}, + users = mutableStateOf(emptyList()), + navigateToUserDetails = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/ui/UserListScreenRoute.kt b/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/ui/UserListScreenRoute.kt new file mode 100644 index 0000000..1179b33 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/ui/UserListScreenRoute.kt @@ -0,0 +1,22 @@ +package com.example.composecodechallenge.features.feature_userlist.ui + +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.example.composecodechallenge.features.feature_userlist.viewmodel.UserListViewModel + +@Composable +internal fun UserListRoute( + viewModel: UserListViewModel = hiltViewModel(), + navigateToUserDetails: (String) -> Unit, +) { + val searchQueryTextState = viewModel.searchQueryText.collectAsStateWithLifecycle() + val users = viewModel.usersState.collectAsStateWithLifecycle() + + UserListScreen( + searchQueryTextState = searchQueryTextState, + onSearchQueryChange = viewModel::onSearchQueryChange, + users = users, + navigateToUserDetails = navigateToUserDetails, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/viewmodel/UserListViewModel.kt b/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/viewmodel/UserListViewModel.kt new file mode 100644 index 0000000..60b7c7f --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/features/feature_userlist/viewmodel/UserListViewModel.kt @@ -0,0 +1,86 @@ +package com.example.composecodechallenge.features.feature_userlist.viewmodel + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.composecodechallenge.features.feature_userlist.model.UserItem +import com.example.composecodechallenge.features.feature_userlist.model.mapper.toUserItem +import com.example.domain.model.error.Error +import com.example.domain.model.userlist.UserModel +import com.example.domain.usecase.GetUsersUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@HiltViewModel +class UserListViewModel @Inject constructor( + private val getUsersUseCase: GetUsersUseCase +) : ViewModel(){ + + private val _usersState = MutableStateFlow(emptyList()) + val usersState = _usersState.asStateFlow() + + private val _searchQueryText = MutableStateFlow(null) + val searchQueryText = _searchQueryText.asStateFlow() + + private var searchJob: Job? = null + + init { + startToCollectSearchQueries() + } + + private fun startToCollectSearchQueries() { + searchQueryText + .filterNotNull() + .debounce(QUERY_DEBOUNCE_IN_MILLIS) + .onEach(::getUsers) + .flowOn(Dispatchers.IO) + .launchIn(viewModelScope) + } + + fun onSearchQueryChange(query: String) { + _searchQueryText.value = query + } + + private fun getUsers(query: String) { + searchJob?.cancel() + + if (query.isBlank()) { + _usersState.value = emptyList() + return + } + + searchJob = viewModelScope.launch { + getUsersUseCase.getUsers(query).fold( + ifRight = ::onSuccessResponse, + ifLeft = ::onErrorResponse + ) + } + } + + private fun onSuccessResponse(users: List) { + viewModelScope.launch { + withContext(Dispatchers.Default) { + _usersState.value = users.map { it.toUserItem() } + } + } + } + + private fun onErrorResponse(error: Error) { + Log.i("error", error.toString()) + } + + private companion object { + private const val QUERY_DEBOUNCE_IN_MILLIS = 500L + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/main/navigation/ItollNavHost.kt b/app/src/main/java/com/example/composecodechallenge/main/navigation/ItollNavHost.kt new file mode 100644 index 0000000..8a75efb --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/main/navigation/ItollNavHost.kt @@ -0,0 +1,39 @@ +package com.example.composecodechallenge.main.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import com.example.composecodechallenge.features.feature_userdetail.navigation.userDetailGraph +import com.example.composecodechallenge.features.feature_userlist.navigation.USER_LIST_ROUTE +import com.example.composecodechallenge.features.feature_userlist.navigation.userListScreen + +/** + * Top-level navigation graph. Navigation is organized as explained at + * https://d.android.com/jetpack/compose/nav-adaptive + * + * The navigation graph defined in this file defines the different top level routes. Navigation + * within each route is handled using state and Back Handlers. + */ +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun ItollNavHost( + navController: NavHostController, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + startDestination: String = USER_LIST_ROUTE, +) { + NavHost( + navController = navController, + startDestination = startDestination, + modifier = Modifier.semantics { + testTagsAsResourceId = true + }.then(modifier) + ) { + userListScreen(navController) + userDetailGraph(navController) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/main/ui/ItollApp.kt b/app/src/main/java/com/example/composecodechallenge/main/ui/ItollApp.kt new file mode 100644 index 0000000..b203256 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/main/ui/ItollApp.kt @@ -0,0 +1,18 @@ +package com.example.composecodechallenge.main.ui + +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.runtime.Composable +import com.example.composecodechallenge.main.navigation.ItollNavHost + +@Composable +fun ItollApp( + windowSizeClass: WindowSizeClass, + appState: ItollAppState = rememberItollAppState( + windowSizeClass = windowSizeClass + ), +) { + ItollNavHost( + navController = appState.navController, + onBackClick = appState::onBackClick + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/main/ui/ItollAppState.kt b/app/src/main/java/com/example/composecodechallenge/main/ui/ItollAppState.kt new file mode 100644 index 0000000..d033b48 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/main/ui/ItollAppState.kt @@ -0,0 +1,29 @@ +package com.example.composecodechallenge.main.ui + +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import kotlinx.coroutines.CoroutineScope + +@Composable +fun rememberItollAppState( + windowSizeClass: WindowSizeClass, + coroutineScope: CoroutineScope = rememberCoroutineScope(), + navController: NavHostController = rememberNavController(), +): ItollAppState { + return remember(navController, coroutineScope, windowSizeClass) { + ItollAppState(navController) + } +} + +@Stable +class ItollAppState(val navController: NavHostController) { + + fun onBackClick() { + navController.navigateUp() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/main/ui/MainActivity.kt b/app/src/main/java/com/example/composecodechallenge/main/ui/MainActivity.kt new file mode 100644 index 0000000..c4fba4b --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/main/ui/MainActivity.kt @@ -0,0 +1,26 @@ +package com.example.composecodechallenge.main.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import com.example.composecodechallenge.main.ui.theme.ItollTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + ItollTheme { + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) + ItollApp( + windowSizeClass = calculateWindowSizeClass( + this + ) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/main/ui/common/SimpleTobBar.kt b/app/src/main/java/com/example/composecodechallenge/main/ui/common/SimpleTobBar.kt new file mode 100644 index 0000000..07c60ec --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/main/ui/common/SimpleTobBar.kt @@ -0,0 +1,79 @@ +package com.example.composecodechallenge.main.ui.common + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import com.example.composecodechallenge.R +import com.example.composecodechallenge.main.ui.theme.ThemePreview +import com.example.composecodechallenge.main.ui.theme.space + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SimpleTopAppBar( + title: String, + onBackClick: () -> Unit +) { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background, + navigationIconContentColor = MaterialTheme.colorScheme.onBackground + ), + title = { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onBackground + ) + }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + modifier = Modifier.size(MaterialTheme.space.xMedium).testTag("back"), + painter = painterResource(id = R.drawable.ic_back), + contentDescription = "back", + tint = MaterialTheme.colorScheme.onBackground + ) + } + } + ) +} + +/** + * SimpleTopAppBar for our first page. We do not need navigation icon in first page + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SimpleTopAppBar( + title: String, +) { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background, + navigationIconContentColor = MaterialTheme.colorScheme.onBackground + ), + title = { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onBackground + ) + }, + ) +} + +@ThemePreview +@Composable +fun PreviewSimpleTopBar() { + SimpleTopAppBar(title = "Simple toolbar") { + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/main/ui/theme/Color.kt b/app/src/main/java/com/example/composecodechallenge/main/ui/theme/Color.kt new file mode 100644 index 0000000..357c5ea --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/main/ui/theme/Color.kt @@ -0,0 +1,38 @@ +package com.example.composecodechallenge.main.ui.theme + +import androidx.compose.ui.graphics.Color + +internal val Blue10 = Color(0xFF001F28) +internal val Blue20 = Color(0xFF003544) +internal val Blue30 = Color(0xFF004D61) +internal val Blue40 = Color(0xFF006780) +internal val Blue80 = Color(0xFF5DD5FC) +internal val Blue90 = Color(0xFFB8EAFF) +internal val DarkPurpleGray10 = Color(0xFF201A1B) +internal val DarkPurpleGray20 = Color(0xFF362F30) +internal val DarkPurpleGray90 = Color(0xFFECDFE0) +internal val DarkPurpleGray95 = Color(0xFFFAEEEF) +internal val DarkPurpleGray99 = Color(0xFFFCFCFC) +internal val Orange10 = Color(0xFF380D00) +internal val Orange20 = Color(0xFF5B1A00) +internal val Orange30 = Color(0xFF812800) +internal val Orange40 = Color(0xFFA23F16) +internal val Orange80 = Color(0xFFFFB59B) +internal val Orange90 = Color(0xFFFFDBCF) +internal val Purple10 = Color(0xFF36003C) +internal val Purple20 = Color(0xFF560A5D) +internal val Purple30 = Color(0xFF702776) +internal val Purple40 = Color(0xFF8B418F) +internal val Purple80 = Color(0xFFFFA9FE) +internal val Purple90 = Color(0xFFFFD6FA) +internal val PurpleGray30 = Color(0xFF4D444C) +internal val PurpleGray50 = Color(0xFF7F747C) +internal val PurpleGray60 = Color(0xFF998D96) +internal val PurpleGray80 = Color(0xFFD0C3CC) +internal val PurpleGray90 = Color(0xFFEDDEE8) +internal val Red10 = Color(0xFF410002) +internal val Red20 = Color(0xFF690005) +internal val Red30 = Color(0xFF93000A) +internal val Red40 = Color(0xFFBA1A1A) +internal val Red80 = Color(0xFFFFB4AB) +internal val Red90 = Color(0xFFFFDAD6) \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/main/ui/theme/Space.kt b/app/src/main/java/com/example/composecodechallenge/main/ui/theme/Space.kt new file mode 100644 index 0000000..336d83c --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/main/ui/theme/Space.kt @@ -0,0 +1,30 @@ +package com.example.composecodechallenge.main.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Immutable +class Space { + + val tiny: Dp = 2.dp + val xSmall: Dp = 4.dp + val small: Dp = 8.dp + val sMedium = 12.dp + val medium: Dp = 16.dp + val xMedium: Dp = 24.dp + val large: Dp = 32.dp + val xLarge: Dp = 48.dp + val xxLarge: Dp = 64.dp +} + +val LocalSpace = staticCompositionLocalOf { Space() } + +val MaterialTheme.space: Space + @Composable + @ReadOnlyComposable + get() = LocalSpace.current \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/main/ui/theme/Theme.kt b/app/src/main/java/com/example/composecodechallenge/main/ui/theme/Theme.kt new file mode 100644 index 0000000..6bd4b5c --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/main/ui/theme/Theme.kt @@ -0,0 +1,79 @@ +package com.example.composecodechallenge.main.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +val LightColorScheme = lightColorScheme( + primary = Purple40, + onPrimary = Color.White, + primaryContainer = Purple90, + onPrimaryContainer = Purple10, + secondary = Orange40, + onSecondary = Color.White, + secondaryContainer = Orange90, + onSecondaryContainer = Orange10, + tertiary = Blue40, + onTertiary = Color.White, + tertiaryContainer = Blue90, + onTertiaryContainer = Blue10, + error = Red40, + onError = Color.White, + errorContainer = Red90, + onErrorContainer = Red10, + background = DarkPurpleGray99, + onBackground = DarkPurpleGray10, + surface = DarkPurpleGray99, + onSurface = DarkPurpleGray10, + surfaceVariant = PurpleGray90, + onSurfaceVariant = PurpleGray30, + inverseSurface = DarkPurpleGray20, + inverseOnSurface = DarkPurpleGray95, + outline = PurpleGray50, +) + +val DarkColorScheme = darkColorScheme( + primary = Purple80, + onPrimary = Purple20, + primaryContainer = Purple30, + onPrimaryContainer = Purple90, + secondary = Orange80, + onSecondary = Orange20, + secondaryContainer = Orange30, + onSecondaryContainer = Orange90, + tertiary = Blue80, + onTertiary = Blue20, + tertiaryContainer = Blue30, + onTertiaryContainer = Blue90, + error = Red80, + onError = Red20, + errorContainer = Red30, + onErrorContainer = Red90, + background = DarkPurpleGray10, + onBackground = DarkPurpleGray90, + surface = DarkPurpleGray10, + onSurface = DarkPurpleGray90, + surfaceVariant = PurpleGray30, + onSurfaceVariant = PurpleGray80, + inverseSurface = DarkPurpleGray90, + inverseOnSurface = DarkPurpleGray10, + outline = PurpleGray60, +) + +@Composable +fun ItollTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + + val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/main/ui/theme/ThemePreview.kt b/app/src/main/java/com/example/composecodechallenge/main/ui/theme/ThemePreview.kt new file mode 100644 index 0000000..d582ff5 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/main/ui/theme/ThemePreview.kt @@ -0,0 +1,16 @@ +package com.example.composecodechallenge.main.ui.theme + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview + +@Preview(name = "Day Mode", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Night Mode", uiMode = Configuration.UI_MODE_NIGHT_YES) +annotation class ThemePreview + +@Composable +fun ItollPreviewTheme(content: @Composable () -> Unit) { + ItollTheme { + content() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/main/ui/theme/Type.kt b/app/src/main/java/com/example/composecodechallenge/main/ui/theme/Type.kt new file mode 100644 index 0000000..1b1f527 --- /dev/null +++ b/app/src/main/java/com/example/composecodechallenge/main/ui/theme/Type.kt @@ -0,0 +1,18 @@ +package com.example.composecodechallenge.main.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) +) \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/ui/theme/Color.kt b/app/src/main/java/com/example/composecodechallenge/ui/theme/Color.kt deleted file mode 100644 index 34c7f76..0000000 --- a/app/src/main/java/com/example/composecodechallenge/ui/theme/Color.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.composecodechallenge.ui.theme - -import androidx.compose.ui.graphics.Color - -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/ui/theme/Theme.kt b/app/src/main/java/com/example/composecodechallenge/ui/theme/Theme.kt deleted file mode 100644 index 5e5dadf..0000000 --- a/app/src/main/java/com/example/composecodechallenge/ui/theme/Theme.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.example.composecodechallenge.ui.theme - -import android.app.Activity -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.core.view.WindowCompat - -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ -) - -@Composable -fun ComposeCodeChallengeTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit -) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme - } - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/example/composecodechallenge/ui/theme/Type.kt b/app/src/main/java/com/example/composecodechallenge/ui/theme/Type.kt deleted file mode 100644 index f50c2d6..0000000 --- a/app/src/main/java/com/example/composecodechallenge/ui/theme/Type.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.composecodechallenge.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -// Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml new file mode 100644 index 0000000..62add91 --- /dev/null +++ b/app/src/main/res/drawable/ic_back.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 52e6691..1f2f583 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ ComposeCodeChallenge + Compose Code Challenge + Search... \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 37a3841..92c4ab5 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,5 @@ -