I'm not so familiar with MVP, but can a presenter ...
# android-architecture
l
I'm not so familiar with MVP, but can a presenter has other presenter as dependency? My concerns are related to testing
s
presenter should contain the business logic separated from (but tied to) your activity/fragment. so presenter inside the presenter doesn’t really make sense. if you want to share logic between presenters, extract those methods/functionalities to another class
l
Alright. Thanks for the info
a
Generally speaking if you feel the need to add other presenters as dependencies then you probably should be extracting logic into repositories. Dependencies on the same level shouldn’t depend on each other generally speaking 🙂
https://developer.android.com/jetpack/guide#overview Have a look at the overview diagram. if you replace viewmodels with presenters, I would normally structure my app based on that archiecture
d
The truth is yes it can but it shouldn't. Could you please provide a scenario?
l
Generally speaking if you feel the need to add other presenters as dependencies then you probably should be extracting logic into repositories
This is a good point @Abhishek Dewan @dewildte I'm using jetpack compose but I will use the terms activity and fragment. I have an activity which acts like a container, holding multiple fragments. The activity shows the views that will persist in every fragment, e.g. bottom navigation bar and top app bar. For each fragment (every fragment has its own presenter): when entering the fragment I call a content-specific operation (fetch data from repo). The flow goes like: presenter->use case->repo->data source. But every operation has a precondition, e.g. "bluetooth device has to be connected". Problems begin if I handle the connection as separate use case. I have 3 options: 1. hide connection logic in shared presenter 2. hide connection logic in shared use case 3. Don't handle connection logic as use case. Handle it in data access class. This is what Abhishek is suggesting. My current favorite is 3. It took a long time to accept that connection logic don't have to be a separate use case. In the end it's a question how granular you wanna be with use cases. What do you think?
a
if I understand correctly what you mentioned then yes I would create something like a ConnectionRepository that talks to the Bluetooth api's and even network api's and it's sole purpose it to track and expose the current connection status. this way you can then inject a connection repo along with any other repos you need into your use case and you can define use cases which take into account the current network status. This way your not really creating a new use case but relying on data provided by the layer below use cases to define your use cases :)
l
Thanks for your input. So you mean something like this?:
Copy code
// use case

fun fetchFromRepo() {
  if (repo.connect()) repo.fetchData()
  
}

// repo

fun connect(): Boolean {
  dataSource.connect() // dataSource is responsible for connecting and also tracking the status. connect() returns always true. If device is not connected, it connects
}
a
Yes 🙂 something along those lines. I personally would do something like this
Copy code
class ConnectionRepo {
  val connectionState : Boolean //tracked and available to use

  //internal tracking mechanism that does whatever you need it to
  // in your case always make sure to be connected
}

class MyUseCase(connectionRepo: ConnectionRepo, otherRepo: Repo) {
  fun fetchFromRepo() {
      if (connectionRepo.connectionState) {
        otherRepo.fetchData()
      }
  }
}
The benefits of doing this are: 1. ConnectionRepo is not available to use in other use cases. 2. You can unit test your code with ease 🙂 This is just a quick blueprint 🙂 that can be molded however you see fit for your codebase 🙂
This is a similar pattern that I use in my app. I just don’t add a use case layer 🙂
l
There is one problem with your approach. What should happen in use case when
connectionState
returns false. That's the reason why I wrote
if (repo.connect()) repo.fetchData()
. There shouldn't be the possibility that the device is not connected. Btw this brings me to another better idea. When a fetch operation is performed, wouldn't it be better to not bother with the connection state in the use case and instead connect silently in a place where all bt operations come together, e.g. in
write
:
Copy code
// dataSource
fun write(bytes: ByteArray) {
  if (!isConnected) connect()
  socket.write(bytes)
}
But this has also drawbacks. It's not so easy.
a
Generally speaking there may be cases where you can’t connect to the network (kind of the nature of mobile stuff) but in your particular use case what your suggesting might work. (I wouldn’t know since I don’t have the context)
l
Yeah you should know that I'm only speaking about bluetooth classic. There is another problem. I'm using coroutines. Connecting to bt socket and listening to bt socket is done in separate coroutines whereas the latter one is a blocking call, that says the coroutine will never return. So I can't use the viewmodel/presenter scope but instead have to use a separate scope which I have to handle on my own. Now you see why I don't get this right.
If you have any ideas what might turn me in the right direction, let me know. I appreciate your help :)
a
For the blocking coroutine you can try something like
Copy code
viewModelScope.launch{
   withContext(IO) {
     runBlockingCoroutine
  }
    anotherCoroutine.
}
The withContext will allow your viewModelScope to not get blocked
l
ok this works of course but then I have to pass the scope from viewmodel all the way down to dataSource. So viewmodel->use-case->repo->dataSource. Is this legit?
a
Depending on your use case yea it could be a legit way to do it
l
ok cool this would solve the second problem. Have you ever seen that a scope is passed such a long way? It's not passing the scope per se which makes me feel uncomfortable but rather the long path it is passed. Have you any information about this?
a
In the previous message you mentioned socket not scope ? Did you mean pass the scope from viewModel to datasource if so then not that probabaly isn’t the right way. The right way would be to inject the coroutine scope into the data source and use it there
l
fuck sorry. it's late here. I correct it
a
No worries, in general the right way would be to inject the scope/coroutine context into your dataSource and ideally your function inside of your data source will be suspending so you can call the blocking coroutine with the withContext 🙂
l
I'm currently not using any di framework but is it common to inject
CoroutineScope
? Currently with no di I can only pass the scope the way down or did I miss something?
a
With no DI you will have to pass it in wherever you instantiate the use case. Normally yes injecting the scope is the right way since it allows you to pass in a testScope or mocked scope while unit testing 🙂
l
Ok one last question here. What do you mean with whereever I instantiate the use case. I'm confused because we were talking about viewmodel scope. Btw I'm not using jetpack viewmodel but rather a custom presenter class. I have a
BasePresenter
class where I create the
CoroutineScope
. I might be blind but do I not have to pass the presenter/viewmodel scope to the use case here or is there a shortcut?
So like this:
Copy code
presenterScope.launch{
   useCase.fetchFromRepo(presenterScope)
}
The scope must be bound to the presenter so that's why I have asked if it's common to inject a scope. An injected scope wouldn't be bound to the presenter
a
So to answer your two questions: 1. You will pass it into the use case wherever you create your use case. For e.g lets say you create a use case like
val useCase = UseCase()
in your presenter then you will need to pass the scope in when doing that as a constructor param to the UseCase. 2. Secondly you don’t need to pass the presenterScope in there, you can always create a new scope like you did with the presenterscope and reserve that for the blocking task and pass that in 🙂. Since you have a reference to it in the presenter it should be easy to tie it to the lifecycle of the presenter also.
Copy code
val blockedScope = CoroutineContext(<http://Dispatchers.IO|Dispatchers.IO>)

presenterScope.launch{
   val useCase = UseCase(blockedScope)
   useCase.fetchFromRepo()
}

class UseCase(scope: CoroutineContext) {
   suspend fun fetchFromRepo() {
      withContext(scope) { blockingcall }
   }
}
Something along those lines.
You can always chose to pass the scope as a function param like you showed
l
Ah ok got you. Damn I have to sleep. Thanks for your patience 🙏
a
Happy I could help :)
d
What is wrong with passing a function into the usecase constructor that tells you if things are connected? For example:
class MyUseCase( val isConnected: () -> Boolean, ...) { ... }
The repository pattern is meant to be an abstraction over the storage and retrieval of objects distributed across memory. It is supposed to make a bunch of things that may be stored either in a local database or outside in the network look like they are in memory (like a List). So you do not need to set up a “repository” for a simple Boolean function.
() -> Boolean
is good enough.
And when unit testing the
UseCase
object you do not need some giant piece of infrastructure to wire it up.
a
Assuming if it's the only thing you'll ever need then nothing wrong with your suggested approach 🙂. As you put you don't need all of the boiler plate code for a simple Boolean.
d
Use suspend fun in presenter and the adapter/gateway that manages the Bluetooth sending/connection and call it with the activity scope, if you really need to do something async, you can always use coroutineScope {} inside a suspend fun. You only need to pass another scope in when you are managing something in a different lifecycle than your activity or viewmodel.