-
Notifications
You must be signed in to change notification settings - Fork 7.7k
Create compass-app first feature #2342
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
Create compass-app first feature #2342
Conversation
<key>com.apple.security.network.client</key> | ||
<true/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was necessary to run on macOS
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking great! Just a few notes, but this is clean.
import 'package:provider/single_child_widget.dart'; | ||
|
||
/// Configure dependencies as a list of Providers | ||
List<SingleChildWidget> get providers { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there an app architecture guarantee that this list will only be accessed when strictly necessary? I ask because I believe each access run the fill innards, reinitializing everything, instead of reusing anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the moment, this dependency tree is created on runApp()
and indeed this function shouldn't be called elsewhere, so it is only called once to setup the top level Providers.
As the dependency tree becomes more complex (and we add config options to it) we will have to convert this getter into a function, and we should be able to cache all those dependencies that don't need to be rebuilt.
Another consideration could be making this a private method in the main.dart
file to ensure it is not called elsewhere.
I think we will come with a better solution in the future when we tackle the Dependency Injection section in the Architecture docs.
Ok<T> get asOk => this as Ok<T>; | ||
|
||
/// Convenience method to cast to Error | ||
Error get asError => this as Error<T>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there be isOk
and isError
boolean getters, as well? Either way, usage docstring to suggest the right way to interrogate a yet-ambiguous Result
instance would be nice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see later on that, because this is a sealed class, pattern matching works for this. That's great! A quick docstring suggesting that would still be useful, IMO :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! I will add that.
@@ -0,0 +1,50 @@ | |||
/// Model class for Destination data | |||
class Destination { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's strictly required to use code generation for data classes, but we should at least have a clear articulation of why we are not. For example, this Destination
class lacking value equality could cause problems later on.
I am personally quite a fan of pkg:freezed
, though that is just one of multiple possible choices.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed! Freezed could be a good option for this project, and we probably want to integrate it later on, as we add more features.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be a good opportunity to 'be opinionated'. I agree that we probably want to add some sort of equality checking at the least and potentially code generation.
@miquelbeltran this isn't something that we need to add in this PR necessarily, but good to consider for later.
Widget build(BuildContext context) { | ||
return ClipRRect( | ||
borderRadius: BorderRadius.circular(10.0), | ||
// TODO: Improve image loading and caching |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does pkg:cached_network_image
deliver everything needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think so, except if the team has a different suggestion, I'd go with that as well.
FYI, the following files are generated by the Flutter tool and shouldn't be checked-in:
|
Why is the code in the |
I believe because there will be a server component at some point. |
That's correct, the example will run with a dart server app as well. Thanks for all the feedback everyone! I'm currently OOO without my laptop but will get to it soon. Glad that we keep the Result class. If anyone has ideas on how to improve it lmk as well. |
Thanks for taking the time to prepare a code example @johnpryan, it was very useful to visualize your proposal. I took a look, and also reply to your points before.
While I like them, I agree that for the sake of simplicity in the example we should avoid them until strictly necessary, so I will refactor the code to remove it.
I agree with the general idea of now making the ViewModel a long-lived top-level dependency. What I am not sure, is if we want the widgets to create and manage the lifecycle of its ViewModel` or if ViewModels should be created outside them and then injected somehow. The FWE MVVM part doesn't really address this: https://docs.flutter.dev/get-started/fwe/state-management#using-mvvm-for-your-applications-architecture For example, looking at Majid's Flutter Engineering book, there the ViewModels are created outside the Widget and passed as constructor param. So I feel everyone has a different opinion on this :) I personally would rather wrap the return ChangeNotifierProvider(
create: (context) => ResultsViewModel(
destinationRepository: context.read(),
),
child: const ResultsScreen(),
); And this code would be in the Testing is of course a bit more difficult than passing it by parameter, since it requires you to still wrap the Widget with the
Related to the previous point. Since we are using Provider, this allows us to use While I think there is value in understanding how to manage the lifecycle of a I think there will be value in presenting the different alternatives nevertheless in the documentation pages we will prepare.
I am used to them, to me "data layer" and "presentation layer" seem very standard when talking about general software architecture, so it would be consistent with the vocabulary we use when documenting the architecture design. It will be unlikely we have a middle "business layer" if we don't need use cases or similar. But I have removed the Still prefer to keep all "data" parts, e.g. repositories, models and services, in the
Perfectly fine for me, if we decide to adopt it for this, we should set up https://dart.dev/tools/linter-rules/prefer_relative_imports (and maybe even should become a default for Edit: I have pushed now some changes based on the feedback |
The |
@TytaniumDev got your feedback and implemented a Also implemented a basic light/dark theme configuration. |
I agree that we shouldn't be using a global ViewModel, and it should be created in the widget itself, like @johnpryan's example. But I also agree with @miquelbeltran that if we're using provider at all, readers would wonder why we're making extra work for ourselves by not using all of it's features. The more I think about this project, the more I think we should favor using packages less, and keeping it vanilla as possible. Big companies with complex apps (and solo devs, for that matter) are going to use this as a starting point, and add packages and remove other packages to meet their specific needs. Starting more vanilla seems like it will better serve our target audience. I don't have a strong opinion on which approach we go with, but I'm curious what everyone thinks here. @miquelbeltran @johnpryan @reidbaker
Is there a dart standard naming convention? Do the Dart/Flutter teams have a standard? For example, do they always have a I'm not aware of any standards, but if there , we should go with those. Obviously this is trivial to change in the future, so we don't need to fret about it now. |
"What I am not sure, is if we want the widgets to create and manage the lifecycle of its ViewModel` or if ViewModels should be created outside them and then injected somehow." This could be a place where my mental definition does not match the use in this pr but to me models are dart object representations of the underlying truth of some data. view_models are specific objects used by a widget (or group of widgets) to represent something on the screen. By my definition view_models should be created outside of the hosted widget as a stream (the concept not necessarily the Stream class) then when a new view_model is created the widget rebuilds. This lets you test your view_model production logic independently of your presentation logic. It also means you can write widget tests for view_model objects that your view_model production logic can't generate. Like for example if you had a user profile without a name you could ensure the widget didnt crash and maybe showed empty if that data was missing AND you could write a test in your view_model production code that ensured that if name was missing you used a translated string for "missing name please contact support". "Folder naming conventions I prefer using the terms models, view_models, and widgets, rather than business, data, and presentation, but I'd love to know what others think."
"It will be unlikely we have a middle "business layer" if we don't need use cases or similar." "The more I think about this project, the more I think we should favor using packages less, and keeping it vanilla as possible. Big companies with complex apps (and solo devs, for that matter) are going to use this as a starting point, and add packages and remove other packages to meet their specific needs. Starting more vanilla seems like it will better serve our target audience." I dont think people will use this app as a starting point. They will use the principles or concepts here and adapt them to their existing project. I think this apps needs to be less 'starting point' and more of an example for how they can accomplish different aspects of a 'real" app. "Is there a dart standard naming convention? Do the Dart/Flutter teams have a standard? For example, do they always have a common folder for shared resources?" Every app I have seen has a "common" so I didnt argue for its exclusion but it really is the worst folder/package name because it gives no indication what should be included or excluded from the folder/package. It grows until there is a circular dependency then code is broken out with a more specific designation. My view is that this is our attempt at articulating a standard. |
One thing I'd like to add to the discussion as well is the MVVM architecture's performance. I imagine lots of people will assume this is the most performant state management strategy because Google is backing it. An issue I've seen with how my previous teams have used MVVM is that the viewmodels end up becoming large, and the amount of individual variables that we want to render in a view from that viewmodel grows to the point where our view is rebuilding quite often when it doesn't need to. I don't think this needs to be addressed in this PR but I think it's something good to start thinking about. |
filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), | ||
child: DecoratedBox( | ||
decoration: BoxDecoration( | ||
color: Theme.of(context).extension<TagChipTheme>()?.chipColor ?? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh this would be an excellent spot to demonstrate good theming defaults best practices! It's common in many of the Flutter material widgets, like here:
Basically, there's an order of priority of where themed elements of a widget come from. They're used in this order:
- Arguments passed in through the widget's constructor
- The widget-specific theme, grabbed from the context (in our case the theme extension defined below)
- A default baked in to the widget itself
If you added a chipColor
and onChipColor
to the constructor of this widget, we'd be able to follow that pattern.
This pattern is really great as it gives devs flexibility to theme one-off widgets easily, while still having an app-wide theme to fall back on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea! I have copied your comment into our internal doc to comment on this when we talk about widget design
@miquelbeltran thanks for addressing my feedback! Here are my thoughts on the dependency-injection patterns we should be recommending:
I don't mind adding the ViewModel object to the The problem with making it a global is when you have two or more views of the same kind that you will need to show at the same time. For example, an app might have a list view that shows all records, or a subset of records based on a query. If they share the same view-model, then its state will be showing whichever view accessed it last. Instead, the thing you want to share between views is probably model.
I prefer to declare the view-model as a required dependency in the constructor, since it's more clear to the reader that a dependency is being provided to the view.
This gets the job done. It's easy enough to wrap your test code in a Provider, so no concerns there, I do wonder if it's a good recommendation to make from an architecture standpoint though. Consider the case where I want to call |
I do feel like it would be more valuable to teach the "vanilla" option, but offer other options (like using ChangeNotifierProvider) that are available to explore.
I don't know what exactly to recommend for folder structure. Here's one option:
|
@reidbaker Thanks for writing up those definitions, I like the distinction between "model", "repository", and "service" especially. I don't know if "bloc" means the same thing to to the majority of Flutter developers, but I'd love to learn more about how that works. I would distinguish between "screen" and "route" also, since to me, a "screen" is a useful name for a widget that displays a Scaffold inside, which let's you return a "screen" widget for each route you define in your routing table in a mobile app scenario.
This folder looks OK to me, it's declaring some things that are common to the entire app, but I understand the concern about keeping it tidy. |
The issue is that your model data is not tightly coupled with your features most of the time. Even in architectures where you have an app facing api and the design intent is closely map to what apps need (uncommon but sometimes exists) you wind up with feature drift where the apis do not exactly align. To get the type of alignment that would encourage ui and data to live together you basically have to get to a level of sync where you are doing server side rendering. |
I will say that my understanding screen and route are weak. It is likely your distinction is the correct one and I am conflating them because I mentally have them to be conflated. |
Thanks @johnpryan those are good points. Later today, I will do one final refactor on this PR injecting the
This has been my personal experience as well. @ericwindmill I have copied many comments from the thread into the "Appendix and Research" section of the document. |
Summary of the latest changes:
final viewModel = ResultsViewModel(
destinationRepository: context.read(),
);
return ResultsScreen(viewModel: viewModel);
Like said before, we can always adjust the patterns later. I will merge the PR and will continue with the rest of the app. |
Part of the implementation of the Compass App for the Architecture sample. **Merge to `compass-app`** This PR introduces the Search Form Screen, in which users can select a region, a date range and the number of guests. The feature is split in 5 different widgets, each one depending on the `SearchFormViewModel`. The architecture follows the same patterns implemented in the previous PR #2342 TODO later on: - Error handling is yet not implemented, we need to introduce a "logger" and a way to handle error responses. - All repositories return local data, next steps include creating the dart server app. - The search query at the moment only takes the "continent" and not the dates and number of guests, that would be implemented later on as well. ## Demo [Screencast from 2024-07-12 14-30-48.webm](https://github.com/user-attachments/assets/afbcdd4e-617a-49cc-894c-8e082748e572) ## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-devrel channel on [Discord]. <!-- Links --> [Flutter Style Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md [CLA]: https://cla.developers.google.com/ [Discord]: https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md [Contributors Guide]: https://github.com/flutter/samples/blob/main/CONTRIBUTING.md
As part of the work for the compass-app / architecture examples
This PR is considerably large, so apologies for that, but as it contains the first feature there is a lot of set up work involved.
Could be easier to review by opening the project on the IDE.
Merge to
compass-app
notmain
cc. @ericwindmill
Details
Folder structure
The project follows this folder structure:
lib/config/
: Put here any configuration files.lib/config/dependencies.dart
: Configures the dependency tree (i.e. Provider)lib/data/models/
: Data classeslib/data/repositories/
: Data repositorieslib/data/services/
: Data services (e.g. network API client)lib/routing
: Everything related to navigation (could be moved tocommon
)lib/ui/core/themes
: several theming classes are here: colors, text styles and the app theme.lib/ui/core/ui
: widget components to use across the applib/ui/<feature>/view_models
: ViewModels for the feature.lib/ui/<feature>/widgets
: Widgets for the feature.Unit tests also follow the same structure.
State Management
Most importantly, the project uses MVVM approach using
ChangeNotifier
with the help of Provider.This could be implemented without Provider or using any other way to inject the VM into the UI classes.
Architecture approach
assets
folder, an abstract class is provided to hide this implementation detail to the Usecase, and also to allow multiple implementations in the future.Screenshots
Extra notes:
app
folder. We need to create aserver
project eventually.TODO:
print()
.Pre-launch Checklist
///
).If you need help, consider asking for advice on the #hackers-devrel channel on Discord.