samples icon indicating copy to clipboard operation
samples copied to clipboard

Create compass-app first feature

Open miquelbeltran opened this issue 1 year ago • 24 comments

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 not main

cc. @ericwindmill

Details

Folder structure

The project follows this folder structure:

image

image

In details:

  • lib/config/: Put here any configuration files.
  • lib/config/dependencies.dart: Configures the dependency tree (i.e. Provider)
  • lib/data/models/: Data classes
  • lib/data/repositories/: Data repositories
  • lib/data/services/: Data services (e.g. network API client)
  • lib/routing: Everything related to navigation (could be moved to common)
  • 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 app
  • lib/ui/<feature>/business: Business logic of the feature (i.e. Usecases)
  • lib/ui/<feature>/presentation: UI classes, including ViewModel.

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

  • Data follows a unidirectional flow from Repository -> Usecase -> ViewModel -> Widgets -> User.
  • The provided data Repository is using local data from the 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

image

Extra notes:

  • Moved the app code to the app folder. We need to create a server project eventually.

TODO:

  • Integrate a logging framework instead of using print().
  • Do proper error handling.
  • Improve image loading and caching.
  • Complete tests with edge-cases and errors.
  • Better Desktop UI.

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.

miquelbeltran avatar Jul 01 '24 12:07 miquelbeltran

Comments on the architecture first before digging into the code.

I appreciate you laid out the folder structure but you have everything in one /lib directory. Have you considered.@ericwindmill I am not sure if you intend for these best practices to cover multi app best practices. Thinking about a multi app strategy matters more to a large company with multiple apps or a consulting company where you want to share your code in multiple vendor apps.

Assuming we wish to focus on single apps I would encourage this pr to include in addition to lib/ adding test/, and testing/ subdirectories.

  • lib/ contains source code
  • test/ has test code, with structure matching /lib
  • testing/ is a subpackage with its own BUILD and lib/, containing mocks and other testing utilities which may be used in other packages' test code. This is most helpful when providing custom mocks.

##"In details"

common_config

Consider adding another example like you did with dependencies.dart referencing capabilities/policies. https://docs.flutter.dev/ui/adaptive-responsive/capabilities

###Consider a more specific name for common/widgets FWIW my experience organizing large flutter projects any folders that contain "types" especially named types become difficult to manage. We found that putting widgets next to the model data for this pr that would be (features/feature/business/presentation) made it easier to find, modify and test ui. If there are widgets that need to be shared across the app (or multiple apps) and they are unrelated to what I am reading as "usecases" then I would bundle them up into <named_business>_ui_core. As an example if google was the business then they might be called "google_ui_core". If you don't like ui core then components (which has too many meanings to count) or some other word/words that indicate a collection of things that can be seen and used to build usecases. This folder will grow to contain more than just "widgets" like animation curves. I think your theme folder actually is similar but with the constraint that the items in that folder are intentionally dynamic and change change.

###"features//data: Data access points for the feature (i.e. Data repositories)" I believe that data access would be used by multiple features which makes its inclusion in confusing. Consider making a /data/repositories and data/model then mentioning that if there is a theme that ties those together then they can be what this pr calls a "feature". As an example "User" might be both a model and a repository. BUT "User" is not a "feature" when it comes to UI. That would be something like a "Profile" or "Avatar".

###Second note on repositories: Consider describing them as a single point of truth for app data as an explicit part of this documentation.

I think the folder naming of business/business is a bit confusing because the naming implies 2 different meanings. If we agree to split out repositories into their own section then the second business folder may not be needed for organization. If we don't then consider using a different name, maybe "logic" to help get across that the concepts are different. features//business/business: Business logic of the feature (i.e. Usecases)

###Consider Adding "Services" layer to your architecture.

As a quick definition Many:Many relationship with a Repository (A single Service can be used by multiple Repositories, and multiple Services can be used by a single Repository) Services are classes that are responsible for wrapping an API endpoint and exposing Futures that return the response corresponding to an API request. Services may throw exceptions if errors are encountered during fetches. A good rule of thumb is that Services are mainly used for making one-off API requests, and don’t hold state inside them. If state is needed, that state should live in the Repository.

My version of "In Details" assuming you took all advice above.

In details:

  • framework/<package_name_1>/lib: Code to be shared across apps
  • app/lib/config: Put here any configuration files.
  • app/lib/config/dependencies.dart: Configures the dependency tree (i.e. Provider)
  • app/lib/config/policies.dart: https://docs.flutter.dev/ui/adaptive-responsive/capabilities
  • app/lib/core_ui/themes: several theming classes are here: colors, text styles and the app theme. Dynamic values.
  • app/lib/core_ui/ui: Shared widgets, animations etc independent of a Usecase.
  • app/libutils: general util classes that don't fit elsewhere // I do not like this but there always exists a utils class
  • app/lib/data/repositories: Sources of model objects that features use. (i.e. repositories)
  • app/lib/data/models: Data models emitted from repositories
  • app/lib/data/services: Dart wrappers for API's, used by repositories to fetch data.
  • app/lib/data/<feature_grouping>: If repositories, model objects and/or services have a logical grouping or are tightly bound then bundle them in a folder.
  • app/lib/<ui_feature>/business/: Business logic of the feature (i.e. Usecases)
  • app/lib/<ui_feature>/presentation: UI classes, including ViewModel.
  • app/lib/routing: Everything related to navigation (could be moved to common)

Architecture approach

"Data follows a unidirectional flow from Repository -> Usecase -> ViewModel -> Widgets -> User." How does user input flow back to the servers? Describing the flow of presentation data as unidirectional is great but we need to articulate how users update the system.

Final note: it is totally reasonable to ignore plugins for now but how to use plugins and structure them for internal consumptions should be considered inscope for this effort.

reidbaker avatar Jul 01 '24 17:07 reidbaker

I think it is probably worthwhile to describe the philosophy that lead us to MVVM as the best practice.

In my own words here is the value and cost laid bare.

##Strengths

###Separation of concerns

When iterating on what the view should look like from the visual design side, those changes should only affect the View (ideally) This lets us solidify and test business logic earlier in the feature development lifecycle, and still be able to change UI elements closer to launch (we can rephrase this for an external audience)

###Dependability

Tracing the path data takes should be clear and inserting optimistic state should apply globally. Optimistic state is classic case of a user pressing "submit" on an item and the desire for the ui to update immediately to something that indicates the system got the users input while waiting on confirmation that an async process completed like a server rpc returning success.

###Testability

Each piece of the architecture should have well defined inputs and outputs, and it’s easy to mock both of those sides

###Expandability

Adding new logic or UI elements to existing code should be straightforward and low risk This is especially helpful when making changes in code that you are less familiar with.

###Easy to Iterate Any View that adheres to the ViewModel is easy to swap in and out, which is great for A/B testing

reidbaker avatar Jul 01 '24 17:07 reidbaker

It is also probably worth articulating why apps should depend on material for theme

reidbaker avatar Jul 01 '24 17:07 reidbaker

For googlers go/google-stream-sdk-architecture-2021 has very different words to describe some of the same concepts.

reidbaker avatar Jul 01 '24 17:07 reidbaker

Hey @reidbaker, thanks for the extended message(s)!

Multi-app / Modularization

Something we commented in the internal doc is that we may not go with modular app at first, but we should indeed explain why you may want to do that (or why not). So at the moment all code goes into the same lib folder.

testing folder utils

Definitely a good idea, already in this relative small PR I had a couple of fakes I kept private in the test files, which could be moved there.

lib folder structure

I had the doubt about if we should go by either one of these:

  • Group per feature first, then per layer.
  • Group per layer, then per feature.

I think that this mixed approach that you propose is better balanced:

  • Repositories, Models & Services are grouped in a data folder. Feature grouping can also apply here as you mention.
  • Business logic and Presentation is grouped per feature.

I will give it a try in this PR.

core_ui/themes and core_ui/ui

Indeed they will contain more things than just pure Widgets, so a more generic name seems a good fit.

When it comes to managing large quantities of Widgets, this is something we should definitely mention in the architecture documentation

Repositories and (API) Services

Makes sense to separate the two, although we probably don't need this yet in the example until we incorporate an external server, this is something that we should mention in the documentation nevertheless.

This kind of brings a questions for me, which is how flexible the architecture should we be. Probably the answer depends from team to team.

In my view, in general should be important to remind devs that not all architecture components may be used or needed. e.g. you may implement a feature that doesn't have usecases and the ViewModel interacts directly with a Repository, or even directly with an API Service, depending on its complexity.

MVVM comments

Your points are great, and I will incorporate them into the internal doc.

miquelbeltran avatar Jul 01 '24 20:07 miquelbeltran

Note to myself to expand upon why you would want to separate repositories and services. I defined what services were but not the advantages of having the layer be different.

reidbaker avatar Jul 01 '24 22:07 reidbaker

I did a bit of code folder reorganization based on the feedback, essentially separating the ui and the data components into their main folders.

data is then organized in the three main types of component, models, repos and services:

image

While the ui code is then separated per feature (in this example results), with a subfolder for all the core shared components:

image

Of course, this structure is not fixed, and I am looking forward to keep iterating over it and improving it.

What is still pending to do, is to organize all the test fakes and other util classes into its own either package or subfolder. Something that I have noted into our app design doc.

miquelbeltran avatar Jul 02 '24 12:07 miquelbeltran

Followup: to pros of MVVM style app architecture with the cons

  • Complete testing at each layer can feel onerous during the creation of a new feature.
  • Doesn’t play nicely with Stateful Flutter Widgets (because the widgets have state the unidirectional model breaks down), TextField, Animations, Dialogs, etc. have workarounds, but in vanilla Flutter they’re easy to use
  • Ratio of boilerplate to unique code for simple features is high

reidbaker avatar Jul 02 '24 17:07 reidbaker

Note to myself to expand upon why you would want to separate repositories and services. I defined what services were but not the advantages of having the layer be different.

If we define services using the definition "one-off API requests, and don’t hold state"

Services can use configuration to talk to different servers in test vs staging vs prod. Services turn api requests/querys into dart objects and dart methods/objects into api requests, completely encapsulating that transformation logic. Having that logic live in focused class makes it easier to handle api migrations and makes it easier to test corner cases than patterns that mix api logic and app logic. As a bonus services are portable to any flutter target including ones that dont yet exist.

Documented elsewhere repositories emit streams of data (not sure if the class is actually a stream). By delegating the api calls and dart object/method now the repository can be responsible for when to fetch the data and how to handle errors and retry logic. Api errors can and will be handled many different layers in the stack unless a developer team has made the decision for where they should be handled.

On a former team when building the app we initially relied on polling for results then swapped to streamed diffs before launch. The changes could happen one service at a time and the rest of the app didnt need to know.

reidbaker avatar Jul 02 '24 17:07 reidbaker

I think you have an internal document but calling out the testing options available to flutter devs probably would be helpful.

  • Dart Unit tests
  • Widget tests, (testing interactions)
  • Screenshot (pixel diffing) tests
  • Native Platform Unit tests (java/kotlin/ts)
  • Integration tests (black box testing)
  • Manual testing

reidbaker avatar Jul 02 '24 17:07 reidbaker

I took the liberty of re-writing this sample with my feedback applied, here's a zip containing the lib directory with the changes I would like to see: lib.zip

johnpryan avatar Jul 02 '24 18:07 johnpryan

@johnpryan I actually disagree with 4. Remove the Result class, every team I've been on has ended up implementing something exactly like this (but with an additional "loading" state as well) and it really cleans up how viewmodels can interact with the repositories. Having type safety on request results comes in really handy, and in my experience is much cleaner and easier to deal with than relying on try/catch errors handling.

TytaniumDev avatar Jul 02 '24 18:07 TytaniumDev

every team I've been on has ended up implementing something exactly like this (but with an additional "loading" state as well) and it really cleans up how viewmodels can interact with the repositories

I tend to follow the "when in doubt, leave it out" principle for things like this. To me, the benefits to me aren't very clear, but maybe there are additional features that would make Result carry it's weight. Take this example:

    final result = await _searchDestinationUsecase.search(continent: continent);
    _loading = false;
    switch (result) {
      case Ok():
        _destinations = result.value;
      case Error():
        // TODO: Handle error
        print(result.error);
    }

IMO, it would be simpler to write:

    try {
      final destinations = await repository.getDestinations();
      _destinations = destinations.where((d) => _filter(d, _continent)).toList();
    } catch (ResultError e) {
      // TODO: Handle error
      print(e);
    } finally {
      _loading = false;
    }

Additionally, every API method needs to declare a generic type <Result<X>>, which manifests in a pretty long function declaration:

Future<Result<List<Destination>>> getDestinations()

We are creating a type that every developer who wants to read this code needs to learn and understand, whereas most developers understand error handling and try/catch already, and might need a little guidance around how to do that in a Flutter context. I'm not sure it carries it's weight.

johnpryan avatar Jul 02 '24 19:07 johnpryan

@johnpryan FWIW I agree with Tyler. Large or complicated apps which is where best practices show the most benefit will have a design pattern for data that is in the progress of lazy loading and data that errors when loaded. You will see unique design patterns depending on the data for example a failed login might hard stop the user but other classes of data might have a "loading" state or require a button to refresh in the error condition. What is true is that the concepts of "this data isnt ready for display yet" and "This data does not meet the guarantees of the model object" are concepts that will have shared implementation across an app. By encoding that into an object you can build utility functions like "if any of these independent model objects are loading continue to wait until they finish or we timeout", or "when any of these model objects error log, toast and send a bug report" without having to write custom error handling logic at each location.

reidbaker avatar Jul 02 '24 20:07 reidbaker

@reidbaker Is that something that every developer needs to know and implement, or could this be a recommendation that we make separately? This seems like it's a useful technique, but is it something we want to include in our architecture recommendations or something that we recommend separately?

johnpryan avatar Jul 02 '24 20:07 johnpryan

Maybe a meeting is the right way to talk through this.

In general I am viewing this project and by extension this pr as our attempt to help complex projects when they begin by sharing best practices that will pay the most dividends way after a project is started. Does "Every developer needs to know and implement" the suggestions I have given? No, most do not. But that isn't my target audience.

Do I think that all large apps would benefit from some of their models having the same way of indicating in process loading and failed to load conditions. Yes.

reidbaker avatar Jul 02 '24 20:07 reidbaker

FWIW I have never found a need to use these types of objects because I've found that I can model the init, loading, loaded, and error state transitions using async functions with a try/catch. That said, I probably don't have a strong enough opinion on to justify a meeting, and I agree that it probably does pull it's weight if you consider that we want to recommend best practices like using sealed.

johnpryan avatar Jul 02 '24 20:07 johnpryan

For me the try/catch method is a non-starter for teams (not solo devs) because exceptions aren't checked in Dart. It puts a large cognitive overhead on every developer to do the following:

  1. Does this function I'm calling throw an error of some kind? (could be inferred because it's a function on a repository)
  2. What are the exceptions that this function can throw? (requires navigating to a source file to look at the dartdoc, if it's well maintained, or the entire code of the function to create an exhaustive list)
  3. What do each of these exceptions mean for the code I'm writing? (what do I do with them in the catch)

While for a Result type thing, the experience is:

  1. This function returns a Result type, in order to access the data I need, I have to access the ok portion of the sealed class
  2. I know I shouldn't force cast data types because it leads to crashes, so I should probably handle the additional states

The explicit result types get as close as possible to forcing the handling of all of the possible result types, which is what we want. You don't need to deep dive into the source code of the function you're calling, because you know exactly what possible values are returned and what they mean for your application.


I was writing this as the conversation was evolving, but if we do implement this suggestion I do think it's tied to it being a sealed class. The big thing for me is that it reduces the possibility of missing that you need to wrap something in a catch in order for your app to behave as you expect.

There's additional benefits if we use this data type along with a Stream or other continuously updating data type. You don't need to mess with ambiguous error handling in the stream functions, as you can just handle the data type like you would any other piece of data.

TytaniumDev avatar Jul 02 '24 20:07 TytaniumDev

Those are good points, thanks for helping me understand the benefits. My only rebuttal would be that the developer still needs to handle errors (and has to deal with the cognitive overhead that comes from that), but at least it is scoped to the getDestinations() function in the DestinationRepositoryLocal class. I also like that there's a clear boundary between the network request and the UI code, and it leverages the type-checker to ensure that you've handled everything that the data layer expects you to. I'm curious how you would use this type for Streams, however, since in principle an error can happen while the stream is still producing data, but if I'm returning a Result<Stream> I'm not sure how that would be handled by the end user.

johnpryan avatar Jul 02 '24 20:07 johnpryan

Ah when I meant it works well with streams, I meant in that you can make streams of Result objects, not streams within the Result. So Stream<Result<String>>. If you're using streams as the basis for a reactive app, it has similar benefits around not having to deal with stream's error handling which gets more complicated than just a try/catch (does the stream end when an error happens? Do I need to restart it? etc).

We found in larger teams that we had a big problem with people missing that they needed to handle errors at all, and when we moved to the explicit type-checked result states it also helped our interactions with UX because we'd sometimes find missing error states that we may have not realized existed before.

TytaniumDev avatar Jul 02 '24 21:07 TytaniumDev

That makes sense. Let's keep the Result class. I think our recommendation could be "Use the Result class for objects returned by the Model layer to the ViewModel layer, so that the ViewModel is guaranteed to handle errors gracefully, and display them using the View"

johnpryan avatar Jul 02 '24 21:07 johnpryan

FYI, the following files are generated by the Flutter tool and shouldn't be checked-in:

  1. windows/flutter/generated_plugin_registrant.cc
  2. windows/flutter/generated_plugin_registrant.h
  3. windows/flutter/generated_plugins.cmake

loic-sharma avatar Jul 02 '24 21:07 loic-sharma

Why is the code in the compass_app/app directory and not compass_app/?

johnpryan avatar Jul 02 '24 21:07 johnpryan

Why is the code in the compass_app/app directory and not compass_app/?

I believe because there will be a server component at some point.

reidbaker avatar Jul 03 '24 15:07 reidbaker

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.

miquelbeltran avatar Jul 04 '24 18:07 miquelbeltran

I took the liberty of re-writing this sample with my feedback applied, here's a zip containing the lib directory with the changes I would like to see: lib.zip

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.

Remove use cases

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.

Don't use a global ViewModel:

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 ResultsScreen with the ChangeNotifierProvider, like this:

  return ChangeNotifierProvider(
    create: (context) => ResultsViewModel(
      destinationRepository: context.read(),
    ),
    child: const ResultsScreen(),
  );

And this code would be in the GoRoute builder for /results.

Testing is of course a bit more difficult than passing it by parameter, since it requires you to still wrap the Widget with the ChangeNotifierProvider, so it has its disadvantages.

Use ListenableBuilder:

Related to the previous point.

Since we are using Provider, this allows us to use ChangeNotifierProvider + Consumer directly in the ResultsViewModel.

While I think there is value in understanding how to manage the lifecycle of a ViewModel within a widget and use ListenableBuilder, I would think many developers would not understand why aren't we just using those two tools that Provider has, since we already incorporate the package.

I think there will be value in presenting the different alternatives nevertheless in the documentation pages we will prepare.

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.

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 presentation folder and instead created view_models and widgets for each feature, as it makes more sense since we don't have usecases either.

Still prefer to keep all "data" parts, e.g. repositories, models and services, in the data folder.

Use relative imports:

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 flutter_lints?)


Edit: I have pushed now some changes based on the feedback

miquelbeltran avatar Jul 05 '24 16:07 miquelbeltran

FYI, the following files are generated by the Flutter tool and shouldn't be checked-in:

1. `windows/flutter/generated_plugin_registrant.cc`

2. `windows/flutter/generated_plugin_registrant.h`

3. `windows/flutter/generated_plugins.cmake`

The .gitignore doesn't include them, neither any of the other GeneratedPluginRegistrant.*. Not sure if that's intentional or not.

miquelbeltran avatar Jul 05 '24 17:07 miquelbeltran

@TytaniumDev got your feedback and implemented a ThemeExtension for the TagChip widget. I may do the same for the ResultCart title later on.

Also implemented a basic light/dark theme configuration.

image

miquelbeltran avatar Jul 08 '24 07:07 miquelbeltran

Don't use a global ViewModel:

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.

Use ListenableBuilder:

Related to the previous point.

Since we are using Provider, this allows us to use ChangeNotifierProvider + Consumer directly in the ResultsViewModel.

While I think there is value in understanding how to manage the lifecycle of a ViewModel within a widget and use ListenableBuilder, I would think many developers would not understand why aren't we just using those two tools that Provider has, since we already incorporate the package.

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

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.

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.

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? @reidbaker @johnpryan

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.

ericwindmill avatar Jul 08 '24 17:07 ericwindmill

"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." I have a pretty strong objection to using a flutter class name in the folder structure. It leads to misunderstandings about what the folder is supposed to contain. FWIW my preference is a wild departure from what we currently use.

  • view_model = dart object containing the data needed for a set of widgets to display some information
  • model = dart object representing the truth about local or remote state
  • repository = class responsible for emitting model objects and handling retry logic and error emitted from services
  • service = class responsible for api calls (local and remote) and converting those calls into dart methods and objects. plugin_service, the same as a service but requires platform implementations
  • view = flutter widgets that take a view model and display their contents and attach their callbacks
  • command = flutter callbacks in a view_model to attach to buttons (or other views) when they are interacted with
  • bloc aka business logic = dart code that takes streams of model objects from repositories and converts them into view models
  • component (possibly usecase here) = the combination of bloc, view_model and view that is an independent logical unit of functionality that can be placed anywhere in the app.
  • screen = flutter route that can be used to navigate, screens do not have business logic and are simple widgets used to format components

"It will be unlikely we have a middle "business layer" if we don't need use cases or similar." I think we need to show the concept of displaying something different than the base model object. This is part of the core logic large apps have to deal with.

"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.

reidbaker avatar Jul 08 '24 17:07 reidbaker