Skip to content

Commit

Permalink
Support nextLearningActivity widget on Android home screen (#598)
Browse files Browse the repository at this point in the history
^ALTAPPS-863 Needs testing
  • Loading branch information
XanderZhu authored Jul 26, 2023
1 parent 5201096 commit cf05146
Show file tree
Hide file tree
Showing 16 changed files with 323 additions and 118 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.hyperskill.app.android.core.view.ui.adapter

import org.hyperskill.app.android.R
import org.hyperskill.app.android.databinding.WidgetDataLoadingErrorBinding
import ru.nobird.android.ui.adapterdelegates.AdapterDelegate
import ru.nobird.android.ui.adapterdelegates.DelegateViewHolder
import ru.nobird.android.ui.adapterdelegates.dsl.adapterDelegate

object DataLoadingErrorAdapterDelegate {
// adapterDelegate function is used instead of creating a special class
// because class casting requires a reified type
inline operator fun <D, reified DT : D> invoke(
crossinline onReloadClick: (DT) -> Unit
): AdapterDelegate<D, DelegateViewHolder<D>> =
adapterDelegate<D, DT>(R.layout.widget_data_loading_error) {
val viewBinding = WidgetDataLoadingErrorBinding.bind(itemView)
viewBinding.reloadButton.setOnClickListener {
item?.let(onReloadClick)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import org.hyperskill.app.android.core.view.ui.updateIsRefreshing
import org.hyperskill.app.android.databinding.FragmentHomeBinding
import org.hyperskill.app.android.gamification_toolbar.view.ui.delegate.GamificationToolbarDelegate
import org.hyperskill.app.android.main.view.ui.navigation.MainScreenRouter
import org.hyperskill.app.android.next_learning_activity.view.delegate.NextLearningActivityDelegate
import org.hyperskill.app.android.problem_of_day.view.delegate.ProblemOfDayCardFormDelegate
import org.hyperskill.app.android.problems_limit.view.ui.delegate.ProblemsLimitDelegate
import org.hyperskill.app.android.stage_implementation.view.dialog.UnsupportedStageBottomSheet
import org.hyperskill.app.android.step.view.screen.StepScreen
import org.hyperskill.app.android.topics_repetitions.view.delegate.TopicsRepetitionCardFormDelegate
import org.hyperskill.app.android.topics_repetitions.view.screen.TopicsRepetitionScreen
Expand All @@ -39,7 +41,8 @@ import ru.nobird.app.presentation.redux.container.ReduxView

class HomeFragment :
Fragment(R.layout.fragment_home),
ReduxView<HomeFeature.State, HomeFeature.Action.ViewAction> {
ReduxView<HomeFeature.State, HomeFeature.Action.ViewAction>,
UnsupportedStageBottomSheet.Callback {
companion object {
fun newInstance(): Fragment =
HomeFragment()
Expand All @@ -66,6 +69,15 @@ class HomeFragment :
private val topicsRepetitionDelegate: TopicsRepetitionCardFormDelegate by lazy(LazyThreadSafetyMode.NONE) {
TopicsRepetitionCardFormDelegate()
}

private val nextLearningActivityDelegate: NextLearningActivityDelegate by lazy(LazyThreadSafetyMode.NONE) {
NextLearningActivityDelegate(requireContext()) { nextLearningActivityMessage ->
homeViewModel.onNewMessage(
HomeFeature.Message.NextLearningActivityWidgetMessage(nextLearningActivityMessage)
)
}
}

private var gamificationToolbarDelegate: GamificationToolbarDelegate? = null

private val onForegroundObserver =
Expand All @@ -91,6 +103,7 @@ class HomeFragment :
initGamificationToolbarDelegate()
initProblemsLimitDelegate()
problemOfDayCardFormDelegate.setup(viewBinding.homeScreenProblemOfDayCard)
nextLearningActivityDelegate.setup(requireContext(), viewBinding.homeNextLearningActivity)
with(viewBinding) {
homeScreenSwipeRefreshLayout.setHyperskillColors()
homeScreenSwipeRefreshLayout.setOnRefreshListener {
Expand Down Expand Up @@ -154,7 +167,7 @@ class HomeFragment :
viewBinding.homeScreenKeepPracticingTextView,
viewBinding.homeScreenProblemOfDayCard.root,
viewBinding.homeScreenTopicsRepetitionCard.root,
viewBinding.homeScreenKeepLearningInWebButton,
viewBinding.homeScreenKeepLearningInWebButton
)
}
}
Expand Down Expand Up @@ -198,9 +211,20 @@ class HomeFragment :
mainScreenRouter = mainScreenRouter,
router = requireRouter()
)
else -> {
is HomeFeature.Action.ViewAction.NavigateTo.StepScreen -> {
requireRouter().navigateTo(
StepScreen(action.stepRoute)
)
}
is HomeFeature.Action.ViewAction.ProblemsLimitViewAction -> {
// no op
}
is HomeFeature.Action.ViewAction.NextLearningActivityWidgetViewAction -> {
nextLearningActivityDelegate.handleAction(
fragment = this,
action = action.viewAction
)
}
}
}

Expand All @@ -220,6 +244,7 @@ class HomeFragment :
problemsLimitViewStateMapper?.let { mapper ->
problemsLimitDelegate?.render(mapper.mapState(state.problemsLimitState))
}
nextLearningActivityDelegate.render(state.nextLearningActivityWidgetState)
}

private fun renderSwipeRefresh(state: HomeFeature.State) {
Expand Down Expand Up @@ -269,4 +294,19 @@ class HomeFragment :
)
}
}

// UnsupportedStageBottomSheet.Callback methods
override fun onShow() {
homeViewModel.onNewMessage(HomeFeature.Message.StageImplementUnsupportedModalShownEventMessage)
}

override fun onDismiss() {
homeViewModel.onNewMessage(HomeFeature.Message.StageImplementUnsupportedModalHiddenEventMessage)
}

override fun onHomeClick() {
homeViewModel.onNewMessage(HomeFeature.Message.StageImplementUnsupportedModalGoToHomeClicked)
childFragmentManager
.dismissDialogFragmentIfExists(UnsupportedStageBottomSheet.TAG)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package org.hyperskill.app.android.next_learning_activity.view.delegate

import android.content.Context
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import org.hyperskill.app.android.R
import org.hyperskill.app.android.core.view.ui.adapter.DataLoadingErrorAdapterDelegate
import org.hyperskill.app.android.core.view.ui.adapter.decoration.HorizontalMarginItemDecoration
import org.hyperskill.app.android.databinding.LayoutNextLearningActivityBinding
import org.hyperskill.app.android.next_learning_activity.view.model.NextLearningActivityLoadingErrorRecyclerItem
import org.hyperskill.app.android.stage_implementation.view.dialog.UnsupportedStageBottomSheet
import org.hyperskill.app.android.study_plan.adapter.ActivityLoadingAdapterDelegate
import org.hyperskill.app.android.study_plan.adapter.StudyPlanActivityAdapterDelegate
import org.hyperskill.app.android.study_plan.delegate.LearningActivityTargetViewActionHandler
import org.hyperskill.app.android.study_plan.model.StudyPlanRecyclerItem
import org.hyperskill.app.next_learning_activity_widget.presentation.NextLearningActivityWidgetFeature
import org.hyperskill.app.next_learning_activity_widget.presentation.NextLearningActivityWidgetFeature.ViewState.Content
import org.hyperskill.app.next_learning_activity_widget.presentation.NextLearningActivityWidgetFeature.ViewState.Empty
import org.hyperskill.app.next_learning_activity_widget.presentation.NextLearningActivityWidgetFeature.ViewState.Idle
import org.hyperskill.app.next_learning_activity_widget.presentation.NextLearningActivityWidgetFeature.ViewState.Loading
import org.hyperskill.app.next_learning_activity_widget.presentation.NextLearningActivityWidgetFeature.ViewState.NetworkError
import org.hyperskill.app.next_learning_activity_widget.view.mapper.NextLearningActivityWidgetViewStateMapper
import ru.nobird.android.ui.adapters.DefaultDelegateAdapter

class NextLearningActivityDelegate(
context: Context,
onNewMessage: (NextLearningActivityWidgetFeature.Message) -> Unit
) {

private val nextLearningActivityAdapter by lazy(LazyThreadSafetyMode.NONE) {
DefaultDelegateAdapter<StudyPlanRecyclerItem>().apply {
addDelegate(
StudyPlanActivityAdapterDelegate {
onNewMessage(NextLearningActivityWidgetFeature.Message.NextLearningActivityClicked)
}
)
addDelegate(ActivityLoadingAdapterDelegate())
addDelegate(
DataLoadingErrorAdapterDelegate<StudyPlanRecyclerItem, NextLearningActivityLoadingErrorRecyclerItem> {
onNewMessage(NextLearningActivityWidgetFeature.Message.RetryContentLoading)
}
)
}
}

private val nextLearningActivityTitleTextColor: Int =
ContextCompat.getColor(context, StudyPlanRecyclerItem.Activity.activeTextColorRes)
private val nextLearningActivityEndIcon: Drawable? =
ContextCompat.getDrawable(context, StudyPlanRecyclerItem.Activity.nextActivityIconRes)

fun setup(context: Context, viewBinding: LayoutNextLearningActivityBinding) {
with(viewBinding) {
with(homeNextLearningActivityRecycler) {
adapter = nextLearningActivityAdapter
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
addItemDecoration(
HorizontalMarginItemDecoration(
horizontalMargin = context.resources.getDimensionPixelOffset(R.dimen.screen_horizontal_padding)
)
)
}
}
}

fun render(state: NextLearningActivityWidgetFeature.State) {
val viewState = NextLearningActivityWidgetViewStateMapper.map(state.contentState)
nextLearningActivityAdapter.items = when (viewState) {
Idle, Empty -> emptyList()
Loading -> listOf(StudyPlanRecyclerItem.ActivityLoading(0, 0))
is Content -> listOf(mapContentToRecyclerItem(viewState))
NetworkError -> listOf(NextLearningActivityLoadingErrorRecyclerItem)
}
}

private fun mapContentToRecyclerItem(content: Content): StudyPlanRecyclerItem.Activity =
StudyPlanRecyclerItem.Activity(
id = content.id,
title = content.title,
subtitle = content.subtitle,
titleTextColor = nextLearningActivityTitleTextColor,
progress = content.progress,
formattedProgress = content.formattedProgress,
endIcon = nextLearningActivityEndIcon,
isClickable = true,
isIdeRequired = content.isIdeRequired
)

fun <TFragment> handleAction(
fragment: TFragment,
action: NextLearningActivityWidgetFeature.Action.ViewAction
) where TFragment : Fragment, TFragment : UnsupportedStageBottomSheet.Callback {
when (action) {
is NextLearningActivityWidgetFeature.Action.ViewAction.NavigateTo.LearningActivityTarget ->
LearningActivityTargetViewActionHandler.handle(
fragment = fragment,
viewAction = action.viewAction
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.hyperskill.app.android.next_learning_activity.view.model

import org.hyperskill.app.android.study_plan.model.StudyPlanRecyclerItem

object NextLearningActivityLoadingErrorRecyclerItem : StudyPlanRecyclerItem
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.hyperskill.app.android.study_plan.adapter

import android.view.ViewGroup
import org.hyperskill.app.android.R
import org.hyperskill.app.android.study_plan.model.StudyPlanRecyclerItem
import ru.nobird.android.ui.adapterdelegates.AdapterDelegate
import ru.nobird.android.ui.adapterdelegates.DelegateViewHolder

class ActivityLoadingAdapterDelegate : AdapterDelegate<
StudyPlanRecyclerItem,
DelegateViewHolder<StudyPlanRecyclerItem>>() {
override fun isForViewType(position: Int, data: StudyPlanRecyclerItem): Boolean =
data is StudyPlanRecyclerItem.ActivityLoading

override fun onCreateViewHolder(parent: ViewGroup): DelegateViewHolder<StudyPlanRecyclerItem> =
object : DelegateViewHolder<StudyPlanRecyclerItem>(
createView(parent, R.layout.item_study_plan_activities_loading)
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,12 @@ import androidx.core.view.updateLayoutParams
import org.hyperskill.app.android.R
import org.hyperskill.app.android.databinding.ItemStudyPlanActivityBinding
import org.hyperskill.app.android.study_plan.model.StudyPlanRecyclerItem
import org.hyperskill.app.study_plan.widget.presentation.StudyPlanWidgetFeature
import ru.nobird.android.ui.adapterdelegates.AdapterDelegate
import ru.nobird.android.ui.adapterdelegates.DelegateViewHolder

class StudyPlanActivityAdapterDelegate(
private val onNewMessage: (StudyPlanWidgetFeature.Message) -> Unit
) :
AdapterDelegate<StudyPlanRecyclerItem, DelegateViewHolder<StudyPlanRecyclerItem>>() {
private val onActivityClicked: (Long) -> Unit
) : AdapterDelegate<StudyPlanRecyclerItem, DelegateViewHolder<StudyPlanRecyclerItem>>() {

override fun isForViewType(position: Int, data: StudyPlanRecyclerItem): Boolean =
data is StudyPlanRecyclerItem.Activity
Expand All @@ -31,7 +29,7 @@ class StudyPlanActivityAdapterDelegate(
itemView.setOnClickListener {
val activityId = (itemData as? StudyPlanRecyclerItem.Activity)?.id
if (activityId != null) {
onNewMessage(StudyPlanWidgetFeature.Message.ActivityClicked(activityId))
onActivityClicked(activityId)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.hyperskill.app.android.study_plan.delegate

import androidx.fragment.app.Fragment
import org.hyperskill.app.android.core.view.ui.navigation.requireRouter
import org.hyperskill.app.android.projects_selection.list.navigation.ProjectSelectionListScreen
import org.hyperskill.app.android.stage_implementation.view.dialog.UnsupportedStageBottomSheet
import org.hyperskill.app.android.stage_implementation.view.navigation.StageImplementationScreen
import org.hyperskill.app.android.step.view.screen.StepScreen
import org.hyperskill.app.android.track_selection.list.navigation.TrackSelectionListScreen
import org.hyperskill.app.learning_activities.presentation.model.LearningActivityTargetViewAction
import org.hyperskill.app.project_selection.list.injection.ProjectSelectionListParams
import org.hyperskill.app.track_selection.list.injection.TrackSelectionListParams
import ru.nobird.android.view.base.ui.extension.showIfNotExists

object LearningActivityTargetViewActionHandler {
fun <TFragment> handle(
fragment: TFragment,
viewAction: LearningActivityTargetViewAction
) where TFragment : Fragment, TFragment : UnsupportedStageBottomSheet.Callback {
when (viewAction) {
is LearningActivityTargetViewAction.NavigateTo.SelectProject -> {
fragment.requireRouter().navigateTo(
ProjectSelectionListScreen(
ProjectSelectionListParams(
trackId = viewAction.trackId,
isNewUserMode = false
)
)
)
}
LearningActivityTargetViewAction.NavigateTo.SelectTrack -> {
fragment.requireRouter().navigateTo(
TrackSelectionListScreen(
TrackSelectionListParams(isNewUserMode = false)
)
)
}
is LearningActivityTargetViewAction.NavigateTo.StageImplement -> {
fragment.requireRouter().navigateTo(
StageImplementationScreen(
projectId = viewAction.projectId,
stageId = viewAction.stageId
)
)
}
is LearningActivityTargetViewAction.NavigateTo.Step -> {
fragment.requireRouter().navigateTo(StepScreen(viewAction.stepRoute))
}
LearningActivityTargetViewAction.ShowStageImplementIDERequiredModal -> {
UnsupportedStageBottomSheet.newInstance()
.showIfNotExists(fragment.childFragmentManager, UnsupportedStageBottomSheet.TAG)
}
}
}
}
Loading

0 comments on commit cf05146

Please sign in to comment.