GregoryConrad / rearch-dart

Re-imagined approach to application design and architecture
https://pub.dev/packages/rearch
MIT License
88 stars 4 forks source link

Please correct your statement on why not get_it :-) #142

Closed escamoteur closed 5 months ago

escamoteur commented 5 months ago

with watch_it there is a reactive extenstion to get_it for quite a while now.

GregoryConrad commented 5 months ago

Hi! đź‘‹

That blurb of the README moreso pertains to state reactivity; last time I used get_it (which was a few years ago) there was no way for states/objects registered with get_it to react to updates in other state/objects registered with get_it (unless I just missed something). Yes, you can perhaps just register streams, but those are outside of get_it and come with their own bundle of problems.

I agree that particular section could be clarified, so I'll keep this open until I clarify it.

escamoteur commented 5 months ago

you missed, that for some years there were the get_it_mixin around and since last August there is the watch_it package that is the companion package for get_it.

GregoryConrad commented 5 months ago

I'm aware of watch_it, but watch_it only provides Widget/UI reactivity, not inter-state reactivity. I.e., in the overused count example, I couldn't have a Count class registered with get_it, and then have a CountPlusOne class who's state watches that of Count (without Streams, ValueNotifiers, etc., which have their own problems that prevent correct composition across states).

escamoteur commented 5 months ago

What is the problem using a Stream or Valuelistenable? why mixing state management with business logic?

GregoryConrad commented 5 months ago

In addition to the items mentioned here (under "why not rx and streams"), there is also a big issue w.r.t. composition, see page 14 here.

why mixing state management with business logic?

Business logic can be stateful--what happens when you want to fetch the current signed in user's posts in a social media application? In this situation, and virtually every other regarding how to build some piece of software, it is easiest to model the problem exactly as described via its underlying data and that data's requirements:

Capsules enable you write your state/business logic exactly like that with no layers of indirection or complications needed, while remaining highly testable, maintainable, etc. etc.

Now if you want to discuss local/ephemeral state, you may use an individual ValueNotifier for just a singular state. But that also isn't as easy to set up as a one line:

final (state, setState) = use.state(); // or useState(); if you are using hooks

One main difference ReArch brings to the table here is that you may use this same "effects" API (use.state above, but there are dozens of others) to write both widgets and your global state/business logic to create cohesion across your app.

escamoteur commented 5 months ago

hmm, thanks for your Thesis, will check it out

have a state that uses the current signed in user to grab the posts of that current user why whould a state grab any data? I'm not sure that this makes it easier to understand what is going on in your app.

I'm working for over a year on a fairly big social media app and we didn't face any of your listed problems. However we use flutter_commands in addition to watch_it as the reactive link for our business logic.

GregoryConrad commented 5 months ago

why whould a state grab any data? I'm not sure that this makes it easier to understand what is going on in your app.

Because it enables declarative code, which in turn enables automatically reactive code. This particular social app example could look something like:

/// Represents the current signed in user.
User currentUserCapsule(CapsuleHandle use) => throw 'impl hidden';

/// Fetches a user's posts via REST API.
Future<List<Post>> Function(User user) fetchUserPostsAction(CapsuleHandle use) => throw 'impl hidden';

/// Represents the posts of the current signed in user.
Future<List<Post>> currentUserPostsCapsule(CapsuleHandle use) {
  final user = use(currentUserCapsule);
  return use(fetchUserPostsAction)(user);
}

The currentUserPostsCapsule is a simple composition of the two other pieces of state/logic, all while remaining highly testable and completely satisfying many well known design principles/patterns like SOLID (for free due to the nature of ReArch's design).

There are other ways to achieve this exact kind of behavior, but I'd argue they aren't as simple as just modeling your app in terms of data flow (which, at the end of the day, is all any application is--just one big data in and out machine).

However we use flutter_commands in addition to watch_it as the reactive link for our business logic.

Had to look up flutter_command since I haven't heard of it before--based on what I'm reading you might really like ReArch haha

A Command is an object that wraps a function that can be executed by calling the command, therefore decoupling your UI from the wrapped function.

In ReArch, you don't need any layers of indirection like this since capsules provide dependency inversion over any functions they contain too. In our example above, all a widget has to do is use(fetchUserPostsAction) and they get their own copy of the function that they can call with any user that might be local/ephemeral state, and still be able to get a different user's posts no problem.

escamoteur commented 5 months ago

Definitely will look into it. How is your view on the currently hyped Signals packages? Am 15. Apr. 2024, 17:25 +0200 schrieb Gregory Conrad @.***>:

why whould a state grab any data? I'm not sure that this makes it easier to understand what is going on in your app. Because it enables declarative code, which in turn enables automatically reactive code. This particular social app example could look something like: /// Represents the current signed in user. User currentUserCapsule(CapsuleHandle use) => throw 'impl hidden';

/// Fetches a user's posts via REST API. Future<List> Function(User user) fetchUserPostsAction(CapsuleHandle use) => throw 'impl hidden';

/// Represents the posts of the current signed in user. Future<List> currentUserPostsCapsule(CapsuleHandle use) { final user = use(currentUserCapsule); return use(fetchUserPostsAction)(user); } The currentUserPostsCapsule is a simple composition of the two other pieces of state/logic, all while remaining highly testable and completely satisfying many well known design principles/patterns like SOLID (for free due to the nature of ReArch's design). There are other ways to achieve this exact kind of behavior, but I'd argue they aren't as simple as just modeling your app in terms of data flow (which, at the end of the day, is all any application is--just one big data in and out machine).

However we use flutter_commands in addition to watch_it as the reactive link for our business logic. Had to look up flutter_command since I haven't heard of it before--based on what I'm reading you might really like ReArch haha A Command is an object that wraps a function that can be executed by calling the command, therefore decoupling your UI from the wrapped function. In ReArch, you don't need any layers of indirection like this since capsules provide dependency inversion over any functions they contain too. In our example above, all a widget has to do is use(fetchUserPostsAction) and they get their own copy of the function that they can call with any user that might be local/ephemeral state. — Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread.Message ID: @.***>

GregoryConrad commented 5 months ago

How is your view on the currently hyped Signals packages?

Funny you mention that; I actually just added a section to the README on that after the question arose a day or two ago in #137.

https://github.com/GregoryConrad/rearch-dart#why-not-signals

Copy pasted for easy reference:

Why not Signals?

Signals only constitute a subset of ReArch's functionality. If you took ReArch and removed:

Then you'd arrive at ReArch.

So, if you like Signals, you'll likely love ReArch, because you'll get all of the benefits of Signals but with many more features (mostly due to ReArch's side effects model). The only main difference is in the API, but that is easy enough to adjust to--signals and capsules tend to map one-to-one.

escamoteur commented 5 months ago

OK, I spend some time trying to understand the whole concept and honestly I find the code with capsules etc very hard to read. I'm pretty sure when writing it it might be different but compared with a combination of watch_it with flutter_commands it feels way harder to understand what is going on in the code. Maybe coming from Rust that might be different, but I'm not sure that it makes live for Flutter developers really easier. I think I can understand the motivation behind it but IMHO using proper OO design helps a lot to document what your app is doing even if it will take a lot more code to write. Curious how much traction you will get with that package.

busslina commented 5 months ago

I find the code with capsules etc very hard to read.

I felt the same (coming from Riverpod), but after a time adapting mentally it's just pretty. A part of all the benefits that his creator mention, personally, from my experience I would highlight the speed of writing/refactoring code it brings. And once you adapt to it I would say it's easier (I only know Riverpod). Also, I didn't used it a lot with Flutter. Mostly in Dart backend, but I will start to use it more on Flutter (same or very similar architecture)

busslina commented 5 months ago

Example:

Future<Ret<String, dynamic>> authTokenCapsule(CapsuleHandle use) async {
  // TODO: check if it is close to expire

  // TODO: schedule expire handler and delete it

  final config = use(appConfigCapsule);
  final configSetter = use(appConfigSetterCapsule);

  final authToken = config.authToken;

  if (authToken != null) return Ret.success(value: authToken);

  final username = ConsoleUtilsV2.readValue('API username: ');
  final password = ConsoleUtilsV2.readPassword('API password: ');

  final res =
      (await use(backendApiLoginCapsule).login(username, password)).logFailed();

  if (res.failed) return Ret.error(res.errMessage!);

  final newAuthToken = res.value!;

  final newConfig = config.copyWith(authToken: newAuthToken);

  newConfig.saveToFile();

  configSetter(newConfig);

  return Ret.success(value: newAuthToken);
}

Dio _authHttpClientCapsule(CapsuleHandle use) {
  return Dio(
    BaseOptions(
      validateStatus: (status) => switch (status) {
        200 => true,
        401 => true,
        _ => false,
      },
    ),
  );
}

BackendApiLoginController backendApiLoginCapsule(CapsuleHandle use) {
  final httpClient = use(_authHttpClientCapsule);

  return BackendApiLoginController(login: (username, password) async {
    final res = await handleResponse(httpClient.post(loginApiUrl, data: {
      'username': username,
      'password': password,
    }));

    if (res.success) {
      return Ret.success(value: res.response.data as String);
    }

    return Ret.error('Status code: ${res.statusCode}');
  });
}

class BackendApiLoginController {
  final Future<Ret<String, dynamic>> Function(String, String) login;

  BackendApiLoginController({
    required this.login,
  });
}
GregoryConrad commented 5 months ago

The API can take a second to wrap your mind around—no denying that, especially when coming directly from OOP. It’s an entirely functional API, and that allows you to essentially write an entire application with zero classes (with the exception of data classes, but those hardly count as a traditional class).

If it helps, think of capsules as an object (like a signal) instead of a function, even though that’s how they’re defined. You could even implement a simple wrapper API around ReArch like this:

// You could do this in your own code:
Capsule<T> capsule<T>(Capsule<T> c) => c;

final myCapsule = capsule((use) {
  return use.state(1234);
});

that might help see how it’s similar to signals or Riverpod. The reason I didn’t do that is because then capsules lose their explicitly written types, and unfortunately won’t be const until Dart supports const lambda functions.

escamoteur commented 5 months ago

I guess that is my main pain point. Dart isn't a first class functional language which leads to not optimal syntax if you try to use it purely functional. f# is an amazing example how good FP code can look like. I'm an OOP guy and still think its a pretty good paradigm especially for full applications. On the server I think FP is a better fit.

GregoryConrad commented 5 months ago

Fair enough. If you like OOP and want to stick to that exclusively in your apps, then ReArch isn’t for you. I used to love OOP some years ago, but got increasingly frustrated with it as time went on. I could never make things exactly like I envisioned them.

as far as the functional aspects, I tried to make ReArch not too reliant on anything FP-specific to the point where it wouldn’t fit in Dart, and I’d say ReArch achieves that. There are things like AsyncValue, but that’s no sin in my book since Flutter itself has an AsyncSnapshot and Riverpod also has a similar AsyncValue.

I do hear what you’re saying about using functional approaches in an OOP language, but I don’t think ReArch goes too far down that rabbit hole unlike some other Dart/JS libraries I’ve seen.