How To Write Use Cases (Interactors) in Java

So-called “use case” objects, also known as “interactors”, are the most recognizable aspects of Robert Martin’s “Clean Architecture” school of thought. This approach to software architecture became very popular lately and you can find its implementations in pretty much any object-oriented language.

While the notion of use case object might seem very simple on the first sight, implementing them properly takes quite a bit of skill in practice. Therefore, in this post, I’ll describe how to design proper use cases in Java and avoid the most common mistakes.

Simple Use Cases

Generally speaking, each use case object should encapsulate one single business flow. Flow, for the sake of our discussion here, is just a sequence of one or more steps.

However, the above definition of use case object bears the question: what distinguishes “business flow” from some other, “non-business” flow? Well, as far as I know, there is no specific litmus test for that. Therefore, in practice, you can extract pretty much any flow into a standalone use case.

For example, this use case handles login process:

public class LoginUseCase {

    public static class LoginResult {
        ...
    }

    public LoginResult logIn(String username, String password) {
        ...
    }
}

And this one can be used to transfer funds to another account:

public class TransferFundsUseCase {
    public static class TransferFundsResult {
        ...
    }

    public TransferFundsResult execute(Account source, Account target, Money amount ) {
        ...
    }
}

Notice two “rules of thumb” that I follow in these use cases:

  • Names of use case classes should explain what they do
  • There should be just one public method that invokes the flow

In addition, I add “UseCase” postfix to my use cases. That’s not mandatory, but I like doing that because it makes it very easy to distinguish use cases from other classes. Then, when I read old code, I can easily see which flows are triggered from specific components.

With respect to the names of use cases’ methods, there are two mainstream approaches.

The first one, which I prefer, is to use meaningful names which describe what these methods do. That’s what you see in the first example above.

The second approach is to just use the same generic name, like “execute”, in all use cases. That’s the second example. Developers who use the second approach argue that since the names of use case classes already describe the encapsulated flows, there is no need to duplicate this information in methods’ names. These developers do have a point, but I, personally, still prefer to be explicit in methods’ naming because when I read old code I pay more attention to methods’ names rather than to class’ names.

Synchronous Use Cases

If you’re backend developer, the previous section contains almost everything you need to know about use cases. But if you develop Android applications, we haven’t even started yet.

The additional complexity in Android arises because many (maybe even most) use cases encapsulate time-consuming flows, by Android standards. These are the flows that can, potentially, take more than few milliseconds to execute. So, whenever you need to make a network request, fetch data from local database, write to a file or even just perform some non-trivial computation – all of these are time-consuming steps.

As you probably know, if you’d invoke time-consuming flows from UI thread in Android, you’d get janky user interface or even full-blown hangs of the application. And since you’ll be initiating most of your flows from UI thread, both use cases that you saw in the previous section can’t be invoked from there.

The simplest way to address this problem is to just assume that components which will invoke use cases will take care of switching the execution to background threads. However, that’s a bit risky assumption to make, especially if you’re not the only developer on the project.

To make the aforementioned assumption about invocation from a background thread a bit safer, you can explicitly mark use cases as “synchronous”. In this context, synchronous use cases are the ones which execute time-consuming flows, synchronously. In other words: these use cases can block the threads that call them for prolonged periods of time. Both use cases described in the previous section are synchronous by nature, but they aren’t marked as such.

To make it very clear that use cases shouldn’t be invoked from UI thread, you can change their implementation a bit in the following manner:

public class LoginUseCaseSync {

    public static class LoginResult {
        ...
    }

    @WorkerThread
    public LoginResult logIn(String username, String password) {
        ...
    }
}

and

public class TransferFundsUseCaseSync {
    public static class TransferFundsResult {
        ...
    }

    @WorkerThread
    public TransferFundsResult execute(Account source, Account target, Money amount ) {
        ...
    }
}

Note the additional “Sync” postfix in class names. This is just a convention that I myself adopted. Whenever I see this postfix, I (and other developers) immediately know that that use case should be invoked from a background thread.

In addition, I add @WorkerThread annotations to use cases’ methods. These annotations also serve as hints to developers, but, more importantly, they can be used by static analysis tools to warn you if you accidentally call these methods from UI thread.

Limitations of Synchronous Use Cases

Using explicit synchronous use cases is a valid strategy, but it’s associated with a bit of inconvenience, moderate risk and a big design issue.

Let’s imagine that you have FetchUserDetailsUseCaseSync use case which is used in 10 different places inside your app. It means that you’ll need to transfer execution to a background thread 10 times wherever you invoke that use case. In addition, upon completion of some of these calls, you’ll probably need to return the execution back to UI thread. In other words, you’ll need to duplicate the associated multithreading logic in many places. This is time-consuming and inconvenient.

The bigger problem with this approach is that multithreading logic is tricky, so by duplicating it 10 times you increase the probability of having multithreaded bugs in your app ten-fold. That’s a considerable risk which you’d most probably want to avoid.

However, in my opinion, the biggest problem with relying on synchronous use cases exclusively is bad design. See, your Activities, Fragments and other types of UI controllers (including the notorious ViewModel’s) shouldn’t deal with multithreading at all. All their code should execute on UI thread and you should avoid any kind of multithreaded constructs there. But if you use synchronous use cases, then, eventually, absolute majority of your UI controllers will need to deal with multithreading and, in my experience, this leads to very messy and error-prone code.

Asynchronous Use Cases Using Callbacks

So, synchronous use cases have some major limitations. How do you work around them? Well, you can make your use cases asynchronous.

The simplest way to design an async use case would be to just pass a callback as method argument:

public class LoginUseCase {

    public interface LoginCallback {
        void onLoggedIn();
        void onLoginFailed();
    }
    
    public void logIn(String username, String password, LoginCallback callback) {
        ...
    }
}

Using this approach, use cases can implement their own multithreading strategy, so they can be invoked on any thread. Later, when the flow completes, one of callback’s methods will be called on UI thread. This scheme is very simple and allows for easy reuse of use cases, but it comes with one major drawback: it’s not “lifecycle-safe”.

As I explained in my posts about Activity and Fragment lifecycles, these UI controllers must be active only when they’re in “started” state. Therefore, they shouldn’t receive any notifications after their onStop() method is called. But in the above example, since the use case is asynchronous, callback’s methods can be called at any time. Therefore, this approach must be augmented.

There are basically two main ways to deal with lifecycle-awareness:

  • Check controller’s state inside callback methods and discard the notification if the state is “stopped”
  • Make use cases cancellable, and then cancel the execution in onStop()

I recommend against making use cases cancellable. Cancelling any flow is tricky, but cancelling multithreaded flows with asynchronous callbacks is a different level of “tricky”. I would estimate that if you’d have 50 cancellable use cases in your app (which is not that much, really), then your chances of not having multithreading bug in at least one of them are close to zero.

Now, in some cases you’ll need to make your use cases cancellable due to product requirements. For example, if your app downloads files from the internet, you’d want to allow the user to cancel ongoing downloads. That’s totally fine. What I do say, however, is that you shouldn’t make your use cases cancellable just to work around lifecycle problem. It’s just too risky.

And no, there is no need to cancel simple database queries or HTTP requests (again, unless it’s explicitly required by product folks). That’s just over-engineering with additional risks for no particular gain.

Asynchronous Use Case Using Observer Design Pattern

So, lifecycles complicate the usage of simple callbacks due to risk of getting spurious notifications in invalid lifecycle states. You can add lifecycle checks to callbacks’ methods to address this issue, or even make your use cases cancellable, but I find both of these approaches too error-prone.

Therefore, I prefer to use Observer design pattern to implement asynchronous use cases:

public class LoginUseCase extends BaseObservable<LoginUseCase.Listener> {

    public interface Listener {
        void onLoggedIn();
        void onLoginFailed();
    }

    public void logInAndNotify(String username, String password) {
        ...
    }

    @UiThread
    private void notifySuccess() {
        for (Listener listener : getListeners()) {
            listener.onLoggedIn();
        }
    }

    @UiThread
    private void notifyFailure() {
        for (Listener listener : getListeners()) {
            listener.onLoginFailed();
        }
    }
}

You can see the source code of BaseObservable class in this Gist.

The idea is simple: other components can register as observers, and then, upon completion of the encapsulated flow, use case notifies all registered observers about the results. Please note the usage of postfix “AndNotify” in method’s name. That’s another convention of mine that helps code readability: whenever you see method with that postfix, you know that it will result in async notification using some callback interface.

When you use this approach, UI controllers register to all relevant use cases during onStart() and unregister during onStop(). As long as this simple convention is followed, they won’t receive spurious notifications and you won’t need to make use cases cancellable.

Therefore, Activities, Fragments and other controllers will contain code like this:

    @Override
    public void onStart() {
        super.onStart();
        mDownloadClipUseCase.registerListener(this);
        mUpdateClipFavoriteStatusUseCase.registerListener(this);
        mUpdateClipDeleteProtectionStatusUseCase.registerListener(this);
        mFetchRecordingsForClipUseCase.registerListener(this);
        mDeleteRecordingUseCase.registerListener(this);

        ...
    }

    @Override
    public void onStop() {
        super.onStop();
        mDownloadClipUseCase.unregisterListener(this);
        mUpdateClipFavoriteStatusUseCase.unregisterListener(this);
        mUpdateClipDeleteProtectionStatusUseCase.unregisterListener(this);
        mFetchRecordingsForClipUseCase.unregisterListener(this);
        mDeleteRecordingUseCase.unregisterListener(this);

        ...
    }

The benefit of this approach is that it’s simple. You don’t need to deal with cancellation inside use cases, and you just need to follow a simple pattern inside UI controllers. However, it’s not all unicorns and rainbows.

The first drawback of this scheme is that it makes the implementation of use cases more complex. In addition, there are all these register/unregister calls in the controllers that you need to keep track of.

The need to unregister from use cases also means that you’ll need to keep references to implementations of callback interfaces. Therefore, either the controller itself will implement them (or some other component that invokes use cases), or you’ll have multiple inner classes. In either case, you can end up with many callback methods scattered inside the components that invoke use cases. This makes understanding the overall application’s flow more difficult.

Despite the drawbacks, I, personally, find this approach to be the most convenient in most cases, so that’s what I usually do.

Alternatives to Observer Design Pattern

Implementing classical Observer design pattern is not the only way to create “observable” use cases. You can also achieve similar results using RxJava or LiveData. There are probably also other tools that I just don’t know about.

I, personally, don’t use any of these alternative approaches. They have their own benefits and drawbacks and the trade-offs never seemed worth it to me. However, if you already use them inside your projects, I’d err on the side of standardization and keep using them to implement use cases as well.

Composition of Use Cases

In the previous section you saw how to use Observer design pattern to implement an asynchronous use case. This implementation will be simple to use from UI controllers, or any other component. However, when it comes to composition, Observer design pattern is non-optimal. Let me explain what I mean by that.

Imagine that you have FetchCurrentUserUseCase. You want to invoke this use case from mutliple UI controllers in your app to display current user’s info on the screen, so you implement it using Observer design pattern. So far everything is great. But then you realize that you need the ability to use user’s info as part of PlaceOfferUseCase. What can you do in this situation?

One straightforward solution would be to make CurrentUser data structure (which is what FetchCurrentUserUseCase produces) part of that use case’s API:

public class PlaceOfferUseCase extends BaseObservable<PlaceOfferUseCase.Listener> {

    public interface Listener {
        void onOfferPlaced();
        void onOfferPlaceFailed();
    }

    public void placeOffer(NewOfferDetails offerDetails, CurrentUser currentUser) {
        ...
    }

    ...
}

This solution will work, but there are (at least) two major problems with it.

The first problem is that you leak implementation details of PlaceOfferUseCase to the outside world. Now, each component that invokes this use case will also need to depend on CurrentUser. The second problem is code duplication. If you use this scheme, each component that uses PlaceOfferUseCase will need to duplicate the logic to fetch the details of the current user and then pass them into placeOffer() method.

All in all, in my opinion, you should avoid the above approach. That’s exactly one of these “quick and dirty” fixes that make your life so much harder in the medium and the long term.

An alternative solution is to pass FetchCurrentUserUseCase into PlaceOfferUseCase as constructor argument. This way, PlaceOfferUseCase will be able to fetch user’s details whenever needed and external clients won’t even know about that. In other words: let’s create composite use cases.

Using use cases’ composition is a great idea, but there is one problem: FetchCurrentUserUseCase is an async use cases that implements Observer design pattern. Therefore, if you use it from within PlaceOfferUseCase (which is Observable by itself), you’ll basically need to find a way to compose Observables. While that’s technically possible, it’s tedious, error-prone and leads to very complex code. Therefore, I recommend to avoid doing that either.

However, the problem is not composition per-se, but the fact that composing async use cases is tricky. Therefore, no need to throw the baby (the composition) out with the bathwater. If composing async use cases is tricky, let’s compose sync use cases instead.

So, I’ll extract the domain flow logic from async FetchCurrentUserUseCase into the corresponding sync version called FetchCurrentUserUseCaseSync, and then use it from within PlaceOfferUseCase:

public class PlaceOfferUseCase extends BaseObservable<PlaceOfferUseCase.Listener> {

    public interface Listener {
        void onOfferPlaced();
        void onOfferPlaceFailed();
    }
   
    private final FetchCurrentUserUseCaseSync mFetchCurrentUserUseCaseSync; // initialized from constructor argument

    public void placeOffer(NewOfferDetails offerDetails) {
        ... 
        CurrentUser currentUser = mFetchCurrentUserUseCaseSync.fetchCurrentUser(); // executed on bg thread
    }

    ...
}

Since inside placeOffer() method I’ll need to transfer the execution to a background thread anyway, I can safely call to fetchCurrentUser() synchronously and wait for the result. This gives me the best of both worlds: composition of use cases and simple synchronous code.

However, by extracting the functional logic from FetchCurrentUserUseCase into the synchronous FetchCurrentUserUseCaseSync, I left the former non-functional. That’s not good because there are still clients that use it. Should I copy the logic back, thus creating duplication between the sync and the async versions? Not at all. Instead, I can simply use the sync version from within the async one:

public class FetchCurrentUserUseCase extends BaseObservable<FetchCurrentUserUseCase.Listener> {

    public interface Listener {
        void onUserFetched(CurrentUser currentUser);
        void onUserFetchFailed();
    }
   
    private final FetchCurrentUserUseCaseSync mFetchCurrentUserUseCaseSync; // initialized from constructor argument

    public void fetchCurrentUserAndNotify() {
        ... 
        CurrentUser currentUser = mFetchCurrentUserUseCaseSync.fetchCurrentUser(); // executed on bg thread
        if (currentUser != null) {
            notifySuccess(currentUser);
        } else {
            notifyFailure();
        }
    }

    ...
}

As you can see, synchronous use cases are simple to compose and it doesn’t take much effort to reuse them in asynchronous use cases, which basically become thin wrappers.

One additional advantage of extraction of domain flow logic into sync use cases is eaiser unit testing. This allows you to write unit tests for domain logic without dealing with either multithreading or asynchronous notifications. Therefore, if I see that specific flows become very complex, I might employ the same design approach to make the logic easier to unit test even if I won’t use the synchronous version in other places.

Summary

In this post I described several strategies that you can use to implement use case classes. These classes, also known as interactors, are probably the most known aspect of “Clean Architecture” school of thought.

Let me list some rules of thumb that you should keep in mind when implementing use cases:

  • Names of use case classes should explain what they do
  • There should be just one public method that invokes the flow
  • Either explicitly mark time-consuming use cases as synchronous, or make them deliver results asynchronously
  • Asynchronous notifications must be delivered on UI thread
  • Make sure that UI controllers can’t get notifications from use cases when they’re stopped
  • Avoid cancellable use cases, unless explicitly required by product’s requirements
  • Compose use cases to reuse existing domain flows in other use cases

One of the main takeaways from this post should be that there is no silver bullet and no shortcuts when implementing use cases. I showed you both the benefits and the drawbacks of various approaches, and also explained my personal preferences. However, as it’s often the case in software, the choice between different alternatives is not a clear cut, but a trade-off. So, now, after you know about your options, you can make an educated decision on your own.

In the follow-up article, I’ll show how to write use cases in Kotlin.

As always, thanks for reading. You can leave your comments and questions below.

Check out my premium

Android Development Courses

20 comments on "How To Write Use Cases (Interactors) in Java"

  1. As always, nice article 🙂 I have few questions, though.
    These interactors are concrete classes, right? I currently implement it as concrete class. And usually, it depends on some gateway let’s say AuthenticationGateway that will invoke gateway.login(param) every time a call to usecase.execute(param) is made. So far, I don’t have issues with this approach. A dilemma arises when inside this interactor say LoginUserUseCase, I have to perform some logic based on the return of the gateway and save it to database. Currently, I use another interactor called SaveUserUseCase to save the user. My question is, am i doing it right? I mean I can do it differently like using another gateway say UserGateway so that my LoginUserUseCase now depends on AuthenticationGateway and UserGateway and call appropriate methods on these gateways.

    In addition to my question,
    1. I actually don’t know if depending on another interactor is a good idea. If it is, how do I do this with multi-module world say LoginUserUseCase lives on authentication module and SaveUserUseCase lives on User module. Do I have to create another use case that combines these two?
    2. How to handle transaction?

    Reply
    • Hello Edwin,
      In my opinion, if you have use case class then you don’t need a “gateway” (I suspect that your “gateways” are similar to “repositories”).
      So, LoginUseCase should send POST to the server, process the response and then, if the response is success, store user’s credentials locally. That’s just one single flow and I don’t see any reason to introduce additional abstractions (except for “technical” abstractions like Retrofit’s Api implementations, or ORM’s DAO wrappers; if that’s what you mean by “gateway”, then it’s alright).
      However, if after successful login you also want to initialize user’s data, then it might be appropriate to extract this flow into standalone use case and then delegate to it from LoginUseCase. Especially if you need to reuse this functionality in some other flow, irrespective of login.
      As for your questions:
      1. Depending on another use cases is a valid idea and I do that all the time.
      2. Not sure what you ask specifically.
      Regards

      Reply
      • What do you mean exactly by – However, if after successful login you also want to initialize user’s data, then it might be appropriate to extract this flow into standalone use case and then delegate to it from LoginUseCase ?
        If you store like user profile, etc? more complex structures than username and password then you should extract into another use case?

        Reply
        • Let’s say that you build e-commerce app and after successful login you need to fetch user’s cart, balance, orders history, promotions, etc. You can do it as part of LoginUseCase, but this functionality isn’t really related to login. It’s just “conicidental” that it comes straight after succesful login. To obey Single Responsibility Principle, this initialization of user’s state should probably be extracted into standalone class. In addition, chances are that at some point you’ll want to execute the same init in other parts of the app. If you’ll have a dedicated use case for that by then, its reuse will be trivially simple.

          Reply
  2. Superb article and I really liked how you have shown the potential approaches to the problem and explained their drawbacks.
    Could you also add an example on how to use Livedata with usecase. In my project I create LiveData object in the Viewmodel and then activity or fragment observer it. From viewModel I create an instance of usecase class and pass the same LiveData instance to usecase class. In the usecase I create a thread which has access to Livedata, make some synchronous call from inside thread and post the result using livedata. Is this a good approach? Will this cause any memory leak issue?

    Reply
    • Hi Hitesh,
      Glad you liked the article.
      I’m not using LiveData at all, but, as far as I can tell, as long as you register to LiveData with LifecycleOwner, there shouldn’t be any memory leaks in your approach. BTW, it’s really not that difficult to check for memory leaks using Android Studio’s profiler and heap dumps.
      Vasiliy

      Reply
  3. Enlightening article?. Please what do you think about use cases that just do one CRUD operation local database? Do they qualify to be called a use case? Also, if a particular screen in an application using MVP presentation architectural pattern has many use cases, and these use cases are injected to the constructor of the presenter of that screen, the number constructor becomes too many, what would you do in such a scenario?

    Reply
    • Hello Kyle,
      Use cases are domain flow abstractions. If domain flow involves just one CRUD operation, then it qualifies as a use case. For example, if your app has “favorites” functionality, then AddToFavoritesUseCase could very well just store one ID into a database.
      As for many constructor arguments, for more complex controllers I sometimes had 10+ use cases. There is nothing wrong with that – it simply reflects the complexity of the controller itself. The only reasonable thing to do in this case would be to break the controller into multiple controllers, but I wouldn’t do that just due to the number of constructor args. I only break controllers when it makes sense from design point of view (e.g. reuse part of functionality in multiple places).

      Reply
  4. Awesome Article as usual.
    However, i would like to ask you the following:

    Since all use cases follows the same pattern,aka one single method
    (“There should be just one public method that invokes the flow”)
    I’m finding a commonality with the ‘Command Design Pattern’ where the command implements the Command interface which the latter has only one execute() method to be implemented by the concrete command classes (which in our case are the use cases described in your article)

    I’m a self taught developer, trying to improve my OO instinct by reading books and following your Contents (Blogs and Udemy courses)

    Can you please clarify to me, what would be the drawbacks/advantages of implementing the Command design pattern in Android application?

    Example(to clarify my question)
    Assume we have have to fetch some data from the API using retrofit.
    In Command Design Pattern World:
    Retrofit is our service. (to be used by the command impl)
    FetchDataUseCase is our command impl.
    controller (Activity/fragment) is our invoker.

    In the case of dependency inject, we inject the Command Factory to our controllers (Similar to the ViewMvcFactory)

    I would appreciate if you can provide your insight about the above implementation regarding its disadvantages/advantages.
    Thank you

    Reply
    • Hello Hadi,
      This is very interesting question which shows that you think deeply about design.
      Command design pattern is used when a client needs to invoke multiple polimorphic actions without being concerned with their details, the number of actions and other constraints. So, for example, Android’s Looper implements command pattern – it receives Runnables and invokes them in order on a single thread, but it doesn’t know anything about their contents or side effects.
      I believe that theoretically you could fit use cases into Command pattern, but the result of that would definitely be sub-optimal. See, there is absolutely no need for controllers to be unaware of the specific use cases that they invoke. In fact, the fact that I can see controller’s declaration and immediately understand which use cases it depends on is a super-power in terms of readability. Therefore, introducing additional complexity to hide this useful info is not a good idea. Furthermore, use cases need to receive parameters from the controllers and then notify back about the results of execution. Theoretically, you can shoehorn this aspect into Command as well, but it will make your code even more complex and obscure.
      Bottom line: Command pattern is used to abstract the “commands” from the clients, whereas in this case I asctually want the clients to know about specific use cases. Otherwise, I wouldn’t invest that much effort in their naming.
      Side note: I don’t even recommend having a base class for use cases with some “execute” method. I’ve seen this pattern in several apps and it has always made the code more complex for zero benefit. You can see how bad it can become in this code review of Google’s Android Dev Summit app.

      Reply
  5. I have TimerUseCase which do some stuff very interval, but you point out there will be single public method, how i can start and stop that TimerUseCase. i mean do i don’t need two public methods for that? or i can just have a single Request with have start or stop boolean?

    Reply
    • Hey Shakil,
      First of all, I’d make sure that this timer is indeed a domain concept. If it’s used just for stuff like debouncing, timeouts, elapsed time measurement, etc, then it’s a general component which, probably, shouldn’t be a use case.
      On the other hand, if that’s some kind of a special domain flow, then it’s totally alright to have additional methods (e.g. cancel()) in use cases. Having one public method to invoke the use case is just a general rule of thumb, not any kind of law.
      Vasiliy

      Reply
    • Hi Arian,
      I don’t use “repositories” in my code. In my opinion, if you have “use cases”, you don’t need another level of abstraction. In this context, I highly recommend this article which covers this topic in much more details.

      Reply
  6. If the data is something that is streaming (ie constant changes in data) and not a one time fetch for data, would it make sense for the streaming data to be a use case as well?

    Reply
    • Hey Mark,
      I think it’s a reasonable approach and I use it as well. However, in these cases, I try to reflect this aspect in the name of the use case (e.g. FetchAndMonitorDataUseCase).

      Reply
  7. Vasiliy, first of all, thanks for your efforts!
    I have a question. As far as I understand it, according to Robert Martin’s “Clean Architecture” it is recommended to convert data when passing it between layers of your architecture: e.g. if you have some Data (domain level) and want to display this data then your should convert it to say DisplayData (presentation layer) that will contain only the information that is needed for the Data to be displayed.
    1) Is this advice(rule) relevant for the framework specific entities? I like this approach because it leads to the strict boundaries between different layers, which seems very flexible to me and the resulting code should be more testable. Moreover, doing this way we can have most of code being framework/platform independent (e.g. only use Android SDK and another frameworks deeply inside implementations of our interfaces).
    2) If the previous question has sense, how to deal with the following case? Let’s say we have to display camera preview in our application. Android SDK provides Camera API to stream camera preview into Surface object (which also belongs to Android SDK). Surface should be prepared by the view. So we have the following chain:

    viewInitialized -> viewModelSetPreviewSurface(Surface) -> startPreviewUseCase(Surface)

    As we can see we should pass Surface instance between the layers. Does this violate the Clean Architecture principles? Thanks in advance.

    Reply
    • Hi,
      I don’t usually use standalone sets of data structures for each “layer” inside the codebase. IMO, this creates too much work and is unpractical in most cases. Maybe there are some types of projects where this would make sense (e.g. projects where each layer belongs to completely different team), but I haven’t encountered them yet.
      Furthermore, Surface instance is not a data. It’s a functional object.
      Lastly, I’d prefer to keep Surface in UI layer exclusively.
      All in all, Camera is such a nuanced and delicate beast that my recommendation would be to just make it work and only then think about “proper design”. After all, there should be just several classes that deal with this part of your app, so it’s totally fine if they’re not “perfect” – their imperfection won’t spread in the codebase.

      Reply
  8. Hello Vasiliy!
    Thanks for the article!
    How, in your opinion, properly use use cases with preferences (e.g. jetpack data store)? I mean, is it ok to have one PreferenceUseCase for saving and getting different values? Or better to break it somehow?
    Thank you in advance!

    Reply
    • Hi,
      The way you persist data is not part of the domain considerations, but an implementation detail. Therefore, I don’t think you need to have a use case for working with preferences at all. What you can do is to have some kind of “preferences helper” class which encapsulates all the details and your use cases use this class when they need to store/retrieve data.
      Regards

      Reply

Leave a Comment