Flutter Clean Architecture and TDD

Created At: 2023-08-23 19:21:50 Updated At: 2024-01-30 15:16:32

Here we will cover many concerns about Flutter clean architecture and TDD. We will list out some questions and answers. The actual BLoC TDD Clean Architecture is covered here.

These questions and answers are equally useful for other Riverpod and GetX clean architecture. The index of this tutorial is here. You may click on any of the link and visit the answers. This is done for better navigation. You may also this clean architecture knowledge save for combining with Riverpod and GetX. There are more questions given answer to the bottom section. 

1. Why should we use create params instead of entity in with params use case?

2. Can I use DataState class instead of Either Dartz? Which method is better?

3. What about the usecases of domain layer?

4. If I have a DTO response when I make an API request to the backend, do I still need the entity of domain layer?

5. Should we use Dart Records or Dartz package?

6. Do we need to test only side, Left or Right?

7. Why are we using Provider and BLoC together?

8. How can we pass authRepository to usecase authRepository?

9. Difference between APIException and APIFailure

10. How to structure the project if different features share the same repositories use cases and/or datasource?

11. Why do we use http.client for Cubit? Why not lazySingleton?

12. Why do we use usecase with extends not with implements?

13. What do the core and util folder do?

14. Can we move the AuthDataSource abstract class to the domain layer?

15. Can we assign the constructor value inside?

1. Why should we use create params instead of entity in with params use case?

For this one, you need to have structure in your code, without structure then your code will be messed up, that's why we create an interface for all our usecases, but in that interface, we can't specify Every single data type, so that's why we specify a generic Param type, and in the implementation of the usecase, we also need to give one data in the argument, so if you have more than one, how can you handle it? You create an object to hold all of them. 

It's similar to having kwargs and args in python.

2. Can I use DataState class instead of Either Dartz? Which method is better?

It shouldn't be a problem if you replaces the dartz with datastate classes, but personally, I find it neater to use dartz's either class, using datastate classes will also make it possible to return more than just Failure or Success, but I don't see any use for that since we already have a BLOC, if you are using another state management where we have Bloc, then sure, you can use a datastate class instead of either, that way, he can handle the extra cases like loading and the rest of them.

3. What about the usecases of domain layer?

The usecases in domain layer holds files in the folder. Each of the file is responsible for doing one job. You may a file create_user.dart just for create user and get_users.dart for getting users from the server.

Each class will hold one single method and this method would reach out to the repository of the domain layer.

See our CreateUser class and it has only one method and one responsibility. So your class name should clarify what this class does.

4. If I have a DTO response when I make an API request to the backend, do I still need the entity of domain layer?

your entity should actually match your backend response, whatever data you're receiving from your backend should be what you collect into one entity, then your model(DTO) in your data layer, is gonna extend the entity, meaning that its essentially the entity with extended functionality, so it'll still have all the attributes of your entity, which has all the information from your backend response.

5. Should we use Dart Records or Dartz package?

There's nothing wrong with using Records, in fact it's a good way to handle error/success cases without having to depend on external packages,

Wowever, I just prefer to use Dartz because it takes care of much of the logic. If we used Records, we would have to define some logic in our Bloc after deconstructing the Record. We will have to check if any of them is null and then go for the other one, but with the API that Dartz provides, we can easily just add what to do when it's left and what to do when it's right, the code is neater and shorter.

Then we have tests. This is where the Dartz shines the most, giving us the ability to directly return a Right or Left when stubbing or hijacking a request, if we used Records, we won't be able to easily return one type, we will be forced to return a full record, but with Dartz, you can easily return just either Left or Right.

I mean at the end of the day, it's up to you, if you want to use records, but I just think dartz makes the code shorter and neater, eliminating the need for me to write extra logic to make it work.

6. Do we need to test only side, Left or Right?

Yes, only need to test one hand, either right or left, your choice, the reason for this is because we will still test the repository for both success and failure, the same in the remote data source, and also in our bloc, so those tests will cover for it,

The real reason for a usecase is just to connect the domain to the data layer, and as such, the only necessary test will be to check if that call to the repository was made indeed. We only do the right or left answer just to make sure that call works, then finally we verify that the call was even made in the first place.

7. Why are we using Provider and BLoC together?

Because in very simple cases where I just want to "abstract" state, using bloc will be too much boilerplate for something so simple. As I explained earlier in the video, we use bloc for the inter-layer communication, but we might have a case where we have so much stuff going on in the state of a stateful widget, in that case, I might need to move the state to somewhere else, just for neater code, if I used bloc, I would have to do a lot of boilerplate, but with provider, all I have to do is create a simple class and push my state there and convert my widget back to a stateless widget, easy as that. It's "Light", which is exactly why it serves the purpose of simple state abstraction.

Another good use will be for simple state management. Take the UserProvider we created for example, it only contains one variable, a User, and two functions, for setting the user, if we used a bloc, we would have to write two files, and write more lines of code in the main cubit or bloc file, but with provider, just a simple short class is enough to store our user and expose it to the rest of the application.

Bloc is just perfect, but sometimes it does too much, and in programming, it's best to keep it simple wherever you can, and provider is light enough to give us a simple API anytime we need to do something simple.

8. How can we pass authRepository to usecase authRepository?

Let's break it down. Let's start from inheritance, if you notice, the implementation "implements" the repository, "implement" keyword is a way to "inherit" another object in dart, it means that the repository implementation is actually a repository.

Like how you're a child of your dad, you implement your dad, in some sense, you're still your dad because you look like him when he was younger, or in some cases your mum, either way, that's how inheritance works, you look and sometimes act like your parent, someone might say, you're really your dad's child. If someone says they wanna speak to your dad, you could go and represent your dad too, and they can be sure that you will pass the message to your dad.

In programming, this is the same, if the repository implementation "implements" or "extends", then it has become a repository and we can pass it, if you need the repository, then the repository implementation can represent it and say, yep, I'm my dad's child, bhaiyi, give me the message and I'll make sure the repository is aware of it. that's how we can pass in a repository implementation rather than a Repository

9. Difference between APIException and APIFailure

That would be great, but a failure is different than an exception, an exception is "throwable", but a failure is not throwable, think of it this way, whenever we have an exception, we can use the exception, but after we receive the exception, we convert it into a friendlier form, which is a failure, a failure is a dart object we've created, it's a normal class that contains some information about the error, but it's not the same as a throwable, they serve two different purposes, you can try doing throw ServerFailure(), and see it won't work, because it's not a "throwable", normally after you catch exceptions, you don't keep using them in your UI, so in our case, we catch it and create a failure to be used by our UI because it's UI friendly

10. How to structure the project if different features share the same repositories use cases and/or datasource?

You could create a core/common/features/user feature and create your layers in there.

Essentially the idea is that you'll create a common feature for both of them, or for any of your features that share common features.

11. Why do we use http.client for Cubit? Why not lazySingleton?

When you register your App logic holder like your cubit as a factory, it means that every time you request an instance of the Cubit, get_it will create a new instance.

This is useful for components that need to maintain separate states, like your ExamCubit. Each screen or part of your app that uses this cubit should have its own instance to manage its state independently. On the other hand, you register the dependencies as lazySingletons. This means that get_it will create these instances once when they are first requested and then reuse the same instance for subsequent requests.

This will make the performance better, and give us consistent data or connection states throughout our other layers. So, I'd say that registering Cubits or Blocs or anything that's your Logic holder as a factory ensures that each screen or component that uses it has its own separate instance to manage its state independently. Meanwhile, registering the dependencies as lazySingletons optimizes resource usage and ensures consistency when working with data sources, repositories, or other shared services.

If you don't register your http.Client(), then your dataSourceImplementation is going to be unhappy, because it needs it in it's constructor, if you're asking why we even have to take the client in the constructor instead of just initializing it in the data source, it's to follow the Dependency Inversion Principle, this is important when writing unit tests, if you're initializing the http.Client() inside your data source, then when you're testing, you won't be able to hijack the response of your http calls and force it to fail or pass for your particular test cases,because you're stuck with the client you've initialize inside the class and you can't touch that instance from outside the class, even if you make it public and try to hijack it's response, the test will throw an error, you can only hijack or stub MOCK versions of dependencies and not their actual implementations. so, you can't do when(DataSource.client.get()), but you can do when(MockClient.get()), so, our class also has to be able to receive a client to use when we are in the test phase and also able to receive a client to use when we are in the real app, hope this answers your question.

12. Why do we use usecase with extends not with implements?

Generally, not only in dart, but like generally, implements is used for Interface "implementation", if you noticed, everywhere we use implements instead of extends will be for an "abstract" class and we're using abstract classes because that's the only way to create an interface in Dart.

Extends is for Inheritance, when you want to create a new class that is like a specialized version of an already existing class, that's it, it will inherit the field variables and the methods of the super, for example, a vehicle parent and a car child, a car "is-a" Vehicle, I heard somewhere before that inheritance provides an "is-a" relationship between two classes

For Interfaces, implementations come into play, you use this when you want a class to declare that it will provide specific methods and these methods can actually be defined in more than one abstract class, so, it's basically like this, You have a contract and you want to adhere to that contract(follow the rules of the contract), you're not becoming that class(interface), you're just promising to provide implementations for all the methods declared in that class(interface).

abstract AuthRepo {}

AuthRepoImplementation implements AuthRepo {}

AuthRepo contains methods that don't have bodies(implementations) yet, when we implement it with AuthRepoImplementation, we're essentially saying, Yo AuthRepo, I want to provide for those children that don't have bodies, I want to give them bodies while still maintaining their signature. So, if extends is for "is-a" relationship, then implements will be a "can-do" or "supports" relationship, abstract CanFly class Bird implements CanFly Birds "can-do" flying, Birds "support" flying in dart we can even use both extends and implements at the same time.

So, a bird "is-a" WingedCreature and a bird "can-do" Fly, we can also implement more than one interface, because that's what it does, just gives "can-do" abilities, so, you can be able to do more than one thing

Bird implements Fly, SIng {}

Extends => When you want to inherit behaviour and create specialized version of a class Implements => when you want to declare that a class will provide specific methods defined in the interface(contract)

13. What do the core and util folder do?

The core is for anything that is shared between features, it could be a service, a utility, anything at all that more than one feature will use, that will go into the core, sometimes, you even have a whole feature that could go in the core folder because that feature might be related to more than one feature, I've not done that much myself but I've seen people do it with clean architecture.

The Utils is for holding static helper classes, they function as tools that make your work easier, for example, instead of having to reuse colours over and over by calling their colour code, I could store it in a colour utility and just call the Colour.primaryColour Or instead of calling assets/images/image.png everytime, I might want to store it and use it over and over, for that, I'll create a utility for my assets, and define static accessors for each asset so that way I will easily reference them later in code.

Now if you also had a config folder, it would probably hold some configurations that you'll use across features.

Where did the idea come from?, It's mostly habits I picked up throughout my programming journey, I didn't really get them from just one place, just over time, I've tried many things and when they don't work, I try other ways I can think of till I find something that works cleanest for me and I stick to it.

14. Can we move the AuthDataSource abstract class to the domain layer

Well, since it deals with talking directly to the server, it should have absolutely nothing to do with the domain layer, so, it should live in the data, you can totally move it to domain, no problems will occur, but following true clean architecture means putting it in data because it deals with things from the outside world directly. 

15. Can we assign the constructor value inside?

You could use the outside constructor version too, but we mostly use that one not for default value, but for initializing private named parameters, for example

for example,

const Constructor(this._uuid);
final String _uuid;

This would work for optional parameters, but it won't work for named, so, you have to initialize it in the body of the constructor, or outside it

// Take a public uuid, then set the private to be the public
const Constructor({String uuid}) : _uuid = uuid;
OR
const Constructor({String uuid}) {
    _uuid = uuid;
}
final String _uuid;

More questions and answers

1. Why do we use late AuthenticationRepositry repository, why not late MockAuthenticationrepository repository?

2. Why UserModel.empty() belongs to UserModel.dart

3. Why we put interface DataSource and DataSourceImpl in the same file?

4. Changing entity property violates the clean architecture rules?

1. Why do we use late AuthenticationRepositry repository, why not late MockAuthenticationrepository repository?

It won't make any difference, I just prefer to declare the parent type that I'm using then pass the fake version, but you can do it the other way too, writing the mock type and passing the mock type, no difference in my opinion, just preference.

2. Why UserModel.empty() belongs to UserModel.dart

The reason why I've kept it in the model itself however is because sometimes in production code, I still need an empty model maybe when I'm doing something like "create customer", when I push to a "create customer screen", I'll need an empty customer model, saved in my state, then I'll slowly populate it, in which case, I'll just use CustomerModel.empty(), and then copyWith after I get the data from the textfields, again, you could do it the way you said, but I do it this way because of needs I've had when writing prod code.

3. Why we put interface DataSource and DataSourceImpl in the same file?

I don't think that would be okay since each feature has it's own datasource interface and also it's own datasource implementation, putting them in the core folder will confuse us when we have a lot of features, you'll have to sift through many files before you find the particular datasource interface you're looking for, that's why they stay in the same data layer as their implementation, now, about separating them, yeah, you could totally separate the interface and the implementation, you could do that, there's no problem with it, but since it's short and also lives in the same layer as it's implementation, I didn't see any need to separate them, it doesn't defeat the open-close principle, since the abstract class itself is closed for modification, but still open for extension, you could have more than one data source implementation files still extending the same data source interface, in that case, I'd advice putting it in different files, but again, my reason is because it's short in my case and it's only one other implementation, else I'd have split them into multiple files.

4. Changing entity property violates the clean architecture rules?

It doesn't violate clean architecture, you have to know what your backend is responding with to create an entity, an entity is simply a way of packaging your response data into something more usable to avoid errors, for example, if you API returns {name: 'Paul'}, you could go ahead and pass around that json data and access it as data['name'], but that is too error prone, as you can easily type the key wrongly and you won't get any compile time error, but when you have a dart object, that eliminates most of those issues, so, changing your entity's properties to match your API response doesn't break any principles of clean architecture, also, it doesn't mean we are depending on the data layer, dependence would mean athat our entity directly gets data from the datasource, either by having an instance of the data source in our entity and fecthing data directly from that instance or having an instance of something else that depends directly on the data source, but this is not the case here, the enitity is totally standing on it's own.

Comment

Add Reviews