I'm using kotlin with MVVM pattern on my project.
In my viewModel I need to generate an error message and toast while input a text.
The error messages are like:
<string name="error_message">%s: is too long</string>
<string name="error_message1">name can't be empty</string>
viewModel.kt
func checkInput(context: Context, text: String) {
if (text.isEmpty()) {
Toast.make(context.getString(R.string.error_message1).show()
}
if (text.length > 10) {
Toast.make(String.format(context.getString(R.string.error_message1), maxNumber).show()
}
}
In the way above it works. But in my knowledge viewModel shouldn't hold any Android UI stuff that is context shouldn't be there. (Correct me if I'm wrong)
If there are no combine strings with variable, then I can return Int, that is return R.string.xxx directly. But now I don't know the proper way to do it.
I know subclass AndroidViewModel then I don't needs to care about context. I don't want to use this way. Because I needs localized my app with Activity's context, I think application context can't achieve it.
How to deal with this situation? thanks!
You have to generate an event type which you need to listen to your ui component (Activity/Fragment) from there you need to display error msg or toast.
only logic part will be remain in ViewModel and display related work goes to ui component.
To achieve this try using Sealed class for this.
sealed class ScreenState {
object Loading : ScreenState()
object Success: ScreenState()
object showToast(var type: String) : ScreenState()
}
wrap this inside live data
private val _screenState = MutableLiveData<ScreenState?>()
val screenStateLiveData: LiveData<ScreenState?> = _screenState
inside your ViewModel
func checkInput(text: String) {
if (text.isEmpty()) {
_screenState.postvalue(ScreenState.showToast("1"))
}
if (text.length > 10) {
_screenState.postvalue(ScreenState.showToast("2"))
}
}
observe the liveData screenStateLiveData in your ui component and display error based on your showToast() type.
private fun observeScreenStateData() {
mViewModel.screenStateLiveData.observe(mContext as FragmentActivity, Observer {
it?.let {
when (it) {
is LiveTvData.Loading -> {}
is LiveTvData.Success ->. {}
is LiveTvData.showToast -> displayErrorMsg(it.type)
}
})
}
First of all why do you need to place checkInput() in ViewModel? You get the final string from the view which is the Activity/Fragment class validate the string and then store it ViewModel.
You can just move your checkInput() to the Activity/Fragment class and validate from there. You can also do the other way around (Store in viewmodel -> Observe from the Activity/Fragment-> then )
But in my knowledge viewModel shouldn't hold any Android UI stuff
that is context shouldn't be there. (Correct me if I'm wrong)
You are right here! For more understanding of why refer here and here
Thanks #Ankit 's answer, it give me the hint, sealed interface is the workaround, that is very like enum in swift, that can pass parameter.
sealed interface ValidateError {
data class NormalError(val resId: Int): ValidateError
data class ParameterError(val resId: Int, val parameter: Any): ValidateError
}
Then in view:
when (it) {
is ValidateError.NormalError -> Toast.make(it.resourceId).show()
is ValidateError.ParameterError -> {
showToast(String.format(getString(it.resourceId), it.parameter))
}
}
Related
Am learning android kotlin follow this:
https://developer.android.com/topic/libraries/architecture/viewmodel#kotlin
class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers(it)
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
Dont know how to write the fun loadUsers()
Here is my User:
class User {
constructor(name: String?) {
this.name = name
}
var name:String? = null
}
If dont use the keyword 'also' , i know how to do it.
But if use 'also' , it seems not work.
Here is how i try to write the fun loadUsers:
private fun loadUsers( it: MutableLiveData<List<User>>){
val users: MutableList<User> = ArrayList()
for (i in 0..9) {
users.add(User("name$i"))
}
it = MutableLiveData<List<User>>(users)
}
Error tips near it : Val cant be ressigned
Part 1: According to the Kotlin documentation, also provides the object in question to the function block as a this parameter. So, every function call and property object you access is implied to refer to your MutableLiveData<List<User>>() object. also returns this from the function block when you are done.
Thus, another way of writing your MutableLiveData<> would be like this:
val users = MutableLiveData<List<User>>()
users.loadUsers()
Part 2: As far as how to implement loadUsers(), that is a separate issue (your question is not clear). You can use Retrofit + RxJava to load the data asynchronously, and that operation is totally outside of the realm of ViewModel or also.
Part 3: With your approach, you have conflicting things going on. Instead of doing a loadUsers() from your lazy {} operation, I would remove your lazy {} operation and create a MutableLiveData<> directly. Then, you can load users later on and update the users property any time new data is loaded. Here is a similar example I worked on a while ago. It uses state flows, but the idea is similar. Also use a data class to model the User instead of a regular class. Another example.
It is solved change to code:
private fun loadUsers( it: MutableLiveData<List<User>>){
val users: MutableList<User> = ArrayList()
for (i in 0..9) {
users.add(User("name$i"))
}
it.value = users
}
it can't be reassigned , but it.value could .
There is function collectAsState() applicable to a StateFlow property in order to observe it in a Composable.
A composable requires a StateFlow because StateFlow guarantees an initial value. A Flow doesn't come with that guarantee.
Now, what is the way to go if I have a StateFlow property but I want to apply an operator (like map) before collecting the Flow in the Composable?
Here an example:
Let's say a repository exposes a StateFlow<MyClass>
val myClassStateFlow: StateFlow<MyClass>
data class MyClass(val a: String)
... and a view model has a dependency on the repository and wants to expose only the property a to its Composable...
val aFlow = myClassState.Flow.map { it.a } // <- this is of type Flow<String>
The map operator changes the type from StateFlow<MyClass> to Flow<String>.
Is it semantically justified that aFlow has no initial value anymore? After all its first emission is derived from the initial value of myClassStateFlow.
It's required to convert Flow back into StateFlow at some point. Which is the more idiomatic place for this?
In the view model using stateIn()? How would the code look like?
In the composable using collectAsState(initial: MyClass) and come up with an initial value (although myClassStateFlow had an initial value)?
See this issue on GitHub
Currently there is no built-in way to transform StateFlows, only Flows. But you can write your own.
Way I ended up solving was to use the example in that post.
First create a notion of a DerivedStateFlow.
class DerivedStateFlow<T>(
private val getValue: () -> T,
private val flow: Flow<T>
) : StateFlow<T> {
override val replayCache: List<T>
get () = listOf(value)
override val value: T
get () = getValue()
#InternalCoroutinesApi
override suspend fun collect(collector: FlowCollector<T>) {
flow.collect(collector)
}
}
Then have an extension on StateFlow like the current map extension on Flow
fun <T1, R> StateFlow<T1>.mapState(transform: (a: T1) -> R): StateFlow<R> {
return DerivedStateFlow(
getValue = { transform(this.value) },
flow = this.map { a -> transform(a) }
)
}
Now in your Repository or ViewModel, you can use it as below.
class MyViewModel( ... ) {
private val originalStateFlow:StateFlow<SomeT> = ...
val someStateFlowtoExposeToCompose =
originalStateFlow
.mapState { item ->
yourTransform(item)
}
}
Now you can consume it as you expect in Compose without any special work, since it returns a StateFlow.
I'm implementing the viewModel and for communicate between the viewModel and fragment I'm doing this :
public class SplashViewModel extends AndroidViewModel {
private LiveData<Boolean> actions;
public SplashViewModel(#NonNull Application application) {
super(application);
actions= new MutableLiveData<>();
}
public void aViewModelMethod() {
//doing some stuff
if (stuff == X){
//I need to hide a view for exemple, I'm doing this
actions.postValue(true);
}
}
Now inside my Fragment I have an observable who is trigger when actions.postValue(true) is reached
viewModel.actions.observe(getViewLifecycleOwner(), new Observer<Boolean>() {
#Override
public void onChanged(Boolean action) {
if (action){
databinding.myView.setVisibility(View.VISIBLE);
}
}
});
This work fine but if I have a lot of communication I need to implement each time a new variable, and observe it ?
It's ok when they are 4 or 5 but what I am suppose to do when they are hundreds ?
I try to change boolean by an integer with a switch and a list of actions, but when the viewModel is initialize it's possible that several postValue are trigger and when I created the observable I'm only get the last one, that make sense.
Usually, I have two observable live data in my view model. First is represent the state of the whole screen. Second I use for "single-shot" events like toasts, navigation, showing dialogs.
My view model:
class PinCreateViewModel(...) : ViewModel() {
val event = MutableLiveData<BaseEvent<String>>()
val state = MutableLiveData<PinCreateViewState>()
}
I have a single state object for the whole screen:
sealed class PinCreateViewState {
object FirstInput : PinCreateViewState()
data class SecondInput(val firstEnteredPin: String) : PinCreateViewState()
object Error : PinCreateViewState()
object Loading : PinCreateViewState()
}
I think with this approach it's easy to think about my screen states, easy to design my screen as a finite state machine, and easy to debug. Especially, I like this approach to very complex screens. In this case, I have a single source of truth for my whole screen state.
But sometimes I want to show dialogs, toast or open new screens. These things are not part of my screen state. And this is why I want to handle them separately. And in this case, I use Events:
sealed class BaseEvent(private val content: String) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): String? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
fun peekContent(): String = content
}
class ErrorEvent(content: String) : BaseEvent(content)
class MessageEvent(content: String) : BaseEvent(content)
And my Fragment interaction with ViewModel looks like this:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
observe(viewModel.event, this::onEvent)
observe(viewModel.state, this::render)
}
private fun render(state: PinCreateViewState) {
when (state) {
PinCreateViewState.FirstInput -> setFirstInputState()
is PinCreateViewState.SecondInput -> setSecondInputState()
PinCreateViewState.Error -> setErrorState()
PinCreateViewState.Loading -> setLoadingState()
}
}
fun onEvent(event: BaseEvent<String>) {
event.getContentIfNotHandled()?.let { text ->
when (event) {
is MessageEvent -> showMessage(text)
is ErrorEvent -> showError(text)
}
}
}
I really like Kotlin Sealed classes because it forces me to handle all possible cases. And I can find unhandled states even before compilation.
PostValue method Posts a task to the main thread to set the given value. If you called this method multiple times before a main thread executed a posted task, only the last value would be dispatched.
If you will have hundreds of communication between your fragment and viewModel so you just have to deduce your fragment logic like if you have to show view on some conditions then just observe one non mutable live data in your fragment and use two live data's mutable and another non mutable .... use non mutable to set that boolean on every sort of stuff and checks in your viewModel and in the beginning assign that live data to your non mutable one.
private val _liveData = MutableLiveData<Boolean>()
internal val liveData: LiveData<Boolean> = _liveData
It's the better approach ,i hope i understand you question better if not please elaborate it more so that i can help .
I am making a list of observable LiveData objects, that should contain Resource object (https://developer.android.com/topic/libraries/architecture/guide.html#addendum). I don't care what type of data that Resource object is containing.
abstract class LiveResources : LiveData<Resource<Any>>() {
private val mediatorLiveData = MediatorLiveData<Resource<Any>>()
protected val resources = HashSet<LiveData<Resource<Any>>>()
fun addResource(source: LiveData<Resource<Any>>) {
resources.add(source)
mediatorLiveData.addSource(source, resourceObserver)
}
fun removeResource(source: LiveData<Resource<Any>>) {
resources.remove(source)
mediatorLiveData.removeSource(source)
}
private val resourceObserver = Observer<Resource<Any>> {
onSourceChange()
}
abstract fun onSourceChange()
}
Unfortunately when I try to use LiveResources.addResource() with LiveData<Resource<List<String>>> I get TypeMismatch error in my IDE, saying that LiveData<Resource<Any>> was expected.
Your Resource (and/or LiveData) class should be defined with generic covariance in order to make it work. Like so:
class Resource<out T> // <- out marks generic type as covariant
Haven't tried it, but I think this would work
fun <T:Any> addResource(source: LiveData<Resource<T>>)
You should generify the classes to accept Resource<T> i.e LiveData<Resource<T>>. Any is the covariance of any object passed, but I think you are not trying to achieve that.
Another friendly advice is that you don't need to add another abstraction on top of MediatorLiveData that solely does the same you have implemented.
I'm trying to genericise the boilerplate around a very common pattern, and Kotlin brings me tantalisingly close.
I've built a class that serves as a listener manager, as follows:
class GenericListenerSupport <EventArgumentType, ListenerFunction: (EventArgumentType) -> Unit> {
private val listeners = mutableListOf<ListenerFunction>()
fun addListener(listener: ListenerFunction) {
listeners.add(listener)
}
fun removeListener(listener: ListenerFunction) {
listeners.remove(listener)
}
fun fireListeners(argument: EventArgumentType) {
listeners.forEach { it.invoke(argument) }
}
}
and it can be used as follows:
class ExampleWithArgument {
private val listenerSupport = GenericListenerSupport<String, (String)->Unit>()
fun exampleAdd() {
listenerSupport.addListener({ value -> System.out.println("My string: "+value)})
}
fun exampleFire() {
listenerSupport.fireListeners("Hello")
}
}
So far, so good. But what if the listener has no arguments? Or stretching even further, multiple parameters.
I can scrape through with this:
class ExampleWithNoArgument {
private val listenerSupport = GenericListenerSupport<Nothing?, (Nothing?)->Unit>()
fun exampleAdd() {
listenerSupport.addListener({ System.out.println("I've got no argument")})
}
fun exampleFiring() {
listenerSupport.fireListeners(null)
}
}
but it smells, and obviously it's no use for multiple parameters.
Is there a better way to pull this off? e.g. something supporting this concept:
private val listenerSupport = GenericListenerSupport<???, (String, Double)->Unit>()
Since your GenericListenerSupport declares a type parameter EventArgumentType and expects an instance of it in fun fireListeners(argument: EventArgumentType), I doubt you can support multiple arguments in a clean way. Instead, I'd suggest using a data class (which is not so much extra code), as a clean and type-safe way to wrap multiple values:
data class MyEvent(val id: String, val value: Double)
private val listenerSupport = GenericListenerSupport<MyEvent, (MyEvent) -> Unit>()
As to passing no value, you can also use Unit, the type that has exactly one value Unit:
listenerSupport.fireListeners(Unit)
The type system and resolution won't allow you to pass no argument where a single one is expected, but, as #Ruckus T-Boom suggested, you can make an extension to fire listeners with no value where Unit is expected:
fun GenericListenerSupport<Unit>.fireListeners() = fireListeners(Unit)
A bit off-topic, but I think you can simplify the type if you don't need custom function types and (EventArgumentType) -> Unit is sufficient:
class GenericListenerSupport<EventArgumentType> {
/* Just use `(EventArgumentType) -> Unit` inside. */
}