├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── zj
│ │ └── architecture
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── zj
│ │ │ └── architecture
│ │ │ ├── MyApp.kt
│ │ │ ├── RouterActivity.kt
│ │ │ ├── login
│ │ │ ├── LoginViewState.kt
│ │ │ ├── flow
│ │ │ │ ├── FlowLoginActivity.kt
│ │ │ │ └── LoginViewModel.kt
│ │ │ └── livedata
│ │ │ │ ├── LoginActivity.kt
│ │ │ │ └── LoginViewModel.kt
│ │ │ ├── mainscreen
│ │ │ ├── MainActivity.kt
│ │ │ ├── MainViewModel.kt
│ │ │ ├── MainViewStates.kt
│ │ │ └── NewsRvAdapter.kt
│ │ │ ├── mockapi
│ │ │ ├── MockApi.kt
│ │ │ ├── MockApiResponse.kt
│ │ │ └── MockInterceptor.kt
│ │ │ ├── network
│ │ │ ├── CoroutineScopeHelper.kt
│ │ │ ├── NetworkExceptionHandler.kt
│ │ │ └── NetworkExt.kt
│ │ │ ├── networkscreen
│ │ │ ├── FlowViewModel.kt
│ │ │ ├── NetworkActivity.kt
│ │ │ └── NetworkViewState.kt
│ │ │ ├── repository
│ │ │ ├── NewsItem.kt
│ │ │ └── NewsRepository.kt
│ │ │ └── utils
│ │ │ └── AppUtils.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── activity_network.xml
│ │ ├── activity_router.xml
│ │ ├── item_view.xml
│ │ ├── layout_error.xml
│ │ ├── layout_loading.xml
│ │ └── layout_login.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── bg_error.png
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── test
│ └── java
│ └── com
│ └── zj
│ └── architecture
│ └── ExampleUnitTest.kt
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── mvi-core
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── zj
│ │ └── mvi
│ │ └── core
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── zj
│ │ └── mvi
│ │ └── core
│ │ ├── LiveEvents.kt
│ │ ├── MVIExt.kt
│ │ ├── MVIFlowExt.kt
│ │ └── SingleLiveEvents.kt
│ └── test
│ └── java
│ └── com
│ └── zj
│ └── mvi
│ └── core
│ └── ExampleUnitTest.kt
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | .idea
4 | /local.properties
5 | /.idea/caches
6 | /.idea/libraries
7 | /.idea/modules.xml
8 | /.idea/workspace.xml
9 | /.idea/navEditor.xml
10 | /.idea/assetWizardSettings.xml
11 | .DS_Store
12 | /build
13 | /captures
14 | .externalNativeBuild
15 | .cxx
16 | local.properties
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Ricardo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Android架构更佳实践
2 | 本项目基于`MVI`架构,探索`Android`开发架构更佳实践,主要包括以下内容
3 | - [MVVM 进阶版:MVI 架构了解一下~](https://juejin.cn/post/7022624191723601928)
4 | - [MVI 架构更佳实践:支持 LiveData 属性监听](https://juejin.cn/post/7025222741322121223)
5 | - [MVI 架构封装:快速优雅地实现网络请求](https://juejin.cn/post/7027815347281477645)
6 |
7 | ## License
8 | ```
9 | MIT License
10 |
11 | Copyright (c) 2022 Ricardo
12 |
13 | Permission is hereby granted, free of charge, to any person obtaining a copy
14 | of this software and associated documentation files (the "Software"), to deal
15 | in the Software without restriction, including without limitation the rights
16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 | copies of the Software, and to permit persons to whom the Software is
18 | furnished to do so, subject to the following conditions:
19 |
20 | The above copyright notice and this permission notice shall be included in all
21 | copies or substantial portions of the Software.
22 |
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29 | SOFTWARE.
30 | ```
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'kotlin-kapt'
5 | id 'kotlin-android-extensions'
6 | }
7 |
8 | android {
9 | compileSdk 31
10 |
11 | defaultConfig {
12 | applicationId "com.zj.architecture"
13 | minSdk 21
14 | targetSdk 31
15 | versionCode 1
16 | versionName "1.0"
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 | compileOptions {
28 | sourceCompatibility JavaVersion.VERSION_1_8
29 | targetCompatibility JavaVersion.VERSION_1_8
30 | }
31 | kotlinOptions {
32 | jvmTarget = '1.8'
33 | }
34 | }
35 |
36 | dependencies {
37 |
38 | implementation 'androidx.core:core-ktx:1.6.0'
39 | implementation 'androidx.appcompat:appcompat:1.3.1'
40 | implementation 'com.google.android.material:material:1.4.0'
41 | implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
42 | implementation("androidx.recyclerview:recyclerview:1.2.1")
43 | implementation("com.google.android.material:material:1.4.0")
44 | implementation("androidx.activity:activity-ktx:1.3.1")
45 | implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
46 |
47 | //ViewModel and LiveData
48 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1")
49 | implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.1")
50 | kapt("androidx.lifecycle:lifecycle-common-java8:2.4.1")
51 |
52 | //Coroutines
53 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1")
54 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1")
55 |
56 | //Image Loading
57 | implementation("io.coil-kt:coil:0.8.0")
58 |
59 | //Webservices
60 | implementation("com.squareup.retrofit2:retrofit:2.6.2")
61 | implementation("com.squareup.retrofit2:converter-gson:2.6.2")
62 | implementation 'com.github.liangjingkanji:StateLayout:1.2.0'
63 | implementation(project(":mvi-core"))
64 | }
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/zj/architecture/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.zj.architecture", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/MyApp.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture
2 |
3 | import android.app.Application
4 | import com.drake.statelayout.StateConfig
5 |
6 | class MyApp : Application() {
7 |
8 | companion object {
9 | lateinit var instance: MyApp
10 |
11 | fun get(): MyApp {
12 | return instance
13 | }
14 | }
15 |
16 | override fun onCreate() {
17 | super.onCreate()
18 | instance = this
19 | StateConfig.apply {
20 | loadingLayout = R.layout.layout_loading
21 | errorLayout = R.layout.layout_error
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/RouterActivity.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.appcompat.app.AppCompatActivity
7 | import com.zj.architecture.login.flow.FlowLoginActivity
8 | import com.zj.architecture.login.livedata.LoginActivity
9 | import com.zj.architecture.mainscreen.MainActivity
10 | import com.zj.architecture.networkscreen.NetworkActivity
11 |
12 | class RouterActivity : AppCompatActivity() {
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | super.onCreate(savedInstanceState)
15 | setContentView(R.layout.activity_router)
16 | }
17 |
18 | fun simple(view: View) {
19 | startActivity(Intent(this, MainActivity::class.java))
20 | }
21 |
22 | fun network(view: View) {
23 | startActivity(Intent(this, NetworkActivity::class.java))
24 | }
25 |
26 | fun login(view: View) {
27 | startActivity(Intent(this, LoginActivity::class.java))
28 | }
29 |
30 | fun loginFlow(view: View) {
31 | startActivity(Intent(this, FlowLoginActivity::class.java))
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/login/LoginViewState.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.login
2 |
3 | import com.zj.architecture.networkscreen.NetworkViewEvent
4 |
5 | data class LoginViewState(val userName: String = "", val password: String = "") {
6 | val isLoginEnable: Boolean
7 | get() = userName.isNotEmpty() && password.length >= 6
8 | val passwordTipVisible: Boolean
9 | get() = password.length in 1..5
10 | }
11 |
12 | sealed class LoginViewEvent {
13 | data class ShowToast(val message: String) : LoginViewEvent()
14 | object ShowLoadingDialog : LoginViewEvent()
15 | object DismissLoadingDialog : LoginViewEvent()
16 | }
17 |
18 | sealed class LoginViewAction {
19 | data class UpdateUserName(val userName: String) : LoginViewAction()
20 | data class UpdatePassword(val password: String) : LoginViewAction()
21 | object Login : LoginViewAction()
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/login/flow/FlowLoginActivity.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.login.flow
2 |
3 | import android.app.ProgressDialog
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.activity.viewModels
7 | import androidx.appcompat.app.AppCompatActivity
8 | import androidx.core.widget.addTextChangedListener
9 | import com.zj.architecture.R
10 | import com.zj.architecture.login.LoginViewAction
11 | import com.zj.architecture.login.LoginViewEvent
12 | import com.zj.architecture.login.LoginViewState
13 | import com.zj.architecture.utils.toast
14 | import com.zj.mvi.core.observeEvent
15 | import com.zj.mvi.core.observeState
16 | import kotlinx.android.synthetic.main.layout_login.*
17 |
18 | class FlowLoginActivity : AppCompatActivity() {
19 | private val viewModel by viewModels()
20 | override fun onCreate(savedInstanceState: Bundle?) {
21 | super.onCreate(savedInstanceState)
22 | setContentView(R.layout.layout_login)
23 | initView()
24 | initViewStates()
25 | initViewEvents()
26 | }
27 |
28 | private fun initView() {
29 | edit_user_name.addTextChangedListener {
30 | viewModel.dispatch(LoginViewAction.UpdateUserName(it.toString()))
31 | }
32 | edit_password.addTextChangedListener {
33 | viewModel.dispatch(LoginViewAction.UpdatePassword(it.toString()))
34 | }
35 | btn_login.setOnClickListener {
36 | viewModel.dispatch(LoginViewAction.Login)
37 | }
38 | }
39 |
40 | private fun initViewStates() {
41 | viewModel.viewStates.let { states ->
42 | states.observeState(this, LoginViewState::userName) {
43 | edit_user_name.setText(it)
44 | edit_user_name.setSelection(it.length)
45 | }
46 | states.observeState(this, LoginViewState::password) {
47 | edit_password.setText(it)
48 | edit_password.setSelection(it.length)
49 | }
50 | states.observeState(this, LoginViewState::isLoginEnable) {
51 | btn_login.isEnabled = it
52 | btn_login.alpha = if (it) 1f else 0.5f
53 | }
54 | states.observeState(this, LoginViewState::passwordTipVisible) {
55 | tv_label.visibility = if (it) View.VISIBLE else View.INVISIBLE
56 | }
57 | }
58 | }
59 |
60 | private fun initViewEvents() {
61 | viewModel.viewEvents.observeEvent(this) {
62 | when (it) {
63 | is LoginViewEvent.ShowToast -> toast(it.message)
64 | is LoginViewEvent.ShowLoadingDialog -> showLoadingDialog()
65 | is LoginViewEvent.DismissLoadingDialog -> dismissLoadingDialog()
66 | }
67 | }
68 | }
69 |
70 | private var progressDialog: ProgressDialog? = null
71 |
72 | private fun showLoadingDialog() {
73 | if (progressDialog == null)
74 | progressDialog = ProgressDialog(this)
75 | progressDialog?.show()
76 | }
77 |
78 | private fun dismissLoadingDialog() {
79 | progressDialog?.takeIf { it.isShowing }?.dismiss()
80 | }
81 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/login/flow/LoginViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.login.flow
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.zj.architecture.login.LoginViewAction
6 | import com.zj.architecture.login.LoginViewEvent
7 | import com.zj.architecture.login.LoginViewState
8 | import com.zj.mvi.core.SharedFlowEvents
9 | import com.zj.mvi.core.setEvent
10 | import com.zj.mvi.core.setState
11 | import com.zj.mvi.core.withState
12 | import kotlinx.coroutines.delay
13 | import kotlinx.coroutines.flow.*
14 | import kotlinx.coroutines.launch
15 |
16 | class LoginViewModel : ViewModel() {
17 | private val _viewStates = MutableStateFlow(LoginViewState())
18 | val viewStates = _viewStates.asStateFlow()
19 | private val _viewEvents = SharedFlowEvents()
20 | val viewEvents = _viewEvents.asSharedFlow()
21 |
22 | fun dispatch(viewAction: LoginViewAction) {
23 | when (viewAction) {
24 | is LoginViewAction.UpdateUserName -> updateUserName(viewAction.userName)
25 | is LoginViewAction.UpdatePassword -> updatePassword(viewAction.password)
26 | is LoginViewAction.Login -> login()
27 | }
28 | }
29 |
30 | private fun updateUserName(userName: String) {
31 | _viewStates.setState { copy(userName = userName) }
32 | }
33 |
34 | private fun updatePassword(password: String) {
35 | _viewStates.setState { copy(password = password) }
36 | }
37 |
38 | private fun login() {
39 | viewModelScope.launch {
40 | flow {
41 | loginLogic()
42 | emit("登录成功")
43 | }.onStart {
44 | _viewEvents.setEvent(LoginViewEvent.ShowLoadingDialog)
45 | }.onEach {
46 | _viewEvents.setEvent(
47 | LoginViewEvent.DismissLoadingDialog, LoginViewEvent.ShowToast(it)
48 | )
49 | }.catch {
50 | _viewStates.setState { copy(password = "") }
51 | _viewEvents.setEvent(
52 | LoginViewEvent.DismissLoadingDialog, LoginViewEvent.ShowToast("登录失败")
53 | )
54 | }.collect()
55 | }
56 | }
57 |
58 | private suspend fun loginLogic() {
59 | withState(viewStates) {
60 | val userName = it.userName
61 | val password = it.password
62 | delay(2000)
63 | throw Exception("登录失败")
64 | "$userName,$password"
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/login/livedata/LoginActivity.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.login.livedata
2 |
3 | import android.app.ProgressDialog
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.activity.viewModels
7 | import androidx.appcompat.app.AppCompatActivity
8 | import androidx.core.widget.addTextChangedListener
9 | import com.zj.architecture.R
10 | import com.zj.architecture.login.LoginViewAction
11 | import com.zj.architecture.login.LoginViewEvent
12 | import com.zj.architecture.login.LoginViewState
13 | import com.zj.mvi.core.observeEvent
14 | import com.zj.mvi.core.observeState
15 | import com.zj.architecture.utils.toast
16 | import kotlinx.android.synthetic.main.layout_login.*
17 |
18 | class LoginActivity : AppCompatActivity() {
19 | private val viewModel by viewModels()
20 | override fun onCreate(savedInstanceState: Bundle?) {
21 | super.onCreate(savedInstanceState)
22 | setContentView(R.layout.layout_login)
23 | initView()
24 | initViewStates()
25 | initViewEvents()
26 | }
27 |
28 | private fun initView() {
29 | edit_user_name.addTextChangedListener {
30 | viewModel.dispatch(LoginViewAction.UpdateUserName(it.toString()))
31 | }
32 | edit_password.addTextChangedListener {
33 | viewModel.dispatch(LoginViewAction.UpdatePassword(it.toString()))
34 | }
35 | btn_login.setOnClickListener {
36 | viewModel.dispatch(LoginViewAction.Login)
37 | }
38 | }
39 |
40 | private fun initViewStates() {
41 | viewModel.viewStates.let { states ->
42 | states.observeState(this, LoginViewState::userName) {
43 | edit_user_name.setText(it)
44 | edit_user_name.setSelection(it.length)
45 | }
46 | states.observeState(this, LoginViewState::password) {
47 | edit_password.setText(it)
48 | edit_password.setSelection(it.length)
49 | }
50 | states.observeState(this, LoginViewState::isLoginEnable) {
51 | btn_login.isEnabled = it
52 | btn_login.alpha = if (it) 1f else 0.5f
53 | }
54 | states.observeState(this, LoginViewState::passwordTipVisible) {
55 | tv_label.visibility = if (it) View.VISIBLE else View.INVISIBLE
56 | }
57 | }
58 | }
59 |
60 | private fun initViewEvents() {
61 | viewModel.viewEvents.observeEvent(this) {
62 | when (it) {
63 | is LoginViewEvent.ShowToast -> toast(it.message)
64 | is LoginViewEvent.ShowLoadingDialog -> showLoadingDialog()
65 | is LoginViewEvent.DismissLoadingDialog -> dismissLoadingDialog()
66 | }
67 | }
68 | }
69 |
70 | private var progressDialog: ProgressDialog? = null
71 |
72 | private fun showLoadingDialog() {
73 | if (progressDialog == null)
74 | progressDialog = ProgressDialog(this)
75 | progressDialog?.show()
76 | }
77 |
78 | private fun dismissLoadingDialog() {
79 | progressDialog?.takeIf { it.isShowing }?.dismiss()
80 | }
81 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/login/livedata/LoginViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.login.livedata
2 |
3 | import androidx.lifecycle.MutableLiveData
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.zj.architecture.login.LoginViewAction
7 | import com.zj.architecture.login.LoginViewEvent
8 | import com.zj.architecture.login.LoginViewState
9 | import com.zj.architecture.utils.asLiveData
10 | import com.zj.mvi.core.LiveEvents
11 | import com.zj.mvi.core.setEvent
12 | import com.zj.mvi.core.setState
13 | import com.zj.mvi.core.withState
14 | import kotlinx.coroutines.delay
15 | import kotlinx.coroutines.flow.*
16 | import kotlinx.coroutines.launch
17 |
18 | class LoginViewModel : ViewModel() {
19 | private val _viewStates = MutableLiveData(LoginViewState())
20 | val viewStates = _viewStates.asLiveData()
21 | private val _viewEvents: LiveEvents = LiveEvents()
22 | val viewEvents = _viewEvents.asLiveData()
23 |
24 | fun dispatch(viewAction: LoginViewAction) {
25 | when (viewAction) {
26 | is LoginViewAction.UpdateUserName -> updateUserName(viewAction.userName)
27 | is LoginViewAction.UpdatePassword -> updatePassword(viewAction.password)
28 | is LoginViewAction.Login -> login()
29 | }
30 | }
31 |
32 | private fun updateUserName(userName: String) {
33 | _viewStates.setState { copy(userName = userName) }
34 | }
35 |
36 | private fun updatePassword(password: String) {
37 | _viewStates.setState { copy(password = password) }
38 | }
39 |
40 | private fun login() {
41 | viewModelScope.launch {
42 | flow {
43 | loginLogic()
44 | emit("登录成功")
45 | }.onStart {
46 | _viewEvents.setEvent(LoginViewEvent.ShowLoadingDialog)
47 | }.onEach {
48 | _viewEvents.setEvent(
49 | LoginViewEvent.DismissLoadingDialog, LoginViewEvent.ShowToast(it)
50 | )
51 | }.catch {
52 | _viewStates.setState { copy(password = "") }
53 | _viewEvents.setEvent(
54 | LoginViewEvent.DismissLoadingDialog, LoginViewEvent.ShowToast("登录失败")
55 | )
56 | }.collect()
57 | }
58 | }
59 |
60 | private suspend fun loginLogic() {
61 | withState(viewStates) {
62 | val userName = it.userName
63 | val password = it.password
64 | delay(2000)
65 | throw Exception("登录失败")
66 | "$userName,$password"
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/mainscreen/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.mainscreen
2 |
3 | import android.os.Bundle
4 | import androidx.activity.viewModels
5 | import androidx.appcompat.app.AppCompatActivity
6 | import com.google.android.material.snackbar.Snackbar
7 | import com.zj.architecture.R
8 | import com.zj.mvi.core.observeEvent
9 | import com.zj.mvi.core.observeState
10 | import com.zj.architecture.repository.NewsItem
11 | import com.zj.architecture.utils.FetchStatus
12 | import com.zj.architecture.utils.toast
13 | import kotlinx.android.synthetic.main.activity_main.*
14 |
15 | class MainActivity : AppCompatActivity() {
16 | private val viewModel: MainViewModel by viewModels()
17 |
18 | private val newsRvAdapter by lazy {
19 | NewsRvAdapter {
20 | viewModel.dispatch(MainViewAction.NewsItemClicked(it.tag as NewsItem))
21 | }
22 | }
23 |
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | super.onCreate(savedInstanceState)
26 | setContentView(R.layout.activity_main)
27 | initView()
28 | initViewModel()
29 | }
30 |
31 | private fun initView() {
32 | rvNewsHome.adapter = newsRvAdapter
33 |
34 | srlNewsHome.setOnRefreshListener {
35 | viewModel.dispatch(MainViewAction.OnSwipeRefresh)
36 | }
37 |
38 | fabStar.setOnClickListener {
39 | viewModel.dispatch(MainViewAction.FabClicked)
40 | }
41 | }
42 |
43 | private fun initViewModel() {
44 | viewModel.viewStates.run {
45 | observeState(this@MainActivity, MainViewState::newsList) {
46 | newsRvAdapter.submitList(it)
47 | }
48 | observeState(this@MainActivity, MainViewState::fetchStatus) {
49 | when (it) {
50 | is FetchStatus.Fetched -> {
51 | srlNewsHome.isRefreshing = false
52 | }
53 | is FetchStatus.NotFetched -> {
54 | viewModel.dispatch(MainViewAction.FetchNews)
55 | srlNewsHome.isRefreshing = false
56 | }
57 | is FetchStatus.Fetching -> {
58 | srlNewsHome.isRefreshing = true
59 | }
60 | }
61 | }
62 | }
63 | viewModel.viewEvents.observeEvent(this) {
64 | renderViewEvent(it)
65 | }
66 | }
67 |
68 | private fun renderViewEvent(viewEvent: MainViewEvent) {
69 | when (viewEvent) {
70 | is MainViewEvent.ShowSnackbar -> {
71 | Snackbar.make(coordinatorLayoutRoot, viewEvent.message, Snackbar.LENGTH_SHORT)
72 | .show()
73 | }
74 | is MainViewEvent.ShowToast -> {
75 | toast(message = viewEvent.message)
76 | }
77 | }
78 | }
79 | }
80 |
81 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/mainscreen/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.mainscreen
2 |
3 | import androidx.lifecycle.MutableLiveData
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.zj.architecture.repository.NewsItem
7 | import com.zj.architecture.repository.NewsRepository
8 | import com.zj.mvi.core.setEvent
9 | import com.zj.mvi.core.setState
10 | import com.zj.architecture.utils.FetchStatus
11 | import com.zj.architecture.utils.PageState
12 | import com.zj.architecture.utils.asLiveData
13 | import com.zj.mvi.core.SingleLiveEvents
14 | import kotlinx.coroutines.launch
15 |
16 |
17 | class MainViewModel : ViewModel() {
18 | private var count: Int = 0
19 | private val repository: NewsRepository = NewsRepository.getInstance()
20 | private val _viewStates: MutableLiveData = MutableLiveData(MainViewState())
21 | val viewStates = _viewStates.asLiveData()
22 | private val _viewEvents: SingleLiveEvents = SingleLiveEvents() //一次性的事件,与页面状态分开管理
23 | val viewEvents = _viewEvents.asLiveData()
24 |
25 | fun dispatch(viewAction: MainViewAction) {
26 | when (viewAction) {
27 | is MainViewAction.NewsItemClicked -> newsItemClicked(viewAction.newsItem)
28 | MainViewAction.FabClicked -> fabClicked()
29 | MainViewAction.OnSwipeRefresh -> fetchNews()
30 | MainViewAction.FetchNews -> fetchNews()
31 | }
32 | }
33 |
34 | private fun newsItemClicked(newsItem: NewsItem) {
35 | _viewEvents.setEvent(MainViewEvent.ShowSnackbar(newsItem.title))
36 | }
37 |
38 | private fun fabClicked() {
39 | count++
40 | _viewEvents.setEvent(MainViewEvent.ShowToast(message = "Fab clicked count $count"))
41 | }
42 |
43 | private fun fetchNews() {
44 | _viewStates.setState {
45 | copy(fetchStatus = FetchStatus.Fetching)
46 | }
47 | viewModelScope.launch {
48 | when (val result = repository.getMockApiResponse()) {
49 | is PageState.Error -> {
50 | _viewStates.setState {
51 | copy(fetchStatus = FetchStatus.Fetched)
52 | }
53 | _viewEvents.setEvent(MainViewEvent.ShowToast(message = result.message))
54 | }
55 | is PageState.Success -> {
56 | _viewStates.setState {
57 | copy(fetchStatus = FetchStatus.Fetched, newsList = result.data)
58 | }
59 | }
60 | }
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/mainscreen/MainViewStates.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.mainscreen
2 |
3 | import com.zj.architecture.repository.NewsItem
4 | import com.zj.architecture.utils.FetchStatus
5 |
6 |
7 | data class MainViewState(
8 | val fetchStatus: FetchStatus = FetchStatus.NotFetched,
9 | val newsList: List = emptyList()
10 | )
11 |
12 | sealed class MainViewEvent {
13 | data class ShowSnackbar(val message: String) : MainViewEvent()
14 | data class ShowToast(val message: String) : MainViewEvent()
15 | }
16 |
17 | sealed class MainViewAction {
18 | data class NewsItemClicked(val newsItem: NewsItem) : MainViewAction()
19 | object FabClicked : MainViewAction()
20 | object OnSwipeRefresh : MainViewAction()
21 | object FetchNews : MainViewAction()
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/mainscreen/NewsRvAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.mainscreen
2 |
3 | import android.view.View
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.DiffUtil
6 | import androidx.recyclerview.widget.ListAdapter
7 | import androidx.recyclerview.widget.RecyclerView
8 | import coil.api.load
9 | import com.zj.architecture.R
10 | import com.zj.architecture.repository.NewsItem
11 | import com.zj.architecture.utils.inflate
12 | import kotlinx.android.extensions.LayoutContainer
13 | import kotlinx.android.synthetic.main.item_view.view.*
14 |
15 | class NewsRvAdapter(private val listener: (View) -> Unit) :
16 | ListAdapter(NewsItemItemCallback()) {
17 |
18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
19 | return MyViewHolder(inflate(parent.context, R.layout.item_view, parent), listener)
20 | }
21 |
22 | override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
23 | holder.bind(getItem(position))
24 | }
25 |
26 | override fun getItemCount() = currentList.size
27 |
28 | inner class MyViewHolder(override val containerView: View, listener: (View) -> Unit) :
29 | RecyclerView.ViewHolder(containerView),
30 | LayoutContainer {
31 |
32 | init {
33 | itemView.setOnClickListener(listener)
34 | }
35 |
36 | fun bind(newsItem: NewsItem) =
37 | with(itemView) {
38 | itemView.tag = newsItem
39 | tvTitle.text = newsItem.title
40 | tvDescription.text = newsItem.description
41 | ivThumbnail.load(newsItem.imageUrl) {
42 | crossfade(true)
43 | placeholder(R.mipmap.ic_launcher)
44 | }
45 | }
46 | }
47 |
48 | internal class NewsItemItemCallback : DiffUtil.ItemCallback() {
49 | override fun areItemsTheSame(oldItem: NewsItem, newItem: NewsItem): Boolean {
50 | return oldItem == newItem
51 | }
52 |
53 | override fun areContentsTheSame(oldItem: NewsItem, newItem: NewsItem): Boolean {
54 | return oldItem.title == newItem.title
55 | }
56 | }
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/mockapi/MockApi.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.mockapi
2 |
3 | import com.zj.architecture.utils.BASE_URL
4 | import okhttp3.OkHttpClient
5 | import retrofit2.Retrofit
6 | import retrofit2.converter.gson.GsonConverterFactory
7 | import retrofit2.http.GET
8 |
9 | interface MockApi {
10 | @GET("mock")
11 | suspend fun getLatestNews(): MockApiResponse
12 |
13 | companion object {
14 | // Please do not follow this code as this has been
15 | // modified to intercept API calls with mock response.
16 | fun create(): MockApi {
17 | val okHttpClient = OkHttpClient()
18 | .newBuilder()
19 | .addInterceptor(MockInterceptor())
20 | .build()
21 |
22 | return Retrofit.Builder()
23 | .client(okHttpClient)
24 | .baseUrl(BASE_URL)
25 | .addConverterFactory(GsonConverterFactory.create())
26 | .build()
27 | .create(MockApi::class.java)
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/mockapi/MockApiResponse.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.mockapi
2 |
3 | import com.zj.architecture.repository.NewsItem
4 |
5 | data class MockApiResponse(
6 | val articles: List? = null
7 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/mockapi/MockInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.mockapi
2 |
3 | import com.zj.architecture.BuildConfig
4 | import okhttp3.*
5 |
6 | class MockInterceptor : Interceptor {
7 |
8 | override fun intercept(chain: Interceptor.Chain): Response {
9 | if (BuildConfig.DEBUG) {
10 | val uri = chain.request().url().uri().toString()
11 | val responseString = when {
12 | uri.endsWith("mock") -> getMockApiResponse
13 | else -> ""
14 | }
15 |
16 | return Response.Builder()
17 | .request(chain.request())
18 | .code(200)
19 | .protocol(Protocol.HTTP_2)
20 | .message(responseString)
21 | .body(
22 | ResponseBody.create(
23 | MediaType.parse("application/json"),
24 | responseString.toByteArray()
25 | )
26 | )
27 | .addHeader("content-type", "application/json")
28 | .build()
29 | } else {
30 | //just to be on safe side.
31 | throw IllegalAccessError("""MockInterceptor is only meant for Testing Purposes and bound to be used only with DEBUG mode""")
32 | }
33 | }
34 | }
35 |
36 | const val getMockApiResponse = """
37 | {
38 | "articles": [
39 | {
40 | "title": "Title",
41 | "description": "Description",
42 | "imageUrl": "imageUrl"
43 | },
44 | {
45 | "title": "Title",
46 | "description": "Description",
47 | "imageUrl": "imageUrl"
48 | }
49 | ]
50 | }"""
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/network/CoroutineScopeHelper.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.network
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.Job
5 | import kotlinx.coroutines.launch
6 |
7 | class CoroutineScopeHelper(private val coroutineScope: CoroutineScope) {
8 | fun rxLaunch(init: LaunchBuilder.() -> Unit): Job {
9 | val result = LaunchBuilder().apply(init)
10 | val handler = NetworkExceptionHandler {
11 | result.onError?.invoke(it)
12 | }
13 | return coroutineScope.launch(handler) {
14 | val res: T = result.onRequest()
15 | result.onSuccess?.invoke(res)
16 | }
17 | }
18 | }
19 |
20 | class LaunchBuilder {
21 | lateinit var onRequest: (suspend () -> T)
22 | var onSuccess: ((data: T) -> Unit)? = null
23 | var onError: ((Throwable) -> Unit)? = null
24 | }
25 |
26 | fun CoroutineScope.rxLaunch(init: LaunchBuilder.() -> Unit) {
27 | val scopeHelper = CoroutineScopeHelper(this)
28 | scopeHelper.rxLaunch(init)
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/network/NetworkExceptionHandler.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.network
2 |
3 | import com.zj.architecture.MyApp
4 | import com.zj.architecture.utils.toast
5 | import kotlinx.coroutines.CoroutineExceptionHandler
6 | import java.net.SocketTimeoutException
7 | import java.net.UnknownHostException
8 | import kotlin.coroutines.AbstractCoroutineContextElement
9 | import kotlin.coroutines.CoroutineContext
10 |
11 | /**
12 | * 通用异常处理
13 | * 在这里处理一些页面通用的异常逻辑
14 | * 可以根据业务需求自定义
15 | */
16 | class NetworkExceptionHandler(
17 | private val onException: (e: Throwable) -> Unit = {}
18 | ) : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler {
19 | override fun handleException(context: CoroutineContext, exception: Throwable) {
20 | onException.invoke(exception)
21 | if (exception is UnknownHostException || exception is SocketTimeoutException) {
22 | MyApp.get().toast("发生网络错误,请稍后重试")
23 | } else {
24 | MyApp.get().toast("请求失败,请重试")
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/network/NetworkExt.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.network
2 |
3 | import com.zj.architecture.MyApp
4 | import com.zj.architecture.utils.toast
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.FlowCollector
7 | import kotlinx.coroutines.flow.catch
8 | import java.net.SocketTimeoutException
9 | import java.net.UnknownHostException
10 |
11 | fun Flow.commonCatch(action: suspend FlowCollector.(cause: Throwable) -> Unit): Flow {
12 | return this.catch {
13 | if (it is UnknownHostException || it is SocketTimeoutException) {
14 | MyApp.get().toast("发生网络错误,请稍后重试")
15 | } else {
16 | MyApp.get().toast("请求失败,请重试")
17 | }
18 | action(it)
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/networkscreen/FlowViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.networkscreen
2 |
3 | import androidx.lifecycle.MutableLiveData
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.zj.architecture.network.commonCatch
7 | import com.zj.mvi.core.setEvent
8 | import com.zj.mvi.core.setState
9 | import com.zj.architecture.utils.asLiveData
10 | import kotlinx.coroutines.delay
11 | import kotlinx.coroutines.flow.*
12 | import kotlinx.coroutines.launch
13 |
14 | class FlowViewModel : ViewModel() {
15 | private val _viewStates = MutableLiveData(NetworkViewState())
16 | val viewStates = _viewStates.asLiveData()
17 | private val _viewEvents: com.zj.mvi.core.SingleLiveEvents =
18 | com.zj.mvi.core.SingleLiveEvents()
19 | val viewEvents = _viewEvents.asLiveData()
20 |
21 | fun dispatch(viewAction: NetworkViewAction) {
22 | when (viewAction) {
23 | is NetworkViewAction.PageRequest -> pageRequest()
24 | is NetworkViewAction.PartRequest -> partRequest()
25 | is NetworkViewAction.MultiRequest -> multiSourceRequest()
26 | is NetworkViewAction.ErrorRequest -> errorRequest()
27 | }
28 | }
29 |
30 | /**
31 | * 页面请求,通常包括刷新页面loading状态等
32 | */
33 | private fun pageRequest() {
34 | viewModelScope.launch {
35 | flow {
36 | delay(2000)
37 | emit("页面请求成功")
38 | }.onStart {
39 | _viewStates.setState { copy(pageStatus = PageStatus.Loading) }
40 | }.onEach {
41 | _viewStates.setState { copy(content = it, pageStatus = PageStatus.Success) }
42 | }.commonCatch {
43 | _viewStates.setState { copy(pageStatus = PageStatus.Error(it)) }
44 | }.collect()
45 | }
46 | }
47 |
48 | /**
49 | * 页面局部请求,例如点赞收藏等,通常需要弹dialog或toast
50 | */
51 | private fun partRequest() {
52 | viewModelScope.launch {
53 | flow {
54 | delay(2000)
55 | emit("点赞成功")
56 | }.onStart {
57 | _viewEvents.setEvent(NetworkViewEvent.ShowLoadingDialog)
58 | }.onEach {
59 | _viewEvents.setEvent(
60 | NetworkViewEvent.DismissLoadingDialog, NetworkViewEvent.ShowToast(it)
61 | )
62 | _viewStates.setState { copy(content = it) }
63 | }.commonCatch {
64 | _viewEvents.setEvent(NetworkViewEvent.DismissLoadingDialog)
65 | }.collect()
66 | }
67 | }
68 |
69 | /**
70 | * 多数据源请求
71 | */
72 | private fun multiSourceRequest() {
73 | viewModelScope.launch {
74 | val flow1 = flow {
75 | delay(1000)
76 | emit("数据源1")
77 | }
78 | val flow2 = flow {
79 | delay(2000)
80 | emit("数据源2")
81 | }
82 | flow1.zip(flow2) { a, b ->
83 | "$a,$b"
84 | }.onStart {
85 | _viewEvents.setEvent(NetworkViewEvent.ShowLoadingDialog)
86 | }.onEach {
87 | _viewEvents.setEvent(
88 | NetworkViewEvent.DismissLoadingDialog, NetworkViewEvent.ShowToast(it)
89 | )
90 | _viewStates.setState { copy(content = it) }
91 | }.commonCatch {
92 | _viewEvents.setEvent(NetworkViewEvent.DismissLoadingDialog)
93 | }.collect()
94 | }
95 | }
96 |
97 | /**
98 | * 请求错误示例
99 | */
100 | private fun errorRequest() {
101 | viewModelScope.launch {
102 | flow {
103 | delay(2000)
104 | throw NullPointerException("")
105 | emit("请求失败")
106 | }.onStart {
107 | _viewEvents.setEvent(NetworkViewEvent.ShowLoadingDialog)
108 | }.onEach {
109 | _viewEvents.setEvent(
110 | NetworkViewEvent.DismissLoadingDialog, NetworkViewEvent.ShowToast(it)
111 | )
112 | _viewStates.setState { copy(content = it) }
113 | }.commonCatch {
114 | _viewEvents.setEvent(NetworkViewEvent.DismissLoadingDialog)
115 | }.collect()
116 | }
117 | }
118 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/networkscreen/NetworkActivity.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.networkscreen
2 |
3 | import android.app.ProgressDialog
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.activity.viewModels
7 | import androidx.appcompat.app.AppCompatActivity
8 | import com.zj.architecture.R
9 | import com.zj.mvi.core.observeEvent
10 | import com.zj.mvi.core.observeState
11 | import com.zj.architecture.utils.toast
12 | import kotlinx.android.synthetic.main.activity_network.*
13 |
14 | class NetworkActivity : AppCompatActivity() {
15 | private val viewModel by viewModels()
16 | override fun onCreate(savedInstanceState: Bundle?) {
17 | super.onCreate(savedInstanceState)
18 | setContentView(R.layout.activity_network)
19 | initViewModel()
20 | }
21 |
22 | private fun initViewModel() {
23 | viewModel.viewStates.let { state ->
24 | state.observeState(this, NetworkViewState::pageStatus) {
25 | when (it) {
26 | is PageStatus.Success -> state_layout.showContent()
27 | is PageStatus.Loading -> state_layout.showLoading()
28 | is PageStatus.Error -> state_layout.showError()
29 | }
30 | }
31 | state.observeState(this, NetworkViewState::content) {
32 | tv_content.text = it
33 | }
34 | }
35 |
36 | viewModel.viewEvents.observeEvent(this) {
37 | when (it) {
38 | is NetworkViewEvent.ShowToast -> toast(it.message)
39 | is NetworkViewEvent.ShowLoadingDialog -> showLoadingDialog()
40 | is NetworkViewEvent.DismissLoadingDialog -> dismissLoadingDialog()
41 | }
42 | }
43 | }
44 |
45 | fun simpleRequest(view: View) {
46 | viewModel.dispatch(NetworkViewAction.PageRequest)
47 | }
48 |
49 | fun partRequest(view: View) {
50 | viewModel.dispatch(NetworkViewAction.PartRequest)
51 | }
52 |
53 | fun multiSource(view: View) {
54 | viewModel.dispatch(NetworkViewAction.MultiRequest)
55 | }
56 |
57 | fun errorRequest(view: View) {
58 | viewModel.dispatch(NetworkViewAction.ErrorRequest)
59 | }
60 |
61 | private var progressDialog: ProgressDialog? = null
62 |
63 | private fun showLoadingDialog() {
64 | if (progressDialog == null)
65 | progressDialog = ProgressDialog(this)
66 | progressDialog?.show()
67 | }
68 |
69 | private fun dismissLoadingDialog() {
70 | progressDialog?.takeIf { it.isShowing }?.dismiss()
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/networkscreen/NetworkViewState.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.networkscreen
2 |
3 | data class NetworkViewState(
4 | val content: String = "等待网络请求内容",
5 | val pageStatus: PageStatus = PageStatus.Success
6 | )
7 |
8 | sealed class NetworkViewEvent {
9 | data class ShowToast(val message: String) : NetworkViewEvent()
10 | object ShowLoadingDialog : NetworkViewEvent()
11 | object DismissLoadingDialog : NetworkViewEvent()
12 | }
13 |
14 | sealed class NetworkViewAction {
15 | object PageRequest : NetworkViewAction()
16 | object PartRequest : NetworkViewAction()
17 | object MultiRequest : NetworkViewAction()
18 | object ErrorRequest : NetworkViewAction()
19 | }
20 |
21 |
22 | sealed class PageStatus {
23 | object Loading : PageStatus()
24 | object Success : PageStatus()
25 | data class Error(val throwable: Throwable) : PageStatus()
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/repository/NewsItem.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.repository
2 |
3 | data class NewsItem(
4 | val title: String,
5 | val description: String,
6 | val imageUrl: String
7 | )
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/repository/NewsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.repository
2 |
3 | import com.zj.architecture.mockapi.MockApi
4 | import com.zj.architecture.utils.PageState
5 | import kotlinx.coroutines.delay
6 |
7 | class NewsRepository {
8 |
9 | companion object {
10 | // For Singleton instantiation
11 | @Volatile
12 | private var instance: NewsRepository? = null
13 |
14 | fun getInstance() =
15 | instance ?: synchronized(this) {
16 | instance ?: NewsRepository().also { instance = it }
17 | }
18 | }
19 |
20 | suspend fun getMockApiResponse(): PageState> {
21 | val articlesApiResult = try {
22 | delay(2000)
23 | MockApi.create().getLatestNews()
24 | } catch (e: Exception) {
25 | return PageState.Error(e)
26 | }
27 |
28 | articlesApiResult.articles?.let { list ->
29 | return PageState.Success(data = list)
30 | } ?: run {
31 | return PageState.Error("Failed to get News")
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/zj/architecture/utils/AppUtils.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture.utils
2 |
3 | import android.content.Context
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.widget.Toast
8 | import androidx.lifecycle.LifecycleOwner
9 | import androidx.lifecycle.LiveData
10 | import androidx.lifecycle.MutableLiveData
11 | import androidx.lifecycle.distinctUntilChanged
12 | import androidx.lifecycle.map
13 | import kotlin.reflect.KProperty1
14 |
15 | const val BASE_URL = "https://your_api_endpoint.com/"
16 |
17 | fun inflate(
18 | context: Context,
19 | viewId: Int,
20 | parent: ViewGroup? = null,
21 | attachToRoot: Boolean = false
22 | ): View {
23 | return LayoutInflater.from(context).inflate(viewId, parent, attachToRoot)
24 | }
25 |
26 | fun Context.toast(message: String, length: Int = Toast.LENGTH_SHORT) {
27 | Toast.makeText(this, message, length).show()
28 | }
29 |
30 | //LCE -> Loading/Content/Error
31 | sealed class PageState {
32 | data class Success(val data: T) : PageState()
33 | data class Error(val message: String) : PageState() {
34 | constructor(t: Throwable) : this(t.message ?: "")
35 | }
36 | }
37 |
38 | fun MutableLiveData.asLiveData(): LiveData {
39 | return this
40 | }
41 |
42 | sealed class FetchStatus {
43 | object Fetching : FetchStatus()
44 | object Fetched : FetchStatus()
45 | object NotFetched : FetchStatus()
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/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_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
17 |
18 |
25 |
26 |
27 |
28 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_network.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
19 |
20 |
26 |
27 |
33 |
34 |
40 |
41 |
47 |
48 |
49 |
50 |
51 |
56 |
57 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_router.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
23 |
24 |
30 |
31 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
26 |
27 |
39 |
40 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_error.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
19 |
20 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_loading.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_login.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
14 |
15 |
24 |
25 |
35 |
36 |
44 |
45 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJiang/android-architecture/420eebff96dbfa5b1751b802bcca2973aa13de9f/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJiang/android-architecture/420eebff96dbfa5b1751b802bcca2973aa13de9f/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJiang/android-architecture/420eebff96dbfa5b1751b802bcca2973aa13de9f/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJiang/android-architecture/420eebff96dbfa5b1751b802bcca2973aa13de9f/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJiang/android-architecture/420eebff96dbfa5b1751b802bcca2973aa13de9f/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJiang/android-architecture/420eebff96dbfa5b1751b802bcca2973aa13de9f/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/bg_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJiang/android-architecture/420eebff96dbfa5b1751b802bcca2973aa13de9f/app/src/main/res/mipmap-xxhdpi/bg_error.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJiang/android-architecture/420eebff96dbfa5b1751b802bcca2973aa13de9f/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJiang/android-architecture/420eebff96dbfa5b1751b802bcca2973aa13de9f/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJiang/android-architecture/420eebff96dbfa5b1751b802bcca2973aa13de9f/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJiang/android-architecture/420eebff96dbfa5b1751b802bcca2973aa13de9f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | android-architecture
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/test/java/com/zj/architecture/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.zj.architecture
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | repositories {
4 | google()
5 | mavenCentral()
6 | maven { url 'https://jitpack.io' }
7 | }
8 | dependencies {
9 | classpath "com.android.tools.build:gradle:7.0.3"
10 | classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10'
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle files
14 | }
15 | }
16 |
17 | task clean(type: Delete) {
18 | delete rootProject.buildDir
19 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJiang/android-architecture/420eebff96dbfa5b1751b802bcca2973aa13de9f/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Oct 20 08:09:16 CST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/mvi-core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/mvi-core/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | compileSdk 31
8 |
9 | defaultConfig {
10 | minSdk 21
11 | targetSdk 31
12 |
13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
14 | consumerProguardFiles "consumer-rules.pro"
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | compileOptions {
24 | sourceCompatibility JavaVersion.VERSION_1_8
25 | targetCompatibility JavaVersion.VERSION_1_8
26 | }
27 | kotlinOptions {
28 | jvmTarget = '1.8'
29 | }
30 | }
31 |
32 | dependencies {
33 |
34 | implementation 'androidx.core:core-ktx:1.7.0'
35 | implementation 'androidx.appcompat:appcompat:1.4.1'
36 | implementation 'com.google.android.material:material:1.5.0'
37 | implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.1")
38 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1")
39 | }
--------------------------------------------------------------------------------
/mvi-core/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RicardoJiang/android-architecture/420eebff96dbfa5b1751b802bcca2973aa13de9f/mvi-core/consumer-rules.pro
--------------------------------------------------------------------------------
/mvi-core/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
--------------------------------------------------------------------------------
/mvi-core/src/androidTest/java/com/zj/mvi/core/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.zj.mvi.core
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.zj.mvi.core.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/mvi-core/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/mvi-core/src/main/java/com/zj/mvi/core/LiveEvents.kt:
--------------------------------------------------------------------------------
1 | package com.zj.mvi.core
2 |
3 | import androidx.annotation.MainThread
4 | import androidx.lifecycle.LifecycleOwner
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.Observer
7 | import java.util.concurrent.atomic.AtomicBoolean
8 |
9 | /**
10 | * LiveEvents
11 | * 负责处理多维度一次性Event,支持多个监听者
12 | * 比如我们在请求开始时发出ShowLoading,网络请求成功后发出DismissLoading与Toast事件
13 | * 如果我们在请求开始后回到桌面,成功后再回到App,这样有一个事件就会被覆盖,因此将所有事件通过List存储
14 | */
15 | class LiveEvents : MutableLiveData>() {
16 |
17 | private val observers = hashSetOf>()
18 |
19 | @MainThread
20 | override fun observe(owner: LifecycleOwner, observer: Observer>) {
21 | observers.find { it.observer === observer }?.let { _ -> // existing
22 | return
23 | }
24 | val wrapper = ObserverWrapper(observer)
25 | observers.add(wrapper)
26 | super.observe(owner, wrapper)
27 | }
28 |
29 | @MainThread
30 | override fun observeForever(observer: Observer>) {
31 | observers.find { it.observer === observer }?.let { _ -> // existing
32 | return
33 | }
34 | val wrapper = ObserverWrapper(observer)
35 | observers.add(wrapper)
36 | super.observeForever(wrapper)
37 | }
38 |
39 | @MainThread
40 | override fun removeObserver(observer: Observer>) {
41 | if (observer is ObserverWrapper<*> && observers.remove(observer)) {
42 | super.removeObserver(observer)
43 | return
44 | }
45 | val iterator = observers.iterator()
46 | while (iterator.hasNext()) {
47 | val wrapper = iterator.next()
48 | if (wrapper.observer == observer) {
49 | iterator.remove()
50 | super.removeObserver(wrapper)
51 | break
52 | }
53 | }
54 | }
55 |
56 | @MainThread
57 | override fun setValue(t: List?) {
58 | observers.forEach { it.newValue(t) }
59 | super.setValue(t)
60 | }
61 |
62 | private class ObserverWrapper(val observer: Observer>) : Observer> {
63 |
64 | private val pending = AtomicBoolean(false)
65 | private val eventList = mutableListOf>()
66 | override fun onChanged(t: List?) {
67 | if (pending.compareAndSet(true, false)) {
68 | observer.onChanged(eventList.flatten())
69 | eventList.clear()
70 | }
71 | }
72 |
73 | fun newValue(t: List?) {
74 | pending.set(true)
75 | t?.let {
76 | eventList.add(it)
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/mvi-core/src/main/java/com/zj/mvi/core/MVIExt.kt:
--------------------------------------------------------------------------------
1 | package com.zj.mvi.core
2 |
3 | import androidx.lifecycle.LifecycleOwner
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.distinctUntilChanged
7 | import androidx.lifecycle.map
8 | import kotlin.reflect.KProperty1
9 |
10 | fun LiveData.observeState(
11 | lifecycleOwner: LifecycleOwner,
12 | prop1: KProperty1,
13 | action: (A) -> Unit
14 | ) {
15 | this.map {
16 | StateTuple1(prop1.get(it))
17 | }.distinctUntilChanged().observe(lifecycleOwner) { (a) ->
18 | action.invoke(a)
19 | }
20 | }
21 |
22 | fun LiveData.observeState(
23 | lifecycleOwner: LifecycleOwner,
24 | prop1: KProperty1,
25 | prop2: KProperty1,
26 | action: (A, B) -> Unit
27 | ) {
28 | this.map {
29 | StateTuple2(prop1.get(it), prop2.get(it))
30 | }.distinctUntilChanged().observe(lifecycleOwner) { (a, b) ->
31 | action.invoke(a, b)
32 | }
33 | }
34 |
35 | fun LiveData.observeState(
36 | lifecycleOwner: LifecycleOwner,
37 | prop1: KProperty1,
38 | prop2: KProperty1,
39 | prop3: KProperty1,
40 | action: (A, B, C) -> Unit
41 | ) {
42 | this.map {
43 | StateTuple3(prop1.get(it), prop2.get(it), prop3.get(it))
44 | }.distinctUntilChanged().observe(lifecycleOwner) { (a, b, c) ->
45 | action.invoke(a, b, c)
46 | }
47 | }
48 |
49 | internal data class StateTuple1(val a: A)
50 | internal data class StateTuple2(val a: A, val b: B)
51 | internal data class StateTuple3(val a: A, val b: B, val c: C)
52 |
53 | fun MutableLiveData.setState(reducer: T.() -> T) {
54 | this.value = this.value?.reducer()
55 | }
56 |
57 | fun SingleLiveEvents.setEvent(vararg values: T) {
58 | this.value = values.toList()
59 | }
60 |
61 | fun LiveEvents.setEvent(vararg values: T) {
62 | this.value = values.toList()
63 | }
64 |
65 | fun LiveData>.observeEvent(lifecycleOwner: LifecycleOwner, action: (T) -> Unit) {
66 | this.observe(lifecycleOwner) {
67 | it.forEach { event ->
68 | action.invoke(event)
69 | }
70 | }
71 | }
72 |
73 | inline fun withState(state: LiveData, block: (T) -> R): R? {
74 | return state.value?.let(block)
75 | }
--------------------------------------------------------------------------------
/mvi-core/src/main/java/com/zj/mvi/core/MVIFlowExt.kt:
--------------------------------------------------------------------------------
1 | package com.zj.mvi.core
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleOwner
5 | import androidx.lifecycle.lifecycleScope
6 | import androidx.lifecycle.repeatOnLifecycle
7 | import kotlinx.coroutines.flow.*
8 | import kotlinx.coroutines.launch
9 | import kotlin.reflect.KProperty1
10 |
11 | /**
12 | * flow部分
13 | */
14 | fun StateFlow.observeState(
15 | lifecycleOwner: LifecycleOwner,
16 | prop1: KProperty1,
17 | action: (A) -> Unit
18 | ) {
19 | lifecycleOwner.lifecycleScope.launch {
20 | lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
21 | this@observeState.map {
22 | StateTuple1(prop1.get(it))
23 | }.distinctUntilChanged().collect { (a) ->
24 | action.invoke(a)
25 | }
26 | }
27 | }
28 | }
29 |
30 | fun StateFlow.observeState(
31 | lifecycleOwner: LifecycleOwner,
32 | prop1: KProperty1,
33 | prop2: KProperty1,
34 | action: (A, B) -> Unit
35 | ) {
36 | lifecycleOwner.lifecycleScope.launch {
37 | lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
38 | this@observeState.map {
39 | StateTuple2(prop1.get(it), prop2.get(it))
40 | }.distinctUntilChanged().collect { (a, b) ->
41 | action.invoke(a, b)
42 | }
43 | }
44 | }
45 | }
46 |
47 | fun StateFlow.observeState(
48 | lifecycleOwner: LifecycleOwner,
49 | prop1: KProperty1,
50 | prop2: KProperty1,
51 | prop3: KProperty1,
52 | action: (A, B, C) -> Unit
53 | ) {
54 | lifecycleOwner.lifecycleScope.launch {
55 | lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
56 | this@observeState.map {
57 | StateTuple3(prop1.get(it), prop2.get(it), prop3.get(it))
58 | }.distinctUntilChanged().collect { (a, b, c) ->
59 | action.invoke(a, b, c)
60 | }
61 | }
62 | }
63 | }
64 |
65 | fun MutableStateFlow.setState(reducer: T.() -> T) {
66 | this.value = this.value.reducer()
67 | }
68 |
69 | inline fun withState(state: StateFlow, block: (T) -> R): R {
70 | return state.value.let(block)
71 | }
72 |
73 | suspend fun SharedFlowEvents.setEvent(vararg values: T) {
74 | val eventList = values.toList()
75 | this.emit(eventList)
76 | }
77 |
78 | fun SharedFlow>.observeEvent(lifecycleOwner: LifecycleOwner, action: (T) -> Unit) {
79 | lifecycleOwner.lifecycleScope.launchWhenStarted {
80 | this@observeEvent.collect {
81 | it.forEach { event ->
82 | action.invoke(event)
83 | }
84 | }
85 | }
86 | }
87 |
88 | typealias SharedFlowEvents = MutableSharedFlow>
89 |
90 | @Suppress("FunctionName")
91 | fun SharedFlowEvents(): SharedFlowEvents {
92 | return MutableSharedFlow()
93 | }
--------------------------------------------------------------------------------
/mvi-core/src/main/java/com/zj/mvi/core/SingleLiveEvents.kt:
--------------------------------------------------------------------------------
1 | package com.zj.mvi.core
2 |
3 | import androidx.annotation.MainThread
4 | import androidx.lifecycle.LifecycleOwner
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.Observer
7 | import java.util.concurrent.atomic.AtomicBoolean
8 |
9 | /**
10 | * SingleLiveEvents
11 | * 负责处理多维度一次性Event
12 | * 比如我们在请求开始时发出ShowLoading,网络请求成功后发出DismissLoading与Toast事件
13 | * 如果我们在请求开始后回到桌面,成功后再回到App,这样有一个事件就会被覆盖,因此将所有事件通过List存储
14 | */
15 | @Deprecated("Use LiveEvents instead")
16 | class SingleLiveEvents : MutableLiveData>() {
17 | private val pending = AtomicBoolean(false)
18 | private val eventList = mutableListOf>()
19 |
20 | @MainThread
21 | override fun observe(owner: LifecycleOwner, observer: Observer>) {
22 | super.observe(owner) { t ->
23 | if (pending.compareAndSet(true, false)) {
24 | eventList.clear()
25 | observer.onChanged(t)
26 | }
27 | }
28 | }
29 |
30 | @MainThread
31 | override fun setValue(t: List?) {
32 | pending.set(true)
33 | t?.let {
34 | eventList.add(it)
35 | }
36 | val list = eventList.flatten()
37 | super.setValue(list)
38 | }
39 | }
--------------------------------------------------------------------------------
/mvi-core/src/test/java/com/zj/mvi/core/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.zj.mvi.core
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | google()
5 | mavenCentral()
6 | maven { url 'https://jitpack.io' }
7 | jcenter() // Warning: this repository is going to shut down soon
8 | }
9 | }
10 | rootProject.name = "android-architecture"
11 | include ':app'
12 | include ':mvi-core'
13 |
--------------------------------------------------------------------------------