compose wear: person details screen

This commit is contained in:
John O'Reilly 2021-10-15 13:44:45 +01:00
parent b079c5d737
commit 7e143e80bd
9 changed files with 227 additions and 8 deletions

View file

@ -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}"

View file

@ -48,6 +48,7 @@ dependencies {
with(Deps.Compose) {
implementation(wearFoundation)
implementation(wearMaterial)
implementation(wearNavigation)
implementation(coilCompose)
implementation(uiTooling)
}

View file

@ -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
}
}

View file

@ -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)
}
}
}
}
}
}

View file

@ -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" }

View file

@ -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 }
}
}

View file

@ -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<PeopleInSpaceViewModel>()
// 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)
}
}
}
}

View file

@ -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)
}

View file

@ -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()) }
}