compose wear: person details screen
This commit is contained in:
parent
b079c5d737
commit
7e143e80bd
9 changed files with 227 additions and 8 deletions
|
@ -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}"
|
||||
|
|
|
@ -48,6 +48,7 @@ dependencies {
|
|||
with(Deps.Compose) {
|
||||
implementation(wearFoundation)
|
||||
implementation(wearMaterial)
|
||||
implementation(wearNavigation)
|
||||
implementation(coilCompose)
|
||||
implementation(uiTooling)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()) }
|
||||
}
|
Loading…
Reference in a new issue