├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── release │ ├── app-release.apk │ └── output-metadata.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── fmt │ │ └── mvi │ │ └── learn │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── fmt │ │ │ └── mvi │ │ │ └── learn │ │ │ ├── App.kt │ │ │ ├── commom │ │ │ ├── ext │ │ │ │ └── ImageExt.kt │ │ │ ├── ui │ │ │ │ └── BaseFragment.kt │ │ │ └── utils │ │ │ │ └── ShareUtils.kt │ │ │ ├── di │ │ │ ├── NetModule.kt │ │ │ └── SingletonModule.kt │ │ │ ├── gobal │ │ │ ├── ConfigKeys.kt │ │ │ ├── Configurator.kt │ │ │ └── GsonExt.kt │ │ │ ├── main │ │ │ ├── action │ │ │ │ └── MainViewAction.kt │ │ │ ├── activity │ │ │ │ └── MainActivity.kt │ │ │ ├── factory │ │ │ │ └── MainFragmentFactory.kt │ │ │ ├── state │ │ │ │ └── MainViewState.kt │ │ │ └── viewmodel │ │ │ │ └── MainViewModel.kt │ │ │ ├── net │ │ │ ├── Api.kt │ │ │ └── ApiService.kt │ │ │ ├── startup │ │ │ └── AppInitializer.kt │ │ │ ├── travel │ │ │ ├── action │ │ │ │ ├── TravelTabViewAction.kt │ │ │ │ └── TravelViewAction.kt │ │ │ ├── activity │ │ │ │ └── TravelDetailActivity.kt │ │ │ ├── adapter │ │ │ │ ├── TravelPageAdapter.kt │ │ │ │ └── TravelTabAdapter.kt │ │ │ ├── fragment │ │ │ │ ├── TravelFragment.kt │ │ │ │ └── TravelTabFragment.kt │ │ │ ├── model │ │ │ │ ├── TravelModel.kt │ │ │ │ └── TravelTabModel.kt │ │ │ ├── state │ │ │ │ ├── TravelTabViewState.kt │ │ │ │ └── TravelViewState.kt │ │ │ └── viewmodel │ │ │ │ ├── TravelTabViewModel.kt │ │ │ │ └── TravelViewModel.kt │ │ │ ├── url │ │ │ └── URLs.kt │ │ │ └── video │ │ │ ├── action │ │ │ └── VideoListViewAction.kt │ │ │ ├── adapter │ │ │ └── VideoListAdapter.kt │ │ │ ├── fragment │ │ │ └── VideoFragment.kt │ │ │ ├── model │ │ │ └── VideoModel.kt │ │ │ ├── state │ │ │ └── VideoListViewState.kt │ │ │ └── viewmodel │ │ │ └── VideoViewModel.kt │ └── res │ │ ├── color │ │ └── tab_tint_color.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ ├── ic_like.xml │ │ ├── ic_location.xml │ │ ├── ic_share.xml │ │ ├── ic_spaceship.xml │ │ ├── ic_travel.xml │ │ ├── ic_video.xml │ │ └── shape_location_bg.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_travel_detail.xml │ │ ├── fragment_common_list.xml │ │ ├── fragment_travel.xml │ │ ├── fragment_video.xml │ │ ├── item_travel.xml │ │ └── item_video.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 │ │ ├── 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 │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── network_security_config.xml │ └── test │ └── java │ └── com │ └── fmt │ └── mvi │ └── learn │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mvi └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 fmtjava 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 | # MVI_Learn 2 | 使用 Jetpack 核心组件 + MVI 架构,快速实现一个 App
3 | 4 | **如果喜欢的话希望给个 `Star` 或 `Fork` ^_^ ,谢谢** 5 | 6 | # 项目截图 7 |
8 |     9 |     10 | 11 |
12 | 13 |
14 | 15 | # 核心架构 16 |
17 | 18 |
19 | 20 | ## 核心组件 21 | - Model:与MVVM中的Model不同的是,MVI的Model主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态。 22 | - View: 与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Model的变化实现界面刷新。 23 | - Intent:此Intent不是Activity的Intent,用户的任何操作都被包装成Intent后发送给Model层进行数据请求。 24 | 25 | # 下载体验 26 | - 点击[![](https://img.shields.io/badge/Download-apk-green.svg)](https://www.pgyer.com/8SDl) 27 | - 下方二维码下载(每日上限100次,如达到上限,还是 clone 源码吧!✧(≖ ◡ ≖✿)))
28 | 29 | 30 | # 感谢 31 | - [flutter_trip](https://github.com/wkl007/flutter_trip) 32 | - [JiaoZiVideoPlayer](https://github.com/Jzvd/JZVideo) 33 | - [MultiStatePage](https://github.com/Zhao-Yan-Yan/MultiStatePage) 34 | - [AgentWeb](https://github.com/Justson/AgentWeb) 35 | - [开放API-2.0](https://api.apiopen.top/) 36 | 37 | # 关于我 38 | - WX:fmtjava 39 | - QQ:2694746499 40 | - Email:2694746499@qq.com 41 | - Github:https://github.com/fmtjava 42 | 43 | # 声明 44 | 项目中的API来自开放API-2.0官网,纯属学习交流使用,不得用于商业用途! 45 | 46 | # License 47 | 48 | Copyright (c) 2022 fmtjava 49 | 50 | Permission is hereby granted, free of charge, to any person obtaining a copy 51 | of this software and associated documentation files (the "Software"), to deal 52 | in the Software without restriction, including without limitation the rights 53 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 54 | copies of the Software, and to permit persons to whom the Software is 55 | furnished to do so, subject to the following conditions: 56 | 57 | The above copyright notice and this permission notice shall be included in all 58 | copies or substantial portions of the Software. 59 | 60 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 61 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 62 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 63 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 64 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 65 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 66 | SOFTWARE. 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | id 'com.google.dagger.hilt.android' 6 | } 7 | 8 | android { 9 | compileSdk 32 10 | 11 | defaultConfig { 12 | applicationId "com.fmt.mvi.learn" 13 | minSdk 21 14 | targetSdk 32 15 | versionCode 2 16 | versionName "1.1" 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 | buildFeatures { 35 | dataBinding true 36 | } 37 | } 38 | def lifecycle_version = "2.5.0" 39 | dependencies { 40 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version") 41 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version") 42 | implementation 'androidx.core:core-ktx:1.8.0' 43 | implementation("androidx.activity:activity-ktx:1.5.0") 44 | implementation("androidx.fragment:fragment-ktx:1.5.0") 45 | implementation "androidx.startup:startup-runtime:1.2.0-alpha01" 46 | implementation 'androidx.appcompat:appcompat:1.4.2' 47 | implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") 48 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 49 | implementation 'com.google.android.material:material:1.6.1' 50 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 51 | implementation "androidx.core:core-splashscreen:1.0.0-rc01" 52 | testImplementation 'junit:junit:4.13.2' 53 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 55 | 56 | implementation 'com.ashokvarma.android:bottom-navigation-bar:2.2.0' 57 | implementation 'cn.jzvd:jiaozivideoplayer:7.7.0' 58 | implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.7' 59 | implementation("io.coil-kt:coil:2.0.0-rc02") 60 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' 61 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0' 62 | implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0' 63 | implementation 'com.orhanobut:logger:2.2.0' 64 | implementation 'com.github.Zhao-Yan-Yan:MultiStatePage:2.0.2' 65 | implementation 'com.github.Justson.AgentWeb:agentweb-core:v5.0.0-alpha.1-androidx' 66 | 67 | implementation "com.google.dagger:hilt-android:2.44" 68 | kapt "com.google.dagger:hilt-compiler:2.44" 69 | } -------------------------------------------------------------------------------- /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/release/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmtjava/MVI_Learn/818338a7a4f9f9832a2804ccb4ab3bcffb795cca/app/release/app-release.apk -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "com.fmt.mvi.learn", 8 | "variantName": "release", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 2, 15 | "versionName": "1.1", 16 | "outputFile": "app-release.apk" 17 | } 18 | ], 19 | "elementType": "File" 20 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/fmt/mvi/learn/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn 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.fmt.mvi.learn", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 29 | 30 | 35 | 38 | 39 | 40 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/App.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class App : Application() -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/commom/ext/ImageExt.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.commom.ext 2 | 3 | import android.widget.ImageView 4 | import androidx.databinding.BindingAdapter 5 | import coil.load 6 | import coil.transform.CircleCropTransformation 7 | 8 | @BindingAdapter(value = ["url", "isCircle"], requireAll = false) 9 | fun ImageView.loadUrl(url: String, isCircle: Boolean) { 10 | load(url) { 11 | crossfade(true) 12 | if (isCircle) { 13 | transformations(CircleCropTransformation()) 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/commom/ui/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.commom.ui 2 | 3 | import androidx.fragment.app.Fragment 4 | 5 | abstract class BaseFragment : Fragment() { 6 | 7 | private var mHasLoadedData = false 8 | 9 | override fun onResume() { 10 | super.onResume() 11 | if (!mHasLoadedData) { 12 | loadPageData() 13 | mHasLoadedData = true 14 | } 15 | } 16 | 17 | abstract fun loadPageData() 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/commom/utils/ShareUtils.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.commom.utils 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | 6 | object ShareUtils { 7 | 8 | private const val TEXT_PLAIN = "text/plain" 9 | 10 | fun share(context: Context, title: String, content: String) { 11 | with(Intent()) { 12 | action = Intent.ACTION_SEND 13 | putExtra(Intent.EXTRA_SUBJECT, title) 14 | putExtra(Intent.EXTRA_TEXT, content) 15 | type = TEXT_PLAIN 16 | context.startActivity(Intent.createChooser(this, title)) 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/di/NetModule.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.di 2 | 3 | import com.fmt.mvi.learn.BuildConfig 4 | import com.fmt.mvi.learn.gobal.ConfigKeys 5 | import com.fmt.mvi.learn.gobal.Configurator 6 | import com.fmt.mvi.learn.net.Api 7 | import com.orhanobut.logger.Logger 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.components.SingletonComponent 12 | import okhttp3.OkHttpClient 13 | import okhttp3.logging.HttpLoggingInterceptor 14 | import retrofit2.Retrofit 15 | import retrofit2.converter.gson.GsonConverterFactory 16 | 17 | @Module 18 | @InstallIn(SingletonComponent::class) 19 | object NetModule { 20 | 21 | @Provides 22 | fun providerOkHttpClient(): OkHttpClient { 23 | return OkHttpClient.Builder() 24 | .apply { 25 | if (BuildConfig.DEBUG) { 26 | addInterceptor(HttpLoggingInterceptor { 27 | Logger.d(it) 28 | 29 | }.setLevel(HttpLoggingInterceptor.Level.BODY)) 30 | } 31 | } 32 | .build() 33 | } 34 | 35 | @Provides 36 | fun providerRetrofit(okHttpClient: OkHttpClient): Retrofit { 37 | return Retrofit.Builder() 38 | .client(okHttpClient) 39 | .addConverterFactory(GsonConverterFactory.create()) 40 | .baseUrl(Configurator.getConfiguration(ConfigKeys.API_HOST)) 41 | .build() 42 | } 43 | 44 | @Provides 45 | fun providerApiService(retrofit: Retrofit): Api { 46 | return retrofit.create(Api::class.java) 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/di/SingletonModule.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.di 2 | 3 | import com.google.gson.Gson 4 | import dagger.Module 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | 8 | @Module 9 | @InstallIn(SingletonComponent::class) 10 | object SingletonModule { 11 | 12 | fun providerGson(): Gson { 13 | return Gson() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/gobal/ConfigKeys.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.gobal 2 | 3 | enum class ConfigKeys { 4 | API_HOST, 5 | APPLICATION_CONTEXT, 6 | CONFIG_READY 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/gobal/Configurator.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.gobal 2 | 3 | import android.content.Context 4 | 5 | /** 6 | * 定义全局变量配置 7 | */ 8 | object Configurator { 9 | 10 | private val CONFIGS = mutableMapOf() 11 | 12 | //链式初始化相关变量 13 | fun withApiHost(host: String): Configurator { 14 | CONFIGS[ConfigKeys.API_HOST] = host 15 | return this 16 | } 17 | 18 | fun withApplicationContext(context: Context): Configurator { 19 | CONFIGS[ConfigKeys.APPLICATION_CONTEXT] = context 20 | return this 21 | } 22 | 23 | fun configure() { 24 | CONFIGS[ConfigKeys.CONFIG_READY] = true 25 | } 26 | 27 | private fun checkConfiguration() { 28 | val isReady = CONFIGS[ConfigKeys.CONFIG_READY] as Boolean 29 | if (!isReady) { 30 | throw RuntimeException("Configuration is not ready,call configure"); 31 | } 32 | } 33 | 34 | fun getConfiguration(key: Any): T { 35 | checkConfiguration() 36 | CONFIGS[key] ?: throw NullPointerException("$key IS NULL") 37 | return CONFIGS[key] as T 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/gobal/GsonExt.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.gobal 2 | 3 | import com.google.gson.Gson 4 | 5 | /** 6 | * 若不你用Hilt进行依赖注入,则可以使用该文件下的工具类 7 | */ 8 | val gson by lazy { Gson() } 9 | 10 | inline fun fromJson(json: String): T { 11 | return gson.fromJson(json, T::class.java) 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/main/action/MainViewAction.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.main.action 2 | 3 | sealed class MainViewAction { 4 | 5 | object GetCurrentTabIndex : MainViewAction() 6 | 7 | data class SaveCurrentTabIndex(val index: Int) : MainViewAction() 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/main/activity/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.main.activity 2 | 3 | import android.os.Bundle 4 | import androidx.activity.viewModels 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 7 | import androidx.core.view.WindowCompat 8 | import androidx.fragment.app.Fragment 9 | import androidx.lifecycle.flowWithLifecycle 10 | import androidx.lifecycle.lifecycleScope 11 | import androidx.viewpager2.adapter.FragmentStateAdapter 12 | import cn.jzvd.Jzvd 13 | import com.ashokvarma.bottomnavigation.BottomNavigationBar 14 | import com.ashokvarma.bottomnavigation.BottomNavigationItem 15 | import com.fmt.mvi.learn.R 16 | import com.fmt.mvi.learn.databinding.ActivityMainBinding 17 | import com.fmt.mvi.learn.main.action.MainViewAction 18 | import com.fmt.mvi.learn.main.factory.MainFragmentFactory 19 | import com.fmt.mvi.learn.main.state.MainViewState 20 | import com.fmt.mvi.learn.main.viewmodel.MainViewModel 21 | import dagger.hilt.android.AndroidEntryPoint 22 | 23 | @AndroidEntryPoint 24 | class MainActivity : AppCompatActivity() { 25 | 26 | companion object { 27 | const val FRAGMENT_COUNT = 2 28 | } 29 | 30 | private lateinit var mBinding: ActivityMainBinding 31 | private val mViewModel by viewModels() 32 | 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | super.onCreate(savedInstanceState) 35 | installSplashScreen() 36 | mBinding = ActivityMainBinding.inflate(layoutInflater) 37 | setContentView(mBinding.root) 38 | initWindow() 39 | initView() 40 | registerUIStateCallback() 41 | mViewModel.dispatch(MainViewAction.GetCurrentTabIndex) 42 | } 43 | 44 | private fun initWindow() { 45 | //https://blog.csdn.net/StjunF/article/details/121840122 46 | //通过WindowInsetsControllerCompat可以简化状态栏、导航栏、键盘控制 47 | WindowCompat.getInsetsController(window, findViewById(android.R.id.content)).apply { 48 | isAppearanceLightStatusBars = true 49 | } 50 | } 51 | 52 | private fun initView() { 53 | mBinding.viewPager.isUserInputEnabled = false 54 | mBinding.viewPager.adapter = object : FragmentStateAdapter(this) { 55 | 56 | override fun getItemCount(): Int = FRAGMENT_COUNT 57 | 58 | override fun createFragment(position: Int): Fragment = 59 | MainFragmentFactory.create(position) 60 | } 61 | mBinding.bottomNavigationView.setActiveColor(R.color.selected_tint_color) 62 | .setInActiveColor(R.color.default_tint_color) 63 | .addItem(BottomNavigationItem(R.drawable.ic_travel, getString(R.string.travel))) 64 | .addItem(BottomNavigationItem(R.drawable.ic_video, getString(R.string.video))) 65 | .setTabSelectedListener(object : BottomNavigationBar.SimpleOnTabSelectedListener() { 66 | override fun onTabSelected(position: Int) { 67 | Jzvd.releaseAllVideos() 68 | mBinding.viewPager.currentItem = position 69 | mViewModel.dispatch(MainViewAction.SaveCurrentTabIndex(position)) 70 | } 71 | }) 72 | .initialise() 73 | } 74 | 75 | private fun registerUIStateCallback() { 76 | lifecycleScope.launchWhenResumed { 77 | mViewModel.state.flowWithLifecycle(lifecycle).collect { viewState -> 78 | when (viewState) { 79 | is MainViewState.InitialDefaultTab -> { 80 | mBinding.bottomNavigationView.selectTab(viewState.index) 81 | } 82 | } 83 | } 84 | } 85 | } 86 | 87 | override fun onPause() { 88 | super.onPause() 89 | Jzvd.releaseAllVideos() 90 | } 91 | 92 | override fun onBackPressed() { 93 | if (Jzvd.backPress()) { 94 | return 95 | } 96 | super.onBackPressed() 97 | } 98 | 99 | override fun onDestroy() { 100 | super.onDestroy() 101 | mBinding.unbind() 102 | MainFragmentFactory.clear() 103 | } 104 | 105 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/main/factory/MainFragmentFactory.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.main.factory 2 | 3 | import android.util.SparseArray 4 | import androidx.fragment.app.Fragment 5 | import com.fmt.mvi.learn.travel.fragment.TravelFragment 6 | import com.fmt.mvi.learn.video.fragment.VideoFragment 7 | 8 | class MainFragmentFactory { 9 | 10 | companion object { 11 | private val fragments by lazy { SparseArray() } 12 | 13 | @JvmStatic 14 | fun create(position: Int): Fragment { 15 | var fragment = fragments.get(position) 16 | if (fragment == null) { 17 | fragment = when (position) { 18 | 0 -> TravelFragment() 19 | else -> VideoFragment() 20 | } 21 | fragments.put(position, fragment) 22 | } 23 | return fragment 24 | } 25 | 26 | @JvmStatic 27 | fun clear() { 28 | fragments.clear() 29 | } 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/main/state/MainViewState.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.main.state 2 | 3 | sealed class MainViewState { 4 | 5 | data class InitialDefaultTab(val index: Int) : MainViewState() 6 | 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/main/viewmodel/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.main.viewmodel 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.fmt.mvi.learn.main.action.MainViewAction 7 | import com.fmt.mvi.learn.main.state.MainViewState 8 | import kotlinx.coroutines.flow.MutableSharedFlow 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.StateFlow 11 | import kotlinx.coroutines.launch 12 | 13 | class MainViewModel(private val savedHandle: SavedStateHandle) : ViewModel() { 14 | 15 | companion object { 16 | private const val SELECTED_TAB_INDEX = "selected_tab_index" 17 | } 18 | 19 | private val _state = MutableStateFlow(MainViewState.InitialDefaultTab(0)) 20 | val state: StateFlow 21 | get() = _state 22 | private val userIntent = MutableSharedFlow() 23 | 24 | init { 25 | viewModelScope.launch { 26 | userIntent.collect { viewAction -> 27 | when (viewAction) { 28 | is MainViewAction.GetCurrentTabIndex -> getDefaultTabSelectedIndex() 29 | is MainViewAction.SaveCurrentTabIndex -> saveTabSelectedIndex(viewAction.index) 30 | } 31 | } 32 | } 33 | } 34 | 35 | fun dispatch(viewAction: MainViewAction) { 36 | viewModelScope.launch { 37 | userIntent.emit(viewAction) 38 | } 39 | } 40 | 41 | private fun getDefaultTabSelectedIndex() { 42 | val index = savedHandle.get(SELECTED_TAB_INDEX) ?: 0 43 | _state.value = MainViewState.InitialDefaultTab(index) 44 | } 45 | 46 | private fun saveTabSelectedIndex(index: Int) { 47 | savedHandle[SELECTED_TAB_INDEX] = index 48 | _state.value = MainViewState.InitialDefaultTab(index) 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/net/Api.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.net 2 | 3 | import com.fmt.mvi.learn.travel.model.Params 4 | import com.fmt.mvi.learn.travel.model.TravelModel 5 | import com.fmt.mvi.learn.travel.model.TravelTabModel 6 | import com.fmt.mvi.learn.url.URLs 7 | import com.fmt.mvi.learn.video.model.VideoListResponseModel 8 | import retrofit2.http.* 9 | 10 | interface Api { 11 | 12 | @GET 13 | suspend fun getTravelTab(@Url url: String = URLs.TRAVEL_TAB_URL): TravelModel 14 | 15 | @POST 16 | suspend fun getTravelCategoryList( 17 | @Url url: String, 18 | @Body params: Params 19 | ): TravelTabModel 20 | 21 | @GET 22 | suspend fun getVideoList( 23 | @Url url: String = URLs.VIDEO_LIST_URL, 24 | @Query("page") page: Int, 25 | @Query("size") size: Int = 10, 26 | ): VideoListResponseModel 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/net/ApiService.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.net 2 | 3 | import com.fmt.mvi.learn.BuildConfig 4 | import com.fmt.mvi.learn.gobal.ConfigKeys 5 | import com.fmt.mvi.learn.gobal.Configurator 6 | import com.orhanobut.logger.Logger 7 | import okhttp3.OkHttpClient 8 | import okhttp3.logging.HttpLoggingInterceptor 9 | import retrofit2.Retrofit 10 | import retrofit2.converter.gson.GsonConverterFactory 11 | 12 | /** 13 | * 若网络不你用Hilt进行依赖注入,则可以使用该文件下的工具类 14 | */ 15 | private val retrofit by lazy { 16 | Retrofit.Builder() 17 | .client(okHttpClient) 18 | .addConverterFactory(GsonConverterFactory.create()) 19 | .baseUrl(Configurator.getConfiguration(ConfigKeys.API_HOST)) 20 | .build() 21 | } 22 | 23 | private val okHttpClient by lazy { 24 | OkHttpClient.Builder() 25 | .apply { 26 | if (BuildConfig.DEBUG) { 27 | addInterceptor(HttpLoggingInterceptor { 28 | Logger.d(it) 29 | 30 | }.setLevel(HttpLoggingInterceptor.Level.BODY)) 31 | } 32 | } 33 | .build() 34 | } 35 | 36 | object ApiService : Api by retrofit.create(Api::class.java) -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/startup/AppInitializer.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.startup 2 | 3 | import android.content.Context 4 | import androidx.startup.Initializer 5 | import com.fmt.mvi.learn.gobal.Configurator 6 | import com.fmt.mvi.learn.url.URLs 7 | import com.orhanobut.logger.AndroidLogAdapter 8 | import com.orhanobut.logger.Logger 9 | 10 | class AppInitializer : Initializer { 11 | 12 | override fun create(context: Context) { 13 | Configurator.withApiHost(URLs.VIDEO_LIST_URL) 14 | .withApplicationContext(context.applicationContext) 15 | .configure() 16 | Logger.addLogAdapter(AndroidLogAdapter()) 17 | } 18 | 19 | override fun dependencies(): MutableList>> { 20 | return mutableListOf() 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/travel/action/TravelTabViewAction.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.travel.action 2 | 3 | import com.fmt.mvi.learn.travel.model.Params 4 | 5 | sealed class TravelTabViewAction { 6 | 7 | data class Refresh(val url: String, val param: Params) : TravelTabViewAction() 8 | 9 | data class LoadMore(val url: String, val param: Params) : TravelTabViewAction() 10 | 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/travel/action/TravelViewAction.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.travel.action 2 | 3 | sealed class TravelViewAction { 4 | 5 | object GetTravelTabs : TravelViewAction() 6 | 7 | object Retry : TravelViewAction() 8 | 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/travel/activity/TravelDetailActivity.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.travel.activity 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.view.KeyEvent 7 | import android.widget.LinearLayout 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.core.view.WindowCompat 10 | import com.fmt.mvi.learn.databinding.ActivityTravelDetailBinding 11 | import com.just.agentweb.AgentWeb 12 | 13 | class TravelDetailActivity : AppCompatActivity() { 14 | 15 | private lateinit var mBinding: ActivityTravelDetailBinding 16 | private lateinit var mAgentWeb: AgentWeb 17 | 18 | companion object { 19 | const val KEY_TITLE = "key_title" 20 | const val KEY_URL = "key_url" 21 | fun start(context: Context, title: String, url: String) { 22 | val intent = Intent(context, TravelDetailActivity::class.java) 23 | intent.putExtra(KEY_TITLE, title) 24 | intent.putExtra(KEY_URL, url) 25 | context.startActivity(intent) 26 | } 27 | } 28 | 29 | override fun onCreate(savedInstanceState: Bundle?) { 30 | super.onCreate(savedInstanceState) 31 | mBinding = ActivityTravelDetailBinding.inflate(layoutInflater) 32 | setContentView(mBinding.root) 33 | initWindow() 34 | initView() 35 | } 36 | 37 | private fun initWindow() { 38 | WindowCompat.getInsetsController(window, findViewById(android.R.id.content)).apply { 39 | isAppearanceLightStatusBars = true 40 | } 41 | } 42 | 43 | private fun initView() { 44 | mBinding.toolbar.apply { 45 | title = intent.getStringExtra(KEY_TITLE) 46 | setSupportActionBar(this) 47 | supportActionBar?.setHomeButtonEnabled(true) 48 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 49 | setNavigationOnClickListener { 50 | onBackPressed() 51 | } 52 | } 53 | mAgentWeb = AgentWeb.with(this) 54 | .setAgentWebParent(mBinding.llContent, LinearLayout.LayoutParams(-1, -1)) 55 | .useDefaultIndicator() 56 | .createAgentWeb() 57 | .ready() 58 | .go(intent.getStringExtra(KEY_URL)) 59 | } 60 | 61 | override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { 62 | return if (mAgentWeb.handleKeyEvent(keyCode, event)) { 63 | true 64 | } else super.onKeyDown(keyCode, event) 65 | } 66 | 67 | override fun onPause() { 68 | mAgentWeb.webLifeCycle.onPause() 69 | super.onPause() 70 | } 71 | 72 | override fun onResume() { 73 | mAgentWeb.webLifeCycle.onResume() 74 | super.onResume() 75 | } 76 | 77 | override fun onDestroy() { 78 | mAgentWeb.webLifeCycle.onDestroy() 79 | super.onDestroy() 80 | } 81 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/travel/adapter/TravelPageAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.travel.adapter 2 | 3 | import android.os.Bundle 4 | import androidx.collection.ArrayMap 5 | import androidx.fragment.app.Fragment 6 | import androidx.viewpager2.adapter.FragmentStateAdapter 7 | import com.fmt.mvi.learn.gobal.gson 8 | import com.fmt.mvi.learn.travel.fragment.TravelTabFragment 9 | import com.fmt.mvi.learn.travel.model.TravelModel 10 | 11 | class TravelPageAdapter(fragment: Fragment, private val travelTabModel: TravelModel) : 12 | FragmentStateAdapter(fragment) { 13 | 14 | private val fragments by lazy { ArrayMap(travelTabModel.tabs.size) } 15 | 16 | override fun getItemCount(): Int { 17 | return travelTabModel.tabs.size 18 | } 19 | 20 | override fun createFragment(position: Int): Fragment { 21 | val tab = travelTabModel.tabs[position] 22 | var fragment = fragments[tab.groupChannelCode] 23 | if (fragment == null) { 24 | val bundle = Bundle() 25 | with(bundle) { 26 | putString(TravelTabFragment.URL, travelTabModel.url) 27 | putString(TravelTabFragment.PARAMS, gson.toJson(travelTabModel.params)) 28 | putString(TravelTabFragment.GROUP_CHANNEL_CODE, tab.groupChannelCode) 29 | putInt(TravelTabFragment.TYPE, tab.type) 30 | } 31 | fragment = TravelTabFragment.newInstance(bundle) 32 | fragments[tab.groupChannelCode] = fragment 33 | } 34 | return fragment 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/travel/adapter/TravelTabAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.travel.adapter 2 | 3 | import android.content.Context 4 | import android.widget.ImageView 5 | import cn.jzvd.JZUtils 6 | import com.chad.library.adapter.base.BaseQuickAdapter 7 | import com.chad.library.adapter.base.module.LoadMoreModule 8 | import com.chad.library.adapter.base.viewholder.BaseDataBindingHolder 9 | import com.fmt.mvi.learn.R 10 | import com.fmt.mvi.learn.databinding.ItemTravelBinding 11 | import com.fmt.mvi.learn.gobal.ConfigKeys 12 | import com.fmt.mvi.learn.gobal.Configurator 13 | import com.fmt.mvi.learn.travel.model.TravelItem 14 | import com.just.agentweb.AgentWebUtils 15 | 16 | class TravelTabAdapter : 17 | BaseQuickAdapter>(R.layout.item_travel), 18 | LoadMoreModule { 19 | 20 | private val mMaxImageWidth = 21 | (JZUtils.getScreenWidth(Configurator.getConfiguration(ConfigKeys.APPLICATION_CONTEXT)) 22 | - AgentWebUtils.dp2px( 23 | Configurator.getConfiguration(ConfigKeys.APPLICATION_CONTEXT), 24 | 20f 25 | )) / 2 26 | 27 | override fun convert(holder: BaseDataBindingHolder, item: TravelItem) { 28 | holder.dataBinding?.item = item 29 | val coverImageInfo = item.article.images[0] 30 | val layoutParams = holder.getView(R.id.iv_cover).layoutParams 31 | layoutParams.width = mMaxImageWidth 32 | layoutParams.height = 33 | (mMaxImageWidth * coverImageInfo.height / coverImageInfo.width).toInt() 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/travel/fragment/TravelFragment.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.travel.fragment 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.Toast 8 | import androidx.fragment.app.viewModels 9 | import androidx.lifecycle.flowWithLifecycle 10 | import androidx.lifecycle.lifecycleScope 11 | import com.fmt.mvi.learn.commom.ui.BaseFragment 12 | import com.fmt.mvi.learn.databinding.FragmentTravelBinding 13 | import com.fmt.mvi.learn.travel.adapter.TravelPageAdapter 14 | import com.fmt.mvi.learn.travel.action.TravelViewAction 15 | import com.fmt.mvi.learn.travel.model.TravelModel 16 | import com.fmt.mvi.learn.travel.state.TravelViewState 17 | import com.fmt.mvi.learn.travel.viewmodel.TravelViewModel 18 | import com.google.android.material.tabs.TabLayoutMediator 19 | import com.zy.multistatepage.state.ErrorState 20 | import com.zy.multistatepage.state.LoadingState 21 | import com.zy.multistatepage.state.SuccessState 22 | import dagger.hilt.android.AndroidEntryPoint 23 | import kotlinx.coroutines.flow.distinctUntilChanged 24 | 25 | @AndroidEntryPoint 26 | class TravelFragment : BaseFragment() { 27 | 28 | private lateinit var mBinding: FragmentTravelBinding 29 | private val mViewModel: TravelViewModel by viewModels() 30 | 31 | override fun onCreateView( 32 | inflater: LayoutInflater, 33 | container: ViewGroup?, 34 | savedInstanceState: Bundle?, 35 | ): View { 36 | mBinding = FragmentTravelBinding.inflate(layoutInflater) 37 | return mBinding.root 38 | } 39 | 40 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 41 | super.onViewCreated(view, savedInstanceState) 42 | registerUIStateCallback() 43 | } 44 | 45 | private fun registerUIStateCallback() { 46 | lifecycleScope.launchWhenResumed { 47 | mViewModel.state.flowWithLifecycle(lifecycle).distinctUntilChanged().collect { state -> 48 | when (state) { 49 | is TravelViewState.LoadingState -> mBinding.multiStateContainer.show() 50 | is TravelViewState.LoadSuccess -> { 51 | mBinding.multiStateContainer.show() 52 | initTravelTab(state.travelTabModel) 53 | } 54 | is TravelViewState.LoadFail -> { 55 | mBinding.multiStateContainer.show { 56 | it.retry { 57 | mViewModel.dispatch(TravelViewAction.Retry) 58 | } 59 | } 60 | Toast.makeText(requireContext(), state.errorMsg, Toast.LENGTH_LONG).show() 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | private fun initTravelTab(travelTabModel: TravelModel) { 68 | for (tab in travelTabModel.tabs) { 69 | mBinding.tabLayout.addTab(mBinding.tabLayout.newTab()) 70 | } 71 | mBinding.viewPager.adapter = TravelPageAdapter(this, travelTabModel) 72 | mBinding.viewPager.offscreenPageLimit = travelTabModel.tabs.size 73 | TabLayoutMediator(mBinding.tabLayout, mBinding.viewPager) { tab, position -> 74 | tab.text = travelTabModel.tabs[position].labelName 75 | }.attach() 76 | } 77 | 78 | override fun loadPageData() { 79 | mViewModel.dispatch(TravelViewAction.GetTravelTabs) 80 | } 81 | 82 | override fun onDestroy() { 83 | super.onDestroy() 84 | mBinding.unbind() 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/travel/fragment/TravelTabFragment.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.travel.fragment 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.Toast 8 | import androidx.fragment.app.viewModels 9 | import androidx.lifecycle.flowWithLifecycle 10 | import androidx.lifecycle.lifecycleScope 11 | import androidx.recyclerview.widget.StaggeredGridLayoutManager 12 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout 13 | import com.chad.library.adapter.base.BaseQuickAdapter 14 | import com.chad.library.adapter.base.listener.OnItemClickListener 15 | import com.chad.library.adapter.base.listener.OnLoadMoreListener 16 | import com.fmt.mvi.learn.R 17 | import com.fmt.mvi.learn.commom.ui.BaseFragment 18 | import com.fmt.mvi.learn.databinding.FragmentCommonListBinding 19 | import com.fmt.mvi.learn.gobal.fromJson 20 | import com.fmt.mvi.learn.travel.adapter.TravelTabAdapter 21 | import com.fmt.mvi.learn.travel.action.TravelTabViewAction 22 | import com.fmt.mvi.learn.travel.activity.TravelDetailActivity 23 | import com.fmt.mvi.learn.travel.model.Params 24 | import com.fmt.mvi.learn.travel.state.TravelTabViewState 25 | import com.fmt.mvi.learn.travel.viewmodel.TravelTabViewModel 26 | import dagger.hilt.android.AndroidEntryPoint 27 | import kotlinx.coroutines.flow.distinctUntilChanged 28 | 29 | @AndroidEntryPoint 30 | class TravelTabFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, OnLoadMoreListener, 31 | OnItemClickListener { 32 | 33 | private lateinit var mBinding: FragmentCommonListBinding 34 | private lateinit var mRequestParams: Params 35 | private val mViewModel: TravelTabViewModel by viewModels() 36 | private val mAdapter by lazy { TravelTabAdapter() } 37 | private var mRequestUrl = "" 38 | private var mGroupChannelCode = "" 39 | private var mCurrentPage = 0 40 | 41 | companion object { 42 | const val URL = "url" 43 | const val PARAMS = "params" 44 | const val GROUP_CHANNEL_CODE = "groupChannelCode" 45 | const val TYPE = "type" 46 | fun newInstance(arguments: Bundle): TravelTabFragment { 47 | val travelTabFragment = TravelTabFragment() 48 | travelTabFragment.arguments = arguments 49 | return travelTabFragment 50 | } 51 | } 52 | 53 | override fun onCreateView( 54 | inflater: LayoutInflater, 55 | container: ViewGroup?, 56 | savedInstanceState: Bundle?, 57 | ): View { 58 | mBinding = FragmentCommonListBinding.inflate(inflater, container, false) 59 | return mBinding.root 60 | } 61 | 62 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 63 | super.onViewCreated(view, savedInstanceState) 64 | initView() 65 | initRequestParams() 66 | registerUIStateCallback() 67 | } 68 | 69 | private fun initView() { 70 | mCurrentPage = 0 71 | mBinding.swipeRefreshLayout.setColorSchemeResources(R.color.selected_tint_color) 72 | mBinding.swipeRefreshLayout.setOnRefreshListener(this) 73 | mBinding.rcvVideo.layoutManager = 74 | StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) 75 | mAdapter.loadMoreModule.setOnLoadMoreListener(this) 76 | mAdapter.setOnItemClickListener(this) 77 | mBinding.rcvVideo.adapter = mAdapter 78 | } 79 | 80 | private fun initRequestParams() { 81 | arguments?.let { bundle -> 82 | mRequestUrl = bundle.getString(URL).toString() 83 | mRequestParams = fromJson(bundle.getString(PARAMS).toString()) 84 | mGroupChannelCode = bundle.getString(GROUP_CHANNEL_CODE).toString() 85 | mRequestParams.groupChannelCode = bundle.getString(GROUP_CHANNEL_CODE).toString() 86 | mRequestParams.type = bundle.getInt(TYPE) 87 | } 88 | } 89 | 90 | private fun registerUIStateCallback() { 91 | lifecycleScope.launchWhenResumed { 92 | mViewModel.state.flowWithLifecycle(lifecycle).distinctUntilChanged() 93 | .collect { viewState -> 94 | when (viewState) { 95 | is TravelTabViewState.LoadingState -> { 96 | mBinding.swipeRefreshLayout.isRefreshing = true 97 | } 98 | is TravelTabViewState.RefreshSuccess -> { 99 | mBinding.swipeRefreshLayout.isRefreshing = false 100 | mAdapter.setList(viewState.travelList) 101 | } 102 | is TravelTabViewState.LoadMoreSuccess -> { 103 | mBinding.swipeRefreshLayout.isRefreshing = false 104 | mAdapter.addData(viewState.travelList) 105 | if (viewState.travelList.isEmpty()) { 106 | mAdapter.loadMoreModule.loadMoreEnd() 107 | } else { 108 | mAdapter.loadMoreModule.loadMoreComplete() 109 | } 110 | } 111 | is TravelTabViewState.LoadError -> { 112 | mBinding.swipeRefreshLayout.isRefreshing = false 113 | Toast.makeText(requireContext(), viewState.errorMsg, Toast.LENGTH_LONG) 114 | .show() 115 | mAdapter.loadMoreModule.loadMoreFail() 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | override fun loadPageData() { 123 | mRequestParams.pagePara.pageIndex = mCurrentPage 124 | mViewModel.dispatch(TravelTabViewAction.Refresh(mRequestUrl, mRequestParams)) 125 | } 126 | 127 | override fun onRefresh() { 128 | mCurrentPage = 0 129 | mRequestParams.pagePara.pageIndex = mCurrentPage 130 | mViewModel.dispatch(TravelTabViewAction.Refresh(mRequestUrl, mRequestParams)) 131 | } 132 | 133 | override fun onLoadMore() { 134 | ++mCurrentPage 135 | mRequestParams.pagePara.pageIndex = mCurrentPage 136 | mViewModel.dispatch(TravelTabViewAction.LoadMore(mRequestUrl, mRequestParams)) 137 | } 138 | 139 | override fun onItemClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) { 140 | val title = mAdapter.data[position].article.articleTitle 141 | val h5Url = mAdapter.data[position].article.urls.find { 142 | it.h5Url != null 143 | }?.h5Url 144 | h5Url?.let { 145 | TravelDetailActivity.start(requireContext(), title, h5Url) 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/travel/model/TravelModel.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.travel.model 2 | 3 | data class TravelModel( 4 | val params: Params, 5 | val tabs: List, 6 | val url: String 7 | ) 8 | 9 | data class Params( 10 | val contentType: String, 11 | val districtId: Int, 12 | var groupChannelCode: String, 13 | val head: Head, 14 | val imageCutType: Int, 15 | val lat: Double, 16 | val locatedDistrictId: Int, 17 | val lon: Double, 18 | val pagePara: PagePara, 19 | var type: Any 20 | ) 21 | 22 | data class Tab( 23 | val groupChannelCode: String, 24 | val labelName: String, 25 | val type: Int 26 | ) 27 | 28 | data class Head( 29 | val auth: Any, 30 | val cid: String, 31 | val ctok: String, 32 | val cver: String, 33 | val extension: List, 34 | val lang: String, 35 | val sid: String, 36 | val syscode: String 37 | ) 38 | 39 | data class PagePara( 40 | var pageIndex: Int, 41 | var pageSize: Int, 42 | val sortDirection: Int, 43 | val sortType: Int 44 | ) 45 | 46 | data class Extension( 47 | val name: String, 48 | val value: String 49 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/travel/model/TravelTabModel.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.travel.model 2 | 3 | data class TravelTabModel( 4 | val totalCount: Int, 5 | val resultList: List 6 | ) 7 | 8 | data class TravelItem( 9 | val type: Int, 10 | val article: Article 11 | ) 12 | 13 | data class Article( 14 | val articleId: Int, 15 | val productType: Int, 16 | val sourceType: Int, 17 | val articleTitle: String, 18 | val author: Author, 19 | val images: List, 20 | val hasVideo: Boolean, 21 | val readCount: Int, 22 | val likeCount: Int, 23 | val commentCount: Int, 24 | val urls: List, 25 | val tags: List, 26 | val topics: List, 27 | val pois: List, 28 | val publishTime: String, 29 | val publishTimeDisplay: String, 30 | val shootTime: String, 31 | val shootTimeDisplay: String, 32 | val level: Int, 33 | val distanceText: String, 34 | val isLike: Boolean, 35 | val imageCounts: Int, 36 | val isCollected: Boolean, 37 | val collectCount: Int, 38 | val articleStatus: Int, 39 | val poiName: String 40 | ) 41 | 42 | data class Author( 43 | val authorId: Int, 44 | val nickName: String, 45 | val clientAuth: String, 46 | val jumpUrl: String, 47 | val coverImage: CoverImage 48 | ) 49 | 50 | data class CoverImage( 51 | val dynamicUrl: String, 52 | val originalUrl: String 53 | ) 54 | 55 | data class Images( 56 | val imageId: Int, 57 | val dynamicUrl: String, 58 | val originalUrl: String, 59 | val width: Double, 60 | val height: Double, 61 | val mediaType: Int, 62 | val lat: Double, 63 | val lon: Double 64 | ) 65 | 66 | data class Urls( 67 | val version: String, 68 | val appUrl: String, 69 | val h5Url: String?, 70 | val wxUrl: String 71 | ) 72 | 73 | data class Tags( 74 | val tagId: Int, 75 | val tagName: String, 76 | val tagLevel: Int, 77 | val parentTagId: Int, 78 | val source: Int, 79 | val sortIndex: Int 80 | ) 81 | 82 | data class Topics( 83 | val topicId: Int, 84 | val topicName: String, 85 | val level: Int 86 | ) 87 | 88 | data class Pois( 89 | val poiType: Int, 90 | val poiId: Int, 91 | val poiName: String, 92 | val districtId: Int, 93 | val districtName: String, 94 | val poiExt: PoiExt, 95 | val source: Int, 96 | val isMain: Int, 97 | val isInChin: Boolean 98 | ) 99 | 100 | data class PoiExt( 101 | val h5Url: String, 102 | val appUrl: String 103 | ) 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/travel/state/TravelTabViewState.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.travel.state 2 | 3 | import com.fmt.mvi.learn.travel.model.TravelItem 4 | 5 | sealed class TravelTabViewState { 6 | 7 | object LoadingState : TravelTabViewState() 8 | 9 | data class RefreshSuccess(val travelList: List) : TravelTabViewState() 10 | 11 | data class LoadMoreSuccess(val travelList: List) : TravelTabViewState() 12 | 13 | data class LoadError(val errorMsg: String) : TravelTabViewState() 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/travel/state/TravelViewState.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.travel.state 2 | 3 | import com.fmt.mvi.learn.travel.model.TravelModel 4 | 5 | sealed class TravelViewState { 6 | 7 | object LoadingState : TravelViewState() 8 | 9 | data class LoadSuccess(val travelTabModel: TravelModel) : TravelViewState() 10 | 11 | data class LoadFail(val errorMsg: String) : TravelViewState() 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/travel/viewmodel/TravelTabViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.travel.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.fmt.mvi.learn.net.Api 6 | import com.fmt.mvi.learn.travel.action.TravelTabViewAction 7 | import com.fmt.mvi.learn.travel.model.Params 8 | import com.fmt.mvi.learn.travel.state.TravelTabViewState 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.flow.MutableSharedFlow 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.SharedFlow 13 | import kotlinx.coroutines.launch 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class TravelTabViewModel @Inject constructor(private val mApiService: Api) : ViewModel() { 18 | 19 | /** 20 | * MutableStateFlow 侧重状态 (State),状态可以是的 UI 组件的可见性,它始终具有一个值(显示/隐藏) 21 | * MutableSharedFlow 侧重事件(Event),事件只有在满足一个或多个前提条件时才会触发,不需要也不应该有默认值 22 | */ 23 | private val _state = MutableStateFlow(TravelTabViewState.LoadingState) 24 | val state: SharedFlow 25 | get() = _state 26 | 27 | private val userIntent = MutableSharedFlow() 28 | 29 | init { 30 | viewModelScope.launch { 31 | userIntent.collect { viewAction -> 32 | when (viewAction) { 33 | is TravelTabViewAction.Refresh -> getTravelCategoryList( 34 | viewAction.url, 35 | viewAction.param 36 | ) 37 | is TravelTabViewAction.LoadMore -> getTravelCategoryList( 38 | viewAction.url, 39 | viewAction.param 40 | ) 41 | } 42 | } 43 | } 44 | } 45 | 46 | private fun getTravelCategoryList(url: String, params: Params) { 47 | viewModelScope.launch { 48 | kotlin.runCatching { 49 | mApiService.getTravelCategoryList(url, params) 50 | }.onSuccess { 51 | _state.emit( 52 | if (params.pagePara.pageIndex == 0) TravelTabViewState.RefreshSuccess(it.resultList) 53 | else TravelTabViewState.LoadMoreSuccess(it.resultList) 54 | ) 55 | }.onFailure { 56 | _state.emit(TravelTabViewState.LoadError(it.message.toString())) 57 | } 58 | } 59 | } 60 | 61 | fun dispatch(viewAction: TravelTabViewAction) { 62 | viewModelScope.launch { 63 | userIntent.emit(viewAction) 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/travel/viewmodel/TravelViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.travel.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.fmt.mvi.learn.net.Api 6 | import com.fmt.mvi.learn.travel.state.TravelViewState 7 | import com.fmt.mvi.learn.travel.action.TravelViewAction 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.flow.MutableSharedFlow 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.StateFlow 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | 15 | @HiltViewModel 16 | class TravelViewModel @Inject constructor(private val mApiService: Api) : ViewModel() { 17 | 18 | private val _state = MutableStateFlow(TravelViewState.LoadingState) 19 | val state: StateFlow 20 | get() = _state 21 | 22 | private val userIntent = MutableSharedFlow() 23 | 24 | init { 25 | viewModelScope.launch { 26 | userIntent.collect { viewAction -> 27 | when (viewAction) { 28 | is TravelViewAction.GetTravelTabs -> getTravelTabs() 29 | is TravelViewAction.Retry -> retry() 30 | } 31 | } 32 | } 33 | } 34 | 35 | private fun retry() { 36 | _state.value = TravelViewState.LoadingState 37 | getTravelTabs() 38 | } 39 | 40 | private fun getTravelTabs() { 41 | viewModelScope.launch { 42 | kotlin.runCatching { 43 | mApiService.getTravelTab() 44 | }.onSuccess { 45 | _state.value = TravelViewState.LoadSuccess(it) 46 | }.onFailure { 47 | _state.value = TravelViewState.LoadFail(it.message.toString()) 48 | } 49 | } 50 | } 51 | 52 | fun dispatch(viewAction: TravelViewAction) { 53 | viewModelScope.launch { 54 | userIntent.emit(viewAction) 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/url/URLs.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.url 2 | 3 | object URLs { 4 | 5 | const val VIDEO_LIST_URL = "https://api.apiopen.top/api/getHaoKanVideo/" 6 | 7 | const val TRAVEL_TAB_URL = "https://apk-1256738511.cos.ap-chengdu.myqcloud.com/FlutterTrip/data/travel_page.json" 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/video/action/VideoListViewAction.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.video.action 2 | 3 | /** 4 | * 定义所有用户操作 5 | */ 6 | sealed class VideoListViewAction { 7 | 8 | object Refresh : VideoListViewAction() 9 | 10 | object Retry : VideoListViewAction() 11 | 12 | data class LoadMore(val page: Int) : VideoListViewAction() 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/video/adapter/VideoListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.video.adapter 2 | 3 | import android.content.Context 4 | import android.graphics.Outline 5 | import android.view.View 6 | import android.view.ViewOutlineProvider 7 | import androidx.databinding.DataBindingUtil 8 | import cn.jzvd.Jzvd 9 | import cn.jzvd.JzvdStd 10 | import coil.load 11 | import com.chad.library.adapter.base.BaseQuickAdapter 12 | import com.chad.library.adapter.base.module.LoadMoreModule 13 | import com.chad.library.adapter.base.viewholder.BaseViewHolder 14 | import com.fmt.mvi.learn.R 15 | import com.fmt.mvi.learn.databinding.ItemVideoBinding 16 | import com.fmt.mvi.learn.gobal.ConfigKeys 17 | import com.fmt.mvi.learn.gobal.Configurator 18 | import com.fmt.mvi.learn.video.model.VideoModel 19 | import com.just.agentweb.AgentWebUtils 20 | 21 | class VideoListAdapter : 22 | BaseQuickAdapter(R.layout.item_video), 23 | LoadMoreModule { 24 | 25 | private val mOutlineProvider = object : ViewOutlineProvider() { 26 | override fun getOutline(view: View, outline: Outline) { 27 | outline.setRoundRect( 28 | 0, 0, view.width, view.height, AgentWebUtils.dp2px( 29 | Configurator.getConfiguration( 30 | ConfigKeys.APPLICATION_CONTEXT 31 | ), 4f 32 | ).toFloat() 33 | ) 34 | } 35 | } 36 | 37 | override fun onItemViewHolderCreated(viewHolder: BaseViewHolder, viewType: Int) { 38 | DataBindingUtil.bind(viewHolder.itemView) 39 | } 40 | 41 | override fun convert(holder: BaseViewHolder, item: VideoModel) { 42 | holder.getView(R.id.video_player).apply { 43 | setUp(item.playUrl, item.title, Jzvd.SCREEN_NORMAL) 44 | posterImageView.load(item.coverUrl) 45 | outlineProvider = mOutlineProvider 46 | clipToOutline = true 47 | } 48 | holder.getBinding()?.item = item 49 | } 50 | 51 | override fun onViewDetachedFromWindow(holder: BaseViewHolder) { 52 | val jzvd: Jzvd? = holder.getViewOrNull(R.id.video_player) 53 | if (jzvd != null && Jzvd.CURRENT_JZVD != null && jzvd.jzDataSource 54 | .containsTheUrl(Jzvd.CURRENT_JZVD.jzDataSource.currentUrl) 55 | ) { 56 | if (Jzvd.CURRENT_JZVD != null && Jzvd.CURRENT_JZVD.screen != Jzvd.SCREEN_FULLSCREEN) { 57 | Jzvd.releaseAllVideos() 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/video/fragment/VideoFragment.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.video.fragment 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.Toast 8 | import androidx.fragment.app.viewModels 9 | import androidx.lifecycle.flowWithLifecycle 10 | import androidx.lifecycle.lifecycleScope 11 | import androidx.recyclerview.widget.LinearLayoutManager 12 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout 13 | import com.chad.library.adapter.base.BaseQuickAdapter 14 | import com.chad.library.adapter.base.listener.OnItemChildClickListener 15 | import com.chad.library.adapter.base.listener.OnLoadMoreListener 16 | import com.fmt.mvi.learn.R 17 | import com.fmt.mvi.learn.commom.ui.BaseFragment 18 | import com.fmt.mvi.learn.commom.utils.ShareUtils 19 | import com.fmt.mvi.learn.databinding.FragmentVideoBinding 20 | import com.fmt.mvi.learn.video.action.VideoListViewAction 21 | import com.fmt.mvi.learn.video.adapter.VideoListAdapter 22 | import com.fmt.mvi.learn.video.state.VideoListViewState 23 | import com.fmt.mvi.learn.video.viewmodel.VideoViewModel 24 | import com.zy.multistatepage.state.EmptyState 25 | import com.zy.multistatepage.state.ErrorState 26 | import com.zy.multistatepage.state.LoadingState 27 | import com.zy.multistatepage.state.SuccessState 28 | import kotlinx.coroutines.flow.distinctUntilChanged 29 | 30 | class VideoFragment : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, OnLoadMoreListener, 31 | OnItemChildClickListener { 32 | 33 | private lateinit var mBinding: FragmentVideoBinding 34 | private val mAdapter by lazy { VideoListAdapter() } 35 | private val mViewModel by viewModels() 36 | private var mCurrentPage = 0 37 | 38 | override fun onCreateView( 39 | inflater: LayoutInflater, 40 | container: ViewGroup?, 41 | savedInstanceState: Bundle?, 42 | ): View { 43 | mBinding = FragmentVideoBinding.inflate(inflater, container, false) 44 | return mBinding.root 45 | } 46 | 47 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 48 | super.onViewCreated(view, savedInstanceState) 49 | initView() 50 | registerUIStateCallback() 51 | } 52 | 53 | private fun initView() { 54 | mCurrentPage = 0 55 | mBinding.swipeRefreshLayout.setColorSchemeResources(R.color.selected_tint_color) 56 | mBinding.swipeRefreshLayout.setOnRefreshListener(this) 57 | mBinding.rcvVideo.layoutManager = LinearLayoutManager(requireContext()) 58 | mAdapter.addChildClickViewIds(R.id.iv_share) 59 | mAdapter.setOnItemChildClickListener(this) 60 | mAdapter.loadMoreModule.setOnLoadMoreListener(this) 61 | mBinding.rcvVideo.adapter = mAdapter 62 | } 63 | 64 | private fun registerUIStateCallback() { 65 | lifecycleScope.launchWhenResumed { 66 | mViewModel.state.flowWithLifecycle(lifecycle).distinctUntilChanged().collect { state -> 67 | mBinding.swipeRefreshLayout.isRefreshing = false 68 | when (state) { 69 | is VideoListViewState.LoadingState -> mBinding.multiStateContainer.show() 70 | is VideoListViewState.RefreshSuccessState -> { 71 | mAdapter.setList(state.videoList) 72 | if (state.videoList.isEmpty()) 73 | mBinding.multiStateContainer.show() 74 | else 75 | mBinding.multiStateContainer.show() 76 | } 77 | is VideoListViewState.LoadMoreSuccessState -> { 78 | mAdapter.addData(state.videoList) 79 | if (state.videoList.isEmpty()) { 80 | mAdapter.loadMoreModule.loadMoreEnd() 81 | } else { 82 | mAdapter.loadMoreModule.loadMoreComplete() 83 | } 84 | } 85 | is VideoListViewState.LoadErrorState -> { 86 | if (mAdapter.itemCount == 0) { 87 | mBinding.multiStateContainer.show { 88 | it.retry { 89 | retry() 90 | } 91 | } 92 | } 93 | onLoadError(state.errorMsg) 94 | } 95 | is VideoListViewState.LoadMoreErrorState -> { 96 | onLoadError(state.errorMsg) 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | private fun onLoadError(errorMsg: String) { 104 | Toast.makeText(requireContext(), errorMsg, Toast.LENGTH_LONG).show() 105 | mAdapter.loadMoreModule.loadMoreFail() 106 | } 107 | 108 | override fun loadPageData() { 109 | mViewModel.dispatch(VideoListViewAction.Refresh) 110 | } 111 | 112 | override fun onRefresh() { 113 | mCurrentPage = 0 114 | mViewModel.dispatch(VideoListViewAction.Refresh) 115 | } 116 | 117 | override fun onLoadMore() { 118 | mViewModel.dispatch(VideoListViewAction.LoadMore(++mCurrentPage)) 119 | } 120 | 121 | private fun retry() { 122 | mCurrentPage = 0 123 | mViewModel.dispatch(VideoListViewAction.Retry) 124 | } 125 | 126 | override fun onDestroy() { 127 | super.onDestroy() 128 | mBinding.unbind() 129 | } 130 | 131 | override fun onItemChildClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) { 132 | if (view.id == R.id.iv_share) { 133 | ShareUtils.share( 134 | requireContext(), 135 | mAdapter.data[position].title, 136 | mAdapter.data[position].playUrl 137 | ) 138 | } 139 | } 140 | 141 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/video/model/VideoModel.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.video.model 2 | 3 | data class VideoListResponseModel( 4 | val code: Int, 5 | val message: String, 6 | val result: VideoListModel 7 | ) 8 | 9 | data class VideoListModel( 10 | val total: Int, 11 | val list: List 12 | ) 13 | 14 | data class VideoModel( 15 | val title: String, 16 | val userName: String, 17 | val userPic: String, 18 | val coverUrl: String, 19 | val playUrl: String, 20 | val duration: String, 21 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/video/state/VideoListViewState.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.video.state 2 | 3 | import com.fmt.mvi.learn.video.model.VideoModel 4 | 5 | /** 6 | * 定义页面所有状态 7 | */ 8 | sealed class VideoListViewState { 9 | 10 | object LoadingState : VideoListViewState() 11 | 12 | data class RefreshSuccessState(val videoList: List) : VideoListViewState() 13 | 14 | data class LoadMoreSuccessState(val videoList: List) : VideoListViewState() 15 | 16 | data class LoadErrorState(val errorMsg: String) : VideoListViewState() 17 | 18 | data class LoadMoreErrorState(val errorMsg: String) : VideoListViewState() 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/fmt/mvi/learn/video/viewmodel/VideoViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn.video.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.fmt.mvi.learn.net.ApiService 6 | import com.fmt.mvi.learn.video.action.VideoListViewAction 7 | import com.fmt.mvi.learn.video.state.VideoListViewState 8 | import kotlinx.coroutines.flow.MutableSharedFlow 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.StateFlow 11 | import kotlinx.coroutines.launch 12 | 13 | class VideoViewModel : ViewModel() { 14 | 15 | private val _state = MutableStateFlow(VideoListViewState.LoadingState) 16 | val state: StateFlow 17 | get() = _state 18 | 19 | private val userIntent = MutableSharedFlow() 20 | 21 | init { 22 | //处理用户事件 23 | viewModelScope.launch { 24 | userIntent.collect { 25 | when (it) { 26 | is VideoListViewAction.Refresh -> refresh() 27 | is VideoListViewAction.Retry -> retry() 28 | is VideoListViewAction.LoadMore -> loadMore(it.page) 29 | } 30 | } 31 | } 32 | } 33 | 34 | private fun refresh() { 35 | getVideoList() 36 | } 37 | 38 | private fun retry() { 39 | _state.value = VideoListViewState.LoadingState 40 | getVideoList() 41 | } 42 | 43 | private fun loadMore(page: Int) { 44 | getVideoList(page) 45 | } 46 | 47 | private fun getVideoList(page: Int = 0) { 48 | viewModelScope.launch { 49 | kotlin.runCatching { 50 | ApiService.getVideoList(page = page) 51 | }.onSuccess { 52 | _state.value = 53 | if (page == 0) 54 | VideoListViewState.RefreshSuccessState(it.result.list) 55 | else 56 | VideoListViewState.LoadMoreSuccessState(it.result.list) 57 | }.onFailure { 58 | _state.value = 59 | if (page == 0) 60 | VideoListViewState.LoadErrorState(it.message.toString()) 61 | else 62 | VideoListViewState.LoadMoreErrorState(it.message.toString()) 63 | } 64 | } 65 | } 66 | 67 | /** 68 | * 分发用户事件 69 | */ 70 | fun dispatch(viewAction: VideoListViewAction) { 71 | viewModelScope.launch { 72 | userIntent.emit(viewAction) 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/res/color/tab_tint_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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/drawable/ic_like.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_location.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_spaceship.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_travel.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_video.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_location_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 20 | 21 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_travel_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_common_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_travel.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 28 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_video.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 18 | 19 | 27 | 28 | 29 | 33 | 34 | 38 | 39 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_travel.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 17 | 18 | 21 | 22 | 29 | 30 | 51 | 52 | 64 | 65 | 76 | 77 | 90 | 91 | 92 | 102 | 103 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_video.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 18 | 19 | 25 | 26 | 36 | 37 | 49 | 50 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /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/fmtjava/MVI_Learn/818338a7a4f9f9832a2804ccb4ab3bcffb795cca/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmtjava/MVI_Learn/818338a7a4f9f9832a2804ccb4ab3bcffb795cca/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmtjava/MVI_Learn/818338a7a4f9f9832a2804ccb4ab3bcffb795cca/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmtjava/MVI_Learn/818338a7a4f9f9832a2804ccb4ab3bcffb795cca/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmtjava/MVI_Learn/818338a7a4f9f9832a2804ccb4ab3bcffb795cca/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmtjava/MVI_Learn/818338a7a4f9f9832a2804ccb4ab3bcffb795cca/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmtjava/MVI_Learn/818338a7a4f9f9832a2804ccb4ab3bcffb795cca/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmtjava/MVI_Learn/818338a7a4f9f9832a2804ccb4ab3bcffb795cca/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmtjava/MVI_Learn/818338a7a4f9f9832a2804ccb4ab3bcffb795cca/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmtjava/MVI_Learn/818338a7a4f9f9832a2804ccb4ab3bcffb795cca/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 | #8A000000 10 | #FFFFFFFF 11 | #CBFFFFFF 12 | #333333 13 | #B2B2B2 14 | #FFA100 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MVI_Learn 3 | 旅行 4 | 视频 5 | 推荐视频 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/test/java/com/fmt/mvi/learn/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.fmt.mvi.learn 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 | plugins { 3 | id 'com.android.application' version '7.2.0' apply false 4 | id 'com.android.library' version '7.2.0' apply false 5 | id 'org.jetbrains.kotlin.android' version '1.6.10' apply false 6 | id 'com.google.dagger.hilt.android' version '2.44' apply false 7 | } 8 | 9 | task clean(type: Delete) { 10 | delete rootProject.buildDir 11 | } -------------------------------------------------------------------------------- /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 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.enableJetifier=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmtjava/MVI_Learn/818338a7a4f9f9832a2804ccb4ab3bcffb795cca/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jul 09 19:56:41 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-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: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmtjava/MVI_Learn/818338a7a4f9f9832a2804ccb4ab3bcffb795cca/mvi -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | maven { url 'https://jitpack.io' } 7 | } 8 | } 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | mavenCentral() 14 | maven { url 'https://jitpack.io' } 15 | } 16 | } 17 | rootProject.name = "MVI_Learn" 18 | include ':app' 19 | --------------------------------------------------------------------------------