You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 5, 2024. It is now read-only.
@@ -18,21 +18,21 @@ The Data Layer is responsible for dealing with the outside world and mapping it
18
18
19
19
### Data source (optional)
20
20
21
-
Wraps an IO operation (e.g. a Ktor http call) and ensures that it won't throw exceptions by making it a total function (i.e. wraps with `try-catch` and returns `Either<ErrorDto, DataDto>` of some raw data model).
21
+
Wraps an IO operation (e.g., a Ktor http call) and ensures that it won't throw exceptions by making it a total function (i.e. wraps with `try-catch` and returns `Either<ErrorDto, DataDto>` of some raw data model).
22
22
23
23
> A data source isn't always needed if it'll do nothing useful. For example, there's no point wrapping Room DB DAOs.
24
24
25
25
### Domain Mapper classes (optional)
26
26
27
-
A classes responsible for transforming and validating raw models (e.g. DTOs, entities) to domain ones. These validations can fail so mappers usually return `Either<Error, DomainModel>`.
27
+
A class responsible for transforming and validating raw models (e.g., DTOs, entities) to domain ones. These validations can fail, so mappers usually return `Either<Error, DomainModel>`.
28
28
29
29
### Repository
30
30
31
-
Combines one or many data sources to implement [CRUD operations](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) and provide validated domain data. Repository functions must be **main-safe** (not blocking the main UI thread) or simply said they must move work on a background thread (e.g. `withContext(Disparchers.IO)`)
31
+
Combines one or many data sources to implement [CRUD operations](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) and provide validated domain data. Repository functions must be **main-safe** (not blocking the main UI thread), or simply said, they must move work on a background thread (e.g.,`withContext(Disparchers.IO)`)
32
32
33
33
## Domain Layer (optional)
34
34
35
-
Optional architecture layer for more complex domain logic that combines one or many repositories with business logic and rules (e.g. calculating the balance in Ivy Wallet).
35
+
Optional architecture layer for more complex domain logic that combines one or many repositories with business logic and rules (e.g., calculating the balance in Ivy Wallet).
36
36
37
37
### UseCases
38
38
@@ -44,16 +44,16 @@ The user of the app sees and interacts only with the UI layer. The UI layer cons
44
44
45
45
### ViewModel
46
46
47
-
The ViewModel combines the data from uses-cases and repositories and transforms it into view-state representation that's formatted and ready to display in your Compose UI. It also handles user interactions and translates them into data/domain layer calls.
47
+
The ViewModel combines the data from use cases and repositories and transforms it into view-state representation that's formatted and ready to display in your Compose UI. It also handles user interactions and translates them into data/domain layer calls.
48
48
49
49
> Simply said, the viewmodel is translator between the UI (user) and the domain. It's like an adapter - adapts domain models to view-state and adapts user interactions into domain calls.
50
50
51
51
### ViewState Mapper classes (optional)
52
52
53
-
In more complex cases, it becomes impractical to put all domain -> view-state mapping in the ViewModel. Also, it's common multiple viewmodels to map the same domain model to the same view-state. In that case, it's good to extract the view-state mapping logic in a separate class that we call a `SomethingViewStateMapper`.
53
+
In more complex cases, it becomes impractical to put all domain -> view-state mapping in the ViewModel. Also, it's common for multiple viewmodels to map the same domain model to the same view-state. In that case, it's good to extract the view-state mapping logic in a separate class that we call a `SomethingViewStateMapper`.
54
54
55
55
### Composables
56
56
57
57
Composables are the screens and UI components that the user sees and interacts with. They should be dumb as fck. Their responsibility and logic should be limited to:
58
-
-displaying the already formatted view-state provided by the VM
59
-
-send UI interactions to the VM in the form of events
58
+
-Displaying the already formatted view-state provided by the VM.
59
+
-Sending UI interactions to the VM in the form of events.
Copy file name to clipboardExpand all lines: docs/guidelines/Data-Modeling.md
+9-10
Original file line number
Diff line number
Diff line change
@@ -23,9 +23,8 @@ The problem with this approach is that our code will have to deal with many impo
23
23
- What to show if `loading = false`, `content = null`, `error = null`?
24
24
- What to do if we have both `loading = true` and `error != null`?
25
25
26
-
There are so many ways things to go wrong - for example, a common one is forgetting to reset `loading` back to `false`.
27
-
A better way to model this would be to use [Algebraic Data types (ADTs)](https://wiki.haskell.org/Algebraic_data_type)
28
-
or simply said in Kotlin: `data classes`, `sealed interfaces`, and combinations of both.
26
+
There are so many ways things can go wrong - for example, a common one is forgetting to reset `loading` back to `false`.
27
+
A better way to model this would be to use [Algebraic Data types (ADTs)](https://wiki.haskell.org/Algebraic_data_type) or, simply said in Kotlin: `data classes`, `sealed interfaces`, and combinations of both.
With the ADTs representation, we eliminate all impossible cases. We also do eliminate that on compile-time, meaning that whatever shit we do - the compiler will never allow the code to run.
37
+
With the ADTs representation, we eliminate all impossible cases. We also eliminate that at compile-time, meaning that whatever shit we do - the compiler will never allow the code to run.
39
38
40
39
**Takeaway:** Model your data using `data classes`, and `sealed interfaces` (and combinations of them) in a way that:
41
40
@@ -60,16 +59,16 @@ data class Order(
60
59
)
61
60
```
62
61
63
-
I'm making this up but the goal is to demonstrate common mistakes and how to fix them.
64
-
Do you spot them?
62
+
I'm making this up, but the goal is to demonstrate common mistakes and how to fix them.
63
+
Do you spot them?
65
64
66
65
Let's think and analyze:
67
66
68
67
1. What if someone orders a `count = 0` or even worse a `count = -1`?
69
68
2. Imagine a function `placeOrder(orderId: UUID, userId: UUID, itemId: UUID, ...)`. How likely is someone to pass a wrong `UUID` and mess UUIDs up?
70
69
3. The `trackingId` seems to be required and important but what if someone passes `trackingId = ""` or `trackingId = "XYZ "`?
71
70
72
-
I can go on but you see the point. So let's how we can fix it.
71
+
I can go on, but you see the point. So let's discuss how we can fix it.
73
72
74
73
```kotlin
75
74
data classOrder(
@@ -109,7 +108,7 @@ PositiveInt.from(-5)
109
108
// Either.Left("-5 is not > 0")
110
109
```
111
110
112
-
The revised data model takes more code but it gives you one important property:
111
+
The revised data model takes more code, but it gives you one important property:
113
112
114
113
> If any of your functions accepts an instance of `order: Order`, you immediately know that it's a valid order and no validation logic is required.
115
114
@@ -118,9 +117,9 @@ This is **validation by construction** and it eliminates undesirable cases asap
118
117
- Order `count` of zero, negative, or infinity by explicitly requiring a `PositiveInt` (unfortunately, that happens at runtime because the compiler can't know if a given integer is positive or not by just looking at the code).
119
118
- The `UUID`s now can't be messed up because the compiler will give you an error, if for example you try to pass `UserId` to a function accepting `OrderId`.
120
119
- The `time` is now always in UTC by using `Instant`.
121
-
- The `trackignId` is trimmed and can't be blank.
120
+
- The `trackingId` is trimmed and can't be blank.
122
121
123
122
To learn more about Exact types you can check [the Arrow Exact GitHub repo](https://github.com/arrow-kt/arrow-exact). The benefit of explicit data models is correctness and reduced complexity of your core logic.
124
123
125
124
> Not all types should be exact. For example, we make an exception for DTOs and entities where working with primitives is easier.
126
-
> However, we still use ADTs and everything in the domain layer where the business logic is must be exact and explicit.
125
+
> However, we still use ADTs and everything in the domain layer where the business logic must be exact and explicit.
Copy file name to clipboardExpand all lines: docs/guidelines/Error-Handling.md
+9-10
Original file line number
Diff line number
Diff line change
@@ -1,11 +1,10 @@
1
1
# Error Handling
2
2
3
3
It's common for operations to fail and we should expect that.
4
-
In Ivy Wallet we **do not throw exceptions** but rather make functions that
5
-
can fail return [Either<Error, Data>](https://arrow-kt.io/learn/typed-errors/working-with-typed-errors/).
4
+
In Ivy Wallet we **do not throw exceptions** but rather make functions that can fail to return [Either<Error, Data>](https://arrow-kt.io/learn/typed-errors/working-with-typed-errors/).
6
5
7
6
Either is a generic data type that models two possible cases:
8
-
-`Either.Left` for the unhappy path (e.g. request failing, invalid input, no network connection)
7
+
-`Either.Left` for the unhappy path (e.g., request failing, invalid input, no network connection)
9
8
-`Either.Right` for the happy path
10
9
11
10
Simplified, `Either` is just:
@@ -24,7 +23,7 @@ fun <E,A,B> Either<E, A>.fold(
24
23
Either.Right-> mapRight(data)
25
24
}
26
25
27
-
// a bunch more extension functions and utils
26
+
// a bunch of more extension functions and utils
28
27
```
29
28
30
29
So in Ivy, operations that can fail (logically or for some other reason) we'll model using **Either**.
@@ -54,7 +53,7 @@ class CryptoInvestor @Inject constructor(
54
53
) {
55
54
suspendfunbuyIfCheap(): Either<String, PositiveDouble> = either {
56
55
val btcPrice = btcDataSource.fetchCurrentPriceUSD().bind()
57
-
// .bind() - if it fails returns Either.Left and short-circuits the function
56
+
// .bind() - if it fails, returns Either.Left and short-circuits the function
58
57
if(btcPrice.value >50_000) {
59
58
// short-circuits and returns Either.Left with the msg below
60
59
raise("BTC is expensive! Won't buy.")
@@ -76,7 +75,7 @@ class CryptoInvestor @Inject constructor(
76
75
77
76
Let's analyze, simplified:
78
77
-`either {}` puts us into a "special" scope where the last line returns `Either.Right` and also gives us access to some functions:
79
-
-`Operation.bind()`: if the operation failsterminates the `either {}` with operation's `Left` value, otherwise `.bind()` returns the operation's `Right` value
78
+
-`Operation.bind()`: if the operation fails, it terminates the `either {}` with operation's `Left` value; otherwise,`.bind()` returns the operation's `Right` value
80
79
-`raise(E)`: like **throw** but for `either {}` - terminates the function with `Left(E)`
81
80
-`Either.mapLeft {}`: transforms the `Left` (error type) of the `Either`. In the example, we do it so we can match the left type of the `either {}`
82
81
@@ -99,7 +98,7 @@ I strongly recommend allocating some time to also go through [Arrow's Working wi
99
98
100
99
- Either is a [monad](https://en.wikipedia.org/wiki/Monad_(functional_programming)).
101
100
-`Either<Throwable, T>` is equivalent to Kotlin's std `Result` type.
102
-
- Many projects create a custom `Result<E, T>` while they can just use `Either` with all of its built-in features.
101
+
- Many projects create a custom `Result<E, T>` while they can just use `Either` with all its built-in features.
103
102
104
-
> In some rare cases it's okay to `throw` a runtime exception. Those are the cases in which you're okay and want the app to crash
105
-
> (e.g. not enough disk space to write in Room DB / local storage).
103
+
> In some rare cases, it's okay to `throw` a runtime exception. These are the cases in which you're okay and want the app to crash
104
+
> (e.g., not enough disk space to write in Room DB / local storage).
Copy file name to clipboardExpand all lines: docs/guidelines/Screen-Architecture.md
+8-9
Original file line number
Diff line number
Diff line change
@@ -1,7 +1,6 @@
1
1
# Screen Architecture
2
2
3
-
Ivy Wallet uses an [Unidirectional Data Flow (UDF)](https://developer.android.com/topic/architecture#unidirectional-data-flow),
4
-
MVI architecture pattern with the Compose runtime for reactive state management in the view-model.
3
+
Ivy Wallet uses a [Unidirectional Data Flow (UDF)](https://developer.android.com/topic/architecture#unidirectional-data-flow) and MVI architecture pattern with the Compose runtime for reactive state management in the view-model.
5
4
It key characteristics are:
6
5
7
6

@@ -15,22 +14,22 @@ Repeat ♻️
15
14
16
15
## ViewModel
17
16
18
-
A class that adapts the domain model to view-state model that the Compose UI can directly display. It combines data from one or many repositories/use-cases and transforms it into a view-state representation consisting of primitives and `@Immutable` structures that composables can draw efficiently.
17
+
A class that adapts the domain model to a view-state model that the Compose UI can directly display. It combines data from one or many repositories/use-cases and transforms it into a view-state representation consisting of primitives and `@Immutable` structures that composables can draw efficiently.
19
18
20
-
Let's address the elephant in the room, why Compose in the ViewModel? The short answer, because it's way more convenient and equally efficient compared to using Flow/LiveData.
19
+
Let's address the elephant in the room - why use Compose in the ViewModel? The short answer: because it's way more convenient and equally efficient compared to using Flow/LiveData.
21
20
22
21
### FAQ
23
22
24
23
**Q: Isn't it an anti-pattern to have Compose and Android/UI logic in the view-model?**
25
24
26
-
A: Firstly, Compose is more modular than it looks on the surface. `compose.runtime` is very different from the `compose.ui`. In our architecture we use only the Compose runtime as a reactive state management library. The compose runtime state is equivalent to Kotlin Flow but with simpler, more elegant and powerful API for the purposes of a view-model.
25
+
A: Firstly, Compose is more modular than it looks on the surface. `compose.runtime` is very different from the `compose.ui`. In our architecture we use only the Compose runtime as a reactive state management library. The compose runtime state is equivalent to Kotlin Flow but with a simpler, more elegant and powerful API for the purposes of a view-model.
27
26
28
27
**Q: Don't we couple our view-models with Compose by doing this?**
29
28
30
29
A: In theory, we couple our ViewModel only with the Compose runtime and its compiler. However, that doesn't matter because:
31
30
32
31
1. Let's admit it, you'll likely won't change Compose as your UI toolkit anytime soon.
33
-
2. If you change Compose, rewriting the UI compsables and components will cost you much more than migrating your view-models because viewmodels if done correctly are very simple adapters of your data/domain layer.
32
+
2. If you do change Compose, rewriting the UI composables and components will cost you much more than migrating your view-models, because viewmodels, if done correctly, are very simple adapters of your data/domain layer.
34
33
35
34
**Q: Can we use Kotlin Flow APIs in a compose viewmodel?**
36
35
@@ -46,7 +45,7 @@ fun getBtcPrice(): String? {
46
45
47
46
**Q: What's the benefit of having Compose in the VM?**
48
47
49
-
A: The main benefit is convenience. With the Compose runtime you don't have to do complex Flows like `combine` (limited to 5 flows only), `flapMapLatest` vs `flatMapCombine` and write all the boilerplate code required. Another benefit is that you also have access to entire Compose runtime API like `remember` (easy memorization), `LaunchedEffect` (execute side-effects under certain conditions) and ofc simple, concise and very readable syntax.
48
+
A: The main benefit is convenience. With the Compose runtime you don't have to do complex Flows like `combine` (limited to 5 flows only), `flapMapLatest` vs `flatMapCombine` and write all the boilerplate code required. Another benefit is that you also have access to the entire Compose runtime API like `remember` (easy memorization), `LaunchedEffect` (execute side-effects under certain conditions), and, ofc, simple, concise, and very readable syntax.
50
49
51
50
All of the above is better seen in code and practice - make sure to check our references to learn more.
52
51
@@ -58,11 +57,11 @@ The view-state is a data model that contains all information that the screen/com
58
57
59
58
## View-event
60
59
61
-
Our users need to be able to interact with the app and its Compose UI. Those interactions include typing input, clicking buttons, gestures and more. The Compose UI captures those interactions and maps them into view-events that the view-model can easily handle and process.
60
+
Our users need to be able to interact with the app and its Compose UI. These interactions include typing input, clicking buttons, gestures, and more. The Compose UI captures these interactions and maps them into view-events that the view-model can easily handle and process.
62
61
63
62
## Composable UI
64
63
65
-
The Compose UI is responsible for rendering the view-state according to its design and allowing the user to interact with the UI. The Compsable UI listens for user interactions and maps to events that it sends to the VM
64
+
The Compose UI is responsible for rendering the view-state according to its design and allowing the user to interact with the UI. The Composable UI listens for user interactions and maps them to events that it sends to the VM.
0 commit comments