Skip to content

Commit

Permalink
Featre/shared docs and cleanup (#57)
Browse files Browse the repository at this point in the history
* Shared repositories refactor + docs

 - completed explanation on readme architecture docs
 - bump versions to reflect latest dev (we need to have something automated for this)
 - TODOs refactors (inits)

* - updated readme and architecture diagram

* - cleanup todos + documentation

* - pods versions upgrades

* - added index and commets on presenters implementation
  • Loading branch information
rodvar authored Nov 18, 2024
1 parent d584cff commit 6a534ab
Show file tree
Hide file tree
Showing 18 changed files with 559 additions and 57 deletions.
66 changes: 59 additions & 7 deletions bisqapps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,35 @@

# Bisq Mobile

## Index

1. [Bisq Mobile](#bisq-mobile)
- [Goal](#goal)
- [How to contribute](#how-to-contribute)
- [Project dev requirements](#project-dev-requirements)
- [Getting started](#getting-started)
- [Getting started for Android Node](#getting-started-for-android-node)
- [UI](#ui)
- [Designs](#designs)
- [Navigation Implementation](#navigation-implementation)
- [Configuring dev env: known issues](#configuring-dev-env-known-issues)

2. [Initial Project Structure](#initial-project-structure)

3. [App Architecture Design Choice](#app-architecture-design-choice)
- [Dumb Views](#dumb-views)
- [UI independently built](#ui-independently-built)
- [Encourage Rich Domain well-test models](#encourage-rich-domain-well-test-models)
- [Presenters guide the orchestra](#presenters-guide-the-orchestra)
- [Repositories key for reactive UI](#repositories-key-for-reactive-ui)
- [Services allow us to have different networking sources](#services-allow-us-to-have-different-networking-sources)

4. [What about Lifecycle and main view components](#what-about-lifecycle-and-main-view-components)

5. [When it’s acceptable to reuse a presenter for my view](#when-its-acceptable-to-reuse-a-presenter-for-my-view)

6. [Why KMP](#why-kmp)

## Goal

This project aims to make Bisq Network accesible in Mobile Platforms following the philosofy of [Bisq2](https://github.com/bisq-network/bisq2/contribute) - to make it
Expand Down Expand Up @@ -81,17 +110,40 @@ Though this can evolve, this is the initial structure of this KMP project:

## App Architecture Design Choice

**note** this is being Worked out at the moment

![Apps Design Architecture](docs/bisqapps_design_arch.png)

This project uses the [MVP](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter) (Model-View-Presenter) Design Pattern with small variations (introducing Repositories) in the following way:
This project uses the [MVP](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter) (Model-View-Presenter) Design Pattern with small variations (__introducing Repositories & we allow reusal of presenters under specific conditions__) in the following way:

- **Dumb Views**: Each View will define it's desired presenter behaviour. For example, for the `AppView` it would define the `AppPresenter` interface. This includes which data its interested in observing and the commands it needs to trigger from user interactions.
- **UI indepdently built**The view will react to changes in the presenter observed data, and call the methods it needs to inform the presenter about user actions. In this way __each view can be created idependently without strictly needing anything else__
- **Encourage Rich Domain well-test models** Same goes for the Models, they can be built (and unit tested) without needing anything else, simple POKOs (Plain Old Kotlin Objects - meaning no external deps). Ideally business logic should go here and the result of executing a business model logic should be put back into the repository for all observers to know.
- **Presenters guide the orchestra** When you want to bring interaction to life, create a presenter (or reuse one if the view is small enough) and implement the interface you defined when doing the view (`AppPresenter` interface). That presenter will generally modify/observe the models through a repository.
- **Repositories key for reactive UI** Now for the presenter to connect to the domain models we use repositories which is basically a storage of data (that abstracts where that data is stored in). The repositories also expose the data in an observable way, so the presenter can satisfy the requested data from the view from the data of the domain model in the ways it see fit. Sometimes it would just be a pathrough. The resposities could also have caching strategy, and persistance. For most of the use cases so far we don't see a strong need for persistance in most of them (with the exception of settings-related repositories) - more on this soon
- **Services allow us to have different networking sources** we are developing 3 apps divided in 2 groups: `node` and `client`. Each group has a very distinct networking setup. We need each type of app build to have only the networking it needs. The proposed separation of concerns not only allows a clean architecture but also allows faster development focus on each complexity separately.


### What about Lifecycle and main view components

As per original specs `single-activity` pattern (or `single-viewcontroller` in iOS) is sufficient for this project. Which means, unless we find a specific use case for it, we'll stick to a single Activity/ViewController for the whole lifecycle of the app.

The app's architecture `BasePresenter` allows a tree like behaviour where a presenter can be a root with dependent child presenters.

We leverage this by having:

- A `MainPresenter` that acts as root in each and all of the apps
- The rest of the presenters require the main presenter as construction parameter to be notified about lifecycle events.


### When its acceptable to reuse a presenter for my view?

It's ok to reuse an existing presenter for your view if:

- Your view is a very small part of a bigger view that renders together (commonly called `Screen`) and you can't foresee reusal for it
- Your view is a very small part of a bigger view and even if its reused the presenter required implementation is minimal

- Each View will define (or reuse) what's the presenter logic it is looking for, including which data its interested in observing in an interface. The view will react to changes in the presenter observed data, and call the methods it needs to inform the presenter about user actions. In this way **each view can be created idependently without strictly needing anything else**
- Same goes for the Models, they can be built (and unit tested) without needing anything else, simple KOJOs.
- When you want to bring interaction to life, create a presenter (or reuse one) and implement the interface you defined when doing the view. That presenter will generally modify/fetch the models through a repository.
- Networking is a crucial part of this project and the networking used and shared by the `xClients` are not the ones used by the `androidNode` but the idea is to have a comprehensive **facade** so that from the point of view of repository/service it just works regardless on how and what objs are being used under the hood to fetch/save data. More on this soon...
To reuse an existing presenter you would have to make it extend your view defined presenter interface and do the right `Koin bind` on its Koin repository definition.

Then you can inject it in the `@Composable` function using `koinInject()`.

## Why KMP

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import network.bisq.mobile.domain.data.repository.SingleObjectRepository
import network.bisq.mobile.presentation.MainPresenter
import network.bisq.mobile.presentation.ui.App
import org.koin.android.ext.android.inject
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
package network.bisq.mobile.android.node

import android.app.Application
import android.os.Process
import bisq.common.facades.FacadeProvider
import bisq.common.facades.android.AndroidGuavaFacade
import bisq.common.facades.android.AndroidJdkFacade
import bisq.common.network.AndroidEmulatorLocalhostFacade
import network.bisq.mobile.android.node.di.androidNodeModule
import network.bisq.mobile.domain.di.domainModule
import network.bisq.mobile.presentation.di.presentationModule
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import java.security.Security

class MainApplication : Application() {
override fun onCreate() {
super.onCreate()

setupKoinDI()
setupBisqCoreStatics()
}

private fun setupBisqCoreStatics() {
FacadeProvider.setLocalhostFacade(AndroidEmulatorLocalhostFacade())
FacadeProvider.setJdkFacade(AndroidJdkFacade(Process.myPid()))
FacadeProvider.setGuavaFacade(AndroidGuavaFacade())

// Androids default BC version does not support all algorithms we need, thus we remove
// it and add our BC provider
Security.removeProvider("BC")
Security.addProvider(BouncyCastleProvider())
}

private fun setupKoinDI() {
startKoin {
androidContext(this@MainApplication)
// order is important, last one is picked for each interface/class key
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package network.bisq.mobile.android.node.di

import network.bisq.mobile.android.node.AndroidNodeGreeting
import bisq.security.SecurityService
import bisq.user.identity.UserIdentityService
import network.bisq.mobile.android.node.domain.data.repository.NodeGreetingRepository
import network.bisq.mobile.android.node.presentation.MainNodePresenter
import network.bisq.mobile.domain.data.repository.SingleObjectRepository
import network.bisq.mobile.android.node.service.AndroidApplicationService
import network.bisq.mobile.android.node.service.AndroidMemoryReportService
import network.bisq.mobile.presentation.MainPresenter
import network.bisq.mobile.presentation.ui.AppPresenter
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.bind
import org.koin.dsl.module

Expand All @@ -15,4 +18,24 @@ val androidNodeModule = module {
// this line showcases both, the posibility to change behaviour of the app by changing one definiton
// and binding the same obj to 2 different abstractions
single<MainPresenter> { MainNodePresenter(get()) } bind AppPresenter::class

// Services
// TODO might not work because of the jars dependencies, needs more work
// single <AndroidMemoryReportService> {
// val context = androidContext()
// AndroidMemoryReportService(context)
// }
// single <AndroidApplicationService> {
// val filesDirsPath = androidContext().filesDir.toPath()
// val androidMemoryService: AndroidMemoryReportService = get()
// AndroidApplicationService(androidMemoryService, filesDirsPath)
// }
// single <UserIdentityService> {
// val applicationService: AndroidApplicationService = get()
// applicationService.userService.userIdentityService
// }
// single <SecurityService> {
// val applicationService: AndroidApplicationService = get()
// applicationService.securityService
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package network.bisq.mobile.android.node.presentation

import android.app.Activity
import android.os.Build
import android.os.Process
import bisq.application.State
import bisq.bonded_roles.market_price.MarketPrice
import bisq.chat.ChatChannelDomain
Expand All @@ -11,11 +10,7 @@ import bisq.chat.common.CommonPublicChatMessage
import bisq.chat.two_party.TwoPartyPrivateChatChannel
import bisq.chat.two_party.TwoPartyPrivateChatMessage
import bisq.common.currency.MarketRepository
import bisq.common.facades.FacadeProvider
import bisq.common.facades.android.AndroidGuavaFacade
import bisq.common.facades.android.AndroidJdkFacade
import bisq.common.locale.LanguageRepository
import bisq.common.network.AndroidEmulatorLocalhostFacade
import bisq.common.network.TransportType
import bisq.common.observable.Observable
import bisq.common.observable.Pin
Expand All @@ -40,8 +35,6 @@ import network.bisq.mobile.android.node.service.AndroidMemoryReportService
import network.bisq.mobile.domain.data.model.Greeting
import network.bisq.mobile.domain.data.repository.GreetingRepository
import network.bisq.mobile.presentation.MainPresenter
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.Security
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
Expand All @@ -51,10 +44,12 @@ import kotlin.math.max
import kotlin.random.Random

@Suppress("UNCHECKED_CAST")
class MainNodePresenter(greetingRepository: NodeGreetingRepository): MainPresenter(greetingRepository as GreetingRepository<Greeting>) {
class MainNodePresenter(greetingRepository: NodeGreetingRepository) :
MainPresenter(greetingRepository as GreetingRepository<Greeting>) {
companion object {
private const val AVATAR_VERSION = 0
}

private val logMessage: Observable<String> = Observable("")
val state = Observable(State.INITIALIZE_APP)
private val shutDownErrorMessage = Observable<String>()
Expand All @@ -71,21 +66,11 @@ class MainNodePresenter(greetingRepository: NodeGreetingRepository): MainPresent


init {
// TODO move to application once DI setup gets merged
FacadeProvider.setLocalhostFacade(AndroidEmulatorLocalhostFacade())
FacadeProvider.setJdkFacade(AndroidJdkFacade(Process.myPid()))
FacadeProvider.setGuavaFacade(AndroidGuavaFacade())

// Androids default BC version does not support all algorithms we need, thus we remove
// it and add our BC provider
Security.removeProvider("BC")
Security.addProvider(BouncyCastleProvider())
log("Static Bisq core setup ready")

CoroutineScope(Dispatchers.IO).launch {
greetingRepository.create(AndroidNodeGreeting())
}
}

override fun onViewAttached() {
super.onViewAttached()
logMessage.addObserver {
Expand Down Expand Up @@ -124,7 +109,6 @@ class MainNodePresenter(greetingRepository: NodeGreetingRepository): MainPresent
printDefaultKeyId()
printLanguageCode()

// At the moment is nor persisting the profile so it will create one on each run
if (userIdentityService.userIdentities.isEmpty()) {
//createUserIfNoneExist();
initializeUserService()
Expand All @@ -133,17 +117,22 @@ class MainNodePresenter(greetingRepository: NodeGreetingRepository): MainPresent
createUserProfile("Android user " + Random(4234234).nextInt(100)).join()
log("Created profile for user")
}

// User profile data
printUserProfiles()

// network metadata
observeNetworkState()
observeNumConnections()
fetchMarketPrice(500L)

// trading
observePrivateMessages()

// Less priority for MVP
publishRandomChatMessage();
observeChatMessages(5)
maybeRemoveMyOldChatMessages()
//
sendRandomMessagesEvery(60L * 100L)
}
}
Expand Down
Loading

0 comments on commit 6a534ab

Please sign in to comment.