├── .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 | [![codebeat badge](https://codebeat.co/badges/15f6548e-a136-4c0d-9f53-460166070ce6)](https://codebeat.co/projects/github-com-stepango-archetype-master) 3 | [![Build Status](https://www.bitrise.io/app/65613de01e0da309.svg?token=6-VjC_AlRy2-0Aq6W-OALw&branch=master)](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> 62 | 63 | /** 64 | * Remove values by given set of keys, if present 65 | */ 66 | fun remove(keys: Set): Completable 67 | 68 | /** 69 | * Removes all data from current repository 70 | */ 71 | fun removeAll(): Completable 72 | 73 | /** 74 | * Observe all objects changes in repository. 75 | * Object sorting not guarantied here, use `.map { sort(it) }` or custom interface extension 76 | * to achieve desired sorting order 77 | */ 78 | fun observeAll(): Observable> 79 | } 80 | 81 | /** 82 | * Key-ListOfValues repository 83 | */ 84 | interface KeyValueListRepo { 85 | /** 86 | * Saves [value] by [key] on given [io.reactivex.Scheduler] 87 | */ 88 | fun save(key: Key, value: List): Single> 89 | 90 | /** 91 | * Removes value by given [key] if present 92 | */ 93 | fun remove(key: Key): Completable 94 | 95 | /** 96 | * @return Observable that contains non empty list if value is present 97 | * or [emptyList] if no values is stored by given [key] 98 | */ 99 | fun observe(key: Key): Observable> 100 | 101 | /** 102 | * Removes all data from current repository 103 | */ 104 | fun removeAll(): Completable 105 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/db/ContractExt.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.db 2 | 3 | import com.stepango.archetype.rx.filterNonEmpty 4 | import io.reactivex.Single 5 | 6 | fun KeyValueRepo.single(key: Key): Single 7 | = observe(key).filterNonEmpty().firstOrError() 8 | 9 | fun SingleValueRepo.single(): Single 10 | = observe().filterNonEmpty().firstOrError() -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/db/RepoSupervisor.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.db 2 | 3 | import io.reactivex.Completable 4 | 5 | interface RepoSupervisor { 6 | fun clear(): Completable 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/fragment/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.fragment 2 | 3 | import android.app.Activity 4 | import android.databinding.DataBindingUtil 5 | import android.databinding.ViewDataBinding 6 | import android.os.Bundle 7 | import android.support.v4.app.Fragment 8 | import android.support.v4.widget.SwipeRefreshLayout 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import android.widget.FrameLayout 13 | import com.stepango.archetype.action.Args 14 | import com.stepango.archetype.action.argsOf 15 | import com.stepango.archetype.activity.BaseActivity 16 | import com.trello.navi2.component.NaviFragment 17 | import io.reactivex.Completable 18 | 19 | abstract class BaseFragment : NaviFragment() { 20 | 21 | var onBackPressedHandler: (activity: Activity) -> Boolean = { false } 22 | 23 | abstract val layoutId: Int 24 | 25 | lateinit var binding: T 26 | 27 | abstract fun initBinding(binding: T, state: Bundle?) 28 | 29 | val activity: BaseActivity get() = super.getActivity() as BaseActivity 30 | 31 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { 32 | super.onCreateView(inflater, container, savedInstanceState) 33 | binding = DataBindingUtil.inflate(inflater, layoutId, container, false) 34 | // Workaround for http://stackoverflow.com/questions/27057449/when-switch-fragment-with-swiperefreshlayout-during-refreshing-fragment-freezes 35 | return if (binding.root is SwipeRefreshLayout) FrameLayout(getActivity()).apply { addView(binding.root) } 36 | else binding.root 37 | } 38 | 39 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 40 | super.onViewCreated(view, savedInstanceState) 41 | initBinding(binding, savedInstanceState) 42 | } 43 | 44 | } 45 | 46 | val Fragment.args: Args get () = arguments ?: Bundle() 47 | 48 | inline fun > showFragment( 49 | activity: BaseActivity, 50 | rootFragmentId: Int, 51 | containerId: Int, 52 | args: Args = argsOf() 53 | ): Completable = Completable.fromCallable { 54 | val rootFragment = findFragment(activity, rootFragmentId) 55 | rootFragment?.let { 56 | val fragment = findChildFragment(containerId, it) 57 | if (fragment !is T) T::class.java.newInstance().replaceIn(fragment = rootFragment, containerId = containerId, args = args) 58 | } ?: throw IllegalArgumentException() 59 | } 60 | 61 | fun findChildFragment(containerId: Int, it: BaseFragment<*>) = it.childFragmentManager?.findFragmentById(containerId) 62 | 63 | fun findFragment(activity: BaseActivity, containerId: Int) = activity.fragmentManager.findFragmentById(containerId) as? BaseFragment<*> -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/fragment/FragmentExt.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.fragment 2 | 3 | import android.app.Activity 4 | import android.app.Fragment 5 | import android.app.FragmentManager 6 | import android.os.Bundle 7 | import android.support.annotation.IdRes 8 | import com.stepango.archetype.action.Args 9 | import com.stepango.archetype.action.argsOf 10 | 11 | fun Fragment.replaceIn( 12 | activity: Activity, 13 | @IdRes containerId: Int = android.R.id.content, 14 | map: Args = argsOf() 15 | ) { 16 | replace(activity.intent?.extras?.apply { putAll(map) }, containerId, activity.fragmentManager) 17 | } 18 | 19 | fun Fragment.replaceIn( 20 | fragment: BaseFragment<*>, 21 | @IdRes containerId: Int, 22 | args: Args = argsOf() 23 | ) { 24 | replace(fragment.arguments?.apply { putAll(args) }, containerId, fragment.childFragmentManager) 25 | } 26 | 27 | private fun Fragment.replace(bundle: Bundle?, containerId: Int, fm: FragmentManager) { 28 | val fmt = this 29 | bundle?.let { if (fmt.arguments == null) fmt.arguments = it else fmt.arguments.putAll(it) } 30 | fm.beginTransaction() 31 | .replace(containerId, fmt) 32 | .commit() 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/glide/GlideImageLoader.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.glide 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.support.v4.content.ContextCompat 6 | import android.widget.ImageView 7 | import com.bumptech.glide.GenericRequestBuilder 8 | import com.bumptech.glide.Glide 9 | import com.bumptech.glide.load.Transformation 10 | import com.bumptech.glide.load.engine.DiskCacheStrategy 11 | import com.bumptech.glide.load.resource.bitmap.FitCenter 12 | import com.stepango.archetype.image.CropCircleTransformation 13 | import com.stepango.archetype.image.ImageLoader 14 | import com.stepango.archetype.util.dp 15 | import java.util.ArrayList 16 | 17 | class GlideImageLoader : ImageLoader { 18 | 19 | override fun load(url: String?): Request = Request(url = url) 20 | override fun load(drawableId: Int): Request = Request(drawableId = drawableId) 21 | 22 | class Request @JvmOverloads constructor( 23 | url: String? = null, 24 | drawableId: Int = 0 25 | ) : ImageLoader.Request(url, drawableId) { 26 | 27 | override fun bitmap(context: Context, width: Float, height: Float): Bitmap 28 | = buildBitmapRequest(context).into(width.dp(context), height.dp(context)).get() 29 | 30 | override fun into(imageView: ImageView) { 31 | buildDrawableRequest(imageView.context).into(imageView) 32 | } 33 | 34 | private fun buildBitmapRequest(ctx: Context) = makeRequest(ctx).asBitmap().apply { 35 | applyTransformations(ctx).apply { if (isNotEmpty()) transform(*this) } 36 | } 37 | 38 | 39 | private fun buildDrawableRequest(ctx: Context) = makeRequest(ctx).apply { 40 | applyTransformations(ctx).apply { if (isNotEmpty()) bitmapTransform(*this) } 41 | } 42 | 43 | private fun makeRequest(context: Context) = Glide.with(context).let { 44 | when { 45 | url != null -> it.load(url) 46 | drawableId > 0 -> it.load(drawableId) 47 | else -> throw IllegalArgumentException() 48 | } 49 | } 50 | 51 | private fun GenericRequestBuilder<*, *, *, *>.applyTransformations(ctx: Context): Array> { 52 | diskCacheStrategy(DiskCacheStrategy.ALL) 53 | val transformations: MutableList> = ArrayList() 54 | if (placeholderId > 0) placeholder(ContextCompat.getDrawable(ctx, placeholderId)) 55 | if (!isAsCircle) { 56 | transformations += FitCenter(ctx) 57 | } 58 | if (isAsCircle) transformations += CropCircleTransformation(ctx) 59 | return transformations.toTypedArray() 60 | } 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/image/CropCircleTransformation.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.image 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapShader 6 | import android.graphics.Canvas 7 | import android.graphics.Matrix 8 | import android.graphics.Paint 9 | import android.graphics.Shader 10 | import com.bumptech.glide.Glide 11 | import com.bumptech.glide.load.Transformation 12 | import com.bumptech.glide.load.engine.Resource 13 | import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool 14 | import com.bumptech.glide.load.resource.bitmap.BitmapResource 15 | 16 | class CropCircleTransformation(private val mBitmapPool: BitmapPool) : Transformation { 17 | 18 | constructor(context: Context) : this(Glide.get(context).bitmapPool) 19 | 20 | override fun transform(resource: Resource, outWidth: Int, outHeight: Int): Resource { 21 | val source = resource.get() 22 | val size = Math.min(source.width, source.height) 23 | 24 | val width = (source.width - size) / 2 25 | val height = (source.height - size) / 2 26 | 27 | var bitmap: Bitmap? = mBitmapPool.get(size, size, Bitmap.Config.ARGB_8888) 28 | if (bitmap == null) { 29 | bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) 30 | } 31 | 32 | val canvas = Canvas(bitmap!!) 33 | val paint = Paint() 34 | val shader = BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) 35 | if (width != 0 || height != 0) { 36 | // source isn't square, move viewport to center 37 | val matrix = Matrix() 38 | matrix.setTranslate((-width).toFloat(), (-height).toFloat()) 39 | shader.setLocalMatrix(matrix) 40 | } 41 | paint.shader = shader 42 | paint.isAntiAlias = true 43 | 44 | val r = size / 2f 45 | canvas.drawCircle(r, r, r, paint) 46 | 47 | return BitmapResource.obtain(bitmap, mBitmapPool) 48 | } 49 | 50 | override fun getId() = "CropCircleTransformation()" 51 | 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/image/ImageLoader.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.image 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.support.annotation.DrawableRes 6 | import android.widget.ImageView 7 | 8 | /** 9 | * Simple image loader interface that encapsulates features we need now 10 | */ 11 | interface ImageLoader { 12 | 13 | /** 14 | * Loads from provided url, path or uri represented as String 15 | * @param url path to image 16 | * @return configurable Request class implementation 17 | */ 18 | fun load(url: String?): Request 19 | 20 | /** 21 | * Loads from provided drawableId 22 | 23 | * @param drawableId - id of drawable 24 | * * 25 | * @return configurable Request class implementation 26 | */ 27 | fun load(@DrawableRes drawableId: Int): Request 28 | 29 | /** 30 | * Configurable image loading Request class made with Builder pattern 31 | * Descendants must implement several `into` methods using user-provided configuration 32 | */ 33 | abstract class Request(protected var url: String? = null, @DrawableRes protected var drawableId: Int = 0) { 34 | protected var isAsCircle: Boolean = false 35 | @DrawableRes protected var placeholderId: Int = 0 36 | @DrawableRes protected var errorId: Int = 0 37 | 38 | fun placeholder(@DrawableRes placeholderId: Int): Request = apply { 39 | this.placeholderId = placeholderId 40 | } 41 | 42 | fun error(@DrawableRes errorId: Int): Request = apply { this.errorId = errorId } 43 | 44 | fun asCircle(): Request = apply { isAsCircle = true } 45 | 46 | abstract fun into(imageView: ImageView) 47 | 48 | abstract fun bitmap(context: Context, width: Float, height: Float): Bitmap 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/logger/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.logger 2 | 3 | interface Logger { 4 | 5 | val isDebugEnabled: Boolean 6 | get() = false 7 | val isInfoEnabled: Boolean 8 | get() = false 9 | val isWarnEnabled: Boolean 10 | get() = false 11 | val isErrorEnabled: Boolean 12 | get() = false 13 | 14 | fun v(message: String) 15 | 16 | fun d(message: String) 17 | 18 | fun i(message: String) 19 | 20 | fun w(message: String) 21 | 22 | fun e(message: String) 23 | 24 | fun e(e: Throwable?, message: String) 25 | } 26 | 27 | /** 28 | * Lazy add a log message if isDebugEnabled is true 29 | */ 30 | inline fun Logger.d(msg: () -> Any?) 31 | = if (isDebugEnabled) d(msg().toString()) else Unit 32 | 33 | /** 34 | * Lazy add a log message if isInfoEnabled is true 35 | */ 36 | inline fun Logger.i(msg: () -> Any?) 37 | = if (isInfoEnabled) i(msg().toString()) else Unit 38 | 39 | /** 40 | * Lazy add a log message if isWarnEnabled is true 41 | */ 42 | inline fun Logger.w(msg: () -> Any?) 43 | = if (isWarnEnabled) w(msg().toString()) else Unit 44 | 45 | /** 46 | * Lazy add a log message if isErrorEnabled is true 47 | */ 48 | inline fun Logger.e(msg: () -> Any?) 49 | = if (isErrorEnabled) e(msg().toString()) else Unit 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/logger/LoggerExt.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.logger 2 | 3 | import com.stepango.archetype.player.di.lazyInject 4 | 5 | val loggerImpl by lazyInject { logger() } 6 | 7 | @Suppress("unused") val Any.logger: Logger 8 | get() = loggerImpl 9 | 10 | inline fun Throwable?.logError(block: () -> Any?) = loggerImpl.e(this, block().toString()) 11 | fun String?.logDebug(tag: String) = loggerImpl.d("$tag:: $this") 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/logger/SimpleLogger.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.logger 2 | 3 | import android.util.Log 4 | import com.stepango.archetype.BuildConfig 5 | import com.stepango.archetype.logger.Logger 6 | 7 | class SimpleLogger : Logger { 8 | 9 | override val isDebugEnabled: Boolean = BuildConfig.DEBUG 10 | override val isInfoEnabled: Boolean = BuildConfig.DEBUG 11 | override val isWarnEnabled: Boolean = BuildConfig.DEBUG 12 | override val isErrorEnabled: Boolean = BuildConfig.DEBUG 13 | 14 | override fun v(message: String) { 15 | Log.v("Logger", message) 16 | } 17 | 18 | override fun d(message: String) { 19 | Log.d("Logger", message) 20 | } 21 | 22 | override fun i(message: String) { 23 | Log.i("Logger", message) 24 | } 25 | 26 | override fun w(message: String) { 27 | Log.w("Logger", message) 28 | } 29 | 30 | override fun e(message: String) { 31 | Log.e("Logger", message) 32 | } 33 | 34 | override fun e(e: Throwable?, message: String) { 35 | Log.e("Logger", "$message. Message: ${e?.message}") 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/ArgsExt.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player 2 | 3 | import com.stepango.archetype.action.Args 4 | 5 | 6 | const val EPISODE_ID = "$PREFIX.episode_id" 7 | 8 | fun Args.episodeId() = this.getLong(EPISODE_ID) 9 | inline fun Args.episodeId(block: () -> Long) = apply { this.putLong(EPISODE_ID, block()) } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player 2 | 3 | import com.stepango.archetype.BuildConfig 4 | 5 | const val PREFIX = BuildConfig.APPLICATION_ID -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/data/db/Repos.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.data.db 2 | 3 | import com.stepango.archetype.action.ContextActionHandler 4 | import com.stepango.archetype.db.KeyValueRepo 5 | import com.stepango.archetype.player.data.db.model.EpisodesModel 6 | import io.reactivex.Completable 7 | 8 | interface EpisodesModelRepo : KeyValueRepo { 9 | 10 | fun pull(): Completable 11 | 12 | fun refreshFiles(ah: ContextActionHandler): Completable 13 | } 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/data/db/memory/InMemoryKeyValueRepo.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.data.db.memory 2 | 3 | import android.databinding.ObservableArrayMap 4 | import android.databinding.ObservableMap.OnMapChangedCallback 5 | import com.stepango.archetype.db.KeyValueRepo 6 | import com.stepango.koptional.Optional 7 | import com.stepango.koptional.toOptional 8 | import io.reactivex.Completable 9 | import io.reactivex.Observable 10 | import io.reactivex.Single 11 | import io.reactivex.subjects.BehaviorSubject 12 | import kotlin.reflect.KClass 13 | 14 | inline fun InMemoryKeyValueRepo(): InMemoryKeyValueRepo = InMemoryKeyValueRepo(Value::class) 15 | 16 | class InMemoryKeyValueRepo(val valClass: KClass) : KeyValueRepo { 17 | 18 | private val map = ObservableArrayMap() 19 | private val publisher: BehaviorSubject> = BehaviorSubject.createDefault(map) 20 | 21 | override fun save(key: Key, value: Value): Single = Single.just(value).doOnSuccess { 22 | map[key] = value 23 | triggerObserveAllNotification() 24 | } 25 | 26 | override fun save(data: Map): Single> { 27 | map.putAll(data) 28 | triggerObserveAllNotification() 29 | return Single.just(map) 30 | } 31 | 32 | override fun removeAll(): Completable = Completable.fromAction { 33 | map.clear() 34 | triggerObserveAllNotification() 35 | } 36 | 37 | private fun triggerObserveAllNotification() { 38 | publisher.onNext(map) 39 | } 40 | 41 | override fun observeAll(): Observable> = publisher 42 | .map { it.values.toList() } 43 | 44 | 45 | override fun remove(key: Key): Completable = Completable.fromAction { 46 | map.remove(key) 47 | triggerObserveAllNotification() 48 | } 49 | 50 | override fun observe(key: Key): Observable> = Observable.create { s -> 51 | val observer = object : OnMapChangedCallback, Key, Value>() { 52 | override fun onMapChanged(sender: ObservableArrayMap, updatedKey: Key) { 53 | try { 54 | if (key == updatedKey) s.onNext(sender[key].toOptional()) 55 | } catch (e: Exception) { 56 | s.onError(e) 57 | } 58 | } 59 | } 60 | map.addOnMapChangedCallback(observer) 61 | s.setCancellable { map.removeOnMapChangedCallback(observer) } 62 | s.onNext(map[key].toOptional()) 63 | } 64 | 65 | 66 | override fun remove(keys: Set): Completable = Completable.fromAction { 67 | keys.forEach { map.remove(it) } 68 | triggerObserveAllNotification() 69 | } 70 | 71 | 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/data/db/memory/InMemoryRepos.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.data.db.memory 2 | 3 | import com.stepango.archetype.action.ContextActionHandler 4 | import com.stepango.archetype.db.KeyValueRepo 5 | import com.stepango.archetype.player.data.db.EpisodesModelRepo 6 | import com.stepango.archetype.player.data.db.model.EpisodesModel 7 | import com.stepango.archetype.player.loader.RefreshDownloadedAction 8 | import com.stepango.archetype.player.network.get.GetEpisodesRequest 9 | import io.reactivex.Completable 10 | 11 | class InMemoryEpisodesRepo( 12 | val refreshDownloadedAction: RefreshDownloadedAction 13 | ) : 14 | EpisodesModelRepo, 15 | KeyValueRepo by InMemoryKeyValueRepo() { 16 | override fun pull(): Completable = 17 | GetEpisodesRequest().execute() 18 | .flatMap { 19 | save(it.associateBy({ it.id }) { it }) 20 | } 21 | .toCompletable() 22 | 23 | override fun refreshFiles(ah: ContextActionHandler) = ah.createAction(refreshDownloadedAction, Unit) 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/data/db/model/EpisodesModel.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.data.db.model 2 | 3 | import com.stepango.archetype.R 4 | import io.mironov.smuggler.AutoParcelable 5 | 6 | data class EpisodesModel( 7 | val name: String, 8 | val summary: String, 9 | val iconUrl: String, 10 | val audioUrl: String, 11 | val content: String?, 12 | var state: EpisodeDownloadState = EpisodeDownloadState.DOWNLOAD, 13 | var file: String? = null, 14 | val id: Long = name.hashCode().toLong() 15 | ) : AutoParcelable 16 | 17 | enum class EpisodeDownloadState { 18 | DOWNLOAD { 19 | override val action = R.id.action_download_episode 20 | override val textId = R.string.action_download_episode 21 | }, 22 | 23 | WAIT { 24 | override val action = R.id.action_idle 25 | override val textId = R.string.action_wait_for_download 26 | }, 27 | 28 | CANCEL { 29 | override val action = R.id.action_cancel_download_episode 30 | override val textId = R.string.action_cancel_download_episode 31 | }, 32 | 33 | RETRY { 34 | override val action = R.id.action_download_episode 35 | override val textId = R.string.action_retry_download_episode 36 | }; 37 | 38 | abstract val action: Int 39 | abstract val textId: Int 40 | fun isWait(): Boolean = this == WAIT 41 | fun textId(): Int = if (textId != 0) textId else R.id.action_download_episode 42 | } 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/data/db/response/feed/Channel.java: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.data.db.response.feed; 2 | 3 | 4 | import org.simpleframework.xml.Element; 5 | import org.simpleframework.xml.ElementList; 6 | import org.simpleframework.xml.Root; 7 | 8 | import java.util.List; 9 | 10 | @Root(strict = false) 11 | public class Channel { 12 | 13 | @Element 14 | public String title; 15 | 16 | @ElementList(inline = true) 17 | public List item; 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/data/db/response/feed/Enclosure.java: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.data.db.response.feed; 2 | 3 | 4 | import org.simpleframework.xml.Attribute; 5 | import org.simpleframework.xml.Root; 6 | 7 | @Root(strict = false) 8 | public class Enclosure { 9 | 10 | @Attribute(name = "url") 11 | public String url; 12 | 13 | @Attribute(name = "type") 14 | public String type; 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/data/db/response/feed/Image.java: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.data.db.response.feed; 2 | 3 | 4 | import org.simpleframework.xml.Attribute; 5 | import org.simpleframework.xml.Root; 6 | 7 | @Root(strict = false) 8 | public class Image { 9 | 10 | @Attribute(name = "href") 11 | public String href; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/data/db/response/feed/Item.java: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.data.db.response.feed; 2 | 3 | 4 | import android.support.annotation.Nullable; 5 | 6 | import org.simpleframework.xml.Element; 7 | import org.simpleframework.xml.Namespace; 8 | import org.simpleframework.xml.Root; 9 | 10 | @Root(strict = false) 11 | public class Item { 12 | 13 | @Element 14 | public String title; 15 | 16 | @Namespace(prefix = "content") 17 | @Element(name = "encoded", required = false) 18 | @Nullable 19 | public String content; 20 | 21 | @Namespace(prefix = "itunes") 22 | @Element(name = "summary") 23 | public String summary; 24 | 25 | @Namespace(prefix = "itunes") 26 | @Element(name = "enclosure") 27 | public Enclosure enclosure; 28 | 29 | @Element(name = "image") 30 | public Image image; 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/data/db/response/feed/Rss.java: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.data.db.response.feed; 2 | 3 | 4 | import org.simpleframework.xml.Element; 5 | import org.simpleframework.xml.Root; 6 | 7 | @Root(strict = false) 8 | public class Rss { 9 | 10 | @Element 11 | public Channel channel; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/data/wrappers/Wrappers.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.data.wrappers 2 | 3 | import com.github.nitrico.lastadapter.Type 4 | import com.stepango.archetype.R 5 | import com.stepango.archetype.action.ActionData 6 | import com.stepango.archetype.action.ContextActionHandler 7 | import com.stepango.archetype.action.execute 8 | import com.stepango.archetype.action.noParams 9 | import com.stepango.archetype.action.with 10 | import com.stepango.archetype.databinding.ItemEpisodeBinding 11 | import com.stepango.archetype.player.data.db.model.EpisodeDownloadState 12 | import com.stepango.archetype.player.data.db.model.EpisodesModel 13 | import com.stepango.archetype.player.loader.CancelDownloadEpisodeAction 14 | import com.stepango.archetype.player.loader.DownloadEpisodeAction 15 | import com.stepango.archetype.player.loader.DownloadEpisodeActionParams 16 | import com.stepango.archetype.player.ui.player.ShowEpisodeAction 17 | import com.stepango.archetype.player.ui.player.ShowEpisodeActionParams 18 | import com.stepango.archetype.ui.LastAdapterItem 19 | import com.stepango.archetype.util.firstLine 20 | import com.stepango.archetype.util.linesCount 21 | 22 | interface EpisodeListItemWrapperFabric { 23 | fun wrap(models: List): List 24 | } 25 | 26 | class EpisodeListItemWrapperFabricImpl( 27 | private val ah: ContextActionHandler, 28 | private val showEpisodeAction: ShowEpisodeAction, 29 | private val cancelDownloadEpisodeAction: CancelDownloadEpisodeAction, 30 | private val downloadEpisodeAction: DownloadEpisodeAction 31 | ) : EpisodeListItemWrapperFabric { 32 | override fun wrap(models: List): List = models.map { 33 | EpisodeListItemWrapper(it, ah, showEpisodeAction, cancelDownloadEpisodeAction, downloadEpisodeAction) 34 | } 35 | } 36 | 37 | class EpisodeListItemWrapper( 38 | model: EpisodesModel, 39 | private val ah: ContextActionHandler, 40 | private val showEpisodeAction: ShowEpisodeAction, 41 | private val cancelDownloadEpisodeAction: CancelDownloadEpisodeAction, 42 | private val downloadEpisodeAction: DownloadEpisodeAction 43 | ) : LastAdapterItem { 44 | override val stableId: Long = model.id 45 | val name: String = model.name 46 | val imageUrl: String = model.iconUrl 47 | val state: EpisodeDownloadState = model.state 48 | val isDownloaded: Boolean = model.file != null 49 | 50 | override fun getBindingType() = Type(R.layout.item_episode) 51 | 52 | fun open() = showEpisodeAction.with(ShowEpisodeActionParams(episodeId = stableId)).execute(ah) 53 | 54 | fun action() { 55 | when (state) { 56 | EpisodeDownloadState.WAIT -> ActionData.IDLE 57 | EpisodeDownloadState.CANCEL -> cancelDownloadEpisodeAction.noParams() 58 | EpisodeDownloadState.DOWNLOAD, 59 | EpisodeDownloadState.RETRY -> downloadEpisodeAction.with(DownloadEpisodeActionParams(stableId)) 60 | }.execute(ah) 61 | } 62 | } 63 | 64 | 65 | interface EpisodeItemWrapperFabric { 66 | fun wrap(model: EpisodesModel): EpisodeWrapper 67 | } 68 | 69 | class EpisodeItemWrapperFabricImpl( 70 | private val ah: ContextActionHandler, 71 | private val cancelDownloadEpisodeAction: CancelDownloadEpisodeAction, 72 | private val downloadEpisodeAction: DownloadEpisodeAction 73 | ) : EpisodeItemWrapperFabric { 74 | override fun wrap(model: EpisodesModel): EpisodeWrapper = EpisodeWrapper( 75 | model, 76 | cancelDownloadEpisodeAction, 77 | downloadEpisodeAction, 78 | ah) 79 | } 80 | 81 | class EpisodeWrapper( 82 | model: EpisodesModel, 83 | private val cancelDownloadEpisodeAction: CancelDownloadEpisodeAction, 84 | private val downloadEpisodeAction: DownloadEpisodeAction, 85 | private val ah: ContextActionHandler 86 | ) { 87 | val episodeId: Long = model.id 88 | val name: String = model.name 89 | val summary: String = model.summary.run { if (this.linesCount() > 1) this.firstLine() else this } 90 | val content: String = model.content ?: model.summary.run { if (this.linesCount() > 1) this else "" } 91 | val audioUrl: String = model.audioUrl 92 | val state: EpisodeDownloadState = model.state 93 | val file: String? = model.file 94 | val isDownloaded: Boolean = model.file != null 95 | 96 | fun action() { 97 | when (state) { 98 | EpisodeDownloadState.WAIT -> ActionData.IDLE 99 | EpisodeDownloadState.CANCEL -> cancelDownloadEpisodeAction.noParams() 100 | EpisodeDownloadState.DOWNLOAD, 101 | EpisodeDownloadState.RETRY -> downloadEpisodeAction.with(DownloadEpisodeActionParams(episodeId)) 102 | }.execute(ah) 103 | } 104 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/di/Injector.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.di 2 | 3 | import android.content.Context 4 | import com.stepango.archetype.App 5 | import com.stepango.archetype.BuildConfig 6 | import com.stepango.archetype.action.ContextActionHandler 7 | import com.stepango.archetype.action.ContextActionHandlerRealFactory 8 | import com.stepango.archetype.action.IntentMaker 9 | import com.stepango.archetype.action.IntentMakerImpl 10 | import com.stepango.archetype.action.argsOf 11 | import com.stepango.archetype.fragment.BaseFragment 12 | import com.stepango.archetype.glide.GlideImageLoader 13 | import com.stepango.archetype.image.ImageLoader 14 | import com.stepango.archetype.logger.Logger 15 | import com.stepango.archetype.logger.SimpleLogger 16 | import com.stepango.archetype.player.data.db.EpisodesModelRepo 17 | import com.stepango.archetype.player.data.db.memory.InMemoryEpisodesRepo 18 | import com.stepango.archetype.player.data.wrappers.EpisodeItemWrapperFabric 19 | import com.stepango.archetype.player.data.wrappers.EpisodeItemWrapperFabricImpl 20 | import com.stepango.archetype.player.data.wrappers.EpisodeListItemWrapperFabric 21 | import com.stepango.archetype.player.data.wrappers.EpisodeListItemWrapperFabricImpl 22 | import com.stepango.archetype.player.loader.CancelDownloadEpisodeAction 23 | import com.stepango.archetype.player.loader.CancelDownloadEpisodeActionImpl 24 | import com.stepango.archetype.player.loader.DownloadEpisodeAction 25 | import com.stepango.archetype.player.loader.DownloadEpisodeActionImpl 26 | import com.stepango.archetype.player.loader.RefreshDownloadedFilesActionImpl 27 | import com.stepango.archetype.player.network.Api 28 | import com.stepango.archetype.player.network.get.BASE_URL 29 | import com.stepango.archetype.player.network.get.WEB_SERVICE_TIMEOUT 30 | import com.stepango.archetype.player.ui.additional.MockToaster 31 | import com.stepango.archetype.player.ui.episodes.EpisodesUseCase 32 | import com.stepango.archetype.player.ui.episodes.EpisodesUseCaseImpl 33 | import com.stepango.archetype.player.ui.player.PlayerComponent 34 | import com.stepango.archetype.player.ui.player.PlayerComponentImpl 35 | import com.stepango.archetype.player.ui.player.ShowEpisodeActionImpl 36 | import com.stepango.archetype.rx.CompositeDisposableHolder 37 | import com.stepango.archetype.rx.CompositeDisposableHolderImpl 38 | import com.stepango.archetype.ui.Toaster 39 | import com.stepango.archetype.viewmodel.LoaderHolder 40 | import com.stepango.archetype.viewmodel.LoaderHolderImpl 41 | import com.stepango.archetype.viewmodel.LoadingProgressHelper 42 | import com.stepango.archetype.viewmodel.LoadingProgressHelperImpl 43 | import com.stepango.archetype.viewmodel.RxLifecycle 44 | import com.stepango.archetype.viewmodel.RxLifecycleImpl 45 | import com.stepango.archetype.viewmodel.ViewModel 46 | import com.stepango.archetype.viewmodel.ViewModelImpl 47 | import com.trello.navi2.Event 48 | import com.trello.navi2.NaviComponent 49 | import okhttp3.Interceptor 50 | import okhttp3.OkHttpClient 51 | import okhttp3.logging.HttpLoggingInterceptor 52 | import retrofit2.Retrofit 53 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 54 | import retrofit2.converter.simplexml.SimpleXmlConverterFactory 55 | import java.util.concurrent.TimeUnit 56 | 57 | 58 | val Any.injector: Injector by lazy { InjectorImpl(App.instance) } 59 | 60 | inline fun lazyInject(crossinline block: Injector.() -> T): Lazy = lazy { Injector().block() } 61 | 62 | interface Injector { 63 | fun logger(): Logger 64 | 65 | fun contextActionHandler(context: Context, compositeDisposableHolder: CompositeDisposableHolder): ContextActionHandler 66 | 67 | fun compositeDisposableHolder(): CompositeDisposableHolder = CompositeDisposableHolderImpl() 68 | 69 | fun intentMaker(): IntentMaker 70 | 71 | //region ui 72 | fun imageLoader(): ImageLoader 73 | 74 | fun toaster(): Toaster 75 | 76 | fun loaderHolder(): LoaderHolder = LoaderHolderImpl() 77 | 78 | fun loadingProgressHelper(): LoadingProgressHelper = LoadingProgressHelperImpl() 79 | //endregion 80 | 81 | //region media 82 | fun player(): PlayerComponent 83 | //endregion 84 | 85 | //region repositories 86 | fun episodesRepo(): EpisodesModelRepo 87 | //endregion 88 | 89 | //region usecase 90 | fun episodesUseCase(): EpisodesUseCase 91 | //endregion 92 | 93 | //region wrapper 94 | fun episodeListItemWrapperFabric(contextActionHandler: ContextActionHandler): EpisodeListItemWrapperFabric 95 | 96 | fun episodeItemWrapperFabric(contextActionHandler: ContextActionHandler): EpisodeItemWrapperFabric 97 | //endregion 98 | 99 | //region network 100 | fun apiService(): Api 101 | 102 | //endregion 103 | 104 | //region fragment 105 | 106 | fun rxlc(naviComponent: NaviComponent, compositeDisposableHolder: CompositeDisposableHolder): RxLifecycle 107 | 108 | fun vm(fragment: BaseFragment<*>): ViewModel 109 | 110 | //endregion 111 | 112 | 113 | companion 114 | 115 | object { 116 | operator fun invoke(): Injector = injector 117 | } 118 | } 119 | 120 | class InjectorImpl(val app: App) : Injector { 121 | 122 | private object logger : Logger by SimpleLogger() 123 | 124 | private val actionHandlerfactory by lazy { ContextActionHandlerRealFactory() } 125 | 126 | override fun logger(): Logger = logger 127 | 128 | override fun contextActionHandler(context: Context, compositeDisposableHolder: CompositeDisposableHolder): ContextActionHandler = 129 | actionHandlerfactory.createActionHandler(context, compositeDisposableHolder) 130 | 131 | override fun intentMaker() = IntentMakerImpl() 132 | 133 | //region media 134 | override fun player() = PlayerComponentImpl(app) 135 | //endregion 136 | 137 | //region ui 138 | override fun imageLoader() = GlideImageLoader() 139 | 140 | //TODO: replace by SimpleToaster with context 141 | override fun toaster(): Toaster = MockToaster() 142 | //endregion 143 | 144 | //region actions 145 | private fun refreshDownloadedFilesAction() = RefreshDownloadedFilesActionImpl() 146 | 147 | private fun showEpisodeAction() = ShowEpisodeActionImpl() 148 | private fun cancelDownloadEpisodeAction(): CancelDownloadEpisodeAction = CancelDownloadEpisodeActionImpl() 149 | private fun downloadEpisodeAction(): DownloadEpisodeAction = DownloadEpisodeActionImpl(intentMaker()) 150 | //endregion 151 | 152 | //region repositories 153 | 154 | private val episodesRepo: EpisodesModelRepo by lazy { 155 | InMemoryEpisodesRepo(refreshDownloadedFilesAction()) 156 | } 157 | 158 | override fun episodesRepo(): EpisodesModelRepo = episodesRepo 159 | //endregion 160 | 161 | //region usecase 162 | override fun episodesUseCase(): EpisodesUseCase = EpisodesUseCaseImpl(episodesRepo()) 163 | //endregion 164 | 165 | //region wrappers 166 | override fun episodeListItemWrapperFabric(contextActionHandler: ContextActionHandler): EpisodeListItemWrapperFabric = 167 | EpisodeListItemWrapperFabricImpl(contextActionHandler, showEpisodeAction(), cancelDownloadEpisodeAction(), downloadEpisodeAction()) 168 | 169 | override fun episodeItemWrapperFabric(contextActionHandler: ContextActionHandler): EpisodeItemWrapperFabric = 170 | EpisodeItemWrapperFabricImpl(contextActionHandler, cancelDownloadEpisodeAction(), downloadEpisodeAction()) 171 | //endregion 172 | 173 | //region network 174 | override fun apiService(): Api = service 175 | 176 | private var okHttpClient: OkHttpClient = httpClient() 177 | 178 | private fun httpClient(): OkHttpClient { 179 | val okHttpClientBuilder = OkHttpClient.Builder() 180 | .readTimeout(WEB_SERVICE_TIMEOUT, TimeUnit.MILLISECONDS) 181 | .connectTimeout(WEB_SERVICE_TIMEOUT, TimeUnit.MILLISECONDS) 182 | if (BuildConfig.DEBUG) { 183 | okHttpClientBuilder.interceptors().add(logInterceptor()) 184 | } 185 | return okHttpClientBuilder.build() 186 | } 187 | 188 | private val restAdapter = Retrofit.Builder() 189 | .baseUrl(BASE_URL) 190 | .client(okHttpClient) 191 | .addConverterFactory(SimpleXmlConverterFactory.create()) 192 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 193 | .build() 194 | 195 | private var service: Api = restAdapter.create(Api::class.java) 196 | 197 | private fun logInterceptor(): Interceptor { 198 | val interceptor = HttpLoggingInterceptor() 199 | interceptor.level = HttpLoggingInterceptor.Level.BODY 200 | return interceptor 201 | } 202 | //endregion 203 | 204 | //region fragment 205 | 206 | override fun rxlc( 207 | naviComponent: NaviComponent, 208 | compositeDisposableHolder: CompositeDisposableHolder 209 | ): RxLifecycle = RxLifecycleImpl( 210 | naviComponent = naviComponent, 211 | compositeDisposableHolder = compositeDisposableHolder, 212 | startEvent = Event.START, 213 | stopEvent = Event.STOP, 214 | detachEvent = Event.DETACH 215 | ) 216 | 217 | override fun vm(fragment: BaseFragment<*>): ViewModel = compositeDisposableHolder().let { compositeDisposableHolder -> 218 | ViewModelImpl( 219 | rxLifecycle = rxlc(fragment, compositeDisposableHolder), 220 | loaderHolder = loaderHolder(), 221 | loadingProgressHelper = loadingProgressHelper(), 222 | args = fragment.arguments ?: argsOf(), 223 | toaster = toaster(), 224 | actionHandler = contextActionHandler(fragment.activity, compositeDisposableHolder) 225 | ) 226 | } 227 | 228 | //endregion 229 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/loader/DownloadEpisodeAction.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.loader 2 | 3 | import android.content.Context 4 | import com.stepango.archetype.action.ContextAction 5 | import com.stepango.archetype.action.IntentAction 6 | import com.stepango.archetype.action.IntentMaker 7 | import com.stepango.archetype.action.startBroadcast 8 | import com.stepango.archetype.action.startService 9 | import com.stepango.archetype.player.data.db.model.EpisodesModel 10 | import com.stepango.archetype.player.di.Injector 11 | import com.stepango.archetype.player.di.lazyInject 12 | import com.stepango.archetype.util.getFileName 13 | import io.reactivex.Completable 14 | import io.reactivex.Observable 15 | import java.io.File 16 | 17 | /** 18 | * Wild, 02.07.2017. 19 | */ 20 | 21 | interface RefreshDownloadedAction : ContextAction 22 | 23 | class RefreshDownloadedFilesActionImpl : RefreshDownloadedAction { 24 | 25 | val episodesRepo by lazyInject { episodesRepo() } 26 | 27 | fun getAllEpisodes(): Observable = episodesRepo.observeAll() 28 | .take(1) 29 | .flatMapIterable { it } 30 | 31 | fun checkEpisodeFile(episode: EpisodesModel, file: File): Completable { 32 | if (!file.exists()) 33 | return Completable.complete() 34 | 35 | return episodesRepo 36 | .save(episode.id, episode.copy(file = file.absolutePath)) 37 | .toCompletable() 38 | } 39 | 40 | override fun invoke(context: Context, params: Unit): Completable { 41 | return getAllEpisodes() 42 | .flatMapCompletable { 43 | val file = File(context.filesDir, getFileName(it.audioUrl)) 44 | return@flatMapCompletable checkEpisodeFile(it, file) 45 | } 46 | } 47 | } 48 | 49 | interface DownloadEpisodeAction : IntentAction 50 | class DownloadEpisodeActionParams(val episodeId: Long) 51 | 52 | class DownloadEpisodeActionImpl( 53 | val intentMaker: IntentMaker 54 | ) : DownloadEpisodeAction { 55 | 56 | override fun invoke(context: Context, params: DownloadEpisodeActionParams): Completable = 57 | EpisodeLoaderService.intent(params.episodeId, intentMaker, context).startService(context) 58 | } 59 | 60 | interface CancelDownloadEpisodeAction : IntentAction 61 | 62 | class CancelDownloadEpisodeActionImpl : CancelDownloadEpisodeAction, IntentMaker by Injector().intentMaker() { 63 | 64 | override fun invoke(context: Context, params: Unit): Completable = startBroadcast(context, CANCEL_DOWNLOAD_ACTION) 65 | 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/loader/EpisodeLoaderService.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.loader 2 | 3 | import android.app.Notification 4 | import android.app.NotificationManager 5 | import android.app.PendingIntent 6 | import android.app.Service 7 | import android.content.BroadcastReceiver 8 | import android.content.Context 9 | import android.content.Intent 10 | import android.content.IntentFilter 11 | import android.os.IBinder 12 | import android.support.v4.app.NotificationCompat 13 | import com.stepango.archetype.R 14 | import com.stepango.archetype.action.Args 15 | import com.stepango.archetype.action.IntentMaker 16 | import com.stepango.archetype.action.argsOf 17 | import com.stepango.archetype.action.intent 18 | import com.stepango.archetype.logger.logger 19 | import com.stepango.archetype.player.data.db.model.EpisodeDownloadState 20 | import com.stepango.archetype.player.data.db.model.EpisodesModel 21 | import com.stepango.archetype.player.di.injector 22 | import com.stepango.archetype.player.di.lazyInject 23 | import com.stepango.archetype.player.episodeId 24 | import com.stepango.archetype.rx.CompositeDisposableHolder 25 | import com.stepango.archetype.rx.filterNonEmpty 26 | import com.stepango.archetype.rx.subscribeBy 27 | import com.stepango.archetype.util.getFileName 28 | import io.reactivex.Completable 29 | import io.reactivex.Observable 30 | import io.reactivex.Scheduler 31 | import io.reactivex.Single 32 | import io.reactivex.schedulers.Schedulers 33 | import io.reactivex.subjects.PublishSubject 34 | import okhttp3.OkHttpClient 35 | import okhttp3.Request 36 | import okhttp3.Response 37 | import okio.BufferedSink 38 | import okio.BufferedSource 39 | import okio.Okio 40 | import java.io.File 41 | import java.io.IOException 42 | import java.util.concurrent.TimeUnit 43 | 44 | const val CANCEL_DOWNLOAD_ACTION = "cancel_action" 45 | 46 | class EpisodeLoaderService : Service() { 47 | 48 | companion object { 49 | fun intent(episodeId: Long, intentMaker: IntentMaker, context: Context) = intentMaker.intent(context, argsOf { episodeId { episodeId } }) 50 | } 51 | 52 | val episodeLoader by lazy { EpisodeLoader(this, injector.compositeDisposableHolder()) } 53 | 54 | val cancelReceiver = object : BroadcastReceiver() { 55 | override fun onReceive(context: Context?, intent: Intent?) { 56 | episodeLoader.stopLoading() 57 | } 58 | } 59 | 60 | override fun onBind(intent: Intent?): IBinder? = null 61 | 62 | override fun onCreate() { 63 | super.onCreate() 64 | registerReceiver(cancelReceiver, IntentFilter(CANCEL_DOWNLOAD_ACTION)) 65 | } 66 | 67 | override fun onDestroy() { 68 | unregisterReceiver(cancelReceiver) 69 | super.onDestroy() 70 | } 71 | 72 | override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { 73 | val args: Args = intent.extras 74 | episodeLoader.queue(args.episodeId()) 75 | return START_STICKY 76 | } 77 | } 78 | 79 | class EpisodeLoader(val service: Service, compositeDisposableHolder: CompositeDisposableHolder) : 80 | ProgressUpdateListener, 81 | CompositeDisposableHolder by compositeDisposableHolder { 82 | 83 | val toaster by lazyInject { toaster() } 84 | val episodes by lazy { Episodes() } 85 | 86 | val queueSubject: PublishSubject = PublishSubject.create() 87 | val notificationHelper = NotificationHelper(service, hashCode()) 88 | val loader = ProgressOkLoader(this) 89 | val waitStack = ArrayList() 90 | 91 | val loadScheduler: Scheduler = Schedulers.single() 92 | 93 | init { 94 | queueSubject 95 | .timeout(10, TimeUnit.MINUTES) 96 | .observeOn(loadScheduler) 97 | .subscribeBy( 98 | onNext = { load(it) }, 99 | onError = { stopLoading() } 100 | ) 101 | .bind() 102 | } 103 | 104 | fun startForeground() { 105 | service.startForeground(hashCode(), notificationHelper.getNotification()) 106 | } 107 | 108 | fun stopForeground() { 109 | service.stopForeground(true) 110 | } 111 | 112 | fun clearWaitStack(): Completable { 113 | return Observable.fromIterable(waitStack) 114 | .flatMapCompletable { episodes.updateState(it, EpisodeDownloadState.DOWNLOAD).toCompletable() } 115 | } 116 | 117 | fun queue(id: Long) { 118 | episodes.updateState(id, EpisodeDownloadState.WAIT) 119 | .doOnSuccess { waitStack.add(it.id) } 120 | .subscribeOn(Schedulers.io()) 121 | .subscribeBy( 122 | onSuccess = { queueSubject.onNext(it.id) }, 123 | onError = { toaster.showToast("error on queue for episode $id") } 124 | ).bind() 125 | } 126 | 127 | fun load(id: Long) { 128 | logger.d("start load for $id") 129 | episodes.getById(id) 130 | .flatMap { loadEpisode(it) } 131 | .subscribeOn(loadScheduler) 132 | .subscribeBy( 133 | onSuccess = { logger.d("loading done for $id") }, 134 | onError = { toaster.showToast("error on loading $id") } 135 | ).bind() 136 | } 137 | 138 | fun loadEpisode(episode: EpisodesModel): Single { 139 | return prepareEpisode(episode) 140 | .flatMap { startTask(it) } 141 | .doAfterTerminate { stopForeground() } 142 | .doOnError { handleLoadErrorFor(episode.id) } 143 | } 144 | 145 | fun prepareEpisode(episode: EpisodesModel): Single { 146 | return episodes.updateState(episode, EpisodeDownloadState.CANCEL) 147 | .doOnSuccess { waitStack.remove(it.id) } 148 | .doOnSuccess { startForeground() } 149 | } 150 | 151 | fun handleLoadErrorFor(id: Long) { 152 | episodes.updateState(id, EpisodeDownloadState.RETRY) 153 | .subscribeOn(Schedulers.io()) 154 | .subscribeBy( 155 | onError = { toaster.showToast("can't set RETRY for episode $id") } 156 | ).bind() 157 | } 158 | 159 | fun startTask(episode: EpisodesModel): Single { 160 | notificationHelper.text(episode.name) 161 | val file = File(service.filesDir, getFileName(episode.audioUrl)) 162 | return EpisodeDownloadTask( 163 | loader.client, 164 | episode, 165 | file) 166 | .execute() 167 | .doOnError { file.delete() } 168 | .flatMap { episodes.updateFile(episode, it) } 169 | } 170 | 171 | override fun onProgressUpdate(current: Long, total: Long, done: Boolean) { 172 | notificationHelper.progress(current, total) 173 | } 174 | 175 | fun stopLoading() { 176 | clearWaitStack() 177 | .doAfterTerminate { resetCompositeDisposable() } 178 | .subscribeOn(Schedulers.io()) 179 | .subscribeBy( 180 | onComplete = { 181 | service.stopForeground(true) 182 | service.stopSelf() 183 | } 184 | ).bind() 185 | } 186 | } 187 | 188 | class Episodes { 189 | val repo by lazyInject { episodesRepo() } 190 | 191 | fun updateState(id: Long, newState: EpisodeDownloadState): Single = getById(id).flatMap { updateState(it, newState) } 192 | 193 | fun updateState(episode: EpisodesModel, newState: EpisodeDownloadState): Single = repo.save(episode.id, episode.copy(state = newState)) 194 | 195 | fun updateFile(episode: EpisodesModel, file: File): Single = repo.save(episode.id, episode.copy(file = file.absolutePath)) 196 | 197 | fun getById(id: Long): Single = repo.observe(id) 198 | .filterNonEmpty() 199 | .firstOrError() 200 | } 201 | 202 | class EpisodeDownloadTask( 203 | val client: OkHttpClient, 204 | val episode: EpisodesModel, 205 | val destination: File 206 | ) { 207 | 208 | fun execute(): Single = Single.fromCallable { 209 | val request: Request = createRequest(episode.audioUrl) 210 | val response: Response = client.newCall(request).execute() 211 | 212 | if (!response.isSuccessful) throw IOException("Unexpected code $response") 213 | saveToFile(response.body().source()) 214 | return@fromCallable destination 215 | } 216 | 217 | fun createRequest(url: String): Request = Request.Builder().url(url).build() 218 | fun saveToFile(src: BufferedSource) { 219 | val sink: BufferedSink = Okio.buffer(Okio.sink(destination)) 220 | sink.writeAll(src) 221 | sink.close() 222 | } 223 | } 224 | 225 | class NotificationHelper(context: Context, val id: Int) { 226 | 227 | val REQUEST_CANCEL = 0 228 | val mgr = context.getSystemService(Context.NOTIFICATION_SERVICE) 229 | as NotificationManager 230 | val builder: NotificationCompat.Builder = NotificationCompat.Builder(context) 231 | .setContentTitle(context.getString(R.string.app_name)) 232 | .setSmallIcon(android.R.drawable.stat_sys_download) 233 | .addAction( 234 | android.R.drawable.ic_menu_close_clear_cancel, 235 | context.getString(android.R.string.cancel), 236 | PendingIntent.getBroadcast( 237 | context, 238 | REQUEST_CANCEL, 239 | Intent(CANCEL_DOWNLOAD_ACTION), 240 | PendingIntent.FLAG_ONE_SHOT 241 | ) 242 | ) 243 | var progress: Int = 0 244 | set(value) { 245 | if (field != value) { 246 | field = value 247 | builder.setProgress(100, field, false) 248 | mgr.notify(id, builder.build()) 249 | } 250 | } 251 | 252 | fun text(value: String) { 253 | builder.setContentText(value) 254 | } 255 | 256 | fun progress(current: Long, total: Long) { 257 | progress = if (total > 0) ((100 * current) / total).toInt() else 100 258 | } 259 | 260 | fun getNotification(): Notification = builder.build() 261 | } 262 | 263 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/loader/ProgressOkLoader.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.loader 2 | 3 | import okhttp3.* 4 | import okio.* 5 | 6 | /** 7 | * Wild, 02.07.2017. 8 | */ 9 | 10 | interface ProgressUpdateListener { 11 | fun onProgressUpdate(current: Long, total: Long, done: Boolean) 12 | } 13 | 14 | class ProgressOkLoader(val listener: ProgressUpdateListener) : Interceptor { 15 | 16 | val client: OkHttpClient = OkHttpClient.Builder() 17 | .addNetworkInterceptor(this) 18 | .build() 19 | 20 | override fun intercept(chain: Interceptor.Chain?): Response? { 21 | val originalResponse = chain?.proceed(chain.request()) ?: return null 22 | 23 | return originalResponse.newBuilder() 24 | .body(ProgressResponseBody(originalResponse.body(), listener)) 25 | .build() 26 | } 27 | } 28 | 29 | class ProgressResponseBody( 30 | val responseBody: ResponseBody, 31 | val listener: ProgressUpdateListener) : ResponseBody() { 32 | 33 | private var bufferedSource: BufferedSource? = null 34 | 35 | override fun contentType(): MediaType = responseBody.contentType() 36 | 37 | override fun source(): BufferedSource { 38 | if (bufferedSource == null) 39 | bufferedSource = Okio.buffer(source(responseBody.source())) 40 | 41 | return bufferedSource!! 42 | } 43 | 44 | override fun contentLength() = responseBody.contentLength() 45 | 46 | private fun source(source: Source): Source { 47 | return object: ForwardingSource(source) { 48 | private var total: Long = 0L 49 | 50 | override fun read(sink: Buffer?, byteCount: Long): Long { 51 | val bytesRead = super.read(sink, byteCount) 52 | total += if (bytesRead != -1L) bytesRead else 0 53 | listener.onProgressUpdate(total, contentLength(), bytesRead == -1L) 54 | return bytesRead 55 | } 56 | } 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/network/Api.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.network 2 | 3 | import com.stepango.archetype.player.data.db.response.feed.Rss 4 | import io.reactivex.Single 5 | import retrofit2.http.GET 6 | 7 | interface Api { 8 | 9 | @GET("Podcast/android.xml") 10 | fun feed(): Single 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/network/NetworkRequest.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.network 2 | 3 | import com.stepango.archetype.player.di.injector 4 | import com.stepango.archetype.rx.networkScheduler 5 | import io.reactivex.Scheduler 6 | import io.reactivex.Single 7 | 8 | 9 | abstract class ApiRequest( 10 | val scheduler: Scheduler = networkScheduler 11 | ) { 12 | 13 | val deps: GraphQLRequestDeps = GraphQLRequestDeps() 14 | val api: Api = deps.api 15 | 16 | protected abstract fun operation(): Single 17 | 18 | /** 19 | * Creates on observable and starts job 20 | */ 21 | fun execute(): Single = operation() 22 | // .doOnError { (it) { "Error executing request $id" } } 23 | .subscribeOn(scheduler) 24 | } 25 | 26 | class GraphQLRequestDeps { 27 | 28 | val api: Api = injector.apiService() 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/network/get/Contants.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.network.get 2 | 3 | const val BASE_URL: String = "http://apptractor.ru" 4 | const val WEB_SERVICE_TIMEOUT = 1000 * 15L -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/network/get/GetEpisodesRequest.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.network.get 2 | 3 | import com.stepango.archetype.player.data.db.model.EpisodesModel 4 | import com.stepango.archetype.player.data.db.response.feed.Item 5 | import com.stepango.archetype.player.network.ApiRequest 6 | import io.reactivex.Single 7 | 8 | class GetEpisodesRequest : ApiRequest>() { 9 | override fun operation(): Single> = 10 | api.feed() 11 | .map { it.channel.item.map(::transformResponse) } 12 | } 13 | 14 | fun transformResponse(feedItem: Item) = EpisodesModel( 15 | feedItem.title, 16 | feedItem.summary, 17 | feedItem.image.href, 18 | feedItem.enclosure.url, 19 | feedItem.content 20 | ) 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/ui/additional/MockToaster.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.ui.additional 2 | 3 | import com.stepango.archetype.logger.logger 4 | import com.stepango.archetype.ui.Toaster 5 | 6 | class MockToaster : Toaster { 7 | override fun showToast(msg: String) { 8 | logger.i(msg) 9 | } 10 | 11 | override fun showToast(id: Int, vararg args: Any) { 12 | logger.i("$id") 13 | } 14 | 15 | override fun showError(id: Int) { 16 | logger.e("$id") 17 | } 18 | 19 | override fun showError(t: Throwable, id: Int) { 20 | logger.e("$id: ${t.message}") 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/ui/episodes/EpisodesScreen.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.ui.episodes 2 | 3 | import android.databinding.ObservableField 4 | import android.os.Bundle 5 | import com.stepango.archetype.R 6 | import com.stepango.archetype.activity.BaseActivity 7 | import com.stepango.archetype.databinding.ScreenEpisodesBinding 8 | import com.stepango.archetype.fragment.BaseFragment 9 | import com.stepango.archetype.player.data.wrappers.EpisodeListItemWrapper 10 | import com.stepango.archetype.player.data.wrappers.EpisodeListItemWrapperFabric 11 | import com.stepango.archetype.player.di.injector 12 | import com.stepango.archetype.player.di.lazyInject 13 | import com.stepango.archetype.viewmodel.ViewModel 14 | import com.stepango.rxdatabindings.setTo 15 | 16 | class EpisodesActivity : BaseActivity() { 17 | override val fragmentProducer = { EpisodesFragment() } 18 | } 19 | 20 | class EpisodesFragment : BaseFragment() { 21 | 22 | override fun initBinding(binding: ScreenEpisodesBinding, state: Bundle?) { 23 | val vm = injector.injector.vm(this) 24 | binding.vm = EpisodesViewModel(injector.episodeListItemWrapperFabric(vm.actionHandler), vm) 25 | } 26 | 27 | override val layoutId = R.layout.screen_episodes 28 | } 29 | 30 | class EpisodesViewModel( 31 | val wrapper: EpisodeListItemWrapperFabric, 32 | vm: ViewModel 33 | ) : 34 | ViewModel by vm { 35 | 36 | private val episodesUseCase by lazyInject { episodesUseCase() } 37 | val episodes: ObservableField> = ObservableField(listOf()) 38 | 39 | init { 40 | refresh() 41 | display() 42 | } 43 | 44 | private fun display() { 45 | episodesUseCase.observeEpisodes() 46 | .map { wrapper.wrap(it) } 47 | .setTo(episodes) 48 | .bindSubscribe() 49 | } 50 | 51 | fun refresh() { 52 | episodesUseCase.updateEpisodes(actionHandler) 53 | .bindSubscribe( 54 | onComplete = { hideLoader() }, 55 | onError = { toaster.showError(it, R.string.episodes_error_loading) } 56 | ) 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/ui/episodes/EpisodesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.ui.episodes 2 | 3 | import com.stepango.archetype.action.ContextActionHandler 4 | import com.stepango.archetype.player.data.db.EpisodesModelRepo 5 | import com.stepango.archetype.player.data.db.model.EpisodesModel 6 | import com.stepango.archetype.player.data.wrappers.EpisodeWrapper 7 | import com.stepango.archetype.rx.filterNonEmpty 8 | import com.stepango.archetype.rx.filterNotEmpty 9 | import io.reactivex.Completable 10 | import io.reactivex.Observable 11 | 12 | interface EpisodesUseCase { 13 | fun observeEpisodes(): Observable> 14 | fun observeEpisode(id: Long): Observable 15 | fun updateEpisodes(ah: ContextActionHandler): Completable 16 | } 17 | 18 | class EpisodesUseCaseImpl( 19 | private val episodesRepo: EpisodesModelRepo 20 | ) : EpisodesUseCase { 21 | 22 | override fun observeEpisodes(): Observable> = episodesRepo.observeAll() 23 | .filterNotEmpty() 24 | 25 | override fun observeEpisode(id: Long): Observable = episodesRepo.observe(id) 26 | .filterNonEmpty() 27 | 28 | override fun updateEpisodes(ah: ContextActionHandler): Completable = 29 | episodesRepo.pull() 30 | .andThen(episodesRepo.refreshFiles(ah)) 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/ui/player/PlayerComponent.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.ui.player 2 | 3 | import android.content.Context 4 | import android.media.MediaPlayer 5 | import android.net.Uri 6 | import com.stepango.archetype.logger.logger 7 | 8 | 9 | interface PlayerComponent { 10 | fun play(url: String) 11 | } 12 | 13 | class PlayerComponentImpl(val context: Context) : PlayerComponent { 14 | 15 | override fun play(url: String) { 16 | logger.d("play from $url") 17 | val mediaPlayer = MediaPlayer.create(context, Uri.parse(url)) 18 | mediaPlayer.start() 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/ui/player/PlayerScreen.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.ui.player 2 | 3 | import android.content.Context 4 | import android.databinding.ObservableField 5 | import android.os.Bundle 6 | import com.stepango.archetype.R 7 | import com.stepango.archetype.action.IntentMaker 8 | import com.stepango.archetype.action.argsOf 9 | import com.stepango.archetype.action.intent 10 | import com.stepango.archetype.activity.BaseActivity 11 | import com.stepango.archetype.databinding.ScreenPlayerBinding 12 | import com.stepango.archetype.fragment.BaseFragment 13 | import com.stepango.archetype.player.data.wrappers.EpisodeItemWrapperFabric 14 | import com.stepango.archetype.player.data.wrappers.EpisodeWrapper 15 | import com.stepango.archetype.player.di.injector 16 | import com.stepango.archetype.player.di.lazyInject 17 | import com.stepango.archetype.player.episodeId 18 | import com.stepango.archetype.viewmodel.ViewModel 19 | import com.stepango.rxdatabindings.setTo 20 | 21 | class PlayerActivity : BaseActivity() { 22 | override val fragmentProducer = { PlayerFragment() } 23 | 24 | companion object { 25 | fun intent(episodeId: Long, intentMaker: IntentMaker, context: Context) = intentMaker.intent(context, argsOf { episodeId { episodeId } }) 26 | } 27 | } 28 | 29 | class PlayerFragment : BaseFragment() { 30 | 31 | override fun initBinding(binding: ScreenPlayerBinding, state: Bundle?) { 32 | val vm = injector.vm(this) 33 | binding.vm = PlayerViewModel(vm, injector.episodeItemWrapperFabric(vm.actionHandler)) 34 | } 35 | 36 | override val layoutId = R.layout.screen_player 37 | } 38 | 39 | class PlayerViewModel( 40 | vm: ViewModel, 41 | val wrapper: EpisodeItemWrapperFabric 42 | ) : ViewModel by vm { 43 | 44 | private val episodesUseCase by lazyInject { episodesUseCase() } 45 | val player by lazyInject { player() } 46 | 47 | val episodeId = args().episodeId() 48 | val episode = ObservableField() 49 | 50 | init { 51 | episodesUseCase.observeEpisode(episodeId) 52 | .map { wrapper.wrap(it) } 53 | .setTo(episode) 54 | .bindSubscribe() 55 | } 56 | 57 | fun play() { 58 | if (episode.get()?.file.isNullOrEmpty()) 59 | player.play(episode.get()?.audioUrl!!) 60 | else 61 | player.play(episode.get()?.file!!) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/player/ui/player/ShowEpisodeAction.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.player.ui.player 2 | 3 | import android.content.Context 4 | import com.stepango.archetype.action.IntentAction 5 | import com.stepango.archetype.action.IntentMaker 6 | import com.stepango.archetype.action.start 7 | import com.stepango.archetype.logger.d 8 | import com.stepango.archetype.logger.logger 9 | import com.stepango.archetype.player.di.Injector 10 | import io.reactivex.Completable 11 | 12 | interface ShowEpisodeAction : IntentAction 13 | class ShowEpisodeActionParams(val episodeId: Long) 14 | 15 | class ShowEpisodeActionImpl : ShowEpisodeAction, IntentMaker by Injector().intentMaker() { 16 | 17 | override fun invoke(context: Context, params: ShowEpisodeActionParams): Completable = 18 | PlayerActivity.intent(params.episodeId, this, context).start(context) 19 | .doOnComplete { logger.d { "aaa" } } 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/resources/ContextExt.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.resources 2 | 3 | import android.content.Context 4 | 5 | fun Number.name(ctx: Context): String = ctx.resources.getResourceEntryName(this.toInt()) -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/rx/CompositeDisposableComponent.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.rx 2 | 3 | import io.reactivex.disposables.CompositeDisposable 4 | import io.reactivex.disposables.Disposable 5 | 6 | 7 | interface CompositeDisposableHolder { 8 | var composite: CompositeDisposable 9 | 10 | fun Disposable.bind(): Unit 11 | 12 | fun bindDisposable(disposable: Disposable): Unit 13 | 14 | fun resetCompositeDisposable() { 15 | synchronized(this) { 16 | composite.clear() 17 | composite = CompositeDisposable() 18 | } 19 | } 20 | } 21 | 22 | class CompositeDisposableHolderImpl : CompositeDisposableHolder { 23 | override var composite = CompositeDisposable() 24 | 25 | override fun bindDisposable(disposable: Disposable) { 26 | composite += disposable 27 | } 28 | 29 | override fun Disposable.bind() { 30 | bindDisposable(this) 31 | } 32 | } 33 | 34 | operator fun CompositeDisposable.plusAssign(disposable: Disposable) { 35 | add(disposable) 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/rx/RxExt.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.rx 2 | 3 | import com.stepango.archetype.viewmodel.onCompleteStub 4 | import com.stepango.archetype.viewmodel.onErrorStub 5 | import com.stepango.archetype.viewmodel.onNextStub 6 | import com.stepango.koptional.Optional 7 | import io.reactivex.Completable 8 | import io.reactivex.Maybe 9 | import io.reactivex.MaybeSource 10 | import io.reactivex.Observable 11 | import io.reactivex.Observable.fromIterable 12 | import io.reactivex.Single 13 | import io.reactivex.disposables.Disposable 14 | import io.reactivex.functions.BiFunction 15 | 16 | fun Observable>.flatten(): Observable = flatMap { fromIterable(it) } 17 | 18 | inline fun Maybe.zipWith(other: MaybeSource, crossinline zipper: (T, U) -> R): Maybe 19 | = zipWith(other, BiFunction { t1, t2 -> zipper(t1, t2) }) 20 | 21 | fun > Observable.filterNotEmpty(): Observable = filter { it?.isNotEmpty() ?: false } 22 | fun Observable>.filterNonEmpty(): Observable = filter { it.isPresent }.map { it.get() } 23 | 24 | fun Observable.subscribeBy( 25 | onNext: (T) -> Unit = onNextStub, 26 | onError: (Throwable) -> Unit = onErrorStub, 27 | onComplete: () -> Unit = onCompleteStub 28 | ): Disposable = subscribe(onNext, onError, onComplete) 29 | 30 | fun Single.subscribeBy( 31 | onSuccess: (T) -> Unit = onNextStub, 32 | onError: (Throwable) -> Unit = onErrorStub 33 | ): Disposable = subscribe(onSuccess, onError) 34 | 35 | fun Completable.subscribeBy( 36 | onComplete: () -> Unit = onCompleteStub, 37 | onError: (Throwable) -> Unit = onErrorStub 38 | ): Disposable = subscribe(onComplete, onError) -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/rx/schedulers.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.rx 2 | 3 | import io.reactivex.Scheduler 4 | import kotlin.properties.Delegates 5 | 6 | 7 | var networkScheduler by Delegates.notNull() 8 | var actionScheduler by Delegates.notNull() 9 | var nonDisposableActionScheduler by Delegates.notNull() 10 | var uiScheduler by Delegates.notNull() -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/rx/singles.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.rx 2 | 3 | import io.reactivex.Single 4 | import io.reactivex.functions.BiFunction 5 | 6 | inline fun zip(source: Single, target: Single, crossinline block: (T1, T2) -> R): Single 7 | = Single.zip(source, target, BiFunction { t1, t2 -> block(t1, t2) }) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/ui/LastAdapterExt.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.ui 2 | 3 | import android.databinding.BindingAdapter 4 | import android.support.v7.widget.LinearLayoutManager 5 | import android.support.v7.widget.RecyclerView 6 | import com.github.nitrico.lastadapter.AbsType 7 | import com.github.nitrico.lastadapter.LastAdapter 8 | import com.github.nitrico.lastadapter.StableId 9 | import com.stepango.archetype.BR 10 | 11 | interface LastAdapterItem : StableId { 12 | fun getBindingType(): AbsType<*>? 13 | } 14 | 15 | @BindingAdapter("items", "scrollDown", requireAll = false) 16 | fun lastAdapterItemsBinding(view: RecyclerView, list: List, scrollDown: Boolean? = false) { 17 | var lastItemWasVisible = false 18 | (view.layoutManager as? LinearLayoutManager)?.let { 19 | lastItemWasVisible = it.findLastCompletelyVisibleItemPosition() == view.adapter?.itemCount?.let { it - 1 } 20 | } 21 | lastAdapter(list) 22 | .type { item, _ -> (item as? LastAdapterItem)?.getBindingType() } 23 | .swap(view) 24 | scrollDown?.let { if (it && lastItemWasVisible) view.scrollToPosition(list.size - 1) } 25 | } 26 | 27 | 28 | internal fun lastAdapter(list: List, stableIds: Boolean = true) = LastAdapter(list, BR.item, stableIds) 29 | 30 | fun LastAdapter.swap(view: RecyclerView, removeAndRecyclerExistingViews: Boolean = false) = 31 | view.swapAdapter(this, removeAndRecyclerExistingViews) 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/ui/SimpleToaster.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.ui 2 | 3 | import android.app.Activity 4 | import android.os.Handler 5 | import android.os.Looper 6 | import android.support.annotation.StringRes 7 | import android.widget.Toast 8 | import com.stepango.archetype.logger.logError 9 | 10 | class SimpleToaster(val ctx: Activity) : Toaster { 11 | 12 | override fun showError(@StringRes id: Int) = showToast(id) 13 | 14 | override fun showError(t: Throwable, @StringRes id: Int) { 15 | t.logError { "Show error to user" } 16 | showError(id) 17 | } 18 | 19 | override fun showToast(@StringRes id: Int, vararg args: Any) { 20 | Handler(Looper.getMainLooper()).post { Toast.makeText(ctx, ctx.getString(id, args), Toast.LENGTH_SHORT).show() } 21 | } 22 | 23 | override fun showToast(msg: String) { 24 | Handler(Looper.getMainLooper()).post { Toast.makeText(ctx, msg, Toast.LENGTH_SHORT).show() } 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/ui/SpaceItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.ui 2 | 3 | import android.graphics.Rect 4 | import android.support.v7.widget.RecyclerView 5 | import android.view.View 6 | 7 | class SpaceItemDecoration(private val space: Int) : RecyclerView.ItemDecoration() { 8 | 9 | override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, 10 | state: RecyclerView.State?) { 11 | outRect.left = space 12 | outRect.bottom = space 13 | outRect.right = space 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/ui/Toaster.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.ui 2 | 3 | import android.support.annotation.StringRes 4 | 5 | interface Toaster { 6 | fun showToast(msg: String) 7 | fun showToast(@StringRes id: Int, vararg args: Any) 8 | 9 | fun showError(@StringRes id: Int) 10 | 11 | fun showError(t: Throwable, @StringRes id: Int) 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/ui/ViewExt.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.ui 2 | 3 | import android.view.View 4 | 5 | fun View.stopAnimation() = apply { clearAnimation(); animate().cancel() } 6 | fun View.show() = apply { visibility = View.VISIBLE } 7 | fun View.gone() = apply { visibility = View.GONE } 8 | fun View.invisible() = apply { visibility = View.INVISIBLE } 9 | var View.visible: Boolean 10 | get() = visibility == View.VISIBLE 11 | set(value) { 12 | if (value) show() else gone() 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/util/ContextUtil.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.util 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.util.DisplayMetrics 8 | import com.stepango.archetype.activity.asBaseActivity 9 | import io.reactivex.Completable 10 | 11 | fun Number.dp(ctx: Context): Int = (ctx.resources.displayMetrics.density * this.toFloat()).toInt() 12 | fun Number.name(ctx: Context): String = ctx.resources.getResourceEntryName(this.toInt()) 13 | 14 | fun Activity.screenWidth(): Int { 15 | val metrics = DisplayMetrics() 16 | this.windowManager.defaultDisplay.getMetrics(metrics) 17 | return metrics.widthPixels 18 | } 19 | 20 | fun setResultAndFinish(bundle: Bundle, context: Context): Completable = Completable.fromCallable { 21 | context.asBaseActivity()?.run { 22 | val data = Intent().putExtras(bundle) 23 | if (parent == null) { 24 | setResult(Activity.RESULT_OK, data) 25 | } else { 26 | parent.setResult(Activity.RESULT_OK, data) 27 | } 28 | finish() 29 | } 30 | } 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/util/StringExt.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.util 2 | 3 | import java.util.StringTokenizer 4 | 5 | fun String.linesCount() = StringTokenizer(this, "\r\n").countTokens() 6 | fun String.firstLine() = this.split("\r\n|\r|\n").first() 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/util/UriUtils.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.util 2 | 3 | import android.net.Uri 4 | 5 | /** 6 | * Wild, 03.07.2017. 7 | */ 8 | 9 | fun getFileName(url: String): String 10 | = Uri.parse(url).pathSegments.joinToString(separator = "") { it } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/viewmodel/LoaderHolder.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.viewmodel 2 | 3 | import android.databinding.ObservableBoolean 4 | 5 | interface LoaderHolder { 6 | val showLoader: ObservableBoolean 7 | val isDataLoaded: ObservableBoolean 8 | 9 | fun showLoader() { 10 | showLoader.set(true) 11 | } 12 | 13 | fun hideLoader() { 14 | showLoader.set(false) 15 | showLoader.notifyChange() 16 | } 17 | } 18 | 19 | class LoaderHolderImpl( 20 | override val showLoader: ObservableBoolean = ObservableBoolean(true), 21 | override val isDataLoaded: ObservableBoolean = ObservableBoolean(false) 22 | ) : LoaderHolder -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/viewmodel/LoadingProgressHelperImpl.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.viewmodel 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import io.reactivex.Single 6 | 7 | interface LoadingProgressBuilder { 8 | val producer: () -> Single 9 | fun onError(handler: (Throwable) -> Unit): LoadingProgressBuilder 10 | fun onResult(handler: (T, Context) -> Unit): LoadingProgressBuilder 11 | fun finishAfterSuccess(finish: Boolean = true): LoadingProgressBuilder 12 | fun show() 13 | } 14 | 15 | interface LoadingProgressHelper { 16 | fun loadingProgress(context: Context, producer: () -> Single): LoadingProgressBuilder 17 | fun loadingProgress(view: View, producer: () -> Single): LoadingProgressBuilder = 18 | loadingProgress(view.context, producer) 19 | } 20 | 21 | class LoadingProgressHelperImpl : LoadingProgressHelper { 22 | 23 | override fun loadingProgress(context: Context, producer: () -> Single): LoadingProgressBuilder = 24 | TODO("show loading fragment") 25 | 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/viewmodel/RxLifecycleImpl.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.viewmodel 2 | 3 | import android.app.Activity 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.CompositeDisposableHolderImpl 8 | import com.trello.navi2.Event 9 | import com.trello.navi2.NaviComponent 10 | import com.trello.navi2.model.ActivityResult 11 | import com.trello.navi2.rx.RxNavi 12 | import io.reactivex.Completable 13 | import io.reactivex.Flowable 14 | import io.reactivex.Maybe 15 | import io.reactivex.Observable 16 | import io.reactivex.Single 17 | 18 | 19 | interface RxLifecycle : CompositeDisposableHolder, NaviComponent { 20 | 21 | val startObservable: Observable 22 | val vmLifeLongSubscription: CompositeDisposableHolder 23 | 24 | fun Observable.bindSubscribe( 25 | onNext: (T) -> Unit = onNextStub, 26 | onError: (Throwable) -> Unit = onErrorStub, 27 | onComplete: () -> Unit = onCompleteStub 28 | ) = startObservable.doOnNext { 29 | this.subscribe(onNext, onError, onComplete).bind() 30 | }.bindSubscribeTillDetach() 31 | 32 | fun Flowable.bindSubscribe( 33 | onNext: (T) -> Unit = onNextStub, 34 | onError: (Throwable) -> Unit = onErrorStub, 35 | onComplete: () -> Unit = onCompleteStub 36 | ) = startObservable.doOnNext { 37 | this.subscribe(onNext, onError, onComplete).bind() 38 | }.bindSubscribeTillDetach() 39 | 40 | fun Single.bindSubscribe( 41 | onSuccess: (T) -> Unit = onNextStub, 42 | onError: (Throwable) -> Unit = onErrorStub 43 | ) = startObservable.doOnNext { 44 | this.subscribe(onSuccess, onError).bind() 45 | }.bindSubscribeTillDetach() 46 | 47 | fun Maybe.bindSubscribe( 48 | onSuccess: (T) -> Unit = onNextStub, 49 | onError: (Throwable) -> Unit = onErrorStub, 50 | onComplete: () -> Unit = onCompleteStub 51 | ) = startObservable.doOnNext { 52 | this.subscribe(onSuccess, onError, onComplete).bind() 53 | }.bindSubscribeTillDetach() 54 | 55 | fun Completable.bindSubscribe( 56 | onComplete: () -> Unit = onCompleteStub, 57 | onError: (Throwable) -> Unit = onErrorStub 58 | ) = startObservable.doOnNext { 59 | this.subscribe(onComplete, onError).bind() 60 | }.bindSubscribeTillDetach() 61 | 62 | fun Observable.bindSubscribeTillDetach( 63 | onNext: (T) -> Unit = onNextStub, 64 | onError: (Throwable) -> Unit = onErrorStub, 65 | onComplete: () -> Unit = onCompleteStub 66 | ) = subscribe(onNext, onError, onComplete).let(vmLifeLongSubscription::bindDisposable) 67 | 68 | fun Flowable.bindSubscribeTillDetach( 69 | onNext: (T) -> Unit = onNextStub, 70 | onError: (Throwable) -> Unit = onErrorStub, 71 | onComplete: () -> Unit = onCompleteStub 72 | ) = subscribe(onNext, onError, onComplete).let(vmLifeLongSubscription::bindDisposable) 73 | 74 | fun Single.bindSubscribeTillDetach( 75 | onSuccess: (T) -> Unit = onNextStub, 76 | onError: (Throwable) -> Unit = onErrorStub 77 | ) = subscribe(onSuccess, onError).let(vmLifeLongSubscription::bindDisposable) 78 | 79 | fun Maybe.bindSubscribeTillDetach( 80 | onSuccess: (T) -> Unit = onNextStub, 81 | onError: (Throwable) -> Unit = onErrorStub, 82 | onComplete: () -> Unit = onCompleteStub 83 | ) = subscribe(onSuccess, onError, onComplete).let(vmLifeLongSubscription::bindDisposable) 84 | 85 | fun Completable.bindSubscribeTillDetach( 86 | onComplete: () -> Unit = onCompleteStub, 87 | onError: (Throwable) -> Unit = onErrorStub 88 | ) = subscribe(onComplete, onError).let(vmLifeLongSubscription::bindDisposable) 89 | 90 | fun ViewModel.observeActivityResult(requestCode: Int, resultCode: Int = Activity.RESULT_OK, onResult: (ActivityResult) -> Unit) 91 | fun ViewModel.activityResultObservable(requestCode: Int, resultCode: Int = Activity.RESULT_OK): Observable 92 | } 93 | 94 | fun NaviComponent.observe(event: Event): Observable = RxNavi.observe(this, event) 95 | 96 | class RxLifecycleImpl( 97 | naviComponent: NaviComponent, 98 | compositeDisposableHolder: CompositeDisposableHolder, 99 | startEvent: Event<*>, 100 | stopEvent: Event<*>, 101 | detachEvent: Event<*> 102 | ) : RxLifecycle, 103 | NaviComponent by naviComponent, 104 | CompositeDisposableHolder by compositeDisposableHolder { 105 | 106 | override val startObservable: Observable = observe(startEvent).cacheWithInitialCapacity(1) 107 | override val vmLifeLongSubscription: CompositeDisposableHolder = CompositeDisposableHolderImpl() 108 | 109 | init { 110 | observe(detachEvent).bindSubscribeTillDetach(onNext = { vmLifeLongSubscription.resetCompositeDisposable() }) 111 | observe(stopEvent).bindSubscribeTillDetach(onNext = { resetCompositeDisposable() }) 112 | startObservable 113 | startObservable.bindSubscribeTillDetach() 114 | } 115 | 116 | override fun ViewModel.observeActivityResult(requestCode: Int, resultCode: Int, onResult: (ActivityResult) -> Unit) = activityResultObservable(requestCode, resultCode) 117 | .doOnNext { showLoader() } 118 | .doOnComplete(this::hideLoader) 119 | .bindSubscribeTillDetach(onNext = onResult) 120 | 121 | override fun ViewModel.activityResultObservable(requestCode: Int, resultCode: Int): Observable = this.observe(Event.ACTIVITY_RESULT) 122 | .doOnNext { logger.d { "LIFECYCLE:: ACTIVITY_RESULT $it" } } 123 | .filter { it.resultCode() == resultCode } 124 | .filter { it.requestCode() == requestCode } 125 | } -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/viewmodel/Stubs.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.viewmodel 2 | 3 | import com.stepango.archetype.logger.logger 4 | import com.stepango.archetype.player.di.Injector 5 | 6 | val onNextStub: (Any) -> Unit = {} 7 | val onErrorStub: (Throwable) -> Unit = { Injector().logger.e(it, "On error not implemented") } 8 | val onCompleteStub: () -> Unit = {} -------------------------------------------------------------------------------- /app/src/main/java/com/stepango/archetype/viewmodel/ViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.stepango.archetype.viewmodel 2 | 3 | import android.os.Parcelable 4 | import com.stepango.archetype.action.ActionData 5 | import com.stepango.archetype.action.Args 6 | import com.stepango.archetype.action.ContextActionHandler 7 | import com.stepango.archetype.action.argsOf 8 | import com.stepango.archetype.bundle.ViewModelStateStub 9 | import com.stepango.archetype.bundle.putState 10 | import com.stepango.archetype.ui.Toaster 11 | import com.trello.navi2.Event 12 | 13 | interface ViewModel : LoaderHolder, RxLifecycle, LoadingProgressHelper { 14 | 15 | val toaster: Toaster 16 | val actionHandler: ContextActionHandler 17 | 18 | fun args(): Args 19 | 20 | fun

executeAction(producer: () -> ActionData

) = 21 | producer().let { 22 | actionHandler.handleAction(it.action, it.params) 23 | } 24 | } 25 | 26 | class ViewModelParamsHolder( 27 | val rxLifecycle: RxLifecycle, 28 | val loaderHolder: LoaderHolder, 29 | val loadingProgressHelper: LoadingProgressHelper, 30 | val args: Args, 31 | val toaster: Toaster, 32 | val actionHandler: ContextActionHandler 33 | ) 34 | 35 | @Suppress("AddVarianceModifier") 36 | class StatefulViewModel( 37 | rxLifecycle: RxLifecycle, 38 | loaderHolder: LoaderHolder, 39 | loadingProgressHelper: LoadingProgressHelper, 40 | args: Args, 41 | val state: T, 42 | toaster: Toaster, 43 | actionHandler: ContextActionHandler 44 | ) : ViewModel by ViewModelImpl( 45 | args = args, 46 | state = state, 47 | toaster = toaster, 48 | actionHandler = actionHandler, 49 | rxLifecycle = rxLifecycle, 50 | loaderHolder = loaderHolder, 51 | loadingProgressHelper = loadingProgressHelper) { 52 | 53 | constructor(params: ViewModelParamsHolder, state: T) : this( 54 | rxLifecycle = params.rxLifecycle, 55 | loaderHolder = params.loaderHolder, 56 | loadingProgressHelper = params.loadingProgressHelper, 57 | args = params.args, 58 | state = state, 59 | toaster = params.toaster, 60 | actionHandler = params.actionHandler 61 | ) 62 | } 63 | 64 | class ViewModelImpl constructor( 65 | rxLifecycle: RxLifecycle, 66 | loaderHolder: LoaderHolder, 67 | loadingProgressHelper: LoadingProgressHelper, 68 | inline val args: Args = argsOf(), 69 | inline val state: Parcelable = ViewModelStateStub.INSTANCE, 70 | override val toaster: Toaster, 71 | override val actionHandler: ContextActionHandler 72 | ) : 73 | ViewModel, 74 | RxLifecycle by rxLifecycle, 75 | LoaderHolder by loaderHolder, 76 | LoadingProgressHelper by loadingProgressHelper { 77 | 78 | init { 79 | observe(Event.SAVE_INSTANCE_STATE).bindSubscribeTillDetach(onNext = { it.putState(state) }) 80 | observe(Event.DESTROY).bindSubscribeTillDetach(onNext = { 81 | actionHandler.stopActions() 82 | }) 83 | } 84 | 85 | override fun args(): Args = args 86 | 87 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_divider.xml: -------------------------------------------------------------------------------- 1 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/include_app_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_episode.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 25 | 26 | 32 | 33 | 44 | 45 |