Have any of you practicing clean architecture in Android apps, stumbled upon a situation where you h...
m
Have any of you practicing clean architecture in Android apps, stumbled upon a situation where you have a use case that orchestrates work on multiple repositories? How do you ensure atomicity of such an operation? F.e. if operations on the repositories are implemented as suspending functions, how do you make sure that calling the use case twice from separate coroutines won't leave the app in an inconsistent state? Let's say the previous use case call hasn't yet completed and you call it again. In such a case there seems to be no place for keeping the ongoing operation job so it can be cancelled, or place to keep the mutex to guard the section.
d
Wondering if the Unit of Work design pattern would help here?
m
Not sure... To my understanding that would require different approach to repositories than what we usually do in Android apps doesn't it? Normally the repositories on Android are written in a way where they expose suspending functions and the work is done when called. And if I call the unit of work in one coroutine, and then the other one in second coroutine I would have the same problem as I currently have using use case. I don't even need to have fully fledged transaction, I would just like to have a place to keep a reference to the running job so I can cancel it if next one is started.
d
It sounds like you just explained half of your solution. I guess the next would be the ability to reverse any work that was done if the job needs to be cancelled?
m
I am ok to simply overwrite what has been done if the use case was canceled because it has been started again with different params. I could make the repository smarter, move some code from the repositories to data sources and have the cancelation or queuing done in the repository level. But that would mostly defeat the point of having use case.
The thing is I need the use case to run in the scope of the application as the viewmodel that started it will die (it's a bottom sheet). But it's theoretically possible to open the bottom sheet again and call the use case second time. At that point it could be that only half of the previous use case work has been done, and what's worse the second part of the first use case work, may be executed after the whole work of the second use case is done, overwriting the expected result.
I know technically how to prevent it. But I am struggling fitting it into clean arch.
d
You could inject the same UseCase and make the status of its operation observable. Or you could have it know how to handle it's own cancelation.
Switching over to a scope managed by the Application class is not too hard.
Google even recommends doing that for calls that must complete after triggered
m
Yes, that's one way. Although to my understanding use cases should be stateless, but maybe I am wrong? 🤔 it's already switched to app scope. So the use case is fully asynchronous.
I've seen google presented in one guide how to make the use case outlive the view, but back then they didn't show how to cancel ;) do you have link to this article, I couldn't find it again. Nvm got it
f
well, if we look at repositories from the point of domain driven design, you would have one repository per aggregate root. The aggregate root itself should represent a transaction boundary. What that means for your context is that if you need atomicity across these operations, maybe they should live within the same aggregate root?
m
DDD is something I am still digesting and strive to understand some of the concepts. What's more I have never had a chance to see a mobile app code written according to the ddd rules with rich domain, so hard for me to answer that. But as far as I understand that would mean the repository doing more. Then I wouldn't have the problem of where to keep the state of the work, it would be in the repository. Btw if you know any good book regarding DDD with more emphasis on Android I would gladly read it. I am currently going through the blue book, but it's... well... big
f
Hard to say whether it would be doing more or maybe just different things? Maybe you can share some code samples? With regards to DDD, I find the discussion on the stratetic patterns much more helpful than the tactical ones. In that sense, I dont think youll find a book that relates these concepts specifically to Android as they are more high level on how you approach the design of a software project.
In that sense, I think it's worth going through the blue book 😊
m
I am happy to share my use case. There is a server API that serves a list of available wallpapers in json format. The wallpapers include urls to preview image and full image. In the app I show a bottom sheet with horizontal list of the wallpapers. Selecting one sets it as the wallpaper of the app main screen. The selected wallpaper should be downloaded so it can work offline. The use case does the following things now • Store the selected wallpaper in data store (there is a repository for that). • Download the wallpaper pictures and store them on disk (this is another repository) The use case is launched in app scope so the bottom sheet can be dismissed while the wallpaper is being set without canceling the work. This is a bit simplied.than the actual needs but should be enough for the purpose of this discussion. Now it is possible that I open the bottom sheet again, select different wallpaper, and the work of the two could overlap.
f
Having dinner soon, but ill happily take a look later!
d
I think I see your problem now. You seem to have two Domain Objects you want to save;
Wallpaper
and
SelectedWallpaper
. It looks to me like you just have an order of operations issue. Is there any issue with the
UseCase
doing things in this order? 1. Download the
Wallpaper
data, if that succeeds then 2. Store the data to disk via the
WallpaperRepository
, if that succeeds then 3. Store the
SelectedWallpaper
data in the
SelectedWallPaperRepository
, if that succeeds then 4. Return
SelectWallpaperUserCase.Result.Success
If any one of these steps fail then you can (if needed) revert any of the data caching and return
SelectWallpaperUserCase.Result.Failure(cause)
How does that look?
Also if the user is dismissing the bottom sheet before the operations are a success then you have to decided what the dismissal means. Is it ok to fail silently? Or is it ok to not dismiss the bottom sheet until the operation completes? Is a dismissal the cancelation of the operations? How much of the operations lifecycle are you communicating to the user?
I hope some of that helps a bit.
m
Well yes, that's more or less how it is now. Actually I already have the
Wallpaper
object, it is passed to the use case, first the use case downloads the pictures and stores them locally, then stores the
Wallpaper
as a selected one with the local paths. Actually the problem here might be that the picture is always saved under the same file name, this makes it possible that when 2 usecases are run at the same time one can overwrite the other files...
All your points about cancellation and dismissal are valid points, for simplicity for this case I wanted to assume that dismissing the bottom sheet doesn't cancel, the operation. It is not the first time I faced and issue where usecase that is trying to do some more things than just call the repository is missing a place to store the state of the operation.
Actually I considered usecases something that shouldn't be driven by the UI. They should be safe to call at any time, but they should return failure when the operation cannot be done. In this case I am even ok to fail silently. But seems that usecase is missing something that will say: "hey, please be aware that there is already another 'set wallpaper' operation going on, perhaps you should wait for the previous one to finish or cancel it before you start"
d
It seems fin to me that the first UseCase can finish as long as the second one overwrites the first ones result. In order to be stateless the UseCases would have to be executed sequentially.
Otherwise you need some kind of state within the UseCase.
My gut is telling me that stateless UseCases are not entirely pragmatic. Mainly idealistic.
The simplest solution should probably be your first attempt. Improvements can be identified and implemented later.
Personally, I would give this particular UseCase state and inject a single instance of it into the ViewModel.
I have 1 more idea.
If you got timestamps from when the request to select a Wallpaper was made you could sync on the times stamps.
Don’t overwrite data with data that is older kinda thing.
m
Interesting approach with the timestamps. Didn't think about it. Although potentially suboptimal leaving the previous work going on knowing it will be discarded anyway. Having you here I could actually learn from your wisdom. Do you consider use cases with more than one method a code smell? As I said the use case I presented is a little bit simplified. Actually there must always be a way to go back to the wallpaper user had selected before. The bottom sheet shows the previous wallpaper at the first position. I introduced a notion of wallpaper slots (say primary and secondary slot) and I have two separate use cases for committing or discarding the pending wallpaper. Actually selecting the wallpaper from the bottom sheet makes the work I described earlier and sets the wallpaper as "pending". Only dismissing the bottom sheet calls the commit use case that swap the wallpaper slots and clears the secondary slot afterwards. This is where I initially had the described problem, but if I would make the use case statefull and include the other methods in the same usecase I would be saved ;)