From a58b1069c2c3b33c8c1dc4ff5c3d17a86981dc8d Mon Sep 17 00:00:00 2001 From: Aleksandr Zhukov Date: Thu, 14 Dec 2023 10:23:43 +0100 Subject: [PATCH 01/11] Implement stub TopicSearchDialogFragment --- .../delegate/GamificationToolbarDelegate.kt | 25 +++--- .../home/view/ui/fragment/HomeFragment.kt | 3 +- .../fragment/LeaderboardFragment.kt | 3 +- .../study_plan/fragment/StudyPlanFragment.kt | 3 +- .../fragment/TopicSearchDialogFragment.kt | 83 +++++++++++++++++++ .../src/main/res/drawable/ic_menu_search.xml | 13 +++ .../main/res/layout/dialog_search_topic.xml | 55 ++++++++++++ .../layout/layout_gamification_toolbar.xml | 18 +++- 8 files changed, 188 insertions(+), 15 deletions(-) create mode 100644 androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt create mode 100644 androidHyperskillApp/src/main/res/drawable/ic_menu_search.xml create mode 100644 androidHyperskillApp/src/main/res/layout/dialog_search_topic.xml diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/gamification_toolbar/view/ui/delegate/GamificationToolbarDelegate.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/gamification_toolbar/view/ui/delegate/GamificationToolbarDelegate.kt index c4dc79a9f4..d981429a6c 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/gamification_toolbar/view/ui/delegate/GamificationToolbarDelegate.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/gamification_toolbar/view/ui/delegate/GamificationToolbarDelegate.kt @@ -3,6 +3,7 @@ package org.hyperskill.app.android.gamification_toolbar.view.ui.delegate import android.content.Context import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams +import androidx.fragment.app.FragmentManager import androidx.lifecycle.LifecycleOwner import com.github.terrakok.cicerone.Router import com.google.android.material.appbar.AppBarLayout @@ -12,31 +13,32 @@ import org.hyperskill.app.android.main.view.ui.navigation.MainScreenRouter import org.hyperskill.app.android.main.view.ui.navigation.Tabs import org.hyperskill.app.android.main.view.ui.navigation.switch import org.hyperskill.app.android.progress.navigation.ProgressScreen +import org.hyperskill.app.android.topic_search.fragment.TopicSearchDialogFragment import org.hyperskill.app.android.view.base.ui.extension.setElevationOnCollapsed import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarFeature +import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarFeature.Message import ru.nobird.android.view.base.ui.extension.setTextIfChanged +import ru.nobird.android.view.base.ui.extension.showIfNotExists class GamificationToolbarDelegate( lifecycleOwner: LifecycleOwner, private val context: Context, private val viewBinding: LayoutGamificationToolbarBinding, - onNewMessage: (GamificationToolbarFeature.Message) -> Unit + onNewMessage: (Message) -> Unit ) { init { with(viewBinding) { gamificationAppBar.setElevationOnCollapsed(lifecycleOwner.lifecycle) gamificationAppBar.setExpanded(true) - gamificationStreakDurationTextView.setOnClickListener { - onNewMessage( - GamificationToolbarFeature.Message.ClickedStreak - ) + onNewMessage(Message.ClickedStreak) } gamificationTrackProgressLinearLayout.setOnClickListener { - onNewMessage( - GamificationToolbarFeature.Message.ClickedProgress - ) + onNewMessage(Message.ClickedProgress) + } + gamificationSearchButton.setOnClickListener { + onNewMessage(Message.ClickedSearch) } } } @@ -68,13 +70,15 @@ class GamificationToolbarDelegate( viewBinding.gamificationTrackProgressTextView.text = progress.formattedValue } } + viewBinding.gamificationSearchButton.isVisible = true } } fun onAction( action: GamificationToolbarFeature.Action.ViewAction, mainScreenRouter: MainScreenRouter, - router: Router + router: Router, + fragmentManager: FragmentManager ) { when (action) { is GamificationToolbarFeature.Action.ViewAction.ShowProfileTab -> @@ -82,7 +86,8 @@ class GamificationToolbarDelegate( GamificationToolbarFeature.Action.ViewAction.ShowProgressScreen -> router.navigateTo(ProgressScreen) GamificationToolbarFeature.Action.ViewAction.ShowSearchScreen -> { - // TODO: ALTAPPS-1059 Show search screen + TopicSearchDialogFragment.newInstance() + .showIfNotExists(fragmentManager, TopicSearchDialogFragment.TAG) } } } diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/home/view/ui/fragment/HomeFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/home/view/ui/fragment/HomeFragment.kt index 36accbbd9e..e52bf3c429 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/home/view/ui/fragment/HomeFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/home/view/ui/fragment/HomeFragment.kt @@ -174,7 +174,8 @@ class HomeFragment : gamificationToolbarDelegate?.onAction( action = action.viewAction, mainScreenRouter = mainScreenRouter, - router = requireRouter() + router = requireRouter(), + fragmentManager = childFragmentManager ) is HomeFeature.Action.ViewAction.NavigateTo.StepScreen -> { requireRouter().navigateTo( diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/leaderboard/fragment/LeaderboardFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/leaderboard/fragment/LeaderboardFragment.kt index eed0b67d6a..68f19c034b 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/leaderboard/fragment/LeaderboardFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/leaderboard/fragment/LeaderboardFragment.kt @@ -118,7 +118,8 @@ class LeaderboardFragment : Fragment(R.layout.fragment_leaderboard) { gamificationToolbarDelegate?.onAction( action = action.viewAction, mainScreenRouter = mainScreenRouter, - router = requireRouter() + router = requireRouter(), + fragmentManager = childFragmentManager ) } is LeaderboardScreenFeature.Action.ViewAction.LeaderboardWidgetViewAction -> diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/study_plan/fragment/StudyPlanFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/study_plan/fragment/StudyPlanFragment.kt index 76380f63e8..5542f36747 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/study_plan/fragment/StudyPlanFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/study_plan/fragment/StudyPlanFragment.kt @@ -153,7 +153,8 @@ class StudyPlanFragment : gamificationToolbarDelegate?.onAction( action = action.viewAction, mainScreenRouter = mainScreenRouter, - router = requireRouter() + router = requireRouter(), + fragmentManager = childFragmentManager ) } is StudyPlanScreenFeature.Action.ViewAction.ProblemsLimitViewAction -> {} diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt new file mode 100644 index 0000000000..860500e1b2 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt @@ -0,0 +1,83 @@ +package org.hyperskill.app.android.topic_search.fragment + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.DialogFragment +import by.kirich1409.viewbindingdelegate.viewBinding +import org.hyperskill.app.android.R +import org.hyperskill.app.android.databinding.DialogSearchTopicBinding +import org.hyperskill.app.android.view.base.ui.extension.wrapWithTheme + +class TopicSearchDialogFragment : DialogFragment() { + + companion object { + const val TAG = "TopicSearchDialogFragment" + + fun newInstance(): TopicSearchDialogFragment = + TopicSearchDialogFragment() + } + + + private val viewBinding: DialogSearchTopicBinding by viewBinding(DialogSearchTopicBinding::bind) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.ThemeOverlay_AppTheme_Dialog_Fullscreen) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = + super.onCreateDialog(savedInstanceState).apply { + setCanceledOnTouchOutside(false) + setCancelable(false) + } + + override fun onStart() { + super.onStart() + dialog?.window?.let { window -> + window.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + window.setWindowAnimations(R.style.ThemeOverlay_AppTheme_Dialog_Fullscreen) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = + inflater.wrapWithTheme(requireActivity()) + .inflate(R.layout.dialog_search_topic, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + with(viewBinding) { + topicSearchToolbar.setNavigationOnClickListener { dismiss() } + topicSearchClearButton.isVisible = topicSearchEditText.text.isNotEmpty() + topicSearchEditText.doOnTextChanged { text, _, _, _ -> + topicSearchClearButton.isVisible = !text.isNullOrEmpty() + // TODO: notify search request changed + } + topicSearchEditText.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + // TODO: make search request + } + false + } + topicSearchEditText.post { + topicSearchEditText.requestFocus() + val inputMethodManager = + requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.showSoftInput(topicSearchEditText, InputMethodManager.SHOW_IMPLICIT) + } + } + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/res/drawable/ic_menu_search.xml b/androidHyperskillApp/src/main/res/drawable/ic_menu_search.xml new file mode 100644 index 0000000000..fd5ebb824b --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_menu_search.xml @@ -0,0 +1,13 @@ + + + + diff --git a/androidHyperskillApp/src/main/res/layout/dialog_search_topic.xml b/androidHyperskillApp/src/main/res/layout/dialog_search_topic.xml new file mode 100644 index 0000000000..f9361c85be --- /dev/null +++ b/androidHyperskillApp/src/main/res/layout/dialog_search_topic.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/androidHyperskillApp/src/main/res/layout/layout_gamification_toolbar.xml b/androidHyperskillApp/src/main/res/layout/layout_gamification_toolbar.xml index 829a27af35..bcd674cbc3 100644 --- a/androidHyperskillApp/src/main/res/layout/layout_gamification_toolbar.xml +++ b/androidHyperskillApp/src/main/res/layout/layout_gamification_toolbar.xml @@ -47,7 +47,8 @@ android:focusable="true" android:background="?attr/selectableItemBackgroundBorderless" android:visibility="gone" - tools:visibility="visible"> + tools:visibility="visible" + android:layout_marginEnd="8dp"> + + Date: Thu, 14 Dec 2023 10:43:29 +0100 Subject: [PATCH 02/11] Setup viewModel --- .../fragment/TopicSearchDialogFragment.kt | 66 +++++++++++++++---- .../core/injection/CommonAndroidAppGraph.kt | 3 + .../injection/CommonAndroidAppGraphImpl.kt | 7 ++ .../injection/PlatformSearchComponent.kt | 7 ++ .../injection/PlatformSearchComponentImpl.kt | 20 ++++++ .../search/presentation/SearchViewModel.kt | 11 ++++ 6 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponent.kt create mode 100644 shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponentImpl.kt create mode 100644 shared/src/androidMain/kotlin/org/hyperskill/app/search/presentation/SearchViewModel.kt diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt index 860500e1b2..95edbdc56b 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt @@ -9,14 +9,21 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import androidx.core.view.isVisible -import androidx.core.widget.doOnTextChanged +import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.DialogFragment +import androidx.lifecycle.ViewModelProvider import by.kirich1409.viewbindingdelegate.viewBinding +import org.hyperskill.app.android.HyperskillApp import org.hyperskill.app.android.R import org.hyperskill.app.android.databinding.DialogSearchTopicBinding import org.hyperskill.app.android.view.base.ui.extension.wrapWithTheme +import org.hyperskill.app.search.presentation.SearchFeature +import org.hyperskill.app.search.presentation.SearchViewModel +import ru.nobird.android.view.base.ui.extension.setTextIfChanged +import ru.nobird.android.view.redux.ui.extension.reduxViewModel +import ru.nobird.app.presentation.redux.container.ReduxView -class TopicSearchDialogFragment : DialogFragment() { +class TopicSearchDialogFragment : DialogFragment(), ReduxView { companion object { const val TAG = "TopicSearchDialogFragment" @@ -25,12 +32,20 @@ class TopicSearchDialogFragment : DialogFragment() { TopicSearchDialogFragment() } + private var viewModelFactory: ViewModelProvider.Factory? = null + private val searchViewModel: SearchViewModel by reduxViewModel(this) { requireNotNull(viewModelFactory) } private val viewBinding: DialogSearchTopicBinding by viewBinding(DialogSearchTopicBinding::bind) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setStyle(STYLE_NORMAL, R.style.ThemeOverlay_AppTheme_Dialog_Fullscreen) + injectComponent() + } + + private fun injectComponent() { + viewModelFactory = + HyperskillApp.graph().buildPlatformSearchComponent().reduxViewModelFactory } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = @@ -39,6 +54,14 @@ class TopicSearchDialogFragment : DialogFragment() { setCancelable(false) } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = + inflater.wrapWithTheme(requireActivity()) + .inflate(R.layout.dialog_search_topic, container, false) + override fun onStart() { super.onStart() dialog?.window?.let { window -> @@ -50,25 +73,26 @@ class TopicSearchDialogFragment : DialogFragment() { } } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? = - inflater.wrapWithTheme(requireActivity()) - .inflate(R.layout.dialog_search_topic, container, false) + override fun onResume() { + super.onResume() + searchViewModel.onNewMessage( + SearchFeature.Message.ViewedEventMessage + ) + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { with(viewBinding) { topicSearchToolbar.setNavigationOnClickListener { dismiss() } topicSearchClearButton.isVisible = topicSearchEditText.text.isNotEmpty() - topicSearchEditText.doOnTextChanged { text, _, _, _ -> + topicSearchEditText.doAfterTextChanged { text -> topicSearchClearButton.isVisible = !text.isNullOrEmpty() - // TODO: notify search request changed + searchViewModel.onNewMessage( + SearchFeature.Message.QueryChanged(text.toString()) + ) } topicSearchEditText.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_SEARCH) { - // TODO: make search request + SearchFeature.Message.SearchClicked } false } @@ -80,4 +104,22 @@ class TopicSearchDialogFragment : DialogFragment() { } } } + + override fun render(state: SearchFeature.ViewState) { + viewBinding.topicSearchEditText.setTextIfChanged(state.query) + when (state.searchResultsViewState) { + SearchFeature.SearchResultsViewState.Empty -> TODO() + SearchFeature.SearchResultsViewState.Idle -> TODO() + SearchFeature.SearchResultsViewState.Loading -> TODO() + SearchFeature.SearchResultsViewState.Error -> TODO() + is SearchFeature.SearchResultsViewState.Content -> TODO() + } + } + + override fun onAction(action: SearchFeature.Action.ViewAction) { + when (action) { + is SearchFeature.Action.ViewAction.OpenStepScreen -> TODO() + is SearchFeature.Action.ViewAction.OpenStepScreenFailed -> TODO() + } + } } \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraph.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraph.kt index f51752d628..0b58a0e4ea 100644 --- a/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraph.kt +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraph.kt @@ -26,6 +26,7 @@ import org.hyperskill.app.project_selection.details.injection.PlatformProjectSel import org.hyperskill.app.project_selection.details.injection.ProjectSelectionDetailsParams import org.hyperskill.app.project_selection.list.injection.PlatformProjectSelectionListComponent import org.hyperskill.app.project_selection.list.injection.ProjectSelectionListParams +import org.hyperskill.app.search.injection.PlatformSearchComponent import org.hyperskill.app.stage_implementation.injection.PlatformStageImplementationComponent import org.hyperskill.app.step.injection.PlatformStepComponent import org.hyperskill.app.step.injection.StepComponent @@ -96,4 +97,6 @@ interface CommonAndroidAppGraph : AppGraph { fun buildPlatformFirstProblemOnboardingComponent(isNewUserMode: Boolean): PlatformFirstProblemOnboardingComponent fun buildPlatformLeaderboardComponent(): PlatformLeaderboardComponent + + fun buildPlatformSearchComponent(): PlatformSearchComponent } \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraphImpl.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraphImpl.kt index 2d81a3dcc8..a407afc8ce 100644 --- a/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraphImpl.kt +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraphImpl.kt @@ -41,6 +41,8 @@ import org.hyperskill.app.project_selection.details.injection.ProjectSelectionDe import org.hyperskill.app.project_selection.list.injection.PlatformProjectSelectionListComponent import org.hyperskill.app.project_selection.list.injection.PlatformProjectSelectionListComponentImpl import org.hyperskill.app.project_selection.list.injection.ProjectSelectionListParams +import org.hyperskill.app.search.injection.PlatformSearchComponent +import org.hyperskill.app.search.injection.PlatformSearchComponentImpl import org.hyperskill.app.stage_implementation.injection.PlatformStageImplementationComponent import org.hyperskill.app.stage_implementation.injection.PlatformStageImplementationComponentImpl import org.hyperskill.app.step.injection.PlatformStepComponent @@ -217,4 +219,9 @@ abstract class CommonAndroidAppGraphImpl : CommonAndroidAppGraph, BaseAppGraph() PlatformLeaderboardComponentImpl( leaderboardComponent = buildLeaderboardScreenComponent() ) + + override fun buildPlatformSearchComponent(): PlatformSearchComponent = + PlatformSearchComponentImpl( + searchComponent = buildSearchComponent() + ) } \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponent.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponent.kt new file mode 100644 index 0000000000..e23ceb37af --- /dev/null +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponent.kt @@ -0,0 +1,7 @@ +package org.hyperskill.app.search.injection + +import org.hyperskill.app.core.injection.ReduxViewModelFactory + +interface PlatformSearchComponent { + val reduxViewModelFactory: ReduxViewModelFactory +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponentImpl.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponentImpl.kt new file mode 100644 index 0000000000..3fe92d3f05 --- /dev/null +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponentImpl.kt @@ -0,0 +1,20 @@ +package org.hyperskill.app.search.injection + +import org.hyperskill.app.core.injection.ReduxViewModelFactory +import org.hyperskill.app.search.presentation.SearchViewModel +import ru.nobird.app.presentation.redux.container.wrapWithViewContainer + +class PlatformSearchComponentImpl( + private val searchComponent: SearchComponent +) : PlatformSearchComponent { + override val reduxViewModelFactory: ReduxViewModelFactory + get() = ReduxViewModelFactory( + mapOf( + SearchViewModel::class.java to { + SearchViewModel( + searchComponent.searchFeature.wrapWithViewContainer() + ) + } + ) + ) +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/search/presentation/SearchViewModel.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/search/presentation/SearchViewModel.kt new file mode 100644 index 0000000000..8d8359ad4e --- /dev/null +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/search/presentation/SearchViewModel.kt @@ -0,0 +1,11 @@ +package org.hyperskill.app.search.presentation + +import org.hyperskill.app.search.presentation.SearchFeature.Action.ViewAction +import org.hyperskill.app.search.presentation.SearchFeature.Message +import org.hyperskill.app.search.presentation.SearchFeature.ViewState +import ru.nobird.android.view.redux.viewmodel.ReduxViewModel +import ru.nobird.app.presentation.redux.container.ReduxViewContainer + +class SearchViewModel( + viewContainer: ReduxViewContainer +) : ReduxViewModel(viewContainer) \ No newline at end of file From c6cee67e3e0b9ab1e696049fc31b73bac902bfa0 Mon Sep 17 00:00:00 2001 From: Aleksandr Zhukov Date: Thu, 14 Dec 2023 12:45:04 +0100 Subject: [PATCH 03/11] Implement loading,result & error ui --- .../fragment/TopicSearchDialogFragment.kt | 70 ++++++++--- .../topic_search/ui/TopicSearchResult.kt | 73 +++++++++++ .../ui/TopicSearchResultContent.kt | 115 ++++++++++++++++++ .../ui/TopicSearchResultDefaults.kt | 9 ++ .../topic_search/ui/TopicSearchSkeleton.kt | 46 +++++++ .../main/res/layout/dialog_search_topic.xml | 15 ++- .../app/core/flowredux/view/HandleActions.kt | 4 +- .../injection/PlatformSearchComponentImpl.kt | 3 +- .../search/presentation/SearchViewModel.kt | 8 +- 9 files changed, 316 insertions(+), 27 deletions(-) create mode 100644 androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResult.kt create mode 100644 androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultContent.kt create mode 100644 androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultDefaults.kt create mode 100644 androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchSkeleton.kt diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt index 95edbdc56b..edee05024d 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt @@ -8,22 +8,34 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager +import android.widget.Toast +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope import by.kirich1409.viewbindingdelegate.viewBinding +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import org.hyperskill.app.android.HyperskillApp import org.hyperskill.app.android.R +import org.hyperskill.app.android.core.view.ui.navigation.requireRouter +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme import org.hyperskill.app.android.databinding.DialogSearchTopicBinding +import org.hyperskill.app.android.step.view.screen.StepScreen +import org.hyperskill.app.android.topic_search.ui.TopicSearchResult import org.hyperskill.app.android.view.base.ui.extension.wrapWithTheme +import org.hyperskill.app.core.view.handleActions import org.hyperskill.app.search.presentation.SearchFeature import org.hyperskill.app.search.presentation.SearchViewModel import ru.nobird.android.view.base.ui.extension.setTextIfChanged -import ru.nobird.android.view.redux.ui.extension.reduxViewModel -import ru.nobird.app.presentation.redux.container.ReduxView -class TopicSearchDialogFragment : DialogFragment(), ReduxView { +class TopicSearchDialogFragment : DialogFragment() { companion object { const val TAG = "TopicSearchDialogFragment" @@ -33,7 +45,7 @@ class TopicSearchDialogFragment : DialogFragment(), ReduxView topicSearchClearButton.isVisible = !text.isNullOrEmpty() searchViewModel.onNewMessage( @@ -103,23 +132,32 @@ class TopicSearchDialogFragment : DialogFragment(), ReduxView TODO() - SearchFeature.SearchResultsViewState.Idle -> TODO() - SearchFeature.SearchResultsViewState.Loading -> TODO() - SearchFeature.SearchResultsViewState.Error -> TODO() - is SearchFeature.SearchResultsViewState.Content -> TODO() - } + private fun setupEditTextRendering() { + searchViewModel.state + .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) + .map { it.query } + .onEach { query -> + viewBinding.topicSearchEditText.setTextIfChanged(query) + } + .launchIn(viewLifecycleOwner.lifecycleScope) } - override fun onAction(action: SearchFeature.Action.ViewAction) { + private fun handleAction(action: SearchFeature.Action.ViewAction) { when (action) { - is SearchFeature.Action.ViewAction.OpenStepScreen -> TODO() - is SearchFeature.Action.ViewAction.OpenStepScreenFailed -> TODO() + is SearchFeature.Action.ViewAction.OpenStepScreen -> { + requireRouter().navigateTo(StepScreen(action.stepRoute)) + dismiss() + } + is SearchFeature.Action.ViewAction.OpenStepScreenFailed -> { + Toast.makeText( + requireContext(), + org.hyperskill.app.R.string.search_open_step_screen_error_message, + Toast.LENGTH_SHORT + ).show() + } } } } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResult.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResult.kt new file mode 100644 index 0000000000..79d76d4e98 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResult.kt @@ -0,0 +1,73 @@ +package org.hyperskill.app.android.topic_search.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.flow.map +import org.hyperskill.app.R +import org.hyperskill.app.android.core.view.ui.widget.compose.ScreenDataLoadingError +import org.hyperskill.app.search.presentation.SearchFeature.Message +import org.hyperskill.app.search.presentation.SearchFeature.SearchResultsViewState +import org.hyperskill.app.search.presentation.SearchViewModel + +@Composable +fun TopicSearchResult(viewModel: SearchViewModel) { + val viewState: SearchResultsViewState by viewModel + .state + .map { it.searchResultsViewState } + .collectAsStateWithLifecycle( + initialValue = SearchResultsViewState.Idle, + lifecycle = LocalLifecycleOwner.current.lifecycle + ) + TopicSearchResult( + viewState = viewState, + onNewMessage = viewModel::onNewMessage + ) +} + +@Composable +fun TopicSearchResult( + viewState: SearchResultsViewState, + onNewMessage: (Message) -> Unit, + modifier: Modifier = Modifier +) { + Box(modifier = modifier.fillMaxSize()) { + when (viewState) { + SearchResultsViewState.Idle -> { + // no op + } + SearchResultsViewState.Empty -> { + // TODO: add empty state + } + SearchResultsViewState.Loading -> { + TopicSearchSkeleton(modifier = Modifier.fillMaxSize()) + } + SearchResultsViewState.Error -> { + ScreenDataLoadingError( + errorMessage = stringResource(id = R.string.search_placeholder_error_description) + ) { + onNewMessage(Message.RetrySearchClicked) + } + } + is SearchResultsViewState.Content -> { + val onItemClick = remember { + { id: Long -> + onNewMessage( + Message.SearchResultsItemClicked(id) + ) + } + } + TopicSearchResultContent( + items = viewState.searchResults, + onItemClick = onItemClick + ) + } + } + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultContent.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultContent.kt new file mode 100644 index 0000000000..8820435cc2 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultContent.kt @@ -0,0 +1,115 @@ +package org.hyperskill.app.android.topic_search.ui + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +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.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.colorResource +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.search.presentation.SearchFeature.SearchResultsViewState.Content.Item + +@Composable +fun TopicSearchResultContent( + items: List, + onItemClick: (Long) -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn( + content = { + itemsIndexed( + items = items, + key = { _, item -> item.id } + ) { index, item -> + TopicSearchResultItem( + title = item.title, + onClick = { + onItemClick(item.id) + }, + modifier = Modifier + .fillParentMaxWidth() + .padding( + top = TopicSearchResultDefaults.resultItemsVerticalSpacing, + bottom = if (index == items.lastIndex) { + TopicSearchResultDefaults.resultItemsVerticalSpacing + } else { + 0.dp + }, + start = TopicSearchResultDefaults.horizontalPadding, + end = TopicSearchResultDefaults.horizontalPadding + ) + ) + } + }, + modifier = modifier + ) +} + +@Composable +fun TopicSearchResultItem( + title: String, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val currentOnClick by rememberUpdatedState(newValue = onClick) + val cornerRadius = dimensionResource(id = R.dimen.corner_radius) + Box( + modifier + .clip(RoundedCornerShape(cornerRadius)) + .border( + width = 1.dp, + color = colorResource(id = org.hyperskill.app.R.color.color_on_surface_alpha_12), + shape = RoundedCornerShape(cornerRadius) + ) + .clickable(onClick = currentOnClick) + .padding(16.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.body2, + color = colorResource(id = org.hyperskill.app.R.color.color_on_surface_alpha_60) + ) + } +} + +@Preview +@Composable +private fun TopicSearchResultItemPreview() { + HyperskillTheme { + TopicSearchResultItem( + title = "Basic data types", + onClick = { + + } + ) + } +} + +@Preview +@Composable +private fun TopicSearchResultContentPreview() { + HyperskillTheme { + TopicSearchResultContent( + items = List(3) { + Item( + id = it.toLong(), + title = "Basic data types" + ) + }, + onItemClick = {} + ) + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultDefaults.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultDefaults.kt new file mode 100644 index 0000000000..c81617ed3d --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultDefaults.kt @@ -0,0 +1,9 @@ +package org.hyperskill.app.android.topic_search.ui + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +object TopicSearchResultDefaults { + val horizontalPadding: Dp = 20.dp + val resultItemsVerticalSpacing: Dp = 10.dp +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchSkeleton.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchSkeleton.kt new file mode 100644 index 0000000000..0977b9d749 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchSkeleton.kt @@ -0,0 +1,46 @@ +package org.hyperskill.app.android.topic_search.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme +import org.hyperskill.app.android.core.view.ui.widget.compose.ShimmerLoading + +@Composable +fun TopicSearchSkeleton( + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .padding(top = TopicSearchResultDefaults.resultItemsVerticalSpacing), + verticalArrangement = Arrangement.spacedBy(TopicSearchResultDefaults.resultItemsVerticalSpacing) + ) { + repeat(5) { + TopicSearchSkeletonItem( + modifier = Modifier.padding(horizontal = TopicSearchResultDefaults.horizontalPadding) + ) + } + } +} + +@Composable +fun TopicSearchSkeletonItem( + modifier: Modifier = Modifier +) { + ShimmerLoading( + modifier = modifier.requiredHeight(50.dp) + ) +} + +@Preview +@Composable +fun TopicSearchSkeletonPreview() { + HyperskillTheme { + TopicSearchSkeleton() + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/res/layout/dialog_search_topic.xml b/androidHyperskillApp/src/main/res/layout/dialog_search_topic.xml index f9361c85be..a71fa86384 100644 --- a/androidHyperskillApp/src/main/res/layout/dialog_search_topic.xml +++ b/androidHyperskillApp/src/main/res/layout/dialog_search_topic.xml @@ -1,10 +1,11 @@ - + android:orientation="vertical"> - \ No newline at end of file + + + \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/core/flowredux/view/HandleActions.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/core/flowredux/view/HandleActions.kt index 6d2762e3d2..339238edc3 100644 --- a/shared/src/androidMain/kotlin/org/hyperskill/app/core/flowredux/view/HandleActions.kt +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/core/flowredux/view/HandleActions.kt @@ -10,8 +10,8 @@ import org.hyperskill.app.core.flowredux.presentation.ReduxFlowViewModel fun ReduxFlowViewModel.handleActions( lifecycleOwner: LifecycleOwner, - minActiveState: Lifecycle.State = Lifecycle.State.STARTED, - onAction: (ViewAction) -> Unit + onAction: (ViewAction) -> Unit, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED ) { actions .flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState) diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponentImpl.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponentImpl.kt index 3fe92d3f05..fb18b0594e 100644 --- a/shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponentImpl.kt +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponentImpl.kt @@ -1,5 +1,6 @@ package org.hyperskill.app.search.injection +import org.hyperskill.app.core.flowredux.presentation.wrapWithFlowView import org.hyperskill.app.core.injection.ReduxViewModelFactory import org.hyperskill.app.search.presentation.SearchViewModel import ru.nobird.app.presentation.redux.container.wrapWithViewContainer @@ -12,7 +13,7 @@ class PlatformSearchComponentImpl( mapOf( SearchViewModel::class.java to { SearchViewModel( - searchComponent.searchFeature.wrapWithViewContainer() + searchComponent.searchFeature.wrapWithFlowView() ) } ) diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/search/presentation/SearchViewModel.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/search/presentation/SearchViewModel.kt index 8d8359ad4e..78eea8bb52 100644 --- a/shared/src/androidMain/kotlin/org/hyperskill/app/search/presentation/SearchViewModel.kt +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/search/presentation/SearchViewModel.kt @@ -1,11 +1,11 @@ package org.hyperskill.app.search.presentation +import org.hyperskill.app.core.flowredux.presentation.FlowView +import org.hyperskill.app.core.flowredux.presentation.ReduxFlowViewModel import org.hyperskill.app.search.presentation.SearchFeature.Action.ViewAction import org.hyperskill.app.search.presentation.SearchFeature.Message import org.hyperskill.app.search.presentation.SearchFeature.ViewState -import ru.nobird.android.view.redux.viewmodel.ReduxViewModel -import ru.nobird.app.presentation.redux.container.ReduxViewContainer class SearchViewModel( - viewContainer: ReduxViewContainer -) : ReduxViewModel(viewContainer) \ No newline at end of file + viewContainer: FlowView +) : ReduxFlowViewModel(viewContainer) \ No newline at end of file From 49090de7c7ddf8d8d093db6ee841c04ca822ff88 Mon Sep 17 00:00:00 2001 From: Aleksandr Zhukov Date: Thu, 14 Dec 2023 14:31:41 +0100 Subject: [PATCH 04/11] Implement empty state --- .../topic_search/ui/TopicSearchEmptyResult.kt | 49 +++++++++++++++++++ .../topic_search/ui/TopicSearchResult.kt | 10 ++-- .../ui/TopicSearchResultContent.kt | 7 ++- 3 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchEmptyResult.kt diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchEmptyResult.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchEmptyResult.kt new file mode 100644 index 0000000000..aebaf768bd --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchEmptyResult.kt @@ -0,0 +1,49 @@ +package org.hyperskill.app.android.topic_search.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.hyperskill.app.R +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme + +@Composable +fun TopicSearchEmptyResult( + modifier: Modifier = Modifier +) { + Box(modifier) { + Column( + modifier = Modifier.align(Alignment.Center), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = stringResource(id = R.string.search_placeholder_empty_title), + style = MaterialTheme.typography.h6, + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + Text( + text = stringResource(id = R.string.search_placeholder_empty_subtitle), + style = MaterialTheme.typography.body1, + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + } +} + +@Preview +@Composable +private fun TopicSearchEmptyResultPreview() { + HyperskillTheme { + TopicSearchEmptyResult() + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResult.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResult.kt index 79d76d4e98..06c8c2e12a 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResult.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResult.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource @@ -43,7 +42,9 @@ fun TopicSearchResult( // no op } SearchResultsViewState.Empty -> { - // TODO: add empty state + TopicSearchEmptyResult( + modifier = Modifier.fillMaxSize() + ) } SearchResultsViewState.Loading -> { TopicSearchSkeleton(modifier = Modifier.fillMaxSize()) @@ -56,7 +57,7 @@ fun TopicSearchResult( } } is SearchResultsViewState.Content -> { - val onItemClick = remember { + /*val onItemClick = remember { { id: Long -> onNewMessage( Message.SearchResultsItemClicked(id) @@ -66,6 +67,9 @@ fun TopicSearchResult( TopicSearchResultContent( items = viewState.searchResults, onItemClick = onItemClick + )*/ + TopicSearchEmptyResult( + modifier = Modifier.fillMaxSize() ) } } diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultContent.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultContent.kt index 8820435cc2..70fae46528 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultContent.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultContent.kt @@ -11,6 +11,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -36,8 +37,10 @@ fun TopicSearchResultContent( ) { index, item -> TopicSearchResultItem( title = item.title, - onClick = { - onItemClick(item.id) + onClick = remember { + { + onItemClick(item.id) + } }, modifier = Modifier .fillParentMaxWidth() From e847638e0a5616e683a788096f88f49a052db1eb Mon Sep 17 00:00:00 2001 From: Aleksandr Zhukov Date: Thu, 14 Dec 2023 14:45:51 +0100 Subject: [PATCH 05/11] Fix preview mode --- .../app/android/topic_search/ui/TopicSearchResult.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResult.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResult.kt index 06c8c2e12a..68d617e4a1 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResult.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResult.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource @@ -57,7 +58,7 @@ fun TopicSearchResult( } } is SearchResultsViewState.Content -> { - /*val onItemClick = remember { + val onItemClick = remember { { id: Long -> onNewMessage( Message.SearchResultsItemClicked(id) @@ -67,9 +68,6 @@ fun TopicSearchResult( TopicSearchResultContent( items = viewState.searchResults, onItemClick = onItemClick - )*/ - TopicSearchEmptyResult( - modifier = Modifier.fillMaxSize() ) } } From 2ce97312bb9a99c18c7e4220295ecac9bf484ef3 Mon Sep 17 00:00:00 2001 From: Aleksandr Zhukov Date: Thu, 14 Dec 2023 16:31:31 +0100 Subject: [PATCH 06/11] Fix keyboard showing --- .../fragment/TopicSearchDialogFragment.kt | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt index edee05024d..c60ab05e51 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt @@ -8,6 +8,7 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager +import android.widget.EditText import android.widget.Toast import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.view.isVisible @@ -38,6 +39,7 @@ import ru.nobird.android.view.base.ui.extension.setTextIfChanged class TopicSearchDialogFragment : DialogFragment() { companion object { + private const val TALKBACK_FOCUS_CHANGE_DELAY_MS: Long = 100 const val TAG = "TopicSearchDialogFragment" fun newInstance(): TopicSearchDialogFragment = @@ -125,16 +127,26 @@ class TopicSearchDialogFragment : DialogFragment() { } false } - topicSearchEditText.post { - topicSearchEditText.requestFocus() - val inputMethodManager = - requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.showSoftInput(topicSearchEditText, InputMethodManager.SHOW_IMPLICIT) - } + requestFocusAndShowKeyboard(topicSearchEditText) } setupEditTextRendering() } + fun requestFocusAndShowKeyboard( + editText: EditText, + ) { + // Without a delay requesting focus on edit text fails when talkback is active. + editText.postDelayed( + { + editText.requestFocus() + val inputMethodManager = + requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT) + }, + TALKBACK_FOCUS_CHANGE_DELAY_MS + ) + } + private fun setupEditTextRendering() { searchViewModel.state .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) From 2a6214031ec7c2bac394112807d26c67e9002041 Mon Sep 17 00:00:00 2001 From: Aleksandr Zhukov Date: Thu, 14 Dec 2023 16:32:38 +0100 Subject: [PATCH 07/11] Fix ktlint --- .../topic_search/fragment/TopicSearchDialogFragment.kt | 1 - .../app/android/topic_search/ui/TopicSearchResultContent.kt | 4 +--- .../app/search/injection/PlatformSearchComponentImpl.kt | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt index c60ab05e51..dedf37ccff 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt @@ -105,7 +105,6 @@ class TopicSearchDialogFragment : DialogFragment() { } } } - } private fun setupToolbar() { diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultContent.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultContent.kt index 70fae46528..e2a6d5a5d0 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultContent.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/ui/TopicSearchResultContent.kt @@ -94,9 +94,7 @@ private fun TopicSearchResultItemPreview() { HyperskillTheme { TopicSearchResultItem( title = "Basic data types", - onClick = { - - } + onClick = {} ) } } diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponentImpl.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponentImpl.kt index fb18b0594e..7988915ef0 100644 --- a/shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponentImpl.kt +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/search/injection/PlatformSearchComponentImpl.kt @@ -3,7 +3,6 @@ package org.hyperskill.app.search.injection import org.hyperskill.app.core.flowredux.presentation.wrapWithFlowView import org.hyperskill.app.core.injection.ReduxViewModelFactory import org.hyperskill.app.search.presentation.SearchViewModel -import ru.nobird.app.presentation.redux.container.wrapWithViewContainer class PlatformSearchComponentImpl( private val searchComponent: SearchComponent From 3257a73db51e4117043df3cc043b1fcd9be316cf Mon Sep 17 00:00:00 2001 From: Aleksandr Zhukov Date: Fri, 15 Dec 2023 12:51:04 +0100 Subject: [PATCH 08/11] Make TopicSearchFragment not a dialog to avoid animation problems during navigation back --- .../delegate/GamificationToolbarDelegate.kt | 10 ++-- .../home/view/ui/fragment/HomeFragment.kt | 3 +- .../fragment/LeaderboardFragment.kt | 3 +- .../study_plan/fragment/StudyPlanFragment.kt | 3 +- ...alogFragment.kt => TopicSearchFragment.kt} | 47 +++---------------- .../navigation/TopicSearchScreen.kt | 11 +++++ ...ch_topic.xml => fragment_search_topic.xml} | 0 .../src/main/res/values/themes.xml | 5 +- 8 files changed, 27 insertions(+), 55 deletions(-) rename androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/{TopicSearchDialogFragment.kt => TopicSearchFragment.kt} (75%) create mode 100644 androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/navigation/TopicSearchScreen.kt rename androidHyperskillApp/src/main/res/layout/{dialog_search_topic.xml => fragment_search_topic.xml} (100%) diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/gamification_toolbar/view/ui/delegate/GamificationToolbarDelegate.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/gamification_toolbar/view/ui/delegate/GamificationToolbarDelegate.kt index d981429a6c..f99a159c5c 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/gamification_toolbar/view/ui/delegate/GamificationToolbarDelegate.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/gamification_toolbar/view/ui/delegate/GamificationToolbarDelegate.kt @@ -3,7 +3,6 @@ package org.hyperskill.app.android.gamification_toolbar.view.ui.delegate import android.content.Context import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams -import androidx.fragment.app.FragmentManager import androidx.lifecycle.LifecycleOwner import com.github.terrakok.cicerone.Router import com.google.android.material.appbar.AppBarLayout @@ -13,12 +12,11 @@ import org.hyperskill.app.android.main.view.ui.navigation.MainScreenRouter import org.hyperskill.app.android.main.view.ui.navigation.Tabs import org.hyperskill.app.android.main.view.ui.navigation.switch import org.hyperskill.app.android.progress.navigation.ProgressScreen -import org.hyperskill.app.android.topic_search.fragment.TopicSearchDialogFragment +import org.hyperskill.app.android.topic_search.navigation.TopicSearchScreen import org.hyperskill.app.android.view.base.ui.extension.setElevationOnCollapsed import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarFeature import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarFeature.Message import ru.nobird.android.view.base.ui.extension.setTextIfChanged -import ru.nobird.android.view.base.ui.extension.showIfNotExists class GamificationToolbarDelegate( lifecycleOwner: LifecycleOwner, @@ -77,8 +75,7 @@ class GamificationToolbarDelegate( fun onAction( action: GamificationToolbarFeature.Action.ViewAction, mainScreenRouter: MainScreenRouter, - router: Router, - fragmentManager: FragmentManager + router: Router ) { when (action) { is GamificationToolbarFeature.Action.ViewAction.ShowProfileTab -> @@ -86,8 +83,7 @@ class GamificationToolbarDelegate( GamificationToolbarFeature.Action.ViewAction.ShowProgressScreen -> router.navigateTo(ProgressScreen) GamificationToolbarFeature.Action.ViewAction.ShowSearchScreen -> { - TopicSearchDialogFragment.newInstance() - .showIfNotExists(fragmentManager, TopicSearchDialogFragment.TAG) + router.navigateTo(TopicSearchScreen) } } } diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/home/view/ui/fragment/HomeFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/home/view/ui/fragment/HomeFragment.kt index e52bf3c429..36accbbd9e 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/home/view/ui/fragment/HomeFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/home/view/ui/fragment/HomeFragment.kt @@ -174,8 +174,7 @@ class HomeFragment : gamificationToolbarDelegate?.onAction( action = action.viewAction, mainScreenRouter = mainScreenRouter, - router = requireRouter(), - fragmentManager = childFragmentManager + router = requireRouter() ) is HomeFeature.Action.ViewAction.NavigateTo.StepScreen -> { requireRouter().navigateTo( diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/leaderboard/fragment/LeaderboardFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/leaderboard/fragment/LeaderboardFragment.kt index 68f19c034b..eed0b67d6a 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/leaderboard/fragment/LeaderboardFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/leaderboard/fragment/LeaderboardFragment.kt @@ -118,8 +118,7 @@ class LeaderboardFragment : Fragment(R.layout.fragment_leaderboard) { gamificationToolbarDelegate?.onAction( action = action.viewAction, mainScreenRouter = mainScreenRouter, - router = requireRouter(), - fragmentManager = childFragmentManager + router = requireRouter() ) } is LeaderboardScreenFeature.Action.ViewAction.LeaderboardWidgetViewAction -> diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/study_plan/fragment/StudyPlanFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/study_plan/fragment/StudyPlanFragment.kt index 5542f36747..76380f63e8 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/study_plan/fragment/StudyPlanFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/study_plan/fragment/StudyPlanFragment.kt @@ -153,8 +153,7 @@ class StudyPlanFragment : gamificationToolbarDelegate?.onAction( action = action.viewAction, mainScreenRouter = mainScreenRouter, - router = requireRouter(), - fragmentManager = childFragmentManager + router = requireRouter() ) } is StudyPlanScreenFeature.Action.ViewAction.ProblemsLimitViewAction -> {} diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchFragment.kt similarity index 75% rename from androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt rename to androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchFragment.kt index dedf37ccff..78592dd1d8 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchDialogFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/fragment/TopicSearchFragment.kt @@ -1,11 +1,8 @@ package org.hyperskill.app.android.topic_search.fragment -import android.app.Dialog import android.content.Context import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.EditText @@ -13,7 +10,7 @@ import android.widget.Toast import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged -import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider @@ -27,33 +24,30 @@ import org.hyperskill.app.android.HyperskillApp import org.hyperskill.app.android.R import org.hyperskill.app.android.core.view.ui.navigation.requireRouter import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme -import org.hyperskill.app.android.databinding.DialogSearchTopicBinding +import org.hyperskill.app.android.databinding.FragmentSearchTopicBinding import org.hyperskill.app.android.step.view.screen.StepScreen import org.hyperskill.app.android.topic_search.ui.TopicSearchResult -import org.hyperskill.app.android.view.base.ui.extension.wrapWithTheme import org.hyperskill.app.core.view.handleActions import org.hyperskill.app.search.presentation.SearchFeature import org.hyperskill.app.search.presentation.SearchViewModel import ru.nobird.android.view.base.ui.extension.setTextIfChanged -class TopicSearchDialogFragment : DialogFragment() { +class TopicSearchFragment : Fragment(R.layout.fragment_search_topic) { companion object { private const val TALKBACK_FOCUS_CHANGE_DELAY_MS: Long = 100 - const val TAG = "TopicSearchDialogFragment" - fun newInstance(): TopicSearchDialogFragment = - TopicSearchDialogFragment() + fun newInstance(): TopicSearchFragment = + TopicSearchFragment() } private var viewModelFactory: ViewModelProvider.Factory? = null private val searchViewModel: SearchViewModel by viewModels { requireNotNull(viewModelFactory) } - private val viewBinding: DialogSearchTopicBinding by viewBinding(DialogSearchTopicBinding::bind) + private val viewBinding: FragmentSearchTopicBinding by viewBinding(FragmentSearchTopicBinding::bind) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setStyle(STYLE_NORMAL, R.style.ThemeOverlay_AppTheme_Dialog_Fullscreen) injectComponent() searchViewModel.handleActions(this, ::handleAction) } @@ -63,31 +57,6 @@ class TopicSearchDialogFragment : DialogFragment() { HyperskillApp.graph().buildPlatformSearchComponent().reduxViewModelFactory } - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = - super.onCreateDialog(savedInstanceState).apply { - setCanceledOnTouchOutside(false) - setCancelable(false) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? = - inflater.wrapWithTheme(requireActivity()) - .inflate(R.layout.dialog_search_topic, container, false) - - override fun onStart() { - super.onStart() - dialog?.window?.let { window -> - window.setLayout( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - window.setWindowAnimations(R.style.ThemeOverlay_AppTheme_Dialog_Fullscreen) - } - } - override fun onResume() { super.onResume() searchViewModel.onNewMessage( @@ -109,7 +78,6 @@ class TopicSearchDialogFragment : DialogFragment() { private fun setupToolbar() { with(viewBinding) { - topicSearchToolbar.setNavigationOnClickListener { dismiss() } topicSearchClearButton.isVisible = topicSearchEditText.text.isNotEmpty() topicSearchClearButton.setOnClickListener { topicSearchEditText.text.clear() @@ -131,7 +99,7 @@ class TopicSearchDialogFragment : DialogFragment() { setupEditTextRendering() } - fun requestFocusAndShowKeyboard( + private fun requestFocusAndShowKeyboard( editText: EditText, ) { // Without a delay requesting focus on edit text fails when talkback is active. @@ -160,7 +128,6 @@ class TopicSearchDialogFragment : DialogFragment() { when (action) { is SearchFeature.Action.ViewAction.OpenStepScreen -> { requireRouter().navigateTo(StepScreen(action.stepRoute)) - dismiss() } is SearchFeature.Action.ViewAction.OpenStepScreenFailed -> { Toast.makeText( diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/navigation/TopicSearchScreen.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/navigation/TopicSearchScreen.kt new file mode 100644 index 0000000000..fd5eb2f5a0 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_search/navigation/TopicSearchScreen.kt @@ -0,0 +1,11 @@ +package org.hyperskill.app.android.topic_search.navigation + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory +import com.github.terrakok.cicerone.androidx.FragmentScreen +import org.hyperskill.app.android.topic_search.fragment.TopicSearchFragment + +object TopicSearchScreen : FragmentScreen { + override fun createFragment(factory: FragmentFactory): Fragment = + TopicSearchFragment.newInstance() +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/res/layout/dialog_search_topic.xml b/androidHyperskillApp/src/main/res/layout/fragment_search_topic.xml similarity index 100% rename from androidHyperskillApp/src/main/res/layout/dialog_search_topic.xml rename to androidHyperskillApp/src/main/res/layout/fragment_search_topic.xml diff --git a/androidHyperskillApp/src/main/res/values/themes.xml b/androidHyperskillApp/src/main/res/values/themes.xml index bf2b0f6da4..a270d00e92 100644 --- a/androidHyperskillApp/src/main/res/values/themes.xml +++ b/androidHyperskillApp/src/main/res/values/themes.xml @@ -114,8 +114,9 @@ true false @color/color_background - @anim/slide_in_from_bottom - @anim/slide_out_to_bottom + + adjustResize