GIfatahTH / states_rebuilder

a simple yet powerful state management technique for Flutter
494 stars 56 forks source link

Could you do a example about Firestore? #67

Closed amoslai5128 closed 4 years ago

amoslai5128 commented 4 years ago

GIfatahTH,

Thanks for your great works and you're a lifesaver, I do love this states_rebuilder.

Could you offer a new example about Firestore based on example 007?
It's more than happy with a user profile upload function or more complex practice!

Btw, where happened to example 009?

Cheers,

GIfatahTH commented 4 years ago

Cloud you offer a new example about Firestore based on example 007? It's more than happy with a user profile upload function or more complex practice!

I will do in the future.

Btw, where happened to example 009? The example 010 should be 009, I Will fix it in the next release.

amoslai5128 commented 4 years ago

Cloud you offer a new example about Firestore based on example 007? It's more than happy with a user profile upload function or more complex practice!

I will do in the future.

Btw, where happened to example 009? The example 010 should be 009, I Will fix it in the next release.

Dear GIfatahTH, I've finished a Firestore version of Example 009/010, instead of your Firebase Database, however, there's something I don't understand:

In data_source, xxx_respository.dart, you defined a Stream<List> countersStream() { } for return a stream. When I change it to Firestore, data doesn't automatically change, I've to use broadcast stream. It looks heavy...

Here is my code: I don't know is there a better alternative of using a broadcast stream?

final StreamController<List<Asset>> _assetsController =
      StreamController<List<Asset>>.broadcast();

  @override
  Stream assetStream(String uid) {
    // Register the handler for when the assets posts data changes
    ref.document(uid).collection('Assets').snapshots().listen((snap) {
      if (snap.documents.isNotEmpty) {
        var posts = snap.documents
            .map((doc) {
              print('resp: ' + doc.data.entries.toString());
              return Asset.fromMap(doc.data, doc.documentID);
            })
            .where((aseet) => aseet.name != null)
            .toList();

        // Add the posts onto the controller
        _assetsController.add(posts);
      }
    });

    // Return the stream underlying our _assetsController.
    return _assetsController.stream;
  }
GIfatahTH commented 4 years ago

I will try it.

amoslai5128 commented 4 years ago

I will try it.

Thank you so much.

amoslai5128 commented 4 years ago

OMG, YOU DID IT :) How generous you are!

So many breaking changes... what's the difference between these three scripts (any performance issues)?

  1. Example you provided observe: () => RM.future(IN.get<UserService>().currentUser())
  2. Doc stated that observe: () => RM.get<UserService>.future((s, stateAsync) => s.currentUser())
  3. This one sometimes doesn't work observe: () => RM.get<UserService>()..setState((s) => s.currentUser(), watch:userServiceRM.state.user),
GIfatahTH commented 4 years ago

The first and the second both give you a new ReactiveModel from the returning future of currentUser. Expect that the second exposes the stateAsycn if you want to await possible currently executing future.

The third one execute the setState on the global UserService ReactiveModel.

So if you want to limit the rebuild to the current StateBuilder use the first or the second. If you want to rebuild all other observer widgets subscribed to the global UserService ReactiveModel use the third.

BTW, in the third case, can you show some cases where it doesn't work.

amoslai5128 commented 4 years ago

The first and the second both give you a new ReactiveModel from the returning future of currentUser. Expect that the second exposes the stateAsycn if you want to await possible currently executing future.

The third one execute the setState on the global UserService ReactiveModel.

So if you want to limit the rebuild to the current StateBuilder use the first or the second. If you want to rebuild all other observer widgets subscribed to the global UserService ReactiveModel use the third.

BTW, in the third case, can you show some cases where it doesn't work.

Thank you for your kind answer, I've got a better understanding. The third one was coursed by other things after I checked, so it's completely no problem :)

amoslai5128 commented 4 years ago

Hey GIfatahTH :) I know it's out of this topic, however, will you plan to release a new article about the least changes from the states_rebuilder?

I figured out your ReactiveModel is not a simple thing at all, to satisfy the curiosity, I've been reading papers, studies for Dependency Injection, Dependency Inversion and Inversion of Control (IoC), but it's over my knowledgable level X_X...

UNTIL I saw old comments from Youtube tutorial (Interestingly he did mention your library LOL) https://www.youtube.com/watch?v=vBT-FhgMaWM

GIfatahTH commented 4 years ago

I figured out your ReactiveModel is not a simple thing at al

Do you mean that it is difficult to understand and implement? And what do you expect from an article to make it simpler?

UNTIL I saw old comments from Youtube tutorial (Interestingly he did mention your library LOL) >https://www.youtube.com/watch?v=vBT-FhgMaWM

Do you mind quote the comment?

amoslai5128 commented 4 years ago

Do you mean that it is difficult to understand and implement? And what do you expect from an article to make it simpler?

Not at all, your library is very handy and good to use, especially with a clean design pattern, Perfect! I wish the doc can tell more about How DI (or Service Locator?) with Reactive Model, the flow of the state... Haha, is it too greedy?

To share a bit what's all about:

  • Vanilla BLoC for state management and also serve as MVP/MVVM architecture, no package, just InheritedWidget + Streams and StreamBuilders + one little widget I created for simplicity (when streams are overkill) inspired on the package states_rebuilder (which is a very simplistic but elegant way of state management using what's already provided by Dart and Flutter). This little widget is basically a stateful widget that uses setState when notified from the BLoC, somehow serves a similar function that of "NotifyPropertyChanged" but without all the hassle of the binding pattern.

It's at the first pinned comment, by searching the keyword "Amen brother! I'm a huge"

GIfatahTH commented 4 years ago

Great!!

I wish the doc can tell more about How DI (or Service Locator?) with Reactive Model, the states >flow, and it might be an idea to have a simple comparison to the latest flutter_bloc. Haha, is it too >greedy?

This is exactly what I'm doing right now:

Any contribution is welcome.

amoslai5128 commented 4 years ago

๐ŸŒWoW, cannot wait more!! I think your library will grow up as popular as flutter bloc๐Ÿ˜ƒ.

Yes, I think a short quick start guideline is necessary. Also a part for people shouldn't do that, some kind of examples on tge table, giving few bad examples on left side, and answer on the right

I can contribute the language translation of traditional Chinese & ZH-SC, if you wish๐ŸŒž

GIfatahTH commented 4 years ago

That would be fantastic.

Now docs are on the v-2.1.0 branch, do you mind checking them?

amoslai5128 commented 4 years ago

That would be fantastic.

Now docs are on the v-2.1.0 branch, do you mind checking them?

Haha, I do like the first part with emoji ๐Ÿ˜†๐Ÿคฃ It looks fun and vibrant๐ŸŒž

amoslai5128 commented 4 years ago

Btw, would the new version 2.1 add multiple RMKeys: [ () => ]?

GIfatahTH commented 4 years ago

In the new version 2.1, to get the reaciveModelcreated in observeManyparameter, we use the rmKey paramter with the new get method.

Here is an example inspired from #83

class StateBuilderTest extends StatelessWidget {
  //create a key
  final rmKey = RMKey();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: StateBuilder(
          observeMany: [
            //two ReactiveModel of type string
            () => RM.create<String>('x'),
            () => RM.create<String>('y'),
            () => RM.create<bool>(false),
          ],
          //Assign it to this StatesBuilder(),
          rmKey: rmKey,
          builder: (context, model) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('${model.state}'),
                RaisedButton(
                  child: Text('Concat string'),
                  onPressed: () {
                    //to get a model we use rmKey.get<T>()
                    if (rmKey.get<bool>().state) {
                      //If more than one model are of the same type,
                      //the default behavior is to get the first one
                      rmKey.get<String>().state += 'x';
                    } else {
                      //in this case rmKey.get<String>(1) means get the second ReactiveModel of type String
                      rmKey.get<String>(1).state += 'y';
                    }
                  },
                ),
                RaisedButton(
                  child: Text('Switch bool'),
                  onPressed: () {
                    print(rmKey.get<bool>().state);
                    rmKey.get<bool>().state = !rmKey.get<bool>().state;
                  },
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}
amoslai5128 commented 4 years ago

In the new version 2.1, to get the reaciveModelcreated in observeManyparameter, we use the rmKey paramter with the new get method.

Here is an example inspired from #83

class StateBuilderTest extends StatelessWidget {
  //create a key
  final rmKey = RMKey();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: StateBuilder(
          observeMany: [
            //two ReactiveModel of type string
            () => RM.create<String>('x'),
            () => RM.create<String>('y'),
            () => RM.create<bool>(false),
          ],
          //Assign it to this StatesBuilder(),
          rmKey: rmKey,
          builder: (context, model) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('${model.state}'),
                RaisedButton(
                  child: Text('Concat string'),
                  onPressed: () {
                    //to get a model we use rmKey.get<T>()
                    if (rmKey.get<bool>().state) {
                      //If more than one model are of the same type,
                      //the default behavior is to get the first one
                      rmKey.get<String>().state += 'x';
                    } else {
                      //in this case rmKey.get<String>(1) means get the second ReactiveModel of type String
                      rmKey.get<String>(1).state += 'y';
                    }
                  },
                ),
                RaisedButton(
                  child: Text('Switch bool'),
                  onPressed: () {
                    print(rmKey.get<bool>().state);
                    rmKey.get<bool>().state = !rmKey.get<bool>().state;
                  },
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

What if my RMKey is defined as Dynamic or other components?

       observeMany: [
          //two ReactiveModel of type Dynamic
          () => RM.create<Dynamic>(Something type unknown ),
          () => RM.create<Dynamic>('A'),
          () => RM.create<ZoomController>(...),
          () => RM.create<PointerDownEvent>(...),
        ],

Should I type as follow?

rmKey.get(0).state += 123;
rmKey.get(1).state += 'ABC';
rmKey.get<ZoomController>.setState((xx) => ....);
GIfatahTH commented 4 years ago

Should I type as follow?

rmKey.get(0).state += 123; rmKey.get(1).state += 'ABC'; rmKey.get<ZoomController>.setState((xx) => ....);

YES ๐Ÿ‘

amoslai5128 commented 4 years ago

Should I type as follow? rmKey.get(0).state += 123; rmKey.get(1).state += 'ABC'; rmKey.get<ZoomController>.setState((xx) => ....);

YES ๐Ÿ‘

Seems like I'm getting more familiar with States_Rebuilder haha Watching on every update ๐Ÿฅ‡

amoslai5128 commented 4 years ago

Hey GIfatahTH,

I want to ask more about the relationship between Service & Domain layer in Clean_architecture, should service handles the main logic when transforms the data into action for UI? Let say I got a data set from server:

Data Source Layer: Code only handles the server connection, without parsing that data? (shouldn't directly contact to Domain?)

Service Layer: It's just like a bridge that directly contacts with Domain & Data Source, so it gets the data first and escapes the JSON parsing functions from Domain, but this layer also does the classification works, finally transforming it into a state.

Domain layer: just like the core, however, it's more like a data model?

UI Layer: Gets the state from Service Layer, and displays it.

Is that correct? I'm avoiding the data source do too much logic without the logic of server connection itself. Otherwise, it'd become a nightmare when switching other backend servers.

I'm getting some confusion with the service layer....

GIfatahTH commented 4 years ago

Design patterns and clean architecture principles are here to help us to keep our code simple, readable, maintainable and testable. They are principles not rules. So whenever an idea make sense for you just use it provided it keeps your code simple, readable, maintainable and testable.

amoslai5128 commented 4 years ago

Design patterns and clean architecture principles are here to help us to keep our code simple, readable, maintainable and testable. They are principles not rules. So whenever an idea make sense for you just use it provided it keeps your code simple, readable, maintainable and testable.

Thank you! I will keep my code as clean as possible HAHA

Old code of using broadcast stream

  @override
  Stream assetStream(String uid) {
    // Register the handler for when the assets posts data changes
    ref.document(uid).collection('Assets').snapshots().listen((snap) {
      if (snap.documents.isNotEmpty) {
        var posts = snap.documents
            .map((doc) {
              print('resp: ' + doc.data.entries.toString());
              return Asset.fromMap(doc.data, doc.documentID);
            })
            .where((aseet) => aseet.name != null)
            .toList();

        // Add the posts onto the controller
        _assetsController.add(posts);
      }
    });
    // Return the stream underlying our _assetsController.
    return _assetsController.stream;
  }
/* Great & Workable Code:
  @override
  Stream<List<Asset>> assetStream(String uid) async* {
    yield* ref.document(uid).collection('Assets').snapshots().asyncMap(
        (snapshot) async => snapshot.documents
            .map((doc) => Asset.fromMap(doc.data, doc.documentID))
            .toList()
              //descending sort b, a
              ..sort((b, a) => a.createTime.compareTo(b.createTime)));
  }

/* Still, I don't know why the below code didn't work as excepted 
    by defining a **final snapshots** to let the code better looking: */
  @override
  Stream<List<Asset>> assetStream(String uid) async* {
    final snapshots =
        ref.document(uid).collection('Assets').getDocuments().asStream(); 
    yield* snapshots.asyncMap(
        (snapshot) async => snapshot.documents
            .map((doc) => Asset.fromMap(doc.data, doc.documentID))
            .toList()
              //descending sort b, a
              ..sort((b, a) => a.createTime.compareTo(b.createTime)));
  }
GIfatahTH commented 4 years ago

I know it's out of this topic, however, will you plan to release a new article about the least changes >from the states_rebuilder

Here it is one:

https://medium.com/flutter-community/immutable-state-management-rebuilder-how-to-refactor-to-immutability-and-vice-versa-b9576d6744f2

amoslai5128 commented 4 years ago

I know it's out of this topic, however, will you plan to release a new article about the least changes >from the states_rebuilder

Here it is one:

https://medium.com/flutter-community/immutable-state-management-rebuilder-how-to-refactor-to-immutability-and-vice-versa-b9576d6744f2

Oh WoW, reading on it!!

amoslai5128 commented 4 years ago

Very good article! Now, I'm trying to do a complex application with states_rebuilder ;)

amoslai5128 commented 4 years ago

I know it's out of this topic, however, will you plan to release a new article about the least changes >from the states_rebuilder

Here it is one:

https://medium.com/flutter-community/immutable-state-management-rebuilder-how-to-refactor-to-immutability-and-vice-versa-b9576d6744f2

Oh, the GitHub link inside this article seems dead. https://github.com/GIfatahTH/states_rebuilder_shopper

amoslai5128 commented 4 years ago

Dear GIfatahTH,

Here I have some suggestions for example 9 - immutable state todo app. We all know that immutability does a good job but it'd be unfriendly for beginners dealing with those exhaustive codes like:

//From Data Class / Todo_State
  @override
  bool operator ==(Object o) {
    if (identical(this, o)) return true;

    return o is Todo &&
        o.id == id &&
        o.complete == complete &&
        o.note == note &&
        o.task == task;
  }

  @override
  int get hashCode {
    return id.hashCode ^ complete.hashCode ^ note.hashCode ^ task.hashCode;
  }

  @override
  String toString() {
    return 'Todo(id: $id,task:$task, complete: $complete)';
  }

However, with a VSCode Data Class extension + Equatable package, it looks much nicer :) The extension can also generate the annoying ### toJson/ fromJson, copyWith() automatically. It's lovely to reduce the typo error.

VSCode Extension: https://marketplace.visualstudio.com/items?itemName=BendixMa.dart-data-class-generator Equatable package: https://pub.dev/packages/equatable

//After the extends Equatable, shorter but no performance difference
  @override
  List<Object> get props => [id, complete, note, task];

  @override
  bool get stringify => true;
amoslai5128 commented 4 years ago

I saw you did define a photoUrl in example 9 immutable todo. Will it be a good idea to do an example of firebase storage for upload photos, docs (with upload progress status & downloaded Url)?

// domain/entities/user.dart
class User {
  .......
  final String photoUrl;

In an immutable way, I've tried to use your example to do that, however, the code looks unclean since I've to get the download Url into Service_State.dart before it's been CopyWith in add_edit_screen.dart .

Here is my current flow with the upload part: upload multiple photos or docs.

  1. UI - deleted an old & picked 2 new images, array[url, url(deleted), local, local]
  2. Service - only upload the local path images, and await repo returns new URLs
  3. Repo - standard Future from official firebase storage doc
  4. Service - received a Storage Task Event from Repo, checking the status, and await the download URL
  5. UI - get status until download URLs has got, taking it to copyWith();

I know step 5 would be unnecessary since it should be done in Repo / Service level, however, I used your data_source/todo_repository.dart, there is a List _cashedTodos. <---- Smart move but Idk how to use it :(

GIfatahTH commented 4 years ago

Usually libraries provides a stream to listen to get the current size.

If this is the case, I follow this pattern:

1- Interface

abstract class IUploaderRepository {
//.. other stuff

Stream<dounble> progress(int totalSize);
}

2- Service

class UploaserService{
//.. other stuff

 Stream<dounble> progress(int totalSize){
   return uploaderRepository.progress(totalSize);
 }
}

3- UI

....
RaisedButton(
onPressed: (){
    // use file picker to get the file ptath
    // get the file total size
    int totalSize = .....;
     // shwoDialog
     showDiaglog(
         context : context,
         builder : (context){
         ..... 
         StateBuilder<double>(
                observe : ()=> RM.get<UpdouderService>().stream((s,_)=>s.progress(totalSize)).
                //You can automatically cloze the dialog after finishing the upload
                onSetState : (context, progressRM){
                    if(progressRM.state >=100){
                       RM.navigator.pop();
                   }
                 },
                builder: (context , progressRM){
                   //  Display some thing
                }
           )
       }
}
)

It remains the data_source part. This is the least important part because it is an implementation detail that can be changed at any time without affecting our application.

Simply choose the provider of your choice (Firebase store for example) and implement the IUploader interface.

amoslai5128 commented 4 years ago

Hey GIfatahTH,

I need your help with the question of complex objects in the immutable class. I would be if you would give me some advice!

Let say I have two classes: Shop, Product. I want to know which option would be suitable when dealing with a bundle of data in long-term. I want to know the good and downside of each of them according to the loading on the network and local state management, and which one should be better.

Option 1: Two classes are separated, Product keeps a shopID to connect back with the Shop. Thus, it'd be two files of service.dart.

@immutable
class Shop {
  final String shopID;
  final String name;
  final double rating;

  CopyWith()....
}

@immutable
class Product {
  final String pid;
  final String shopID;  // shopID for query to its owner;
  final name;
  final imgURL;
  final .....

  CopyWith()....
}

Option 2: Two classes are together, Shop has a complex class object. Thus, it'd be only one ReactiveModel for Service.dart .

@immutable
class Shop {
  final String shopID;
  final String name;
  final double rating;
  final UnmodifiableListView <Proudct> products;  // Or List <Product>, which's better? 

  CopyWith()....
}

@immutable
class Product {
  final String pid;
  final name;
  final imgURL;
  final .....

  CopyWith()....
}
GIfatahTH commented 4 years ago

The answer starts by known the relation between the two entities. is it one-to-many (one Shop can have many products or in the other sense one Product can be in many shops) or is the relation many-to-many?

What do you think?

amoslai5128 commented 4 years ago

The answer starts by known the relation between the two entities. is it one-to-many (one Shop can have many products or in the other sense one Product can be in many shops) or is the relation many-to-many?

What do you think?

Thank you for your reply,

I think one shop can have many products, one to many. Thus, option 2 would be suitable, right? And there's another question, should I put these two together in single service rather than separate out each of them for a different state? Such as ShopService, ProductService.dart?

amoslai5128 commented 4 years ago

OMG, what a breaking change in version 3.0, hehe, Functional injection looks very cool! However, any performance/security difference between old and new?

GIfatahTH commented 4 years ago

what a breaking change in version 3.0

Although, global functional injection is a new feature, old apps written with the Injectorapproach still work with v.3.0.

There are two minor breaking changes in v3.0 : StateBuilderonly rebuilds when notification has data. So one that has a code like this :

StateBuilder(
 observer : () => modelRM,
 builder: (context, modelRM){
 return Text (myModel.state);
}
)

will not notice any change and performance is better because we are minimizing the unnecessary rebuilds (onWaint and hasError statuses).

BUT one that writes something like this:

StateBuilder(
 observer : () => modelRM,
 builder: (context, modelRM){
  if (modelRM.isWaiting){
  return SomeWaitingWidget();
 }
 return Text (myModel.state);
}
)

Will notice that SomeWaitingWidgetis not displayed when the modelRMis waiting.

To Solve:

either use the shouldRebuildparameter:

StateBuilder(
 observer : () => modelRM,
 shouldRebuild : (_)=> true,
 builder: (context, modelRM){
  if (modelRM.isWaiting){
  return SomeWidget();
 }
 return Text (myModel.state);
}
)

or use WhenBuilderOrinstead of StateBuilder:

WhenBuilderOr(
 observer : () => modelRM,
 builder: (context, modelRM){
  if (modelRM.isWaiting){
  return SomeWidget();
 }
 return Text (myModel.state);
}
)

The other breaking change is that the API of StateWithMixinBuilderis changed and I think the StateWithMixinBuilderis not widely used.

However, any performance/security difference between old and new

the global functional injection is more secure and has more performance.

By the way, I think it is time to close this issue, and open new issues for any question or comment under related opened issues.

amoslai5128 commented 4 years ago

Thank you so much for the work. Anything that I can contribute? Like formatting the readme, or something else.

amoslai5128 commented 4 years ago

Hey, I've improved the readme formatting on your v3.0.0 branch, and I cannot pull the request.

amoslai5128 commented 4 years ago

That's all right, I pulled.