-
-
Notifications
You must be signed in to change notification settings - Fork 309
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
Code generator design #48
Comments
I've requested that the #46 branch would be merged, it has the package structure and configs already, so should work as a starting point. |
I experimented with generating actions, and I think that the easiest way to support generics, optional parameters and named parameters is to skip the
Generates:
|
@katis I agree with your idea, we should merge the branch (@pavanpodila ). Few my opinion
@observable
List<String> favoriteColorList = [
'red',
'blue',
'yellow',
]; /// Generated
ObservableList<String> _favoriteColorList;
@override
set favoriteColorList(List<String> favoriteColorList) {
_favoriteColorList
..clear()
..addAll(favoriteColorList);
}
List<String> get favoriteColorList => _favoriteColorList;
/// constructor code is not posted
/// initialize value and nulled user-defined value will be done in constructor
Few things that need to discuss:
|
@katis Firstly, thanks for an educative discourse on the options you took. I have personally learnt a few interesting things around the use of ActionsAs regards Inject annotationFor the store annotation, I think we should adopt what MobX has done instead:
@inject('auth')
class SomeWidget extends StatelessWidget { }
@inject((Map<String, Object> storeMap) => storeMap('auth'))
class SomeWidget extends StatelessWidget { } Not sure if we can overload the Some other thoughtsI feel we don't have to adopt all the API conventions from MobX as some may not be valid for Dart. For example, the lack of overloading and dynamic dispatch of a function with variable types of arguments. This is why we are stuck in a world of We should be prudent in picking what makes sense for Dart and stay idiomatic for the language. Curious to hear what you guys think. |
The |
@pavanpodila The |
Got it. I think we have some creative license here to improve the the Developer eXperience (DX) of using mobx.dart. |
@copydog Not familiar with mustache4dart, is there some advantage over using pure Dart string templates? |
@katis We can get more clear code for loop and control flow. And allow us to create one template file for each annotation. here is the file structure that I used, very clean and maintainable |
Agree with @copydog. Mustache will also allow us to iterate faster with the generated code. The role of the generator would be to just prepare the context for the mustache template, which is kind of fixed for the most part. How we use that context to generate the final code can keep changing in the template. |
Sure, mustache is fine by me. My code uses Dart template strings currently, but the code is ugly in a quick&dirty kind of way, so needs refactoring anyways |
Is there really a need for this? Isn't an InheritedWidget enough? |
I don't think it's even possible to begin with. The dart way of your function-based example would be this: Something selector(Map anotherThing) {}
@Inject(selector)
class Foo {} Closures are not compile time constant. And we'd need to create a factory for the widget to be able to inject the field; which is not very ideal. |
Yeah, I doubt it's possible in Dart in the way it works with JS. InheritedWidget or making a little hook that does the same seems enough to me. |
Also, would this make sense to offer a simplified Example: Instead of: @store
class Foo {
factory Foo() = _$Foo;
@observable
String foo;
@computed
int get length => _foo.length;
@action
void someAction() {}
} we have a simplified: @store
class Foo {
factory Foo() = _$Foo;
String foo;
int get length => _foo.length;
void someAction() {}
} This is much easier to write, which would make users less tempted to make one HUGE store instead of multiple smaller ones. And it could be used as a replacement for the lack of mirroring to make a |
Finally (sorry for the spam), an alternative to the issue about @store
abstract class Foo {
Foo._();
factory Foo() = _$Foo;
int get computed => observable
int get observable;
set observable(int value);
void action() {}
}
This is now fine for the generated class to use class _$Foo extends Foo {
_$Foo(): super._();
final _$observable = Observable<String>(null, name: 'User.firstName');
@override
String get observable => _$observable.value;
@override
set observable(String value) => _$observable.value = value;
} |
I did think about that, but making a separate getter and setter seems too much trouble for a simple field. |
I think that the idea about the Instead of the code generator searching for a class with an annotation, it searches for abstract classes implementing a
If the class implements
|
I like the reuse of Actually the arguments to |
What would I think we still have an issue with actions: class Foo {
Foo._();
factory Foo() = _$Foo;
} We have to use this trick just so that This looks pretty bad. And if the user wants to pass custom values to the constructor or define final variables, things get even worse. |
Currently Maybe something like:
The constructor issue is I think unsolvable.
Edit: I think the builder wouldn't help much, Darts cascade notation makes leaving the constructor empty bearable: User()
..firstName = 'John'
..lastName = 'Johnson'; |
Shouldn't this be done by an I'm asking because if we never really have a use-case for this, then the interface just adds some noise. A |
Another possibility is mixins using the shorthand syntax: class Merged = SomeClass with SomeMixin; We could define a mixin that would be the equivalent of your proposed mixin Store<T> {} Then the user write: class Test = _$Test with Store<_Test>;
mixin _Test {
int foo;
int get bar => 42;
void action() {
print('hello world');
}
} And we generate: abstract class _$Test with _Test {
int get foo {
// Subscribe to the value
return super.foo;
}
set foo(int value) {
// Notify a value changed
super.foo = value;
}
int get bar {
// Subscribe to the computed value
return super.bar;
}
} The But at the same time this covers your suggestion of a |
I guess the limitation with a mixin is that the user can't add a constructor at all? I wouldn't be happy with that. Apparently spying/tracing requires more detailed events about the things happening with observables than you can get with autorun ( https://mobx.js.org/refguide/observe.html ) and the |
I played around with mixins and you could enable user defined constructors like this:
But mixins can't have constructors, and |
Ah interesting, I didn't think of swapping the class/mixin.
Would a lazy initialization works? T _field;
T get field {
_field ??= T();
return _field;
} |
I suppose it would. I'll take a look at the mixin approach, there might be issues with codegen since super mixins are a new feature that is not fully supported on all platforms (?) dart-lang/language#12 |
This issue is likely outdated. It says that dartpad doesn't support them, but it actually does. And only the If an issue arises (which I doubt), we can generate the old mixin syntax instead: abstract class _$User extends UserBase {} |
@rrousselGit The mixin method is working, I tried it in the |
I am using |
I guess you could make them free functions instead? :/ |
Fair enough. I'm still trying to understand your comment regarding this
Why do we really need a this circular relationship? Wouldn't be better if its implemented in another way so we can skip this "forbidden" relationship? I'm just loudthinking here 😄 |
Basically, if the codegen generates a mixin that overrides stuff of the base class the declaration must be mixin _$User on User {} Then if the class would use the mixin class User with _$User {} This would be circular relationship which won't work. We could instead reverse the codegen so that the user creates a mixin, and we generate a class for it. This would remove the need for a the mixin application, but then the user couldn't add a constructor. To me that would be a bigger problem because mixin initialization is pretty limited. I think we should change the codegen a bit that it would search for all uses of the mixin, instead of just the short @JsonSerializable()
class User extends UserBase with _$User {
User(String id) : super(id);
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
} |
Why do we need to decorate it this way: @JsonSerializable()
class User = UserBase with _$User; ? What about this instead: class User = UserBase with _$User;
@JsonSerializable()
class UserBase {
} |
Because then the serialization creates a |
For now it can be done creating a temp class and then inherent it... @JsonSerializable()
class User extends _TempUser {
User(String id) : super(id);
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
class _TempUser = UserBase with _$_TempUser;
abstract class UserBase implements Store {
UserBase(this.id);
final String id;
@observable
String firstName = '';
@observable
String lastName = '';
@computed
String get fullName => '$firstName $lastName';
@action
void updateNames({String firstName, String lastName}) {
if (firstName != null) this.firstName = firstName;
if (lastName != null) this.lastName = lastName;
}
} |
Interesting, because the IDE doesn't see any error until we run the code. It seems doable though: void main() {
final f = User.fromJSON();
print(f.foo);
}
class User = UserBase with Foo;
mixin Foo on UserBase {}
class UserBase {
final String foo;
UserBase(this.foo);
UserBase.fromJSON(): this('foo');
} We should probably talk about this use-case on https://github.com/dart-lang/language |
Maybe I should make a small tweak for the declaration syntax:
or go back to the annotation
This way the base class doesn't need to be abstract or contain anything related to Mobx (besides annotations) and it would conceptually be similar to JS:
We also need a convention for the Base class, for documentation and such. Should it be |
Conceptually spying and tracing work similar to the |
@katis the approach you suggested is a bit cleaner now. How about the names like below? @observable
class User = _User with _UserMixin;
class _User {} |
Tried to implement the discussed syntax, but there's a bug in Dart that prevents it from working: dart-lang/sdk#35011 |
I was going through a bunch of other packages that do code generation and really like the approach @rrousselGit took for How functional_widget does itEssentially he introduces a capitalized Class identifier for a lowercase-named function identifier. Thus... @widget
Widget foo(BuildContext context, int value) {
return Text('$value');
} generates... class Foo extends StatelessWidget {
final int value;
const Foo(this.value, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return foo(context, value);
}
} and you use it as... runApp(
Foo(42)
); Proposal for a Store implemented classWhat if we take the implementation that @katis did and mix some of the ideas from @rrousselGit 's As a user, you write an abstract class abstract class CounterBase implements Store {
@observable
int value = 0;
@action
void increment() {
value++;
}
} and the generator generates... class Counter extends CounterBase {
// Rest of the implementation as is
} So, we introduce the class identifier
Do you guys see any potential issues with this approach?? |
Just passing there: While this works fine, this has other issues that I'm working on: Renaming Once we have a support for that in |
Good point! Refactoring will be a pain point. But if you rename Won't that take care of the problem? [Update]: I see the problem now. The code that was using |
Indeed. I took that option for But I wouldn't recommend it for mobx |
On a separate note, circling back to what @katis asked earlier, can we adopt the convention like so: class Counter = _CounterBase with _CounterMixin;
abstract class _CounterBase implements Store {}
|
The |
Got it. Let's keep it then. |
One of the benefits of having the user define the class that implements the mixin is that they can add more code to the wrapped class, I wanted to use the InheritedWidget
|
How does renaming fields work in that case BTW? Since the generator users will use the wrapper class instead of the private base model, it may be possible that renaming fields breaks. |
The generated fields and methods retain the same name via |
If we inject this method into the generated code, you would add a dependency on Flutter. I think there are cases where you will want to keep the Stores as pure data models. Perhaps, have a second interface ( |
|
Ah...now I see it. Thanks for clarifying |
Closing this as the generators are in place and working |
Since we've started working on the code generation, I thought I'd open a discussion thread about its design.
We need at least three working annotations,
@observable
,@computed
and@action
.The generator will find all classes with a specific annotation (
@observable
,@store
?) and creates a private subclass_$ParentClass
that overrides the annotated fields, getters and methods with Mobx magic.I'll use parts of this annotated class in the next sections:
@observable
The first approach I tried was with a hidden
Observable<T>
field:This had some downsides. One was that the
User
creates storage for the originalfirstName
field which then ends up unused. Second is that the initialization becomes slightly more complicated. The initial value must be copied from the superclasses field, and the field must be nulled so that it doesn't keep the initial value in memory. Code would look something like this:My second attempt was with an Atom which made the initialization a non-issue, since it just uses the original field:
I think the Atom method is better, for the reasons above.
@computed
Computed should be simple, just override a getter and use the original one:
@action
The
@action
annotation has a name conflict with the currentaction()
builder. We could removeaction()
and replace it with multiple action variants for different function arities:action0<R>(() => ...)
,action1<A, R>((a: A) => ...)
,action2<A, B, R>((a: A, b: B) => ...)
. The names are ugly, but they could be used by the codegen and I think that once the code generator is done, people won't use the raw versions much.Named vs. positional arguments will add complexity in the generator.
Automatic conversions
Mobx JS makes objects and arrays automatically observable when you use the
@observable
decorator.You can opt-out by using
@observable.ref
decorator instead.Dart can't support the feature for classes, but it would be possible (sort of) for collections like Maps and Lists.
I'm not a fan of it as a default, and using an
ObservableList
is not much harder than using aList
.vs.
Since we can't modify the actual list assigned to the variable like in JS but only wrap it, it might be confusing that the non-observable List you passed to
ShoppingCart.items
is suddenly a reactive list when accessed fromitems
.Constructors
The generator should be able to create a constructor that matches the superclass factory that delegates to the subclass:
observe()
support in annotated classesWe would want the generated classes to be supported in devtools etc, but the problem is that the
Atoms
,Computeds
etc are hidden in the subclass. It would be ugly to expose them as public, and you'd have to cast theUser
to_$User
.Another way would be to define an interface like:
make the annotated class abstract, and implement
Store
:Then the code generation could implement Store automatically:
Store interface could also expose a Map of observable fields, so devtools could iterate and analyze them.
The text was updated successfully, but these errors were encountered: