Skip to content

Commit 16acbc3

Browse files
authored
Android: Onboarding questionnaire (#924)
^ALTAPPS-1145
1 parent 75552cf commit 16acbc3

File tree

18 files changed

+481
-12
lines changed

18 files changed

+481
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.hyperskill.app.android.core.extensions
2+
3+
import androidx.compose.foundation.layout.PaddingValues
4+
import androidx.compose.foundation.layout.calculateEndPadding
5+
import androidx.compose.foundation.layout.calculateStartPadding
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.ui.platform.LocalLayoutDirection
8+
9+
@Composable
10+
operator fun PaddingValues.plus(other: PaddingValues): PaddingValues {
11+
val layoutDirection = LocalLayoutDirection.current
12+
return PaddingValues(
13+
start = this.calculateStartPadding(layoutDirection) +
14+
other.calculateStartPadding(layoutDirection),
15+
top = this.calculateTopPadding() + other.calculateTopPadding(),
16+
end = this.calculateEndPadding(layoutDirection) +
17+
other.calculateEndPadding(layoutDirection),
18+
bottom = this.calculateBottomPadding() + other.calculateBottomPadding(),
19+
)
20+
}

androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/activity/MainActivity.kt

+17-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ import org.hyperskill.app.android.paywall.navigation.PaywallScreen
4848
import org.hyperskill.app.android.step.view.screen.StepScreen
4949
import org.hyperskill.app.android.streak_recovery.view.delegate.StreakRecoveryViewActionDelegate
5050
import org.hyperskill.app.android.track_selection.list.navigation.TrackSelectionListScreen
51+
import org.hyperskill.app.android.users_questionnaire.onboarding.fragment.UsersQuestionnaireOnboardingFragment
52+
import org.hyperskill.app.android.users_questionnaire.onboarding.navigation.UsersQuestionnaireOnboardingScreen
5153
import org.hyperskill.app.android.welcome.navigation.WelcomeScreen
5254
import org.hyperskill.app.main.presentation.AppFeature
5355
import org.hyperskill.app.main.presentation.MainViewModel
@@ -139,6 +141,7 @@ class MainActivity :
139141
observeAuthFlowSuccess()
140142
observeNotificationsOnboardingFlowFinished()
141143
observeFirstProblemOnboardingFlowFinished()
144+
observeUsersQuestionnaireOnboardingCompleted()
142145
observePaywallCompleted()
143146

144147
mainViewModel.logScreenOrientation(screenOrientation = resources.configuration.screenOrientation)
@@ -228,6 +231,19 @@ class MainActivity :
228231
}
229232
}
230233

234+
private fun observeUsersQuestionnaireOnboardingCompleted() {
235+
lifecycleScope.launch {
236+
router
237+
.observeResult(UsersQuestionnaireOnboardingFragment.USERS_QUESTIONNAIRE_ONBOARDING_FINISHED)
238+
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
239+
.collectLatest {
240+
mainViewModel.onNewMessage(
241+
WelcomeOnboardingFeature.Message.UsersQuestionnaireOnboardingCompleted
242+
)
243+
}
244+
}
245+
}
246+
231247
override fun onNewIntent(intent: Intent?) {
232248
super.onNewIntent(intent)
233249
if (intent != null) {
@@ -280,7 +296,7 @@ class MainActivity :
280296
is WelcomeOnboardingFeature.Action.ViewAction.NavigateTo.Paywall ->
281297
router.newRootScreen(PaywallScreen(viewAction.paywallTransitionSource))
282298
WelcomeOnboardingFeature.Action.ViewAction.NavigateTo.UsersQuestionnaireOnboardingScreen ->
283-
TODO("ALTAPPS-1145: Implement QuestionnaireOnboardingScreen navigation")
299+
router.newRootScreen(UsersQuestionnaireOnboardingScreen)
284300
}
285301
is AppFeature.Action.ViewAction.StreakRecoveryViewAction ->
286302
StreakRecoveryViewActionDelegate.handleViewAction(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.hyperskill.app.android.users_questionnaire.onboarding.fragment
2+
3+
import android.os.Bundle
4+
import android.view.LayoutInflater
5+
import android.view.View
6+
import android.view.ViewGroup
7+
import androidx.compose.ui.platform.ComposeView
8+
import androidx.compose.ui.platform.ViewCompositionStrategy
9+
import androidx.fragment.app.Fragment
10+
import androidx.fragment.app.viewModels
11+
import androidx.lifecycle.ViewModelProvider
12+
import com.google.android.material.snackbar.Snackbar
13+
import org.hyperskill.app.android.HyperskillApp
14+
import org.hyperskill.app.android.core.view.ui.navigation.requireAppRouter
15+
import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme
16+
import org.hyperskill.app.android.users_questionnaire.onboarding.ui.UsersQuestionnaireOnboardingScreen
17+
import org.hyperskill.app.core.view.handleActions
18+
import org.hyperskill.app.users_questionnaire.onboarding.presentation.UsersQuestionnaireOnboardingFeature.Action.ViewAction
19+
import org.hyperskill.app.users_questionnaire.onboarding.presentation.UsersQuestionnaireOnboardingViewModel
20+
21+
class UsersQuestionnaireOnboardingFragment : Fragment() {
22+
companion object {
23+
const val USERS_QUESTIONNAIRE_ONBOARDING_FINISHED = "USERS_QUESTIONNAIRE_ONBOARDING_FINISHED"
24+
fun newInstance(): UsersQuestionnaireOnboardingFragment =
25+
UsersQuestionnaireOnboardingFragment()
26+
}
27+
28+
private var viewModelFactory: ViewModelProvider.Factory? = null
29+
private val usersQuestionnaireOnboardingViewModel: UsersQuestionnaireOnboardingViewModel by viewModels {
30+
requireNotNull(viewModelFactory)
31+
}
32+
33+
override fun onCreate(savedInstanceState: Bundle?) {
34+
super.onCreate(savedInstanceState)
35+
injectComponent()
36+
usersQuestionnaireOnboardingViewModel.handleActions(this, onAction = ::onAction)
37+
}
38+
39+
private fun injectComponent() {
40+
val platformUsersQuestionnaireOnboardingComponent =
41+
HyperskillApp.graph().buildPlatformUsersQuestionnaireOnboardingComponent()
42+
viewModelFactory = platformUsersQuestionnaireOnboardingComponent.reduxViewModelFactory
43+
}
44+
45+
override fun onCreateView(
46+
inflater: LayoutInflater,
47+
container: ViewGroup?,
48+
savedInstanceState: Bundle?
49+
): View =
50+
ComposeView(requireContext()).apply {
51+
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner))
52+
setContent {
53+
HyperskillTheme {
54+
UsersQuestionnaireOnboardingScreen(viewModel = usersQuestionnaireOnboardingViewModel)
55+
}
56+
}
57+
}
58+
59+
private fun onAction(action: ViewAction) {
60+
when (action) {
61+
ViewAction.CompleteUsersQuestionnaireOnboarding -> {
62+
requireAppRouter().sendResult(USERS_QUESTIONNAIRE_ONBOARDING_FINISHED, Any())
63+
}
64+
is ViewAction.ShowSendSuccessMessage -> {
65+
Snackbar.make(requireView(), action.message, Snackbar.LENGTH_SHORT).show()
66+
}
67+
}
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.hyperskill.app.android.users_questionnaire.onboarding.navigation
2+
3+
import androidx.fragment.app.Fragment
4+
import androidx.fragment.app.FragmentFactory
5+
import com.github.terrakok.cicerone.androidx.FragmentScreen
6+
import org.hyperskill.app.android.users_questionnaire.onboarding.fragment.UsersQuestionnaireOnboardingFragment
7+
8+
object UsersQuestionnaireOnboardingScreen : FragmentScreen {
9+
override fun createFragment(factory: FragmentFactory): Fragment =
10+
UsersQuestionnaireOnboardingFragment.newInstance()
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.hyperskill.app.android.users_questionnaire.onboarding.ui
2+
3+
import androidx.compose.foundation.layout.PaddingValues
4+
import androidx.compose.ui.unit.dp
5+
6+
object UsersQuestionnaireOnboardingDefaults {
7+
val ContentPadding: PaddingValues = PaddingValues(
8+
top = 20.dp,
9+
start = 20.dp,
10+
end = 20.dp,
11+
bottom = 8.dp
12+
)
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.hyperskill.app.android.users_questionnaire.onboarding.ui
2+
3+
import org.hyperskill.app.users_questionnaire.onboarding.presentation.UsersQuestionnaireOnboardingFeature
4+
5+
object UsersQuestionnaireOnboardingPreviewDefault {
6+
7+
private enum class SelectedChoice {
8+
NONE,
9+
FIRST,
10+
LAST
11+
}
12+
13+
private fun getViewState(
14+
selectedChoice: SelectedChoice,
15+
isSendButtonEnabled: Boolean
16+
) =
17+
UsersQuestionnaireOnboardingFeature.ViewState(
18+
title = "How did you hear about Hyperskill?",
19+
choices = listOf(
20+
"Google",
21+
"Youtube",
22+
"Instagram",
23+
"Tiktok",
24+
"News",
25+
"Friends",
26+
"Other"
27+
),
28+
selectedChoice = when (selectedChoice) {
29+
SelectedChoice.NONE -> null
30+
SelectedChoice.FIRST -> "Google"
31+
SelectedChoice.LAST -> "Other"
32+
},
33+
textInputValue = when (selectedChoice) {
34+
SelectedChoice.NONE -> null
35+
SelectedChoice.FIRST,
36+
SelectedChoice.LAST -> "example text"
37+
},
38+
isTextInputVisible = selectedChoice == SelectedChoice.LAST,
39+
isSendButtonEnabled = when (selectedChoice) {
40+
SelectedChoice.NONE -> false
41+
SelectedChoice.FIRST,
42+
SelectedChoice.LAST -> isSendButtonEnabled
43+
}
44+
)
45+
46+
fun getUnselectedViewState(): UsersQuestionnaireOnboardingFeature.ViewState =
47+
getViewState(SelectedChoice.NONE, false)
48+
49+
fun getFirstOptionSelectedViewState(): UsersQuestionnaireOnboardingFeature.ViewState =
50+
getViewState(SelectedChoice.FIRST, true)
51+
52+
fun getOtherOptionSelectedViewState(isSendButtonEnabled: Boolean): UsersQuestionnaireOnboardingFeature.ViewState =
53+
getViewState(SelectedChoice.LAST, isSendButtonEnabled)
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package org.hyperskill.app.android.users_questionnaire.onboarding.ui
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Spacer
6+
import androidx.compose.foundation.layout.fillMaxSize
7+
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.layout.height
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.foundation.rememberScrollState
11+
import androidx.compose.foundation.verticalScroll
12+
import androidx.compose.material.MaterialTheme
13+
import androidx.compose.material.Scaffold
14+
import androidx.compose.material.Text
15+
import androidx.compose.runtime.Composable
16+
import androidx.compose.runtime.DisposableEffect
17+
import androidx.compose.runtime.getValue
18+
import androidx.compose.ui.Modifier
19+
import androidx.compose.ui.res.stringResource
20+
import androidx.compose.ui.text.style.TextAlign
21+
import androidx.compose.ui.tooling.preview.Preview
22+
import androidx.compose.ui.tooling.preview.PreviewParameter
23+
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
24+
import androidx.compose.ui.unit.dp
25+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
26+
import org.hyperskill.app.R
27+
import org.hyperskill.app.android.core.extensions.plus
28+
import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillButton
29+
import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTextButton
30+
import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme
31+
import org.hyperskill.app.users_questionnaire.onboarding.presentation.UsersQuestionnaireOnboardingFeature
32+
import org.hyperskill.app.users_questionnaire.onboarding.presentation.UsersQuestionnaireOnboardingFeature.ViewState
33+
import org.hyperskill.app.users_questionnaire.onboarding.presentation.UsersQuestionnaireOnboardingViewModel
34+
35+
@Composable
36+
fun UsersQuestionnaireOnboardingScreen(viewModel: UsersQuestionnaireOnboardingViewModel) {
37+
DisposableEffect(viewModel) {
38+
viewModel.onNewMessage(
39+
UsersQuestionnaireOnboardingFeature.Message.ViewedEventMessage
40+
)
41+
onDispose {
42+
// no op
43+
}
44+
}
45+
val viewState by viewModel.state.collectAsStateWithLifecycle()
46+
UsersQuestionnaireOnboardingScreen(
47+
viewState = viewState,
48+
onChoiceClicked = viewModel::onChoiceClicked,
49+
onTextInputChanged = viewModel::onTextInputChanged,
50+
onSendClick = viewModel::onSendButtonClick,
51+
onSkipClick = viewModel::onSkipButtonClick
52+
)
53+
}
54+
55+
@Composable
56+
fun UsersQuestionnaireOnboardingScreen(
57+
viewState: ViewState,
58+
onChoiceClicked: (String) -> Unit,
59+
onTextInputChanged: (String) -> Unit,
60+
onSendClick: () -> Unit,
61+
onSkipClick: () -> Unit,
62+
modifier: Modifier = Modifier
63+
) {
64+
Scaffold { padding ->
65+
Column(
66+
modifier = modifier
67+
.fillMaxSize()
68+
.verticalScroll(rememberScrollState())
69+
.padding(padding + UsersQuestionnaireOnboardingDefaults.ContentPadding)
70+
) {
71+
Text(
72+
text = viewState.title,
73+
style = MaterialTheme.typography.h5,
74+
textAlign = TextAlign.Center,
75+
modifier = Modifier.fillMaxWidth()
76+
)
77+
Spacer(modifier = Modifier.height(20.dp))
78+
UsersQuestionnaireOptionsList(
79+
choices = viewState.choices,
80+
selectedChoice = viewState.selectedChoice,
81+
textInputValue = viewState.textInputValue,
82+
isTextInputVisible = viewState.isTextInputVisible,
83+
onChoiceClicked = onChoiceClicked,
84+
onTextInputChanged = onTextInputChanged,
85+
onDoneClick = onSendClick
86+
)
87+
Spacer(modifier = Modifier.height(32.dp))
88+
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
89+
HyperskillButton(
90+
onClick = onSendClick,
91+
enabled = viewState.isSendButtonEnabled,
92+
modifier = Modifier.fillMaxWidth()
93+
) {
94+
Text(text = stringResource(id = R.string.users_questionnaire_onboarding_send_button_text))
95+
}
96+
HyperskillTextButton(
97+
onClick = onSkipClick,
98+
modifier = Modifier.fillMaxWidth()
99+
) {
100+
Text(text = stringResource(id = R.string.users_questionnaire_onboarding_skip_button_text))
101+
}
102+
}
103+
}
104+
}
105+
}
106+
107+
private class UsersQuestionnaireOnboardingPreviewParameterProvider : PreviewParameterProvider<ViewState> {
108+
override val values: Sequence<ViewState>
109+
get() = sequenceOf(
110+
UsersQuestionnaireOnboardingPreviewDefault.getUnselectedViewState(),
111+
UsersQuestionnaireOnboardingPreviewDefault.getFirstOptionSelectedViewState(),
112+
UsersQuestionnaireOnboardingPreviewDefault.getOtherOptionSelectedViewState(false),
113+
UsersQuestionnaireOnboardingPreviewDefault.getOtherOptionSelectedViewState(true)
114+
)
115+
}
116+
117+
@Preview(device = "id:pixel_3", showSystemUi = true)
118+
@Composable
119+
private fun UsersQuestionnaireOnboardingScreenPreview(
120+
@PreviewParameter(UsersQuestionnaireOnboardingPreviewParameterProvider::class)
121+
viewState: ViewState
122+
) {
123+
HyperskillTheme {
124+
UsersQuestionnaireOnboardingScreen(
125+
viewState = viewState,
126+
onChoiceClicked = {},
127+
onTextInputChanged = {},
128+
onSendClick = {},
129+
onSkipClick = {}
130+
)
131+
}
132+
}
133+
134+
@Preview(device = "id:Nexus S", showSystemUi = true)
135+
@Composable
136+
private fun UsersQuestionnaireOnboardingScreenPreviewSmallDevice(
137+
@PreviewParameter(UsersQuestionnaireOnboardingPreviewParameterProvider::class)
138+
viewState: ViewState
139+
) {
140+
HyperskillTheme {
141+
UsersQuestionnaireOnboardingScreen(
142+
viewState = viewState,
143+
onChoiceClicked = {},
144+
onTextInputChanged = {},
145+
onSendClick = {},
146+
onSkipClick = {}
147+
)
148+
}
149+
}

0 commit comments

Comments
 (0)