Complete migration to Compose (#194)
* Migrate disclaimer screen to Compose * Migrate base URL screen to Compose * Migrate base URL screen to Compose * Migrate authentication screen to Compose * Initialize add recipe screen * Remove unused resources * Display add recipe operation result * Add delete icon to ingredients and instructions * Allow navigating between fields on add recipe * Allow navigating between fields on authentication screen * Allow to proceed from keyboard on base url screen * Use material icons for recipe item * Expose base URL as flow * Initialize Compose navigation * Allow sending logs again * Allow to override navigation and top bar per screen * Add additional logs * Migrate share recipe screen to Compose * Fix unit tests * Restore recipe list tests * Ensure authentication is shown after URL input * Add autofill to authentication * Complete first set up test * Use image vector from Icons instead of drawable * Add transition animations * Fix logging host in Host header * Do not fail test if login token is used
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
package gq.kirmanak.mealient.ui
|
||||
|
||||
data class ActivityUiState(
|
||||
val isAuthorized: Boolean = false,
|
||||
val navigationVisible: Boolean = false,
|
||||
val searchVisible: Boolean = false,
|
||||
val checkedMenuItem: CheckableMenuItem? = null,
|
||||
) {
|
||||
val canShowLogin: Boolean get() = !isAuthorized
|
||||
|
||||
val canShowLogout: Boolean get() = isAuthorized
|
||||
}
|
||||
|
||||
enum class CheckableMenuItem {
|
||||
ShoppingLists,
|
||||
RecipesList,
|
||||
AddRecipe,
|
||||
ChangeUrl,
|
||||
Login
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package gq.kirmanak.mealient.ui
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface ActivityUiStateController {
|
||||
|
||||
fun updateUiState(update: (ActivityUiState) -> ActivityUiState)
|
||||
|
||||
fun getUiState(): ActivityUiState
|
||||
|
||||
fun getUiStateFlow(): StateFlow<ActivityUiState>
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package gq.kirmanak.mealient.ui
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class ActivityUiStateControllerImpl @Inject constructor() : ActivityUiStateController {
|
||||
private val uiState = MutableStateFlow(ActivityUiState())
|
||||
|
||||
override fun updateUiState(update: (ActivityUiState) -> ActivityUiState) {
|
||||
uiState.getAndUpdate(update)
|
||||
}
|
||||
|
||||
override fun getUiState(): ActivityUiState = uiState.value
|
||||
|
||||
override fun getUiStateFlow(): StateFlow<ActivityUiState> = uiState.asStateFlow()
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package gq.kirmanak.mealient.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
abstract class BaseComposeFragment : Fragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var logger: Logger
|
||||
|
||||
final override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
logger.v { "onCreateView() called" }
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
AppTheme {
|
||||
Screen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
abstract fun Screen()
|
||||
}
|
||||
@@ -30,8 +30,7 @@ sealed class OperationUiState<T> {
|
||||
class Initial<T> : OperationUiState<T>() {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return true
|
||||
return javaClass == other?.javaClass
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@@ -42,8 +41,7 @@ sealed class OperationUiState<T> {
|
||||
class Progress<T> : OperationUiState<T>() {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
return true
|
||||
return javaClass == other?.javaClass
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
||||
@@ -45,4 +45,5 @@ object Dimens {
|
||||
val Medium = 16.dp
|
||||
|
||||
val Large = 24.dp
|
||||
|
||||
}
|
||||
@@ -11,9 +11,6 @@ import gq.kirmanak.mealient.ui.util.LoadingHelperFactoryImpl
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface UiModule {
|
||||
|
||||
@Binds
|
||||
fun bindActivityUiStateController(impl: ActivityUiStateControllerImpl): ActivityUiStateController
|
||||
|
||||
@Binds
|
||||
fun bindLoadingHelperFactory(impl: LoadingHelperFactoryImpl): LoadingHelperFactory
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package gq.kirmanak.mealient.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import gq.kirmanak.mealient.ui.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun BaseScreen(
|
||||
topAppBar: @Composable () -> Unit = { },
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
content: @Composable (Modifier) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { topAppBar() },
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) { paddingValues ->
|
||||
content(
|
||||
Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun BaseScreenWithNavigation(
|
||||
baseScreenState: BaseScreenState,
|
||||
drawerState: DrawerState = rememberDrawerState(initialValue = DrawerValue.Closed),
|
||||
topAppBar: @Composable () -> Unit = { NavigationTopAppBar(drawerState) },
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
content: @Composable (Modifier) -> Unit,
|
||||
) {
|
||||
ModalNavigationDrawer(
|
||||
drawerContent = {
|
||||
DrawerContent(
|
||||
drawerState = drawerState,
|
||||
drawerItems = baseScreenState.drawerItems,
|
||||
)
|
||||
},
|
||||
drawerState = drawerState,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { topAppBar() },
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) { paddingValues ->
|
||||
content(
|
||||
Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BaseScreenState internal constructor(
|
||||
drawerItems: List<DrawerItem>,
|
||||
) {
|
||||
|
||||
val drawerItems by mutableStateOf(drawerItems)
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberBaseScreenState(
|
||||
drawerItems: List<DrawerItem>,
|
||||
): BaseScreenState {
|
||||
return remember {
|
||||
BaseScreenState(
|
||||
drawerItems = drawerItems,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun previewBaseScreenState(): BaseScreenState {
|
||||
return rememberBaseScreenState(
|
||||
drawerItems = emptyList(),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun NavigationTopAppBar(
|
||||
drawerState: DrawerState,
|
||||
) {
|
||||
TopAppBar(
|
||||
title = { },
|
||||
navigationIcon = {
|
||||
OpenDrawerIconButton(
|
||||
drawerState = drawerState,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OpenDrawerIconButton(
|
||||
drawerState: DrawerState,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.semantics { testTag = "open-drawer-button" },
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
drawerState.open()
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = stringResource(R.string.view_toolbar_navigation_icon_content_description),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package gq.kirmanak.mealient.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import gq.kirmanak.mealient.ui.Dimens
|
||||
import gq.kirmanak.mealient.ui.R
|
||||
|
||||
interface DrawerItem {
|
||||
|
||||
@Composable
|
||||
fun getName(): String
|
||||
|
||||
val icon: ImageVector
|
||||
|
||||
@Composable
|
||||
fun isSelected(): Boolean
|
||||
|
||||
val onClick: (DrawerState) -> Unit
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun DrawerContent(
|
||||
drawerState: DrawerState,
|
||||
drawerItems: List<DrawerItem>,
|
||||
) {
|
||||
ModalDrawerSheet {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(Dimens.Medium),
|
||||
text = stringResource(id = R.string.menu_navigation_drawer_header),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
|
||||
drawerItems.forEach { item ->
|
||||
NavigationDrawerItem(
|
||||
name = item.getName(),
|
||||
selected = item.isSelected(),
|
||||
icon = item.icon,
|
||||
onClick = { item.onClick(drawerState) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NavigationDrawerItem(
|
||||
name: String,
|
||||
selected: Boolean,
|
||||
icon: ImageVector,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
androidx.compose.material3.NavigationDrawerItem(
|
||||
modifier = modifier
|
||||
.padding(horizontal = Dimens.Medium),
|
||||
label = {
|
||||
Text(
|
||||
text = name,
|
||||
)
|
||||
},
|
||||
selected = selected,
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package gq.kirmanak.mealient.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
|
||||
@Composable
|
||||
fun TopProgressIndicator(
|
||||
isLoading: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable BoxScope.() -> Unit = {},
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier,
|
||||
) {
|
||||
if (isLoading) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.TopCenter)
|
||||
.semantics { testTag = "progress-indicator" },
|
||||
)
|
||||
}
|
||||
|
||||
content()
|
||||
}
|
||||
}
|
||||
6
ui/src/main/res/values/strings.xml
Normal file
6
ui/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">Mealient</string>
|
||||
<string name="menu_navigation_drawer_header" translatable="false">@string/app_name</string>
|
||||
<string name="view_toolbar_navigation_icon_content_description">Open navigation drawer</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user