I have set up a standard SAFE application with the dotnet new SAFE command resulting in the two Server and Client projects (and the Shared folder).
The Client project has the Client.fs file with the view function and the bootstrapping code, which is just as the template generates it:
Program.mkProgram init update view
#if DEBUG
|> Program.withConsoleTrace
|> Program.withHMR
#endif
|> Program.withReact "elmish-app"
#if DEBUG
|> Program.withDebugger
#endif
|> Program.run
I have now added a simple component:
type MyComponentProps = {
data : int
}
type MyComponent(initialProps) =
inherit Component<MyComponentProps, obj>(initialProps)
do
base.setInitState({ data = initialProps.data })
override this.render() =
h1 [] [str ("Hello World " + this.props.data.ToString())]
override this.componentDidMount() =
// Do something useful here... ;)
console.log("Component Did Mount!")
But I cannot figure out how to properly instantiate this component in the view function. I really think this ought to work:
let view (model : Model) (dispatch : Msg -> unit) =
MyComponent({data = 42}) :> ReactElement
But this results in the browser in the - now - dreaded:
Error: Objects are not valid as a React child (found: object with keys {props, context, refs, updater, state}). If you meant to render a collection of children, use an array instead. I really don't get that since the type is ReactElement which should be fine...
Instantiating the component manually and calling render() does sort of work, but of course componentDidMount() is not called:
let myComponent = MyComponent({data = 42})
myComponent.render()
So: How do I get MyComponent properly instantiated and "injected" into React?
You can use ofType from Fable.Helpers.React
open Fable.Helpers.React
let view (model : Model) (dispatch : Msg -> unit) =
ofType<MyComponent,_,_> { data = 42 } []
Related
I want to load data from Firestore, and combine it with other data using Flow combine()
ViewModel:
private val userCurrentProject = MutableStateFlow("")
val projects = repository
.listenToProject() //listening via Firestore snapshot listener, no problem here
.combine(userCurrentProject) { projects, currentProjectName ->
// combine works and called normally
projects.map { project ->
project.apply {
isUserCurrentProject = name == currentProjectName
}
}
}
fun setCurrentProject(projectName: String) = viewModelScope.launch {
userCurrentProject.emit(projectName)
}
Composables:
fun ProjectListScreen(navController: NavHostController, viewModel: ProjectsViewModel) {
val projects by viewModel.projects.collectAsState(initial = emptyList())
// This is where the problem started
// Lazy column not updated when projects flow is emitting new value
// Even Timber log does not called
Timber.d("Projects : $projects")
LazyColumn {
items(projects) { project ->
ProjectItem(project = project) {
currentlySelectedProject = project
scope.launch { bottomSheetState.show() }
}
}
}
The flow is working normally, but the state never got updated, I don't know why. Maybe this is a problem with collectAsState()?
But the state is updated when I navigate to next screen (add new project screen), then press back (popBackStack)
NB: using asLiveData() with observeAsState() does not work either.
I've finally found the answer
The culprit is that a State of custom object/class behaves differently than a state of primitives (String, Int, etc.)
For a State of object, you need to use copy()
So I just changed this part of ViewModel
val projects = repository
.listenProject()
.combine(userCurrentProject) { projects, currentProjectName ->
projects.map { project ->
// use copy instead of apply
val isCurrentProject = project.name == currentProjectName
project.copy(isUserCurrentProject = isCurrentProject)
}
}
From the documentation, I can pass string, integer etc. But how can I pass objects on navigation?
Note: If I set the argument type parcelable then the app crashes with java.lang.UnsupportedOperationException: Parcelables don't support default values..
composable(
"vendor/details/{vendor}",
arguments = listOf(navArgument("vendor") {
type = NavType.ParcelableType(Vendor::class.java)
})
) {
// ...
}
The following workarounds based on navigation-compose version 2.4.0-alpha05.
I found 2 workarounds for passing objects.
1. Convert the object into JSON string:
Here we can pass the objects using the JSON string representation of the object.
Example code:
val ROUTE_USER_DETAILS = "user-details/user={user}"
// Pass data (I am using Moshi here)
val user = User(id = 1, name = "John Doe") // User is a data class.
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(User::class.java).lenient()
val userJson = jsonAdapter.toJson(user)
navController.navigate(
ROUTE_USER_DETAILS.replace("{user}", userJson)
)
// Receive Data
NavHost {
composable(ROUTE_USER_DETAILS) { backStackEntry ->
val userJson = backStackEntry.arguments?.getString("user")
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(User::class.java).lenient()
val userObject = jsonAdapter.fromJson(userJson)
UserDetailsView(userObject) // Here UserDetailsView is a composable.
}
}
// Composable function/view
#Composable
fun UserDetailsView(
user: User
){
// ...
}
2. Passing the object using NavBackStackEntry:
Here we can pass data using navController.currentBackStackEntry and receive data using navController.previousBackStackEntry.
Example code:
val ROUTE_USER_DETAILS = "user-details/{user}"
// Pass data
val user = User(id = 1, name = "John Doe") // User is a parcelable data class.
navController.currentBackStackEntry?.arguments?.putParcelable("user", user)
navController.navigate(ROUTE_USER_DETAILS)
// Receive data
NavHost {
composable(ROUTE_USER_DETAILS) { backStackEntry ->
val userObject = navController.previousBackStackEntry?.arguments?.getParcelable<User>("user")
UserDetailsView(userObject) // Here UserDetailsView is a composable.
}
}
// Composable function/view
#Composable
fun UserDetailsView(
user: User
){
// ...
}
Important Note: The 2nd solution will not work if we pop up back stacks on navigate.
Parcelables currently don't support default values so you need to pass your object as String value. Yes it is a work around.. So instead of passing object itself as Parcelize object we can turn that object into JSON (String) and pass it through navigation and then parse that JSON back to Object at destination. You can use GSON for object to json string conversion...
Json To Object
fun <A> String.fromJson(type: Class<A>): A {
return Gson().fromJson(this, type)
}
Object To Json String
fun <A> A.toJson(): String? {
return Gson().toJson(this)
}
User NavType.StringType instead of NavType.ParcelableType..
composable("detail/{item}",
arguments = listOf(navArgument("item") {
type = NavType.StringType
})
) {
it.arguments?.getString("item")?.let { jsonString ->
val user = jsonString.fromJson(User::class.java)
DetailScreen( navController = navController, user = user )
}
}
Now navigate by passing string..
val userString = user.toJson()
navController.navigate(detail/$userString")
EDIT: There is also a limit for the Json-String that you can navigate. If the length of the Json-String is tooo long then the NavController won't recognize your Composable Route eventually throw an exception... Another work around would be to use a Global Variable and set its value in before navigating.. then pass this value as arguments in your Composable Functions..
var globalUser : User? = null // global object somewhere in your code
.....
.....
// While Navigating
click { user->
globalUser = user
navController.navigate(detail/$userString")
}
// Composable
composable( "detail") {
DetailScreen(
navController = navController,
globalUser)
}
NOTE :-> ViewModels can also be used to achieve this..
Let me give you very simple answers.
We have different options like.
Using Arguments but issue with this is that you can't share long or complex objects, only simple types like Int, String, etc.
Now you are thinking about converting objects to JsonString and trying to pass it, but this trick only works for small or easy objects.
Exception look like this:
java.lang.IllegalArgumentException: Navigation destination that matches request NavDeepLinkRequest{ uri="VERY LONG OBJECT STRING" } cannot be found in the navigation graph NavGraph(0x0) startDestination={Destination(0x2e9fc7db) route=Screen_A}
Now we have a Parsable Type in navArgument, but we need to put that object in current backStack and need to retrieve from next screen. The problem with this solution is you need keep that screen in your backStack. You can't PopOut your backStack. Like, if you want to popout your Login Screen when you navigate to Main Screen, then you can't retrieve Object from Login Screen to Main Screen.
You need to Create SharedViewModel. Make sure you only use shared state and only use this technique when above two are not suitable for you.
With Arguments:
You can just make this object Serializable and pass it to the backStackEntry arguments, also you can pass String, Long etc :
data class User (val name:String) : java.io.Serializable
val user = User("Bob")
navController.currentBackStackEntry?.arguments?.apply {
putString("your_key", "key value")
putSerializable("USER", user)
)
}
to get value from arguments you need to do next:
navController.previousBackStackEntry?.arguments?.customGetSerializable("USER")
code for customGetSerializable function:
#Suppress("DEPRECATION")
inline fun <reified T : Serializable> Bundle.customGetSerializable(key: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) getSerializable(key, T::class.java)
else getSerializable(key) as? T
}
With savedStateHandle
Sometimes you have nullable arguments, so you can use savedStateHandle:
appState.navController.currentBackStackEntry?.savedStateHandle?.set("USER", user)
and get value:
navController.previousBackStackEntry?.savedStateHandle?.get("USER")
#HiltViewModel
class JobViewModel : ViewModel() {
var jobs by mutableStateOf<Job?>(null)
private set
fun allJob(job:Job)
{
Toast.makeText(context,"addJob ${job.companyName}", Toast.LENGTH_LONG).show()
jobs=job
}
#Composable
fun HomeNavGraph(navController: NavHostController,
) {
val jobViewModel:JobViewModel= viewModel() // note:- same jobViewModel pass
in argument because instance should be same , otherwise value will null
val context = LocalContext.current
NavHost(
navController = navController,
startDestination = NavigationItems.Jobs.route
) {
composable(
route = NavigationItems.Jobs.route
) {
JobsScreen(navController,jobViewModel)
}
composable(
route= NavigationItems.JobDescriptionScreen.route
)
{
JobDescriptionScreen(jobViewModel=jobViewModel)
}
}
}
}
in function argument (jobViewModel: JobViewModel)
items(lazyJobItems) {
job -> Surface(modifier = Modifier.clickable {
if (job != null) {
jobViewModel.allJob(job=job)
navController.navigate(NavigationItems.JobDescriptionScreen.route)
}
I am having problems with a test in a netcoreapp2.2 .net core test project.
Before the tests starts I need to fetch some data that will be shared between the tests.
However, when running the following test from command line it will hang.
Executing the test like this:
dotnet test --filter "Test async initialization"
The faulty code looks like this:
let c = new HttpClient (BaseAddress = (Uri "https://swapi.co/api/people/1/"))
let luke =
async {
return! c.GetStringAsync "" |> Async.AwaitTask
} |> Async.RunSynchronously
[<Fact>]
let ``Test async initialization`` () =
Assert.NotNull(luke)
While if I put the creation of the HttpClient inside the luke fetcher like this it works:
let luke =
let c = new HttpClient (BaseAddress = (Uri "https://swapi.co/api/people/1/"))
async {
return! c.GetStringAsync "" |> Async.AwaitTask
} |> Async.RunSynchronously
[<Fact>]
let ``Test async initialization`` () =
Assert.NotNull(luke)
This means I can't share the same HttpClient between different fetchers.
Anyone knows what is going on, and how to share the same client between multiple functions?
The problem is caused because the "initialization" code isn't really initialization code. Those are just two static fields that will be evaluated only when requested. If you debug the unit test you'll see that c and luke execute only when execution reaches the line
Assert.NotNull(luke)
If you use a decompiler like JustDecompile you'll see that the module's code is placed in a static class called Tests$ whose static constructor initializes its own c and luke properties. Test async initialization is placed in a Tests class with its own c and luke properties that delegate to the Tests$ class.
Long story sort, none of that "initialization" code runs until the value of luke is requested. I don't know why that ends up blocking the test, most likely there's a conflict with the test runner. It's enough that the initialization code doesn't run at initialization.
To make the initialization code run when it should, a "classic" test type can be used :
namespace MyTests
open System
open Xunit
open System.Net.Http
open Xunit.Abstractions
type Tests() =
static let c = new HttpClient (BaseAddress = (Uri "https://swapi.co/api/people/1/"))
static let luke =
async {
return! c.GetStringAsync "" |> Async.AwaitTask
} |> Async.RunSynchronously
static do
//Pity we can't actually print here
printfn "Even more initialization!"
[<Fact>]
let ``Test async initialization`` () =
Assert.NotNull(luke)
The static bindings in this case are executed before any of the tests, as they should, and the code doesn't block. This initialization will happen only once.
To capture output the test class constructor should accept an ITestOutputHelper parameter. That's easy to do now that we have a test class :
type Tests(output:ITestOutputHelper) =
...
[<Fact>]
let ``Test async initialization`` () =
Assert.NotNull(luke)
output.WriteLine "It worked!"
Per-test initialization should go in a do block :
type Tests(output:ITestOutputHelper) =
do
output.WriteLine "This prints before each test"
I have the following code in F#:
let CreateSampleDataFromJson<'T>(path) =
let uri = new Uri(path)
async {
let file = StorageFile.GetFileFromApplicationUriAsync(uri)
let jsonText = FileIO.ReadTextAsync(file)
return JsonObject<'T>.Parse(jsonText)
}
The problem I'm having is that file is an IAsyncOperation<StorageFile> and not a StorageFile as ReadTextAsync expects.
In C# you can do something similar to this:
var file = await StorageFile.GetFileFromApplicationUriAsync(uri)
i.e.
public async Task<T> CreateSampleDataFromUrl<T>(string path)
{
var uri = new Uri(path);
var file = await StorageFile.GetFileFromApplicationUriAsync(uri);
var jsonText = await FileIO.ReadTextAsync(file);
return JsonObject<T>.Parse(jsonText);
}
The problem is that I don't know how to await an IAsyncOperation in F#. The usual let! doesn't work. i.e. the following fails to compile:
async {
let! file = StorageFile.GetFileFromApplicationUriAsync(uri)
With the compiler error:
error FS0001: This expression was expected to have type Async<'a> but here has type IAsyncOperation<StorageFile>
I found a document that said there's an AsTask() extension method defined in the System.WindowsRuntimeSystemExtensions class which I can use as follows:
let! file = StorageFile.GetFileFromApplicationUriAsync(uri).AsTask() |> Async.AwaitTask
Is there a standard way of doing this or something available in an F# library somewhere that makes this a bit nicer?
Your solution seems fine by me. If you're looking for a nicer syntax, how about rolling it into a function like this (without the possibly gratuitous type annotations):
let await<'a> (op: IAsyncOperation<'a>) : Async<'a> =
op.AsTask() |> Async.AwaitTask
This will give you the almost exact same syntax you'd see in c#:
async {
let! file = await <| StorageFile.GetFileFromApplicationUriAsync(uri)
...
}
The compiler errors you were getting with your previous approaches are to be expected. All async workflow cares about is the F#-specific Async type. This type gives you a way to interop with the rest of .NET world through Tasks, but that's it. IAsyncOperation is from a 'different part of the world', I wouldn't expect F# core libraries to support it anytime soon.
In an Actor Model, the actors have some sort of message loop where messages are matched using e.g. pattern matching (depending on language ofc)
e.g. pseudo F#
let message_loop() =
let! message = receive_message() //sync receive message
match message with
| Foo(a,b) -> DoStuff(a,b)
| Bar(c) -> DoOtherStuff(c)
| _ -> error ("unknown message")
message_loop()
So pretty much, a message signature is matched and associated with some action to perform on the message content.
Is there any conceptual difference between this and calling actual methods?
e.g. if I would do the following in C# 5:
class MyActor
{
//message signature Foo(a,b)
public void Foo(int a,string b)
{
Act( () => DoStuff(a,b) );
}
//message signature Bar(c)
public void Bar(int c)
{
Act( () => DoOtherStuff(c));
}
// the rest is infrasturcture code, can be refactored into an Actor Base class
//this emulates the sync behavior of the above actor
//each action is pushed onto a queue
//and then processed synchronously by the message handler
private void Act(Action action)
{
actions.Post(action);
}
private BufferBlock<Action> actions = new BufferBlock<Action>();
//this needs max degreee of parallellism = 1
private ActionBlock<Action> messageHandler = new ....
}
This way, invoking a method on MyActor will result in an async message posted onto a message queue which only handles a single kind of message; an Action.
However, the behavor associated with the message is contained in the message itself (posted from the public method)
So would this be a considered a clean way to do actors in C# 5 / Async CTP?
The benefits would be that messages are simply defined as normal messages instead of creating awkward message DTO like classes.
So would this be enough to make it work?
There is a slight difference between Task-based asynchrony and MailboxProcessor. Mailbox processor will always end up in the same thread, similar to Winforms message loop. Task keeps a SynchronizationContext. This means the same behavior for Winforms and WPF, but you could end up on a different thread when working with a thread-pool.
Otherwise, and conceptually, looks right to me.
I would say your approach is reasonable.
It is actually a good practice to encapsulate an F# agent behind an interface, itself dispatching messages to the agent:
type IPrintText =
abstract Stop : unit -> unit
abstract Print : string -> unit
module Printer =
type private Message =
| PrintText of string
| Stop
let Start () =
let agent =
MailboxProcessor.Start (fun inbox ->
let rec loop () = async {
let! msg = inbox.Receive()
return!
match msg with
| PrintText text ->
printfn "%s" text
loop ()
| Stop -> async.Zero()
}
loop ())
{ new IPrintText with
member x.Stop () = agent.Post Stop
member x.Print text = agent.Post <| PrintText text }
let agent = Printer.Start ()
agent.Print "Test"
agent.Stop ()