Android-CleanArchitecture
Android-CleanArchitecture copied to clipboard
Composing Use Case observables
Hi guys, great project, at my company we have based an app on this template and the principles in the accompanying blog post.
For some more advanced features in the app, we ran into an issue that is very similar to another one mentioned by @amatkivskiy (https://github.com/android10/Android-CleanArchitecture/issues/63). We would like to be able to compose and combine use cases together to form new use cases.
As mentioned, RxJava provides a huge toolbox of operators for composing observables which would be perfect. However, the Use Case interface in this project accepts Subscribers and doesn't expose Observables directly.
This makes it awkward to compose and combine use cases with RxJava operators. The approach I went with was first to use Subjects to adapt the subscriber interface into an observable, then to create a new custom observable. This works but isn't great as there's a lot of nesting and boilerplate.
I was wondering what the reasoning behind the design choice of having Use Cases accept Subscribers rather than returning Observables is? Is it so that Use Cases hide the knowledge of which scheduler is used to subscribe on/observe on? What would be the downsides of having Use Cases return a single Observable?
I'm still new to using Rx in an Android app architecture, so I'm keen to hear some discussion around this and learn something. Thanks again for contributing this to the community :)
Hi @kiwiandroiddev,
I will try to give you an example on how I understand Use Cases. I think it's intended to be one Use Case for one specific process.
Lets say a flight destination is a Use Case. If your are going to take a flight toward a Country, you cannot take another flight at same time to another, unless you divide yourself right? But if you need it, you can flight toward the Country2, making a transfer/transit when you arrive at Country1 by another flight toward the Country2. Maybe you need to buy something from Country1 before to land to Country2.
Use Cases, usually should deal with Repositories, and Repository calls can be chained because they expose Observables. There is no reason to not have multiple repositories in one Use Case.
Repositories retrieve data, but a Use Case performs a specific operation. Think in Use Cases like that information provided by your Product Team, or by the Team that define requirements, generally using UML.
This is my approach, I hope it can be useful for you.
Best.
@kuassivi Thanks for explanation. But I think that @kiwiandroiddev ment the use case when you want to use for example RxLifecycle. It requires access to the Observable.
Ok, but I think there is no limit to use RxLifecycle inside a Use Case through a Repository, even passing a Provider. That could also decouple the lifecycle logic from your Activity. It is just my point of view.
I have a question about Use Case too. We now encapsulate Observable in an UseCase class. So we need to make some actions like transform data from Domain model to View model on main thread. And it is not really good. We'd better to do transform data on background thread. How do you guys think about it?
@kuassivi small note : RxLifecycle shouldn't be used inside a Use Case, you'd be breaking boundaries. RxLifecycle can be used in the Presenter to compose and subscribe to the Use Case.
@lenguyenthanh Domain model to View model transformations should happen in the Presenter. You can still do scheduling there.
@Trikke thx for answering my question. But Use Case don't expose Observable class for us so how we can do scheduling there?
@lenguyenthanh If I'm not wrong, model transformations are performed in the map()
function of an Observable. That function is performing on the Thread configured in the subscribeOn()
function, which is automatically set in the buildObservable()
method of the Use Case.
To test it, put a breakpoint inside the map()
method of your Observer, then look for the current thread name with Thread.currentThread().getName()
.
@kuassivi It should be like that. But in this example it looks like this:
private void showUsersCollectionInView(Collection<User> usersCollection) {
final Collection<UserModel> userModelsCollection =
this.userModelDataMapper.transform(usersCollection);
this.viewListView.renderUserList(userModelsCollection);
}
private void getUserList() {
this.getUserListUseCase.execute(new UserListSubscriber());
}
private final class UserListSubscriber extends DefaultSubscriber<List<User>> {
@Override public void onCompleted() {
// bla bla
}
@Override public void onError(Throwable e) {
// bla bla
}
@Override public void onNext(List<User> users) {
UserListPresenter.this.showUsersCollectionInView(users);
}
}
You can see that, transforming process is on onNext() which is on main thread.
@Trikke agree, putting .compose(activity.<Long>bindToLifecycle())
inside a Use Case is going to break its boundaries, anyway I don't think Use Cases must expose Observables.
@lenguyenthanh yep. The transformation should be inside the UseCase to work with the same thread. Something like:
@Override public Observable buildUseCaseObservable() {
return this.userRepository.users().map(users -> this.dataMapper.transform(users));
}
@kuassivi Agree, but (but again :smile: ) UseCase should not know about presentation layer so it normally can't have dataMapper instance. So how do you solve this problem? Using DI or any other way?
@lenguyenthanh Ah yes, i noticed. This example has a different setup for Use Case subscription than the one i'm using. Sorry, i forgot that. But are Domain to View Model transformations that expensive? What kind of expensive objects are you mapping, or are we talking about thousands of objects being mapped at the same time? Usually this is just some data mapping. Maybe just map them in the subscriber in the Presenter, and do some performance testing to see if this is really an issue.
@kuassivi Transformations to View Models can't be inside the Use Case, you'd be breaking boundaries.
No mention, because this probably can be a long discussion.
If you are pulling new data when scrolling down a list, something usual, you may need to map every time your retrieved data, so that can be expensive.
I don't think I'm breaking boundaries in this case, although I see there are a lot of boundaries. A mapping process in a Observable is common, and I'm not specifying what kind of mapper is in the implementation.
If you have a mapping process to be performed for that Use Case, then provide it, otherwise not.
You are breaking boundaries in this case because the Use Case is Domain-specific and the mapper maps Domain Entities to View Models. Because that mapper knows about View-specific logic, it must reside in that layer and cannot be used by the Use Case.
For @lenguyenthanh , there are a few solutions:
- map data on the subscriber in the Presenter and pass View Models to the View. Performance can then be tested to see if this mapping is actually a problem for the UI thread. This depends on how much data and how often this would happen
- expose the Observable chain in the Use Case so the Presenter can take advantage of that.
I'm using the 2nd approach already, mainly because in my sentiment the data flow doesn't stop at the Use Case. It should continue flowing through to the Presenter as there is View logic that consumes or reacts to this data. (ie : map stuff, show buttons on certain conditions)
Sure, but as I said ... I'm not specifying what kind of mapper is in the implementation.
of the Use Case.
The mapper can map from one domain to another different domain right? In this case, will it break boundaries?
@Trikke thanks for your solutions. I prefer 2nd option too, it's more flexible. I'm thinking about adding transformer object to Use Case objects but it doesn't look good. Could you please give me an example of how you implement it?
@kuassivi We were talking about a Domain Entity to View Model mapper, which @lenguyenthanh was asking questions about. I'm not sure what you mean by "I'm not specifying what kind of mapper is in the implementation". A mapper just maps data from one kind to another, But where this mapper resides depends greatly on what data we are talking about.
The "Domain" is a layer in which al your business logic must reside. Usually Use Cases and other classes that describe the "core business" of your app. There is just one "Domain" layer. The "View" or "Repository" are not other "Domains", but actual separate layers. So when data crosses a layer, we have to make sure an inner layer knows nothing of the outer layer. Since the "Domain" layer is an inner layer opposite to the "View" layer, we have to take care in not exposing the View Models in the "Domain" layer.
@lenguyenthanh in my implementation, the Observables in a Use Case are just exposed, but do contain all business rules. The Presenter can chain on that, but only for view logic.
I'll give an example here of something simplified and made up. Please don't look at this code for compile correctness, i quickly typed it by hand.
in Use Case (there is no specific business logic for getting a User, so we just return one from the Repo)
public GetUserDetailsUseCase(String userId, UserRepository userRepository) {
this.userId = userId;
this.userRepository = userRepository;
}
public Observable<UserEntity> get() {
return userRepository.getUser(userId).compose(this.<UserEntity>applySchedulers());
}
in Presenter (on button press, we load a user, map it to the view model, and let the view render that)
public loadUserOnButtonPress() {
getUserUseCase.get()
.map(this.structureToModelMapper.transformUser)
.subscribe(new Subscriber(){
@Override public void onNext(UserModel user) {
getView().showUser(user);
}
});
}
@Trikke I get your ideal, you made my day. Thank you so much.
Kudos @Trikke :+1:
@Trikke so you are following a little bit different kind of building a UseCase. I mean according to this repo your implementation is a little bit different as you break the abstraction buildUse...
.
@spirosoik No, he just changed the method name and return type. Otherwise the method is the same as buildUseCase
.
I finally realized it could be interesting to expose the Observable, so following the suggestion from @Trikke I refactored the UseCase abstract class to this one. I hope this helps someone.
public abstract class UseCase<T> {
private final ThreadExecutor threadExecutor;
private final PostExecutionThread postExecutionThread;
protected UseCase(ThreadExecutor threadExecutor,
PostExecutionThread postExecutionThread) {
this.threadExecutor = threadExecutor;
this.postExecutionThread = postExecutionThread;
}
private Observable.Transformer<T, T> applySchedulers() {
return observable -> observable
.subscribeOn(Schedulers.from(threadExecutor))
.observeOn(postExecutionThread.getScheduler());
}
protected abstract Observable<T> buildUseCaseObservable();
final public Observable<T> get() {
return buildUseCaseObservable().compose(applySchedulers());
}
final public Subscription execute(Subscriber<T> subscriber) {
return get().subscribe(subscriber);
}
}
public class GetUserDetails extends UseCase<User> {
private final int userId;
private final UserRepository userRepository;
@Inject
public GetUserDetails(int userId, UserRepository userRepository,
ThreadExecutor threadExecutor,
PostExecutionThread postExecutionThread) {
super(threadExecutor, postExecutionThread);
this.userId = userId;
this.userRepository = userRepository;
}
@Override protected Observable<User> buildUseCaseObservable() {
return this.userRepository.user(this.userId);
}
}
I haven't provided the unsubscribe()
method because I prefer to handle them with a CompositeSubscription
in the Presenter.
@spirosoik I'm just using a different implementation, and passing the data flow into the Presenter. No boundaries are broken. The Presenter ultimately handles the Subscription
.
It's quite easy to then map data to an Adapter
or a paging mechanism. This video also talks about this and they show ways they have implemented this.
Thanks @Trikke and @kuassivi, that has explained a lot. From that I gather there's nothing wrong with exposing Observables from the domain layer, and I think we'll move towards that in our project to get that extra flexibility in the presentation layer.
What's still not completely clear to me is the thinking behind encapsulating Observables in the domain layer in the original implementation. As @Trikke says data flow doesn't stop at the domain layer and surely you would get the biggest payoff from a reactive architecture by applying it all the way down the stack. I'm curious as there must be some advantage to this approach?
@kiwiandroiddev right! As an app scales, there are new requirements and surely refactors must by applied. This is, let's say a pet project for learning purpose and in this specific use case, not a lot of data composition is required at the presentation layer. With that being said, this is the main purpose for these sort of discussions: to receive feedback and input in order to improve the codebase :smile:
@android10 That makes sense :) This project and these kind of discussions are great for advancing the state of art of Android app development I think, so thanks again for the contribution.
Is it should be ok or considered bad practice, if we mix the implementation?
I mean some UseCase expose it's observable and other hide it. Because not all UseCase need to be chained or composed with other observable.
@rshah , no don't start mixing. You stick with either system. And it's generally a better idea to have them composable, even if you don't "need" to do so for some use cases.
@Trikke What about exposing types ? In your example UseCase knows about UserEntity which is not part of domain, but part of data. If response from api is different from model domain is know about then UseCase should now about this type, but I think response from server should not be a part of domain
@zoopolitic Indeed, an api response wouldn't be part of the domain. Usually that would reside in the Data Layer. And you'd use a Mapper to map it from a UserApiResponse to a UserEntity.
@Trikke But your Use case return Observable< T >, so if api returns UserApiResponse - then UseCase will return Observable< UserApiResponse > and this response is part of data, but domain knows about it it this case