Search UI skeleton (#651)
* Add UI skeleton for the search feature This CL adds an UI skeleton of the search feature including: - The search navigation icon in the NiaTopAppBar - The text field to enter the search query - Almost empty ViewModelpull/676/head
parent
f3faec8432
commit
d36fe8df59
@ -0,0 +1 @@
|
|||||||
|
/build
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("nowinandroid.android.feature")
|
||||||
|
id("nowinandroid.android.library.compose")
|
||||||
|
id("nowinandroid.android.library.jacoco")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.google.samples.apps.nowinandroid.feature.search"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.samples.apps.nowinandroid.feature.search
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.assertIsFocused
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
|
import androidx.compose.ui.test.onParent
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI test for checking the correct behaviour of the Search screen.
|
||||||
|
*/
|
||||||
|
class SearchScreenTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
private lateinit var clearSearchText: String
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
composeTestRule.activity.apply {
|
||||||
|
clearSearchText = getString(R.string.clear_search_text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun searchTextField_isFocused() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
SearchScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription(clearSearchText)
|
||||||
|
// The parent of the IconButton whose contentDescription matches the clearSearchText
|
||||||
|
// should be the TextField for search
|
||||||
|
.onParent()
|
||||||
|
.assertIsFocused()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2023 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.samples.apps.nowinandroid.feature.search
|
||||||
|
|
||||||
|
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.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsTopHeight
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
|
||||||
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
|
||||||
|
import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews
|
||||||
|
import com.google.samples.apps.nowinandroid.core.ui.R.string
|
||||||
|
import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.search.R as searchR
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SearchRoute(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
viewModel: SearchViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
SearchScreen(
|
||||||
|
modifier = modifier,
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onSearchQueryChanged = viewModel::onSearchQueryChanged,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SearchScreen(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onBackClick: () -> Unit = {},
|
||||||
|
onSearchQueryChanged: (String) -> Unit = {},
|
||||||
|
) {
|
||||||
|
TrackScreenViewEvent(screenName = "Search")
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing))
|
||||||
|
SearchToolbar(
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onSearchQueryChanged = onSearchQueryChanged,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SearchToolbar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onBackClick: () -> Unit = {},
|
||||||
|
onSearchQueryChanged: (String) -> Unit = {},
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
IconButton(onClick = { onBackClick() }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = NiaIcons.ArrowBack,
|
||||||
|
contentDescription = stringResource(
|
||||||
|
id = string.back,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SearchTextField(onSearchQueryChanged = onSearchQueryChanged)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun SearchTextField(onSearchQueryChanged: (String) -> Unit) {
|
||||||
|
val textState = remember { mutableStateOf("") }
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
TextField(
|
||||||
|
colors = TextFieldDefaults.textFieldColors(
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
disabledIndicatorColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = NiaIcons.Search,
|
||||||
|
contentDescription = stringResource(
|
||||||
|
id = searchR.string.search,
|
||||||
|
),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { textState.value = "" }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = NiaIcons.Close,
|
||||||
|
contentDescription = stringResource(
|
||||||
|
id = searchR.string.clear_search_text,
|
||||||
|
),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onValueChange = {
|
||||||
|
textState.value = it
|
||||||
|
onSearchQueryChanged(it)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp)
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
shape = RoundedCornerShape(32.dp),
|
||||||
|
value = textState.value,
|
||||||
|
)
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun SearchToolbarPreview() {
|
||||||
|
NiaTheme {
|
||||||
|
SearchToolbar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DevicePreviews
|
||||||
|
@Composable
|
||||||
|
private fun SearchScreenPreview() {
|
||||||
|
NiaTheme {
|
||||||
|
SearchScreen()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.samples.apps.nowinandroid.feature.search
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class SearchViewModel @Inject constructor() : ViewModel() {
|
||||||
|
|
||||||
|
fun onSearchQueryChanged(searchQuery: String) {
|
||||||
|
// TODO: Pass it to UseCase
|
||||||
|
println("New search query is '$searchQuery'")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.samples.apps.nowinandroid.feature.search.navigation
|
||||||
|
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.search.SearchRoute
|
||||||
|
|
||||||
|
const val searchRoute = "search_route"
|
||||||
|
|
||||||
|
fun NavController.navigateToSearch(navOptions: NavOptions? = null) {
|
||||||
|
this.navigate(searchRoute, navOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavGraphBuilder.searchScreen(onBackClick: () -> Unit) {
|
||||||
|
// TODO: Handle back stack for each top-level destination. At the moment each top-level
|
||||||
|
// destination may have own search screen's back stack.
|
||||||
|
composable(route = searchRoute) {
|
||||||
|
SearchRoute(onBackClick = onBackClick)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2023 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<string name="search">Search</string>
|
||||||
|
<string name="clear_search_text">Clear search text</string>
|
||||||
|
</resources>
|
Loading…
Reference in new issue