I am trying to create a livedata with firebase but for some reason I recieve an error while using this statement "_loginFlow.value = result" - it says: "Required: FirebaseUser>?, Found: FirebaseUser>
What would cause this ? Any help is appreciated!
My code:
private val _loginFlow = MutableStateFlow<Resource<FirebaseUser>?>(null)
val loginFlow: StateFlow<Resource<FirebaseUser>?> = _loginFlow
fun loginUser(email: String, password: String) = viewModelScope.launch {
_loginFlow.value = Resource.Loading
val result = repository.login(email, password)
_loginFlow.value = result
}
#Composable
fun LoginButton(onClick: () -> Unit) {
// LOGIN BUTTON
Spacer(modifier = Modifier.height(20.dp))
Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
Button(
onClick = {
viewModel?.loginUser(email, password)
},
shape = RoundedCornerShape(50.dp),
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
) {
Text(text = "Login")
}
}
AuthRepository:
interface AuthRepository {
val currentUser: FirebaseUser?
suspend fun login(email: String, password: String): Resource<FirebaseUser>
suspend fun signup(name: String, email: String, password: String): Resource<FirebaseUser>
fun logout()
}
Resource:
sealed class Resource<out R> {
data class Success<out R>(val result: R) : Resource<R>()
data class Failure(val exception: Exception) : Resource<Nothing>()
object Loading : Resource<Nothing>()
}
First of all you don't need to add '?' in MutableStateFlow<Resource<FirebaseUser>?>, as Resource should never be null, it's always one of the values you defined inside it. So just remove it and then check.
Related
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)
}
)
}
}
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 am struggling to understand what is the best way to get this to work.
I have some input fields and I created a TextFieldState to keep all the state in one place.
But it is not triggering a re-composition of the composable so the state never updates.
I saw this stack overflow answer on a similar question, but I just find it confusing and it doesn't make sense to me
Here is the code:
The Composable:
#Composable
fun AddTrip (
addTripVm: AddTripVm = hiltViewModel()
) {
var name = addTripVm.getNameState()
var stateTest = addTripVm.getStateTest()
Column(
//verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
) {
Text(text = "Add Trip")
Column(
){
println("From Composable: ${name.value.value}") //No Recomposition
meTextField(
value = name.value.value,
onChange = {
addTripVm.updateName(it)
},
placeholder = "Name",
)
}
View Model code:
#HiltViewModel
class AddTripVm #Inject constructor(
private val tripRepository: TripRepositoryContract,
private val tripValidator: TripValidatorContract
): TripValidatorContract by tripValidator, ViewModel() {
/**
* Name of the trip, this is required
*/
private val nameState: MutableState<TextFieldState> = mutableStateOf(TextFieldState())
private var stateTest = mutableStateOf("");
fun updateStateTest(newValue: String) {
stateTest.value = newValue
}
fun getStateTest(): MutableState<String> {
return stateTest
}
fun getNameState(): MutableState<TextFieldState> {
return nameState;
}
fun updateName(name: String) {
println("From ViewModel? $name")
nameState.value.value = name
println("From ViewModel after update: ${nameState.value.value}") //Updates perfectly
}
}
Text field state:
data class TextFieldState(
var value: String = "",
var isValid: Boolean? = null,
var errorMessage: String? = null
)
Is this possible? Or do I need to separate the value as a string and keep the state separate for if its valid or not?
You don't change instance of nameState's value with
nameState.value.value = name
It's the same object which State checks by default with
fun <T> structuralEqualityPolicy(): SnapshotMutationPolicy<T> =
StructuralEqualityPolicy as SnapshotMutationPolicy<T>
private object StructuralEqualityPolicy : SnapshotMutationPolicy<Any?> {
override fun equivalent(a: Any?, b: Any?) = a == b
override fun toString() = "StructuralEqualityPolicy"
}
MutableState use this as
fun <T> mutableStateOf(
value: T,
policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)
Easiest way is to set
nameState.value = nameState.value.copy(value= name)
other option is to write your own SnapshotMutationPolicy
The following code is from the official Advanced State in Jetpack Compose Codelab.
In the function fun DetailsScreen(), uiState.isLoading -> {...} will be fired when isLoading is true.
I searched all the code in the project, I can only find the code val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) to pass value to isLoading.
Will uiState.isLoading -> {...} always be fired in the project?
data class DetailsUiState(
val cityDetails: ExploreModel? = null,
val isLoading: Boolean = false,
val throwError: Boolean = false
)
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
when {
uiState.cityDetails != null -> {
DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
}
uiState.isLoading -> {
Box(modifier.fillMaxSize()) {
CircularProgressIndicator(
color = MaterialTheme.colors.onSurface,
modifier = Modifier.align(Alignment.Center)
)
}
}
else -> { onErrorLoading() }
}
}
#HiltViewModel
class DetailsViewModel #Inject constructor(
private val destinationsRepository: DestinationsRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val cityName = savedStateHandle.get<String>(KEY_ARG_DETAILS_CITY_NAME)!!
val cityDetails: Result<ExploreModel>
get() {
val destination = destinationsRepository.getDestination(cityName)
return if (destination != null) {
Result.Success(destination)
} else {
Result.Error(IllegalArgumentException("City doesn't exist"))
}
}
}
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
No
The default value of isLoading is false:
val isLoading: Boolean = false,
So the constructor calls that don't explicitly set it to true (DetailsUiState(throwError = true) and DetailsUiState(throwError = true)) will result in it being false.
I'm learning Compose by the article.
A stateless composable is a composable that doesn't hold any state. An easy way to achieve stateless is by using state hoisting, so I replace Code B with Code A, it's great!
The article tell me:
By hoisting the state out of HelloContent, it's easier to reason about the composable, reuse it in different situations, and test. HelloContent is decoupled from how its state is stored. Decoupling means that if you modify or replace HelloScreen, you don't have to change how HelloContent is implemented.
So I write Code C, it stores the value of name in a SharedPreferences, I think that Code C is just like Code A, but in fact, I can't input any letter with Code C, what wrong with the Code C ?
Code A
#Composable
fun HelloScreen() {
var name by rememberSaveable { mutableStateOf("") }
HelloContent(name = name, onNameChange = { name = it })
}
#Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}
Code B
#Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var name by remember { mutableStateOf("") }
if (name.isNotEmpty()) {
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
}
}
Code C
#Composable
fun HelloScreen() {
var name: String by PreferenceTool( LocalContext.current ,"zipCode", "World")
HelloContent(name = name, onNameChange = { name = it })
}
#Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}
class PreferenceTool<T>(
private val context: Context,
private val name: String,
private val default: T
) {
private val prefs: SharedPreferences by lazy {
PreferenceManager.getDefaultSharedPreferences(context)
}
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = findPreference(name, default)
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
putPreference(name, value)
}
#Suppress("UNCHECKED_CAST")
private fun findPreference(name: String, default: T): T = with(prefs) {
val res: Any = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default) ?: default
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
else -> throw IllegalArgumentException("This type can be saved into Preferences")
}
res as T
}
#SuppressLint("CommitPrefEdits")
private fun putPreference(name: String, value: T) = with(prefs.edit()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
else -> throw IllegalArgumentException("This type can't be saved into Preferences")
}.apply()
}
}
Om is completely right about the reasons why your code doesn't work, and his answer will work.
To understand why you need a MutableState in compose I suggest you start with documentation, including this youtube video which explains the basic principles.
But PreferenceManager is deprecated and now you can use DataStore instead.
With compose in can be used like this:
#Composable
fun <T> rememberPreference(
key: Preferences.Key<T>,
defaultValue: T,
): MutableState<T> {
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val state = remember {
context.dataStore.data
.map {
it[key] ?: defaultValue
}
}.collectAsState(initial = defaultValue)
return remember {
object : MutableState<T> {
override var value: T
get() = state.value
set(value) {
coroutineScope.launch {
context.dataStore.edit {
it[key] = value
}
}
}
override fun component1() = value
override fun component2(): (T) -> Unit = { value = it }
}
}
}
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "preferences")
Usage:
var name by rememberPreference(stringPreferencesKey("zipCode"), "World")
One key piece is missing in Code C, which is MutableState.
In Compose the only way to modify content UI is by mutation of its corresponding state.
Your code doesn't have a mutable state object backing up PreferenceTool. So use of setValue by property delegation only modifies the SharedPreference (by calling putPreference(name, value)) but change is not propagated to UI.
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = findPreference(name, default)
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
putPreference(name, value)
}
In order to correct the behavior, add a MutableState object within PreferenceTool.
This way the updates are detected by Compose and UI is updated accordingly.
class PreferenceTool<T>(
private val context: Context,
private val name: String,
private val default: T
) {
private val prefs: SharedPreferences by lazy {
PreferenceManager.getDefaultSharedPreferences(context)
}
private val state = mutableStateOf(findPreference(name, default))
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = state.value
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
state.value = value
putPreference(name, value)
}
#Suppress("UNCHECKED_CAST")
private fun findPreference(name: String, default: T): T = { ... }
#SuppressLint("CommitPrefEdits")
private fun putPreference(name: String, value: T) = { ... }
}