From 7e143e80bd5973743ae63183972bf8349cd08cad Mon Sep 17 00:00:00 2001 From: John O'Reilly Date: Fri, 15 Oct 2021 13:44:45 +0100 Subject: [PATCH] compose wear: person details screen --- buildSrc/src/main/java/Dependencies.kt | 1 + wearApp/build.gradle.kts | 1 + .../surrus/peopleinspace/FillMaxRectangle.kt | 80 +++++++++++++++++++ .../com/surrus/peopleinspace/MainActivity.kt | 41 +++++++++- .../peopleinspace/PeopleInSpaceApplication.kt | 2 + .../peopleinspace/PeopleInSpaceViewModel.kt | 24 ++++++ .../peopleinspace/PersonDetailsScreen.kt | 68 ++++++++++++++++ .../{PersonList.kt => PersonListScreen.kt} | 9 +-- .../com/surrus/peopleinspace/di/AppModule.kt | 9 +++ 9 files changed, 227 insertions(+), 8 deletions(-) create mode 100644 wearApp/src/main/java/com/surrus/peopleinspace/FillMaxRectangle.kt create mode 100644 wearApp/src/main/java/com/surrus/peopleinspace/PeopleInSpaceViewModel.kt create mode 100644 wearApp/src/main/java/com/surrus/peopleinspace/PersonDetailsScreen.kt rename wearApp/src/main/java/com/surrus/peopleinspace/{PersonList.kt => PersonListScreen.kt} (96%) create mode 100644 wearApp/src/main/java/com/surrus/peopleinspace/di/AppModule.kt diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 8311531..578966a 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -97,6 +97,7 @@ object Deps { const val wearFoundation = "androidx.wear.compose:compose-foundation:${Versions.wearCompose}" const val wearMaterial = "androidx.wear.compose:compose-material:${Versions.wearCompose}" + const val wearNavigation = "androidx.wear.compose:compose-navigation:${Versions.wearCompose}" const val coilCompose = "io.coil-kt:coil-compose:1.3.1" const val accompanistNavigationAnimation = "com.google.accompanist:accompanist-navigation-animation:${Versions.accompanist}" diff --git a/wearApp/build.gradle.kts b/wearApp/build.gradle.kts index 836f64c..4668e66 100644 --- a/wearApp/build.gradle.kts +++ b/wearApp/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { with(Deps.Compose) { implementation(wearFoundation) implementation(wearMaterial) + implementation(wearNavigation) implementation(coilCompose) implementation(uiTooling) } diff --git a/wearApp/src/main/java/com/surrus/peopleinspace/FillMaxRectangle.kt b/wearApp/src/main/java/com/surrus/peopleinspace/FillMaxRectangle.kt new file mode 100644 index 0000000..658569e --- /dev/null +++ b/wearApp/src/main/java/com/surrus/peopleinspace/FillMaxRectangle.kt @@ -0,0 +1,80 @@ +package androidx.wear.compose.samples.shared + +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.layout.LayoutModifier +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.platform.InspectorValueInfo +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.constrainHeight +import androidx.compose.ui.unit.constrainWidth +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.offset +import kotlin.math.sqrt + +@Stable +fun Modifier.fillMaxRectangle() = composed { + val isRound = LocalContext.current.resources.configuration.isScreenRound + + var inset: Dp = 0.dp + + if (isRound) { + val screenHeightDp = LocalContext.current.resources.configuration.screenHeightDp + val screenWidthDp = LocalContext.current.resources.configuration.smallestScreenWidthDp + val maxSquareEdge = (sqrt(((screenHeightDp * screenWidthDp) / 2).toDouble())) + inset = Dp(((screenHeightDp - maxSquareEdge) / 2).toFloat()) + } + this.then(RectangleInsetModifier( + inset = inset, + inspectorInfo = debugInspectorInfo { + name = "fillMaxRectangle" + }) + ) +} + +private class RectangleInsetModifier( + val inset: Dp = 0.dp, + inspectorInfo: InspectorInfo.() -> Unit +) : LayoutModifier, InspectorValueInfo(inspectorInfo) { + init { + require( + (inset.value >= 0f || inset == Dp.Unspecified) + ) { + "Inset must be non-negative" + } + } + + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints + ): MeasureResult { + + val totalOffsetInPx = inset.roundToPx() * 2 + + val placeable = measurable.measure(constraints.offset(-totalOffsetInPx, -totalOffsetInPx)) + + val width = constraints.constrainWidth(placeable.width + totalOffsetInPx) + val height = constraints.constrainHeight(placeable.height + totalOffsetInPx) + return layout(width, height) { + placeable.place(inset.roundToPx(), inset.roundToPx()) + } + } + + override fun hashCode(): Int { + var result = inset.hashCode() + result = 31 * result + inset.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + val otherModifier = other as? RectangleInsetModifier ?: return false + return inset == otherModifier.inset + } +} diff --git a/wearApp/src/main/java/com/surrus/peopleinspace/MainActivity.kt b/wearApp/src/main/java/com/surrus/peopleinspace/MainActivity.kt index 8b20238..e5da1cd 100644 --- a/wearApp/src/main/java/com/surrus/peopleinspace/MainActivity.kt +++ b/wearApp/src/main/java/com/surrus/peopleinspace/MainActivity.kt @@ -3,23 +3,58 @@ package com.surrus.peopleinspace import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.runtime.CompositionLocalProvider +import androidx.navigation.NavType +import androidx.navigation.navArgument +import androidx.wear.compose.material.ExperimentalWearMaterialApi +import androidx.wear.compose.navigation.SwipeDismissableNavHost +import androidx.wear.compose.navigation.composable +import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import coil.ImageLoader import coil.compose.LocalImageLoader import com.surrus.common.repository.PeopleInSpaceRepositoryInterface import org.koin.android.ext.android.inject -class MainActivity : ComponentActivity() { - private val peopleInSpaceRepository: PeopleInSpaceRepositoryInterface by inject() +sealed class Screen(val route: String) { + object PersonList : Screen("personList") + object PersonDetails : Screen("personDetails") +} + +const val PERSON_NAME_NAV_ARGUMENT = "personName" + +class MainActivity : ComponentActivity() { private val imageLoader: ImageLoader by inject() + @OptIn(ExperimentalWearMaterialApi::class, ExperimentalAnimationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { CompositionLocalProvider(LocalImageLoader provides imageLoader) { - PersonListScreen(peopleInSpaceRepository, personSelected = {}) + val navController = rememberSwipeDismissableNavController() + + SwipeDismissableNavHost( + navController = navController, + startDestination = Screen.PersonList.route + ) { + + composable(Screen.PersonList.route) { + PersonListScreen(personSelected = { + navController.navigate(Screen.PersonDetails.route + "/${it.name}") + }) + } + + composable( + route = Screen.PersonDetails.route + "/{$PERSON_NAME_NAV_ARGUMENT}" + ) { navBackStackEntry -> + val personName = navBackStackEntry.arguments?.getString(PERSON_NAME_NAV_ARGUMENT) + personName?.let { + PersonDetailsScreen(personName) + } + } + } } } } diff --git a/wearApp/src/main/java/com/surrus/peopleinspace/PeopleInSpaceApplication.kt b/wearApp/src/main/java/com/surrus/peopleinspace/PeopleInSpaceApplication.kt index 89eb455..19fb157 100644 --- a/wearApp/src/main/java/com/surrus/peopleinspace/PeopleInSpaceApplication.kt +++ b/wearApp/src/main/java/com/surrus/peopleinspace/PeopleInSpaceApplication.kt @@ -7,6 +7,7 @@ import coil.ImageLoader import coil.util.CoilUtils import coil.util.DebugLogger import com.surrus.common.di.initKoin +import com.surrus.peopleinspace.di.appModule import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.logging.LoggingEventListener @@ -27,6 +28,7 @@ class PeopleInSpaceApplication : Application(), KoinComponent { androidContext(this@PeopleInSpaceApplication) modules(imageLoader(this@PeopleInSpaceApplication)) + modules(appModule) } logger.d { "PeopleInSpaceApplication" } diff --git a/wearApp/src/main/java/com/surrus/peopleinspace/PeopleInSpaceViewModel.kt b/wearApp/src/main/java/com/surrus/peopleinspace/PeopleInSpaceViewModel.kt new file mode 100644 index 0000000..32739ca --- /dev/null +++ b/wearApp/src/main/java/com/surrus/peopleinspace/PeopleInSpaceViewModel.kt @@ -0,0 +1,24 @@ +package com.surrus.peopleinspace + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import co.touchlab.kermit.Kermit +import com.surrus.common.remote.Assignment +import com.surrus.common.repository.PeopleInSpaceRepository +import com.surrus.common.repository.PeopleInSpaceRepositoryInterface +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn + +class PeopleInSpaceViewModel( + private val peopleInSpaceRepository: PeopleInSpaceRepositoryInterface +) : ViewModel() { + + val peopleInSpace = peopleInSpaceRepository.fetchPeopleAsFlow() + .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + + val issPosition = peopleInSpaceRepository.pollISSPosition() + + fun getPerson(personName: String): Assignment? { + return peopleInSpace.value.find { it.name == personName } + } +} diff --git a/wearApp/src/main/java/com/surrus/peopleinspace/PersonDetailsScreen.kt b/wearApp/src/main/java/com/surrus/peopleinspace/PersonDetailsScreen.kt new file mode 100644 index 0000000..54abffb --- /dev/null +++ b/wearApp/src/main/java/com/surrus/peopleinspace/PersonDetailsScreen.kt @@ -0,0 +1,68 @@ +package com.surrus.peopleinspace + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material.MaterialTheme +import androidx.wear.compose.material.Text +import androidx.wear.compose.samples.shared.fillMaxRectangle +import coil.compose.rememberImagePainter +import org.koin.androidx.compose.getViewModel + +@Composable +fun PersonDetailsScreen(personName: String) { + val peopleInSpaceViewModel = getViewModel() + + // TODO look for cleaner way of doing this + val peopleState = peopleInSpaceViewModel.peopleInSpace.collectAsState() + + val person by remember(peopleState, personName) { + derivedStateOf { + peopleState.value.find { it.name == personName } + } + } + + val scrollState = rememberScrollState() + + person?.let { person -> + + Box(modifier = Modifier + .fillMaxSize() + .fillMaxRectangle()) { + + Column( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + .verticalScroll(scrollState), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text( + person.name, + style = MaterialTheme.typography.body2, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.size(12.dp)) + + val imageUrl = person.personImageUrl ?: "" + if (imageUrl.isNotEmpty()) { + Image( + painter = rememberImagePainter(imageUrl), + modifier = Modifier.size(120.dp), contentDescription = person.name + ) + } + Spacer(modifier = Modifier.size(24.dp)) + + val bio = person.personBio ?: "" + Text(bio, style = MaterialTheme.typography.body1, textAlign = TextAlign.Center) + } + } + } +} diff --git a/wearApp/src/main/java/com/surrus/peopleinspace/PersonList.kt b/wearApp/src/main/java/com/surrus/peopleinspace/PersonListScreen.kt similarity index 96% rename from wearApp/src/main/java/com/surrus/peopleinspace/PersonList.kt rename to wearApp/src/main/java/com/surrus/peopleinspace/PersonListScreen.kt index 9190696..2d1a28c 100644 --- a/wearApp/src/main/java/com/surrus/peopleinspace/PersonList.kt +++ b/wearApp/src/main/java/com/surrus/peopleinspace/PersonListScreen.kt @@ -35,6 +35,7 @@ import coil.annotation.ExperimentalCoilApi import coil.compose.rememberImagePainter import com.surrus.common.remote.Assignment import com.surrus.common.repository.PeopleInSpaceRepositoryInterface +import org.koin.androidx.compose.getViewModel const val PersonListTag = "PersonList" const val PersonTag = "Person" @@ -42,12 +43,10 @@ const val NoPeopleTag = "NoPeople" @Composable fun PersonListScreen( - peopleInSpaceRepository: PeopleInSpaceRepositoryInterface, - personSelected: (person: Assignment) -> Unit + personSelected: (person: Assignment) -> Unit, + peopleInSpaceViewModel: PeopleInSpaceViewModel = getViewModel() ) { - val peopleState by peopleInSpaceRepository - .fetchPeopleAsFlow() - .collectAsState(initial = null) + val peopleState by peopleInSpaceViewModel.peopleInSpace.collectAsState() PersonListScreen(peopleState, personSelected) } diff --git a/wearApp/src/main/java/com/surrus/peopleinspace/di/AppModule.kt b/wearApp/src/main/java/com/surrus/peopleinspace/di/AppModule.kt new file mode 100644 index 0000000..ab3817e --- /dev/null +++ b/wearApp/src/main/java/com/surrus/peopleinspace/di/AppModule.kt @@ -0,0 +1,9 @@ +package com.surrus.peopleinspace.di + +import com.surrus.peopleinspace.PeopleInSpaceViewModel +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val appModule = module { + viewModel { PeopleInSpaceViewModel(get()) } +}