Skip to content

Commit 03e1549

Browse files
RUM-9511: Characters and CharacterDetails screens
1 parent 827666e commit 03e1549

14 files changed

+877
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.benchmark.sample.navigation
8+
9+
import android.os.Build
10+
import android.os.Parcelable
11+
import androidx.annotation.IdRes
12+
import androidx.core.os.bundleOf
13+
import androidx.fragment.app.Fragment
14+
import androidx.navigation.NavController
15+
16+
internal inline fun <reified T : Parcelable> NavController.navigate(@IdRes id: Int, arg: T) {
17+
val bundle = bundleOf(getArgKey<T>() to arg)
18+
navigate(id, bundle)
19+
}
20+
21+
@Suppress("DEPRECATION")
22+
internal inline fun <reified T : Parcelable> Fragment.args(): Lazy<T> {
23+
return lazy {
24+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
25+
arguments?.getParcelable(getArgKey<T>(), T::class.java)!!
26+
} else {
27+
arguments?.getParcelable(getArgKey<T>())!!
28+
}
29+
}
30+
}
31+
32+
private inline fun <reified T> getArgKey(): String {
33+
return T::class.qualifiedName ?: "arg"
34+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.benchmark.sample.ui.common
8+
9+
import androidx.compose.animation.AnimatedVisibility
10+
import androidx.compose.foundation.clickable
11+
import androidx.compose.foundation.layout.Column
12+
import androidx.compose.foundation.layout.Spacer
13+
import androidx.compose.foundation.layout.fillMaxWidth
14+
import androidx.compose.foundation.layout.height
15+
import androidx.compose.foundation.layout.padding
16+
import androidx.compose.material3.Card
17+
import androidx.compose.material3.CardDefaults
18+
import androidx.compose.material3.MaterialTheme
19+
import androidx.compose.material3.Text
20+
import androidx.compose.runtime.Composable
21+
import androidx.compose.runtime.getValue
22+
import androidx.compose.runtime.mutableStateOf
23+
import androidx.compose.runtime.remember
24+
import androidx.compose.runtime.setValue
25+
import androidx.compose.ui.Modifier
26+
import androidx.compose.ui.tooling.preview.Preview
27+
import androidx.compose.ui.unit.dp
28+
29+
@Composable
30+
internal fun ExpandableListCard(
31+
title: String,
32+
items: List<String>,
33+
onTitleClicked: (Boolean) -> Unit
34+
) {
35+
var expanded by remember { mutableStateOf(false) }
36+
37+
Card(
38+
modifier = Modifier
39+
.fillMaxWidth()
40+
.padding(8.dp)
41+
.clickable {
42+
expanded = !expanded
43+
onTitleClicked(expanded)
44+
},
45+
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
46+
) {
47+
Column(modifier = Modifier.padding(16.dp)) {
48+
Text(
49+
text = title,
50+
style = MaterialTheme.typography.titleMedium
51+
)
52+
53+
AnimatedVisibility(visible = expanded) {
54+
Column {
55+
Spacer(modifier = Modifier.height(8.dp))
56+
items.forEach { item ->
57+
Text(
58+
text = item,
59+
style = MaterialTheme.typography.bodyMedium,
60+
modifier = Modifier.padding(vertical = 4.dp)
61+
)
62+
}
63+
}
64+
}
65+
}
66+
}
67+
}
68+
69+
@Preview(showBackground = true)
70+
@Composable
71+
internal fun ExpandableListCardPreview() {
72+
ExpandableListCard(
73+
title = "Expandable List",
74+
items = listOf("Item 1", "Item 2", "Item 3"),
75+
{ }
76+
)
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.benchmark.sample.ui.rumauto.screens.characterdetails
8+
9+
import android.os.Bundle
10+
import android.view.LayoutInflater
11+
import android.view.View
12+
import android.view.ViewGroup
13+
import androidx.compose.runtime.getValue
14+
import androidx.compose.ui.platform.ComposeView
15+
import androidx.fragment.app.Fragment
16+
import androidx.fragment.app.viewModels
17+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
18+
import com.datadog.benchmark.sample.activities.scenarios.benchmarkActivityComponent
19+
import com.datadog.benchmark.sample.navigation.args
20+
import com.datadog.benchmark.sample.network.rickandmorty.models.Character
21+
import javax.inject.Inject
22+
23+
internal class RumAutoCharacterDetailFragment : Fragment() {
24+
25+
private val character: Character by args()
26+
27+
@Inject
28+
internal lateinit var viewModelFactory: AssistedRumAutoCharacterDetailViewModelFactory
29+
30+
private val viewModel: RumAutoCharacterDetailsViewModel by viewModels {
31+
viewModelFactory.create(character)
32+
}
33+
34+
override fun onCreateView(
35+
inflater: LayoutInflater,
36+
container: ViewGroup?,
37+
savedInstanceState: Bundle?
38+
): View {
39+
requireActivity().benchmarkActivityComponent.inject(this)
40+
41+
return ComposeView(requireContext()).apply {
42+
setContent {
43+
val state by viewModel.state.collectAsStateWithLifecycle()
44+
RumAutoCharacterScreen(state, dispatch = viewModel::dispatch)
45+
}
46+
}
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.benchmark.sample.ui.rumauto.screens.characterdetails
8+
9+
import androidx.compose.foundation.layout.Column
10+
import androidx.compose.foundation.layout.Spacer
11+
import androidx.compose.foundation.layout.fillMaxSize
12+
import androidx.compose.foundation.layout.height
13+
import androidx.compose.foundation.layout.padding
14+
import androidx.compose.foundation.layout.width
15+
import androidx.compose.foundation.rememberScrollState
16+
import androidx.compose.foundation.verticalScroll
17+
import androidx.compose.material3.MaterialTheme
18+
import androidx.compose.material3.Text
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.ui.Alignment
21+
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.layout.ContentScale
23+
import androidx.compose.ui.tooling.preview.Preview
24+
import androidx.compose.ui.unit.dp
25+
import coil.compose.AsyncImage
26+
import com.datadog.benchmark.sample.network.rickandmorty.models.Character
27+
import com.datadog.benchmark.sample.ui.common.ExpandableListCard
28+
29+
@Composable
30+
internal fun RumAutoCharacterScreen(
31+
state: RumAutoCharacterState,
32+
modifier: Modifier = Modifier,
33+
dispatch: (RumAutoCharacterAction) -> Unit = {}
34+
) {
35+
val scrollState = rememberScrollState()
36+
37+
Column(
38+
modifier = modifier
39+
.verticalScroll(scrollState)
40+
.padding(horizontal = 8.dp)
41+
) {
42+
AsyncImage(
43+
model = state.character.image,
44+
contentDescription = state.character.name,
45+
modifier = Modifier
46+
.align(Alignment.CenterHorizontally)
47+
.width(200.dp),
48+
contentScale = ContentScale.FillWidth
49+
)
50+
Text(text = state.character.name, style = MaterialTheme.typography.headlineSmall)
51+
Spacer(modifier = Modifier.height(4.dp))
52+
Text(text = state.character.status.toString(), style = MaterialTheme.typography.labelLarge)
53+
Spacer(modifier = Modifier.height(4.dp))
54+
Text(text = state.character.species, style = MaterialTheme.typography.labelLarge)
55+
Spacer(modifier = Modifier.height(4.dp))
56+
Text(text = state.character.gender.toString(), style = MaterialTheme.typography.labelLarge)
57+
Spacer(modifier = Modifier.height(4.dp))
58+
Text(text = state.character.created, style = MaterialTheme.typography.labelLarge)
59+
60+
ExpandableListCard(
61+
title = "Appears in ${state.character.episode.count()} episodes",
62+
items = state.episodesTask?.optionalResult?.optionalResult?.map { it.name } ?: emptyList(),
63+
onTitleClicked = { isExpanded ->
64+
if (isExpanded) {
65+
dispatch(RumAutoCharacterAction.LoadEpisodes)
66+
}
67+
}
68+
)
69+
}
70+
}
71+
72+
@Preview(showBackground = true)
73+
@Composable
74+
internal fun RumAutoCharacterScreenPreview() {
75+
RumAutoCharacterScreen(
76+
modifier = Modifier.fillMaxSize(),
77+
state = RumAutoCharacterState(
78+
character = Character(
79+
id = 1,
80+
name = "Rick Sanchez",
81+
status = Character.Status.ALIVE,
82+
species = "Human",
83+
type = "",
84+
gender = Character.Gender.MALE,
85+
origin = Character.LocationInfo("Earth (C-137)", ""),
86+
location = Character.LocationInfo("Citadel of Ricks", ""),
87+
image = "",
88+
episode = emptyList(),
89+
url = "",
90+
created = ""
91+
),
92+
episodesTask = null
93+
)
94+
)
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.benchmark.sample.ui.rumauto.screens.characterdetails
8+
9+
import androidx.lifecycle.ViewModel
10+
import androidx.lifecycle.viewModelScope
11+
import com.datadog.benchmark.sample.network.KtorHttpResponse
12+
import com.datadog.benchmark.sample.network.rickandmorty.RickAndMortyNetworkService
13+
import com.datadog.benchmark.sample.network.rickandmorty.models.Character
14+
import com.datadog.benchmark.sample.network.rickandmorty.models.Episode
15+
import com.datadog.benchmark.sample.utils.BenchmarkAsyncTask
16+
import com.datadog.benchmark.sample.utils.StateMachine
17+
import kotlinx.coroutines.CoroutineDispatcher
18+
import kotlinx.coroutines.Job
19+
import kotlinx.coroutines.launch
20+
21+
internal data class RumAutoCharacterState(
22+
val character: Character,
23+
val episodesTask: BenchmarkAsyncTask<KtorHttpResponse<List<Episode>>, EpisodesTaskKey>?
24+
) {
25+
class EpisodesTaskKey(val character: Character)
26+
}
27+
28+
internal sealed interface RumAutoCharacterAction {
29+
object LoadEpisodes : RumAutoCharacterAction
30+
data class EpisodesLoadingFinished(
31+
val key: RumAutoCharacterState.EpisodesTaskKey,
32+
val response: KtorHttpResponse<List<Episode>>
33+
) : RumAutoCharacterAction
34+
}
35+
36+
internal class RumAutoCharacterDetailsViewModel(
37+
private val defaultDispatcher: CoroutineDispatcher,
38+
private val character: Character,
39+
private val rickAndMortyNetworkService: RickAndMortyNetworkService
40+
) : ViewModel() {
41+
private val stateMachine = StateMachine.create(
42+
scope = viewModelScope,
43+
initialState = RumAutoCharacterState(character, null),
44+
dispatcher = defaultDispatcher,
45+
processAction = ::processAction
46+
)
47+
48+
val state = stateMachine.state
49+
50+
fun dispatch(action: RumAutoCharacterAction) {
51+
stateMachine.dispatch(action)
52+
}
53+
54+
private fun processAction(prev: RumAutoCharacterState, action: RumAutoCharacterAction): RumAutoCharacterState {
55+
return prev.copy(
56+
episodesTask = processLoadEpisodesTask(prev, action)
57+
)
58+
}
59+
60+
private fun processLoadEpisodesTask(
61+
prev: RumAutoCharacterState,
62+
action: RumAutoCharacterAction
63+
): BenchmarkAsyncTask<KtorHttpResponse<List<Episode>>, RumAutoCharacterState.EpisodesTaskKey>? {
64+
return when (action) {
65+
is RumAutoCharacterAction.LoadEpisodes -> {
66+
if (prev.episodesTask == null) {
67+
val key = RumAutoCharacterState.EpisodesTaskKey(prev.character)
68+
val job = launchEpisodesLoading(key)
69+
70+
BenchmarkAsyncTask.Loading(job, key)
71+
} else {
72+
prev.episodesTask
73+
}
74+
}
75+
76+
is RumAutoCharacterAction.EpisodesLoadingFinished -> {
77+
BenchmarkAsyncTask.Result(action.response, action.key)
78+
}
79+
}
80+
}
81+
82+
private fun launchEpisodesLoading(key: RumAutoCharacterState.EpisodesTaskKey): Job {
83+
return viewModelScope.launch(defaultDispatcher) {
84+
val response = rickAndMortyNetworkService.getEpisodes(
85+
ids = key.character.episode.mapNotNull { episodeIdFromUrl(it) }
86+
)
87+
88+
dispatch(RumAutoCharacterAction.EpisodesLoadingFinished(key, response))
89+
}
90+
}
91+
}
92+
93+
private fun episodeIdFromUrl(url: String): String? {
94+
return url.split("/").lastOrNull()
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.benchmark.sample.ui.rumauto.screens.characterdetails
8+
9+
import androidx.lifecycle.ViewModel
10+
import androidx.lifecycle.ViewModelProvider
11+
import com.datadog.benchmark.sample.di.common.CoroutineDispatcherQualifier
12+
import com.datadog.benchmark.sample.di.common.CoroutineDispatcherType
13+
import com.datadog.benchmark.sample.network.rickandmorty.RickAndMortyNetworkService
14+
import com.datadog.benchmark.sample.network.rickandmorty.models.Character
15+
import dagger.assisted.Assisted
16+
import dagger.assisted.AssistedFactory
17+
import dagger.assisted.AssistedInject
18+
import kotlinx.coroutines.CoroutineDispatcher
19+
20+
internal class RumAutoCharacterDetailViewModelFactory @AssistedInject constructor(
21+
@Assisted private val character: Character,
22+
@CoroutineDispatcherQualifier(CoroutineDispatcherType.Default)
23+
private val defaultDispatcher: CoroutineDispatcher,
24+
private val rickAndMortyNetworkService: RickAndMortyNetworkService
25+
) : ViewModelProvider.Factory {
26+
27+
@Suppress("UNCHECKED_CAST")
28+
override fun <T : ViewModel> create(modelClass: Class<T>): T {
29+
return RumAutoCharacterDetailsViewModel(
30+
defaultDispatcher = defaultDispatcher,
31+
character = character,
32+
rickAndMortyNetworkService = rickAndMortyNetworkService
33+
) as T
34+
}
35+
}
36+
37+
@AssistedFactory
38+
internal interface AssistedRumAutoCharacterDetailViewModelFactory {
39+
fun create(character: Character): RumAutoCharacterDetailViewModelFactory
40+
}

0 commit comments

Comments
 (0)