├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── by │ │ └── ve │ │ └── dialogsbinding │ │ ├── App.kt │ │ ├── databinding │ │ └── BindingAdapters.kt │ │ ├── lifecycle │ │ ├── DatabindingSingleLiveEvent.kt │ │ ├── LifecycleExtensions.kt │ │ └── SingleLiveEvent.kt │ │ ├── service │ │ └── FirstTryFailingService.kt │ │ └── ui │ │ ├── demo │ │ ├── chooser │ │ │ ├── SolutionChooserActivity.kt │ │ │ └── SolutionChooserViewModel.kt │ │ ├── dialog │ │ │ ├── base │ │ │ │ ├── BaseSolutionViewModel.kt │ │ │ │ └── SolutionViewModel.kt │ │ │ ├── solution1 │ │ │ │ ├── DialogControlEvent.kt │ │ │ │ ├── Solution1Activity.kt │ │ │ │ └── Solution1ViewModel.kt │ │ │ ├── solution2 │ │ │ │ ├── ErrorView.kt │ │ │ │ ├── Solution2Activity.kt │ │ │ │ └── Solution2ViewModel.kt │ │ │ └── solution3 │ │ │ │ ├── DialogViewModel.kt │ │ │ │ ├── Solution3Activity.kt │ │ │ │ └── Solution3ViewModel.kt │ │ └── toast │ │ │ ├── JustAnotherFragment.kt │ │ │ ├── ToastsDemoActivity.kt │ │ │ ├── ToastsDemoFragment.kt │ │ │ ├── ToastsDemoNavigationViewModel.kt │ │ │ └── ToastsDemoViewModel.kt │ │ ├── dialog │ │ ├── common │ │ │ ├── DialogUiConfig.kt │ │ │ ├── IDialogUiConfig.kt │ │ │ └── IDialogViewModel.kt │ │ ├── fragment │ │ │ ├── CommonDialogFragment.kt │ │ │ ├── DialogEvent.kt │ │ │ ├── DialogNavigator.kt │ │ │ └── DialogViewModel.kt │ │ └── view │ │ │ └── DialogShowingView.kt │ │ └── toast │ │ ├── ToastShowingView.kt │ │ └── ToastViewModel.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_solution3_dialog.xml │ ├── activity_solution3_embed.xml │ ├── activity_solution_1_and_2.xml │ ├── activity_solution_chooser.xml │ ├── fragment_just_another.xml │ ├── fragment_toasts_demo.xml │ ├── layout_dialog.xml │ ├── layout_request_state.xml │ └── layout_toast.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.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 │ ├── attrs_dialog_showing_view.xml │ ├── attrs_toast_view.xml │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── demo_gifs ├── dialog_solution_1.gif ├── dialog_solution_3.gif ├── toasts_state_problem.gif └── toasts_state_problem_fixed.gif ├── 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 | .cxx 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo project for [Modile People: Open Android Meetup](https://events.epam.com/events/mobile-people-open-android-meetup/talks/12259) 2 | ## Dialogs 3 | The demo project contains all three solutions for dialogs show up. You can run any of them via clicking "Solution #" button. Notice, that for "Solution 3" demo contains both cases with embed view and `DialogShowingView`. 4 | 5 | For instance you can start "Solution 1" like this: 6 | 7 | 8 | 9 | Or you can start "Solution 3" for both scenarios like this: 10 | 11 | 12 | 13 | To simulate "Duplicated state" problem for both solutions 1 and 2 you can follow the next steps: 14 | 1. Open the required solution in the demo application; 15 | 2. Press "Home" button; 16 | 3. Press "Terminate Application" in Logcat window in Android Studio (this will kill the app process, make sure you select your device and process in Logcat dropdowns at top); 17 | 4. Get back to the application through the "Recent apps". 18 | 19 | ## Toasts 20 | The demo project contains demo of the "State problem" for `ToastView`. You can check it following the steps from the gifs below. 21 | 22 | With problem: 23 | 24 | 25 | 26 | With problem fixed: 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion 29 8 | buildToolsVersion "29.0.2" 9 | defaultConfig { 10 | applicationId "by.ve.dialogsbinding" 11 | minSdkVersion 18 12 | targetSdkVersion 29 13 | versionCode 1 14 | versionName "1.0" 15 | } 16 | 17 | dataBinding { 18 | enabled = true 19 | } 20 | 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | 28 | compileOptions { 29 | sourceCompatibility = JavaVersion.VERSION_1_8 30 | targetCompatibility = JavaVersion.VERSION_1_8 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 36 | implementation "androidx.appcompat:appcompat:1.1.0" 37 | implementation "androidx.core:core-ktx:1.1.0" 38 | implementation "androidx.lifecycle:lifecycle-extensions:2.1.0" 39 | implementation "androidx.lifecycle:lifecycle-common-java8:2.1.0" 40 | implementation "androidx.constraintlayout:constraintlayout:1.1.3" 41 | implementation "org.koin:koin-android:2.0.1" 42 | implementation "org.koin:koin-androidx-viewmodel:2.0.1" 43 | implementation "org.greenrobot:eventbus:3.1.1" 44 | implementation "io.reactivex.rxjava2:rxjava:2.2.12" 45 | implementation "io.reactivex.rxjava2:rxandroid:2.1.1" 46 | implementation "io.reactivex.rxjava2:rxkotlin:2.4.0" 47 | } 48 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/App.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding 2 | 3 | import android.app.Application 4 | import androidx.fragment.app.FragmentActivity 5 | import androidx.lifecycle.LifecycleOwner 6 | import by.ve.dialogsbinding.service.FirstTryFailingService 7 | import by.ve.dialogsbinding.ui.demo.chooser.SolutionChooserViewModel 8 | import by.ve.dialogsbinding.ui.demo.dialog.solution1.Solution1ViewModel 9 | import by.ve.dialogsbinding.ui.demo.dialog.solution2.ErrorView 10 | import by.ve.dialogsbinding.ui.demo.dialog.solution2.Solution2ViewModel 11 | import by.ve.dialogsbinding.ui.demo.dialog.solution3.Solution3ViewModel 12 | import by.ve.dialogsbinding.ui.demo.toast.ToastsDemoNavigationViewModel 13 | import by.ve.dialogsbinding.ui.demo.toast.ToastsDemoViewModel 14 | import by.ve.dialogsbinding.ui.dialog.fragment.DialogNavigator 15 | import by.ve.dialogsbinding.ui.dialog.fragment.DialogViewModel 16 | import org.greenrobot.eventbus.EventBus 17 | import org.koin.android.ext.koin.androidContext 18 | import org.koin.androidx.viewmodel.dsl.viewModel 19 | import org.koin.core.context.startKoin 20 | import org.koin.dsl.module 21 | 22 | 23 | class App : Application() { 24 | 25 | override fun onCreate() { 26 | super.onCreate() 27 | 28 | val serviceModule = module { 29 | 30 | factory { FirstTryFailingService() } 31 | } 32 | 33 | val chooserActivityModule = module { 34 | 35 | viewModel { SolutionChooserViewModel() } 36 | } 37 | 38 | val solution1ActivityModule = module { 39 | 40 | viewModel { Solution1ViewModel(get(), get()) } 41 | } 42 | 43 | val solution2ActivityModule = module { 44 | 45 | viewModel { Solution2ViewModel(get()) } 46 | 47 | factory { (activity: FragmentActivity) -> activity } 48 | 49 | factory { params -> ErrorView(get { params }, get { params }, get()) } 50 | } 51 | 52 | val solution3ActivityModule = module { 53 | 54 | viewModel { Solution3ViewModel(get()) } 55 | } 56 | 57 | val toastsDemoModule = module { 58 | 59 | viewModel { ToastsDemoNavigationViewModel() } 60 | 61 | viewModel { ToastsDemoViewModel() } 62 | } 63 | 64 | val dialogModule = module { 65 | 66 | single { EventBus.getDefault() } 67 | 68 | viewModel { DialogViewModel() } 69 | 70 | factory { (activity: FragmentActivity) -> activity.supportFragmentManager } 71 | 72 | factory { params -> DialogNavigator(get { params }) } 73 | } 74 | 75 | startKoin { 76 | androidContext(this@App) 77 | modules( 78 | listOf( 79 | serviceModule, 80 | dialogModule, 81 | chooserActivityModule, 82 | solution1ActivityModule, 83 | solution2ActivityModule, 84 | solution3ActivityModule, 85 | toastsDemoModule 86 | ) 87 | ) 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/databinding/BindingAdapters.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.databinding 2 | 3 | import android.view.View 4 | import androidx.databinding.BindingAdapter 5 | 6 | 7 | @BindingAdapter("onClick") 8 | fun View.onClick(listener: (() -> Unit)?) { 9 | setOnClickListener { 10 | listener?.invoke() 11 | } 12 | } 13 | 14 | @BindingAdapter("visibleOrGone") 15 | fun View.visibleOrGone(isVisible: Boolean?) { 16 | if (isVisible != null) { 17 | visibility = if (isVisible) View.VISIBLE else View.GONE 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/lifecycle/DatabindingSingleLiveEvent.kt: -------------------------------------------------------------------------------- 1 | package com.vmn.playplex.arch 2 | 3 | import android.util.Log 4 | import androidx.annotation.MainThread 5 | import androidx.lifecycle.LifecycleOwner 6 | import androidx.lifecycle.MutableLiveData 7 | import androidx.lifecycle.Observer 8 | import java.util.concurrent.atomic.AtomicBoolean 9 | 10 | /** 11 | * A lifecycle-aware observable that sends only new updates after subscription, used for events like 12 | * navigation and Snackbar messages. 13 | *

14 | * This avoids a common problem with events: on configuration change (like rotation) an update 15 | * can be emitted if the observer is active. This LiveData only calls the observable if there's an 16 | * explicit call to setValue() or call(). 17 | *

18 | * Note that only one observer is going to be notified of changes. 19 | */ 20 | class DatabindingSingleLiveEvent : MutableLiveData() { 21 | 22 | private val pendingNotification = AtomicBoolean(false) 23 | private val pendingValue = AtomicBoolean(false) 24 | 25 | @MainThread 26 | override fun observe(owner: LifecycleOwner, observer: Observer) { 27 | 28 | if (hasActiveObservers()) { 29 | Log.w(TAG, "Multiple observers registered but only one will be notified of changes.") 30 | } 31 | 32 | // Observe the internal MutableLiveData 33 | super.observe(owner, Observer { t -> notifyObserverIfPending(t, observer) }) 34 | } 35 | 36 | override fun observeForever(observer: Observer) { 37 | 38 | if (hasActiveObservers()) { 39 | Log.w(TAG, "Multiple observers registered but only one will be notified of changes.") 40 | } 41 | 42 | // Observe the internal MutableLiveData 43 | super.observeForever { t -> notifyObserverIfPending(t, observer) } 44 | } 45 | 46 | @MainThread 47 | override fun setValue(t: T?) { 48 | pendingNotification.set(true) 49 | pendingValue.set(true) 50 | super.setValue(t) 51 | } 52 | 53 | /** 54 | * Used for cases where T is Void, to make calls cleaner. 55 | */ 56 | @MainThread 57 | fun call() { 58 | value = null 59 | } 60 | 61 | private fun notifyObserverIfPending(t: T?, observer: Observer) { 62 | if (pendingNotification.compareAndSet(true, false)) { 63 | observer.onChanged(t) 64 | } 65 | } 66 | 67 | override fun getValue(): T? = 68 | if (pendingValue.compareAndSet(true, false)) { 69 | super.getValue() 70 | } else { 71 | null 72 | } 73 | 74 | companion object { 75 | 76 | private const val TAG = "BindingSingleLiveEvent" 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/lifecycle/LifecycleExtensions.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.lifecycle 2 | 3 | import androidx.lifecycle.LifecycleOwner 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.Observer 6 | 7 | inline fun LifecycleOwner.observe( 8 | liveData: LiveData, 9 | crossinline observer: (T) -> Unit 10 | ) { 11 | liveData.observe(this, Observer { 12 | observer.invoke(it) 13 | }) 14 | } 15 | 16 | inline fun LifecycleOwner.observeEmptyEvent( 17 | liveData: LiveData, 18 | crossinline observer: () -> Unit 19 | ) { 20 | liveData.observe(this, Observer { 21 | observer.invoke() 22 | }) 23 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/lifecycle/SingleLiveEvent.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.lifecycle 2 | 3 | import android.util.Log 4 | import androidx.annotation.MainThread 5 | import androidx.lifecycle.LifecycleOwner 6 | import androidx.lifecycle.MutableLiveData 7 | import androidx.lifecycle.Observer 8 | import java.util.concurrent.atomic.AtomicBoolean 9 | 10 | 11 | /** 12 | * A lifecycle-aware observable that sends only new updates after subscription, used for events like 13 | * navigation and Snackbar messages. 14 | * 15 | * 16 | * This avoids a common problem with events: on configuration change (like rotation) an update 17 | * can be emitted if the observer is active. This LiveData only calls the observable if there's an 18 | * explicit call to setValue() or call(). 19 | * 20 | * 21 | * Note that only one observer is going to be notified of changes. 22 | */ 23 | class SingleLiveEvent : MutableLiveData() { 24 | 25 | private val mPending = AtomicBoolean(false) 26 | 27 | @MainThread 28 | override fun observe(owner: LifecycleOwner, observer: Observer) { 29 | if (hasActiveObservers()) { 30 | Log.w(TAG, "Multiple observers registered but only one will be notified of changes.") 31 | } 32 | 33 | // Observe the internal MutableLiveData 34 | super.observe(owner, Observer { t -> 35 | if (mPending.compareAndSet(true, false)) { 36 | observer.onChanged(t) 37 | } 38 | }) 39 | } 40 | 41 | @MainThread 42 | override fun setValue(t: T?) { 43 | mPending.set(true) 44 | super.setValue(t) 45 | } 46 | 47 | /** 48 | * Used for cases where T is Void, to make calls cleaner. 49 | */ 50 | @MainThread 51 | fun call() { 52 | value = null 53 | } 54 | 55 | companion object { 56 | 57 | private const val TAG = "SingleLiveEvent" 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/service/FirstTryFailingService.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.service 2 | 3 | import io.reactivex.Completable 4 | import java.util.concurrent.TimeUnit 5 | 6 | private const val DELAY_SECONDS = 2L 7 | 8 | class FirstTryFailingService { 9 | 10 | private var hasAlreadyFailed = false 11 | 12 | fun request(): Completable = if (hasAlreadyFailed) { 13 | Completable.complete() 14 | } else { 15 | hasAlreadyFailed = true 16 | Completable.error(RuntimeException("First call always fails!")) 17 | .delay(DELAY_SECONDS, TimeUnit.SECONDS) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/chooser/SolutionChooserActivity.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.chooser 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.databinding.DataBindingUtil 8 | import by.ve.dialogsbinding.R 9 | import by.ve.dialogsbinding.databinding.ActivitySolutionChooserBinding 10 | import by.ve.dialogsbinding.lifecycle.observe 11 | import by.ve.dialogsbinding.ui.demo.dialog.solution1.Solution1Activity 12 | import by.ve.dialogsbinding.ui.demo.dialog.solution2.Solution2Activity 13 | import by.ve.dialogsbinding.ui.demo.dialog.solution3.ErrorStyle 14 | import by.ve.dialogsbinding.ui.demo.dialog.solution3.Solution3Activity 15 | import by.ve.dialogsbinding.ui.demo.toast.ToastsDemoActivity 16 | import org.koin.androidx.viewmodel.ext.android.viewModel 17 | import kotlin.reflect.KClass 18 | 19 | 20 | class SolutionChooserActivity : AppCompatActivity() { 21 | 22 | private val solutionChooserViewModel by viewModel() 23 | 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | super.onCreate(savedInstanceState) 26 | DataBindingUtil.setContentView( 27 | this, 28 | R.layout.activity_solution_chooser 29 | ).also { 30 | it.lifecycleOwner = this 31 | it.viewModel = solutionChooserViewModel 32 | } 33 | 34 | with(solutionChooserViewModel) { 35 | observe(solution1SelectedEvent) { 36 | startActivity(Solution1Activity::class) 37 | } 38 | observe(solution2SelectedEvent) { 39 | startActivity(Solution2Activity::class) 40 | } 41 | observe(solution3DialogSelectedEvent) { 42 | startSolution3Activity(ErrorStyle.DIALOG) 43 | } 44 | observe(solution3EmbedSelectedEvent) { 45 | startSolution3Activity(ErrorStyle.EMBED) 46 | } 47 | observe(toastsDemoSelectedEvent) { 48 | startActivity(ToastsDemoActivity::class) 49 | } 50 | } 51 | } 52 | 53 | private fun startActivity(clazz: KClass) { 54 | val intent = Intent(this, clazz.java) 55 | startActivity(intent) 56 | } 57 | 58 | private fun startSolution3Activity(errorStyle: ErrorStyle) { 59 | Solution3Activity.start(this, errorStyle) 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/chooser/SolutionChooserViewModel.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.chooser 2 | 3 | import androidx.lifecycle.ViewModel 4 | import by.ve.dialogsbinding.lifecycle.SingleLiveEvent 5 | 6 | 7 | class SolutionChooserViewModel : ViewModel() { 8 | 9 | val solution1SelectedEvent = SingleLiveEvent() 10 | 11 | val solution2SelectedEvent = SingleLiveEvent() 12 | 13 | val solution3DialogSelectedEvent = SingleLiveEvent() 14 | 15 | val solution3EmbedSelectedEvent = SingleLiveEvent() 16 | 17 | val toastsDemoSelectedEvent = SingleLiveEvent() 18 | 19 | fun onSolution1Click() { 20 | solution1SelectedEvent.call() 21 | } 22 | 23 | fun onSolution2Click() { 24 | solution2SelectedEvent.call() 25 | } 26 | 27 | fun onSolution3DialogClick() { 28 | solution3DialogSelectedEvent.call() 29 | } 30 | 31 | fun onSolution3EmbedClick() { 32 | solution3EmbedSelectedEvent.call() 33 | } 34 | 35 | fun onToastsDemoClick() { 36 | toastsDemoSelectedEvent.call() 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/dialog/base/BaseSolutionViewModel.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.dialog.base 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import by.ve.dialogsbinding.R 7 | import by.ve.dialogsbinding.service.FirstTryFailingService 8 | import io.reactivex.android.schedulers.AndroidSchedulers 9 | import io.reactivex.disposables.CompositeDisposable 10 | import io.reactivex.rxkotlin.plusAssign 11 | import io.reactivex.rxkotlin.subscribeBy 12 | 13 | abstract class BaseSolutionViewModel( 14 | private val service: FirstTryFailingService 15 | ) : ViewModel(), SolutionViewModel { 16 | 17 | private val _requestState = MutableLiveData(R.string.request_state_not_done) 18 | 19 | override val requestState: LiveData get() = _requestState 20 | 21 | private val disposables = CompositeDisposable() 22 | 23 | override fun onErrorCancel() { 24 | hideErrorDialog() 25 | } 26 | 27 | override fun onErrorRetry() { 28 | hideErrorDialog() 29 | doRequest() 30 | } 31 | 32 | override fun doRequest() { 33 | disposables += service.request() 34 | .observeOn(AndroidSchedulers.mainThread()) 35 | .subscribeBy( 36 | onComplete = { 37 | _requestState.value = R.string.request_state_success 38 | hideErrorDialog() 39 | }, 40 | onError = { 41 | _requestState.value = R.string.request_state_failed 42 | showErrorDialog() 43 | } 44 | ) 45 | } 46 | 47 | override fun onCleared() { 48 | disposables.dispose() 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/dialog/base/SolutionViewModel.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.dialog.base 2 | 3 | import androidx.lifecycle.LiveData 4 | 5 | interface SolutionViewModel { 6 | 7 | val requestState: LiveData 8 | 9 | fun doRequest() 10 | 11 | fun showErrorDialog() 12 | 13 | fun hideErrorDialog() 14 | 15 | fun onErrorCancel() 16 | 17 | fun onErrorRetry() 18 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/dialog/solution1/DialogControlEvent.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.dialog.solution1 2 | 3 | import by.ve.dialogsbinding.ui.dialog.common.IDialogUiConfig 4 | 5 | sealed class DialogControlEvent { 6 | 7 | data class Show(val tag: String, val uiConfig: IDialogUiConfig) : DialogControlEvent() 8 | 9 | data class Hide(val tag: String) : DialogControlEvent() 10 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/dialog/solution1/Solution1Activity.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.dialog.solution1 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.databinding.DataBindingUtil 6 | import by.ve.dialogsbinding.R 7 | import by.ve.dialogsbinding.databinding.ActivitySolution1And2Binding 8 | import by.ve.dialogsbinding.lifecycle.observe 9 | import by.ve.dialogsbinding.ui.demo.dialog.solution1.DialogControlEvent.Hide 10 | import by.ve.dialogsbinding.ui.demo.dialog.solution1.DialogControlEvent.Show 11 | import by.ve.dialogsbinding.ui.dialog.fragment.DialogNavigator 12 | import org.koin.android.ext.android.inject 13 | import org.koin.androidx.viewmodel.ext.android.viewModel 14 | import org.koin.core.parameter.parametersOf 15 | 16 | class Solution1Activity : AppCompatActivity() { 17 | 18 | private val dialogNavigator: DialogNavigator by inject { parametersOf(this) } 19 | 20 | private val viewModel by viewModel() 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | DataBindingUtil.setContentView( 25 | this, 26 | R.layout.activity_solution_1_and_2 27 | ).also { 28 | it.lifecycleOwner = this 29 | it.viewModel = viewModel 30 | } 31 | observe(viewModel.dialogControlEvent, ::showOrHideDialog) 32 | } 33 | 34 | private fun showOrHideDialog(event: DialogControlEvent) { 35 | when (event) { 36 | is Show -> dialogNavigator.showDialog(event.tag, event.uiConfig) 37 | is Hide -> dialogNavigator.hideDialog(event.tag) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/dialog/solution1/Solution1ViewModel.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.dialog.solution1 2 | 3 | import by.ve.dialogsbinding.lifecycle.SingleLiveEvent 4 | import by.ve.dialogsbinding.service.FirstTryFailingService 5 | import by.ve.dialogsbinding.ui.demo.dialog.base.BaseSolutionViewModel 6 | import by.ve.dialogsbinding.ui.dialog.common.STANDARD_DIALOG_CONFIG 7 | import by.ve.dialogsbinding.ui.dialog.fragment.DialogEvent.NegativeButtonClickEvent 8 | import by.ve.dialogsbinding.ui.dialog.fragment.DialogEvent.PositiveButtonClickEvent 9 | import org.greenrobot.eventbus.EventBus 10 | import org.greenrobot.eventbus.Subscribe 11 | import org.greenrobot.eventbus.ThreadMode 12 | 13 | private const val DIALOG_TAG = "Error" 14 | 15 | class Solution1ViewModel( 16 | service: FirstTryFailingService, 17 | private val dialogEventBus: EventBus 18 | ) : BaseSolutionViewModel(service) { 19 | 20 | val dialogControlEvent = SingleLiveEvent() 21 | 22 | init { 23 | dialogEventBus.register(this) 24 | } 25 | 26 | override fun onCleared() { 27 | dialogEventBus.unregister(this) 28 | } 29 | 30 | @Subscribe(threadMode = ThreadMode.MAIN) 31 | fun onPositiveButtonClick(event: PositiveButtonClickEvent) { 32 | event.doIfTagMatches(DIALOG_TAG, ::onErrorRetry) 33 | } 34 | 35 | @Subscribe(threadMode = ThreadMode.MAIN) 36 | fun onNegativeButtonClick(event: NegativeButtonClickEvent) { 37 | event.doIfTagMatches(DIALOG_TAG, ::onErrorCancel) 38 | } 39 | 40 | override fun showErrorDialog() { 41 | dialogControlEvent.value = DialogControlEvent.Show( 42 | tag = DIALOG_TAG, 43 | uiConfig = STANDARD_DIALOG_CONFIG 44 | ) 45 | } 46 | 47 | override fun hideErrorDialog() { 48 | dialogControlEvent.value = DialogControlEvent.Hide(DIALOG_TAG) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/dialog/solution2/ErrorView.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.dialog.solution2 2 | 3 | import androidx.lifecycle.DefaultLifecycleObserver 4 | import androidx.lifecycle.LifecycleOwner 5 | import by.ve.dialogsbinding.lifecycle.observe 6 | import by.ve.dialogsbinding.ui.dialog.fragment.DialogEvent.NegativeButtonClickEvent 7 | import by.ve.dialogsbinding.ui.dialog.fragment.DialogEvent.PositiveButtonClickEvent 8 | import by.ve.dialogsbinding.ui.dialog.fragment.DialogNavigator 9 | import org.greenrobot.eventbus.EventBus 10 | import org.greenrobot.eventbus.Subscribe 11 | import org.greenrobot.eventbus.ThreadMode 12 | 13 | private const val DIALOG_TAG = "Error" 14 | 15 | class ErrorView( 16 | private val lifecycleOwner: LifecycleOwner, 17 | private val dialogNavigator: DialogNavigator, 18 | private val dialogEventBus: EventBus 19 | ) : DefaultLifecycleObserver { 20 | 21 | var viewModel: Solution2ViewModel? = null 22 | set(value) { 23 | field = value 24 | value?.let { 25 | bind(it) 26 | } 27 | } 28 | 29 | init { 30 | lifecycleOwner.lifecycle.addObserver(this) 31 | } 32 | 33 | override fun onCreate(owner: LifecycleOwner) { 34 | dialogEventBus.register(this) 35 | } 36 | 37 | override fun onDestroy(owner: LifecycleOwner) { 38 | dialogEventBus.unregister(this) 39 | } 40 | 41 | @Subscribe(threadMode = ThreadMode.MAIN) 42 | fun onPositiveButtonClick(event: PositiveButtonClickEvent) { 43 | event.doIfTagMatches(DIALOG_TAG) { viewModel?.onErrorRetry() } 44 | } 45 | 46 | @Subscribe(threadMode = ThreadMode.MAIN) 47 | fun onNegativeButtonClick(event: NegativeButtonClickEvent) { 48 | event.doIfTagMatches(DIALOG_TAG) { viewModel?.onErrorCancel() } 49 | } 50 | 51 | private fun bind(viewModel: Solution2ViewModel) { 52 | with(lifecycleOwner) { 53 | observe(viewModel.isDialogVisible, ::updateErrorDialogState) 54 | } 55 | } 56 | 57 | private fun updateErrorDialogState(visible: Boolean) { 58 | if (visible) { 59 | dialogNavigator.showDialog(DIALOG_TAG, viewModel!!.errorDialogConfig) 60 | } else { 61 | dialogNavigator.hideDialog(DIALOG_TAG) 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/dialog/solution2/Solution2Activity.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.dialog.solution2 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.databinding.DataBindingUtil 6 | import by.ve.dialogsbinding.R 7 | import by.ve.dialogsbinding.databinding.ActivitySolution1And2Binding 8 | import org.koin.android.ext.android.inject 9 | import org.koin.androidx.viewmodel.ext.android.viewModel 10 | import org.koin.core.parameter.parametersOf 11 | 12 | class Solution2Activity : AppCompatActivity() { 13 | 14 | private val errorView: ErrorView by inject { parametersOf(this) } 15 | 16 | private val viewModel by viewModel() 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | DataBindingUtil.setContentView( 21 | this, 22 | R.layout.activity_solution_1_and_2 23 | ).also { 24 | it.lifecycleOwner = this 25 | it.viewModel = viewModel 26 | } 27 | errorView.viewModel = viewModel 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/dialog/solution2/Solution2ViewModel.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.dialog.solution2 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import by.ve.dialogsbinding.service.FirstTryFailingService 5 | import by.ve.dialogsbinding.ui.demo.dialog.base.BaseSolutionViewModel 6 | import by.ve.dialogsbinding.ui.dialog.common.STANDARD_DIALOG_CONFIG 7 | 8 | class Solution2ViewModel( 9 | service: FirstTryFailingService 10 | ) : BaseSolutionViewModel(service) { 11 | 12 | val isDialogVisible = MutableLiveData(false) 13 | 14 | val errorDialogConfig = STANDARD_DIALOG_CONFIG 15 | 16 | override fun showErrorDialog() { 17 | isDialogVisible.value = true 18 | } 19 | 20 | override fun hideErrorDialog() { 21 | isDialogVisible.value = false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/dialog/solution3/DialogViewModel.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.dialog.solution3 2 | 3 | import by.ve.dialogsbinding.ui.dialog.common.IDialogViewModel 4 | 5 | class DialogViewModel( 6 | private val positiveClick: (() -> Unit)? = null, 7 | private val negativeClick: (() -> Unit)? = null 8 | ) : IDialogViewModel { 9 | 10 | override fun onPositiveButtonClick() { 11 | positiveClick?.invoke() 12 | } 13 | 14 | override fun onNegativeButtonClick() { 15 | negativeClick?.invoke() 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/dialog/solution3/Solution3Activity.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.dialog.solution3 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.databinding.DataBindingUtil 8 | import by.ve.dialogsbinding.R 9 | import by.ve.dialogsbinding.databinding.ActivitySolution3DialogBinding 10 | import by.ve.dialogsbinding.databinding.ActivitySolution3EmbedBinding 11 | import org.koin.androidx.viewmodel.ext.android.viewModel 12 | 13 | enum class ErrorStyle { DIALOG, EMBED } 14 | 15 | class Solution3Activity : AppCompatActivity() { 16 | 17 | private val viewModel by viewModel() 18 | 19 | private val errorStyle: ErrorStyle by lazy { intent.getSerializableExtra(EXTRA_ERROR_STYLE) as ErrorStyle } 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | bindContentView() 24 | } 25 | 26 | private fun bindContentView() { 27 | when (errorStyle) { 28 | ErrorStyle.DIALOG -> DataBindingUtil.setContentView( 29 | this, 30 | R.layout.activity_solution3_dialog 31 | ).also { 32 | it.viewModel = viewModel 33 | } 34 | ErrorStyle.EMBED -> DataBindingUtil.setContentView( 35 | this, 36 | R.layout.activity_solution3_embed 37 | ).also { 38 | it.viewModel = viewModel 39 | } 40 | }.lifecycleOwner = this 41 | } 42 | 43 | companion object { 44 | 45 | private const val EXTRA_ERROR_STYLE = "EXTRA_ERROR_STYLE" 46 | 47 | fun start(context: Context, errorStyle: ErrorStyle) { 48 | val intent = Intent(context, Solution3Activity::class.java) 49 | .putExtra(EXTRA_ERROR_STYLE, errorStyle) 50 | context.startActivity(intent) 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/dialog/solution3/Solution3ViewModel.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.dialog.solution3 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import by.ve.dialogsbinding.service.FirstTryFailingService 5 | import by.ve.dialogsbinding.ui.demo.dialog.base.BaseSolutionViewModel 6 | import by.ve.dialogsbinding.ui.dialog.common.STANDARD_DIALOG_CONFIG 7 | 8 | class Solution3ViewModel( 9 | service: FirstTryFailingService 10 | ) : BaseSolutionViewModel(service) { 11 | 12 | val isDialogVisible = MutableLiveData(false) 13 | 14 | val errorDialogConfig = STANDARD_DIALOG_CONFIG 15 | 16 | val errorDialogViewModel = DialogViewModel( 17 | positiveClick = ::onErrorRetry, 18 | negativeClick = ::onErrorCancel 19 | ) 20 | 21 | override fun showErrorDialog() { 22 | isDialogVisible.value = true 23 | } 24 | 25 | override fun hideErrorDialog() { 26 | isDialogVisible.value = false 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/toast/JustAnotherFragment.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.toast 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.databinding.DataBindingUtil 7 | import androidx.fragment.app.Fragment 8 | import by.ve.dialogsbinding.R 9 | import by.ve.dialogsbinding.databinding.FragmentJustAnotherBinding 10 | import org.koin.androidx.viewmodel.ext.android.sharedViewModel 11 | 12 | class JustAnotherFragment : Fragment() { 13 | 14 | private val viewModel by sharedViewModel() 15 | 16 | override fun onCreateView( 17 | inflater: LayoutInflater, 18 | container: ViewGroup?, 19 | savedInstanceState: Bundle? 20 | ) = DataBindingUtil.inflate( 21 | inflater, 22 | R.layout.fragment_just_another, 23 | container, 24 | false 25 | ).also { 26 | it.lifecycleOwner = viewLifecycleOwner 27 | it.viewModel = viewModel 28 | }.root 29 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/toast/ToastsDemoActivity.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.toast 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import by.ve.dialogsbinding.lifecycle.observe 6 | import org.koin.androidx.viewmodel.ext.android.viewModel 7 | 8 | class ToastsDemoActivity : AppCompatActivity() { 9 | 10 | private val viewModel by viewModel() 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | 15 | if (savedInstanceState == null) { 16 | supportFragmentManager.beginTransaction() 17 | .replace(android.R.id.content, ToastsDemoFragment()) 18 | .commit() 19 | } 20 | 21 | observe(viewModel.backEvent) { 22 | onBackPressed() 23 | } 24 | observe(viewModel.showAnotherFragmentEvent) { 25 | supportFragmentManager.beginTransaction() 26 | .replace(android.R.id.content, JustAnotherFragment()) 27 | .addToBackStack(null) 28 | .commit() 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/toast/ToastsDemoFragment.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.toast 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.databinding.DataBindingUtil 7 | import androidx.fragment.app.Fragment 8 | import by.ve.dialogsbinding.R 9 | import by.ve.dialogsbinding.databinding.FragmentToastsDemoBinding 10 | import org.koin.androidx.viewmodel.ext.android.sharedViewModel 11 | import org.koin.androidx.viewmodel.ext.android.viewModel 12 | 13 | class ToastsDemoFragment : Fragment() { 14 | 15 | private val navigationViewModel by sharedViewModel() 16 | 17 | private val viewModel by viewModel() 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, 21 | container: ViewGroup?, 22 | savedInstanceState: Bundle? 23 | ) = DataBindingUtil.inflate( 24 | inflater, 25 | R.layout.fragment_toasts_demo, 26 | container, 27 | false 28 | ).also { 29 | it.lifecycleOwner = viewLifecycleOwner 30 | it.navigationViewModel = navigationViewModel 31 | it.viewModel = viewModel 32 | }.root 33 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/toast/ToastsDemoNavigationViewModel.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.toast 2 | 3 | import androidx.lifecycle.ViewModel 4 | import by.ve.dialogsbinding.lifecycle.SingleLiveEvent 5 | 6 | class ToastsDemoNavigationViewModel : ViewModel() { 7 | 8 | val showAnotherFragmentEvent = SingleLiveEvent() 9 | 10 | val backEvent = SingleLiveEvent() 11 | 12 | fun onShowAnotherFragmentClick() { 13 | showAnotherFragmentEvent.call() 14 | } 15 | 16 | fun onBackClick() { 17 | backEvent.call() 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/demo/toast/ToastsDemoViewModel.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.demo.toast 2 | 3 | import androidx.lifecycle.ViewModel 4 | import by.ve.dialogsbinding.R 5 | import by.ve.dialogsbinding.lifecycle.SingleLiveEvent 6 | import by.ve.dialogsbinding.ui.toast.ToastDisplaySignal 7 | import by.ve.dialogsbinding.ui.toast.ToastViewModel 8 | import com.vmn.playplex.arch.DatabindingSingleLiveEvent 9 | 10 | class ToastsDemoViewModel : ViewModel() { 11 | 12 | val showToastBrokenEvent = SingleLiveEvent() 13 | 14 | val showToastFixedEvent = DatabindingSingleLiveEvent() 15 | 16 | val toastBrokenViewModel = ToastViewModel( 17 | icon = R.mipmap.ic_launcher_round, 18 | text = R.string.toast_broken_navigation 19 | ) 20 | 21 | val toastFixedViewModel = ToastViewModel( 22 | icon = R.mipmap.ic_launcher_round, 23 | text = R.string.toast_fixed_navigation 24 | ) 25 | 26 | fun onShowToastBrokenClick() { 27 | showToastBrokenEvent.value = ToastDisplaySignal 28 | } 29 | 30 | fun onShowToastFixedClick() { 31 | showToastFixedEvent.value = ToastDisplaySignal 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/dialog/common/DialogUiConfig.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.dialog.common 2 | 3 | import androidx.annotation.StringRes 4 | import by.ve.dialogsbinding.R 5 | import kotlinx.android.parcel.Parcelize 6 | 7 | val STANDARD_DIALOG_CONFIG = DialogUiConfig( 8 | title = R.string.error_title, 9 | message = R.string.error_message, 10 | positiveButtonText = R.string.error_retry, 11 | negativeButtonText = R.string.error_cancel 12 | ) 13 | 14 | @Parcelize 15 | data class DialogUiConfig( 16 | @StringRes override val title: Int, 17 | @StringRes override val message: Int, 18 | @StringRes override val positiveButtonText: Int? = null, 19 | @StringRes override val negativeButtonText: Int? = null 20 | ) : IDialogUiConfig -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/dialog/common/IDialogUiConfig.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.dialog.common 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.StringRes 5 | 6 | interface IDialogUiConfig : Parcelable { 7 | 8 | @get:StringRes 9 | val title: Int 10 | 11 | @get:StringRes 12 | val message: Int 13 | 14 | @get:StringRes 15 | val positiveButtonText: Int? 16 | 17 | @get:StringRes 18 | val negativeButtonText: Int? 19 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/dialog/common/IDialogViewModel.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.dialog.common 2 | 3 | 4 | interface IDialogViewModel { 5 | fun onPositiveButtonClick() 6 | fun onNegativeButtonClick() 7 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/dialog/fragment/CommonDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.dialog.fragment 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import android.widget.FrameLayout 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.databinding.ViewDataBinding 9 | import androidx.fragment.app.DialogFragment 10 | import by.ve.dialogsbinding.BR 11 | import by.ve.dialogsbinding.R 12 | import by.ve.dialogsbinding.lifecycle.observeEmptyEvent 13 | import by.ve.dialogsbinding.ui.dialog.common.DialogUiConfig 14 | import by.ve.dialogsbinding.ui.dialog.common.IDialogUiConfig 15 | import by.ve.dialogsbinding.ui.dialog.fragment.DialogEvent.NegativeButtonClickEvent 16 | import by.ve.dialogsbinding.ui.dialog.fragment.DialogEvent.PositiveButtonClickEvent 17 | import org.greenrobot.eventbus.EventBus 18 | import org.koin.android.ext.android.inject 19 | import org.koin.androidx.viewmodel.ext.android.viewModel 20 | 21 | private const val EXTRA_UI_CONFIG = "EXTRA_UI_CONFIG" 22 | 23 | class CommonDialogFragment : DialogFragment() { 24 | 25 | companion object { 26 | 27 | fun newInstance(uiConfig: IDialogUiConfig) = CommonDialogFragment().apply { 28 | arguments = Bundle().apply { 29 | putParcelable(EXTRA_UI_CONFIG, uiConfig) 30 | } 31 | } 32 | } 33 | 34 | private val dialogEventBus by inject() 35 | 36 | private val viewModel by viewModel() 37 | 38 | private val uiConfig by lazy { arguments!!.getParcelable(EXTRA_UI_CONFIG) } 39 | 40 | override fun onCreate(savedInstanceState: Bundle?) { 41 | super.onCreate(savedInstanceState) 42 | isCancelable = false 43 | observeEmptyEvent(viewModel.positiveButtonClickEvent) { 44 | dialogEventBus.post(PositiveButtonClickEvent(tag!!)) 45 | } 46 | observeEmptyEvent(viewModel.negativeButtonClickEvent) { 47 | dialogEventBus.post(NegativeButtonClickEvent(tag!!)) 48 | } 49 | } 50 | 51 | override fun onCreateView( 52 | inflater: LayoutInflater, 53 | container: ViewGroup?, 54 | savedInstanceState: Bundle? 55 | ) = FrameLayout(requireActivity()).also { 56 | DataBindingUtil.inflate(inflater, R.layout.layout_dialog, it, true) 57 | ?.apply { 58 | lifecycleOwner = viewLifecycleOwner 59 | setVariable(BR.viewModel, viewModel) 60 | setVariable(BR.uiConfig, uiConfig) 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/dialog/fragment/DialogEvent.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.dialog.fragment 2 | 3 | 4 | sealed class DialogEvent(private val dialogTag: String) { 5 | 6 | fun doIfTagMatches(expectedTag: String, action: () -> Unit) { 7 | if (expectedTag == dialogTag) { 8 | action.invoke() 9 | } 10 | } 11 | 12 | class PositiveButtonClickEvent(dialogTag: String) : DialogEvent(dialogTag) 13 | 14 | class NegativeButtonClickEvent(dialogTag: String) : DialogEvent(dialogTag) 15 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/dialog/fragment/DialogNavigator.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.dialog.fragment 2 | 3 | import androidx.fragment.app.FragmentManager 4 | import by.ve.dialogsbinding.ui.dialog.common.IDialogUiConfig 5 | 6 | 7 | class DialogNavigator(private val fragmentManager: FragmentManager) { 8 | 9 | fun showDialog(tag: String, uiConfig: IDialogUiConfig) { 10 | if (fragmentManager.isFragmentNotExist(tag)) { 11 | CommonDialogFragment.newInstance(uiConfig).show(fragmentManager, tag) 12 | } 13 | } 14 | 15 | fun hideDialog(tag: String) { 16 | val fragment = fragmentManager.findFragmentByTag(tag) 17 | if (fragment != null) { 18 | fragmentManager.beginTransaction().remove(fragment).commit() 19 | } 20 | } 21 | 22 | private fun FragmentManager.isFragmentNotExist(tag: String) = 23 | findFragmentByTag(tag) == null 24 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/dialog/fragment/DialogViewModel.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.dialog.fragment 2 | 3 | import androidx.lifecycle.ViewModel 4 | import by.ve.dialogsbinding.lifecycle.SingleLiveEvent 5 | import by.ve.dialogsbinding.ui.dialog.common.IDialogViewModel 6 | 7 | 8 | class DialogViewModel : ViewModel(), IDialogViewModel { 9 | 10 | val positiveButtonClickEvent = SingleLiveEvent() 11 | 12 | val negativeButtonClickEvent = SingleLiveEvent() 13 | 14 | override fun onPositiveButtonClick() { 15 | positiveButtonClickEvent.call() 16 | } 17 | 18 | override fun onNegativeButtonClick() { 19 | negativeButtonClickEvent.call() 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/dialog/view/DialogShowingView.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.dialog.view 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Dialog 5 | import android.content.Context 6 | import android.util.AttributeSet 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.widget.FrameLayout 10 | import androidx.annotation.AttrRes 11 | import androidx.annotation.LayoutRes 12 | import androidx.annotation.StyleRes 13 | import androidx.core.content.res.use 14 | import androidx.databinding.BindingAdapter 15 | import androidx.databinding.DataBindingUtil 16 | import androidx.databinding.ViewDataBinding 17 | import by.ve.dialogsbinding.BR 18 | import by.ve.dialogsbinding.R 19 | import by.ve.dialogsbinding.ui.dialog.common.DialogUiConfig 20 | import by.ve.dialogsbinding.ui.dialog.common.IDialogViewModel 21 | 22 | private const val EMPTY_RESOURCE = -1 23 | 24 | @SuppressLint("Recycle") 25 | class DialogShowingView @JvmOverloads constructor( 26 | context: Context, 27 | attrs: AttributeSet? = null, 28 | @AttrRes defStyleAttr: Int = 0 29 | ) : View(context, attrs) { 30 | 31 | private lateinit var dialog: Dialog 32 | 33 | private lateinit var binding: ViewDataBinding 34 | 35 | private var dialogVisibility: Int = GONE 36 | set(value) { 37 | field = value 38 | if (value == VISIBLE) { 39 | dialog.show() 40 | } else { 41 | dialog.dismiss() 42 | } 43 | } 44 | 45 | var bindingData: Pair? = null 46 | set(value) { 47 | field = value 48 | value?.let { (config, viewModel) -> 49 | binding.setVariable(BR.uiConfig, config) 50 | binding.setVariable(BR.viewModel, viewModel) 51 | } 52 | } 53 | 54 | init { 55 | context.obtainStyledAttributes(attrs, R.styleable.DialogShowingView, defStyleAttr, 0).use { 56 | @StyleRes 57 | val dialogStyle = 58 | it.getResourceId(R.styleable.DialogShowingView_dialogStyle, EMPTY_RESOURCE) 59 | require(dialogStyle != EMPTY_RESOURCE) { 60 | "Dialog style must be defined" 61 | } 62 | 63 | @LayoutRes 64 | val dialogLayout = 65 | it.getResourceId(R.styleable.DialogShowingView_dialogLayout, EMPTY_RESOURCE) 66 | require(dialogLayout != EMPTY_RESOURCE) { 67 | "Dialog layout must be defined" 68 | } 69 | 70 | createDialog(context, dialogLayout, dialogStyle) 71 | } 72 | } 73 | 74 | private fun createDialog(context: Context, @LayoutRes dialogLayout: Int, @StyleRes dialogStyle: Int) { 75 | val frameLayout = FrameLayout(context) 76 | 77 | binding = DataBindingUtil.inflate( 78 | LayoutInflater.from(context), 79 | dialogLayout, 80 | frameLayout, 81 | true 82 | ) 83 | 84 | dialog = Dialog(context, dialogStyle).apply { 85 | setContentView(frameLayout) 86 | setCancelable(false) 87 | } 88 | } 89 | 90 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 91 | setMeasuredDimension(0, 0) 92 | } 93 | 94 | override fun setVisibility(visibility: Int) { 95 | dialogVisibility = visibility 96 | } 97 | 98 | override fun getVisibility() = dialogVisibility 99 | 100 | /** 101 | * Sometimes while showing the dialog we need to replace its holder fragment or activity. In this case we 102 | * need to dismiss dialog. 103 | */ 104 | override fun onDetachedFromWindow() { 105 | dialog.dismiss() 106 | super.onDetachedFromWindow() 107 | } 108 | } 109 | 110 | @BindingAdapter(value = ["dialogConfig", "dialogViewModel"], requireAll = false) 111 | fun DialogShowingView.bindTextAndActions( 112 | dialogConfig: DialogUiConfig? = null, 113 | dialogViewModel: IDialogViewModel? = null 114 | ) { 115 | bindingData = Pair(dialogConfig, dialogViewModel) 116 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/toast/ToastShowingView.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.toast 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.util.AttributeSet 6 | import android.view.Gravity 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.widget.Toast 10 | import androidx.annotation.AttrRes 11 | import androidx.annotation.LayoutRes 12 | import androidx.core.content.res.use 13 | import androidx.databinding.BindingAdapter 14 | import androidx.databinding.DataBindingUtil 15 | import androidx.databinding.ViewDataBinding 16 | import by.ve.dialogsbinding.BR 17 | import by.ve.dialogsbinding.R 18 | 19 | object ToastDisplaySignal 20 | 21 | private const val EMPTY_RESOURCE = -1 22 | private const val DEFAULT_OFFSET = 0 23 | 24 | @SuppressLint("Recycle") 25 | class ToastShowingView @JvmOverloads constructor( 26 | context: Context, 27 | attrs: AttributeSet? = null, 28 | @AttrRes defStyleAttr: Int = 0 29 | ) : View(context, attrs, defStyleAttr) { 30 | 31 | private lateinit var toast: Toast 32 | 33 | private lateinit var binding: ViewDataBinding 34 | 35 | var bindingData: ToastViewModel? = null 36 | set(value) { 37 | field = value 38 | value?.let { it -> 39 | binding.setVariable(BR.viewModel, it) 40 | } 41 | } 42 | 43 | init { 44 | context.obtainStyledAttributes(attrs, R.styleable.ToastShowingView).use { 45 | @LayoutRes 46 | val layoutResId = it.getResourceId(R.styleable.ToastShowingView_layout, EMPTY_RESOURCE) 47 | require(layoutResId != EMPTY_RESOURCE) { 48 | "Toast layout must be provided!" 49 | } 50 | 51 | val xOffset = 52 | it.getDimensionPixelSize(R.styleable.ToastShowingView_xOffset, DEFAULT_OFFSET) 53 | val yOffset = 54 | it.getDimensionPixelSize(R.styleable.ToastShowingView_yOffset, DEFAULT_OFFSET) 55 | 56 | val gravity = it.getInteger(R.styleable.ToastShowingView_gravity, Gravity.NO_GRAVITY) 57 | val duration = it.getInteger(R.styleable.ToastShowingView_duration, Toast.LENGTH_SHORT) 58 | 59 | createToast(context, layoutResId, gravity, xOffset, yOffset, duration) 60 | } 61 | } 62 | 63 | fun show() { 64 | toast.show() 65 | } 66 | 67 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 68 | setMeasuredDimension(0, 0) 69 | } 70 | 71 | override fun onDetachedFromWindow() { 72 | toast.cancel() 73 | super.onDetachedFromWindow() 74 | } 75 | 76 | private fun createToast( 77 | context: Context, 78 | @LayoutRes layoutResId: Int, 79 | gravity: Int, 80 | xOffset: Int, 81 | yOffset: Int, 82 | durationFlag: Int 83 | ) { 84 | binding = DataBindingUtil.inflate( 85 | LayoutInflater.from(context), 86 | layoutResId, 87 | null, 88 | false 89 | ) 90 | toast = Toast(context).apply { 91 | setGravity(gravity, xOffset, yOffset) 92 | duration = durationFlag 93 | view = binding.root 94 | } 95 | } 96 | } 97 | 98 | @BindingAdapter("toastDisplaySignal") 99 | fun ToastShowingView.show(signal: ToastDisplaySignal?) { 100 | signal?.let { show() } 101 | } 102 | 103 | @BindingAdapter("toastViewModel") 104 | fun ToastShowingView.bindViewModel(viewModel: ToastViewModel?) { 105 | if (viewModel != null) { 106 | bindingData = viewModel 107 | } 108 | } -------------------------------------------------------------------------------- /app/src/main/java/by/ve/dialogsbinding/ui/toast/ToastViewModel.kt: -------------------------------------------------------------------------------- 1 | package by.ve.dialogsbinding.ui.toast 2 | 3 | import androidx.annotation.DrawableRes 4 | import androidx.annotation.StringRes 5 | 6 | data class ToastViewModel(@DrawableRes val icon: Int, @StringRes val text: Int) -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_solution3_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 16 | 17 | 22 | 23 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_solution3_embed.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 16 | 17 | 22 | 23 | 37 | 38 |