Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ALTAPPS-998: Android first problem onboarding #703

Merged
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
Loading