main screen view and basic pokedex view

This commit is contained in:
Owen LeJeune
2022-09-15 12:52:43 -04:00
parent f30e4b1120
commit edd4559971
23 changed files with 854 additions and 46 deletions

17
.idea/deploymentTargetDropDown.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_3a_API_33.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-09-15T01:01:05.349795Z" />
</component>
</project>

View File

@@ -17,7 +17,7 @@
tools:targetApi="31"
android:name=".MYDexApplication">
<activity
android:name=".MainActivity"
android:name=".ui.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.MYDex">

View File

@@ -1,37 +0,0 @@
package com.owenlejeune.mydex
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.rememberNavController
import com.kieronquinn.monetcompat.app.MonetCompatActivity
import com.owenlejeune.mydex.ui.theme.MYDexTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainActivity : MonetCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launchWhenCreated {
monet.awaitMonetReady()
setContent {
MYDexTheme(monetCompat = monet) {
val appNavController = rememberNavController()
Box {
}
}
}
}
}
}

View File

@@ -1,6 +1,7 @@
package com.owenlejeune.mydex.api.pokeapi
import com.owenlejeune.mydex.api.Client
import com.owenlejeune.mydex.api.pokeapi.v2.PokemonApi
import com.owenlejeune.mydex.preferences.AppPreferences
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -15,6 +16,8 @@ class PokeApiClient: KoinComponent {
private val client: Client by inject { parametersOf(BASE_URL) }
private val preferences: AppPreferences by inject()
fun createPokemonService(): PokemonApi {
return client.create(PokemonApi::class.java)
}
}

View File

@@ -1,5 +1,6 @@
package com.owenlejeune.mydex.api.pokeapi.v2
import android.util.Log
import com.owenlejeune.mydex.api.pokeapi.v2.model.misc.PaginatedResponse
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.Pokemon
import org.koin.core.component.KoinComponent
@@ -10,6 +11,8 @@ class PokemonService: KoinComponent {
companion object {
private const val DEFAULT_LIMIT = 20
val TAG = PokemonService::class.java.simpleName
}
private val service: PokemonApi by inject()
@@ -22,7 +25,11 @@ class PokemonService: KoinComponent {
val limit = DEFAULT_LIMIT
val offset = DEFAULT_LIMIT * page
return service.getPaginatedPokemon(offset = offset, limit = limit)
Log.d(TAG, "Paginated: page=$page, limit=$limit, offset=$offset")
return service.getPaginatedPokemon(offset = offset, limit = limit).apply {
Log.d(TAG, "Response: $isSuccessful, ${body()?.results?.map { it.name }}")
}
}
}

View File

@@ -0,0 +1,44 @@
package com.owenlejeune.mydex.api.pokeapi.v2.paging
import android.util.Log
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.owenlejeune.mydex.api.pokeapi.v2.PokemonService
import com.owenlejeune.mydex.api.pokeapi.v2.model.misc.NameAndUrl
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.Pokemon
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class PokemonPagingSource: PagingSource<Int, Pokemon>(), KoinComponent {
companion object {
private val TAG = PokemonPagingSource::class.java.simpleName
}
private val pokemonService: PokemonService by inject()
override fun getRefreshKey(state: PagingState<Int, Pokemon>): Int? {
return state.anchorPosition
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Pokemon> {
return try {
val nextPage = params.key ?: 1
val response = pokemonService.getPokemon(nextPage)
if (response.isSuccessful) {
val responseBody = response.body()!!
// Log.d(TAG, "${results.map { it.name }}")
LoadResult.Page(
data = listOf(responseBody),
prevKey = if (nextPage == 1) null else nextPage - 1,
nextKey = if (responseBody == null) null else nextPage + 1
)
} else {
LoadResult.Invalid()
}
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}

View File

@@ -0,0 +1,18 @@
package com.owenlejeune.mydex.api.pokeapi.v2.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.owenlejeune.mydex.api.pokeapi.v2.model.misc.NameAndUrl
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.Pokemon
import com.owenlejeune.mydex.api.pokeapi.v2.paging.PokemonPagingSource
import kotlinx.coroutines.flow.Flow
class PokemonViewModel: ViewModel() {
val pokemon: Flow<PagingData<Pokemon>> = Pager(PagingConfig(pageSize = Int.MAX_VALUE)) {
PokemonPagingSource()
}.flow.cachedIn(viewModelScope)
}

View File

@@ -5,7 +5,10 @@ import com.google.gson.TypeAdapterFactory
import com.owenlejeune.mydex.BuildConfig
import com.owenlejeune.mydex.api.*
import com.owenlejeune.mydex.api.pokeapi.PokeApiClient
import com.owenlejeune.mydex.api.pokeapi.v2.PokemonApi
import com.owenlejeune.mydex.api.pokeapi.v2.PokemonService
import com.owenlejeune.mydex.preferences.AppPreferences
import com.owenlejeune.mydex.utils.ResourceUtils
import org.koin.dsl.module
val networkModule = module {
@@ -20,14 +23,20 @@ val networkModule = module {
single {
GsonBuilder().apply {
get<List<TypeAdapterFactory>>().forEach { taf -> registerTypeAdapterFactory(taf) }
}
}.create()
}
single { PokeApiClient() }
single { get<PokeApiClient>().createPokemonService() }
single { PokemonService() }
}
val preferencesModule = module {
single { AppPreferences(get()) }
}
val Modules = listOf(networkModule, preferencesModule)
val appModule = module {
factory { ResourceUtils(get()) }
}
val Modules = listOf(networkModule, preferencesModule, appModule)

View File

@@ -0,0 +1,49 @@
package com.owenlejeune.mydex.extensions
import android.app.Activity
import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.window.layout.WindowMetricsCalculator
@Composable
fun Activity.rememberWindowSize(): Size {
val configuration = LocalConfiguration.current
val windowMetrics = remember(configuration) {
WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this)
}
return windowMetrics.bounds.toComposeRect().size
}
enum class WindowSizeClass { Compact, Medium, Expanded }
@Composable
fun Activity.rememberWindowSizeClass(): WindowSizeClass {
val windowSize = rememberWindowSize()
val windowSizeDp = with(LocalDensity.current) {
windowSize.toDpSize()
}
return getWindowSizeClass(windowSizeDp)
}
private fun getWindowSizeClass(windowDpSize: DpSize): WindowSizeClass = when {
windowDpSize.width < 0.dp -> throw IllegalArgumentException("Dp value cannot be negative")
windowDpSize.width < 600.dp -> WindowSizeClass.Compact
windowDpSize.width < 840.dp -> WindowSizeClass.Medium
else -> WindowSizeClass.Expanded
}
fun <T> Activity.launchActivity(activity: Class<T>) {
val intent = Intent(this, activity)
startActivity(intent)
finish()
}

View File

@@ -0,0 +1,68 @@
package com.owenlejeune.mydex.extensions
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.paging.compose.LazyPagingItems
fun <T: Any> LazyGridScope.lazyPagingItems(
lazyPagingItems: LazyPagingItems<T>,
itemContent: @Composable LazyGridItemScope.(value: T?) -> Unit
) {
items(lazyPagingItems.itemCount) { index ->
itemContent(lazyPagingItems[index])
}
}
fun <T: Any> LazyGridScope.listItems(
items: List<T>,
itemContent: @Composable (value: T) -> Unit
) {
items(items.size) { index ->
itemContent(items[index])
}
}
fun LazyGridScope.header(
content: @Composable LazyGridItemScope.() -> Unit
) {
item(
span = {
GridItemSpan(maxLineSpan)
},
content = content
)
}
fun <T: Any> LazyListScope.listItems(
items: Collection<T>,
itemContent: @Composable (value: T) -> Unit
) {
items(items.size) { index ->
itemContent(items.elementAt(index))
}
}
fun <T: Any?> LazyListScope.listItems(
items: List<T?>,
key: (T?) -> Any,
itemContent: @Composable (value: T?) -> Unit
) {
items(items.size, key = { key(items[it]) }) { index ->
itemContent(items[index])
}
}
fun <T: Any> LazyListScope.lazyPagingItems(
lazyPagingItems: LazyPagingItems<T>,
itemContent: @Composable LazyItemScope.(value: T?) -> Unit
) {
items(lazyPagingItems.itemCount) { index ->
itemContent(lazyPagingItems[index])
}
}

View File

@@ -0,0 +1,5 @@
package com.owenlejeune.mydex.extensions
fun String.charAtFromEnd(index: Int): Char {
return get(length-1-index)
}

View File

@@ -0,0 +1,54 @@
package com.owenlejeune.mydex.ui
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.kieronquinn.monetcompat.app.MonetCompatActivity
import com.owenlejeune.mydex.preferences.AppPreferences
import com.owenlejeune.mydex.ui.navigation.DataNavItem
import com.owenlejeune.mydex.ui.navigation.MainNavItem
import com.owenlejeune.mydex.ui.theme.MYDexTheme
import com.owenlejeune.mydex.ui.views.AppScaffold
import com.owenlejeune.mydex.ui.views.PokedexView
import org.koin.java.KoinJavaComponent
class MainActivity : MonetCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launchWhenCreated {
monet.awaitMonetReady()
setContent {
MYDexTheme(monetCompat = monet) {
val appNavController = rememberNavController()
Box {
MainNavigationRoutes(appNavController = appNavController)
}
}
}
}
}
@Composable
private fun MainNavigationRoutes(
startDestination: String = MainNavItem.MainView.route,
appNavController: NavHostController,
preferences: AppPreferences = KoinJavaComponent.get(AppPreferences::class.java)
) {
NavHost(navController = appNavController, startDestination = startDestination) {
composable(MainNavItem.MainView.route) {
AppScaffold(appNavController = appNavController)
}
composable(DataNavItem.Pokedex.route) {
PokedexView(appNavController = appNavController)
}
}
}
}

View File

@@ -0,0 +1,194 @@
package com.owenlejeune.mydex.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.modifier.modifierLocalConsumer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import com.owenlejeune.mydex.R
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RoundedTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
focusRequester: FocusRequester = remember { FocusRequester() },
requestFocus: Boolean = false,
placeHolder: String = "",
placeHolderTextColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
backgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant,
textStyle: TextStyle = MaterialTheme.typography.bodySmall,
cursorColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
singleLine: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
enabled: Boolean = true,
readOnly: Boolean = false,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
shape: Shape = RoundedCornerShape(25.dp)
) {
Surface(
modifier = modifier
.clip(shape),
shape = shape,
color = backgroundColor
) {
Row(Modifier.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
if (leadingIcon != null) {
leadingIcon()
}
Box(modifier = Modifier
.padding(start = 8.dp, end = 8.dp)
.fillMaxHeight()
.weight(1f),
contentAlignment = Alignment.CenterStart
) {
if (value.isEmpty() && placeHolder.isNotEmpty()) {
Text(
text = placeHolder,
style = textStyle,
color = placeHolderTextColor,
fontSize = 14.sp
)
}
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val coroutineScope = rememberCoroutineScope()
BasicTextField(
modifier = Modifier
.focusRequester(focusRequester)
.fillMaxWidth()
.bringIntoViewRequester(bringIntoViewRequester)
.onFocusEvent {
if (it.isFocused) {
coroutineScope.launch {
delay(200)
bringIntoViewRequester.bringIntoView()
}
}
},
value = value,
onValueChange = onValueChange,
singleLine = singleLine,
textStyle = textStyle.copy(color = textColor),
cursorBrush = SolidColor(cursorColor),
maxLines = maxLines,
enabled = enabled,
readOnly = readOnly,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
}
if (trailingIcon != null) {
trailingIcon()
}
}
}
if (requestFocus) {
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
}
@Composable
fun SearchBar(
placeholder: String,
onClick: () -> Unit
) {
RoundedTextField(
modifier = Modifier
.height(55.dp)
.clickable(
onClick = onClick
),
value = "",
enabled = false,
onValueChange = { },
placeHolder = placeholder,
leadingIcon = {
Image(
imageVector = Icons.Filled.Search,
contentDescription = null,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.primary)
)
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MenuItemButton(
modifier: Modifier,
text: String,
color: Color,
onClick: () -> Unit
) {
Card(
modifier = modifier
.height(80.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = color),
onClick = onClick
) {
Box(
modifier = Modifier.fillMaxSize()
) {
AsyncImage(
model = R.drawable.pokeball_s,
contentDescription = null,
modifier = Modifier
.align(Alignment.CenterEnd)
.offset(x = 20.dp, y = 0.dp)
.size(width = 96.dp, height = 96.dp),
colorFilter = ColorFilter.tint(Color.White.copy(alpha = 0.15f))
)
Text(
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 16.dp),
text = text,
style = MaterialTheme.typography.bodyLarge.copy(color = Color.White)
)
}
}
}

View File

@@ -0,0 +1,32 @@
package com.owenlejeune.mydex.ui.navigation
import androidx.annotation.StringRes
import androidx.compose.ui.graphics.Color
import com.owenlejeune.mydex.R
import com.owenlejeune.mydex.ui.theme.*
import com.owenlejeune.mydex.utils.ResourceUtils
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
sealed class DataNavItem(
val route: String,
val color: Color,
@StringRes titleRes: Int
): KoinComponent {
companion object {
val Pages by lazy { listOf(Pokedex, Moves, Abilities, Items, Locations, TypeCharts) }
}
private val resourceUtils: ResourceUtils by inject()
val title = resourceUtils.getString(titleRes)
object Pokedex: DataNavItem("pokedex_route", PokeTeal, R.string.pokedex_nav_title)
object Moves: DataNavItem("moves_route", PokeRed, R.string.moves_nav_title)
object Abilities: DataNavItem("abilities_route", PokeLightBlue, R.string.abilities_nav_title)
object Items: DataNavItem("items_route", PokeYellow, R.string.items_nav_title)
object Locations: DataNavItem("locations_route", PokePurple, R.string.locations_nav_title)
object TypeCharts: DataNavItem("type_charts_route", PokeBrown, R.string.type_charts_nav_title)
}

View File

@@ -0,0 +1,11 @@
package com.owenlejeune.mydex.ui.navigation
sealed class MainNavItem(val route: String) {
companion object {
val Items by lazy { listOf(MainView) }
}
object MainView: MainNavItem("main_route")
}

View File

@@ -2,7 +2,16 @@ package com.owenlejeune.mydex.ui.theme
import androidx.compose.ui.graphics.Color
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
val PokeBlack = Color(0xFF303943)
val PokeBlue = Color(0xFF429BED)
val PokeBrown = Color(0xFFB1736C)
val PokeLightBlue = Color(0xFF58ABF6)
val PokeLightBrown = Color(0xFFCA8179)
val PokeLightPurple = Color(0xFF9F5BBA)
val PokeLightRed = Color(0xFFF7786B)
val PokeLightTeal = Color(0xFF2CDAB1)
val PokeLightYellow = Color(0xFFFFCE4B)
val PokePurple = Color(0xFF7C538C)
val PokeRed = Color(0xFFFA6555)
val PokeTeal = Color(0xFF4FC1A6)
val PokeYellow = Color(0xFFF6C747)

View File

@@ -0,0 +1,156 @@
package com.owenlejeune.mydex.ui.views
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import coil.compose.AsyncImage
import com.owenlejeune.mydex.extensions.WindowSizeClass
import com.owenlejeune.mydex.extensions.rememberWindowSizeClass
import com.owenlejeune.mydex.preferences.AppPreferences
import com.owenlejeune.mydex.ui.MainActivity
import com.owenlejeune.mydex.ui.components.MenuItemButton
import com.owenlejeune.mydex.ui.components.SearchBar
import com.owenlejeune.mydex.ui.navigation.DataNavItem
import org.koin.java.KoinJavaComponent
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainActivity.AppScaffold(
appNavController: NavHostController,
preferences: AppPreferences = KoinJavaComponent.get(AppPreferences::class.java)
) {
val windowSize = rememberWindowSizeClass()
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
val topAppBarScrollState = rememberTopAppBarScrollState()
val scrollBehavior = remember(decayAnimationSpec) {
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec, topAppBarScrollState)
}
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
}
) { innerPadding ->
Box(modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
) {
MainContent(
windowSize = windowSize,
appNavController = appNavController,
topBarScrollBehaviour = scrollBehavior
)
}
}
}
@Composable
private fun BoxScope.MainContent(
windowSize: WindowSizeClass,
appNavController: NavHostController,
topBarScrollBehaviour: TopAppBarScrollBehavior
) {
if (windowSize != WindowSizeClass.Expanded) {
SingleColumnMainContent(
appNavController = appNavController
)
}
}
@Composable
private fun BoxScope.SingleColumnMainContent(appNavController: NavHostController) {
val tint = MaterialTheme.colorScheme.secondaryContainer
AsyncImage(
model = com.owenlejeune.mydex.R.drawable.pokeball,
contentDescription = null,
modifier = Modifier
.offset(x = 90.dp, y = (-40).dp)
.size(width = 240.dp, height = 240.dp)
.align(Alignment.TopEnd),
colorFilter = ColorFilter.tint(tint)
)
Column(
modifier = Modifier.padding(all = 36.dp)
) {
Spacer(modifier = Modifier.height(120.dp))
Text(
text = stringResource(id = com.owenlejeune.mydex.R.string.main_screen_title),
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 30.sp,
color = MaterialTheme.colorScheme.onBackground
)
)
Spacer(modifier = Modifier.height(50.dp))
SearchBar(placeholder = stringResource(id = com.owenlejeune.mydex.R.string.main_search_placeholder)) {}
Spacer(modifier = Modifier.height(32.dp))
val cols = 2
val rows = DataNavItem.Pages.size/cols
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
for (i in 0 until rows) {
val first = DataNavItem.Pages[2*i]
val second = if (2*i+1 <= DataNavItem.Pages.size) {
DataNavItem.Pages[2*i+1]
} else {
null
}
MenuItemRow(appNavController = appNavController, first = first, second = second)
}
}
}
}
@Composable
private fun MenuItemRow(
appNavController: NavHostController,
first: DataNavItem,
second: DataNavItem?
) {
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
MenuItemButton(
modifier = Modifier.weight(1f),
text = first.title,
color = first.color,
onClick = {
appNavController.navigate(first.route)
}
)
second?.let {
MenuItemButton(
modifier = Modifier.weight(1f),
text = second.title,
color = second.color,
onClick = {
appNavController.navigate(second.route)
}
)
}
}
}

View File

@@ -0,0 +1,143 @@
package com.owenlejeune.mydex.ui.views
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.paging.compose.collectAsLazyPagingItems
import coil.compose.AsyncImage
import com.owenlejeune.mydex.R
import com.owenlejeune.mydex.api.pokeapi.v2.PokemonService
import com.owenlejeune.mydex.api.pokeapi.v2.model.misc.NameAndUrl
import com.owenlejeune.mydex.api.pokeapi.v2.model.pokemon.Pokemon
import com.owenlejeune.mydex.api.pokeapi.v2.viewmodel.PokemonViewModel
import com.owenlejeune.mydex.extensions.charAtFromEnd
import com.owenlejeune.mydex.extensions.header
import com.owenlejeune.mydex.extensions.lazyPagingItems
import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.get
@Composable
fun PokedexView(
appNavController: NavController
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.background)
) {
val tint = MaterialTheme.colorScheme.secondaryContainer
AsyncImage(
model = R.drawable.pokeball,
contentDescription = null,
modifier = Modifier
.offset(x = 90.dp, y = (-40).dp)
.size(width = 240.dp, height = 240.dp)
.align(Alignment.TopEnd),
colorFilter = ColorFilter.tint(tint)
)
SmallTopAppBar(
modifier = Modifier
.statusBarsPadding()
.blur(radius = 10.dp),
navigationIcon = {
IconButton(onClick = { appNavController.popBackStack() }) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null)
}
},
title = {},
colors = TopAppBarDefaults
.smallTopAppBarColors(
containerColor = Color.Transparent.copy(alpha = 0.4f)
)
)
val scrollState = rememberScrollState()
val lazyPokemon = PokemonViewModel().pokemon.collectAsLazyPagingItems()
LazyVerticalGrid(
columns = GridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.padding(start = 36.dp, end = 36.dp, top = 160.dp)
.fillMaxSize()
) {
header {
Text(
text = stringResource(id = R.string.main_screen_title),
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 30.sp,
color = MaterialTheme.colorScheme.onBackground
),
modifier = Modifier.padding(bottom = 50.dp)
)
}
lazyPagingItems(lazyPokemon) { item ->
item?.let {
PokedexCard(pokemon = item)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PokedexCard(
pokemon: Pokemon
) {
Card(
modifier = Modifier
.height(80.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
) {
Box(
modifier = Modifier.fillMaxSize()
) {
AsyncImage(
model = R.drawable.pokeball_s,
contentDescription = null,
modifier = Modifier
.align(Alignment.BottomEnd)
.offset(x = 20.dp, y = 0.dp)
.size(width = 96.dp, height = 96.dp),
colorFilter = ColorFilter.tint(Color.White.copy(alpha = 0.15f))
)
Text(
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 16.dp),
text = pokemon.name,
style = MaterialTheme.typography.bodyLarge.copy(color = Color.White)
)
}
}
}

View File

@@ -0,0 +1,11 @@
package com.owenlejeune.mydex.utils
import android.content.Context
class ResourceUtils(private val context: Context) {
fun getString(stringRes: Int): String {
return context.getString(stringRes)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -1,3 +1,15 @@
<resources>
<string name="app_name">MYDex</string>
<!-- main screen -->
<string name="main_screen_title">What Pokémon\nare you looking for?</string>
<string name="main_search_placeholder">Search Pokémon, Move, Ability, etc.</string>
<!-- nav titles -->
<string name="pokedex_nav_title">Pokedex</string>
<string name="moves_nav_title">Moves</string>
<string name="abilities_nav_title">Abilities</string>
<string name="items_nav_title">Items</string>
<string name="locations_nav_title">Locations</string>
<string name="type_charts_nav_title">Type charts</string>
</resources>

View File

@@ -2,5 +2,8 @@
<resources>
<style name="Theme.MYDex" parent="Theme.AppCompat.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowTranslucentStatus">true</item>
</style>
</resources>