diff --git a/README.md b/README.md index b29aa74..127f25e 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Gradle JDK: JetBrains Runtime version 17.0.6/...** 0. Jetpack [Read here](https://developer.android.com/jetpack/getting-started) 0. Hilt & Jetpack [Read here](https://developer.android.com/jetpack/androidx/releases/hilt) 0. Compose [Read here](https://developer.android.com/jetpack/androidx/releases/compose-ui) +0. Compose State [Read here](https://developer.android.com/jetpack/compose/state) 0. MVVM [Read here](https://blog.mindorks.com/mvc-mvp-mvvm-architecture-in-android) 0. View Models [Read here](https://developer.android.com/topic/libraries/architecture/viewmodel) 0. DataModel [Read here](https://developer.android.com/topic/libraries/architecture/viewmodel) diff --git a/app/build.gradle b/app/build.gradle index d650c35..801deb2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,14 +51,34 @@ android { dependencies { implementation 'androidx.core:core-ktx:1.8.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' + implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) + + implementation "androidx.navigation:navigation-runtime-ktx:$nav_version" + + /*Compose*/ + implementation 'androidx.compose.material:material' 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' - implementation 'androidx.navigation:navigation-runtime-ktx:2.5.3' + + /*Dependency injection library*/ + implementation "com.google.dagger:hilt-android:$hilt_version" + kapt "com.google.dagger:hilt-android-compiler:$hilt_version" + + /*Dependency injection and compose library*/ + implementation "androidx.hilt:hilt-navigation-compose:$hilt_nav_version" + + /*ViewModel libraries*/ + implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + implementation "androidx.compose.runtime:runtime-livedata:$livedata_version" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-runtime-compose:$uistate_version" + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' @@ -66,11 +86,4 @@ dependencies { androidTestImplementation 'androidx.compose.ui:ui-test-junit4' debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' - - /*Dependency injection library*/ - implementation "com.google.dagger:hilt-android:$hilt_version" - kapt "com.google.dagger:hilt-android-compiler:$hilt_version" - - /*Dependency injection and compose library*/ - implementation "androidx.hilt:hilt-navigation-compose:1.0.0" } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0c2100b..a984700 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ diff --git a/app/src/main/java/com/example/flickrapp/MainActivity.kt b/app/src/main/java/com/example/flickrapp/MainActivity.kt index 749ec66..a069ac9 100644 --- a/app/src/main/java/com/example/flickrapp/MainActivity.kt +++ b/app/src/main/java/com/example/flickrapp/MainActivity.kt @@ -4,9 +4,8 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -20,12 +19,10 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { FlickrAppTheme { - // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background + color = MaterialTheme.colors.background ) { -// Greeting("Android") NavigationApp() } } diff --git a/app/src/main/java/com/example/flickrapp/presentation/FlickrViewModel.kt b/app/src/main/java/com/example/flickrapp/presentation/FlickrViewModel.kt index c63e58e..13996f1 100644 --- a/app/src/main/java/com/example/flickrapp/presentation/FlickrViewModel.kt +++ b/app/src/main/java/com/example/flickrapp/presentation/FlickrViewModel.kt @@ -1,6 +1,101 @@ package com.example.flickrapp.presentation import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update class FlickrViewModel(): ViewModel() { -} \ No newline at end of file + + private val _searchText = MutableStateFlow("") + val searchText = _searchText.asStateFlow() + + + private val _isSearching = MutableStateFlow(false) + val isSearching = _isSearching.asStateFlow() + + private val _picture = MutableStateFlow(pictureList) + val pictures = searchText + .debounce(500L) + .onEach { _isSearching.update { true } } + .combine(_picture) { text, pictureMatches -> + if(text.isBlank()) { + pictureMatches + } else { + delay(500L) + pictureMatches.filter { + it.doesMatchSearchQuery(text) + } + } + } + .onEach { _isSearching.update { false } } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(1000), + _picture.value + ) + fun onSearchTextChange(text: String) { + _searchText.update { text } + } +} + +private val pictureList = listOf( + Picture( + name = "sunset", + ), + Picture( + name = "forest", + ), + Picture( + name = "dog", + ), + Picture( + name = "food", + ), + Picture( + name = "travel", + ), + Picture( + name = "computer", + ), + Picture( + name = "cat", + ), + Picture( + name = "shoes", + ), + Picture( + name = "selfie", + ), + Picture( + name = "beach", + ), + Picture( + name = "house", + ), + Picture( + name = "cars", + ), +) + +data class Picture( + val name: String, +) { + fun doesMatchSearchQuery(query: String): Boolean { + val matchingCombinations = listOf( + "$name", + "${name.first()}}", + ) + + return matchingCombinations.any { + it.contains(query, ignoreCase = true) + } + } +} diff --git a/app/src/main/java/com/example/flickrapp/presentation/home/FlickrHomeScreen.kt b/app/src/main/java/com/example/flickrapp/presentation/home/FlickrHomeScreen.kt index d173bd0..53f7420 100644 --- a/app/src/main/java/com/example/flickrapp/presentation/home/FlickrHomeScreen.kt +++ b/app/src/main/java/com/example/flickrapp/presentation/home/FlickrHomeScreen.kt @@ -1,17 +1,74 @@ package com.example.flickrapp.presentation +import androidx.compose.foundation.ExperimentalFoundationApi +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.material3.Text +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController +import com.example.flickrapp.R +import com.example.flickrapp.presentation.home.ImagesGrid +import com.example.flickrapp.presentation.utils.Loading +import com.example.flickrapp.ui.theme.AppDimension +import com.example.flickrapp.utils.TopAppBarFlickr +@OptIn(ExperimentalFoundationApi::class) @Composable fun FlickrHomeScreen(navController: NavController, viewModel: FlickrViewModel) { - Text( - text = "Hello Flickr!", - modifier = Modifier.fillMaxSize(), - color = Color.Red - ) + val searchText by viewModel.searchText.collectAsStateWithLifecycle() + val isSearching by viewModel.isSearching.collectAsStateWithLifecycle() + val pictures by viewModel.pictures.collectAsStateWithLifecycle() + + + Column() { + Row( + modifier = Modifier.fillMaxWidth() + ) { + TopAppBarFlickr(title = stringResource(R.string.app_name)) { + navController.backQueue + } + } + Row( + modifier = Modifier + .fillMaxSize() + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(AppDimension.smallPadding) + ) { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = searchText, + onValueChange = { viewModel.onSearchTextChange(it) }, + trailingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = stringResource(R.string.search) + ) + }, + placeholder = { Text(text = stringResource(R.string.search)) } + ) + Spacer(modifier = Modifier.height(AppDimension.normalPadding)) + if (isSearching) { + Loading() + } else { + ImagesGrid(pictures) + } + } + } + } } diff --git a/app/src/main/java/com/example/flickrapp/presentation/home/ImageGrid.kt b/app/src/main/java/com/example/flickrapp/presentation/home/ImageGrid.kt new file mode 100644 index 0000000..c009fe3 --- /dev/null +++ b/app/src/main/java/com/example/flickrapp/presentation/home/ImageGrid.kt @@ -0,0 +1,85 @@ +package com.example.flickrapp.presentation.home + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.example.flickrapp.R +import com.example.flickrapp.presentation.Picture +import com.example.flickrapp.ui.theme.AppDimension +import com.example.flickrapp.ui.theme.ExtendedTheme +import kotlin.random.Random + + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ImagesGrid(pictures: List) { + LazyVerticalStaggeredGrid( + modifier = Modifier + .fillMaxSize(), + columns = StaggeredGridCells.Fixed(2), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(pictures.size) { + ItemBox(pictures[it]) + } + } +} + +@Composable +fun ItemBox(item: Picture) { + Box( + modifier = Modifier + .fillMaxSize() + .height(Random.nextInt(100, 300).dp) + .clip(RoundedCornerShape(AppDimension.smallPadding)) + .background( + Color( + Random.nextLong(0xFFFFFFFF) + ).copy(alpha = 1f) + ), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource( + id = R.drawable.ic_launcher_foreground, + ), + contentDescription = "", + modifier = Modifier + .align(Alignment.Center) + .clip(RoundedCornerShape(AppDimension.normalPadding)) + .background(ExtendedTheme.colors.primary) + .size(80.dp), + contentScale = ContentScale.Crop, + ) + + Text( + text = "${item.name}", + modifier = Modifier + .fillMaxSize() + .padding(AppDimension.normalPadding), + textAlign = TextAlign.Center + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/flickrapp/presentation/utils/Loanding.kt b/app/src/main/java/com/example/flickrapp/presentation/utils/Loanding.kt new file mode 100644 index 0000000..d90c510 --- /dev/null +++ b/app/src/main/java/com/example/flickrapp/presentation/utils/Loanding.kt @@ -0,0 +1,17 @@ +package com.example.flickrapp.presentation.utils + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun Loading(){ + Box(modifier = Modifier.fillMaxSize()) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/flickrapp/ui/theme/AppDimension.kt b/app/src/main/java/com/example/flickrapp/ui/theme/AppDimension.kt new file mode 100644 index 0000000..1a5960f --- /dev/null +++ b/app/src/main/java/com/example/flickrapp/ui/theme/AppDimension.kt @@ -0,0 +1,12 @@ +package com.example.flickrapp.ui.theme + +import androidx.compose.ui.unit.dp + +class AppDimension { + companion object { + val defaultPadding = 0.dp + val xSmallPadding = 4.dp + val smallPadding = 8.dp + val normalPadding = 16.dp + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/flickrapp/ui/theme/Color.kt b/app/src/main/java/com/example/flickrapp/ui/theme/Color.kt index d54e6d5..73800a4 100644 --- a/app/src/main/java/com/example/flickrapp/ui/theme/Color.kt +++ b/app/src/main/java/com/example/flickrapp/ui/theme/Color.kt @@ -6,6 +6,8 @@ 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 +val Pink100 = Color(0xFFFF6197) +val PinkGrey80 = Color(0xFFFFBFD5) +val Pink120 = Color(0xFFD6094C) +val White = Color(0xFFFFFFFF) +val Black = Color(0xFF000000) diff --git a/app/src/main/java/com/example/flickrapp/ui/theme/Theme.kt b/app/src/main/java/com/example/flickrapp/ui/theme/Theme.kt index a6c355c..223d78e 100644 --- a/app/src/main/java/com/example/flickrapp/ui/theme/Theme.kt +++ b/app/src/main/java/com/example/flickrapp/ui/theme/Theme.kt @@ -1,56 +1,58 @@ package com.example.flickrapp.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.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color 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 = lightColors( + primary = Pink100, + primaryVariant = PinkGrey80, + secondary = Pink120 ) -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 +@Immutable +data class ExtendedColors( + val primary: Color, + val primaryVariant: Color, + val secondary: Color, + val white: Color, + val black: Color +) - /* 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), - */ +val extendedColors = ExtendedColors ( + primary = Pink100, + primaryVariant = PinkGrey80, + secondary = Pink120, + white = Color.White, + black = Color.Black ) +val LocalExtendedColors = staticCompositionLocalOf { + ExtendedColors( + primary = Color.Unspecified, + primaryVariant = Color.Unspecified, + secondary = Color.Unspecified, + white = Color.Unspecified, + black = Color.Unspecified + ) +} @Composable fun FlickrAppTheme( 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 @@ -62,9 +64,19 @@ fun FlickrAppTheme( } } - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) + CompositionLocalProvider( + LocalExtendedColors provides extendedColors + ) { + MaterialTheme( + colors = colorScheme, + typography = Typography, + content = content + ) + } +} +object ExtendedTheme { + val colors: ExtendedColors + @Composable + get() = LocalExtendedColors.current + } \ No newline at end of file diff --git a/app/src/main/java/com/example/flickrapp/ui/theme/Type.kt b/app/src/main/java/com/example/flickrapp/ui/theme/Type.kt index 97f0537..a52da28 100644 --- a/app/src/main/java/com/example/flickrapp/ui/theme/Type.kt +++ b/app/src/main/java/com/example/flickrapp/ui/theme/Type.kt @@ -1,34 +1,31 @@ package com.example.flickrapp.ui.theme -import androidx.compose.material3.Typography +import androidx.compose.material.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( + body1 = 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( + ), + h1 = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = 0.sp ), - labelSmall = TextStyle( + h2 = 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/java/com/example/flickrapp/utils/TopAppBarFlickr.kt b/app/src/main/java/com/example/flickrapp/utils/TopAppBarFlickr.kt new file mode 100644 index 0000000..1421594 --- /dev/null +++ b/app/src/main/java/com/example/flickrapp/utils/TopAppBarFlickr.kt @@ -0,0 +1,51 @@ +package com.example.flickrapp.utils + +import android.content.res.Resources.Theme +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Close +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.example.flickrapp.R +import com.example.flickrapp.ui.theme.AppDimension.Companion.xSmallPadding +import com.example.flickrapp.ui.theme.ExtendedTheme + +@Composable +fun TopAppBarFlickr(title: String, imageVectorType: ImageVector = ImageVector.NONE, onClickBack: () -> Unit) { + val imageVector = when (imageVectorType) { + ImageVector.NONE -> null + ImageVector.ARROW -> Icons.Filled.ArrowBack + ImageVector.CLOSE -> Icons.Filled.Close + } + + TopAppBar( + title = { + Text(text = title) + }, + navigationIcon = { + IconButton(onClick = { + onClickBack.invoke() + }) { + imageVector?.let { + Icon( + imageVector, + stringResource(id = R.string.go_back) + ) + } + } + }, + backgroundColor = ExtendedTheme.colors.primary, + contentColor = ExtendedTheme.colors.white, + elevation = xSmallPadding, + ) +} + +enum class ImageVector(val imageVectorId: Int){ + NONE(0), + ARROW(1), + CLOSE(2) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fc9b55b..3155840 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ FlickrApp + Go Back + Search \ No newline at end of file diff --git a/build.gradle b/build.gradle index 867f1b4..6465b7d 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,13 @@ buildscript { ext.hilt_version = "2.44" + ext.nav_version = "2.5.3" + ext.kotlin_version = "1.8.0" + ext.uistate_version = "2.6.0" + ext.compose_version = "1.4.6" + ext.livedata_version = "1.4.0" + ext.hilt_nav_version = "1.0.0" + ext.lifecycle_version = "2.6.1" } plugins {