├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── stepango
│ │ │ └── archetype
│ │ │ ├── App.kt
│ │ │ ├── action
│ │ │ ├── Action.kt
│ │ │ ├── ActionHandler.kt
│ │ │ ├── Args.kt
│ │ │ ├── ContextActionHandler.kt
│ │ │ ├── IntentAction.kt
│ │ │ └── IntentMaker.kt
│ │ │ ├── activity
│ │ │ └── BaseActivty.kt
│ │ │ ├── bundle
│ │ │ └── BundleUtils.kt
│ │ │ ├── databindings
│ │ │ ├── ImageViewBindings.kt
│ │ │ ├── RecyclerViewBindings.kt
│ │ │ ├── ViewBindings.kt
│ │ │ └── WebViewBinding.kt
│ │ │ ├── db
│ │ │ ├── BaseRepos.kt
│ │ │ ├── Contract.kt
│ │ │ ├── ContractExt.kt
│ │ │ └── RepoSupervisor.kt
│ │ │ ├── fragment
│ │ │ ├── BaseFragment.kt
│ │ │ └── FragmentExt.kt
│ │ │ ├── glide
│ │ │ └── GlideImageLoader.kt
│ │ │ ├── image
│ │ │ ├── CropCircleTransformation.kt
│ │ │ └── ImageLoader.kt
│ │ │ ├── logger
│ │ │ ├── Logger.kt
│ │ │ ├── LoggerExt.kt
│ │ │ └── SimpleLogger.kt
│ │ │ ├── player
│ │ │ ├── ArgsExt.kt
│ │ │ ├── Constants.kt
│ │ │ ├── data
│ │ │ │ ├── db
│ │ │ │ │ ├── Repos.kt
│ │ │ │ │ ├── memory
│ │ │ │ │ │ ├── InMemoryKeyValueRepo.kt
│ │ │ │ │ │ └── InMemoryRepos.kt
│ │ │ │ │ ├── model
│ │ │ │ │ │ └── EpisodesModel.kt
│ │ │ │ │ └── response
│ │ │ │ │ │ └── feed
│ │ │ │ │ │ ├── Channel.java
│ │ │ │ │ │ ├── Enclosure.java
│ │ │ │ │ │ ├── Image.java
│ │ │ │ │ │ ├── Item.java
│ │ │ │ │ │ └── Rss.java
│ │ │ │ └── wrappers
│ │ │ │ │ └── Wrappers.kt
│ │ │ ├── di
│ │ │ │ └── Injector.kt
│ │ │ ├── loader
│ │ │ │ ├── DownloadEpisodeAction.kt
│ │ │ │ ├── EpisodeLoaderService.kt
│ │ │ │ └── ProgressOkLoader.kt
│ │ │ ├── network
│ │ │ │ ├── Api.kt
│ │ │ │ ├── NetworkRequest.kt
│ │ │ │ └── get
│ │ │ │ │ ├── Contants.kt
│ │ │ │ │ └── GetEpisodesRequest.kt
│ │ │ └── ui
│ │ │ │ ├── additional
│ │ │ │ └── MockToaster.kt
│ │ │ │ ├── episodes
│ │ │ │ ├── EpisodesScreen.kt
│ │ │ │ └── EpisodesUseCase.kt
│ │ │ │ └── player
│ │ │ │ ├── PlayerComponent.kt
│ │ │ │ ├── PlayerScreen.kt
│ │ │ │ └── ShowEpisodeAction.kt
│ │ │ ├── resources
│ │ │ └── ContextExt.kt
│ │ │ ├── rx
│ │ │ ├── CompositeDisposableComponent.kt
│ │ │ ├── RxExt.kt
│ │ │ ├── schedulers.kt
│ │ │ └── singles.kt
│ │ │ ├── ui
│ │ │ ├── LastAdapterExt.kt
│ │ │ ├── SimpleToaster.kt
│ │ │ ├── SpaceItemDecoration.kt
│ │ │ ├── Toaster.kt
│ │ │ └── ViewExt.kt
│ │ │ ├── util
│ │ │ ├── ContextUtil.kt
│ │ │ ├── StringExt.kt
│ │ │ └── UriUtils.kt
│ │ │ └── viewmodel
│ │ │ ├── LoaderHolder.kt
│ │ │ ├── LoadingProgressHelperImpl.kt
│ │ │ ├── RxLifecycleImpl.kt
│ │ │ ├── Stubs.kt
│ │ │ └── ViewModel.kt
│ └── res
│ │ ├── drawable
│ │ └── bg_divider.xml
│ │ ├── layout
│ │ ├── include_app_bar.xml
│ │ ├── item_episode.xml
│ │ ├── screen_episodes.xml
│ │ └── screen_player.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── actions.xml
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ ├── java
│ └── com
│ │ └── stepango
│ │ └── archetype
│ │ ├── FeedResponseParsingTest.kt
│ │ ├── player
│ │ ├── data
│ │ │ └── db
│ │ │ │ └── memory
│ │ │ │ └── InMemoryKeyValueRepoTest.kt
│ │ ├── di
│ │ │ └── Injector.kt
│ │ ├── network
│ │ │ └── get
│ │ │ │ └── GetEpisodesRequestTest.kt
│ │ └── ui
│ │ │ └── episodes
│ │ │ └── EpisodesUseCaseImplTest.kt
│ │ └── viewmodel
│ │ ├── Stubs.kt
│ │ └── ViewModelImplTest.kt
│ └── resources
│ ├── feed_response.xml
│ └── mockito-extensions
│ └── org.mockito.plugins.MockMaker
├── archetype_mobius_2017.pdf
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Archetype
2 | [](https://codebeat.co/projects/github-com-stepango-archetype-master)
3 | [](https://www.bitrise.io/app/65613de01e0da309)
4 |
5 | Badass MVVM architecture.
6 |
7 | At the moment Archetype contains implementation of Android Dev podcast player.
8 |
9 | Official Telegram chat https://t.me/archetype_android
10 |
11 | Mobius Russia 2017 Talk https://www.youtube.com/watch?v=M3fTMBfmBqU&t=1380s
12 |
13 | # Main libraries and concepts
14 | - Android SDK, JDK 1.8 and [Kotlin](https://kotlinlang.org/)
15 | - [Reactive programming](http://reactivex.io/) with [RxJava2](https://github.com/ReactiveX/RxJava) for asynchronous tasks
16 | - [Retrofit](https://github.com/square/retrofit) - for simple REST implementation
17 |
18 | ## Build
19 | Project uses Gradle as build system. You can find main gradle config for Android app module here: `app/build.gradle`
20 |
21 | # Code organisation rules:
22 |
23 | ## Basic
24 | - All or no arguments should be named when pass to function, partial naming is not allowed
25 |
26 | ## Kotlin
27 | - Order of declarations inside class or file: `val`, `var`, `constructor`, `init`, `fun`, `private fun`
28 |
29 | ## DataBindings
30 | - All general function's annotated with `@BindingAdapter` should be stored in `*.databindings` package, filename should be `'ViewName'Bindings.kt`.
31 | - `@BindingAdapter` functions that couldn't be reused should be stored in file that contains related VM or should be grouped in separate file named `'Feature'Bindings.kt`
32 | - All all bindings in xml should start with `bind:` prefix
33 | - All ViewModels in XML should be named `vm`
34 |
35 | ## Gradle
36 | - All lib and gradle plugin versions should be stored in root `build.gradle` file.
37 |
38 | ## Rx
39 | - Subscribing to observable allowed only with `subscribeBy` or `bindSubscribe` extension methods.
40 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: "io.mironov.smuggler"
3 | apply plugin: 'kotlin-android'
4 | apply plugin: "kotlin-kapt"
5 |
6 | android {
7 | compileSdkVersion 27
8 | buildToolsVersion '27.0.3'
9 | defaultConfig {
10 | applicationId "com.stepango.archetype"
11 | minSdkVersion 21
12 | targetSdkVersion 27
13 | versionCode 1
14 | versionName "1.0"
15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | dataBinding.enabled = true
24 | }
25 |
26 | dependencies {
27 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
28 | implementation "io.reactivex.rxjava2:rxjava:2.1.10"
29 | implementation "io.reactivex.rxjava2:rxandroid:2.0.2"
30 | implementation "com.stepango.koptional:koptional:1.2.0"
31 |
32 | implementation("com.android.support:recyclerview-v7:$supportLibVersion") {
33 | exclude group: "com.android.support", module: "support-v4"
34 | }
35 |
36 | implementation("com.stepango.rxdatabindings:rxdatabindings:1.2.1@aar") {
37 | exclude group: "com.android.databinding", module: "library"
38 | exclude group: "com.android.databinding", module: "baseLibrary"
39 | exclude group: "com.android.databinding", module: "adapters"
40 | }
41 |
42 | implementation("com.github.nitrico.lastadapter:lastadapter:$lastAdapterVersion") {
43 | exclude group: "com.android.databinding", module: "library"
44 | exclude group: "com.android.databinding", module: "baseLibrary"
45 | exclude group: "com.android.databinding", module: "adapters"
46 | }
47 |
48 | // compile("com.android.databinding:library:$dataBinding") {
49 | // exclude group: "com.android.support", module: "support-v4"
50 | // }
51 |
52 | //TODO: remove it and solve version clash problem
53 | implementation "com.android.support:support-v4:$supportLibVersion"
54 |
55 | implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
56 | testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion"
57 | implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
58 | implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
59 | implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
60 | implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
61 | implementation("com.squareup.retrofit2:converter-simplexml:$retrofitVersion") {
62 | exclude module: 'stax'
63 | exclude module: 'stax-api'
64 | exclude module: 'xpp3'
65 | }
66 | implementation 'com.trello.navi2:navi:2.0'
67 |
68 | implementation "com.github.bumptech.glide:glide:$glideVersion"
69 | implementation "com.github.bumptech.glide:okhttp3-integration:$glideOkHttp@aar"
70 |
71 | testImplementation 'junit:junit:4.12'
72 | testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
73 | testImplementation "com.nhaarman:mockito-kotlin:1.5.0"
74 | testImplementation 'org.mockito:mockito-core:2.13.0'
75 |
76 | }
77 |
78 | kapt {
79 | generateStubs = true
80 | }
81 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/stepangoncarov/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stepango/archetype/App.kt:
--------------------------------------------------------------------------------
1 | package com.stepango.archetype
2 |
3 | import android.app.Application
4 | import com.stepango.archetype.rx.actionScheduler
5 | import com.stepango.archetype.rx.networkScheduler
6 | import com.stepango.archetype.rx.nonDisposableActionScheduler
7 | import com.stepango.archetype.rx.uiScheduler
8 | import io.reactivex.android.schedulers.AndroidSchedulers
9 | import io.reactivex.schedulers.Schedulers
10 |
11 | open class App : Application() {
12 |
13 | companion object {
14 | lateinit var instance: App
15 | }
16 |
17 | init {
18 | instance = this
19 | initSchedulers()
20 | }
21 |
22 | private fun initSchedulers() {
23 | networkScheduler = Schedulers.io()
24 | actionScheduler = Schedulers.io()
25 | nonDisposableActionScheduler = Schedulers.computation()
26 | uiScheduler = AndroidSchedulers.mainThread()
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/stepango/archetype/action/Action.kt:
--------------------------------------------------------------------------------
1 | package com.stepango.archetype.action
2 |
3 | import android.content.Context
4 | import io.reactivex.Completable
5 |
6 | interface ContextAction
{
7 | fun isDisposable() = true
8 | operator fun invoke(context: Context, params: P): Completable
9 | }
10 |
11 | interface IDAction : ContextAction
12 |
13 | class IdleAction : ContextAction {
14 | override fun invoke(context: Context, params: Unit): Completable = Completable.complete()
15 | }
16 |
17 | data class ActionData(val action: ContextAction
, val params: P) {
18 |
19 | @Suppress("UNCHECKED_CAST")
20 | fun asHolder() = object : ActionDataHolder {
21 | override fun actionData(): ActionData? = this@ActionData as ActionData
22 | }
23 |
24 | companion object {
25 | val IDLE = ActionData(IdleAction(), Unit)
26 | }
27 | }
28 |
29 | data class NamedActionData(val name: String, val action: ActionData) {
30 | override fun toString() = name
31 |
32 | companion object {
33 | val IDLE = NamedActionData("", ActionData.IDLE)
34 | }
35 | }
36 |
37 | interface ActionDataHolder {
38 | fun actionData(): ActionData?
39 | }
40 |
41 | interface ActionHandlerHolder {
42 | val actionHandler: ContextActionHandler
43 | }
44 |
45 | fun ContextAction
.with(param: P): ActionData
= ActionData(this, param)
46 | fun
ActionData
.execute(ah: ContextActionHandler) = ah.handleAction(action, params)
47 | fun ContextAction.noParams(): ActionData = ActionData(this, Unit)
--------------------------------------------------------------------------------
/app/src/main/java/com/stepango/archetype/action/ActionHandler.kt:
--------------------------------------------------------------------------------
1 | package com.stepango.archetype.action
2 |
3 | import android.content.Context
4 | import com.stepango.archetype.rx.CompositeDisposableHolder
5 | import io.reactivex.Completable
6 |
7 | interface ContextActionHandlerFactory {
8 | fun createActionHandler(context: Context, compositeDisposableHolder: CompositeDisposableHolder): ContextActionHandler
9 | }
10 |
11 | interface ContextActionHandler {
12 | fun stopActions(): Completable
13 | fun handleAction(contextAction: ContextAction
, params: P)
14 | fun
createAction(contextAction: ContextAction
, params: P): Completable
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/stepango/archetype/action/Args.kt:
--------------------------------------------------------------------------------
1 | package com.stepango.archetype.action
2 |
3 | import android.os.Bundle
4 |
5 | typealias Args = Bundle
6 |
7 | fun argsOf(): Args = Bundle()
8 |
9 | fun argsOf(block: Args.() -> Unit) = argsOf().apply(block)
10 |
11 | fun Args.copy(): Args = Bundle(this)
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stepango/archetype/action/ContextActionHandler.kt:
--------------------------------------------------------------------------------
1 | package com.stepango.archetype.action
2 |
3 | import android.content.Context
4 | import com.stepango.archetype.logger.d
5 | import com.stepango.archetype.logger.logger
6 | import com.stepango.archetype.rx.CompositeDisposableHolder
7 | import com.stepango.archetype.rx.actionScheduler
8 | import com.stepango.archetype.rx.nonDisposableActionScheduler
9 | import io.reactivex.Completable
10 | import io.reactivex.CompletableObserver
11 | import io.reactivex.disposables.Disposable
12 |
13 | class ContextActionHandlerRealFactory : ContextActionHandlerFactory {
14 | override fun createActionHandler(context: Context, compositeDisposableHolder: CompositeDisposableHolder): ContextActionHandler = ContextActionHandlerImpl(context, compositeDisposableHolder)
15 | }
16 |
17 | class ContextActionHandlerImpl(
18 | val context: Context,
19 | val compositeDisposableHolder: CompositeDisposableHolder
20 | ) :
21 | CompositeDisposableHolder by compositeDisposableHolder, ContextActionHandler {
22 |
23 | override fun stopActions(): Completable = Completable.fromAction {
24 | resetCompositeDisposable()
25 | }
26 |
27 | inline fun ContextActionHandler.execute(data: ActionData) =
28 | handleAction(data.action, data.params)
29 |
30 | override fun createAction(contextAction: ContextAction
, params: P): Completable =
31 | contextAction.invoke(context, params).doOnSubscribe { logger.d { "Action:: createAction ${contextAction::class.java.name}" } }
32 |
33 | override fun
handleAction(contextAction: ContextAction
, params: P) {
34 | val actionName = contextAction::class.java.name
35 | logger.d { "Action:: handle $actionName" }
36 | val observer = observer(actionName)
37 | val isDisposable = contextAction.isDisposable()
38 | contextAction.invoke(context, params)
39 | .subscribeOn(if (isDisposable) actionScheduler else nonDisposableActionScheduler)
40 | .subscribeWith(observer)
41 | if (isDisposable) observer.disposable?.bind()
42 | }
43 |
44 | private fun observer(actionName: String) = object : CompletableObserver {
45 | var disposable: Disposable? = null
46 |
47 | override fun onComplete() {
48 | logger.d { "$actionName - completed successfully" }
49 | disposable?.let(composite::remove)
50 | }
51 |
52 | override fun onSubscribe(d: Disposable) {
53 | disposable = d
54 | }
55 |
56 | override fun onError(e: Throwable) {
57 | logger.e(e, "$actionName - completed with error")
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/stepango/archetype/action/IntentAction.kt:
--------------------------------------------------------------------------------
1 | package com.stepango.archetype.action
2 |
3 | interface IntentAction : ContextAction, IntentMaker
4 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stepango/archetype/action/IntentMaker.kt:
--------------------------------------------------------------------------------
1 | package com.stepango.archetype.action
2 |
3 | import android.app.Activity
4 | import android.app.Service
5 | import android.content.ActivityNotFoundException
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.os.Bundle
9 | import com.stepango.archetype.R
10 | import com.stepango.archetype.activity.asBaseActivity
11 | import com.stepango.archetype.player.di.Injector
12 | import io.reactivex.Completable
13 |
14 | import kotlin.reflect.KClass
15 |
16 | interface IntentMaker {
17 | fun make(ctx: Context, cls: KClass) = Intent(ctx, cls.java)
18 | fun make(action: String) = Intent(action)
19 | }
20 |
21 | class IntentMakerImpl : IntentMaker
22 |
23 | inline fun IntentMaker.intent(
24 | context: Context, args: Args = argsOf()
25 | ): Intent {
26 | val cls = T::class
27 | return intent(cls, context, args)
28 | }
29 |
30 | fun IntentMaker.intent(cls: KClass, context: Context, args: Args)
31 | = make(context, cls).apply { args.let { putExtras(it) } }
32 |
33 | inline fun IntentMaker.startIntent(
34 | context: Context, args: Args = argsOf(), requestCode: Int? = null, options: Bundle? = Bundle.EMPTY
35 | ) = intent(context, args).start(context, requestCode, options)
36 |
37 | inline fun IntentMaker.startService(
38 | context: Context, args: Args = argsOf(), options: Bundle? = Bundle.EMPTY
39 | ) = intent(context, args).startService(context, options)
40 |
41 | fun IntentMaker.startIntent(
42 | cls: KClass, context: Context, map: Args = argsOf(), requestCode: Int? = null, options: Bundle? = Bundle.EMPTY
43 | ) = intent(cls, context, map).start(context, requestCode, options)
44 |
45 | fun IntentMaker.startBroadcast(
46 | context: Context, action: String
47 | ) = make(action).sendBroadcast(context)
48 |
49 | // Here we are using nullable resourceType because methods like putExtra() in tests returns null
50 | fun Intent.start(
51 | ctx: Context,
52 | requestCode: Int? = null,
53 | options: Bundle? = Bundle.EMPTY
54 | ): Completable = Completable.fromCallable {
55 | try {
56 | if (requestCode == null) {
57 | ctx.startActivity(this, options)
58 | } else {
59 | if (action.isNullOrEmpty()) {
60 | ctx.asBaseActivity()!!.startActivityForResultOverrode(this, requestCode, options)
61 | } else {
62 | val chooser = Intent.createChooser(this, ctx.getString(R.string.choose_action))
63 | ctx.asBaseActivity()!!.startActivityForResultOverrode(chooser, requestCode, options)
64 | }
65 | }
66 | } catch (e: Exception) {
67 | if (e is ActivityNotFoundException) {
68 | Injector().toaster().showError(e, R.string.activity_not_found_error_text)
69 | } else {
70 | throw e
71 | }
72 | }
73 | }
74 |
75 | fun Intent.sendBroadcast(
76 | ctx: Context
77 | ): Completable = Completable.fromCallable {
78 | ctx.sendBroadcast(this)
79 | }
80 |
81 | fun Intent.startService(
82 | ctx: Context,
83 | options: Bundle? = Bundle.EMPTY
84 | ): Completable = Completable.fromCallable {
85 | this.putExtras(options)
86 | ctx.startService(this)
87 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/stepango/archetype/activity/BaseActivty.kt:
--------------------------------------------------------------------------------
1 | package com.stepango.archetype.activity
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.ContextWrapper
6 | import android.content.Intent
7 | import android.os.Bundle
8 | import android.view.WindowManager
9 | import com.stepango.archetype.fragment.replaceIn
10 | import com.stepango.archetype.fragment.BaseFragment
11 | import com.trello.navi2.component.NaviActivity
12 |
13 | abstract class BaseActivity : NaviActivity() {
14 |
15 | open val onBackPressedHandler: (activity: Activity) -> Boolean by lazy {
16 | fragment.onBackPressedHandler
17 | }
18 | open val fragmentProducer: () -> BaseFragment<*> = { throw IllegalArgumentException() }
19 | open val containerId = android.R.id.content
20 | lateinit var fragment: BaseFragment<*>
21 |
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | initActivityState(savedInstanceState)
25 | }
26 |
27 | private fun initActivityState(savedInstanceState: Bundle?) {
28 | if (savedInstanceState == null) {
29 | fragment = fragmentProducer()
30 | fragment.replaceIn(this, containerId)
31 | } else {
32 | fragment = fragmentManager.findFragmentById(containerId) as BaseFragment<*>
33 | }
34 | }
35 |
36 | override fun onBackPressed() {
37 | if (!onBackPressedHandler(this)) super.onBackPressed()
38 | }
39 |
40 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
41 | super.onActivityResult(requestCode, resultCode, data)
42 | fragment.onActivityResult(requestCode, resultCode, data)
43 | }
44 |
45 | /**
46 | * use this function because android's AppCompatActivity not allowing to mock startActivityForResult method
47 | */
48 | fun startActivityForResultOverrode(intent: Intent?, requestCode: Int, options: Bundle?) {
49 | startActivityForResult(intent, requestCode, options)
50 | }
51 | }
52 |
53 | fun BaseActivity?.showKeyboardOnStart()
54 | = this?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
55 |
56 | tailrec fun Context.asBaseActivity(): BaseActivity? {
57 | if (this is BaseActivity) {
58 | return this
59 | } else if (this is ContextWrapper) {
60 | return this.baseContext.asBaseActivity()
61 | }
62 | return null
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stepango/archetype/bundle/BundleUtils.kt:
--------------------------------------------------------------------------------
1 | package com.stepango.archetype.bundle
2 |
3 | import android.os.Bundle
4 | import android.os.Parcel
5 | import android.os.Parcelable
6 | import com.stepango.archetype.logger.d
7 | import com.stepango.archetype.logger.logger
8 |
9 | inline fun Bundle?.extract(defaultValueProducer: () -> T): T {
10 | this ?: return defaultValueProducer()
11 | val key = T::class.java.name
12 | if (containsKey(key)) {
13 | logger.d { "State:: restored $key" }
14 | return getParcelable(key)
15 | } else {
16 | return defaultValueProducer()
17 | }
18 | }
19 |
20 | fun Bundle.putState(value: T) {
21 | val name = value::class.java.name
22 | putParcelable(name, value)
23 | logger.d { "State:: saved $name" }
24 | }
25 |
26 | class ViewModelStateStub private constructor(@Transient val ignore: Boolean = true) : Parcelable {
27 |
28 | override fun describeContents(): Int = 0
29 |
30 | override fun writeToParcel(dest: Parcel, flags: Int) = Unit
31 |
32 | companion object {
33 | val INSTANCE = ViewModelStateStub()
34 |
35 | @Suppress("unused")
36 | val CREATOR: Parcelable.Creator = object : Parcelable.Creator {
37 |
38 | override fun createFromParcel(source: Parcel) = ViewModelStateStub()
39 |
40 | override fun newArray(size: Int): Array = arrayOfNulls(size)
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/stepango/archetype/databindings/ImageViewBindings.kt:
--------------------------------------------------------------------------------
1 | package com.stepango.archetype.databindings
2 |
3 | import android.databinding.BindingAdapter
4 | import android.support.annotation.DrawableRes
5 | import android.widget.ImageView
6 | import com.stepango.archetype.player.di.Injector
7 |
8 | @BindingAdapter(value = *arrayOf(
9 | "imageUrl",
10 | "imagePlaceholder",
11 | "imageCircle"
12 | ), requireAll = false)
13 | fun loadImage(view: ImageView, url: String?, @DrawableRes placeholder: Int, asCircle: Boolean?) {
14 | url ?: return
15 | val imageLoader = Injector().imageLoader()
16 | imageLoader.load(url).apply {
17 | if (placeholder > 0) placeholder(placeholder)
18 | if (asCircle == true) asCircle()
19 | into(view)
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/stepango/archetype/databindings/RecyclerViewBindings.kt:
--------------------------------------------------------------------------------
1 | package com.stepango.archetype.databindings
2 |
3 | import android.databinding.BindingAdapter
4 | import android.support.v7.widget.DefaultItemAnimator
5 | import android.support.v7.widget.DividerItemDecoration
6 | import android.support.v7.widget.LinearLayoutManager
7 | import android.support.v7.widget.OrientationHelper
8 | import android.support.v7.widget.RecyclerView
9 | import com.stepango.archetype.R
10 | import com.stepango.archetype.ui.SpaceItemDecoration
11 |
12 | @BindingAdapter("useDefaults")
13 | fun useDefaults(view: RecyclerView, useDefaults: Boolean) {
14 | if (!useDefaults) return
15 | view.clipToPadding = false
16 | view.apply {
17 | itemAnimator = DefaultItemAnimator()
18 | }
19 | view.layoutManager = LinearLayoutManager(view.context)
20 | }
21 |
22 | @BindingAdapter("space")
23 | fun space(view: RecyclerView, space: Float) {
24 | view.addItemDecoration(SpaceItemDecoration(space.toInt()))
25 | }
26 |
27 | @BindingAdapter("addDivider")
28 | fun addDivider(view: RecyclerView, divider: Boolean) {
29 | if (!divider) return
30 | val decoration = DividerItemDecoration(view.context, OrientationHelper.VERTICAL)
31 | decoration.setDrawable(view.context.getDrawable(R.drawable.bg_divider))
32 | view.addItemDecoration(decoration)
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stepango/archetype/databindings/ViewBindings.kt:
--------------------------------------------------------------------------------
1 | package com.stepango.archetype.databindings
2 |
3 | import android.databinding.BindingAdapter
4 | import android.view.View
5 | import com.stepango.archetype.ui.visible
6 |
7 | @BindingAdapter("visible")
8 | fun visible(v: View, visible: Boolean) {
9 | v.visible = visible
10 | }
11 |
12 | @BindingAdapter("requestFocus")
13 | fun requestFocus(v: View, focus: Boolean) {
14 | if (focus) v.post { v.requestFocus() }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/stepango/archetype/databindings/WebViewBinding.kt:
--------------------------------------------------------------------------------
1 | package com.stepango.archetype.databindings
2 |
3 | import android.databinding.BindingAdapter
4 | import android.webkit.WebSettings
5 | import android.webkit.WebView
6 |
7 |
8 | @BindingAdapter("loadData")
9 | fun loadData(webView: WebView, data: String) {
10 | webView.loadData(data, "text/html; charset=utf-8", null)
11 | }
12 |
13 | @BindingAdapter("useDefaults")
14 | fun userDefaults(webView: WebView, useDefaults: Boolean) {
15 | if (useDefaults) {
16 | webView.settings.defaultTextEncodingName = "utf-8"
17 | webView.settings.loadWithOverviewMode = true
18 | webView.settings.useWideViewPort = true
19 | webView.settings.textSize = WebSettings.TextSize.LARGEST
20 | }
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stepango/archetype/db/BaseRepos.kt:
--------------------------------------------------------------------------------
1 | package com.stepango.archetype.db
2 |
3 | import io.reactivex.Completable
4 | import io.reactivex.Single
5 |
6 | interface PullableKeyValue {
7 | // would like to use varargs but impossible for now because of
8 | // https://youtrack.jetbrains.com/issue/KT-9495
9 | /**
10 | * Sync data between source and repo by pulling data from source.
11 | * [pull] method implementation should include `save` method call to sync data
12 | *
13 | * Empty [keys] indicates that we need to pull all entities.
14 | * Probably need to add some limits description here or another method/interface
15 | * (total number of entities, number of pages, etc.)
16 | */
17 | fun pull(keys: List = emptyList()): Completable
18 |
19 | fun pull(key: Key): Completable = throw NotImplementedError()
20 | }
21 |
22 | /**
23 | * Push data to remote receiver that connected to particular repo
24 | */
25 | interface Pushable {
26 | /**
27 | * Sync data between source and repo by pushing data to source.
28 | * [push] method implementation should include `save/pull` method call to sync data
29 | */
30 | fun push(value: Value): Single
31 | }
32 |
33 | interface PullableKeyValueRepo : KeyValueRepo, PullableKeyValue
34 |
--------------------------------------------------------------------------------
/app/src/main/java/com/stepango/archetype/db/Contract.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Interfaces describe reactive Repository contract. In rxJava2 terms repository should
3 | * implement following rules:
4 | *
5 | * - `save` methods should return `Single`
6 | * - `observe` methods should return `Observable`, for back pressure handling Observable could be converted to Flowable
7 | * - `remove` methods should return Completable
8 | */
9 | package com.stepango.archetype.db
10 |
11 | import com.stepango.koptional.Optional
12 | import io.reactivex.Completable
13 | import io.reactivex.Observable
14 | import io.reactivex.Single
15 |
16 | /**
17 | * Single value repository that don't accept null as value
18 | */
19 | interface SingleValueRepo {
20 | /**
21 | * Saves [value] on given [io.reactivex.Scheduler]
22 | */
23 | fun save(value: Value): Single
24 |
25 | /**
26 | * Removes value on given [io.reactivex.Scheduler] if present
27 | */
28 | fun remove(): Completable
29 |
30 | /**
31 | * @return Observable that contains [Optional.Some] if value is present
32 | * or [Optional.EMPTY] if value is null
33 | */
34 | fun observe(): Observable>
35 | }
36 |
37 | /**
38 | * Key-Value repository that don't accept null as a `value` or `key`
39 | */
40 | interface KeyValueRepo {
41 | /**
42 | * Saves [value] by [key] on given [io.reactivex.Scheduler]
43 | */
44 | fun save(key: Key, value: Value): Single
45 |
46 | /**
47 | * Removes value by given [key] if present
48 | */
49 | fun remove(key: Key): Completable
50 |
51 | /**
52 | * @return Observable that contains [Optional.Some] if value is present
53 | * or [Optional.EMPTY] if value is null by given [key]
54 | */
55 | fun observe(key: Key): Observable>
56 |
57 | /**
58 | * Saves given [Map] of objects by it's keys
59 | * @return map of saved objects
60 | */
61 | fun save(data: Map): Single