From bbc439ac274fabdfd322cec59f4b556e7a06f1ee Mon Sep 17 00:00:00 2001 From: ashutosh-kumar-kushwaha Date: Thu, 29 Feb 2024 12:33:46 +0530 Subject: [PATCH 1/5] Refactor: Migrate SearchFragment to Jetpack Compose --- mifosng-android/build.gradle.kts | 12 +- .../api/datamanager/DataManagerSearch.kt | 4 +- .../com/mifos/api/services/SearchService.kt | 4 +- .../online/search/SearchFragment.kt | 346 +++------------- .../online/search/SearchRepository.kt | 4 +- .../online/search/SearchRepositoryImp.kt | 4 +- .../mifosxdroid/online/search/SearchScreen.kt | 391 ++++++++++++++++++ .../online/search/SearchUiState.kt | 19 +- .../online/search/SearchUseCase.kt | 23 ++ .../online/search/SearchViewModel.kt | 63 +-- .../views/MultiFloatingActionButton.kt | 112 +++++ .../mifos/viewmodels/SearchViewModelTest.kt | 84 ++-- 12 files changed, 677 insertions(+), 389 deletions(-) create mode 100644 mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt create mode 100644 mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUseCase.kt create mode 100644 mifosng-android/src/main/java/com/mifos/mifosxdroid/views/MultiFloatingActionButton.kt diff --git a/mifosng-android/build.gradle.kts b/mifosng-android/build.gradle.kts index 9e5378b9a44..09c86ea9ba8 100644 --- a/mifosng-android/build.gradle.kts +++ b/mifosng-android/build.gradle.kts @@ -121,6 +121,7 @@ android { dependencies { implementation(project(":feature:auth")) + implementation(project(":core:common")) // Multidex dependency implementation("androidx.multidex:multidex:2.0.1") @@ -238,16 +239,21 @@ dependencies { implementation("com.github.openMF:fineract-client:2.0.3") // Jetpack Compose - implementation("androidx.compose.material:material:1.6.0") - implementation("androidx.compose.compiler:compiler:1.5.8") + implementation("androidx.compose.material:material:1.6.1") + implementation("androidx.compose.compiler:compiler:1.5.9") implementation("androidx.compose.ui:ui-tooling-preview:1.6.1") implementation("androidx.activity:activity-compose:1.8.2") debugImplementation("androidx.compose.ui:ui-tooling:1.6.1") - implementation("androidx.compose.material3:material3:1.1.2") + implementation("androidx.compose.material3:material3:1.2.0") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") implementation("androidx.compose.material:material-icons-extended:1.6.1") // ViewModel utilities for Compose implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") implementation("androidx.hilt:hilt-navigation-compose:1.1.0") + + implementation("com.google.accompanist:accompanist-drawablepainter:0.35.0-alpha") + + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1") + } \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/api/datamanager/DataManagerSearch.kt b/mifosng-android/src/main/java/com/mifos/api/datamanager/DataManagerSearch.kt index e4d6029f748..ee7d29eba31 100644 --- a/mifosng-android/src/main/java/com/mifos/api/datamanager/DataManagerSearch.kt +++ b/mifosng-android/src/main/java/com/mifos/api/datamanager/DataManagerSearch.kt @@ -11,10 +11,10 @@ import javax.inject.Singleton */ @Singleton class DataManagerSearch @Inject constructor(private val baseApiManager: BaseApiManager) { - fun searchResources( + suspend fun searchResources( query: String?, resources: String?, exactMatch: Boolean? - ): Observable> { + ): List { return baseApiManager.searchApi.searchResources(query, resources, exactMatch) } } \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/api/services/SearchService.kt b/mifosng-android/src/main/java/com/mifos/api/services/SearchService.kt index 32e0ed82456..6a71e258a7c 100644 --- a/mifosng-android/src/main/java/com/mifos/api/services/SearchService.kt +++ b/mifosng-android/src/main/java/com/mifos/api/services/SearchService.kt @@ -15,9 +15,9 @@ import rx.Observable */ interface SearchService { @GET(APIEndPoint.SEARCH) - fun searchResources( + suspend fun searchResources( @Query("query") clientName: String?, @Query("resource") resources: String?, @Query("exactMatch") exactMatch: Boolean? - ): Observable> + ): List } \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchFragment.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchFragment.kt index 6b56cdbacb3..83a724d20d5 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchFragment.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchFragment.kt @@ -5,340 +5,98 @@ package com.mifos.mifosxdroid.online.search import android.os.Bundle -import android.text.TextUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.animation.Animation -import android.view.animation.AnimationUtils -import android.view.inputmethod.EditorInfo -import android.widget.ArrayAdapter -import android.widget.Toast -import androidx.lifecycle.ViewModelProvider +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.mifos.mifosxdroid.activity.home.HomeActivity import com.mifos.mifosxdroid.R -import com.mifos.mifosxdroid.adapters.SearchAdapter import com.mifos.mifosxdroid.core.MifosBaseFragment -import com.mifos.mifosxdroid.core.util.Toaster.show -import com.mifos.mifosxdroid.databinding.FragmentClientSearchBinding +import com.mifos.mifosxdroid.views.FabType import com.mifos.objects.SearchedEntity import com.mifos.objects.navigation.ClientArgs import com.mifos.utils.Constants -import com.mifos.utils.Network import dagger.hilt.android.AndroidEntryPoint -import uk.co.deanwild.materialshowcaseview.MaterialShowcaseSequence -import uk.co.deanwild.materialshowcaseview.ShowcaseConfig - @AndroidEntryPoint class SearchFragment : MifosBaseFragment() { - private lateinit var binding: FragmentClientSearchBinding - - private lateinit var viewModel: SearchViewModel - - private lateinit var searchOptionsValues: Array - private lateinit var searchAdapter: SearchAdapter - - // determines weather search is triggered by user or system - private var autoTriggerSearch = false - private lateinit var searchedEntities: MutableList - private lateinit var searchOptionsAdapter: ArrayAdapter - private var resources: String? = null - private var isFabOpen = false - private lateinit var fabOpen: Animation - private lateinit var fabClose: Animation - private lateinit var rotateForward: Animation - private lateinit var rotateBackward: Animation - private lateinit var layoutManager: LinearLayoutManager - private var checkedFilter = 0 - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - searchedEntities = ArrayList() - fabOpen = AnimationUtils.loadAnimation(context, R.anim.fab_open) - fabClose = AnimationUtils.loadAnimation(context, R.anim.fab_close) - rotateForward = AnimationUtils.loadAnimation(context, R.anim.rotate_forward) - rotateBackward = AnimationUtils.loadAnimation(context, R.anim.rotate_backward) - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentClientSearchBinding.inflate(inflater, container, false) (activity as HomeActivity).supportActionBar?.title = getString(R.string.dashboard) - viewModel = ViewModelProvider(this)[SearchViewModel::class.java] - searchOptionsValues = - requireActivity().resources.getStringArray(R.array.search_options_values) - showUserInterface() - - - viewModel.searchUiState.observe(viewLifecycleOwner) { - when (it) { - is SearchUiState.ShowProgress -> showProgressbar(it.state) - is SearchUiState.ShowSearchedResources -> { - showProgressbar(false) - showSearchedResources(it.searchedEntities) - } - - is SearchUiState.ShowError -> { - showProgressbar(false) - showMessage(it.message) - } - - is SearchUiState.ShowNoResultFound -> { - showProgressbar(false) - showNoResultFound() + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + SearchScreen( + onFabClick = { fabType -> + onFabClick(fabType) + }, + ){ searchedEntity -> + onSearchOptionClick(searchedEntity) } } } - return binding.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.fabClient.setOnClickListener { - findNavController().navigate(R.id.action_navigation_dashboard_to_createNewClientFragment) - } - - binding.fabCenter.setOnClickListener { - findNavController().navigate(R.id.action_navigation_dashboard_to_createNewCenterFragment) - } - - binding.fabGroup.setOnClickListener { - findNavController().navigate(R.id.action_navigation_dashboard_to_createNewGroupFragment) - } - - binding.etSearch.setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - onClickSearch() - return@setOnEditorActionListener true + private fun onFabClick(fabType: FabType) { + when (fabType) { + FabType.CLIENT -> { + findNavController().navigate(R.id.action_navigation_dashboard_to_createNewClientFragment) } - return@setOnEditorActionListener false - } - - - binding.btnSearch.setOnClickListener { - onClickSearch() - } - - binding.fabCreate.setOnClickListener { - if (isFabOpen) { - binding.fabCreate.startAnimation(rotateBackward) - binding.fabClient.startAnimation(fabClose) - binding.fabCenter.startAnimation(fabClose) - binding.fabGroup.startAnimation(fabClose) - binding.fabClient.isClickable = false - binding.fabCenter.isClickable = false - binding.fabGroup.isClickable = false - isFabOpen = false - } else { - binding.fabCreate.startAnimation(rotateForward) - binding.fabClient.startAnimation(fabOpen) - binding.fabCenter.startAnimation(fabOpen) - binding.fabGroup.startAnimation(fabOpen) - binding.fabClient.isClickable = true - binding.fabCenter.isClickable = true - binding.fabGroup.isClickable = true - isFabOpen = true + FabType.CENTER -> { + findNavController().navigate(R.id.action_navigation_dashboard_to_createNewCenterFragment) + } + FabType.GROUP -> { + findNavController().navigate(R.id.action_navigation_dashboard_to_createNewGroupFragment) } - autoTriggerSearch = false - } - } - - private fun showFilterDialog() { - val dialogBuilder = MaterialAlertDialogBuilder(requireContext()) - dialogBuilder.setSingleChoiceItems( - R.array.search_options, - checkedFilter - ) { dialog, index -> - checkedFilter = index - resources = if (checkedFilter == 0) null else searchOptionsValues[checkedFilter - 1] - autoTriggerSearch = true - onClickSearch() - binding.filterSelectionButton.text = - getResources().getStringArray(R.array.search_options)[index] - dialog.dismiss() } - dialogBuilder.show() } - private fun showUserInterface() { - searchOptionsAdapter = ArrayAdapter.createFromResource( - (requireActivity()), - R.array.search_options, android.R.layout.simple_spinner_item - ) - searchOptionsAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - binding.filterSelectionButton.setOnClickListener { showFilterDialog() } - binding.filterSelectionButton.text = - getResources().getStringArray(R.array.search_options)[0] - binding.etSearch.requestFocus() - layoutManager = LinearLayoutManager(activity) - layoutManager.orientation = LinearLayoutManager.VERTICAL - binding.rvSearch.layoutManager = layoutManager - binding.rvSearch.setHasFixedSize(true) - searchAdapter = SearchAdapter { searchedEntity: SearchedEntity -> - when (searchedEntity.entityType) { - Constants.SEARCH_ENTITY_LOAN -> { - val action = SearchFragmentDirections.actionNavigationDashboardToClientActivity( - ClientArgs(clientId = searchedEntity.entityId) - ) - findNavController().navigate(action) - } - - Constants.SEARCH_ENTITY_CLIENT -> { - val action = SearchFragmentDirections.actionNavigationDashboardToClientActivity( - ClientArgs(clientId = searchedEntity.entityId) - ) - findNavController().navigate(action) - } + private fun onSearchOptionClick(searchedEntity: SearchedEntity) { + when (searchedEntity.entityType) { + Constants.SEARCH_ENTITY_LOAN -> { + val action = SearchFragmentDirections.actionNavigationDashboardToClientActivity( + ClientArgs(clientId = searchedEntity.entityId) + ) + findNavController().navigate(action) + } - Constants.SEARCH_ENTITY_GROUP -> { - val action = searchedEntity.entityName?.let { - SearchFragmentDirections.actionNavigationDashboardToGroupsActivity( - searchedEntity.entityId, - it - ) - } - action?.let { findNavController().navigate(it) } - } + Constants.SEARCH_ENTITY_CLIENT -> { + val action = SearchFragmentDirections.actionNavigationDashboardToClientActivity( + ClientArgs(clientId = searchedEntity.entityId) + ) + findNavController().navigate(action) + } - Constants.SEARCH_ENTITY_SAVING -> { - val action = SearchFragmentDirections.actionNavigationDashboardToClientActivity( - ClientArgs(savingsAccountNumber = searchedEntity.entityId) + Constants.SEARCH_ENTITY_GROUP -> { + val action = searchedEntity.entityName?.let { + SearchFragmentDirections.actionNavigationDashboardToGroupsActivity( + searchedEntity.entityId, + it ) - findNavController().navigate(action) } - - Constants.SEARCH_ENTITY_CENTER -> { - val action = - SearchFragmentDirections.actionNavigationDashboardToCentersActivity( - searchedEntity.entityId - ) - findNavController().navigate(action) - } - } - } - binding.rvSearch.adapter = searchAdapter - binding.cbExactMatch.setOnCheckedChangeListener { _, _ -> onClickSearch() } - showGuide() - } - - private fun showGuide() { - val config = ShowcaseConfig() - config.delay = 250 // half second between each showcase view - val sequence = MaterialShowcaseSequence(activity, "123") - sequence.setConfig(config) - var etSearchIntro: String = getString(R.string.et_search_intro) - var i = 1 - for (s: String in searchOptionsValues) { - etSearchIntro += "\n$i.$s" - i++ - } - val spSearchIntro = getString(R.string.sp_search_intro) - val cbExactMatchIntro = getString(R.string.cb_exactMatch_intro) - val btSearchIntro = getString(R.string.bt_search_intro) - sequence.addSequenceItem( - binding.etSearch, - etSearchIntro, getString(R.string.got_it) - ) - sequence.addSequenceItem( - binding.filterSelectionButton, - spSearchIntro, getString(R.string.next) - ) - sequence.addSequenceItem( - binding.cbExactMatch, - cbExactMatchIntro, getString(R.string.next) - ) - sequence.addSequenceItem( - binding.btnSearch, - btSearchIntro, getString(R.string.finish) - ) - sequence.start() - } - - private fun showSearchedResources(searchedEntities: List) { - searchAdapter.setSearchResults(searchedEntities) - this.searchedEntities = searchedEntities.toMutableList() - } - - private fun showNoResultFound() { - searchedEntities.clear() - searchAdapter.notifyDataSetChanged() - show(binding.etSearch, getString(R.string.no_search_result_found)) - } - - private fun showMessage(message: String) { - Toast.makeText(activity, message, Toast.LENGTH_SHORT).show() - } - - private fun showProgressbar(b: Boolean) { - if (b) { - showMifosProgressDialog() - } else { - hideMifosProgressDialog() - } - } - - override fun onPause() { - //Fragment getting detached, keyboard if open must be hidden - hideKeyboard(binding.etSearch) - super.onPause() - } - - /** - * There is a need for this method in the following cases : - * - * - * 1. If user entered a search query and went out of the app. - * 2. If user entered a search query and got some search results and went out of the app. - * - * @param outState - */ - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - try { - val queryString = binding.etSearch.editableText.toString() - if (queryString != "") { - outState.putString(LOG_TAG + binding.etSearch.id, queryString) + action?.let { findNavController().navigate(it) } } - } catch (npe: NullPointerException) { - //Looks like edit text didn't get initialized properly - } - } - override fun onViewStateRestored(savedInstanceState: Bundle?) { - super.onViewStateRestored(savedInstanceState) - if (savedInstanceState != null) { - val queryString = savedInstanceState.getString(LOG_TAG + binding.etSearch.id) - if (!TextUtils.isEmpty(queryString)) { - binding.etSearch.setText(queryString) + Constants.SEARCH_ENTITY_SAVING -> { + val action = SearchFragmentDirections.actionNavigationDashboardToClientActivity( + ClientArgs(savingsAccountNumber = searchedEntity.entityId) + ) + findNavController().navigate(action) } - } - } - private fun onClickSearch() { - hideKeyboard(binding.etSearch) - if (!Network.isOnline(requireContext())) { - showMessage(getStringMessage(com.github.therajanmaurya.sweeterror.R.string.no_internet_connection)) - return - } - val query = binding.etSearch.editableText.toString().trim { it <= ' ' } - if (query.isNotEmpty()) { - viewModel.searchResources(query, resources, binding.cbExactMatch.isChecked) - } else { - if (!autoTriggerSearch) { - show(binding.etSearch, getString(R.string.no_search_query_entered)) + Constants.SEARCH_ENTITY_CENTER -> { + val action = + SearchFragmentDirections.actionNavigationDashboardToCentersActivity( + searchedEntity.entityId + ) + findNavController().navigate(action) } } } - - companion object { - private val LOG_TAG = SearchFragment::class.java.simpleName - } } \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchRepository.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchRepository.kt index 085acb94c97..6c725e69ace 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchRepository.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchRepository.kt @@ -8,10 +8,10 @@ import rx.Observable */ interface SearchRepository { - fun searchResources( + suspend fun searchResources( query: String?, resources: String?, exactMatch: Boolean? - ): Observable> + ): List } \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchRepositoryImp.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchRepositoryImp.kt index 9e4bf49df1e..3c642b0e69c 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchRepositoryImp.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchRepositoryImp.kt @@ -10,11 +10,11 @@ import javax.inject.Inject */ class SearchRepositoryImp @Inject constructor(private val dataManagerSearch: DataManagerSearch) : SearchRepository { - override fun searchResources( + override suspend fun searchResources( query: String?, resources: String?, exactMatch: Boolean? - ): Observable> { + ): List { return dataManagerSearch.searchResources(query, resources, exactMatch) } diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt new file mode 100644 index 00000000000..dd8d6656340 --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt @@ -0,0 +1,391 @@ +package com.mifos.mifosxdroid.online.search + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.hilt.navigation.compose.hiltViewModel +import com.amulyakhare.textdrawable.TextDrawable +import com.amulyakhare.textdrawable.util.ColorGenerator +import com.google.accompanist.drawablepainter.rememberDrawablePainter +import com.mifos.mifosxdroid.R +import com.mifos.mifosxdroid.views.FabButton +import com.mifos.mifosxdroid.views.FabButtonState +import com.mifos.mifosxdroid.views.FabType +import com.mifos.mifosxdroid.views.MultiFloatingActionButton +import com.mifos.objects.SearchedEntity + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchScreen( + onFabClick: (FabType) -> Unit, + onSearchOptionClick: (SearchedEntity) -> Unit +) { + val viewModel: SearchViewModel = hiltViewModel() + var selectedFilter by remember { mutableIntStateOf(0) } + val searchOptions = stringArrayResource(id = R.array.search_options) + var showFilterDialog by remember { mutableStateOf(false) } + val searchUiState = viewModel.searchUiState.collectAsState().value + val snackbarHostState = remember { SnackbarHostState() } + val context = LocalContext.current + var fabButtonState by remember { mutableStateOf(FabButtonState.Collapsed) } + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = stringResource(id = R.string.search), + fontSize = 24.sp + ) + }, + actions = { + Button( + onClick = { + showFilterDialog = true + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface + ) + ) { + Row { + Text( + text = searchOptions[selectedFilter], + fontSize = 16.sp + ) + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null + ) + } + } + } + ) + }, + snackbarHost = { + SnackbarHost( + hostState = snackbarHostState + ) + }, + floatingActionButton = { + MultiFloatingActionButton( + fabButtons = listOf( + FabButton( + fabType = FabType.CLIENT, + iconRes = R.drawable.ic_person_black_24dp + ), + FabButton( + fabType = FabType.CENTER, + iconRes = R.drawable.ic_centers_24dp + ), + FabButton( + fabType = FabType.GROUP, + iconRes = R.drawable.ic_group_black_24dp + ) + ), + fabButtonState = fabButtonState, + onFabButtonStateChange = { + fabButtonState = it + }, + onFabClick = onFabClick + ) + } + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(it) + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + var searchText by remember { mutableStateOf("") } + var exactMatchChecked by remember { mutableStateOf(false) } + + OutlinedTextField( + value = searchText, + onValueChange = { + searchText = it + }, + modifier = Modifier + .fillMaxWidth(), + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null + ) + }, + label = { + Text( + text = stringResource(id = R.string.search_hint), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Search + ), + keyboardActions = KeyboardActions( + onSearch = { + if (searchText.isEmpty()) { + viewModel.showError(context.getString(R.string.no_search_query_entered)) + return@KeyboardActions + } + viewModel.searchResources( + searchText, + if (selectedFilter == 0) null else searchOptions[selectedFilter], + exactMatchChecked + ) + } + ), + maxLines = 1, + textStyle = TextStyle( + fontSize = 18.sp + ) + ) + Button( + onClick = { + if (searchText.isEmpty()) { + viewModel.showError(context.getString(R.string.no_search_query_entered)) + return@Button + } + viewModel.searchResources( + searchText, + if (selectedFilter == 0) null else searchOptions[selectedFilter], + exactMatchChecked + ) + }, + modifier = Modifier + .fillMaxWidth() + ) { + Text( + text = stringResource(id = R.string.search), + fontSize = 16.sp + ) + } + Row( + modifier = Modifier + .wrapContentWidth() + .clickable { + exactMatchChecked = !exactMatchChecked + } + .padding(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = exactMatchChecked, + onCheckedChange = { + exactMatchChecked = it + }, + modifier = Modifier + .size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(id = R.string.exact_match), + fontSize = 16.sp + ) + } + LazyColumn( + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + if (searchUiState.searchedEntities.isNotEmpty()) { + items(searchUiState.searchedEntities.size) { position -> + ClientItem( + searchedEntity = searchUiState.searchedEntities[position], + onSearchOptionClick = onSearchOptionClick + ) + } + } + } + + if (showFilterDialog) { + FilterDialog( + searchOptions = searchOptions, + selected = selectedFilter, + onSelected = { + selectedFilter = it + }, + onDismiss = { + showFilterDialog = false + } + ) + } + + if (searchUiState.error != null) { + LaunchedEffect(searchUiState.error) { + snackbarHostState.showSnackbar(searchUiState.error) + viewModel.resetErrorMessage() + } + } + + if (searchUiState.isLoading) { + LoadingDialog( + onDismissRequest = { + viewModel.dismissDialog() + } + ) + } + } + } +} + +@Preview(showSystemUi = true, showBackground = true) +@Composable +fun SearchScreenPreview() { + SearchScreen( + onFabClick = {}, + onSearchOptionClick = {} + ) +} + +@Composable +fun ClientItem(searchedEntity: SearchedEntity, onSearchOptionClick: (SearchedEntity) -> Unit) { + val color = ColorGenerator.MATERIAL.getColor(searchedEntity.entityType) + val drawable = + TextDrawable.builder().round().build(searchedEntity.entityType?.get(0).toString(), color) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .clickable { + onSearchOptionClick(searchedEntity) + } + ) { + Image( + modifier = Modifier + .width(50.dp) + .height(50.dp), + contentDescription = null, + painter = rememberDrawablePainter(drawable = drawable), + ) + Text( + text = searchedEntity.entityName ?: "", + fontSize = 16.sp + ) + } +} + +@Composable +fun FilterDialog( + searchOptions: Array, + selected: Int, + onSelected: (Int) -> Unit, + onDismiss: () -> Unit +) { + Dialog( + onDismissRequest = onDismiss + ) { + Card { + Column { + searchOptions.forEachIndexed { position, text -> + SearchOption( + text = text, + selected = selected == position, + onSelected = { + onSelected(position) + onDismiss() + } + ) + } + } + } + } +} + +@Composable +fun SearchOption(text: String, selected: Boolean, onSelected: () -> Unit) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { + onSelected() + } + ) { + RadioButton( + selected = selected, + onClick = { + onSelected() + } + ) + Text( + text = text, + fontSize = 16.sp + ) + } +} + +@Composable +fun LoadingDialog( + onDismissRequest: () -> Unit +) { + Dialog(onDismissRequest = onDismissRequest) { + Card { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .padding(horizontal = 32.dp, vertical = 16.dp) + ) { + CircularProgressIndicator() + Text( + text = "Loading...", + fontSize = 16.sp + ) + } + } + } +} \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUiState.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUiState.kt index d02b0a69154..bbca72469ea 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUiState.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUiState.kt @@ -2,17 +2,8 @@ package com.mifos.mifosxdroid.online.search import com.mifos.objects.SearchedEntity -/** - * Created by Aditya Gupta on 06/08/23. - */ -sealed class SearchUiState { - - data class ShowProgress(val state: Boolean) : SearchUiState() - - data class ShowError(val message: String) : SearchUiState() - - data class ShowSearchedResources(val searchedEntities: List) : SearchUiState() - - object ShowNoResultFound : SearchUiState() - -} +data class SearchUiState( + val isLoading: Boolean = false, + val error: String? = null, + val searchedEntities: List = emptyList() +) \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUseCase.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUseCase.kt new file mode 100644 index 00000000000..003f87808fa --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUseCase.kt @@ -0,0 +1,23 @@ +package com.mifos.mifosxdroid.online.search + +import com.mifos.core.common.utils.Resource +import com.mifos.objects.SearchedEntity +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class SearchUseCase @Inject constructor(private val repository: SearchRepository) { + operator fun invoke(query: String?, resources: String?, exactMatch: Boolean?): Flow>> = flow{ + emit(Resource.Loading()) + try { + val searchedEntities = repository.searchResources(query, resources, exactMatch) + if (searchedEntities.isEmpty()) { + emit(Resource.Error("No Search Result found")) + } else { + emit(Resource.Success(searchedEntities)) + } + } catch (e: Exception) { + emit(Resource.Error(e.message ?: "An unexpected error occurred")) + } + } +} \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchViewModel.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchViewModel.kt index e36dc3dd4f1..24b99d7862e 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchViewModel.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchViewModel.kt @@ -1,44 +1,55 @@ package com.mifos.mifosxdroid.online.search -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData + import androidx.lifecycle.ViewModel -import com.mifos.objects.SearchedEntity +import androidx.lifecycle.viewModelScope +import com.mifos.core.common.utils.Resource import dagger.hilt.android.lifecycle.HiltViewModel -import rx.Subscriber -import rx.android.schedulers.AndroidSchedulers -import rx.schedulers.Schedulers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import javax.inject.Inject /** * Created by Aditya Gupta on 06/08/23. */ @HiltViewModel -class SearchViewModel @Inject constructor(private val repository: SearchRepository) : ViewModel() { +class SearchViewModel @Inject constructor(private val searchUseCase: SearchUseCase) : ViewModel() { - private val _searchUiState = MutableLiveData() + private val _searchUiState = MutableStateFlow(SearchUiState()) - val searchUiState: LiveData - get() = _searchUiState + val searchUiState: StateFlow + get() = _searchUiState.asStateFlow() fun searchResources(query: String?, resources: String?, exactMatch: Boolean?) { - _searchUiState.value = SearchUiState.ShowProgress(true) - repository.searchResources(query, resources, exactMatch) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeOn(Schedulers.io()) - .subscribe(object : Subscriber>() { - override fun onCompleted() {} - override fun onError(e: Throwable) { - _searchUiState.value = SearchUiState.ShowError(e.message.toString()) + searchUseCase(query, resources, exactMatch).onEach { + when (it) { + is Resource.Loading -> { + _searchUiState.value = _searchUiState.value.copy(isLoading = true) + } + + is Resource.Success -> { + _searchUiState.value = SearchUiState(searchedEntities = it.data!!) } - override fun onNext(searchedEntities: List) { - if (searchedEntities.isEmpty()) { - _searchUiState.value = SearchUiState.ShowNoResultFound - } else { - _searchUiState.value = SearchUiState.ShowSearchedResources(searchedEntities) - } + is Resource.Error -> { + _searchUiState.value = _searchUiState.value.copy(isLoading = false, error = it.message) } - }) + } + }.launchIn(viewModelScope) + } + + fun showError(error: String) { + _searchUiState.value = _searchUiState.value.copy(error = error) + } + + fun dismissDialog() { + _searchUiState.value = _searchUiState.value.copy(isLoading = false) + } + + fun resetErrorMessage() { + _searchUiState.value = _searchUiState.value.copy(error = null) } -} \ No newline at end of file +} diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/views/MultiFloatingActionButton.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/views/MultiFloatingActionButton.kt new file mode 100644 index 00000000000..3dcfbf09611 --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/views/MultiFloatingActionButton.kt @@ -0,0 +1,112 @@ +package com.mifos.mifosxdroid.views + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp + +enum class FabType { + CLIENT, CENTER, GROUP +} + +sealed class FabButtonState { + object Collapsed : FabButtonState() + object Expand : FabButtonState() + + fun isExpanded() = this == Expand + + fun toggleValue() = if (isExpanded()) { + Collapsed + } else { + Expand + } +} + +data class FabButton( + val fabType: FabType, + val iconRes: Int, +) + + +@Composable +fun FabItem( + fabButton: FabButton, + onFabClick: (FabType) -> Unit +) { + FloatingActionButton( + onClick = { + onFabClick(fabButton.fabType) + }, + modifier = Modifier + .size(48.dp) + ) { + Icon( + painter = painterResource(id = fabButton.iconRes), + contentDescription = null + ) + } +} + +@Composable +fun MultiFloatingActionButton( + fabButtons: List, + fabButtonState: FabButtonState, + onFabButtonStateChange: (FabButtonState) -> Unit, + onFabClick: (FabType) -> Unit +) { + val rotation by animateFloatAsState( + if (fabButtonState.isExpanded()) + 45f + else + 0f, label = "mainFabRotation" + ) + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + AnimatedVisibility( + visible = fabButtonState.isExpanded(), + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + Column { + fabButtons.forEach { + FabItem( + fabButton = it, + onFabClick = onFabClick + ) + Spacer(modifier = Modifier.height(24.dp)) + } + } + } + + FloatingActionButton( + onClick = { + onFabButtonStateChange(fabButtonState.toggleValue()) + }, + modifier = Modifier + .rotate(rotation) + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null + ) + } + } +} \ No newline at end of file diff --git a/mifosng-android/src/test/java/com/mifos/viewmodels/SearchViewModelTest.kt b/mifosng-android/src/test/java/com/mifos/viewmodels/SearchViewModelTest.kt index aefe2eabaaa..930dfc127b9 100644 --- a/mifosng-android/src/test/java/com/mifos/viewmodels/SearchViewModelTest.kt +++ b/mifosng-android/src/test/java/com/mifos/viewmodels/SearchViewModelTest.kt @@ -1,23 +1,26 @@ package com.mifos.viewmodels -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.Observer -import com.mifos.mifosxdroid.util.RxSchedulersOverrideRule +import com.mifos.core.common.utils.Resource import com.mifos.objects.SearchedEntity -import com.mifos.mifosxdroid.online.search.SearchRepository +import com.mifos.mifosxdroid.online.search.SearchUseCase import com.mifos.mifosxdroid.online.search.SearchViewModel -import com.mifos.mifosxdroid.online.search.SearchUiState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assert.* import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations import org.mockito.junit.MockitoJUnitRunner -import rx.Observable /** * Created by Aditya Gupta on 02/09/23. @@ -25,73 +28,66 @@ import rx.Observable @RunWith(MockitoJUnitRunner::class) class SearchViewModelTest { - @get:Rule - val overrideSchedulersRule = RxSchedulersOverrideRule() - - @get:Rule - val instantTaskExecutorRule = InstantTaskExecutorRule() - - @Mock - lateinit var searchRepository: SearchRepository - @Mock - lateinit var searchUiStateObserver: Observer + lateinit var searchUseCase: SearchUseCase private lateinit var searchViewModel: SearchViewModel - @Mock - private lateinit var searchedEntities: List - + private val dispatcher: TestDispatcher = StandardTestDispatcher() + @OptIn(ExperimentalCoroutinesApi::class) @Before fun setUp() { MockitoAnnotations.openMocks(this) - searchViewModel = SearchViewModel(searchRepository) - searchViewModel.searchUiState.observeForever(searchUiStateObserver) + searchViewModel = SearchViewModel(searchUseCase) + Dispatchers.setMain(dispatcher) } - @Test - fun testSearchAll_SuccessfulSearchAllReceivedFromRepository_ReturnsSuccess() { - + fun testSearchAll_SuccessfulSearchAllReceivedFromUseCase_ReturnsSuccess() { Mockito.`when`( - searchRepository.searchResources( + searchUseCase( Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean() ) ).thenReturn( - Observable.just(searchedEntities) + flowOf(Resource.Success(listOf(SearchedEntity()))) ) - searchViewModel.searchResources("query", "resources", false) - Mockito.verify(searchUiStateObserver).onChanged(SearchUiState.ShowProgress(true)) - Mockito.verify(searchUiStateObserver, Mockito.never()) - .onChanged(SearchUiState.ShowError("error")) - Mockito.verify(searchUiStateObserver) - .onChanged(SearchUiState.ShowSearchedResources(searchedEntities)) + + runTest { + searchViewModel.searchResources("query", "resources", false) + } + + assertNotEquals(0, searchViewModel.searchUiState.value.searchedEntities.size) + assertEquals(searchViewModel.searchUiState.value.isLoading, false) + assertNull(searchViewModel.searchUiState.value.error) } @Test - fun testSearchAll_UnsuccessfulSearchAllReceivedFromRepository_ReturnsError() { + fun testSearchAll_UnsuccessfulSearchAllReceivedFromUseCase_ReturnsError() { Mockito.`when`( - searchRepository.searchResources( + searchUseCase( Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean() ) ).thenReturn( - Observable.error(RuntimeException("some error message")) + flowOf(Resource.Error("some error message")) ) - searchViewModel.searchResources("query", "resources", false) - Mockito.verify(searchUiStateObserver).onChanged(SearchUiState.ShowProgress(true)) - Mockito.verify(searchUiStateObserver) - .onChanged(SearchUiState.ShowError("some error message")) - Mockito.verify(searchUiStateObserver, Mockito.never()) - .onChanged(SearchUiState.ShowSearchedResources(searchedEntities)) + + runTest { + searchViewModel.searchResources("query", "resources", false) + } + + assertEquals(0, searchViewModel.searchUiState.value.searchedEntities.size) + assertEquals(searchViewModel.searchUiState.value.isLoading, false) + assertNotNull(searchViewModel.searchUiState.value.error) } + @OptIn(ExperimentalCoroutinesApi::class) @After fun tearDown() { - searchViewModel.searchUiState.removeObserver(searchUiStateObserver) + Dispatchers.resetMain() } -} \ No newline at end of file +} From e9968d19335e35034015bfbb7dae44d9bd6cc082 Mon Sep 17 00:00:00 2001 From: ashutosh-kumar-kushwaha Date: Mon, 18 Mar 2024 14:11:04 +0530 Subject: [PATCH 2/5] Fixed the imports --- .../com/mifos/mifosxdroid/online/search/SearchFragment.kt | 4 ++-- .../java/com/mifos/mifosxdroid/online/search/SearchScreen.kt | 2 +- .../java/com/mifos/mifosxdroid/online/search/SearchUiState.kt | 2 +- .../java/com/mifos/mifosxdroid/online/search/SearchUseCase.kt | 2 +- .../src/test/java/com/mifos/viewmodels/SearchViewModelTest.kt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchFragment.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchFragment.kt index 83a724d20d5..847cbcfcd93 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchFragment.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchFragment.kt @@ -15,8 +15,8 @@ import com.mifos.mifosxdroid.activity.home.HomeActivity import com.mifos.mifosxdroid.R import com.mifos.mifosxdroid.core.MifosBaseFragment import com.mifos.mifosxdroid.views.FabType -import com.mifos.objects.SearchedEntity -import com.mifos.objects.navigation.ClientArgs +import com.mifos.core.objects.SearchedEntity +import com.mifos.core.objects.navigation.ClientArgs import com.mifos.utils.Constants import dagger.hilt.android.AndroidEntryPoint diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt index dd8d6656340..7866ef79a0f 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt @@ -64,7 +64,7 @@ import com.mifos.mifosxdroid.views.FabButton import com.mifos.mifosxdroid.views.FabButtonState import com.mifos.mifosxdroid.views.FabType import com.mifos.mifosxdroid.views.MultiFloatingActionButton -import com.mifos.objects.SearchedEntity +import com.mifos.core.objects.SearchedEntity @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUiState.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUiState.kt index bbca72469ea..d83b5d71089 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUiState.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUiState.kt @@ -1,6 +1,6 @@ package com.mifos.mifosxdroid.online.search -import com.mifos.objects.SearchedEntity +import com.mifos.core.objects.SearchedEntity data class SearchUiState( val isLoading: Boolean = false, diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUseCase.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUseCase.kt index 003f87808fa..82e3081ce9b 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUseCase.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchUseCase.kt @@ -1,7 +1,7 @@ package com.mifos.mifosxdroid.online.search import com.mifos.core.common.utils.Resource -import com.mifos.objects.SearchedEntity +import com.mifos.core.objects.SearchedEntity import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import javax.inject.Inject diff --git a/mifosng-android/src/test/java/com/mifos/viewmodels/SearchViewModelTest.kt b/mifosng-android/src/test/java/com/mifos/viewmodels/SearchViewModelTest.kt index 930dfc127b9..d266d039bdd 100644 --- a/mifosng-android/src/test/java/com/mifos/viewmodels/SearchViewModelTest.kt +++ b/mifosng-android/src/test/java/com/mifos/viewmodels/SearchViewModelTest.kt @@ -1,7 +1,7 @@ package com.mifos.viewmodels import com.mifos.core.common.utils.Resource -import com.mifos.objects.SearchedEntity +import com.mifos.core.objects.SearchedEntity import com.mifos.mifosxdroid.online.search.SearchUseCase import com.mifos.mifosxdroid.online.search.SearchViewModel import kotlinx.coroutines.Dispatchers From 3934a10d1a12a6f2082341e9e543c3cb9bacb1bb Mon Sep 17 00:00:00 2001 From: ashutosh-kumar-kushwaha Date: Sat, 11 May 2024 00:40:58 +0530 Subject: [PATCH 3/5] Resolved Merge conflicts --- gradle/libs.versions.toml | 2 ++ mifosng-android/build.gradle.kts | 8 ++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6920552f421..24fbead5f55 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,5 @@ [versions] +accompanistDrawablepainter = "0.35.0-alpha" accompanistSwiperefresh = "0.25.1" accompanistPermission = "0.34.0" adapterRxjava = "2.9.0" @@ -118,6 +119,7 @@ truthVersion = '1.1.5' [libraries] # AndroidX Libraries +accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanistDrawablepainter" } accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanistSwiperefresh" } accompanist-permission = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermission" } android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" } diff --git a/mifosng-android/build.gradle.kts b/mifosng-android/build.gradle.kts index b399e50c49b..8c4d7a8a0a7 100644 --- a/mifosng-android/build.gradle.kts +++ b/mifosng-android/build.gradle.kts @@ -222,6 +222,7 @@ dependencies { androidTestImplementation(libs.mockito.android) testImplementation(libs.junit.jupiter) testImplementation(libs.androidx.core.testing) + testImplementation(libs.kotlinx.coroutines.test) //Android-Jobs implementation(libs.android.job) @@ -262,11 +263,6 @@ dependencies { // ViewModel utilities for Compose implementation(libs.androidx.lifecycle.viewModelCompose) implementation(libs.androidx.hilt.navigation.compose) - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") - implementation("androidx.hilt:hilt-navigation-compose:1.1.0") - - implementation("com.google.accompanist:accompanist-drawablepainter:0.35.0-alpha") - - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1") + implementation(libs.accompanist.drawablepainter) } \ No newline at end of file From eed260762400eb12685fc4006fb813050d8fa50b Mon Sep 17 00:00:00 2001 From: ashutosh-kumar-kushwaha Date: Wed, 15 May 2024 03:20:45 +0530 Subject: [PATCH 4/5] Remove Dialog in ProgressBar, Use Mifos Mifos Color Scheme and use collectAsStateWithLifecycle --- mifosng-android/build.gradle.kts | 2 + .../mifosxdroid/online/search/SearchScreen.kt | 67 +++++++------------ 2 files changed, 25 insertions(+), 44 deletions(-) diff --git a/mifosng-android/build.gradle.kts b/mifosng-android/build.gradle.kts index 8c4d7a8a0a7..bc680ad9e03 100644 --- a/mifosng-android/build.gradle.kts +++ b/mifosng-android/build.gradle.kts @@ -132,6 +132,7 @@ dependencies { implementation(project(":core:datastore")) implementation(project(":core:network")) implementation(project(":core:common")) + implementation(project(":core:designsystem")) // Multidex dependency implementation(libs.androidx.multidex) @@ -263,6 +264,7 @@ dependencies { // ViewModel utilities for Compose implementation(libs.androidx.lifecycle.viewModelCompose) implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.androidx.lifecycle.runtimeCompose) implementation(libs.accompanist.drawablepainter) } \ No newline at end of file diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt index 7866ef79a0f..4f4c842ef47 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold @@ -36,7 +35,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -56,15 +54,17 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.util.ColorGenerator import com.google.accompanist.drawablepainter.rememberDrawablePainter +import com.mifos.core.designsystem.theme.Black +import com.mifos.core.objects.SearchedEntity import com.mifos.mifosxdroid.R import com.mifos.mifosxdroid.views.FabButton import com.mifos.mifosxdroid.views.FabButtonState import com.mifos.mifosxdroid.views.FabType import com.mifos.mifosxdroid.views.MultiFloatingActionButton -import com.mifos.core.objects.SearchedEntity @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -76,7 +76,7 @@ fun SearchScreen( var selectedFilter by remember { mutableIntStateOf(0) } val searchOptions = stringArrayResource(id = R.array.search_options) var showFilterDialog by remember { mutableStateOf(false) } - val searchUiState = viewModel.searchUiState.collectAsState().value + val searchUiState = viewModel.searchUiState.collectAsStateWithLifecycle().value val snackbarHostState = remember { SnackbarHostState() } val context = LocalContext.current var fabButtonState by remember { mutableStateOf(FabButtonState.Collapsed) } @@ -97,7 +97,7 @@ fun SearchScreen( }, colors = ButtonDefaults.buttonColors( containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onSurface + contentColor = Black ) ) { Row { @@ -237,15 +237,24 @@ fun SearchScreen( fontSize = 16.sp ) } - LazyColumn( - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - if (searchUiState.searchedEntities.isNotEmpty()) { - items(searchUiState.searchedEntities.size) { position -> - ClientItem( - searchedEntity = searchUiState.searchedEntities[position], - onSearchOptionClick = onSearchOptionClick - ) + + if (searchUiState.isLoading) { + CircularProgressIndicator( + modifier = Modifier + .padding(40.dp) + .align(Alignment.CenterHorizontally) + ) + } else { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + if (searchUiState.searchedEntities.isNotEmpty()) { + items(searchUiState.searchedEntities.size) { position -> + ClientItem( + searchedEntity = searchUiState.searchedEntities[position], + onSearchOptionClick = onSearchOptionClick + ) + } } } } @@ -269,14 +278,6 @@ fun SearchScreen( viewModel.resetErrorMessage() } } - - if (searchUiState.isLoading) { - LoadingDialog( - onDismissRequest = { - viewModel.dismissDialog() - } - ) - } } } } @@ -366,26 +367,4 @@ fun SearchOption(text: String, selected: Boolean, onSelected: () -> Unit) { fontSize = 16.sp ) } -} - -@Composable -fun LoadingDialog( - onDismissRequest: () -> Unit -) { - Dialog(onDismissRequest = onDismissRequest) { - Card { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .padding(horizontal = 32.dp, vertical = 16.dp) - ) { - CircularProgressIndicator() - Text( - text = "Loading...", - fontSize = 16.sp - ) - } - } - } } \ No newline at end of file From 083c62f3aee4c5369fbd11658e202749967cc34d Mon Sep 17 00:00:00 2001 From: ashutosh-kumar-kushwaha Date: Mon, 20 May 2024 18:58:27 +0530 Subject: [PATCH 5/5] Fix the TextField moved to below on click issue --- .../java/com/mifos/mifosxdroid/online/search/SearchScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt index 4f4c842ef47..c30771480be 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/online/search/SearchScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -111,7 +112,8 @@ fun SearchScreen( ) } } - } + }, + windowInsets = WindowInsets(0, 0, 0, 0) ) }, snackbarHost = {