Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Navigation solution #2

Merged
merged 18 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,6 @@ dependencies {
coreLibraryDesugaring(libs.android.desugaring)
implementation(libs.koin.android)
testImplementation(libs.junit)
implementation(libs.compose.navigation)
implementation(libs.koin.compose)
}
11 changes: 2 additions & 9 deletions app/src/main/kotlin/co/touchlab/kampkit/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,17 @@ package co.touchlab.kampkit.android
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import co.touchlab.kampkit.android.ui.BreedsScreen
import co.touchlab.kampkit.android.ui.MainNavCoordinator
import co.touchlab.kampkit.android.ui.theme.KaMPKitTheme
import co.touchlab.kampkit.core.injectLogger
import co.touchlab.kampkit.ui.breeds.BreedsViewModel
import co.touchlab.kermit.Logger
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.component.KoinComponent

class MainActivity : ComponentActivity(), KoinComponent {

private val log: Logger by injectLogger("MainActivity")
private val viewModel: BreedsViewModel by viewModel()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
KaMPKitTheme {
BreedsScreen(viewModel, log)
MainNavCoordinator()
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion app/src/main/kotlin/co/touchlab/kampkit/android/MainApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.content.SharedPreferences
import android.util.Log
import co.touchlab.kampkit.core.AppInfo
import co.touchlab.kampkit.core.initKoin
import co.touchlab.kampkit.ui.breedDetails.BreedDetailsViewModel
import co.touchlab.kampkit.ui.breeds.BreedsViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.parameter.parametersOf
Expand All @@ -18,7 +19,14 @@ class MainApp : Application() {
initKoin(
module {
single<Context> { this@MainApp }
viewModel { BreedsViewModel(get(), get { parametersOf("BreedsViewModel") }) }
viewModel {
BreedsViewModel(get(), get { parametersOf("BreedsViewModel") })
}
viewModel { params ->
BreedDetailsViewModel(
params.get(), get(), get { parametersOf("BreedDetailsViewModel") }
)
}
single<SharedPreferences> {
get<Context>().getSharedPreferences("KAMPSTARTER_SETTINGS", MODE_PRIVATE)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package co.touchlab.kampkit.android.ui

import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.TweenSpec
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kampkit.android.R
import co.touchlab.kampkit.domain.breed.Breed
import co.touchlab.kampkit.ui.breedDetails.BreedDetailsViewModel
import co.touchlab.kampkit.ui.breedDetails.BreedDetailsViewState

@Composable
fun BreedDetailsScreen(viewModel: BreedDetailsViewModel) {
val state by viewModel.detailsState.collectAsStateWithLifecycle()
val error = state.error
Box(Modifier.fillMaxSize()) {
when {
state.isLoading -> Loading()
error != null -> Error(error)
else -> DetailsContents(
state = state,
onFavoriteClick = viewModel::onFavoriteClick
)
}
}
}

@Composable
private fun BoxScope.DetailsContents(
state: BreedDetailsViewState,
onFavoriteClick: () -> Unit
) {
Row(Modifier.align(Alignment.Center)) {
Text(state.breed?.name ?: "")
Spacer(Modifier.width(4.dp))
state.breed?.let { breed ->
FavoriteIcon(
breed = breed,
onClick = onFavoriteClick
)
}
}
}

@Composable
private fun BoxScope.Error(error: String) {
Text(
text = error,
color = Color.Red,
modifier = Modifier.align(Alignment.Center)
)
}

@Composable
fun BoxScope.Loading() {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}

@Composable
fun FavoriteIcon(
breed: Breed,
onClick: () -> Unit
) {
Crossfade(
targetState = !breed.favorite,
animationSpec = TweenSpec(
durationMillis = 500,
easing = FastOutSlowInEasing
),
modifier = Modifier.clickable { onClick() }
) { fav ->
if (fav) {
Image(
painter = painterResource(id = R.drawable.ic_favorite_border_24px),
contentDescription = stringResource(R.string.favorite_breed, breed.name)
)
} else {
Image(
painter = painterResource(id = R.drawable.ic_favorite_24px),
contentDescription = stringResource(R.string.unfavorite_breed, breed.name)
)
}
}
}
63 changes: 32 additions & 31 deletions app/src/main/kotlin/co/touchlab/kampkit/android/ui/BreedsScreen.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package co.touchlab.kampkit.android.ui

import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.TweenSpec
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
Expand All @@ -23,13 +20,15 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.touchlab.kampkit.android.R
import co.touchlab.kampkit.domain.breed.Breed
import co.touchlab.kampkit.ui.breeds.BreedsNavRequest
import co.touchlab.kampkit.ui.breeds.BreedsViewModel
import co.touchlab.kampkit.ui.breeds.BreedsViewState
import co.touchlab.kermit.Logger
Expand All @@ -39,16 +38,26 @@ import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
@Composable
fun BreedsScreen(
viewModel: BreedsViewModel,
onBreedDetailsNavRequest: (breedId: Long) -> Unit,
log: Logger
) {
val breedsState by viewModel.breedsState.collectAsStateWithLifecycle()

breedsState.breedsNavRequest?.let { navRequest ->
LaunchedEffect(navRequest) {
if (navRequest is BreedsNavRequest.ToDetails) {
onBreedDetailsNavRequest(navRequest.breedId)
viewModel.onBreedDetailsNavRequestCompleted()
}
}
}

BreedsScreenContent(
dogsState = breedsState,
onRefresh = { viewModel.refreshBreeds() },
onSuccess = { data -> log.v { "View updating with ${data.size} breeds" } },
onError = { exception -> log.e { "Displaying error: $exception" } },
onFavorite = { viewModel.updateBreedFavorite(it.id) }
onBreedClick = { viewModel.onBreedClick(it) },
)
}

Expand All @@ -58,7 +67,7 @@ fun BreedsScreenContent(
onRefresh: () -> Unit = {},
onSuccess: (List<Breed>) -> Unit = {},
onError: (String) -> Unit = {},
onFavorite: (Breed) -> Unit = {}
onBreedClick: (breedId: Long) -> Unit = {},
) {
Surface(
color = MaterialTheme.colors.background,
Expand All @@ -76,7 +85,7 @@ fun BreedsScreenContent(
LaunchedEffect(breeds) {
onSuccess(breeds)
}
Success(successData = breeds, favoriteBreed = onFavorite)
Success(successData = breeds, onBreedClick = onBreedClick)
}
}

Expand All @@ -91,7 +100,7 @@ fun BreedsScreenContent(
}

@Composable
fun Empty() {
private fun Empty() {
Column(
modifier = Modifier
.fillMaxSize()
Expand All @@ -104,32 +113,32 @@ fun Empty() {
}

@Composable
fun Error(error: String) {
private fun Error(error: String) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = error)
Text(text = error, color = Color.Red)
}
}

@Composable
fun Success(
private fun Success(
successData: List<Breed>,
favoriteBreed: (Breed) -> Unit
onBreedClick: (breedId: Long) -> Unit
) {
DogList(breeds = successData, favoriteBreed)
DogList(breeds = successData, onBreedClick)
}

@Composable
fun DogList(breeds: List<Breed>, onItemClick: (Breed) -> Unit) {
fun DogList(breeds: List<Breed>, onItemClick: (breedId: Long) -> Unit) {
LazyColumn {
items(breeds) { breed ->
DogRow(breed) {
onItemClick(it)
onItemClick(breed.id)
}
Divider()
}
Expand All @@ -150,24 +159,16 @@ fun DogRow(breed: Breed, onClick: (Breed) -> Unit) {

@Composable
fun FavoriteIcon(breed: Breed) {
Crossfade(
targetState = !breed.favorite,
animationSpec = TweenSpec(
durationMillis = 500,
easing = FastOutSlowInEasing
if (breed.favorite) {
Image(
painter = painterResource(id = R.drawable.ic_favorite_24px),
contentDescription = stringResource(R.string.unfavorite_breed, breed.name)
)
} else {
Image(
painter = painterResource(id = R.drawable.ic_favorite_border_24px),
contentDescription = stringResource(R.string.favorite_breed, breed.name)
)
) { fav ->
if (fav) {
Image(
painter = painterResource(id = R.drawable.ic_favorite_border_24px),
contentDescription = stringResource(R.string.favorite_breed, breed.name)
)
} else {
Image(
painter = painterResource(id = R.drawable.ic_favorite_24px),
contentDescription = stringResource(R.string.unfavorite_breed, breed.name)
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package co.touchlab.kampkit.android.ui

import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import org.koin.androidx.compose.get
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf

private const val BREEDS = "breeds"
private const val BREED_DETAILS = "breedDetails"
private const val BREED_ID_ARG = "breedId"

@Composable
fun MainNavCoordinator() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "breeds") {
composable(BREEDS) {
BreedsScreen(
viewModel = koinViewModel(),
onBreedDetailsNavRequest = { breedId ->
navController.navigateToBreedDetails(breedId)
},
log = get { parametersOf("BreedsScreen") }
)
}
composable(
route = "$BREED_DETAILS/{$BREED_ID_ARG}",
arguments = listOf(navArgument(BREED_ID_ARG) { type = NavType.LongType })
) {
val breedId = it.arguments?.getLong(BREED_ID_ARG)
BreedDetailsScreen(
viewModel = koinViewModel { parametersOf(breedId) },
)
}
}
}

private fun NavController.navigateToBreedDetails(breedId: Long) {
navigate("$BREED_DETAILS/$breedId")
}
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ androidx-core = "1.9.0"
androidx-test-junit = "1.1.3"
androidx-activity-compose = "1.5.1"
androidx-lifecycle = "2.6.0"
androidx-navigation-compose = "2.5.3"

junit = "4.13.2"

Expand Down Expand Up @@ -51,6 +52,7 @@ compose-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "co
compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" }
compose-material = { module = "androidx.compose.material:material", version.ref = "compose" }
compose-activity = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation-compose" }

coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
Expand All @@ -62,6 +64,7 @@ junit = { module = "junit:junit", version.ref = "junit" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin"}

kotlinx-dateTime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }

Expand Down
Loading
Loading