copper-leaf / ballast Goto Github PK
View Code? Open in Web Editor NEWOpinionated Application State Management framework for Kotlin Multiplatform
Home Page: https://copper-leaf.github.io/ballast/
License: BSD 3-Clause "New" or "Revised" License
Opinionated Application State Management framework for Kotlin Multiplatform
Home Page: https://copper-leaf.github.io/ballast/
License: BSD 3-Clause "New" or "Revised" License
Hi. In one of my KMP Android and iOS projects, I try to bump ballast (core and navigation) from 4.0.0 to 4.2.1 bump, but I get an error:
> Could not resolve all files for configuration ':apps:product:product-ui:iosX64CompilationDependenciesMetadata'.
> Could not find uri-kmp-iosx64-0.0.18.klib (com.eygraber:uri-kmp-iosx64:0.0.18).
Searched in the following locations:
https://plugins.gradle.org/m2/com/eygraber/uri-kmp-iosx64/0.0.18/uri-kmp-iosx64-0.0.18.klib
https://plugins.gradle.org/m2/com/eygraber/uri-kmp-iosx64/0.0.18/uri-kmp-iosx64-0.0.18.jar
It seems like an issue with `uri-kmp?
While setting up ballast I'm not able to properly define the debugger dependency. I always get the following error while syncing:
Could not find io.github.copper-leaf:ballast-debugger:4.0.0
I did a quick maven artifact search and the artifact indeed isn't available. There are some ballast related debug artifacts (ending with android-debug
, but I'm not sure those are the same.
Using annotations on inputs, have an interceptor track whether an input failed, and if so, send it again to be retried. Include a callback which is notified when the same input has failed many times, exceeding some user-defined threshold.
After some time using and thinking about the Repository module, I've come to the conclusion that it's current stated use-case is not the most ideal for structuring your application. Most notably, the method of using an EventBus for communication between Repository instances is flawed. Instead, Repositories that depend on the state of other Repositories should be arranged in a hierarchy (either a true tree, or a Directed Acyclic Graph (DAG)), rather than something like an unstructured Graph as is the current setup.
The inspiration for this change comes from thinking about the application as a whole, and understanding the purpose of the Repository layer and the Repositories within that. Just as the UI screens and their ViewModels only ever passively observe from Repositories, and thereby isolate themselves from the implementation details of the Repository while also eliminating strange communication patterns by enforcing the UDF flow, the same should be true of Repositories amongst themselves. Parent repositories should send their state to children, which eventually flows to UIs, to deeper components within the UI, etc.
Ultimately, it should follow a flow that is something like this:
flowchart TD
Global["Global State (authentication)"]
Router
Repositories
Screens
Components
Global --> Router
Global --> Repositories
Router --> Screens
Repositories --> Screens
Screens --> Components
The extent of changes should be as follows:
Cached
API generic enough such that it could be used in any ViewModel, or even outside of a ViewModel if it needs to be done in a standard suspending functionCached
wrapper into a Repository ViewModel without all the boilerplate required nowEventBus
Additional nice-to-have features include:
Cached
values in the Repository, such as time-based or (depending on platform) based on total size of data stored in the ViewModelNow that the Debugger allows one to send States or Inputs as JSON, the next logical improvement to this feature is to make it easier to use across multiple sessions. This would involve several additional features:
Currently, because of a mismatch in Kotlin versions between Ktor 1.x, Ktor 2.x, and Compose, the current version of the Ballast debugger only works with the Ktor 1.x. The Websocket client should be pluggable so that the default behavior is the 1.x client, but if needed one could connect to a 2.x client they write themselves, or even use a different HTTP client library altogether
Add built-in features to ballast-repository
module to observe flows instead of only doing one-shot fetches. Also, it should support time-based expiry for cache, such that it may still be refreshed when fetchWithCache
is called even if forceRefresh
is false. it might also make sense to support a pluggable refresh policy that has implementations for force refresh and/or time-based expiry.
For an example use-case, consider the refreshing logic in the repository layer of KaMPKit
Show another tab of a selected View model which displays a graphical view of ViewModel activity.
Add a module to implement undo/redo functionality. it should contain a "controller" which can be used by one or many ViewModels, and your application code calls .undo()
or .redo()
on it.
The controller keeps track of the states and Inputs that went through each of its connected ViewModels in a buffer of "frames" (whose entries can be configured to expire by either time, number of frames or total size in memory). It captures changes into frames, and calling undo/redo will restore the state of the previous/next frame.
It is probably out of scope to handle undo/redo of persistent things (would not un-delete a file for example). Maybe an annotation or interface to give manual control of what to undo might be doable.
VM config classes (and even config builder classes) are not independent in terms of lifecycle from the VMs constructed with them.
When this invariant is not held, then the system completely breaks. Adding logging shows that it throws ClosedSendChannelException
s and then becomes unresponsive, as shown in the second reproduction in https://github.com/rocketraman/test-ballast-4-csce.
The workaround is simply to instantiate the config builder and config each time the VM is itself instantiated. In other words, when using a DI system, use a factory
or provider
rather than a singleton
.
However, noting this issue because the API design allows for configs to be instantiated separately, but the implementation does not. This isn't the best API affordance for consumers, and should be "fixed" using one of the following solutions -- and I'm putting them in the order that I think is best to worst, from the perspective of an API consumer:
The implementation of Ballast's VMs should be modified so that the lifecycle of a config is not tied to the lifecycle of the VM it is injected into.
Modify the API in such a way that API consumers cannot do something so simple to break Ballast. The constructor of a VM could accept the config parameters directly, or it could accept a builder object which creates new instances of any stateful classes specified by the config at VM construction time.
At the least, update documentation to explain the lifecycle of a config class, and update kdocs as well.
Reference conversation from Slack: https://kotlinlang.slack.com/archives/C03GTEJ9Y3E/p1699907007258999?thread_ts=1699623937.047949&cid=C03GTEJ9Y3E.
The IntelliJ plugin is currently hardcoded to listen on port 9684 for the debugger websocket connection. There should be a screen in the settings menu to change this port.
I'm investigating which state management library I want to use for my multi-platform (iOS, Android) app. From what I'm reading Ballast seems like an interesting alternative.
I'm following the instructions on the web site: https://copper-leaf.github.io/ballast/wiki/platforms/ios/, but I get compiler errors when trying to compile the CombineAdapters
file (copied from the KaMPKit repo as per the instructions): FlowAdapter
and EventHandler
types cannot be found by Xcode.
After a lot of searching, I think the documentation might be out of date, because if I add the dependencies and exports for ballast-api
and ballast-viewmodel
, the file is compiled correctly.
cocoapods {
summary = "Some description for the Shared Module"
homepage = "Link to the Shared Module homepage"
version = "1.0"
ios.deploymentTarget = "14.1"
podfile = project.file("../TryBallast/Podfile")
framework {
baseName = "shared"
isStatic = false // SwiftUI preview requires dynamic framework
export("io.github.copper-leaf:ballast-api:3.0.1") // <=====
export("io.github.copper-leaf:ballast-core:3.0.1")
export("io.github.copper-leaf:ballast-repository:3.0.1")
export("io.github.copper-leaf:ballast-viewmodel:3.0.1") // <=====
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0")
api("io.github.copper-leaf:ballast-api:3.0.1") // <=====
api("io.github.copper-leaf:ballast-core:3.0.1")
api("io.github.copper-leaf:ballast-repository:3.0.1")
api("io.github.copper-leaf:ballast-viewmodel:3.0.1") // <=====
}
}
[...]
}
I am not an iOS developer, so I don't really know how Ballast should be integrated into an iOS app. I've got some basic documentation showing how it could be done, how I've managed to make it work as a POC, but the solution there is far from optimal.
I need someone who's done a lot of KMM development to help me figure out the best way to integrate Ballast into an iOS app. I would like to have patterns for both a pure Swift UI app, as well as a traditional UIKit application. The biggest unknowns for me right now are how to handle navigation from a Ballast Event Handler, and how to manage the lifecycle of the ViewModel.
The current implementation expects that ViewModel state that should be persisted between sessions, is stored with a custom persistent store. There is no built-in support for saving VM state, and restoring it at a later point in time. A custom Interceptor should be created that will watch all state changes and persist them with a pluggable "store". The interceptor will watch all state changes and persist the state, and it will restore the state when the VM is restarted.
Out-of-the-box, it should include "stores" for:
While it is useful to be able to rollback to previous states, it would be really nice to be able to upload JSON to the client application, which can deserialize it and set it as the ViewModel's State. Additionally, it would be nice for clients to be able to send serialized States rather than the .toString()
version of State, so that they can be explored in a tree view in the UI.
Ideally, this would be implemented in a way completely agnostic of the client serialization framework. There would just be a callback to (de)serialize States, and the debugger just trusts that the client is configured correctly to accept it.
Hi,
I'm seeing a few crashes from users exiting a specific screen.
As the app uses Compose's NavHost
, the culprit VM is scoped to a NavBackStackEntry
.
I can't reproduce the crash on my devices but here's what I could gather:
OutlinedTextField
(uses a TextFieldValue
)checkValidState
To investigate, I've overridden my ViewModel's onCleared
class MyVM : AndroidViewModel<Inputs, Events, State>(
[...]
override fun onCleared() {
Logs.i("onCleared")
super.onCleared()
}
And added logs to onValueChange
onValueChange = {
Logs.i("onValueChange")
currentName = it
vm.trySend(NameEditorContract.Inputs.ChangeName(it.text))
}
Then, with a Pixel 4A 5G:
It's possible to have onValueChange
displayed after onCleared
, but only if the cursor is not placed at the end of the OutlinedTextField.
(Note that it doesn't crash for me as BallastViewModelImpl.onCleared
hasn't been called yet)
But on a LGV30, onValueChange
is never displayed after onCleared
โฆ
Not sure if ballast is too strict or if compose doesn't respect some sort of contract here,
what do you think?
Stacktrace:
Fatal Exception: java.lang.IllegalStateException: VM is cleared!
at com.copperleaf.ballast.internal.BallastViewModelImpl.checkValidState(BallastViewModelImpl.kt:198)
at com.copperleaf.ballast.internal.BallastViewModelImpl.trySend-JP2dKIU(BallastViewModelImpl.kt:102)
at com.copperleaf.ballast.core.AndroidViewModel.trySend-JP2dKIU(AndroidViewModel.kt:2)
at com.azefsw.audioconnect.settings.ui.name.NameEditorViewKt$NameEditorView$2$1$1.invoke(NameEditorView.kt:59)
at com.azefsw.audioconnect.settings.ui.name.NameEditorViewKt$NameEditorView$2$1$1.invoke(NameEditorView.kt:57)
at androidx.compose.foundation.text.BasicTextFieldKt$BasicTextField$7$1.invoke(BasicTextField.kt:279)
at androidx.compose.foundation.text.BasicTextFieldKt$BasicTextField$7$1.invoke(BasicTextField.kt:277)
at androidx.compose.foundation.text.TextFieldState$onValueChange$1.invoke(CoreTextField.kt:762)
at androidx.compose.foundation.text.TextFieldState$onValueChange$1.invoke(CoreTextField.kt:757)
at androidx.compose.foundation.text.TextFieldDelegate$Companion.onBlur$foundation_release(TextFieldDelegate.java:242)
at androidx.compose.foundation.text.CoreTextFieldKt.onBlur(CoreTextField.kt:840)
at androidx.compose.foundation.text.CoreTextFieldKt.access$onBlur(CoreTextFieldKt.java:1)
at androidx.compose.foundation.text.CoreTextFieldKt$CoreTextField$2$invoke$$inlined$onDispose$1.dispose(Effects.kt:485)
at androidx.compose.runtime.DisposableEffectImpl.c(Effects.kt:4)
at androidx.compose.runtime.CompositionImpl$RememberEventDispatcher.dispatchRememberObservers(Composition.kt:995)
at androidx.compose.runtime.CompositionImpl.applyChangesInLocked(Composition.kt:774)
at androidx.compose.runtime.CompositionImpl.applyChanges(Composition.kt:794)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:526)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:454)
at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.doFrame(AndroidUiFrameClock.android.kt:8)
at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch(AndroidUiDispatcher.java:109)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch(AndroidUiDispatcher.java:41)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame(AndroidUiDispatcher.android.kt:69)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1002)
at android.view.Choreographer.doCallbacks(Choreographer.java:816)
at android.view.Choreographer.doFrame(Choreographer.java:748)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:990)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6762)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
ballast version 1.1.0
compose version: 1.2.0-beta02
It would be nice to see other information reported in the Debugger that is defined by the end-user. Examples would be Analytics events, HTTP calls, or SQL queries. This would bring more information to one single place that would be very useful in local development. And if I could create a standalone Desktop Application or Browser Extension for the debugger, would even be useful for QA/UAT engineers to verify things like analytics are being tracked properly.
This would add a feature where one could register some generic "logging" interface anywhere in their code that also connects to the Debugger. These logging events aren't necessarily part of a ViewModel, but instead would go directly to the Connection and be sent to the DebuggerUI, showing up as an extra panel in the connection view. A basic DSL would allow one to specify properties such as the title and description (shown as list items in the UI) and a map of extra properties. It should also allow one to create IDs for these events and update the status of a single event over time (for example, logging an HTTP call before it's made, then setting the response status code and body after the call finishes).
There should be out-of-the-box adapters available for Okhttp and Ktor HTTP clients, Ballast Analytics, and Sqldelight. End-users would be free to create their own reporters for anything else they would like to track as well.
Sometimes there's the need to launch an Input
right on ViewModel init. For example, when you need to fetch some data to shown on the screen.
Checking the examples, I've seen that the way it's been done is to use the Activities onCreate
, but in other architectures like a pure Compose app, using lifecycle is not really the way. Also, delegating this kind of internal actions to the view doesn't look completely correct.
Another approach I've been using is to use the init
block of the ViewModel
, but this is a bit cumbersome since, ideally, you will declare different ViewModels for each platform and you'll have to replicate this logic in each of them:
class MyViewModel(
config: BallastViewModelConfiguration<MyContract.Input, MyContract.Event, MyContract.State>,
) : AndroidViewModel<MyContract.Input, MyContract.Event, MyContract.State>(config) {
init {
trySend(MyContract.Input.MyInput)
}
}
My proposal is to be able to indicate a list of desired Inputs to be launched on ViewModel
initialization. You can see how some other state management libraries are doing it here.
Ideally this will be set in the InputHandler
since this is the class written in common code.
Unless I'm missing something, the current implementation of awaitViewModelStart
doesn't do anything, as take
isn't a terminal operator.
/**
* Suspend until the ViewModel has started
*/
public suspend inline fun <Inputs : Any, Events : Any, State : Any> Flow<BallastNotification<Inputs, Events, State>>.awaitViewModelStart() {
filter {
it is BallastNotification.ViewModelStatusChanged<Inputs, Events, State> && it.status == Status.Running
}.take(1)
}
There is a lot of boilerplate involved in using Ballast Repository with Cached<T>
values, and nearly all of that is entirely generic. The only part of it that is actually useful is the fetcher function itself. There should be a default implementation that manages its cached values in a map and defines all the Inputs necessary for the entire pattern, so users only need to provide functions to map Keys to cached values.
Basic idea: add annotations to Contract object, and generate typealiases and ViewModel implementations that make it much nicer to read Ballast code you write.
Example:
@BallastContract
object ExampleContract {
// ...
}
Would generate:
typealias ExampleInputHandler = InputHandler<ExampleContract.Inputs, ExampleContract.Events, ExampleContract.State>
// similar typealiases for other interfaces you'll implement,and their scopes
Ideally, the processor would be smart enough that it could generate typealiases and ViewModels of arbitrary types without special support, meaning plugins could have their boilerplate generated using the same semantics without needing to create their own processors. It would require the user passing the class reference to the @BallastContract
annotation, and those classes having the right number of type parameters, a single public constructor, etc.
With the 4.0.0 updates to InputStrategies, Ballast could conceivably be updated to support distributed processing in a server-side environment. Think of something like the State is held in Redis, and Inputs are AWS SQS messages. It would be interesting to see if it would be possible to build such an integration while maintaining compatibility with the existing Interceptors and API.
For ViewModels that are updated very quickly, it may be desirable to skip some State emissions in order to not overload the disk and make sure that writes to the disk don't slow down the main ViewModel's processing.
To that end, all writes should be persisted asynchronously, and a configurable delay between emissions. By default, the delay will be 0. If there is a delay, then that much time will wait before sending the state to the adapter. If a new State is emitted before that delay completes, then the latest State value will be persisted. This needs to be more clever than a simple .mapLatest
, as we don't want to reset the timer because emissions may continually prevent the adapter from ever being called.
One use case for Ballast in server side applications, or in front end apps without Androids WorkManager, is in scheduling jobs to be run repeatedly in the background. Ballast could support this fairly easily by creating a new module that adds an Interceptor into a View model which dispatches inputs at the appropriate time.
A first POC could be a basic timer-based scheduler. The next addition would be supporting cron syntax for running jobs at a specific time interval.
A final step would be to somehow run the ViewModel in a WorkManager and tie into the filtering behaviors there. It would also be nice to support the same kind of filtering by network state or other properties in iOS or JS.
Open questions:
Hello,
I think it'd be beneficial to create a Slack channel in the Kotlin Slack Workspace for this project. There's much to talk about in making this project bigger.
Best,
-Sami
P.S. I'm looking for a solid KMM MVI library after finding Orbit MVI not well supported when it comes to KMM/iOS, hence want to help in that department in any way possible.
Another server-side use case is a background queue, such as AWS SQS. This module could allow the Ballast API to run on top of an SQS queue, implemented as an Input strategy.
Inputs sent to the VM would be automatically serialized and placed into the queue. Any other distributed server could then read from the queue, deserialize the input, and then process it.
A first POC should disable the use of State through the use of a custom InputStrategy Guardian. This would effectively make a Ballast distributed queue a purely Stateless queue. Future work could add some way to store and rehydrate state for each Input pulled from the queue, which would allow it to process longer distributed "user journeys".
Further extensions could support specific types of common Java server-side queues, like sending transactional emails, SMS messages, or push notifications.
Open questions are mostly the same as with the Scheduler.
Using Compose/Web create an interactive example TODO application based on Ballast. It should highlight some of the core concepts of Ballast, and also display the Kotlin source code within that page so users can see how it all fits together.
Hello
Compose multiplatform for iOS has recently been released (Alfa): https://blog.jetbrains.com/kotlin/2023/05/compose-multiplatform-for-ios-is-in-alpha/
Does Ballast support Compose multiplatform iOS (Alfa) in the current version (3.0.2)?
I recently created an example project showing how one might make an alternative API surface for Ballast Navigation. It's described as something that could be implemented with KSP, though the processor is not implemented. It simply shows what kind of code might be generated.
I don't think I want to maintain a KSP processor for this kind of Navigation API directly within the Ballast repo, mostly because there are many different ways this generated API could look, and the "best API" is going to be highly subjective. But it could be a great idea for someone to build and maintain as a community library!
The io.ktor:ktor-http
dependency of Ballast Navigation module includes the necessary functionality for parsing URLs, but also includes a bunch of other stuff used by the Ktor HTTP server/client that is not necessary for parsing URLs. It's a much bigger dependency than I initially thought when adopting it, and is prohibitively large for JS applications, producing bundles that are unacceptably large, and it should be removed and replaced.
Ideally, the replacement would be a fully-multiplatform URL parser, like Ktor, but in the absence of such a library, it will likely be better to actual/expect the URL functionality.
The current examples are separated by Platform (web, android, desktop), and the same features are re-implemented in each platform with very little difference between them. This also makes the example projects have a bunch of additional "architecture" that may make it difficult to understand the intended use-case and configuration of these features.
Instead, let's break these examples out into a handful of separate, much simpler example projects. The majority of these should be defined as Compose Multiplatform projects with Android, Desktop, and Web targets, with as much as possible defined in the commonMain
sourceset. These examples can then each be individually embedded into their respective documentation pages.
The goal is for each project to include the absolute minimum amount of boilerplate necessary to demonstrate a particular feature of Ballast, as well as avoid duplication of the examples. This will also allow more flexibility in different kinds of examples, as each one is easier to setup and maintain. The idea is to show less about how to set up your KMP repos, and focus more on how to setup and use Ballast.
Right now, the Test module implements a custom ViewModel and wraps everything in a really terrible, hacky way so that it can inspect each Input and wait for them to be completed. But now, with a more robust Interceptor API, it should be possible to directly observe what's sent through the Interceptor to implement the same test logic of waiting for Inputs to complete, without messing with the internals of the ViewModel or creating special APIs just for the Test module (BallastViewModelImpl.awaitSideEffectsCompletion()
)
The Material UI definitely looks out of place in the IDE, but Jetbrains has started working on a theme to integrate Compose Desktop apps seamlessly with IntelliJ, Jewel.
The UI could also do with some better organization to make the actions on each component in the Debugger more clear, and also provide more info at-a-glance. Here's a screenshot from initial unfinished updates with a 3rd-party Intellij theme, showing how it could be improved
Create a version of Ballast that supports WASM targets. It will likely need to wait until all of its libraries also support WASM (coroutines, serialization, UUID, etc.) or only develop this on another branch and only release the modules which don't have any external dependencies other than Coroutines.
Add templates to the IntelliJ plugin, for more quickly creating the boilerplate necessary
Please, add Wasm target support.
While the current recommendation for Ballast Navigation is to keep ViewModels ephemeral and manage data shared between screens in a Repository, there are still some valid use cases for keeping a ViewModel instance alive and tied to the state of the Backstack.
For instance, a sub-flow that shares data among multiple steps in a mobile view. This data is technically persistent in that it is shared by multiple screens, but ephemeral in that the data should be cleared once the user exits or completes the flow. The current method would require managing that data globally in an application Repository, but then you need to keep track of when the user has left the flow to know when to clear it. This is a lot of bookkeeping that is easy to get wrong if done manually.
I think the best solution is to have an optional feature in the ballast-navigation
artifact that scopes CoroutineScope instances to each entry in the Backstack. The scope is killed once the Backstack entry leaves the Backstack. Users then register ViewModels or other dependencies against that CoroutineScope. Notably, this API is not a full DI feature. It allows you to register a factory function to create some instance, and an API to get or create those instances. The CoroutineScope allows one to hook into cleanup events.
This basically reimplements the Android ViewModel provider functionality, but in KMP, and with less ceremony and no reflection.
The Debugger UI is nice, but it would be nicer if it was bundled as an IntelliJ plugin instead. IntelliJ plugins already support Compose Desktop, so it's more of a matter of getting the project structured to build and publish the plugin, and move the debugger UI code there instead
Ballast is basically an FSM already, so it would be nice to have a proper FSM DSL like Tinder/StateMachine backed by a Ballast ViewModel, so that it could be used in a multiplatform project.
It would also be nice to extend that DSL to track additional state in the machine rather than just the declarated Type of each state, and send data through the Edges to create distinct States. Additionally, being able to extend the FSM with push-down automata would also be great, being able to inspect a stack of states and use that to determine the results of new actions.
Being able to integrate this DSL as an InputFilter
would able be nice
In addition to running "scripts" which send a series of Inputs and assert the result of all of them, there should be a way to mock everything out except the InputHandler. This new helper function should create a mock InputHandlerScope
and stub out everything except for a StateFlow of the current state, and pass that with a single Input to an InputHandler. After running the test, it will collect TestResults
and deliver those to the test for making assertions
Hi. I've got an issue with my text field adding extra characters. I checked it using mutableStateOf and it works as expected. Code related to the email below, nothing fancy...
in Contract
internal object LoginContract {
data class State(
val email: String = "",
)
sealed interface Inputs {
data class ChangeEmail(val newEmail: String) : Inputs
}
...
}
in Content
OutlinedTextField(
text = state.email,
onTextChange = { vm.trySend(LoginContract.Inputs.ChangeEmail(it)) },
...
}
in Input
internal class LoginInputHandler : KoinComponent, InputHandler<LoginContract.Inputs, LoginContract.Events, LoginContract.State> {
override suspend fun LoginInput.handleInput(input: LoginContract.Inputs) = when (input) {
is LoginContract.Inputs.ChangeEmail -> updateState { it.copy(email = input.newEmail) }
...
}
}
A module to allow synchronizing multiple clients over a remote connection.
With ballast navigation it is easy to go to a /null
path when navigating past the top of the back stack with the back button.
See reproduction number 1 from https://github.com/rocketraman/test-ballast-4-csce.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.