-
-
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
Multi page applicatiion #144
Comments
In my opinion, each page should have its own module with its But this requires some extra work. Thanks to @dsyme's help on Slack, I've made a small sample that splits one page per file instead of everything the App module. If you want to take a look: https://github.com/TimLariviere/ElmishContacts It's certainly not the most optimized example, and I wonder if I can remove some code. You can see it in action here: The downside compared to one mega file is that it requires a lot more messages to make a multi-page action.
|
Could you make the same sample with XAML pages? |
I might give it a shot in the next few days. |
@xperiandri There was a multi-page half-elmish sample implementing a master-detail app but I removed it, see https://github.com/fsprojects/Elmish.XamarinForms/tree/d1f3a833bde59983cc671eab4b8d781c0dcb3307/Samples/StaticXaml/MasterDetailApp. As mentioned on another thread I wasn't happy about the composition model |
So what is your point of XAML-elmish? Will you introduce something to make it less clumsy? |
Because defining UI in code is definitely not what I ever considered to do. |
EXF is primarily a code-oriented framework oriented around full-elmish, with an added half-elmish Xaml option. In full-elmish the correspondence between the We'd definitely accept further samples under "Samples/StaticView" and any contributions to make half-elmish easier. |
I am just digging through the docs, issues, and the samples to get an idea on how all this is intended to work. I ended up with the same question as raised here as I spent the last couple of years working on a fairly large Xamarin.iOS project with in the end roughly ~100 dialogs (-> 100 controllers + 100 corresponding view models). I can't imagine handling an application of that size in one big This thread is almost one year old, so:
|
Fabulous (and any Elm-like framework I think) relies on a unique central processing loop, and as far as I know there's no way around that. For bigger projects like the one you mention, maybe one way to ease this is to use a "vertical split". From my experience with ElmishContacts, this shouldn't be too complicated. Hypothetically, you could also split your Fabulous app into several Fabulous apps (like the logical groups).
I didn't saw other Fabulous apps trying another approach. Mostly due to their size.
I don't think so. |
Thanks for your feedback!
What you (I) usually do is to replace the A classic example even for simple apps would be a login page for all cases in which the user isn't authenticated and a let's say start page for all other cases. Once the user signed in you probably don't want to have the login page around in your navigation stack as it would most likely never see daylight. So you make a decision at startup on whether to set the login page or the start page as your application's I guess that could somehow happen/be managed by the "main Fabulous app", the root node so to speak.
Random thoughts: Let's say I have a page like that. It leverages MVU based on the Half Elmish functionality of Fabulous and it could theoretically be instantiated without the small ceremony in here. This would leave two questions: 1) How to navigate, and 2) how to manage (load/save) the state. Navigation could be done by XF's navigation model. Easy. And State management would be possible in various ways (reading from/writing to a database, using a Redux store etc.). So at the end of the day it would leave us with pages that leverage MVU for themselves but are not coupled together in one big application. I can't tell right now if this would be a good idea or a step backwards, though :-). |
Turns out @Zaid-Ajaj already dealt with it:
Update: There's also a helpful comment by @cmeeren there linking to some reddit threads and this one. |
Yes, I watched the talk by @Zaid-Ajaj with great interest, but he seems to advocate patterns that are somewhat opposite to other clear Elm/TEA/MVU scaling guidelines. Componentization can, as I understand it, lead to a lot of boilerplate, duplicated state with possibility for bugs, etc. For the record, here are the links from my comment to that video: https://www.reddit.com/r/elm/comments/5xdl9z/elm_architecture_with_a_reduxlike_store_pattern/ https://www.reddit.com/r/elm/comments/5jd2xn/how_to_structure_elm_with_multiple_models/dbuu0m4/ https://www.reddit.com/r/elm/comments/65s0g4/resources_regarding_scaling_elm_apps/ https://dev.to/elmupdate/summarising-elm-scaling-strategy-1bjn |
I gave it a quick try. Result: It's already possible to host one program per page. See this sample where from the Main Page you can navigate to Page 2 which is actually running the same stuff with the whole MVU cycle for itself. It's of course kind of dirty and not at all how the Elm stuff is intended to work with in the first place I guess. On the other hand it brings the core idea of MVU to life and deals with the main trade-off with the current state of Fabulous: Having a new abstraction layer on top of another abstraction layer that took years to become as stable and powerful as it is today: XAML (for XF). |
@cmeeren You seem to have missed the main point of my talk, I was not advocating one certain way of doing things (componentization) but rather introducing one way of scaling. As I mentioned in the very beginning of the talk, the topic poorly documented and commands were seen as black boxes so I spent a great deal of the talk explaining commands and their relation to the dispatch loop etc. because people were starting to build apps without thinking too much about these concerns. Let's say that my talk was an intro into the topic, not a "this is how you should do it" kind of thing. As for going "opposite to other clear Elm/TEA/MVU scaling guidelines", I have to disagree. In the functional world, there are too many way to come up with abstractions to break things down and componentization in Elm(ish) is just one way of doing so on component level. The argument that it eventually leads to boilerplate code, literally doesn't mean anything without context because it all depends on the complexity of the application being written: the interactions between pieces of user interface and the significance of said boilerplate with respect to the amount of the overall code. For example, if your components don't need to talk to each other, then the cost of boilerplate you need to write is affordable compared to gains of componentization. I find components easy, not just to think about but to explain to audience of entry-level Elmish which is again a big deal in teams adopting the paradigm as they make the mental jump from modelling one page to multiple pages and connecting them together in DIY fashion (people are a very important factor, think why vue.js and golang are so popular). In conclusion, componentization in Elmish is not the silver bullet but can still be successfully applied to many many user interface applications. To make a long story short, this kind of discussion really comes down to purist vs pragmatist perspective. I very much belong to the latter category which is why I don't mind a bit of boilerplate code in my apps. Hence I would still use this technique where appropriate. |
Thanks for the feedback, @Zaid-Ajaj! Disclaimer: All of the below should be taken as strong opinions, weakly held. I'm interested in learning and growing, both personally and for us as a community. And none of this is to be taken personally. :)
Absolutely, which I also mention in the comment. If the components do not need to communicate or don't share state, components are great! My point was just that in cases where components do need to talk to each other or know about each other's state, such as in your example (around 47:00 as mentioned in the comment, where the child components needed access to the parent state, which then had to be duplicated, and thus updated several places), you not only get unnecessary boilerplate (updating in several places), but also unnecessary possibility for bugs (not properly synchronizing state). The reason I say "unnecessary" is because, as described in the sources I link to, there is an alternative way to split up your functions that do not impart any unneeded boilerplate or requirement to synchronize state across components: Instead of splitting up into full MVU components, simply split your M by itself, V by itself, and U by itself by delegating to helper functions (V/U) or composing types (M). The only drawback to this is that you don't have multiple smaller MVU components that can be reasoned about in isolation in their full MVU lifecycle. But that's not possible anyway if the child component must communicate with its parent or know about its state (which is what your example shows). If a child component really is separate from the parent (a rule of thumb could be that it must be reusable across domains), then yes, I agree that it would likely be better to have a separate MVU component. :)
Let me be clear that I think you're doing an excellent job at that. :) Rock on! Now, I agree that components are easy to reason about, but as said, only insofar as they can be understood in isolation. The reason I wrote the comment (my first YouTube comment, I think!) is exactly because, based on the discussions and tips in the links I provided, components are not in fact the simplest solution if they depend on each other. I can vividly imagine new users being annoyed at the MVU pattern because they are factoring out into child components and need to duplicate state in the child component, and get bugs because of missing state synchronization (and the boilerplate needed to update the state all necessary places). How to factor out stuff when scaling is a common point of confusion among those new to MVU, because as programmers we're so very used to thinking in terms of components, but in this case (if the components are coupled by communicating upwards or sharing state), very little good comes out of it. As Richard Feldman says:
(If you haven't yet read the links I posted, I highly recommend checking them out!)
I see what you're getting at, but I don't think it has anything to do with purity in this particular case. My point is that componentization (again, unless the components are actually independent) often gives unnecessary boilerplate and introduces chance of bugs in state synchronization. On the other hand, splitting M, V and U by themselves does not lead to unnecessary boilerplate or state duplication, and IMHO are in fact easier to understand than coupled components. In my mind then (unless the components are independent), splitting M, V, and U by themselves is the pragmatic choice. :) |
@cmeeren @Zaid-Ajaj Thanks for the discussion :) Let me come back to Fabulous here and instead of thinking of abstract components as widgets or something like that let's talk about Xamarin Forms Pages. Let's say we have 3 of them:
I have a (very) hard time seeing all the local concerns of those 3 pages being represented in the global state. I do see the advantages as time-travel debugging etc. but mixing global and local concerns ... well, concerns me. For example while globally only a list of all contacts may be important, each of those pages has different local stuff to deal with (e.g. validation for 3.). Interestingly I had to deal with that (as probably any of us) in the past. One of my last approaches, which turned out to be quite successful for the team, has been using MVVM + Redux (sample, blog post). This works as follows: All 3 pages subscribe to the information held in in the global Redux Store that is of particular interest for them. While 1) may get notified when any contact changes, 2) may only update itself when the displayed contact has been updated. Besides that all of the pages are able to keep local state (within the view model). I can't help but to think of combining that global state (redux style) with MVU at the local level. 🤔 😀 |
An important question is, why? Is there a sound basis for that concern, or is it just a felling? Let's explore that for a bit. I do get where you're coming from as I've had the same thoughts myself. Interestingly I've also developed an MVVM + Redux solution for an app I have in production at work. I briefly mention it in this issue: elmish/Elmish.WPF#40 (search for "The way I have solved this"). It sounds like we have more or less exactly the same architecture, because your description perfectly describes my approach, too. The reason for the Redux/MVVM hybrid was that Fabulous/Elmish.XF didn't exist when I created the app, and MVU requires highly nontrivial UI rendering support. The homegrown hybrid works well, but it's far more complex than MVU would have been. As programmers we have a very strong urge to factor out whatever feels like separate concerns. A part of me would like to have the global app model be just the truly global stuff, such as global domain objects (contacts in your example). But I've come to accept that the model in MVU is not intended to be a just a database of global domain objects and other global state. One of the main strengths of MVU is its simplicity, stemming from its unidirectional flow in a single loop. This however requires that everything you need to do business logic and build the view should be part of the model. In your example, that includes raw input so that you can validate it and display error messages. The benefit you get from MVU is fewer concepts and moving parts to keep in mind. Now, as the links I posted previously mention, you can easily split M, V, and U by themselves. So you don't need to have all text field values directly at the root of your model. You can create a separate type for the contact edit form, containing the input values and other relevant stuff, and simply have this type as a member of your main state. You can also have a separate "update" function that updates just this member, if that branch of the main update function becomes too complex. (This helper would accept as params all the global stuff it needs too, e.g. the contact list.) To hammer the point that the MVU model at first seems somewhat counter-intuitive when it comes to componentization/factoring, and because I think it might answer your question, allow me to highlight some quotes from two of the links I posted: First: Elm Architecture with a Redux-like store pattern Question:
Reply by Richard Feldman:
Then from another thread: How to structure Elm with multiple models? Question:
Reply by Richard Feldman:
And in a later comment:
The OP then gets straight to the heart of the matter:
To which Richard has this excellent answer which is highly worth reading in its entirety for all its concrete advice on splitting up M, V, and U by themselves. The core of the argument boils down to:
(Emphasis his.) In summary then, my advice, based on my own limited experience and basically everything well-founded I've read about the matter, is this: Just start developing with everything in a single model, view, and update, and if things get too big (not just "feels" too big, but actually causes you pain), treat one symptom at a time by splitting one thing at a time. Pain-driven development seems to be a good rule of thumb in the MVU world. As described, premature componentization is a root of many evils. :) |
Thanks for your feedback, very much appreciated! I've already read Richard Feldman's answers you linked to before, and I've also seen his talk on the topic. I think we must be careful not to follow a cargo cult here, but that may just be my impression ;-). In general I don't (yet) think that the Redux/MVVM approach I (probably both of us) took is more complex than TEA. At least my team had no problems working with it, despite being new to the architecture and F# at the same time. It obviously focuses only on the "MU part" and leaves the view out. But I caught myself already a couple of times thinking that something MVU does is strange or complex, but in fact isn't very much different from what I have been implementing with Redux after giving it a second thought.
It feels a bit like watching one of my kids going to touch a hotplate... could be an induction hotplate, though ;-). Thanks. I will give it a try and make my own experiments and decisions. |
Yes, one should always bring one's own best judgements to bear. Personally I'm always wary of the limitations of my own experience and very willing to listen to other's lived experiences. Richard says he has 100k lines of Elm code in production and can report that the MVU pattern in its purest form (Elm is pure, after all) works very well at that scale. He describes in concrete and detailed terms how one can solve scaling problems in ways that are consistent both internally, with the MVU pattern as a whole, and with my own experiences and reasoning. So I'm not afraid of a cult in this particular context. (Should my own future experiences indicate differently, I'll just have to reconcile that to the best of my ability when the time comes.) That said, every project is different and there may very well be some unique constraints - technical or otherwise - that calls for a solution that deviates slightly or markedly from accepted norms. Furthermore, each developer is different, and a custom architecture might make more sense to some than "idiomatic" MVU - or simply require less investment in learning, meaning there can always be valid business (or personal) reasons to use A instead of B.
Yes, the proof is in the pudding, so the only way to be sure is to try different things for oneself, making notes of the pain points of each strategy, and always being critical of whether the pain is purely emotional (because it feels unusual) or is based in real problems. (For example, I'd guess emotional pain due to a pattern feeling unusual is the source of a lot of poor FP code from experienced programmers that are new to FP.) Do let us know of your experience! :) |
That's a very good point, I am totally with you on this one, just let me say that around 47:00 I was already out of time to fully explain my discontent for the drawbacks of the approach. I remember noting that one "should be careful with components when syncing the data" but that's obviously not enough. I wanted to explain that when using this approach, people will start abusing
That's the thing, In my mind I think about components as if they were mechanical constructs, not much thinking is involved and the algorithm is simple, just implement a smaller MVU app and "connect" it later. But this "connect" part is not as straightforward as one might think once the components start talking to each other. On the other hand, using just functions is probably the best way to go but it stays "open ended" and vague as to where you should apply your refactorings and points of integration, there are no obvious rules to "just use functions" without making the code more complex than it should be (not implying that this is a problem per se). Lately, I have been narrowing down all my guidelines to just this: "Minimize the places where updates happen" "as long as the data model is correct => minimal amount of information needed" then you shouldn't be able to mess up the user interface.
I think I am in the wrong here, I though that you dismissed components altogether which is why I assumed your purist position but no, I think we are pretty much looking for the same thing (yes I looked up your links and they were very helpful) thanks a lot for sharing your insights, I really appreciate it! |
I agree with basically everything you just said. :) Of potential relevance to the topic at hand; I had an epiphany last night while working with/on Elmish.WPF. I'll write a lengthy issue on the Elmish.WPF repo describing it (because it requires some breaking changes to Elmish.WPF), but I'd like to share my insights in this conversaion because I think it might be useful. The gist of it is this: The problem with state duplication/synchronization in components is only really a problem if you have a separate Thankfully, by following Richard Feldman's advice in this comment, you can split the model, message, update, and view separately and think of terms of "which information do I need" and end up with something that is almost its own separate MVU component, with a lot of the same benefits (understandable/testable in relative isolation) but avoids the drawbacks (state duplication/synchronization). As an example, let's say you have a complicated form with a lot of inputs. Understandably, you might not want all those fields directly in your root state, or all the messages as cases directly in your top-level message type. What you can do is:
Splitting all of these by themselves and thinking in terms of which information they need avoids the problem of state duplication/synchronization, and lands you with something that is as easily testable as a separate component. For example, let's say you have some autocomplete in the form, based on a global list of entities in your main model. This autocomplete is the responsibility of the If we had designed the form as a completely separate sub-component (not accepting any "extra" data in any functions), the model would need a duplicate of the parent's entity list to be used in the Care must of course be taken to not store any derived state in the child model, too (the same goes for the parent model by itself, of course, but it's more insidious for child models). For example, a list of pre-processed entity names must not be stored for the autocomplete. If this was done, that would mean that if the parent model's list of entities is updated, the child model's entity name list becomes out of date (because the child model is only updated for child messages, so it doesn't know that the parent model's list of entities is updated). Any derived state must be the responsibility of the (Note that in some cases, I've seen derived state as a proposed solution for avoiding repeating expensive computations, but AFAIK a sprinkling of memoization and lazy views solve this without the need for duplicated state.) Let me know if this was unclear. |
I followed the advice to start with one simple This is what I came up with tonight. Actually I think it's similar to what @TimLariviere has built with the ElmishContacts sample. I decided it would be a good idea to have the pages access some global state as well, but not the state of all their siblings. So I introduced some The app's view and update functions are still the main integration points that will grow and become a bit ugly over time. But I realized that at least regarding the view function I already have such a central point in my MVVM applications as well – using view presenters. The huge amount of boilerplate code in the central update function is unfortunate, but I think I can live with that for now. |
Perhaps this tip is related to live update currently being limited to one file only? Personally I'd never want to place all code in a single file unless it's just a demo app (or another just as trivial app - I don't know of any real apps that are that simple.)
I absolutely agree. This makes sense with almost any complex part of an Elm/Elmish app. :) The key word is "split", though. In light of the recent discussion (and particularly my previous comment), I'm eager to learn: What do you get with the I can imagine |
I guess It was my conclusion of this talk https://www.youtube.com/watch?v=XpDsk374LDE. Probably it doesn't even matter how complex an app actually is, as long as more than one person is working on it at the same time you will sooner or later run into merge conflicts. (iOS Storyboards and Android Layouts calling...) But I think we agree on that.
Simplicity. Each page gets its own model, and besides that there is one shared global state for information that concerns the whole app, like session information (try to think of something like Of course I could just pass the information needed for a particular page to its update and view functions, but that would require to have some "page specific" logic in Disclaimer: I obviously have no idea if this is going to work out. It's still just an early thought-experiment. And the app I am building is fairly small, so I won't be able to make some profound conclusions in the near future.
Maybe. I think it's a trade-off between having to implement page-specific stuff at a central integration point, and dealing with the global model all the time at page level. It certainly would require some discipline and care-taking regarding the structure of the global model. On the other hand I can say that one global shared model works pretty well in the Redux world. -- Edit: I got the same feedback now 2x. I will probably try to go firsts with just passing along the information needed to the pages. Let's see how this works out. Thanks for the feedback @cmeeren! |
Only if passing parts of the app state as parameters is considered page-specific logic. :) I didn't mean that you needed the parent Furthermore, building on your authentication-related case: What if the user isn't signed in? Then
I get what you mean, but you might need to extend Personally I think that passing the required params is clearer, simpler, and safer. But I appreciate that that different people think differently. Do let us know if you try both and find out that |
I just came across this excellent article by @MangelMaxime. The "Everything is a function" section is basically saying the same thing I've been trying to here. :) |
Have you seen my edit above? I agree :-). |
Yep, I just wanted to mention the great article. :) |
Closing the issue since the initial question has received some answers. |
I'm trying understand how a multi page application is going to look.
Logically, a multi page application should be split on files where each file represents a page or a subset of the application. However, in the
AllControls
sample I've seen that the navigation stack and all pages are maintained in the same file.It makes me wonder, how will a complex application will look like? Will it have only one file with
init
,update
andview
and ViewElements will be split on modules, or it will have a few modules withinit
,update
andview
?The text was updated successfully, but these errors were encountered: