-
-
Notifications
You must be signed in to change notification settings - Fork 122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Experiments: Stronger typing, Mixing Dynamic/Static views, Componentization, SwiftUI-like DSL #738
Comments
Stronger typing & attribute-based diffingTo support those meant to rewrite most of the foundation of Fabulous. I went the simplest definition of ViewElement as possible type Attribute =
| Property of set: (obj * obj -> unit) * unset: (obj -> unit)
| Event of subscribe: (obj * obj -> unit) * unsubscribe: (obj * obj -> unit)
type ViewElement(targetType: Type, create: unit -> obj, attributes: KeyValuePair<Attribute, obj> array) =
member x.TargetType = targetType
member x.Attributes = attributes
member x.Create() = create()
module DynamicViews =
type DynamicViewElement<'T>(create: unit -> 'T, attributes) =
inherit ViewElement(typeof<'T>, (create >> box), attributes)
In Fabulous.XamarinForms, to check the correct usage of the controls at compile-time, I introduced interfaces mapping to the most interesting sets of controls: type IBindableObject = interface end
type IPage = inherit IBindableObject
type IView = inherit IBindableObject
type ICell = inherit IBindableObject
type IMenuItem = inherit IBindableObject Those interfaces are used by types inheriting type DynamicBindableObject<'T when 'T :> BindableObject>(create, attribs) =
inherit DynamicViewElement<'T>(create, attribs)
interface IBindableObject
type DynamicPage<'T when 'T :> Page>(create, attribs) =
inherit DynamicBindableObject<'T>(create, attribs)
interface IPage
type DynamicView<'T when 'T :> View>(create, attribs) =
inherit DynamicBindableObject<'T>(create, attribs)
interface IView
type DynamicCell<'T when 'T :> Cell>(create, attribs) =
inherit DynamicBindableObject<'T>(create, attribs)
interface ICell And when declaring a control (e.g. This allows container controls to require a specific subset of controls. Today, writing this would compile, even though it's invalid for Xamarin.Forms (will throw a runtime exception) View.ListView([
View.Button()
]) But with the new types, it would not compile View.ListView([
View.Button() // DynamicView<Button> is not compatible with ICell
]) Regarding the namespace Fabulous.XamarinForms
type Attribute<'T> =
| Property of set: ('T * obj -> unit) * unset: (obj -> unit)
| BindableProperty of property: Xamarin.Forms.BindableProperty
| CollectionProperty of set: ('T * obj -> unit) * unset: (obj -> unit)
| AttachedProperty of property: Xamarin.Forms.BindableProperty
type EventAttribute<'T> =
| EventHandler of (obj -> IEvent<System.EventHandler<'T>, 'T>)
let mapXFAttributeToAttribute ... = (...)
// usage
let LabelTextProperty = Attribute<_>.BindableProperty Xamarin.Forms.Label.TextProperty |
Intellisense can actually tell you optional parameters. It is the right-most filter, then scroll to after let-bindings. |
@Happypig375 Yes. But only in Visual Studio (Windows). It's not the case in Visual Studio for Mac and Rider. Will include examples. |
Merging Fabulous and Fabulous.StaticViewsWith the rewrite of ViewElement from above, it allows to declare a StaticViewElement to use static views alongside dynamic views. module StaticViews =
type StaticViewElement<'T>(create: unit -> 'T, setState: (obj * obj -> unit), unsetState: (obj -> unit), state) =
inherit ViewElement(typeof<'T>, (create >> box), [| KeyValuePair(Property (setState, unsetState), state) |])
member x.State = state To keep it compatible with Fabulous.XamarinForms interfaces namespace Fabulous.XamarinForms.StaticViews
[<AbstractClass>]
type StaticView<'T when 'T :> View>() as this =
inherit StaticViewElement<'T>(this.Create, ignore, ignore, null)
interface IView
abstract Create: unit -> 'T
[<AbstractClass>]
type StaticView<'TView, 'TState when 'TView :> View>(state: 'TState) as this =
inherit StaticViewElement<'TView>(this.Create, StaticHelpers.setBindingContext, StaticHelpers.unsetBindingContext, state)
interface IView
abstract Create: unit -> 'TView With this, we can create both "stateless" static controls (Flutter's // Stateless, once created, it won't ever change
type MyButton() =
inherit StaticView<Button>()
override x.Create() =
// This could be any Xamarin.Forms instances. Like a whole page or even bigger. Even 3rd party controls
Xamarin.Forms.Button(Text = "Click me")
// Stateful, it can define bindings to listen to changes inside its BindingContext
type MyStatefulButtonState =
{ Foo: string
Click: unit -> unit }
type MyStatefulButton(foo: string, click: unit -> unit) =
inherit StaticView<Button, MyStatefulButtonState>({ Foo = foo; Click = click })
override x.Create() =
let button = Xamarin.Forms.Button(TextColor = Color.Blue)
button.SetBinding(Xamarin.Forms.Button.TextProperty, nameof x.State.Foo)
button.SetBinding(Xamarin.Forms.Button.CommandProperty, nameof x.State.Click)
button Once declared, they can be used directly with their dynamic counterparts let view model dispatch =
View.ContentPage(
View.StackLayout([
View.Label(text = "Hello")
View.ListView([
MyCell()
View.TextCell()
])
MyButton()
MyStatefulButton(foo = model.Foo, click = fun () -> dispatch Bar)
])
) Note: This will most likely change in the future because I did not check what it meant to diff a user-defined F# record at runtime. |
Running Fabulous as a componentThis one is the simplest experiment from all these. Fabulous.XamarinForms defines a type XamarinFormsHost(app: Application) =
interface IHost with
member __.GetRootView() =
match app.MainPage with
| null -> failwith "No root view"
| rootView -> rootView :> obj
member __.SetRootView(rootView) =
match rootView with
| :? Page as page -> app.MainPage <- page
| _ -> failwithf "Incorrect model type: expected a page but got a %O" (rootView.GetType())
module XamarinFormsProgram =
let runWith app arg program =
let host = XamarinFormsHost(app)
program
|> Program.runWithFabulous host arg This is great, but it prevents to use anything else with Fabulous. But simply defining a second type ContentViewHost(contentView: ContentView) =
interface IHost with
member __.GetRootView() =
match contentView.Content with
| null -> failwith "No root view"
| rootView -> rootView :> obj
member __.SetRootView(rootView) =
match rootView with
| :? View as view -> contentView.Content <- view
| _ -> failwithf "Incorrect model type: expected a View but got a %O" (rootView.GetType()) To keep everything consistent, I had to rename the Program.mkProgram init update view
|> Program.withConsoleTrace
|> XamarinFormsProgram.run app You would do either // Inside an existing app
type MyFabulousControlInXamarinForms() as this =
inherit Xamarin.Forms.ContentView
do
Component.useCmd init update view // It's simply a rename of mkProgram I tried in the experiment
|> Component.withConsoleTrace
|> Component.run this
// A full-Fabulous app
type App() as app =
inherit Xamarin.Forms.Application
do
Component.useCmd init update view
|> Component.withConsoleTrace
|> Component.runAsApplication app Note: I think we can even require "component"-Fabulous to ensure |
Why should we change the Fabulous DSL?Overall, I really like the Fabulous DSL. It's:
No repeated tokens like most other Elmish DSLs (e.g.
You can find all available properties for the control you're writing (except when IDEs go in the way...)
You only have access to available properties, ain't no Bad user experience in most IDEsWhat motivated this question is mostly a bad user experience with the various IDEs when writing an app with Fabulous. An important thing when working with a UI Framework for me is the discoverability of the various properties/events I can set. Only if I don't find anything resembling what I want to do, I go check the documentation. To allow only declaring the relevant properties, Fabulous makes heavy use of optional method parameters. But most IDEs handle many optional parameters pretty badly. Visual Studio for Mac is not great when autocompleting optional parameters. When asking for autocomplete, it will show you everything matching what you typed, regardless of the context. And if you dare put your cursor over the type to see what's the available properties, you're greeted with this tooltip. It's ok I guess. Except if you were interested in one of the first parameters. Oops, it's off-screen. Better luck next time I guess. Terser than Visual Studio for Mac (but without colors), Rider's compact tooltip is even harder to read or find anything interesting in it. Visual Studio on Windows fares a little better. At least the tooltip is compact and colored, even if it's not really useful. Intellisense still shows everything, but at least you can filter it (but you have to know it). So overall, user experience in the various IDE goes from kinda bad to terrible. A few shortcomings with the current Fabulous DSL
The current Fabulous DSL has a few shortcomings, especially with properties that can have several types set to them (e.g.
Frameworks like Xamarin.Forms change over time, and what was the way to do one thing might be replaced by something else later. Usually there's a grace period where the framework marks the property/event as obsolete before being removed for good. Fabulous can't handle that currently because we can't mark a single optional parameter as obsolete. |
Why not use Elmish.React / Feliz / Avalonia.FuncUI DSL instead?DISCLAIMER: What's following are my own opinions. You might like things in those DSLs that I don't. I'm not attacking in any way your preference. All those DSLs are great, and have pros to them. But they all lack things I like in Fabulous. Elmish.React / Avalonia.FuncUI repeat keywords that are not necessary, which means more noise, which means less readability. Elmish.React let navBrand =
Navbar.navbar [ Navbar.Color IsWhite ]
[ Container.container [ ]
[ Navbar.Brand.div [ ]
[ Navbar.Item.a [ Navbar.Item.CustomClass "brand-text" ]
[ str "SAFE Admin" ] ]
Navbar.menu [ ]
[ Navbar.Start.div [ ]
[ Navbar.Item.a [ ]
[ str "Home" ]
Navbar.Item.a [ ]
[ str "Orders" ]
Navbar.Item.a [ ]
[ str "Payments" ]
Navbar.Item.a [ ]
[ str "Exceptions" ] ] ] ] ] What's happening there? You'll only find out if you put cognitive effort into it. Avalonia.FuncUI DockPanel.create [
DockPanel.children [
Button.create [
Button.dock Dock.Bottom
Button.onClick (fun _ -> dispatch Reset)
Button.content "reset"
]
(...)
]
] That's better, but still. I know we're dealing with a DockPanel/Button, I don't really need to be reminded all the time. Feliz Bulma.footer [
color.isDanger
prop.style [ style.paddingBottom 48 ]
prop.children [
Bulma.columns [
columns.isCentered
prop.children [
Bulma.column [
column.isNarrow
prop.children [
Bulma.image [
Html.img [
prop.src Assets.logoSvg
prop.style [ style.width 200 ]
]
]
]
]
]
]
]
] It's definitely better to read than Elmish.React. Though having What does it mean to have a Overall, they're all great but I still like Fabulous DSL better. |
Fabulous as component looks very good. In our project, we use a (kind of) same technique, but we use main loop as a message passing engine. In your implementation they will be fully separated, which is great.
|
SwiftUI-like DSLI must say I was quite in awe when first playing with SwiftUI from Apple. Their IDE-integration was so sweet. Basically, you have the following: HStack(alignment = .leading) {
Label("Hello world")
.font(.title)
.color(.grey)
Image("path/to/image")
} So to use a control, you have 4 distinct parts.
The meaningful properties are not available as additional property methods. Even though F# doesn't have the ContentPage(
StackLayout(spacing = 10.) [
Label("Fabulous Todo List")
.font(NamedSize.Title)
.textColor(Color.Blue)
.horizontalOptions(LayoutOptions.Center)
.padding(top = 50., bottom = 20.)
Grid(coldefs = [ Star; Absolute 50 ]) [
Entry(
text = model.EntryValue,
textChanged = fun args -> dispatch (EntryTextChanged args.NewTextValue)
)
Button("Add", fun () -> dispatch AddTodo)
.gridColumn(1)
]
ListView(model.Todos) (fun item ->
TextCell(item)
.contextActions([
MenuItem("Delete", fun() -> dispatch (RemoveTodo item))
])
)
]
) The signal-to-noise ratio is pretty good. You're also immediately aware of what's important and what's not with the most meaningful properties inside the parentheses. If you use a Label, 100% of the time you'll want a text set to it. So make it mandatory. If you want a Button, you'll want to set a content and listen for the click so make it mandatory. Optional properties that are less-likely to be used are pushed as external methods. // Current
Label(text = "Hello", fontSize = Named NamedSize.Title)
Label(text = "Hello", fontSize = FontSize 10.)
// Versus
Label("Hello").font(NamedSize.Title)
Label("Hello").font(10.) This also allows us to provide helpers for things done often // Current
Label(text = "Hello", padding = Thickness(10.))
Label(text = "Hello", padding = Thickness(0., 50., 0., 20.))
Label(text = "Hello", padding = Thickness(20., 30.))
// Versus
Label("Hello").padding(10.)
Label("Hello").padding(top = 50., bottom = 20.)
Label("Hello").padding(leftRight = 20., topBottom = 30.)
Label("Hello").padding() // Could even have a default padding values (e.g. 5px all around) Controls can have different constructors depending how you want to use them. // Templated ListView
let items = [ 1; 2; 3; 4 ]
ListView(items) (fun item -> // knows its `int`
TextCell(item.ToString())
)
// Flexible ListView
ListView() [
for i = 0 to 10 ->
TextCell(i.ToString())
] Note: This is all super experimental, it is completely based on a "compilable" syntax, not actually ran. A lot of hurdles are to be expected before having a working sample. |
@vshapenko No, static views don't mean you'll have the hand on the controls creation. Fabulous will still create or destroy controls as it sees fit. (Fabulous won't dive inside a static view though, it'll just create or destroy the instance. What happens inside doesn't concern Fabulous.) The various issues you mention need to be fixed before that. |
@TimLariviere If you are interested we have as yet an unreleased implementation in C# (sorry) but from a coding experience it does work similar to what you have discussed about. We don't have many actual concrete classes, instead we rely on a covariant interface specification together with extension methods using this interface so that code is correctly applying the correct properties and hopefully the IDE shows just the right extension methods. Whilst our base classes don't need a generator app, since they use reflection to build a map of what properties, events & methods are available (e.g. the user could do something like .With(v=> v.Orientation,Orientation.Vertical), we do however have a generator app which then plumbs into using these reflection maps that have been built up. |
@VistianOpenSource That would be really interesting if you can share it.
Yeah. Initially, I tried that but F# lacks support for covariance. So a
This is a fine solution, but in Fabulous we tend to change the input of some properties/events so they're easier to use in a functional context with MVU. C# / XAML <Button Command="{Binding ClickCommand}" /> public ICommand ClickCommand { get; } = new RelayCommand(ExecuteClickCommand, CanExecuteClickCommand);
public void ExecuteClickCommand() { ... }
public bool CanExecuteClickCommand() { ... } Fabulous Button(command = (fun () -> ...), commandCanExecute = true) Also I know developers who will go to great length to avoid reflection in mobile apps (because of performance and linker). |
@TimLariviere On the input types sometimes being different, yes we do that as well. As well as people being able to write their own extension methods , the generator app is quite easy to add new things into since it uses handlebars to generate all of the code, meaning its very easy to drop in a different generator for just say certain properties, just add another template. On the reflection front, I've got a test app on Android that starts up in around 2 secs, its got recycler views, linear lists, edit views and text fields in it. Write regard to the reflection I used to use ReactiveUI so I took some of my learnings from that. With regard to linking the generator creates a small stub inside each extension method to ensure that it links correctly, so I can turn on full linking inside the app and it works fine, but it only pulls in what is required so keeps the size pretty small e.g. 8MB for the simple test app i referenced, or around 12,500KB for same solution with AOT turned on. |
@TimLariviere do you think it makes sense (as part of this level of re-organization) to integrate something like FSharp.Data.Adaptive or https://github.com/dsyme/fabulous-adaptive into the project? |
@zakaluka Yes, that's something I had in the back of my head. I'd like So I don't really envision to integrate Fabulous-Adaptive back in this repository, but that's definitely a thing to take into account if we can let an additional (I don't have much knowledge of what adaptive views would require though, so that might be wishful thinking). |
@TimLariviere Don mentioned the other day how intrusive the adaptive stuff can be SaturnFramework/Saturn#228 (comment) |
I think the "SwiftUI-like DSL" is definitely the way to go. Not just for Fabulous but also for Fable.React. |
|
@charlesroddie I'm still playing a lot with the new DSL and StaticView. /// This code right here is pure Xamarin.Forms. So could be XAML, C#, or F#.
/// Only the `DeleteButton` type (and constructor) is relevant for Fabulous
type DeleteButton() as target =
inherit Xamarin.Forms.Button(BackgroundColor = Color.Red)
do
target.SetBinding(Xamarin.Forms.Button.TextProperty, Binding("Text"))
target.SetBinding(Xamarin.Forms.Button.CommandProperty, Binding("Clicked")) /// The dynamic UI with a reference to the DeleteButton above
/// Basically, there would be 4 main "helpers" representing
/// the 4 categories of controls: StaticView, StaticPage, StaticCell, StaticMenuItem
let view (model: Model) =
StackLayout(children = [
Label("Hello")
.horizontalOptions(LayoutOptions.Center)
StaticView(DeleteButton, fun dispatch -> {| Text = "Reset"; Clicked = Command.msg dispatch Reset |})
.horizontalOptions(LayoutOptions.Center) // Since we use StaticView here, we can provide dynamic properties accessible on Xamarin.Forms.View
]) This allows to not have to declare a wrapper type before using an existing control, while still benefiting from the (partial) possibility of setting some dynamic properties.
If you're talking about the
Yes, I agree with you on some points. The fluent syntax feels a little wrong to implement, with the passing of an object (struct actually) around. But I see this fluent syntax more as a descriptive language than proper F# code. From the users point of view, it will just work while providing a nice syntax. I think users don't really care about the exact thing Fabulous will do behind the scenes. |
@TimLariviere I said I would let you know when we were putting our code up on Github, well we have started the process of moving our code based UI library over to Github. I’ve put some notes regarding functionality up there, the code will start appearing there this week. https://github.com/VistianOpenSource/Birch |
Although a SwiftUI DSL is indeed warranted, would the SwiftUI like DSL directly use the SwiftUI API its entirely different to usual Xamarin.iOS API calls. |
@7sharp9 No, it's more like taking inspiration from the SwiftUI syntax (declarative fluent builders). SwiftUI is quite different in its usage than Fabulous, it relies a lot on bindings. Fabulous will still use MVU. And there won't be one "Fabulous DSL". |
It would just be good to consume the native API's and controls thats all, without wrapping too much. All iOS devices work at 60fps minimum, things like the iPad will be 120fps so you really want to be using the metal renders like SwiftUI. |
Thing is there isn't that 'much' wrapping. In effect its just applying the appropriate values to properties on whatever your target views/controls are. |
@7sharp9 For that, you could write a Each time the model changes, Fabulous will provide you the previous and current ViewElements and let you apply the delta how you want. |
Following an old discussion on Gitter and a recent discussion on MAUI, I've realized we can include Application with its resources and menus in the view function instead of doing things like #696 (comment) It works quite well. let view (model: Model) =
Application(
ContentPage(
(...)
)
).menu(
MainMenu([
Menu([
MenuItem("Quit CounterApp", CloseApp)
.accelerator("cmd+q")
])
Menu("Actions", [
MenuItem("Increment", Increment)
MenuItem("Decrement", Decrement)
])
])
)
let runnerDefinition = Program.AsApplication.useCmdMsg init update view mapCmdMsgToCmd
type CounterApp () as app =
inherit Application ()
let _ =
App.runnerDefinition
|> Program.withConsoleTrace
|> Program.AsApplication.run app |
@TimLariviere I know Fabulous is what it is, it would just be good to leverage the whole of SwiftUI, hot reloading, view preview etc. |
@TimLariviere any way we can help you with the new DSL? I'm actually quite looking forward to it and would be willing to help to speed it up. |
@uxsoft Thanks for your proposition. I currently need feedback on the syntax and architecture of the PoC I got working here : https://github.com/TimLariviere/Fabulous/tree/play So if you want, you could test it. If you notice anything or want to discuss about the PoC, you can open an issue on my fork repository. (Note: if you're on macOS, you'll need to |
Sorry, I'm late to this. I'm working on an MVU library for Xamarin.Forms that took React + Redux approach: https://github.com/shirshov/laconic. Meaning virtual views, diffing, and touching actual views for changed values only. Maybe you'll find some bits useful. @TimLariviere wrote:
That's pretty much what I did, using Views created with Laconic and views created with XAML can co-exist; Laconic views can be hosted inside XAML views (the inverse is possible too, but ugly). More of C# note, but, @TimLariviere wrote:
I thought about it and abandoned the idea... Sometimes it's hard to decide which attributes are the defining ones for a control. Take BoxView for example: quite often it's used as an invisible spacer, where its presence is all that necessary, no need to set any attribute. Other times it's used as a horizontal divider line, requiring only |
@shirshov I agree that controls don't all have a clear use case and defining custom constructors for them is not easy. Also, having specific constructors and no constructor are not mutually exclusive in my opinion. |
Have you considered requiring a key for views added to |
As @TimLariviere says, and from own experience, having those properties which more than likely are going to be used not in the constructor just means you end up doing more typing. There will be many controls/views for which it doesn't make sense but for things like labels and text edit controls it does cut down on type amount you type. |
@shirshov @TimLariviere on the diffing front either the Myer or the Heckel algoritm for performance of diffs. This does accommodate quite easily issues of inserts / deletes and generally does produce an optimal update for the user interface. If you want implementations of either just let me know,I've got both of them implemented in C#. |
FYI both iOS and Android have 'diffable' abilities to update uicollectionview/recyclerview. Androids implementation uses Myers algorithm, not use what iOS uses. Hashing is very important for these implementations for them to work effectively/ |
@shirshov Yes, we introduced a @VistianOpenSource Thanks for the algorithm recommendations. |
We now have implicit yields in 4.7, so you could technically still recreate the syntax with a computation builder.
One advantage is that you can choose not to emit a child, unlike with a list.
|
@deviousasti With F# 4.7, lists can do the same. let items = [
"Implicit"
"Yields"
"Are"
if false then
"Not"
"Great"
] |
@deviousasti I thought of using computation expressions, but I find them inconvenient to write. Also I'd like for the DSL to be as lightweight as possible, both in allocations and dll size in release mode. Technically, a Grid() (seq {
...
}) But for practicality, contrary to my example here, I moved the children list has a parameter of Grid([
...
])
// or if multiple params
Grid(rowdefs = [Auto; Star], children = [
...
]) This is better when we want to add additional properties Grid(rowdefs = [Auto; Star], children = [
...
]).alignment(LayoutOptions.Center)
// compared to
(Grid(rowdefs = [Auto; Star]) [
...
]).alignment(LayoutOptions.Center) |
Right. What was I thinking. I've used that very feature in Elmish. d'oh |
I'm using computation expressions for my SwiftUI binding: chkn/Xamarin.SwiftUI#37 I find they are quite lightweight- as you can see in the disassembly on the PR, the compiler is very good at inlining them in optimized (release) builds. The only thing I find (mildly) annoying is that, as previously mentioned, you need to wrap the whole expression in parenthesis to add fluent-style properties: (Button(fun () -> printfn "clicked") {
Text("1")
Text("2")
Text("3")
})
.Background(Color.Red)
.Font(...)
// etc |
@chkn Pretty impressive work. Thanks for sharing this!
Yeah. I guess that's something we could get used to and no longer think about after a while. Maybe another way to handle this would be to abuse the fluent builders, and do something like this: Button(fun () -> printfn "clicked")
.Background(Color.Red)
.Font(...)
{
Text("1")
Text("2")
Text("3")
} Weirdly, this feels like XAML in the structure. 😄 |
@TimLariviere Thanks!
Interesting idea! I do think it's more aesthetically pleasing than the parenthetical version, but I'd hesitate to adopt that syntax for a couple reasons:
|
I implemented Swift-like syntax in another project using just plain old classes and an index accessor that takes a list: Button(fun () -> printfn "clicked")
.Background(Color.Red)
.Font(...)
.[[
Text("1")
Text("2")
Text("3")
]] It's simple and it works, but it's not as elegant as the fluent builders. |
Current Fabulous DSL is very pretty. You are right about IDE autocompletion, but I think this is IDE's problem, not Fabulous. We can try to create issue for Rider to add better support for Fabulous like syntax. Their F# team is always open to discuss. |
@Dolfik1 Even though Rider could accommodate us, it's not the primary IDE most people use and it's a paid one. I think the main problem with IntelliSense is that Fabulous only has 1 type But after that, like I explained in one of the posts at the top, the current Fabulous DSL is not really flexible and does not allow to easily extend existing controls, since all properties and events are optional constructor parameters. The new proposed DSL will be more flexible with this. Especially with attached properties that are currently quite hard to support correctly in Fabulous today. Of course, I don't plan to immediately discard the old DSL. |
Work on Fabulous v2 has started in https://github.com/TimLariviere/Fabulous-new and will include most experiments listed here. |
DISCLAIMER: All the following points are experiments only and there's no guarantee they will be available anytime in the future.
Index:
In the last few days, I've been experimenting with a couple of ideas.
Introduction
Instead of having all
View.XXX()
return untypedViewElement
, it should return a typed ViewElement to make the compiler check for invalid uses (like putting a Button as an item of ListView -- ListView only accepts Cell-derivated controls).Currently in Fabulous.XamarinForms, we diff controls by calling the Update method attached to it (e.g.
View.Button()
will have a link toViewBuilders.UpdateButton()
).This update method will check each one of the properties of Button (and will call UpdateVisualElement, etc. to update the inherited properties/events).
This is perfectly fine in 99% of the cases, except when dealing with overridden properties or attached properties.
This becomes particularly tricking to always apply the correct behavior and it results in a lot of custom code.
Instead if we went with "attribute"-based diffing like FuncUI, overridden properties could simply be dealt with by storing the overridden attribute updater instead of the base one in an attributes array (e.g. ViewElement.Attributes).
Same with attached properties, there would be no need to be aware that
Grid.Row
is an attached property applicable only on the Grid inheritedLayout<T>.Children
items.Currently you need to choose from either
Fabulous
orFabulous.StaticViews
to write apps. Mixing the 2 is not possible.Though there's instances where being able to use the 2 makes sense.
Fabulous.StaticViews makes it easy to use 3rd party controls (or custom XAML views) without spending too much time writing wrappers for it.
It would also be way easier to "market" Fabulous, less confusing on which package to use.
Fabulous.XamarinForms currently plugs itself in the Xamarin.Forms.Application.MainPage property and acts from there.
It is quite easy to allow it to plug into a ContentView.Content property instead, allowing it to run anywhere, in multiple instances, even in existing C# apps.
I really like the Fabulous DSL, though IDEs make it difficulty to know what's exactly is available when having like 20 optional method parameters.
Elmish.React - Feliz - Avalonia.FuncUI all have great advantages, but also have different issues in my opinion, I will explain why in a comment below.
So I tried various approaches to keep it close to what's Fabulous DSL offers today, while trying to make it IDE-friendly.
After pushing the F# syntax to its limit several times, I went with a SwiftUI-like syntax.
I think it offers the best balance between readability and discoverability, as well as being nice with the IDEs.
I added a little Flutter-like widgets to the mix to make it great with static views code.
Current experiments can be found in https://github.com/TimLariviere/Fabulous/tree/play
Read the comments below for more information.
The text was updated successfully, but these errors were encountered: