fabulous-dev / Fabulous

Declarative UI framework for cross-platform mobile & desktop apps, using MVU and F# functional programming
https://fabulous.dev
Apache License 2.0
1.14k stars 121 forks source link

Support for alternative frontends in Fabulous #831

Closed Dolfik1 closed 1 year ago

Dolfik1 commented 3 years ago

Use case

Fabulous uses Xamarin.Forms as frontend to draw UI. XF have many problems: lot of bugs, performance issues, platform-specific bugs, poor support from developers. Moreover, we need to write lot of boilerplate code to add native custom control (XF renderer and Fabulous DSL wrapper).

Proposal

Add support for alternative frontends for Fabulous (for example, Android native and iOS native). This will open up many possibilities:

  1. Write totally native apps with Fabulous (single/milti platform);
  2. Use shared (cross-platform) update/model code and native (platform-specific) view code. This gives even more possibilities with the new component architecture;
  3. Native Fabulous also can be used in XF projects as custom renderer which takes a single argument (model).

I did a proof of concept for Fabulous Android. This required some changes in the code generator (see my comments for PR):

https://github.com/Dolfik1/Fabulous/pull/1

https://user-images.githubusercontent.com/5128766/103538504-5ce5c880-4ea7-11eb-902b-93bed01e97cf.mp4

TimLariviere commented 3 years ago

That's the idea behind the splitting of Fabulous to Fabulous and Fabulous.XamarinForms. There is an initial support for it already. Some PoCs were made as well: https://github.com/TimLariviere/Fabulous.iOS, https://github.com/TimLariviere/Fabulous.WPF, https://github.com/unoplatform/uno.fabulous.

Fabulous.CodeGen might need changes to correctly accomodate a new platform. Don't hesitate to send small changes that we can integrate into Fabulous.CodeGen without breaking the current behavior.

Just need to state it though, this repository will keep its focus on Xamarin.Forms (and later on MAUI). Supporting a new frontend is an immense workload. I'm personally on XF alone for 2 years now, and there is still so much to do. 😄

So if you want a different frontend, you'll need to manage it yourself. Of course, I will do everything possible to integrate changes and new features you require into Fabulous and Fabulous.CodeGen to make it simple for you.

Dolfik1 commented 3 years ago

There are only one problem with codegen for Android at this moment: we can not instantiate types with constructors. Every View in Android have Context argument. Context argument can be taken from View's parent (or View's Activity if View have no parents). Do you have any thoughts how to change CodeGen/Fabulous behaviour to add support for constructors?

TimLariviere commented 3 years ago

CodeGen works by going through a series of steps.

It has been built to be flexible. If you wish to change one of the steps' behavior, you can swap it out with your own logic. You can find the default logic here: https://github.com/fsprojects/Fabulous/blob/3465eb59953ba7a4280dd3825f84c7ee4ca0687c/Fabulous.CodeGen/src/Fabulous.CodeGen/Program.fs#L117-L124

By itself, Fabulous.CodeGen can't be run. It needs to be hosted in another application that provides it the missing pieces. In the case of Fabulous.XF, this is the project Fabulous.XF.Generator. https://github.com/fsprojects/Fabulous/tree/master/Fabulous.XamarinForms/tools/Fabulous.XamarinForms.Generator

In this specific project, I wrote an advanced Optimizer step specifically for Xamarin.Forms types that I inject into the program. https://github.com/fsprojects/Fabulous/blob/3465eb59953ba7a4280dd3825f84c7ee4ca0687c/Fabulous.XamarinForms/tools/Fabulous.XamarinForms.Generator/Program.fs#L40-L54

All the steps are replaceable through the Program record. So my first guess is that you should be able to adapt those steps to your use case.

Initially I tried to add support for constructor parameters, but for the frontends I had in head (XF, WPF, Uno, Avalonia) they did not use them. It complicated things a lot for CodeGen.

Though in the case of Android, since it's always the same parameter (Context), I think you can change the last step (CodeGenerator) to output the parameter in the code directly. Might need to adapt upstream steps too to not filter out types with that one constructor parameter.

And if you have any idea to support correctly constructor parameters for every frontends, I'm open to ideas.

Dolfik1 commented 3 years ago

I think we need to add constructor parameter for type to json config. However, the ViewElement has a Create function that has no parameters. So, we need a way to pass parameters to Create function. Ofcourse, we can use array of objects, but this way is unclear.

I can try to add constructor parameter in code generator, but what about ViewElement.Create function?

TimLariviere commented 3 years ago

I can see an obj optional parameter for the Create method. This will let you pass any value you need (like a record type holding your Context value). Frontends like XF will just ignore it.

In addition to that, when the components PR passes (this month normally), you'll be able to create your own AndroidViewElement by implementing the interface IViewElement. This will let you write your own logic for Create, including reading your record type from the obj parameter. Something like

member this.Create(programDefinition, ?additionalData) =
    let context =
        match additionalData with
        | Some (?: AndroidConstructorData as d) -> d.Context
        | _ -> failwith "Context required"

    let control = createControl context
    control
Dolfik1 commented 3 years ago

I am currently working on Fabulous.iOS and Fabulous.Android. I will post here problems I faced to take into account in future Fabulous releases.

Dolfik1 commented 3 years ago

In some cases we need to inject properties into constructors.

For example UIKit.UIBarButtonItem:

Screenshot 2021-01-20 at 13 45 48

There is constructor with systemItem parameter. This parameter can be specified only in constructor and can not be changed. We also can create UIKit.UIBarButtonItem without systemItem parameter:

Screenshot 2021-01-20 at 13 49 45

We can specify required constructor with createCode property, but Fabulous does not pass ViewElement into Create function.

TimLariviere commented 3 years ago

We can specify required constructor with createCode property, but Fabulous does not pass ViewElement into Create function.

I do agree with you that we should change Fabulous to pass the ViewElement to the Create function.

Regarding your need to access the Android Context when creating a control, where does that context come from? Does it come from the root of your application? (e.g. Program.mk... |> XamarinFormsProgram.run app) Or does it come from one of the parent control in the UI tree?

Dolfik1 commented 3 years ago

Regarding your need to access the Android Context when creating a control, where does that context come from? Does it come from the root of your application? (e.g. Program.mk... |> XamarinFormsProgram.run app) Or does it come from one of the parent control in the UI tree?

Actually Android context come from View's parent: https://github.com/Dolfik1/Fabulous/blob/03e825cad92b3b9a77adc4a3c6c171d803b24372/Fabulous.XamarinForms/src/Fabulous.Android/Collections.fs#L309

Android may also have no parent view. In this case view should be attached to Fragment or Activity (Android Fragment also should be attached to Activity or View).

https://github.com/Dolfik1/Fabulous/blob/03e825cad92b3b9a77adc4a3c6c171d803b24372/Fabulous.XamarinForms/src/Fabulous.Android/Host.fs#L19

https://github.com/Dolfik1/Fabulous/blob/03e825cad92b3b9a77adc4a3c6c171d803b24372/Fabulous.CodeGen/src/Fabulous.CodeGen/Generator/CodeGenerator.fs#L95

TimLariviere commented 3 years ago

If you can always access the context from the direct parent, maybe we could change Create to static member Create(curr: ViewElement, parentOpt: obj voption) then?

Fabulous can provide the parent control instance (XF or Android, or any other) to the create function.

Dolfik1 commented 3 years ago

If you can always access the context from the direct parent, maybe we could change Create to static member Create(curr: ViewElement, parentOpt: obj voption) then?

Fabulous can provide the parent control instance (XF or Android, or any other) to the create function.

This should work. I will try this approach later.

Dolfik1 commented 3 years ago

iOS constraints can reference a parent view from child view. Currently this is not possible with Fabulous. Fabulous create View and Update it's properties before attach view to parent. We need somehow apply ViewElement properties to View after attach to parent.

Dolfik1 commented 3 years ago

Hm. iOS do not allow to reference parent from child in constraints. It looks like visual disigner convert child -> parent constaint to parent -> child.

Dolfik1 commented 3 years ago

iOS constraints can reference a parent view from child view. Currently this is not possible with Fabulous. Fabulous create View and Update it's properties before attach view to parent. We need somehow apply ViewElement properties to View after attach to parent.

This is still actual. I used wrong method to update constraints.

TimLariviere commented 1 year ago

Fabulous now supports Xamarin.Forms, Maui, Avalonia, and potentially more.