I just learned Jetpack Compose and building a simple login screen with retrofit to connect with the API.
I'm able to navigate from login screen to home screen. But I'm wondering if I'm doing it right.
Here is my login screen composable
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun InsertNumberScreen(
modifier: Modifier = Modifier,
navHostController: NavHostController,
viewModel: LoginViewModel = viewModel(factory = LoginViewModel.provideFactory(
navHostController = navHostController,
owner = LocalSavedStateRegistryOwner.current
)),
) {
var phoneNumber by remember {
mutableStateOf("")
}
var isActive by remember {
mutableStateOf(false)
}
val modalBottomSheetState =
rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val coroutine = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {
BottomSheetLoginContent(phoneNumber){
//Here I call login function inside viewModel
viewModel.login(phoneNumber)
}
},
sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
) {
Column {
TopAppBarCustom(text = "")
LoginText(modifier = modifier.padding(16.dp))
Row(modifier = modifier.padding(16.dp)) {
Prefix()
PhoneNumber(
shape = RoundedCornerShape(topEnd = 16.dp, bottomEnd = 16.dp),
value = phoneNumber,
onValueChange = {
isActive = it.length >= 10
phoneNumber = it
})
}
Spacer(
modifier = Modifier
.fillMaxHeight()
.weight(1f)
)
BottomContainer(isEnabled = isActive) {
coroutine.launch {
if (modalBottomSheetState.isVisible) {
modalBottomSheetState.animateTo(ModalBottomSheetValue.Hidden)
} else {
modalBottomSheetState.animateTo(ModalBottomSheetValue.Expanded)
}
}
}
}
}
}
Here is my ViewModel
class LoginViewModel(val navHostController: NavHostController) : ViewModel() {
var result by mutableStateOf(Data(0, "", Message("", "")))
fun login(phone: String) {
val call: Call<Data> = Network.NetworkInterface.login(phone)
call.enqueue(
object : Callback<Data> {
override fun onResponse(call: Call<Data>, response: Response<Data>) {
if (response.code() == 400) {
val error =
Gson().fromJson(response.errorBody()!!.charStream(), Data::class.java)
result = error
navHostController.navigate("login")
} else {
result = response.body()!!
navHostController.navigate("home")
}
}
override fun onFailure(call: Call<Data>, t: Throwable) {
Log.d("Data Login", t.message.toString())
}
}
)
}
companion object {
fun provideFactory(
navHostController: NavHostController,
owner: SavedStateRegistryOwner,
defaultArgs: Bundle? = null,
): AbstractSavedStateViewModelFactory =
object : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T {
return LoginViewModel(navHostController) as T
}
}
}
}
In my viewModel class, it has a constructor NavHostController. And then, in the login method, I call navHostController.navigate() to navigate to home screen if the login is success.
The question is, is it okay to call navHostController.navigate() directly inside the viewModel? Because I follow codelabs from Google and the navigation is handled in the sort of NavHostBootstrap composable (Something like this)
#Composable
fun RallyNavHost(
navController: NavHostController,
modifier: Modifier = Modifier
){
NavHost(navController = navController, startDestination = Overview.route, modifier = modifier){
composable(Overview.route){
OverviewScreen(
onClickSeeAllAccounts = {
navController.navigateSingleTopTo(Accounts.route)
},
onClickSeeAllBills = {
navController.navigateSingleTopTo(Bills.route)
},
onAccountClick = {
Log.d("Account Clicked", it)
navController.navigateToSingleAccount(it)
}
)
}
}
Related
I have a problem with navigation in my Compose app. So starting from the beginning I want to have two navigation graphs: one for authentication related things and second for main functionalities, which should be accessible only after login. So typical case.
I want to use Splash Screen API from Android 12, so in my main activity I could do something like this:
class AppMainActivity : ComponentActivity() {
private val viewModel by viewModels<SplashViewModel>()
private var userAuthState: UserAuthState = UserAuthState.UNKNOWN
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.onEvent(SplashEvent.CheckAuthentication)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.isAuthenticated.collect {
userAuthState = it
}
}
}
setContent {
CarsLocalizerTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
val scaffoldState = rememberScaffoldState()
val navController = rememberNavController()
Scaffold(
modifier = Modifier.fillMaxSize(),
scaffoldState = scaffoldState
) {
val splash = installSplashScreen()
splash.setKeepOnScreenCondition {
userAuthState != UserAuthState.UNKNOWN
}
when (userAuthState) {
UserAuthState.UNAUTHENTICATED -> {
AuthNavigation(
navController = navController,
scaffoldState = scaffoldState
)
}
UserAuthState.AUTHENTICATED -> {
HomeScreen(
viewModel = hiltViewModel(),
onLogout = { navController.popBackStack() }
)
}
UserAuthState.UNKNOWN -> {}
}
}
}
}
}
}
}
Here I am collecting StateFlow from view model, which describes if user is authenticated already or no. If authenticated successfully then go to HomeScreen, which has HomeNavigation inside. If not authenticated, go to Authentication nav graph. Problem is that this approach would not work, since activity is created only once, so if I user will login, this when
when (userAuthState) {
UserAuthState.UNAUTHENTICATED -> {
AuthNavigation(
navController = navController,
scaffoldState = scaffoldState
)
}
UserAuthState.AUTHENTICATED -> {
HomeScreen(
viewModel = hiltViewModel(),
onLogout = { navController.popBackStack() }
)
}
UserAuthState.UNKNOWN -> {}
}
Won’t be called again.
I was trying to find some solution for my problem but, can’t find anything helpful. Maybe somebody had such issue before, or saw something useful? Will be very glad for any help.
Rest of my code:
SplashViewModel
#HiltViewModel
class SplashViewModel #Inject constructor(
private val authenticateUseCase: AuthenticateUseCase
): ViewModel() {
private val _isAuthenticated = MutableStateFlow<UserAuthState>(value = UserAuthState.UNKNOWN)
val isAuthenticated: StateFlow<UserAuthState> = _isAuthenticated.asStateFlow()
fun onEvent(event: SplashEvent) {
when (event) {
is SplashEvent.CheckAuthentication -> {
viewModelScope.launch {
val result = authenticateUseCase()
when (result) {
true -> {
_isAuthenticated.emit(UserAuthState.AUTHENTICATED)
}
false -> {
_isAuthenticated.emit(UserAuthState.UNAUTHENTICATED)
}
}
}
}
}
}
}
AuthNavigation
#Composable
fun AuthNavigation(
navController: NavHostController,
scaffoldState: ScaffoldState
) {
NavHost(
navController = navController,
startDestination = Screen.Login.route,
modifier = Modifier.fillMaxSize()
) {
composable(Screen.Login.route) {
LoginScreen(
onNavigate = { navController.navigate(it) } ,
onLogin = {
navController.popBackStack()
},
scaffoldState = scaffoldState,
viewModel = hiltViewModel()
)
}
composable(Screen.Register.route) {
RegisterScreen(
onPopBackstack = { navController.popBackStack() },
scaffoldState = scaffoldState,
viewModel = hiltViewModel()
)
}
composable(Screen.Onboarding.route) {
OnboardingScreen(
onCompleted = { navController.popBackStack() },
viewModel = hiltViewModel()
)
}
}
}
HomeScreen
#Composable
fun HomeScreen(
viewModel: HomeViewModel,
onLogout: () -> Unit
) {
val navController = rememberNavController()
val scaffoldState = rememberScaffoldState()
var appBarTitle by remember {
mutableStateOf("")
}
LaunchedEffect(key1 = true) {
viewModel.userName.collectLatest {
appBarTitle = "Hello $it"
}
}
Scaffold(
scaffoldState = scaffoldState,
topBar = {
if (appBarTitle.isEmpty()) {
AppBarWithName("Hello")
} else {
AppBarWithName(appBarTitle)
}
},
bottomBar = { BottomNavigationBar(navController) },
content = { padding ->
Box(modifier = Modifier.padding(padding)) {
HomeNavigation(
navController = navController,
scaffoldState = scaffoldState,
onLogout = { onLogout() }
)
}
}
)
}
HomeNavigation
#Composable
fun HomeNavigation(
navController: NavHostController,
scaffoldState: ScaffoldState,
onLogout: () -> Unit
) {
NavHost(
navController = navController,
startDestination = Screen.Map.route
) {
composable(Screen.Map.route) {
MapScreen(viewModel = hiltViewModel())
}
composable(Screen.ManageCars.route) {
ManageCarsScreen(
viewModel = hiltViewModel(),
scaffoldState = scaffoldState,
onAddCar = {
navController.navigate(Screen.AddCar.route)
}
)
}
composable(Screen.AddCar.route) {
AddCarScreen(
viewModel = hiltViewModel(),
onPopBackstack = {
navController.popBackStack()
}
)
}
composable(Screen.Logout.route) {
LogoutDialogScreen(
viewModel = hiltViewModel(),
onLogout = {
navController.popBackStack()
onLogout()
},
onCancel = {
navController.popBackStack()
}
)
}
}
}
You need to store the UserAuthState as MutableState, this way, when the value is updated, setContent will automatically re-compose:
private var userAuthState = mutableStateOf(UserAuthState.UNKNOWN)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.isAuthenticated.collect { userAuthState.value = it }
}
}
setContent {
when (userAuthState.value) {
etc....
}
}
From the docs:
mutableStateOf creates an observable MutableState, which is an
observable type integrated with the compose runtime.
Any changes to value schedules recomposition of any composable
functions that read value.
Recently in my app I've been using simple navigation component arguments passing. Since I've added Hilt ViewModel, i came across something called saveStateHandle and apparently I can pass arguments between screens easly with this. How can i do that? I implemented the code in my HiltViewModel
#HiltViewModel
class ExerciseViewModel #Inject constructor(
private val repository: ExerciseRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
val data: MutableState<DataOrException<List<Exercise>, Boolean, Exception>> =
mutableStateOf(
DataOrException(null, true, Exception(""))
)
val muscleGroup: String? = savedStateHandle[Constants.MUSCLE_GROUP_KEY]
private val _exerciseListFromDb = MutableStateFlow<List<Exercise>>(emptyList())
val exerciseListFromDb = _exerciseListFromDb.asStateFlow()
init {
getExercises()
viewModelScope.launch(Dispatchers.IO) {
repository.getAllExercisesFromDb().collect() {
_exerciseListFromDb.value = it
}
}
}
private fun getExercises() {
viewModelScope.launch {
data.value.loading = true
data.value = repository.getExercises()
if (data.value.data.toString().isNotEmpty())
data.value.loading = false
}
}
fun insertExerciseToDb(exercise: Exercise) = viewModelScope.launch {
repository.insertExerciseToDb(exercise)
}
fun deleteExerciseFromDb(exercise: Exercise) = viewModelScope.launch {
repository.deleteExerciseFromDb(exercise)
}
}
I want to pass muscleGroup parameter between screens HomeScreen -> SampleExercisesScreen. How do I send parameter from HomeScreen to HiltViewModel ExerciseViewModel and then use it in SampleExercisesScreen and other screens?
#Composable
fun HomeScreen(navController: NavController) {
Surface(modifier = Modifier.fillMaxSize(),
color = AppColors.mBackground) {
Column {
Header()
Row(modifier = Modifier
.fillMaxWidth()
.padding(top = 50.dp)){
MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.body, muscleGroup = "Chest", navController)
MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.male, muscleGroup = "Back", navController)
MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.shoulder, muscleGroup = "Shoulders", navController)
}
Row(modifier = Modifier.fillMaxWidth()){
MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.muscle, muscleGroup = "Biceps", navController)
MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.triceps, muscleGroup = "Triceps", navController)
MuscleButton(modifier = Modifier.weight(1f), icon = R.drawable.leg, muscleGroup = "Legs", navController)
}
}
}
},
#Composable
fun SampleExerciseScreen(navController: NavController, muscleGroup: String, exerciseList: List<Exercise>?) {
val mExerciseList = exerciseList!!.filter { it.muscle == muscleGroup }
Log.d("TEST", "$mExerciseList, $muscleGroup")
Surface(modifier = Modifier.fillMaxSize(),
color = AppColors.mBackground) {
Column {
MyTopBar(navController = navController)
LazyColumn(Modifier.weight(1f)){
items(mExerciseList) {
ExerciseRow(exercise = it)
}
}
GoToButton(navController = navController, text = "YOUR EXERCISES", route = Screen.UserExercises.passMuscleGroup(muscleGroup))
}
}
}
NavGraph
#Composable
fun SetupNavGraph(navController: NavHostController, viewModel: ExerciseViewModel) {
val exerciseList = viewModel.data.value.data?.toList()
val exerciseListFromDb = viewModel.exerciseListFromDb.collectAsState().value
val muscleGroup = viewModel.muscleGroup
NavHost(navController = navController, startDestination = Screen.Home.route) {
composable(
route = Screen.Home.route
) {
HomeScreen(navController = navController)
}
composable(
route = Screen.SampleExercise.route,
) {
SampleExerciseScreen(
navController = navController,
muscleGroup = muscleGroup.toString(),
exerciseList = exerciseList
)
}
composable(
route = Screen.UserExercises.route,
arguments = listOf(navArgument(MUSCLE_GROUP_KEY) {
type = NavType.StringType
})
) {
UserExercisesScreen(
navController = navController,
muscleGroup = it.arguments?.getString(MUSCLE_GROUP_KEY).toString(),
viewModel = viewModel,
exerciseListFromDb = exerciseListFromDb
)
}
composable(
route = Screen.Add.route,
arguments = listOf(navArgument(MUSCLE_GROUP_KEY) {
type = NavType.StringType
})
) {
AddScreen(
navController = navController, muscleGroup = it.arguments?.getString(MUSCLE_GROUP_KEY).toString(),
viewModel = viewModel
)
}
}
}
I have a blog post here that explains it: https://www.francescvilarino.com/passing-arguments-to-screens-in-jetpack-compose
But basically you have to retrieve the arguments in the viewmodel from the SavedStateHandle, using the key that you used in your route for each argument.
navController.navigate(buildTwoRoute(argument))
then
#HiltViewModel
class TwoViewModel #Inject constructor(
savedStateHandle: SavedStateHandle,
) : ViewModel() {
init {
val argument = savedStateHandle.get<String>(DestinationOneArg).orEmpty()
}
}
So for this project, I am setting up a Login Screen that logs into a database that is suppose to validate the email. It's my first time actually doing something like this. I have made a login page with firebase. But this time im tring to login to an Azure DB. Here are some code for better clarification
Here's the API
#Singleton
interface MMRApi {
#FormUrlEncoded
#POST(LOGIN_EP)
suspend fun sendLoginInfo(
#Field("EmailId") emailId: String,
#Field("MobileMakeModel") mobileMake: String
): Response<LoginCreds>
}
Here's the Request and Response Fields
//Request Data Class
data class LoginCreds(
#SerializedName("EmailId") val emailId: String,
#SerializedName("MobileMakeModel") var mobileModel: String
){
companion object{
const val PLATFORM = "Android"
}
private fun getModel(): String{
return "$PLATFORM ${Build.MODEL}".also { mobileModel = it }
}
}
//Response Data Class
data class LoginResponse(
#SerializedName("data") val data: List<DataInfo>,
#SerializedName("status") val status: Int,
#SerializedName("message") val message: String
){
override fun toString(): String {
return "\n" +
"List of Data: $data" +
"Status: $status" +
"Message:$message"
}
}
data class DataInfo(
#SerializedName("Id") val id: Int,
#SerializedName("CustomerId") val customerId: String,
#SerializedName("UserAttributeId") val userAttributeId: Int,
#SerializedName("Enabled") val enabled: Boolean
){
override fun toString(): String {
return "\n" +
"Id: $id" +
"CustomerId: $customerId" +
"UserAttributeId: $userAttributeId" +
"Enabled: $enabled"
}
}
The Repository
class MMRRepository #Inject constructor(private val api: MMRApi) {
suspend fun postLogin(emailId: String, mobileMake: String): Response<LoginCreds>{
return api.sendLoginInfo(emailId, mobileMake)
}
}
The ViewModel
#HiltViewModel
class LoginViewModel #Inject constructor(private val repository: MMRRepository)
: ViewModel() {
val loginPostResponse: MutableLiveData<Response<LoginCreds>> = MutableLiveData()
fun pushLogin(emailId: String, mobileMake: String){
viewModelScope.launch(Dispatchers.IO) {
val response = repository.postLogin(emailId, mobileMake)
loginPostResponse.postValue(response)
}
}
}
The Composable
#Composable
fun MMRLoginScreen(navController: NavController, loginViewModel: LoginViewModel = hiltViewModel()){
var emailId by rememberSaveable { mutableStateOf("") }
Surface(modifier = Modifier
.fillMaxSize()
.padding(top = 65.dp)
) {
Column(
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(id = R.drawable.parabit_logo_orange_blue),
contentDescription = stringResource(R.string.company_logo_string),
contentScale = ContentScale.Fit,
modifier = Modifier
.width(225.dp)
.height(95.dp)
.padding(top = 43.dp)
)
Text(
text = "MMR Bluetooth Access Control",
color = Color(0xFFF47B20),
fontWeight = Bold,
fontSize = 18.sp,
)
Spacer(modifier = Modifier.height(10.dp))
LoginField(loading = false){
loginViewModel.pushLogin(emailId = it, mobileMake = Build.MODEL)
navController.navigate(MMRScreens.MainScreen.name)
}
}
}
}
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun LoginField(
loading: Boolean = false,
onDone: (String) -> Unit = {email ->}
){
val email = rememberSaveable { mutableStateOf("") }
val keyboardController = LocalSoftwareKeyboardController.current
val valid = remember(email.value){
email.value.trim().isNotEmpty()
}
Column(
modifier = Modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
EmailInput(
emailState = email,
)
SubmitButton(
textId = "Login",
loading = loading,
validInputs = valid
){
onDone(email.value.trim())
keyboardController?.hide()
}
}
}
#Composable
fun SubmitButton(
textId: String,
loading: Boolean,
validInputs: Boolean,
onClick: () -> Unit
) {
Button(
onClick = onClick,
modifier = Modifier
.padding(3.dp)
.fillMaxWidth(),
enabled = !loading && validInputs,
colors = ButtonDefaults.buttonColors(Color(0xFFF47B20))
) {
if (loading) CircularProgressIndicator(modifier = Modifier.size(25.dp))
else Text(text = textId, modifier = Modifier.padding(5.dp))
}
}
Any help is appreciated. Thank you.
So it was a simple. First i had to set up the API to send the two fields I need in order to get a response.
#Singleton
interface MMRApi {
#FormUrlEncoded
#POST(LOGIN_EP)
suspend fun sendLoginInfo(
#Field("EmailId") emailId: String,
#Field("MobileMakeModel") mobileMake: String
): LoginResponse
}
The LoginResponse data class is used to store the response of the API once the input field goes through. Then for the instance, You set it up where the instance is used to tell you if the email worked.
#Module
#InstallIn(SingletonComponent::class)
class AppModule {
#Provides
#Singleton
fun provideLoginApi(): MMRApi{
return Retrofit.Builder()
.baseUrl(BASE_URL)
//Used to pick up the responses for the API Calls
.client(
OkHttpClient.Builder().also { client ->
if (BuildConfig.DEBUG) {
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
client.addInterceptor(logging)
}
}.connectTimeout(100, TimeUnit.SECONDS)
.readTimeout(100, TimeUnit.SECONDS)
.build()
)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(MMRApi::class.java)
}
}
Everything else was standard and I was able to get the Login Authentication to work
I have a splash screen that I show when the app launch. I'm using SplashScreenAPI for the splash screen. I'm also using accompanist navigation library to navigate with animations. After I updated the accompanist version from 0.24.7-alpha to 0.24.8-beta I've encountered an issue. The issue is :
As you can see after the splash screen is shown there is a blank screen for a sec, then it navigates to the start destination.
Here is also the behavior for version 0.24.7-alpha:
In case of need here is also the code:
#OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
#AndroidEntryPoint
class AuthActivity : ComponentActivity() {
private val splashViewModel: SplashViewModel by viewModels()
private lateinit var splashScreen: SplashScreen
override fun onCreate(savedInstanceState: Bundle?) {
splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
splashScreen.setKeepOnScreenCondition {
splashViewModel.isLoading.value
}
splashScreen.setOnExitAnimationListener {
val startDestination = splashViewModel.navDestination.value.route
if (startDestination == AUTH_GRAPH) {
it.remove()
} else {
val intent = Intent(this#AuthActivity, MainActivity::class.java)
startActivity(intent)
finish()
}
}
setContent {
val errorMessage by splashViewModel.errorFlow.collectAsState()
val widthSizeClass = calculateWindowSizeClass(this).widthSizeClass
ProvideLocalWindowWidthSizeClass(widthSizeClass = widthSizeClass) {
RubiBrandsTheme {
RubiBrandsBackground {
errorMessage?.asString(this#AuthActivity)?.let {
ErrorDialog(message = it)
}
AuthNavGraph(
navController = rememberAnimatedNavController(),
activity = this#AuthActivity
)
}
}
}
}
}
}
I just check the authentication and then navigate the user based on success or not.
Here is my MainActivity :
#[AndroidEntryPoint OptIn(ExperimentalMaterial3WindowSizeClassApi::class)]
class MainActivity : ComponentActivity(), AuthenticationManager {
private val mViewModel by viewModels<MainActivityViewModel>()
#Inject
lateinit var authenticationMediator: AuthenticationMediator
private lateinit var navController: NavHostController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
authenticationMediator.registerAuthenticationManager(this)
setContent {
val widthSizeClass = calculateWindowSizeClass(this).widthSizeClass
val showUnauthorizedDialog = mViewModel.showUnauthorizedDialog
ProvideLocalWindowWidthSizeClass(widthSizeClass = widthSizeClass) {
RubiBrandsTheme {
RubiBrandsBackground {
navController = rememberAnimatedNavController()
val bottomSheetNavigator =
rememberBottomSheetNavigator()
val scaffoldState = rememberScaffoldState()
navController.navigatorProvider += bottomSheetNavigator
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val rubiBrandsTopLevelNavigation = remember(navController) {
RubiBrandsTopLevelNavigation(navController)
}
val topLevelDestinations = listOf(
TopLevelDestination.DashboardDestination,
TopLevelDestination.OrdersDestination,
TopLevelDestination.ProductsDestination,
TopLevelDestination.AccountDestination
)
val topLevelDestinationRoutes =
topLevelDestinations.map(TopLevelDestination::route)
val isNavBarVisible =
navBackStackEntry?.destination?.route in topLevelDestinationRoutes
ModalBottomSheetLayout(
modifier = Modifier
.systemBarsPadding()
.imePadding(),
bottomSheetNavigator = bottomSheetNavigator,
sheetElevation = dimensionResource(id = R.dimen.dimen_16),
sheetShape = RoundedCornerShape(
topStart = dimensionResource(id = R.dimen.dimen_16),
topEnd = dimensionResource(
id = R.dimen.dimen_16
)
),
sheetBackgroundColor = RubiBrandsTheme.colors.filterBottomSheetBackgroundColor
) {
RubiBrandsScaffold(
modifier = Modifier
.systemBarsPadding()
.imePadding(),
bottomBar = {
if (isNavBarVisible) {
RubiBrandsBottomNavigationView(
onNavigateToTopLevelDestination = rubiBrandsTopLevelNavigation::navigateTo,
currentDestination = currentDestination,
topLevelDestinations = topLevelDestinations
)
}
},
scaffoldState = scaffoldState
) {
//navigation graph
Box(
modifier = Modifier
.padding(it)
) {
MainNavGraph(
navController = navController,
mainActivity = this#MainActivity,
mainActivityViewModel = mViewModel,
)
showUnauthorizedDialog?.getContentIfNotHandled()?.let { show ->
if (show) {
RubiBrandsUnauthorizedScreen {
logClickEvent(ItemNames.UNAUTHORIZED_DIALOG_OKAY_BUTTON)
logout()
}
}
}
}
}
}
}
}
}
}
}
#Composable
fun MainNavGraph(
navController: NavHostController,
modifier: Modifier = Modifier,
) {
AnimatedNavHost(
navController = navController,
startDestination = MAIN_GRAPH,
) {
//main nav graph
mainNavGraph(navController)
}
fun NavGraphBuilder.mainNavGraph(
navController: NavController
) {
navigation(
startDestination = TopLevelDestination.DashboardDestination.route,
route = MAIN_GRAPH,
) {
addDashboardScreen(navController)
}
}
Here is also compose dependencies and the versions :
object Compose : Library {
object Version {
const val COMPOSE_VERSION = "1.2.0"
const val COMPOSE_ACTIVITY_VERSION = "1.5.1"
const val COMPOSE_CONSTRAINT_LAYOUT_VERSION = "1.0.1"
const val HILT_NAVIGATION_COMPOSE_VERSION = "1.0.0"
}
const val COMPOSE_UI = "androidx.compose.ui:ui:$COMPOSE_VERSION"
const val COMPOSE_MATERIAL = "androidx.compose.material:material:$COMPOSE_VERSION"
const val COMPOSE_UI_TOOLING_PREV =
"androidx.compose.ui:ui-tooling-preview:$COMPOSE_VERSION"
const val COMPOSE_ACTIVITY = "androidx.activity:activity-compose:$COMPOSE_ACTIVITY_VERSION"
const val COMPOSE_UI_TOOLING = "androidx.compose.ui:ui-tooling:$COMPOSE_VERSION"
const val COMPOSE_UI_TEST_MANIFEST = "androidx.compose.ui:ui-test-manifest:$COMPOSE_VERSION"
const val COMPOSE_CONSTRAINT_LAYOUT =
"androidx.constraintlayout:constraintlayout-compose:$COMPOSE_CONSTRAINT_LAYOUT_VERSION"
const val HILT_NAVIGATION_COMPOSE =
"androidx.hilt:hilt-navigation-compose:$HILT_NAVIGATION_COMPOSE_VERSION"
const val COMPOSE_LIVE_DATA = "androidx.compose.runtime:runtime-livedata:$COMPOSE_VERSION"
override val components: List<String>
get() = listOf(
COMPOSE_UI,
COMPOSE_MATERIAL,
COMPOSE_UI_TOOLING_PREV,
COMPOSE_ACTIVITY,
)
}
I know that my compose version should be compatible with the accompanist version. As per this I should set the accompanist version to 0.25.1 but because of this issue, I don't update the animation version.
I am building a simple app following Mitch Tabian's youtube tutorial about Jetpack Compose.
In the State Hoisting video, he extracts the code for the search TextField into a separate Composable. When I do so, my textField doesn't update the value and I can't find what I am doing wrong.
SearchAppBar Composable
#Composable
fun SearchAppBar(
query: String,
onQueryChanged: (String) -> Unit,
onExecuteSearch: () -> Unit,
selectedCategory: FoodCategory?,
onSelectedCategoryChanged: (String) -> Unit
) {
Surface(
modifier = Modifier
.fillMaxWidth(),
color = Color.White,
elevation = 4.dp
) {
Column {
Row(modifier = Modifier.fillMaxWidth()) {
val focusManager = LocalFocusManager.current
OutlinedTextField(
value = query,
onValueChange = { newValue -> onQueryChanged(newValue) },
modifier = Modifier
.background(color = MaterialTheme.colors.surface)
.fillMaxWidth()
.padding(8.dp),
label = {
Text(text = "Search")
},
...
Fragment
class RecipeListFragment : Fragment() {
private val viewModel: RecipeListViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setContent {
val recipes = viewModel.recipes.value
val query = viewModel.query.value
val selectedCategory = viewModel.selectedCategory.value
Column {
SearchAppBar(
query = query,
onQueryChanged = { viewModel.onQueryChanged(query) },
onExecuteSearch = { viewModel::newSearch },
selectedCategory = selectedCategory,
onSelectedCategoryChanged = { viewModel::onSelectedCategoryChanged })
LazyColumn {
itemsIndexed(items = recipes) { index, recipe ->
RecipeCard(recipe = recipe, onClick = { })
}
}
}
}
}
}
}
ViewModel
class RecipeListViewModel #Inject constructor(private val repository: RecipeRepository, #Named("auth_token") private val token: String) : ViewModel() {
val recipes: MutableState<List<Recipe>> = mutableStateOf(listOf())
val query = mutableStateOf("")
val selectedCategory: MutableState<FoodCategory?> = mutableStateOf(null)
init {
newSearch()
}
fun onQueryChanged(query: String) {
this.query.value = query
}
fun newSearch() {
viewModelScope.launch {
recipes.value = repository.search(token = token, page = 1, query = query.value)
}
}
fun onSelectedCategoryChanged(category: String) {
val newCategory = getFoodCategory(category)
selectedCategory.value = newCategory
onQueryChanged(category)
}
}
The following are no longer states that are observed.
val recipes = viewModel.recipes.value
val query = viewModel.query.value
val selectedCategory = viewModel.selectedCategory.value
Delay the .value call or use the var by viewModel.recipes or use restructuring val (recipes, _) = viewModel.recipes