Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/futuredapp/arkitekt

Arkitekt is a set of architectural tools based on Android Architecture Components, which gives you a solid base to implement the concise, testable and solid application.
https://github.com/futuredapp/arkitekt

android architecture-components compose databinding jetpack jetpack-compose kotlin mvvm mvvm-architecture

Last synced: about 20 hours ago
JSON representation

Arkitekt is a set of architectural tools based on Android Architecture Components, which gives you a solid base to implement the concise, testable and solid application.

Awesome Lists containing this project

README

        

# Arkitekt

[![Download](https://img.shields.io/maven-central/v/app.futured.arkitekt/core)](https://search.maven.org/search?q=app.futured.arkitekt)
[![Build Status](https://github.com/futuredapp/arkitekt/workflows/Check%205.x/badge.svg)](https://github.com/futuredapp/arkitekt/actions)

Arkitekt is a set of architectural tools based on Android Architecture Components, which gives you a solid base to implement the concise, testable and solid application.

# Installation

```groovy
android {
// AGP < 4.0.0
dataBinding {
enabled = true
}

// AGP >= 4.0.0
buildFeatures {
dataBinding = true
}
}

dependencies {
implementation("app.futured.arkitekt:core:LatestVersion")
implementation("app.futured.arkitekt:bindingadapters:LatestVersion")
implementation("app.futured.arkitekt:dagger:LatestVersion")
implementation("app.futured.arkitekt:cr-usecases:LatestVersion")
implementation("app.futured.arkitekt:rx-usecases:LatestVersion")

// Testing
testImplementation("app.futured.arkitekt:core-test:LatestVersion")
testImplementation("app.futured.arkitekt:rx-usecases-test:LatestVersion")
testImplementation("app.futured.arkitekt:cr-usecases-test:LatestVersion")
}
```

## Snapshot installation

Add new maven repo to your top level gradle file.

```
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
```

Snapshots are grouped based on major version, so for version 5.x use:

```groovy
implementation "app.futured.arkitekt:arkitekt:5.X.X-SNAPSHOT"
```

# Features

Arkitekt combines built-in support for Dagger 2 dependency injection, View DataBinding,
ViewModel and RxJava or Coroutines use cases. Architecture described here is used among wide variety
of projects and it's production ready.

![MVVM architecture](extras/architecture-diagram.png)

# Usage

## Table of contents

1. [Getting started - Minimal project file hierarchy](#getting-started---minimal-project-file-hierarchy)
2. [Use Cases](#use-cases)
3. [Propagating data model changes into UI](#propagating-data-model-changes-into-ui)
4. [Stores (Repositories)](#stores-repositories)

## Getting started - Minimal project file hierarchy
Minimal working project must contain files as presented in `example-minimal`
module. File hierarchy might looks like this:
```
example-minimal
`-- src/main
|-- java/com/example
| |-- injection
| | |-- ActivityBuilderModule.kt
| | |-- ApplicationComponent.kt
| | `-- ApplicationModule.kt
| |-- ui
| | |-- base/BaseActivity.kt
| | `-- main
| | |-- MainActivity.kt
| | |-- MainActivityModule.kt
| | |-- MainView.kt
| | |-- MainViewModel.kt
| | |-- MainViewModelFactory.kt
| | `-- MainViewState.kt
| `-- App.kt
`-- res/layout/activity_main.xml
```

Keep in mind this description focuses on architecture `.kt` files. Android related files like an
`AndroidManifest.xml` are omitted. Let's describe individual files one by one:

##### `ActivityBuilderModule.kt`
File contains Dagger module class that takes responsibility of proper injection
into Activities. This is the place where every Activity and its `ActivityModule`
in project must be specified to make correct ViewModel injection work.

```kotlin
@Module
abstract class ActivityBuilderModule {

@ContributesAndroidInjector(modules = [MainActivityModule::class])
abstract fun mainActivity(): MainActivity
}
```

##### `ApplicationComponent.kt`

ApplicationComponent interface combines your singleton Dagger modules and defines
how `DaggerApplicationComponent` should be generated.
```kotlin
@Singleton
@Component(
modules = [
AndroidInjectionModule::class,
AndroidSupportInjectionModule::class,
ActivityBuilderModule::class,
ApplicationModule::class
]
)
interface ApplicationComponent : AndroidInjector {

@Component.Builder
interface Builder {

@BindsInstance
fun application(app: App): Builder

fun build(): ApplicationComponent
}
}
```

##### `ApplicationModule.kt`

Application module definition. Your singleton scoped objects might
be specified here and injected wherever needed. Example implementation:
```kotlin
@Module
class ApplicationModule {

@Singleton
@Provides
fun moshi(): Moshi = Moshi.Builder().build()
}
```

##### `BaseActivity.kt`

All of Activities in the project should inherit from this class to make DataBinding work properly.
Be aware of fact BR class used in this class is generated when there is at least one layout file
with correctly defined data variables. Read more [here](#activity_mainxml).
```kotlin
abstract class BaseActivity, VS : ViewState, B : ViewDataBinding> :
BaseDaggerBindingActivity() {

override val brViewVariableId = BR.view
override val brViewModelVariableId = BR.viewModel
override val brViewStateVariableId = BR.viewState
}
```

##### `MainActivity.kt`

Example Activity implementation. `viewModelFactory` and `layoutResId` must be overridden in every
Activity in order to make ViewModel injection and DataBinding work. `ActivityMainBinding` used
in `BaseActivity` constructor is generated from related `activity_main.xml` layout file. Make sure this file
exists and have root tag `` before you try to build your code. `ViewModel` can be
accessed through derived `viewModel` field.
```kotlin
class MainActivity : BaseActivity(), MainView {

@Inject override lateinit var viewModelFactory: MainViewModelFactory

override val layoutResId = R.layout.activity_main
}
```

##### `MainActivityModule.kt`

`MainActivity` scoped module. It becomes useful when you want to provide specific
activity related configuration e.g.:

```kotlin
@Module
abstract class MainActivityModule {

@Provides
fun provideUser(activity: MainActivity): User =
activity.intent.getParcelableExtra("user")
}
```

##### `MainView.kt`

Interface representing actions executable on your Activity/Fragment. These actions
might be invoked directly from xml layout thanks to `view` data variable.
```kotlin
interface MainView : BaseView
```

##### `MainViewModel.kt`

Activity/Fragment specific ViewModel implementation. You can choose between extending
`BaseViewModel` or `BaseRxViewModel` with build-in support for RxJava based use cases.
```kotlin
class MainViewModel @Inject constructor() : BaseViewModel() {

override val viewState = MainViewState
}
```

##### `MainViewModelFactory.kt`

Factory responsible for `ViewModel` creation. It is injected in Activity/Fragment.
```kotlin
class MainViewModelFactory @Inject constructor(
override val viewModelProvider: Provider
) : BaseViewModelFactory() {
override val viewModelClass = MainViewModel::class
}
```

##### `MainViewState.kt`

State representation of an screen. Should contain set of `LiveData` fields observed
by Activity/Fragment. State is stored in `ViewModel` thus survives screen rotation.
```kotlin
object MainViewState : ViewState {
val user = DefaultValueLiveData(User.EMPTY)
}
```

##### `activity_main.xml`

Layout file containing proper DataBinding variables initialization. Make sure correct
types are defined.
```xml





```

## Use Cases

Modules `cr-usecases` and `rx-usecases` contains set of base classes useful for easy execution of
background tasks based on Coroutines or RxJava streams respectively. In terms of Coroutines
two base types are available - `UseCase` (single result use case) and `FlowUseCase` (multi result use case).
RxJava base use cases match base Rx "primitives": `ObservableUseCase`, `SingleUseCase`, `FlowableUseCase`, `MaybeUseCase`
and finally `CompletableUseCase`.

Following example describes how to make an API call and how to deal with
result of this call.

##### LoginUseCase.kt
```kotlin
class LoginUseCase @Inject constructor(
private val apiManager: ApiManager // Retrofit Service
) : SinglerUseCase() {

override fun prepare(args: LoginData): Single {
return apiManager.getUser(args)
}
}

data class LoginData(val email: String, val password: String)
```
##### LoginViewState.kt
```kotlin
class LoginViewState : ViewState {
// IN - values provided by UI
val email = DefaultValueLiveData("")
val password = DefaultValueLiveData("")

// OUT - Values observed by UI
val fullName = MutableLiveData()
val isLoading = MutableLiveData()
}
```

##### LoginViewModel.kt
```kotlin
class LoginViewModel @Inject constructor(
private val loginUseCase: LoginUseCase // Inject UseCase
) : BaseRxViewModel() {
override val viewState = LoginViewState()

fun logIn() = with(viewState) {
loginUseCase.execute(LoginData(email.value, email.password)) {
onStart {
isLoading.value = true
}
onSuccess {
isLoading.value = false
fullName.value = user.fullName // handle success & manipulate state
}
onError {
isLoading.value = false
// handle error
}
}
}
}
```

### Synchronous execution of cr-usecase

Module `cr-usecases` allows you to execute use cases synchronously.
```kotlin
fun onButtonClicked() = launchWithHandler {
// ...
val data = useCase.execute().getOrDefault("Default")
// ...
}
```
`execute` method returns a `Result` that can be either successful `Success` or failed `Error`.

`launchWithHandler` launches a new coroutine encapsulated with a try-catch block. By default exception thrown in `launchWithHandler` is rethrown but it is possible to override this behavior with `defaultErrorHandler` or just log these exceptions in `logUnhandledException`.

### Global error logger for handled errors in use-cases

In order to set an application-wide error logger for all handled errors in all use-cases, it is possible to set the following method in the `Application` class:

```kotlin
UseCaseErrorHandler.globalOnErrorLogger = { error ->
CustomLogger.logError(error)
}
```

The `globalOnErrorLogger` callback in the `UseCaseErrorHandler` will be called for every error thrown in all use-cases that have defined onError receiver in the execute method.

The following execute method will trigger `globalOnErrorLogger`:

```kotlin
useCase.execute {
...
onError {
isLoading = false
}
...
}
```

The following execute method won't trigger `globalOnErrorLogger` because onError is not defined and execute method will throw an unhandled exception.

```kotlin
useCase.execute {}
```

## Propagating data model changes into UI
There are two main ways how to reflect data model changes in UI. Through `ViewState` observation
or one-shot `Events`.

### ViewState observation

You can observe state changes and reflect these changes in UI via DataBinding
observation directly in xml layout:

```xml










```

### Events
Events are one-shot messages sent from `ViewModel` to an Activity/Fragment. They
are based on `LiveData` bus. Events are guaranteed to be delivered only once even when
there is screen rotation in progress. Basic event communication might look like this:

##### `MainEvents.kt`
```kotlin
sealed class MainEvent : Event()

object ShowDetailEvent : MainEvent()
```

##### `MainViewModel.kt`
```kotlin
class MainViewModel @Inject constructor() : BaseViewModel() {

override val viewState = MainViewState

fun onDetail() {
sendEvent(ShowDetailEvent)
}
}
```

##### `MainActivity.kt`
```kotlin
class MainActivity : BaseActivity(), MainView {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

observeEvent(ShowDetailEvent::class) {
startActivity(DetailActivity.getStartIntent(this))
}
}
}
```

## Stores (Repositories)
All our applications respect broadly known repository pattern. The main message this
pattern tells: Define `Store` (Repository) classes with single entity related business logic
eg. `UserStore`, `OrderStore`, `DeviceStore` etc. Let's see this principle on `UserStore` class
from sample app:

```kotlin
@Singleton
class UserStore @Inject constructor() {
private val userRelay = BehaviorRelay.createDefault(User.EMPTY)

fun setUser(user: User) {
userRelay.accept(user)
// ... optionally persist user
}

fun getUser(): Observable {
return userRelay.hide()
}
}
```

With this approach only one class is responsible for `User` related data access. Besides
custom classes, Room library `Dao`s or for example Retrofit API interfaces might be
perceived on the same domain level as stores. Thanks to use cases we can easily access,
manipulate and combine this kind of data on background threads.

```kotlin
class GetUserFullNameObservabler @Inject constructor(
private val userStore: UserStore
) : ObservablerUseCase() {

override fun prepare(): Observable {
return userStore.getUser()
.map { "${it.firstName} ${it.lastName}" }
}
}
```

We strictly respect this injection hierarchy:

| Application Component | Injects |
| --------- | --------------------- |
| `Activity/Fragment` | `ViewModel` |
| `ViewModel` | `ViewState`, `UseCase` |
| `UseCase` | `Store` |
| `Store` | `Dao`, `Persistence`, `ApiService` |

## SavedStateHandle

Arkitekt also supports `SavedStateHandle` in `ViewModel`. To have access to `SavedStateHandle` instance you have to use `BaseSavedStateViewModelFactory` base class instead of `BaseViewModelFactory` in your ViewModelFactory implementation and provide `SavedStateRepositoryOwner` in your Activity/Fragment module if using Dagger.
`SavedStateHandle` instance is part of `BaseViewModel` class so you can access it via `savedStateHandle` field. Beware that this field may be null if you don't use `BaseSavedStateViewModelFactory` as base class for your `ViewModelFactory` implementation.

```kotlin
@Module
class MainActivityModule {

@Provides
fun savedStateRegistryOwner(activity: MainActivity): SavedStateRegistryOwner = activity
}
```

```kotlin
class MainViewModelFactory @Inject constructor(
savedStateRegistryOwner: SavedStateRegistryOwner,
override val viewModelProvider: Provider
) : BaseSavedStateViewModelFactory(savedStateRegistryOwner) {
override val viewModelClass = MainViewModel::class
}
```
## Testing

In order to create successful applications, it is highly encouraged to write tests for your application. But testing can be tricky sometimes so here are our best practices and utilities that will help you to achieve this goal with this library.

See [these tests](https://github.com/futuredapp/arkitekt/tree/5.x/example/src/) in `example` module for more detailed sample.

### ViewModel testing

[core-test](#Download) dependency contains utilities to help you with ViewModel testing.

`ViewModelTest` that should be used as a base class for view model tests since it contains JUnit rules for dealing with a live data and with RxJava in tests.

See [these tests](https://github.com/futuredapp/arkitekt/tree/5.x/example/src/test/java/app/futured/arkitekt/sample/ui/) in `example` module for more detailed sample of view model testing.

### Events testing

The [spy](https://github.com/mockk/mockk#spy) object should be used for an easy way of testing that expected events were sent to the view.

```kotlin
viewModel = spyk(SampleViewModel(mockViewState, ...), recordPrivateCalls = true)
...
verify { viewModel.sendEvent(ExpectedEvent) }
```
### Mocking of observeWithoutOwner

When you are using `observeWithoutOwner` extensions then `everyObserveWithoutOwner` will be helpful for mocking of these methods.

So if a method in the view model looks somehow like this:
```kotlin
viewState.counter.observeWithoutOwner { value ->
viewState.counterText.value = value.toString()
}
```
then it can be mocked with the following method:
```kotlin
val counterLambda = viewModel.everyObserveWithoutOwner {
viewState.counter
}
...
counterLambda.invoke(1)
```
invoke(...) call will invoke a lambda argument passed to the `observeWithoutOwner` method in the tested method.

### Mocking of Use Cases

Add [rx-usecase-test](#Download) or [cr-usecase-test](#Download) dependencies containing utilities to help you with mocking use cases in a view model.

Since all 'execute' methods for [use cases](#use-cases) are implemented as extension functions, we created testing methods that will help you to easily mock them.

So if a method in the view model looks somehow like this:
```kotlin
fun onLoginClicked(name: String, password: String) {
loginUseCase.execute(LoginData(name, password)) {
onSuccess = { ... }
}
}
```
then it can be mocked with the following method:
```kotlin
mockLoginUseCase.mockExecute(args = ...) { Single.just(user) } // For RxJava Use Cases
or
mockLoginUseCase.mockExecute(args = ...) { user } // For Coroutines Use Cases
```
In case that use case is using nullable arguments:
```kotlin
mockLoginUseCase.mockExecuteNullable(args = ...) { Single.just(user) } // For RxJava Use Cases
or
mockLoginUseCase.mockExecuteNullable(args = ...) { user } // For Coroutines Use Cases
```

### Activity and Fragment tests

[core-test](#Download) dependency contains utilities to help you with espresso testing.

If you want to test Activities or Fragments then you have few possibilities. You can test them with the mocked implementation of a view model and view state, or you can test them with the real implementation of a view model and view state and with mocked use cases.

Since Fragments and Activities from the dagger module are using AndroidInjection, we created utilities to deal with this.

In your tests, you can use `doAfterActivityInjection` and `doAfterFragmentInjection` to overwrite injected dependencies. These methods are called right after `AndroidInjection` and that allows overwriting of needed dependencies. In the following example, we are replacing the view model with the implementation that is using a view model with mocked dependencies and some random class with mocked implementation.

```kotlin
doAfterActivityInjection { activity ->
val provider = SampleViewModel(mockk(), SampleViewState()).asProvider()
activity.viewModelFactory = SampleViewModelFactory(viewModelProvider)
activity.someInjectedClass = mockk()
}
```
See [these tests](https://github.com/futuredapp/arkitekt/tree/5.x/example/src/sharedTest/java/app/futured/arkitekt/sample/ui) in `example` module for more detailed samples of espresso test that can be executed as local unit tests or connected android tests.

# License
Arkitekt is available under the MIT license. See the [LICENSE file](LICENCE) for more information.

Created with ❤ at Futured. Inspired by [Alfonz library](https://github.com/petrnohejl/Alfonz).