Skip to content

Commit

Permalink
Android first problem onboarding (#703)
Browse files Browse the repository at this point in the history
^ALTAPPS-998
  • Loading branch information
XanderZhu authored Oct 11, 2023
1 parent 1e44ea9 commit 81d30d6
Show file tree
Hide file tree
Showing 34 changed files with 524 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.hyperskill.app.android.core.view.ui.widget.compose

import androidx.compose.runtime.Composable
import com.google.accompanist.themeadapter.material.MdcTheme

@Composable
fun HyperskillTheme(content: @Composable () -> Unit) {
MdcTheme(
setTextColors = true,
setDefaultFontFamily = true,
content = content
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.hyperskill.app.android.first_problem_onboarding.fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModelProvider
import org.hyperskill.app.android.HyperskillApp
import org.hyperskill.app.android.core.view.ui.navigation.requireAppRouter
import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme
import org.hyperskill.app.android.first_problem_onboarding.ui.FirstProblemOnboardingScreen
import org.hyperskill.app.core.view.handleActions
import org.hyperskill.app.first_problem_onboarding.presentation.FirstProblemOnboardingFeature.Action.ViewAction
import org.hyperskill.app.first_problem_onboarding.presentation.FirstProblemOnboardingViewModel
import ru.nobird.android.view.base.ui.extension.argument
import ru.nobird.android.view.base.ui.extension.snackbar

class FirstProblemOnboardingFragment : Fragment() {
companion object {
const val FIRST_PROBLEM_ONBOARDING_FINISHED = "FIRST_PROBLEM_ONBOARDING_FINISHED"
fun newInstance(isNewUserMode: Boolean): FirstProblemOnboardingFragment =
FirstProblemOnboardingFragment().apply {
this.isNewUserMode = isNewUserMode
}
}

private var isNewUserMode: Boolean by argument()

private var viewModelFactory: ViewModelProvider.Factory? = null
private val firstProblemOnboardingViewModel: FirstProblemOnboardingViewModel by viewModels {
requireNotNull(viewModelFactory)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
injectComponent()
firstProblemOnboardingViewModel.handleActions(this, block = ::onAction)
}

private fun injectComponent() {
val platformFirstProblemOnboardingComponent =
HyperskillApp.graph().buildPlatformFirstProblemOnboardingComponent(isNewUserMode)
viewModelFactory = platformFirstProblemOnboardingComponent.reduxViewModelFactory
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner))
setContent {
HyperskillTheme {
FirstProblemOnboardingScreen(viewModel = firstProblemOnboardingViewModel)
}
}
}

private fun onAction(action: ViewAction) {
when (action) {
is ViewAction.CompleteFirstProblemOnboarding -> {
requireAppRouter().sendResult(FIRST_PROBLEM_ONBOARDING_FINISHED, action.stepRoute ?: Any())
}
ViewAction.ShowNetworkError -> {
requireView().snackbar(org.hyperskill.app.R.string.first_problem_onboarding_network_error)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.hyperskill.app.android.first_problem_onboarding.navigation

import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentFactory
import com.github.terrakok.cicerone.androidx.FragmentScreen
import org.hyperskill.app.android.first_problem_onboarding.fragment.FirstProblemOnboardingFragment

class FirstProblemOnboardingScreen(
private val isNewUserMode: Boolean
) : FragmentScreen {
override fun createFragment(factory: FragmentFactory): Fragment =
FirstProblemOnboardingFragment.newInstance(isNewUserMode)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package org.hyperskill.app.android.first_problem_onboarding.ui

import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.sp
import org.hyperskill.app.R
import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillButton
import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme
import org.hyperskill.app.first_problem_onboarding.presentation.FirstProblemOnboardingFeature.Message
import org.hyperskill.app.first_problem_onboarding.presentation.FirstProblemOnboardingFeature.ViewState

@Composable
fun FirstProblemOnboardingContent(
content: ViewState.Content,
onNewMessage: (Message) -> Unit
) {
val onButtonClicked by rememberUpdatedState {
onNewMessage(Message.CallToActionButtonClicked)
}
FirstProblemOnboardingContent(
title = content.title,
subtitle = content.subtitle,
image = painterResource(id = getImage(content.isNewUserMode)),
buttonText = content.buttonText,
onButtonClicked = onButtonClicked
)
}

@Composable
fun FirstProblemOnboardingContent(
title: String,
subtitle: String,
image: Painter,
buttonText: String,
onButtonClicked: () -> Unit
) {
FirstProblemOnboardingDefaults.ContentRootColumn {
Column(
verticalArrangement = Arrangement.spacedBy(FirstProblemOnboardingDefaults.HeaderBottomPadding)
) {
Text(
text = title,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.h5,
fontSize = 24.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Text(
text = subtitle,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.body1,
color = colorResource(id = R.color.text_secondary),
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
Image(
painter = image,
contentDescription = null,
modifier = Modifier.fillMaxWidth()
)
HyperskillButton(
onClick = onButtonClicked,
modifier = Modifier.fillMaxWidth()
) {
Text(text = buttonText)
}
}
}

private fun getImage(isNewUserMode: Boolean) =
if (isNewUserMode) {
org.hyperskill.app.android.R.drawable.img_first_problem_onboarding_new_user
} else {
org.hyperskill.app.android.R.drawable.img_first_problem_onboarding_experienced_user
}

private data class FirstProblemOnboardingData(
val title: String,
val subtitle: String,
val buttonText: String,
@DrawableRes val imageRes: Int
)

private class SampleFirstProblemOnboardingPreviewDataProvider :
PreviewParameterProvider<FirstProblemOnboardingData> {
override val values: Sequence<FirstProblemOnboardingData> = sequenceOf(
FirstProblemOnboardingData(
title = "Let's keep going",
subtitle = "It seems you've already made progress. Continue learning on '{project(or track).title}'!",
buttonText = "Keep learning",
imageRes = getImage(isNewUserMode = false)
),
FirstProblemOnboardingData(
title = "Great choice!",
subtitle = "Embark on your journey in '{project(or track).title}' right now!",
buttonText = "Start learning",
imageRes = getImage(isNewUserMode = true)
)
)
}

@Preview(
group = "Light theme",
showBackground = true,
showSystemUi = true
)
@Composable
private fun FirstProblemOnboardingScreenLightThemePreview(
@PreviewParameter(SampleFirstProblemOnboardingPreviewDataProvider::class)
onboardingData: FirstProblemOnboardingData
) {
FirstProblemOnboardingScreenPreview(onboardingData)
}

@Preview(
group = "Dart theme",
showBackground = true,
showSystemUi = true,
uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL
)
@Composable
private fun FirstProblemOnboardingScreenDarkModePreview(
@PreviewParameter(SampleFirstProblemOnboardingPreviewDataProvider::class)
onboardingData: FirstProblemOnboardingData
) {
FirstProblemOnboardingScreenPreview(onboardingData)
}

@Preview(
group = "Small device",
showBackground = true,
showSystemUi = true,
device = "spec:parent=Nexus S"
)
@Composable
private fun FirstProblemOnboardingScreenSmallDevicePreview(
@PreviewParameter(SampleFirstProblemOnboardingPreviewDataProvider::class)
onboardingData: FirstProblemOnboardingData
) {
FirstProblemOnboardingScreenPreview(onboardingData)
}

@Composable
private fun FirstProblemOnboardingScreenPreview(
onboardingData: FirstProblemOnboardingData
) {
HyperskillTheme {
FirstProblemOnboardingContent(
title = onboardingData.title,
subtitle = onboardingData.subtitle,
buttonText = onboardingData.buttonText,
image = painterResource(id = onboardingData.imageRes),
onButtonClicked = {}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.hyperskill.app.android.first_problem_onboarding.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.hyperskill.app.R

object FirstProblemOnboardingDefaults {
val ContentPadding: PaddingValues = PaddingValues(
top = 24.dp,
bottom = 32.dp,
start = 20.dp,
end = 20.dp
)

val HeaderBottomPadding: Dp = 8.dp

@Composable
fun ContentRootColumn(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.SpaceBetween,
content: @Composable ColumnScope.() -> Unit
) {
Column(
modifier = modifier
.fillMaxSize()
.background(colorResource(id = R.color.layer_1))
.padding(ContentPadding),
verticalArrangement = verticalArrangement,
content = content
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.hyperskill.app.android.first_problem_onboarding.ui

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.hyperskill.app.android.R
import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme
import org.hyperskill.app.android.core.view.ui.widget.compose.ShimmerLoading

@Composable
fun FirstProblemOnboardingSkeleton() {
FirstProblemOnboardingDefaults.ContentRootColumn {
Column(
modifier = Modifier.weight(1f).fillMaxWidth()
) {
ShimmerLoading(
radius = dimensionResource(id = R.dimen.corner_radius),
modifier = Modifier
.width(160.dp)
.height(28.dp)
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(FirstProblemOnboardingDefaults.HeaderBottomPadding))
ShimmerLoading(
radius = dimensionResource(id = R.dimen.corner_radius),
modifier = Modifier
.width(320.dp)
.height(16.dp)
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(4.dp))
ShimmerLoading(
radius = dimensionResource(id = R.dimen.corner_radius),
modifier = Modifier
.width(280.dp)
.height(16.dp)
.align(Alignment.CenterHorizontally)
)
}
ShimmerLoading(
radius = dimensionResource(id = R.dimen.corner_radius),
modifier = Modifier
.clip(MaterialTheme.shapes.small)
.fillMaxWidth()
.height(dimensionResource(id = R.dimen.action_button_height))
)
}
}

@Preview(
showBackground = true,
showSystemUi = true
)
@Composable
private fun FirstProblemOnboardingSkeletonPreview() {
HyperskillTheme {
FirstProblemOnboardingSkeleton()
}
}
Loading

0 comments on commit 81d30d6

Please sign in to comment.