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.15k stars 122 forks source link

CollectionView diff algo in 0.55 - preview3 #760

Closed vshapenko closed 4 years ago

vshapenko commented 4 years ago

Hello! Given following code :

module App =

    type Selected =
        |Items1
        |Items2
        |Items3

    type Model = 
      {
       Items1:string list
       Items2:string list
       Items3:string list
       Selected:Selected
      }

    type Msg = 
        | Select of Selected
        | AddSelected

    let items tag initial = List.init 100 (fun i -> sprintf "I am %s index %i" tag (i+initial))
    let items1 = items "item 1" 0
    let items2 = items "item 2" 0
    let items3 = items "item 3" 0

    let initModel = { Items1 = items1;Items2 = items2;Items3=items3;Selected = Items1}

    let init () = initModel, Cmd.none

    let update msg model =
        match msg with
        | Select selected ->
            {model with Selected = selected}, Cmd.none
        | AddSelected ->
            let model=
                match model.Selected with
                |Items1 -> {model with Items1 = List.append model.Items1 (items "item 1" model.Items1.Length)}
                |Items2 -> {model with Items2 = List.append model.Items2 (items "item 2" model.Items2.Length)}
                |Items3 -> {model with Items3 =  List.append model.Items3 (items "item 3" model.Items3.Length)}
            model,Cmd.none

    let drawItem model item =
        match model.Selected with
        | Items1 -> View.Label(item)
        | Items2 -> View.StackLayout (children =[View.Label(item)])
        | Items3 -> View.Grid (children =[View.Label(item)])
    let drawSelected model dispatch= 
         let items =
            match model.Selected with
            | Items1 -> model.Items1
            | Items2 -> model.Items2
            | Items3 -> model.Items3
         View.ContentPage(
          content = View.StackLayout(padding = Thickness 20.0, verticalOptions = LayoutOptions.CenterAndExpand,
                                     children=[
                                         View.Button("Change",command= (fun()->
                                                                        let next =
                                                                            match model.Selected with
                                                                            | Items1 ->Items2
                                                                            | Items2 ->Items3
                                                                            | Items3 -> Items1

                                                                        dispatch (Select next)
                                                                        ))
                                         View.CollectionView(key= string model.Selected,items = (items|>List.map (drawItem model)))

            ]))

    let view (model: Model) dispatch =
       drawSelected model dispatch

    // Note, this declaration is needed if you enable LiveUpdate
    let program = XamarinFormsProgram.mkProgram init update view

i have a strange collection view behavior - when i press Change button, i suspect collection would be populated by items chosen by Selected flag, but instead i get old items remain in collection and a new ones. Looks like there is a bug in calculating diff for ItemsView.

TimLariviere commented 4 years ago

@vshapenko The PR #765 fixes the issue.

Also noticed the above sample was quite slow (measured up to 8 seconds on iOS) but after inspection, most of the time is spent outside of Fabulous (Fabulous spends like 2ms, Xamarin.Forms takes the remaining of the 8 seconds to create the items everytime on insert or replace).

It's because of various factors:

The reason here seems to be mostly the latest.

Tested it with the following code:

match model.Selected with
| Items1 -> model.Items1 |> List.map (fun item -> View.Grid (children = [ View.Label(item) ]))
| Items2 -> model.Items2 |> List.map (fun item -> View.Grid (children = [ View.Button(item) ]))
| Items3 -> model.Items3 |> List.map (fun item -> View.Grid (children = [ View.Entry(item) ]))

It immediately fell to ~150ms when clicking the "Change" button.

So the recommendations I can make for your sample:

Combining same root type + bigger height (tested at 150) gives ~30ms for the sample above.

Also don't hesitate to make use of performance helpers like dependsOn when you know your data is not likely to change.

let drawSelected model dispatch =
    let items1 = model.Items1 |> List.map (fun item -> 
        dependsOn item (fun _ item ->
            View.Label(text = item)
        )
    )
    let items2 = model.Items2 |> List.map (fun item -> 
        dependsOn item (fun _ item ->
            View.StackLayout (children =[View.Label(item)])
        )
    )
    let items3 = model.Items3 |> List.map (fun item -> 
        dependsOn item (fun _ item ->
            View.Grid (children =[View.Label(item)])
        )
    )

    View.ContentPage(
        View.StackLayout(
            verticalOptions = LayoutOptions.FillAndExpand,
            children = [
                View.Button(
                    text = "Change",
                    command = (fun () ->
                        let next =
                            match model.Selected with
                            | Items1 ->Items2
                            | Items2 ->Items3
                            | Items3 -> Items1

                        dispatch (Select next)
                    )
                )

                View.CollectionView(key = "Items1", isVisible = (model.Selected = Items1), items = items1)
                View.CollectionView(key = "Items2", isVisible = (model.Selected = Items2), items = items2)
                View.CollectionView(key = "Items3", isVisible = (model.Selected = Items3), items = items3)
            ]
        )
    )