Hello everyone I am new to Jetpack Compose.
Needed clarification reagarding the usage of Copy Function in kotlin Data Classes.
Here i am using a NetworkFetchState abstract class, which helps me determine the state of the network call.
// Abstract Class
abstract class NetworkFetchState(
val isLoading: Boolean = false,
val isSuccess: Boolean = false,
val isError: Boolean = false,
val error: Throwable? = null,
val errorMessage: String? = null
)
I am creating the data class that is extending this abstract class
data class LoginDataState(
val responseData: LoginResponse? = null
) : NetworkFetchState() // extending the Abstract Class
Now inside the ViewModel Class i am creating a mutable state flow
class MyViewModel:ViewModel(){
// Mutable State Flow of the Data State
private val _loginDataState = MutableStateFlow(LoginDataState())
// readonly value of the __loginDataState
val loginDataState: StateFlow<LoginDataState> get() = _loginDataState
/*
* Here I am performing network calls inside the view model scope
* based on the result from the network call i am trying to update the MutableStateFlow
*/
fun makeNetworkCall(){
// ....
_loginDataState.update { prevState ->
prevState.copy(
// ---- PROBLEM HERE ----
// isLoading, isSuccess.. etc (all other variables from abstract class)
// are not available
)
}
}
}
all the member variables that are extending from abstract class are not visible.
What am i doing wrong?
The .copy function is a function generated by kotlin compiler for all data classes. As per the documentation, it's using only properties declared in the primary constructor.
If you want to change those properties with copy function, you will have to add them to the primary constructor somehow.
// this would work
data class LoginDataState(
val responseData: LoginResponse? = null,
val _isLoading: Boolean = false,
) : NetworkFetchState(isLoading = _isLoading)
// this is probably better
interface NetworkFetchState {
val isLoading: Boolean get() = false
}
data class LoginDataState(
val responseData: LoginResponse? = null,
override val isLoading: Boolean = false,
) : NetworkFetchState
Related
In a Jetpack Compose component I'm subscribing to Room LiveData object using observeAsState.
The initial composition goes fine, data is received from ViewModel/LiveData/Room.
val settings by viewModel.settings.observeAsState(initial = AppSettings()) // Works fine the first time
A second composition is initiated, where settings - A non nullable variable is set to null, and the app crashed with an NPE.
DAO:
#Query("select * from settings order by id desc limit 1")
fun getSettings(): LiveData<AppSettings>
Repository:
fun getSettings(): LiveData<AppSettings> {
return dao.getSettings()
}
ViewModel:
#HiltViewModel
class SomeViewModel #Inject constructor(
private val repository: AppRepository
) : ViewModel() {
val settings = repository.getSettings()
}
Compose:
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun ItemsListScreen(viewModel: AppViewModel = hiltViewModel()) {
val settings by viewModel.settings.observeAsState(initial = AppSettings())
Edit:
Just to clearify, the DB data does not change. the first time settings is fetched within the composable, a valid instance is returned.
Then the component goes into recomposition, when ItemsListScreen is invoked for the second time, then settings is null (the variable in ItemsListScreen).
Once the LiveData<Appsettings> is subscribed to will have a default value of null. So you get the default value required by a State<T> object, when you call LiveData<T>::observeAsState, followed by the default LiveData<T> value, this being null
LiveData<T> is a Java class that allows nullable objects. If your room database doesn't have AppSettings it will set it a null object on the LiveData<AppSettings> instance. As Room is also a Java library and not aware of kotlin language semantics.
Simply put this is an interop issue.
You should use LiveData<AppSettings?> in kotlin code and handle null objects, or use some sort of MediatorLiveData<T> that can filter null values for example some extensions functions like :
#Composable
fun <T> LiveData<T?>.observeAsNonNullState(initial : T & Any, default : T & Any) : State<T> =
MediatorLiveData<T>().apply {
addSource(this) { t -> value = t ?: default }
}.observeAsState(initial = initial)
#Composable
fun <T> LiveData<T?>.observeAsNonNullState(initial : T & Any) : State<T> =
MediatorLiveData<T>().apply {
addSource(this) { t -> t?.run { value = this } }
}.observeAsState(initial = initial)
If you only need to fetch settings when viewModel is initialised, you can try putting it in an init block inside your ViewModel.
Modifying simple values and data classes using EditText is fairly straight forward, and generally looks like this:
data class Person(var firstName: String, var lastName: Int)
// ...
val (person, setPerson) = remember { mutableStateOf(Person()) }
// common `onChange` function handles both class properties, ensuring maximum code re-use
fun <T> onChange(field: KMutableProperty1<Person, T>, value: T) {
val nextPerson = person.copy()
field.set(nextPerson, value)
setPerson(nextPerson)
}
// text field for first name
TextField(
value = person.firstName,
onChange = { it -> onChange(Person::firstName, it) })
// text field for last name name
TextField(
value = person.lastName,
onChange = { it -> onChange(Person::lastName, it) })
As you can see, the code in this example is highly reusable: thanks to Kotlin's reflection features, we can use a single onChange function to modify every property in this class.
However, a problem arises when the Person class is not instantiated from scratch, but rather pulled from disk via Room. For example, a PersonDao might contain a `findOne() function like so:
#Query("SELECT * FROM peopleTable WHERE id=:personId LIMIT 1")
fun findOne(personId: String): LiveData<Person>
However, you cannot really use this LiveData in a remember {} for many reasons:
While LiveData has a function called observeAsState(), it returns State<T> and not MutableState<T>, meaning that you cannot modify it with the TextFields. As such this does not work:
remember { personFromDb.observeAsState()}
You cannot .copy() the Person that you get from your database because your component will render before the Room query is returned, meaning that you cannot do this, because the Person class instance will be remembered as null:
remember { mutableStateOf(findPersonQueryResult.value) }
Given that, what is the proper way to handle this? Should the component that contains the TextFields be wrapped in another component that handles the Room query, and only displays the form when the query is returned? What would that look like with this case of LiveData<Person>?
I would do it with a copy and an immutable data class
typealias PersonID = Long?
#Entity
data class Person(val firstName: String, val lastName: String) {
#PrimaryKey(autoGenerate = true)
val personID: PersonID = null
}
//VM or sth
object VM {
val liveData: LiveData<Person> = MutableLiveData() // your db call
val personDao: PersonDao? = null // Pretending it exists
}
#Dao
abstract class PersonDao {
abstract fun upsert(person: Person)
}
#Composable
fun test() {
val personState = VM.liveData.observeAsState(Person("", ""))
TextField(
value = personState.value.firstName,
onValueChange = { fName -> VM.personDao?.upsert(personState.value.copy(firstName = fName))}
)
}
Why arguments in Kotlin Triple class define as val?
public data class Triple<out A, out B, out C>(
public val first: A,
public val second: B,
public val third: C
) : Serializable {
/**
* Returns string representation of the [Triple] including its [first], [second] and [third] values.
*/
public override fun toString(): String = "($first, $second, $third)"
}
Is there a way for changing first , second or third value after we set them ?
private var mSituation=Triple<Boolean,Boolean,Boolean>(first = false, second = false, third = false)
mSituation.first=true // val cannot be reassigned
You can use copy method of data classes
private var mSituation = Triple(first = false, second = false, third = false)
mSituation = mSituation.copy(first = true)
But you shouldn't use Triple this way, no one will understand what these values mean, even you will forget in a few weeks
Create your own data class with meaningful property names and var modifier, if you need it
Properties in Kotlin classes can be declared either as mutable using the var keyword, or as read-only using the val keyword.
public class Triple< A>(
public var first: A
)
fun main() {
var mSituation=Triple<Boolean>(first = false)
mSituation.first=true
println("Hello, world!!!")
}
https://kotlinlang.org/docs/reference/properties.html
I have a LiveData property for login form state like this
private val _authFormState = MutableLiveData<AuthFormState>(AuthFormState())
val authFormState: LiveData<AuthFormState>
get() =_authFormState
The AuthFormState data class has child data objects for each field
data class AuthFormState (
var email: FieldState = FieldState(),
var password: FieldState = FieldState()
)
and the FieldState class looks like so
data class FieldState(
var error: Int? = null,
var isValid: Boolean = false
)
When user types in some value into a field the respective FieldState object gets updated and assigned to the parent AuthFormState object
fun validateEmail(text: String) {
_authFormState.value!!.email = //validation result
}
The problem is that the authFormState observer is not notified in this case.
Is it possible to trigger the notification programically?
Maybe you can do:
fun validateEmail(text: String) {
val newO = _authFormState.value!!
newO.email = //validation result
_authFormState.setValue(newO)
}
You have to set the value to itself, like this: _authFormState.value = _authFormState.value to trigger the refresh. You could write an extension method to make this cleaner:
fun <T> MutableLiveData<T>.notifyValueModified() {
value = value
}
For such a simple data class, I would recommend immutability to avoid issues like this altogether (replaces all those vars with vals). Replace validateEmail() with something like this:
fun validateEmail(email: String) = //some modified version of email
When validating fields, you can construct a new data object and set it to the live data.
fun validateFields() = _authFormState.value?.let {
_authFormState.value = AuthFormState(
validateEmail(it.email),
validatePassword(it.password)
)
}
I am receiving a JSON data model that has a map wrapper Table. I'm trying to use generics to pass in the type that is beyond the wrapper but it's not translating well at runtime. Here's an example of my JSON file:
{
"Table": [
{
"paymentmethod_id": 1,
"paymentmethod_description": "Cash",
"paymentmethod_code": "Cash",
"paymentmethod_is_ach_onfile": false,
"paymentmethod_is_element": false,
"paymentmethod_is_reward": false,
"paymentmethod_is_openedgeswipe": false,
"paymentmethod_update_user_id": 1,
"paymentmethod_insert_user_id": 1,
"paymentmethod_insertdate": "2014-10-07 14:53:16",
"paymentmethod_deleted": false,
"paymentmethod_is_mobile_visible": true
}
]
}
The wrapper class I'm using is called Table.
data class Table<T>(
#SerializedName("Table") val models : Array<T>
)
The actual model class is PaymentMethod.
data class PaymentMethod(
#SerializedName("paymentmethod_id") val idNumber : Int = -1
)
I have created a generic data manager class that takes < T > type. I think use subclasses of the data manager to localize the input and results (such as declaring the model class PaymentMethod.
open class NXDataManager<T>(manager: NXNetworkManager? = null, rpc : String?, parameters: List<Pair<String, String>>? = null, method : String = "get")
{
...
open fun sendRequest(completionHandler: (models:Array<T>) -> Unit, errorHandler: (error:FuelError) -> Unit) {
val request = NXNetworkRequest(rpc, parameters, method)
request.send(manager, completionHandler = { s: String ->
val table: Table<T> = Gson().fromJson(s)
completionHandler(table.models)
}, errorHandler = errorHandler)
}
inline fun <reified T> Gson.fromJson(json: String) = this.fromJson<T>(json, object: TypeToken<T>() {}.type)
}
My subclassed data manager specifies the model to parse into.
final public class PaymentMethodsDataManager : NXDataManager<PaymentMethod>
{
constructor () : super("genGetPaymentMethods")
}
When I run the code as:
val table: Table<T> = Gson().fromJson(s)
I get an error message java.lang.ClassCastException: java.lang.Object[] cannot be cast to Networking.PaymentMethod[]. However, when I pass in an explicit type it works as expected--parsing the array into PaymentMethod models:
val table: Table<PaymentMethod> = Gson().fromJson(s)
Any ideas of how I can still use the generic type T?
Data Class :
data class Table<T>(
#SerializedName("Table") val models : Array<T>
)
to JSON:
val gson = Gson()
val json = gson.toJson(table)
from JSON:
val json = getJson()
val table = gson.fromJson(json, Table::class.java)
Method fromJson is generic, so when you call it for Table<T> variable it creates Array<Any> as most suitable. You need to notice that PaymentMethod class extends T generic, but I don't know is it even possible. If you find out how to make it, use something like following:
val table: Table<T> = Gson().fromJson<Table<PaymentMethod>>(s)
In your case I'm using gson adapters. Following function creates object with specified type parameter:
fun getObjectFromString(type: Type, string: String) =
Gson().getAdapter(TypeToken.get(type)).fromJson(string)
To use it write something following:
val table: Table<T> = getObjectFromString(Table<PaymentMethod>::class.java, s) as Table<PaymentMethod>
Update
To avoid spare class cast you can use reified generic function:
inline fun <reified T> getObjectFromString(string: String): T =
getGsonConverter().getAdapter(TypeToken.get(T::class.java)).fromJson(string)!!
In that case using would be easier:
val table: Table<T> = getObjectFromString<Table<PaymentMethod>>(s)
I used first solution in cases where I don't know what type the object would be - I've got only Type variable with information about that object.
java.lang.ClassCastException: java.lang.Object[] cannot be cast to
Networking.PaymentMethod[]
Your JSON is
{
"Table": [
{
"paymentmethod_id": 1,
"paymentmethod_description": "Cash",
"paymentmethod_code": "Cash",
"paymentmethod_is_ach_onfile": false,
"paymentmethod_is_element": false,
"paymentmethod_is_reward": false,
"paymentmethod_is_openedgeswipe": false,
"paymentmethod_update_user_id": 1,
"paymentmethod_insert_user_id": 1,
"paymentmethod_insertdate": "2014-10-07 14:53:16",
"paymentmethod_deleted": false,
"paymentmethod_is_mobile_visible": true
}
]
}
Create a data class, PaymentMethod.
We frequently create classes whose main purpose is to hold data. In
such a class some standard functionality and utility functions are
often mechanically derivable from the data.
data class PaymentMethod(#SerializedName("Table") val table:ArrayList<PaymentData> )
data class PaymentData
(
#SerializedName("paymentmethod_id") val paymentmethod_id: Int,
#SerializedName("paymentmethod_description") val paymentmethod_description: String,
#SerializedName("paymentmethod_code") val paymentmethod_code:String,
#SerializedName("paymentmethod_is_ach_onfile") val paidStatus:Boolean,
#SerializedName("paymentmethod_is_element") val paymentmethod_is_element:Boolean,
#SerializedName("paymentmethod_is_reward") val paymentmethod_is_reward:Boolean,
#SerializedName("paymentmethod_is_openedgeswipe") val paymentmethod_is_openedgeswipe:Boolean,
#SerializedName("paymentmethod_update_user_id") val paymentmethod_update_user_id:Int,
#SerializedName("paymentmethod_insert_user_id") val paymentmethod_insert_user_id:Int,
#SerializedName("paymentmethod_insertdate") val paymentmethod_insertdate:String,
#SerializedName("paymentmethod_deleted") val paymentmethod_deleted:Boolean),
#SerializedName("paymentmethod_is_mobile_visible") val paymentmethod_is_mobile_visible:Boolean
)
You can call this way
val paymentDATA = Gson().fromJson<PaymentMethod>("JSON_RESPONSE", PaymentMethod::class.java)
val _adapterPaymentHistory = paymentDATA.table