Skip to content
This repository was archived by the owner on Nov 5, 2024. It is now read-only.

Commit 1a0de49

Browse files
authored
Fix issue #3133 (#3269)
* Fix typos in docs/Guidelines.md * Fix Typos in docs/guidelines/Architecture.md * Fix typos in Architecture.md * Fix typos in Data-Modeling.md * Fix typos in Error-Handling.md * Fix typos in Screen-Architecture.md * Fix typos in Unit-Testing.md * Fix typo in Screen-Architecture.md * Fix typo in Unit-Testing.md * Fix typo in Architecture.md * Revert removal of .idea/.name * Remove mistakenly added .idea/checkstyle-idea.xml * Update .gitignore
1 parent fe7eacd commit 1a0de49

7 files changed

+59
-61
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ captures/
4848
.idea/dictionaries
4949
.idea/libraries
5050
.idea/deploymentTargetDropDown.xml
51+
.idea/checkstyle-idea.xml
5152
#misc.xml is annoying and useless
5253
.idea/misc.xml
5354
# Android Studio 3 in .gitignore file.

docs/Guidelines.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ The biggest challenge in software engineering is the fight against **complexity*
55
1. Your PR works and doesn't break anything.
66
2. Your PR is simple and doesn't add complexity.
77

8-
Software engineering is also about **thinking**. Don't just follow blindly best practices and strive to do the "right" thing. Instead, ask yourself:
8+
Software engineering is also about **thinking**. Don't just blindly follow best practices and strive to do the "right" thing. Instead, ask yourself:
99

1010
- Is this the simplest solution?
1111
- Am I over-engineering? What does this give me?

docs/guidelines/Architecture.md

+10-10
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ Ivy Wallet follows a less constrained version of [the official Android Developer
66

77
![data-mapping](../assets/data-mapping.svg)
88

9-
> "Programming is a game of information. We receive/send data on which we perform arbitrary transformations and logic." — Iliyan Germanov
9+
> "Programming is a game of information. We receive and send data, on which we perform arbitrary transformations and logic." — Iliyan Germanov
1010
11-
**Architecture:** _Data Layer → Domain Layer (optional) → UI layer_
11+
**Architecture:** _Data layer → Domain layer (optional) → UI layer_
1212

1313
![architecture](../assets/architecture.svg)
1414

@@ -18,21 +18,21 @@ The Data Layer is responsible for dealing with the outside world and mapping it
1818

1919
### Data source (optional)
2020

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).
2222

2323
> A data source isn't always needed if it'll do nothing useful. For example, there's no point wrapping Room DB DAOs.
2424
2525
### Domain Mapper classes (optional)
2626

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>`.
2828

2929
### Repository
3030

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)`)
3232

3333
## Domain Layer (optional)
3434

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).
3636

3737
### UseCases
3838

@@ -44,16 +44,16 @@ The user of the app sees and interacts only with the UI layer. The UI layer cons
4444

4545
### ViewModel
4646

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.
4848

4949
> 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.
5050
5151
### ViewState Mapper classes (optional)
5252

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`.
5454

5555
### Composables
5656

5757
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.

docs/guidelines/Data-Modeling.md

+9-10
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ The problem with this approach is that our code will have to deal with many impo
2323
- What to show if `loading = false`, `content = null`, `error = null`?
2424
- What to do if we have both `loading = true` and `error != null`?
2525

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.
2928

3029
```kotlin
3130
sealed interface ScreenUiState {
@@ -35,7 +34,7 @@ sealed interface ScreenUiState {
3534
}
3635
```
3736

38-
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.
3938

4039
**Takeaway:** Model your data using `data classes`, and `sealed interfaces` (and combinations of them) in a way that:
4140

@@ -60,16 +59,16 @@ data class Order(
6059
)
6160
```
6261

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?
6564

6665
Let's think and analyze:
6766

6867
1. What if someone orders a `count = 0` or even worse a `count = -1`?
6968
2. Imagine a function `placeOrder(orderId: UUID, userId: UUID, itemId: UUID, ...)`. How likely is someone to pass a wrong `UUID` and mess UUIDs up?
7069
3. The `trackingId` seems to be required and important but what if someone passes `trackingId = ""` or `trackingId = "XYZ "`?
7170

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.
7372

7473
```kotlin
7574
data class Order(
@@ -109,7 +108,7 @@ PositiveInt.from(-5)
109108
// Either.Left("-5 is not > 0")
110109
```
111110

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:
113112

114113
> 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.
115114
@@ -118,9 +117,9 @@ This is **validation by construction** and it eliminates undesirable cases asap
118117
- 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).
119118
- 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`.
120119
- 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.
122121

123122
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.
124123

125124
> 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.

docs/guidelines/Error-Handling.md

+9-10
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
# Error Handling
22

33
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/).
65

76
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)
98
- `Either.Right` for the happy path
109

1110
Simplified, `Either` is just:
@@ -24,7 +23,7 @@ fun <E,A,B> Either<E, A>.fold(
2423
Either.Right -> mapRight(data)
2524
}
2625

27-
// a bunch more extension functions and utils
26+
// a bunch of more extension functions and utils
2827
```
2928

3029
So in Ivy, operations that can fail (logically or for some other reason) we'll model using **Either**.
@@ -45,7 +44,7 @@ interface BtcDataSource {
4544
}
4645

4746
interface MyBank {
48-
suspend fun currentblBalanceUSD(): Either<Unit, PositiveDouble>
47+
suspend fun currentBalanceUSD(): Either<Unit, PositiveDouble>
4948
}
5049

5150
class CryptoInvestor @Inject constructor(
@@ -54,7 +53,7 @@ class CryptoInvestor @Inject constructor(
5453
) {
5554
suspend fun buyIfCheap(): Either<String, PositiveDouble> = either {
5655
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
5857
if(btcPrice.value > 50_000) {
5958
// short-circuits and returns Either.Left with the msg below
6059
raise("BTC is expensive! Won't buy.")
@@ -76,7 +75,7 @@ class CryptoInvestor @Inject constructor(
7675

7776
Let's analyze, simplified:
7877
- `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 fails terminates 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
8079
- `raise(E)`: like **throw** but for `either {}` - terminates the function with `Left(E)`
8180
- `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 {}`
8281

@@ -99,7 +98,7 @@ I strongly recommend allocating some time to also go through [Arrow's Working wi
9998

10099
- Either is a [monad](https://en.wikipedia.org/wiki/Monad_(functional_programming)).
101100
- `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.
103102

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).

docs/guidelines/Screen-Architecture.md

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Screen Architecture
22

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.
54
It key characteristics are:
65

76
![screen-architecture](../assets/screen-vm.svg)
@@ -15,22 +14,22 @@ Repeat ♻️
1514

1615
## ViewModel
1716

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.
1918

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.
2120

2221
### FAQ
2322

2423
**Q: Isn't it an anti-pattern to have Compose and Android/UI logic in the view-model?**
2524

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.
2726

2827
**Q: Don't we couple our view-models with Compose by doing this?**
2928

3029
A: In theory, we couple our ViewModel only with the Compose runtime and its compiler. However, that doesn't matter because:
3130

3231
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.
3433

3534
**Q: Can we use Kotlin Flow APIs in a compose viewmodel?**
3635

@@ -46,7 +45,7 @@ fun getBtcPrice(): String? {
4645

4746
**Q: What's the benefit of having Compose in the VM?**
4847

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.
5049

5150
All of the above is better seen in code and practice - make sure to check our references to learn more.
5251

@@ -58,11 +57,11 @@ The view-state is a data model that contains all information that the screen/com
5857
5958
## View-event
6059

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.
6261

6362
## Composable UI
6463

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.
6665

6766
## References
6867

0 commit comments

Comments
 (0)