Skip to content

Commit c466e20

Browse files
RUM-9511: Episodes and EpisodeDetails screens
1 parent 35410ba commit c466e20

File tree

9 files changed

+639
-0
lines changed

9 files changed

+639
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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.episodedetails
8+
9+
import com.datadog.benchmark.sample.ui.rumauto.screens.common.details.characterItemDelegate
10+
import com.datadog.benchmark.sample.utils.recycler.BaseRecyclerViewItem
11+
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
12+
import javax.inject.Inject
13+
14+
internal class RumAutoEpisodeDetailsAdapter @Inject constructor(
15+
private val viewModel: RumAutoEpisodeDetailsViewModel
16+
) : ListDelegationAdapter<List<BaseRecyclerViewItem>>() {
17+
init {
18+
delegatesManager.apply {
19+
addDelegate(
20+
characterItemDelegate { viewModel.dispatch(RumAutoEpisodeDetailsAction.OnCharacterClicked(it)) }
21+
)
22+
}
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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.episodedetails
8+
9+
import android.os.Bundle
10+
import android.view.LayoutInflater
11+
import android.view.View
12+
import android.view.ViewGroup
13+
import androidx.fragment.app.Fragment
14+
import androidx.lifecycle.lifecycleScope
15+
import androidx.lifecycle.viewModelScope
16+
import androidx.recyclerview.widget.GridLayoutManager
17+
import com.datadog.benchmark.sample.activities.scenarios.benchmarkActivityComponent
18+
import com.datadog.benchmark.sample.navigation.args
19+
import com.datadog.benchmark.sample.network.rickandmorty.models.Episode
20+
import com.datadog.benchmark.sample.ui.rumauto.screens.common.details.CharacterItem
21+
import com.datadog.benchmark.sample.ui.rumauto.screens.episodedetails.di.DaggerRumAutoEpisodeDetailsComponent
22+
import com.datadog.benchmark.sample.ui.rumauto.screens.episodedetails.di.RumAutoEpisodeDetailsComponent
23+
import com.datadog.benchmark.sample.utils.componentHolderViewModel
24+
import com.datadog.benchmark.sample.utils.recycler.applyNewItems
25+
import com.datadog.sample.benchmark.databinding.FragmentRumAutoEpisodeDetailsBinding
26+
import kotlinx.coroutines.flow.launchIn
27+
import kotlinx.coroutines.flow.onEach
28+
import javax.inject.Inject
29+
30+
internal class RumAutoEpisodeDetailsFragment : Fragment() {
31+
32+
private val episode: Episode by args()
33+
34+
private val component: RumAutoEpisodeDetailsComponent by componentHolderViewModel {
35+
DaggerRumAutoEpisodeDetailsComponent.factory().create(
36+
deps = requireActivity().benchmarkActivityComponent,
37+
viewModelScope = viewModelScope,
38+
episode = episode
39+
)
40+
}
41+
42+
@Inject
43+
internal lateinit var adapter: RumAutoEpisodeDetailsAdapter
44+
45+
@Inject
46+
internal lateinit var viewModel: RumAutoEpisodeDetailsViewModel
47+
48+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
49+
component.inject(this)
50+
51+
val binding: FragmentRumAutoEpisodeDetailsBinding = FragmentRumAutoEpisodeDetailsBinding.inflate(
52+
inflater,
53+
container,
54+
false
55+
)
56+
57+
@Suppress("MagicNumber")
58+
binding.characterDetailsRecycler.layoutManager = GridLayoutManager(requireContext(), 3)
59+
binding.characterDetailsRecycler.adapter = adapter
60+
61+
viewModel.state
62+
.onEach { state ->
63+
binding.episodeDetailsTitle.text = state.episode.name
64+
binding.episodeDetailsCode.text = state.episode.episodeCode
65+
binding.episodeDetailsAirDate.text = state.episode.airDate
66+
binding.episodeDetailsCreated.text = state.episode.created
67+
68+
val characters = state
69+
.charactersLoadingTask
70+
.optionalResult
71+
?.optionalResult
72+
?.map { character ->
73+
CharacterItem(
74+
character = character,
75+
key = character.id.toString()
76+
)
77+
} ?: emptyList()
78+
79+
adapter.applyNewItems(characters)
80+
}
81+
.launchIn(lifecycleScope)
82+
83+
return binding.root
84+
}
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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.episodedetails
8+
9+
import com.datadog.benchmark.sample.di.common.CoroutineDispatcherQualifier
10+
import com.datadog.benchmark.sample.di.common.CoroutineDispatcherType
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.ui.rumauto.RumAutoScenarioNavigator
16+
import com.datadog.benchmark.sample.ui.rumauto.screens.episodedetails.di.RumAutoEpisodeDetailsScope
17+
import com.datadog.benchmark.sample.utils.BenchmarkAsyncTask
18+
import com.datadog.benchmark.sample.utils.StateMachine
19+
import kotlinx.coroutines.CoroutineDispatcher
20+
import kotlinx.coroutines.CoroutineScope
21+
import kotlinx.coroutines.Job
22+
import kotlinx.coroutines.flow.StateFlow
23+
import kotlinx.coroutines.launch
24+
import javax.inject.Inject
25+
26+
internal data class RumAutoEpisodeDetailsState(
27+
val episode: Episode,
28+
val charactersLoadingTask: BenchmarkAsyncTask<KtorHttpResponse<List<Character>>, CharactersLoadingTask>
29+
) {
30+
class CharactersLoadingTask(val ids: List<String>)
31+
}
32+
33+
internal sealed interface RumAutoEpisodeDetailsAction {
34+
data class CharactersLoadingTaskFinished(
35+
val result: KtorHttpResponse<List<Character>>,
36+
val task: RumAutoEpisodeDetailsState.CharactersLoadingTask
37+
) : RumAutoEpisodeDetailsAction
38+
39+
data class OnCharacterClicked(val character: Character) : RumAutoEpisodeDetailsAction
40+
}
41+
42+
@RumAutoEpisodeDetailsScope
43+
internal class RumAutoEpisodeDetailsViewModel @Inject constructor(
44+
private val episode: Episode,
45+
private val rickAndMortyNetworkService: RickAndMortyNetworkService,
46+
@CoroutineDispatcherQualifier(CoroutineDispatcherType.Default) private val defaultDispatcher: CoroutineDispatcher,
47+
private val viewModelScope: CoroutineScope,
48+
private val rumAutoScenarioNavigator: RumAutoScenarioNavigator
49+
) {
50+
private val stateMachine = StateMachine.create(
51+
scope = viewModelScope,
52+
dispatcher = defaultDispatcher,
53+
initialState = RumAutoEpisodeDetailsState(
54+
episode = episode,
55+
charactersLoadingTask = run {
56+
val task = RumAutoEpisodeDetailsState.CharactersLoadingTask(characterIds(episode))
57+
val job = launchCharactersLoadingTask(task)
58+
BenchmarkAsyncTask.Loading(job, task)
59+
}
60+
),
61+
processAction = ::processAction
62+
)
63+
64+
val state: StateFlow<RumAutoEpisodeDetailsState> = stateMachine.state
65+
66+
fun dispatch(action: RumAutoEpisodeDetailsAction) {
67+
stateMachine.dispatch(action)
68+
}
69+
70+
private fun processAction(
71+
prev: RumAutoEpisodeDetailsState,
72+
action: RumAutoEpisodeDetailsAction
73+
): RumAutoEpisodeDetailsState {
74+
return when (action) {
75+
is RumAutoEpisodeDetailsAction.CharactersLoadingTaskFinished -> {
76+
prev.copy(
77+
charactersLoadingTask = BenchmarkAsyncTask.Result(action.result, action.task)
78+
)
79+
}
80+
81+
is RumAutoEpisodeDetailsAction.OnCharacterClicked -> {
82+
viewModelScope.launch {
83+
rumAutoScenarioNavigator.openCharacterScreen(action.character)
84+
}
85+
prev
86+
}
87+
}
88+
}
89+
90+
private fun launchCharactersLoadingTask(task: RumAutoEpisodeDetailsState.CharactersLoadingTask): Job {
91+
return viewModelScope.launch(defaultDispatcher) {
92+
val result = rickAndMortyNetworkService.getCharacters(task.ids)
93+
dispatch(RumAutoEpisodeDetailsAction.CharactersLoadingTaskFinished(result, task))
94+
}
95+
}
96+
97+
private fun characterIds(episode: Episode): List<String> {
98+
return episode.characters.mapNotNull { it.split("/").lastOrNull() }
99+
}
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.episodedetails.di
8+
9+
import com.datadog.benchmark.sample.di.common.DispatchersModule
10+
import com.datadog.benchmark.sample.network.rickandmorty.RickAndMortyNetworkService
11+
import com.datadog.benchmark.sample.network.rickandmorty.models.Episode
12+
import com.datadog.benchmark.sample.ui.rumauto.RumAutoScenarioNavigator
13+
import com.datadog.benchmark.sample.ui.rumauto.screens.episodedetails.RumAutoEpisodeDetailsFragment
14+
import dagger.BindsInstance
15+
import dagger.Component
16+
import kotlinx.coroutines.CoroutineScope
17+
import javax.inject.Scope
18+
19+
internal interface RumAutoEpisodeDetailsComponentDependencies {
20+
val rickAndMortyNetworkService: RickAndMortyNetworkService
21+
val rumAutoScenarioNavigator: RumAutoScenarioNavigator
22+
}
23+
24+
@Scope
25+
internal annotation class RumAutoEpisodeDetailsScope
26+
27+
@Component(
28+
dependencies = [
29+
RumAutoEpisodeDetailsComponentDependencies::class
30+
],
31+
modules = [
32+
DispatchersModule::class
33+
]
34+
)
35+
@RumAutoEpisodeDetailsScope
36+
internal interface RumAutoEpisodeDetailsComponent {
37+
@Component.Factory
38+
interface Factory {
39+
fun create(
40+
deps: RumAutoEpisodeDetailsComponentDependencies,
41+
@BindsInstance viewModelScope: CoroutineScope,
42+
@BindsInstance episode: Episode
43+
): RumAutoEpisodeDetailsComponent
44+
}
45+
46+
fun inject(fragment: RumAutoEpisodeDetailsFragment)
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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.episodes
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.foundation.layout.fillMaxSize
14+
import androidx.compose.runtime.getValue
15+
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.platform.ComposeView
17+
import androidx.fragment.app.Fragment
18+
import androidx.fragment.app.viewModels
19+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
20+
import com.datadog.benchmark.sample.activities.scenarios.benchmarkActivityComponent
21+
import javax.inject.Inject
22+
23+
internal class RumAutoEpisodesListFragment : Fragment() {
24+
@Inject
25+
internal lateinit var viewModelFactory: RumAutoEpisodesListViewModelFactory
26+
27+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
28+
requireActivity().benchmarkActivityComponent.inject(this)
29+
30+
val viewModel by viewModels<RumAutoEpisodesListViewModel> {
31+
viewModelFactory
32+
}
33+
34+
return ComposeView(requireContext()).apply {
35+
setContent {
36+
val state by viewModel.state.collectAsStateWithLifecycle()
37+
38+
RumAutoEpisodesListScreen(
39+
modifier = Modifier.fillMaxSize(),
40+
state = state,
41+
dispatch = viewModel::dispatch
42+
)
43+
}
44+
}
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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.episodes
8+
9+
import androidx.compose.foundation.clickable
10+
import androidx.compose.foundation.layout.Box
11+
import androidx.compose.foundation.layout.Column
12+
import androidx.compose.foundation.layout.Row
13+
import androidx.compose.foundation.layout.Spacer
14+
import androidx.compose.foundation.layout.height
15+
import androidx.compose.foundation.lazy.LazyColumn
16+
import androidx.compose.foundation.lazy.LazyListState
17+
import androidx.compose.foundation.lazy.items
18+
import androidx.compose.foundation.lazy.rememberLazyListState
19+
import androidx.compose.material3.Divider
20+
import androidx.compose.material3.MaterialTheme
21+
import androidx.compose.material3.Text
22+
import androidx.compose.runtime.Composable
23+
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.unit.dp
25+
import com.datadog.benchmark.sample.utils.LastItemTracker
26+
27+
@Composable
28+
internal fun RumAutoEpisodesListScreen(
29+
modifier: Modifier,
30+
state: RumAutoEpisodesListState,
31+
dispatch: (RumAutoEpisodesListAction) -> Unit
32+
) {
33+
val lazyListState: LazyListState = rememberLazyListState()
34+
35+
LastItemTracker(
36+
allItems = state.allEpisodes,
37+
lazyListState = lazyListState,
38+
onEndReached = { dispatch(RumAutoEpisodesListAction.EndReached) },
39+
itemId = { it.id }
40+
)
41+
42+
Box(modifier = modifier) {
43+
LazyColumn(state = lazyListState) {
44+
items(state.allEpisodes, key = { it.id }) { item ->
45+
Spacer(modifier = Modifier.height(8.dp))
46+
Column(
47+
modifier = Modifier.clickable { dispatch(RumAutoEpisodesListAction.EpisodeClicked(item)) }
48+
) {
49+
Text(text = item.name, style = MaterialTheme.typography.labelLarge)
50+
Row {
51+
Text(text = item.episodeCode, modifier = Modifier.weight(1.0f))
52+
Spacer(modifier = Modifier.height(2.dp))
53+
Text(text = item.airDate)
54+
}
55+
}
56+
Spacer(modifier = Modifier.height(8.dp))
57+
Divider(
58+
modifier = Modifier.height(1.dp),
59+
color = MaterialTheme.colorScheme.outlineVariant,
60+
thickness = 1.dp
61+
)
62+
}
63+
}
64+
}
65+
}

0 commit comments

Comments
 (0)