• Stars
    star
    123
  • Rank 290,145 (Top 6 %)
  • Language
    Kotlin
  • License
    MIT License
  • Created over 6 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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.

Arkitekt

Download Build Status

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

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:

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

Usage

Table of contents

  1. Getting started - Minimal project file hierarchy
  2. Use Cases
  3. Propagating data model changes into UI
  4. 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.

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

@Singleton
@Component(
    modules = [
        AndroidInjectionModule::class,
        AndroidSupportInjectionModule::class,
        ActivityBuilderModule::class,
        ApplicationModule::class
    ]
)
interface ApplicationComponent : AndroidInjector<App> {

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

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

abstract class BaseActivity<VM : BaseViewModel<VS>, VS : ViewState, B : ViewDataBinding> :
    BaseDaggerBindingActivity<VM, VS, B>() {

    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 <layout> before you try to build your code. ViewModel can be accessed through derived viewModel field.

class MainActivity : BaseActivity<MainViewModel, MainViewState, ActivityMainBinding>(), 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.:

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

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.

class MainViewModel @Inject constructor() : BaseViewModel<MainViewState>() {

    override val viewState = MainViewState
}
MainViewModelFactory.kt

Factory responsible for ViewModel creation. It is injected in Activity/Fragment.

class MainViewModelFactory @Inject constructor(
    override val viewModelProvider: Provider<MainViewModel>
) : BaseViewModelFactory<MainViewModel>() {
    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.

object MainViewState : ViewState {
    val user = DefaultValueLiveData<User>(User.EMPTY)
}
activity_main.xml

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

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable name="view" type="app.futured.arkitekt.sample.ui.main.MainView"/>
        <variable name="viewModel" type="app.futured.arkitekt.sample.ui.main.MainViewModel"/>
        <variable name="viewState" type="app.futured.arkitekt.sample.ui.main.MainViewState"/>
    </data>

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:gravity="center">

    </LinearLayout>
</layout>

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
class LoginUseCase @Inject constructor(
    private val apiManager: ApiManager // Retrofit Service
) : SinglerUseCase<LoginData, User>() {

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

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

    // OUT - Values observed by UI
    val fullName = MutableLiveData<String>()
    val isLoading = MutableLiveData<Boolean>()
}
LoginViewModel.kt
class LoginViewModel @Inject constructor(
    private val loginUseCase: LoginUseCase // Inject UseCase
) : BaseRxViewModel<LoginViewState>() {
    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.

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:

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:

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.

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:

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable name="view" type="app.futured.arkitekt.sample.ui.detail.DetailView"/>
        <variable name="viewModel" type="app.futured.arkitekt.sample.ui.detail.DetailViewModel"/>
        <variable name="viewState" type="app.futured.arkitekt.sample.ui.detail.DetailViewState"/>
    </data>
    
    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewState.myTextLiveData}"/>
</layout>

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
sealed class MainEvent : Event<MainViewState>()

object ShowDetailEvent : MainEvent()
MainViewModel.kt
class MainViewModel @Inject constructor() : BaseViewModel<MainViewState>() {

    override val viewState = MainViewState

    fun onDetail() {
        sendEvent(ShowDetailEvent)
    }
}
MainActivity.kt
class MainActivity : BaseActivity<MainViewModel, MainViewState, ActivityMainBinding>(), 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:

@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<User> {
        return userRelay.hide()
    }
}

With this approach only one class is responsible for User related data access. Besides custom classes, Room library Daos 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.

class GetUserFullNameObservabler @Inject constructor(
    private val userStore: UserStore
) : ObservablerUseCase<String>() {

    override fun prepare(): Observable<String> {
        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.

@Module
class MainActivityModule {

    @Provides
    fun savedStateRegistryOwner(activity: MainActivity): SavedStateRegistryOwner = activity
}
class MainViewModelFactory @Inject constructor(
    savedStateRegistryOwner: SavedStateRegistryOwner,
    override val viewModelProvider: Provider<MainViewModel>
) : BaseSavedStateViewModelFactory<MainViewModel>(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 in example module for more detailed sample.

ViewModel testing

core-test 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 in example module for more detailed sample of view model testing.

Events testing

The spy object should be used for an easy way of testing that expected events were sent to the view.

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:

viewState.counter.observeWithoutOwner { value ->
    viewState.counterText.value = value.toString() 
}

then it can be mocked with the following method:

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 or cr-usecase-test dependencies containing utilities to help you with mocking use cases in a view model.

Since all 'execute' methods for 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:

fun onLoginClicked(name: String, password: String) {
    loginUseCase.execute(LoginData(name, password)) {
        onSuccess = { ... }
    }
}

then it can be mocked with the following method:

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:

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

doAfterActivityInjection<SampleActivity> { activity ->  
    val provider = SampleViewModel(mockk(), SampleViewState()).asProvider()  
    activity.viewModelFactory = SampleViewModelFactory(viewModelProvider)  
    activity.someInjectedClass = mockk()  
}	

See these tests 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 for more information.

Created with ❀ at Futured. Inspired by Alfonz library.

More Repositories

1

donut

Doughnut-like graph view capable of displaying multiple datasets with assignable colors
Kotlin
541
star
2

hauler

Library with swipe to dismiss Activity gesture implementation
Kotlin
434
star
3

TFBubbleItUp

Custom view for writing tags, contacts and etc. - written in Swift
Swift
330
star
4

Sonar

Radar style view written in swift
Swift
129
star
5

TFTransparentNavigationBar

Custom transition between controllers in UINavigationController that makes navigation bar transparent on specified controllers
Swift
46
star
6

sheet-happens

Gradle plugin for generating Android / KMP string resources from Google Spreadsheets.
Kotlin
45
star
7

arkitekt-kmm

KMM library for UseCase abstraction
Kotlin
44
star
8

iOS-templates

Templates for MVVM-C architecture
Makefile
32
star
9

FuntastyKit

A collection of Swift utilities and protocols used in our projects
Swift
30
star
10

MVVM-C-Example

Example project for MVVM-C architecture in Swift
Swift
29
star
11

infinity

[Deprecated] Infinite paginated RecyclerView scrolling
Java
27
star
12

kmp-futured-template

Template for creating Kotlin Multiplatform applications at Futured.
Kotlin
20
star
13

FTAPIKit

Declarative and generic REST API framework using Codable.
Swift
19
star
14

Engineering-Handbook

Best practices and guidelines for developing apps in Futured
15
star
15

Gutenberg

Tiny library for generating attributed string with emoticons as images
Shell
14
star
16

bitrise-step-apk-info

Ruby
11
star
17

CellKit

Table View and Collection View data source wrapper
Swift
10
star
18

FormStateKit

A Swift package for simple management of forms and their fields. Focused on SwiftUI.
Swift
9
star
19

FTPropertyWrappers

Property wrappers for User Defaults, Keychain, StoredSubject and synchronization.
Swift
8
star
20

TFTableDescriptor

iOS Library / Simple table descriptor with dynamic cell height support
Objective-C
6
star
21

taste

[Deprecated]
Java
6
star
22

LocationManager

Swift
6
star
23

danger

The Danger rules we use at @futuredapp
Ruby
6
star
24

tasting

Android UI Automator wrapper for human-like scenario testing
Kotlin
6
star
25

Monkey-Madness

Easy to use ADB monkey command line wrapper
Shell
6
star
26

bitrise-step-ipa-info

Ruby
5
star
27

Funtasty-iOS-Playbook

Conventions for writing iOS applications
4
star
28

Realm-Codegen

Code Generation of Realm model files in Swift using Sourcery
Swift
2
star
29

OpenAPI-Project-Template

Template for OpenAPI specifications
Makefile
2
star
30

android-project-template

Kotlin
2
star
31

dev-academy-ios

Swift
2
star
32

f-academy-project-android

Kotlin
2
star
33

android-project-template-compose

Kotlin
1
star
34

brmo2023-kmm-talk

Code for the "KMM: Shared Navigation -- Finding The Sweet Spot" talk presented at BrMo2023 & Betsys Mobile dev meetup 2023
Kotlin
1
star
35

FTLogKit

Yet another logging framework for Apple platforms
Swift
1
star
36

ui-test-examples

xcode ui test examples
Swift
1
star
37

SwiftMonkeys

Monkeys for UI automated testing. Also known as UI/Application Exerciser Monkey.
Objective-C
1
star
38

TFSlideControl

Swift
1
star
39

.github

1
star
40

FCodeWars

1
star
41

FTTestingKit

Mockups and async extension to XCTestCase
Swift
1
star
42

FuturedKit

SwiftUI state management tools, resources and views used by Futured.
Swift
1
star
43

iOS-project-template

Template for creating iOS projects we use @futuredapp.
Ruby
1
star