()
--------------------------------------------------------------------------------
/module_home/src/main/java/com/quyunshuo/module/home/fragment/InternalRepository.kt:
--------------------------------------------------------------------------------
1 | //package com.quyunshuo.module.home.fragment
2 | //
3 | //import com.quyunshuo.androidbaseframemvvm.base.mvvm.m.BaseRepository
4 | //import kotlinx.coroutines.delay
5 | //import javax.inject.Inject
6 | //
7 | ///**
8 | // * @author DBoy 2021/7/6
9 | // * - 文件描述 :
10 | // */
11 | //class InternalRepository @Inject constructor() : BaseRepository() {
12 | //
13 | // suspend fun getData() = request {
14 | // delay(1000)
15 | // emit("数据加载成功")
16 | // }
17 | //}
--------------------------------------------------------------------------------
/module_home/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/ktx/EditTextKtx.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.ktx
2 |
3 | import android.text.InputFilter
4 | import android.widget.EditText
5 |
6 | /**
7 | * EditText相关扩展方法
8 | *
9 | * @author Qu Yunshuo
10 | * @since 2020/9/17
11 | */
12 |
13 | /**
14 | * 过滤掉空格和回车
15 | */
16 | fun EditText.filterBlankAndCarriageReturn() {
17 | val filterList = mutableListOf()
18 | filterList.addAll(filters)
19 | filterList.add(InputFilter { source, _, _, _, _, _ -> if (source == " " || source == "\n") "" else null })
20 | filters = filterList.toTypedArray()
21 | }
--------------------------------------------------------------------------------
/module_home/build.gradle:
--------------------------------------------------------------------------------
1 | import com.quyunshuo.androidbaseframemvvm.buildsrc.ProjectBuildConfig
2 |
3 | //****************************************
4 | //******** module_home 的配置文件 *********
5 | //****************************************
6 |
7 | plugins {
8 | alias(libs.plugins.kotlin)
9 | alias(libs.plugins.hilt)
10 | id "kotlin-kapt"
11 | }
12 | if (ProjectBuildConfig.isAppMode) {
13 | apply plugin: 'com.android.application'
14 | } else {
15 | apply plugin: 'com.android.library'
16 | }
17 |
18 | apply from: '../base_module.gradle'
19 |
20 | android {
21 | resourcePrefix "home_"
22 | namespace 'com.quyunshuo.module.home'
23 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/network/NetworkStateChangeListener.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils.network
2 |
3 | /**
4 | * 网络状态改变监听起
5 | *
6 | * @author Qu Yunshuo
7 | * @since 2021/7/11 4:56 下午
8 | */
9 | interface NetworkStateChangeListener {
10 |
11 | /**
12 | * 网络类型更改回调
13 | * @param type NetworkTypeEnum 网络类型
14 | * @return Unit
15 | */
16 | fun networkTypeChange(type: NetworkTypeEnum)
17 |
18 | /**
19 | * 网络连接状态更改回调
20 | * @param isConnected Boolean 是否已连接
21 | * @return Unit
22 | */
23 | fun networkConnectChange(isConnected: Boolean)
24 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/ThreadUtils.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils
2 |
3 | import android.os.Build
4 | import android.os.Looper
5 |
6 | /**
7 | * 线程相关工具类
8 | *
9 | * @author Qu Yunshuo
10 | * @since 2023/3/12 19:29
11 | */
12 | object ThreadUtils {
13 |
14 | /**
15 | * 判断当前是否是主线程
16 | * 在 [Build.VERSION.SDK_INT] >= [Build.VERSION_CODES.M] 有一个简化方法来判断当前线程是否是主线程
17 | * ```
18 | * Looper.getMainLooper().isCurrentThread()
19 | * ```
20 | *
21 | * @return Boolean
22 | */
23 | fun isMainThread(): Boolean = Looper.getMainLooper().thread == Thread.currentThread()
24 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/status/ViewStatusHelper.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils.status
2 |
3 | import android.os.Bundle
4 |
5 | /**
6 | * @author DBoy 2021/8/5
7 | * - 文件描述 : 采用了一种链式调用,所有对象持有自己父级帮助类,进行场景回复时先恢复链头的数据
8 | */
9 | abstract class ViewStatusHelper(val parentViewStatusHelper: ViewStatusHelper?) {
10 |
11 | open fun onRestoreInstanceStatus(savedInstanceState: Bundle?) {
12 | parentViewStatusHelper?.onRestoreInstanceStatus(savedInstanceState)
13 | }
14 |
15 | open fun onSaveInstanceState(bundle: Bundle) {
16 | parentViewStatusHelper?.onSaveInstanceState(bundle)
17 | }
18 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/ktx/PopupWindowKtx.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.ktx
2 |
3 | import android.view.View
4 | import android.view.ViewGroup
5 | import android.widget.PopupWindow
6 |
7 | /**
8 | * PopupWindow相关扩展
9 | *
10 | * @author Qu Yunshuo
11 | * @since 1/4/21 10:48 AM
12 | */
13 |
14 | /**
15 | * 测量view宽高
16 | */
17 | fun PopupWindow.makeDropDownMeasureSpec(measureSpec: Int): Int {
18 | val mode =
19 | if (measureSpec == ViewGroup.LayoutParams.WRAP_CONTENT) View.MeasureSpec.UNSPECIFIED else View.MeasureSpec.EXACTLY
20 | return View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.getSize(measureSpec), mode)
21 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/mvvm/v/FrameView.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.mvvm.v
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.viewbinding.ViewBinding
5 |
6 | /**
7 | * View层基类抽象
8 | *
9 | * @author Qu Yunshuo
10 | * @since 10/13/20
11 | */
12 | interface FrameView {
13 |
14 | /**
15 | * 创建 [ViewBinding] 实例
16 | */
17 | fun createVB(): VB
18 |
19 | /**
20 | * 初始化 View
21 | */
22 | fun VB.initView()
23 |
24 | /**
25 | * 订阅 [LiveData]
26 | */
27 | fun initObserve()
28 |
29 | /**
30 | * 用于在页面创建时进行请求接口
31 | */
32 | fun initRequestData()
33 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | maven { url = java.net.URI.create("https://maven.google.com") }
7 | }
8 | }
9 |
10 | dependencyResolutionManagement {
11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
12 | repositories {
13 | google()
14 | mavenCentral()
15 | gradlePluginPortal()
16 | maven { url = java.net.URI.create("https://maven.google.com") }
17 | }
18 | }
19 |
20 | rootProject.name = "AndroidBaseFrameMVVM"
21 |
22 | include(
23 | ":app",
24 | ":lib_base",
25 | ":lib_common",
26 | ":module_home"
27 | )
--------------------------------------------------------------------------------
/module_home/src/main/java/com/quyunshuo/module/home/activity/HomeRepository.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.module.home.activity
2 |
3 | import com.quyunshuo.androidbaseframemvvm.base.mvvm.m.BaseRepository
4 | import com.quyunshuo.module.home.net.HomeApiService
5 | import kotlinx.coroutines.delay
6 | import javax.inject.Inject
7 |
8 | /**
9 | * 首页M层
10 | *
11 | * @author Qu Yunshuo
12 | * @since 5/25/21 5:42 PM
13 | */
14 | class HomeRepository @Inject constructor() : BaseRepository() {
15 |
16 | @Inject
17 | lateinit var mApi: HomeApiService
18 |
19 | /**
20 | * 模拟获取数据
21 | */
22 | suspend fun getData() = request {
23 | delay(1000L)
24 | emit("Hello Hilt")
25 | }
26 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # AGP 8.x 警告生成
2 | # Please add these rules to your existing keep rules in order to suppress warnings.
3 | # This is generated automatically by the Android Gradle plugin.
4 | -dontwarn dalvik.system.VMStack
5 | -dontwarn javax.lang.model.element.Element
6 | -dontwarn org.bouncycastle.jsse.BCSSLParameters
7 | -dontwarn org.bouncycastle.jsse.BCSSLSocket
8 | -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
9 | -dontwarn org.conscrypt.Conscrypt$Version
10 | -dontwarn org.conscrypt.Conscrypt
11 | -dontwarn org.conscrypt.ConscryptHostnameVerifier
12 | -dontwarn org.openjsse.javax.net.ssl.SSLParameters
13 | -dontwarn org.openjsse.javax.net.ssl.SSLSocket
14 | -dontwarn org.openjsse.net.ssl.OpenJSSE
--------------------------------------------------------------------------------
/app/src/main/java/com/quyunshuo/androidbaseframemvvm/app/AppApplication.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.app
2 |
3 | import com.quyunshuo.androidbaseframemvvm.base.BaseApplication
4 | import dagger.hilt.android.HiltAndroidApp
5 | import org.greenrobot.eventbus.EventBus
6 |
7 | /**
8 | * App壳
9 | *
10 | * @author Qu Yunshuo
11 | * @since 4/23/21 6:08 PM
12 | */
13 | @HiltAndroidApp
14 | class AppApplication : BaseApplication() {
15 |
16 | override fun onCreate() {
17 | // 开启EventBusAPT,优化反射效率 当组件作为App运行时需要将添加的Index注释掉 因为找不到对应的类了
18 | EventBus
19 | .builder()
20 | // .addIndex(MainEventIndex())
21 | .installDefaultEventBus()
22 | super.onCreate()
23 | }
24 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/ktx/NetUtils.kt:
--------------------------------------------------------------------------------
1 | package com.ad.newad
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.NetworkCapabilities
6 |
7 | /**
8 | * 网络相关工具类
9 | */
10 | object NetUtils {
11 |
12 | /**
13 | * 当前网络是否是 Wi-Fi
14 | */
15 | fun currentNetIsWiFi(context: Context): Boolean {
16 | val connectivityManager =
17 | context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
18 | val networkCapabilities =
19 | connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
20 | return networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ?: false
21 | }
22 | }
--------------------------------------------------------------------------------
/lib_common/src/main/java/com/quyunshuo/androidbaseframemvvm/common/helper/ResponseException.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.common.helper
2 |
3 | /**
4 | * 自定义响应异常的抽象类型
5 | *
6 | * @author Qu Yunshuo
7 | * @since 2021/8/27 9:50 上午
8 | */
9 | interface IResponseException
10 |
11 | /**
12 | * 请求响应异常,主要为各种code码专门定义的异常
13 | *
14 | * @param type IResponseCode 异常类型枚举,用于标记该异常的类型
15 | * @param msg String 异常信息
16 | *
17 | * @author Qu Yunshuo
18 | * @since 2021/7/9 2:57 下午
19 | */
20 | class ResponseException(val type: IResponseCode, val msg: String) : Exception(), IResponseException
21 |
22 | /**
23 | * 空异常,表示该异常已经被处理过了,不需要再做额外处理了
24 | *
25 | * @author Qu Yunshuo
26 | * @since 2021/7/9 3:11 下午
27 | */
28 | class ResponseEmptyException : Exception(), IResponseException
--------------------------------------------------------------------------------
/lib_base/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
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/ktx/ActivityKtx.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.ktx
2 |
3 | import android.app.Activity
4 | import android.view.WindowManager
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.lifecycle.Lifecycle
7 |
8 | /**
9 | * 设置当前 [Activity] 是否允许截屏操作
10 | *
11 | * @receiver [Activity]
12 | * @param isAllow Boolean 是否允许截屏
13 | */
14 | fun Activity.isAllowScreenCapture(isAllow: Boolean) {
15 | if (isAllow) {
16 | window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
17 | } else {
18 | window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
19 | }
20 | }
21 |
22 | /**
23 | * 判断当前 Activity 是否是 [Lifecycle.State.RESUMED]
24 | */
25 | fun AppCompatActivity.isResumed(): Boolean = lifecycle.currentState == Lifecycle.State.RESUMED
26 |
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/CoilGIFImageLoader.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils
2 |
3 | import android.os.Build.VERSION.SDK_INT
4 | import coil.ImageLoader
5 | import coil.decode.GifDecoder
6 | import coil.decode.ImageDecoderDecoder
7 | import com.quyunshuo.androidbaseframemvvm.base.BaseApplication
8 |
9 | /**
10 | * 用于加载 Gif 的 Coil ImageLoader
11 | *
12 | * @author Qu Yunshuo
13 | * @since 2021/9/6 4:26 下午
14 | */
15 | object CoilGIFImageLoader {
16 |
17 | val imageLoader = ImageLoader.Builder(BaseApplication.context)
18 | .componentRegistry {
19 | if (SDK_INT >= 28) {
20 | add(ImageDecoderDecoder(BaseApplication.context))
21 | } else {
22 | add(GifDecoder())
23 | }
24 | }
25 | .build()
26 | }
--------------------------------------------------------------------------------
/lib_common/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
--------------------------------------------------------------------------------
/module_home/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
--------------------------------------------------------------------------------
/module_home/src/androidTest/java/com/quyunshuo/module/home/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.module.home
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.quyunshuo.module.home.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/mvvm/m/BaseRepository.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.mvvm.m
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.flow.Flow
5 | import kotlinx.coroutines.flow.FlowCollector
6 | import kotlinx.coroutines.flow.flow
7 | import kotlinx.coroutines.flow.flowOn
8 |
9 | /**
10 | * 仓库层 Repository 基类
11 | *
12 | * @author Qu Yunshuo
13 | * @since 8/27/20
14 | */
15 | open class BaseRepository {
16 |
17 | /**
18 | * 发起请求封装
19 | * 该方法将flow的执行切换至IO线程
20 | *
21 | * @param requestBlock 请求的整体逻辑
22 | * @return Flow @BuilderInference block: suspend FlowCollector.() -> Unit
23 | */
24 | protected fun request(requestBlock: suspend FlowCollector.() -> Unit): Flow {
25 | return flow(block = requestBlock).flowOn(Dispatchers.IO)
26 | }
27 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/ClipboardUtils.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils
2 |
3 | import android.content.ClipData
4 | import android.content.ClipboardManager
5 | import android.content.Context
6 | import com.quyunshuo.androidbaseframemvvm.base.BaseApplication
7 |
8 | /**
9 | * 剪切板工具类
10 | *
11 | * @author Qu Yunshuo
12 | * @since 2023/5/31 10:27
13 | */
14 | object ClipboardUtils {
15 |
16 | /**
17 | * 复制内容到剪切板
18 | *
19 | * @param text String 内容
20 | * @param label String 标签,用于区分内容
21 | */
22 | fun copyToClipboard(text: String, label: String = "") {
23 | val clipboard =
24 | BaseApplication.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
25 | val clip = ClipData.newPlainText(label, text)
26 | clipboard.setPrimaryClip(clip)
27 | }
28 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/ktx/LifecycleOwnerKtx.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.ktx
2 |
3 | import androidx.lifecycle.LifecycleOwner
4 | import androidx.lifecycle.LiveData
5 |
6 | /**
7 | * 对LiveData订阅的简化封装
8 | *
9 | * 使用示例
10 | * ```
11 | * override fun initObserve() {
12 | * observeLiveData(mViewModel.stateViewLD, ::processStateViewLivaData)
13 | * }
14 | *
15 | * private fun processStateViewLivaData(data: StateLayoutEnum) {
16 | * ...
17 | * }
18 | * ```
19 | *
20 | * @receiver LifecycleOwner
21 | * @param liveData LiveData 需要进行订阅的LiveData
22 | * @param action action: (t: T) -> Unit 处理订阅内容的方法
23 | * @return Unit
24 | */
25 | inline fun LifecycleOwner.observeLiveData(
26 | liveData: LiveData,
27 | crossinline action: (t: T) -> Unit
28 | ) {
29 | liveData.observe(this) { it?.let { t -> action(t) } }
30 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/network/NetworkTypeEnum.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils.network
2 |
3 | /**
4 | * 网络类型的枚举
5 | *
6 | * @author Qu Yunshuo
7 | * @since 2021/8/22 10:35 下午
8 | */
9 | enum class NetworkTypeEnum {
10 |
11 | /**
12 | * 使用蜂窝移动网络传输
13 | */
14 | TRANSPORT_CELLULAR,
15 |
16 | /**
17 | * 使用Wi-Fi传输
18 | */
19 | TRANSPORT_WIFI,
20 |
21 | /**
22 | * 使用蓝牙传输
23 | */
24 | TRANSPORT_BLUETOOTH,
25 |
26 | /**
27 | * 使用以太网传输
28 | */
29 | TRANSPORT_ETHERNET,
30 |
31 | /**
32 | * 使用 VPN 传输
33 | */
34 | TRANSPORT_VPN,
35 |
36 | /**
37 | * 使用 Wi-Fi Aware 传输
38 | */
39 | TRANSPORT_WIFI_AWARE,
40 |
41 | /**
42 | * 使用 LoWPAN 传输
43 | */
44 | TRANSPORT_LOWPAN,
45 |
46 | /**
47 | * 其他
48 | */
49 | OTHER
50 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/view/OnSingleClickListener.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.view
2 |
3 | import android.view.View
4 |
5 | /**
6 | * 带有防抖效果的单击监听
7 | *
8 | * @param mDelayTime Int 防抖间隔时间,单位是毫秒,默认值 500ms
9 | * @param mListener (v: View) -> Unit 具体的点击事件
10 | *
11 | * @author Qu Yunshuo
12 | * @since 2023/3/15 23:39
13 | */
14 | class OnSingleClickListener(
15 | private val mDelayTime: Int = 500,
16 | private val mListener: (v: View) -> Unit
17 | ) : View.OnClickListener {
18 |
19 | /**
20 | * 上次有效点击的时间
21 | */
22 | private var mLastClickTime = 0L
23 | override fun onClick(v: View) {
24 | val currentTimeMillis = System.currentTimeMillis()
25 | if (currentTimeMillis - mLastClickTime >= mDelayTime) {
26 | mLastClickTime = currentTimeMillis
27 | mListener.invoke(v)
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/module_home/src/main/java/com/quyunshuo/module/home/di/DIHomeNetServiceModule.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.module.home.di
2 |
3 | import com.quyunshuo.module.home.net.HomeApiService
4 | import dagger.Module
5 | import dagger.Provides
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.components.SingletonComponent
8 | import retrofit2.Retrofit
9 | import javax.inject.Singleton
10 |
11 | /**
12 | * 全局作用域的Home组件网络接口代理依赖注入模块
13 | *
14 | * @author Qu Yunshuo
15 | * @since 6/4/21 5:51 PM
16 | */
17 | @Module
18 | @InstallIn(SingletonComponent::class)
19 | class DIHomeNetServiceModule {
20 |
21 | /**
22 | * Home模块的[HomeApiService]依赖提供方法
23 | *
24 | * @param retrofit Retrofit
25 | * @return HomeApiService
26 | */
27 | @Singleton
28 | @Provides
29 | fun provideHomeApiService(retrofit: Retrofit): HomeApiService {
30 | return retrofit.create(HomeApiService::class.java)
31 | }
32 | }
--------------------------------------------------------------------------------
/lib_common/src/main/java/com/quyunshuo/androidbaseframemvvm/common/helper/ResponseCodeEnum.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.common.helper
2 |
3 | /**
4 | * 请求响应code枚举抽象
5 | *
6 | * @author Qu Yunshuo
7 | * @since 2021/7/9 2:56 下午
8 | */
9 | interface IResponseCode {
10 |
11 | /**
12 | * 获取该枚举的code码
13 | * @return Int
14 | */
15 | fun getCode(): Int
16 |
17 | /**
18 | * 获取该枚举的描述
19 | * @return String
20 | */
21 | fun getMessage(): String
22 | }
23 |
24 | /**
25 | * 请求响应code的枚举
26 | *
27 | * @author Qu Yunshuo
28 | * @since 2021/7/9 2:55 下午
29 | */
30 | enum class ResponseCodeEnum : IResponseCode {
31 |
32 | // 通用异常
33 | ERROR {
34 | override fun getCode() = 100
35 | override fun getMessage() = "处理失败"
36 | },
37 |
38 | // 成功
39 | SUCCESS {
40 | override fun getCode() = 200
41 | override fun getMessage() = "成功"
42 | }
43 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/app/ApplicationLifecycle.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.app
2 |
3 | import android.app.Application
4 | import android.content.Context
5 |
6 | /**
7 | * Application 生命周期 用于初始化各个组件
8 | *
9 | * @author Qu Yunshuo
10 | * @since 4/23/21 5:22 PM
11 | */
12 | interface ApplicationLifecycle {
13 |
14 | /**
15 | * 同[Application.attachBaseContext]
16 | * @param context Context
17 | */
18 | fun onAttachBaseContext(context: Context)
19 |
20 | /**
21 | * 同[Application.onCreate]
22 | * @param application Application
23 | */
24 | fun onCreate(application: Application)
25 |
26 | /**
27 | * 同[Application.onTerminate]
28 | * @param application Application
29 | */
30 | fun onTerminate(application: Application)
31 |
32 | /**
33 | * 主线程前台初始化
34 | * @return MutableList<() -> String> 初始化方法集合
35 | */
36 | fun initByFrontDesk(): MutableList<() -> String>
37 |
38 | /**
39 | * 不需要立即初始化的放在这里进行后台初始化
40 | */
41 | fun initByBackstage()
42 | }
--------------------------------------------------------------------------------
/module_home/src/main/res/layout/home_activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
22 |
23 |
--------------------------------------------------------------------------------
/module_home/src/main/java/com/quyunshuo/module/home/activity/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.module.home.activity
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.MutableLiveData
5 | import com.quyunshuo.androidbaseframemvvm.base.ktx.launchIO
6 | import com.quyunshuo.androidbaseframemvvm.base.mvvm.vm.BaseViewModel
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.flow.catch
9 | import kotlinx.coroutines.flow.collect
10 | import javax.inject.Inject
11 |
12 | /**
13 | * 首页的VM层
14 | *
15 | * @property mRepository HomeRepository 仓库层 通过Hilt注入
16 | *
17 | * @author Qu Yunshuo
18 | * @since 5/25/21 5:41 PM
19 | */
20 | @HiltViewModel
21 | class HomeViewModel @Inject constructor(private val mRepository: HomeRepository) : BaseViewModel() {
22 |
23 | val data = MutableLiveData()
24 |
25 | /**
26 | * 模拟获取数据
27 | */
28 | fun getData() {
29 | launchIO {
30 | mRepository.getData()
31 | .catch { Log.d("qqq", "getData: $it") }
32 | .collect { data.postValue(it) }
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/lib_common/src/main/java/com/quyunshuo/androidbaseframemvvm/common/helper/ExceptionHandler.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.common.helper
2 |
3 | import com.quyunshuo.androidbaseframemvvm.base.utils.toast
4 |
5 | /**
6 | * 响应code异常统一处理
7 | *
8 | * 该方法主要做两件事:
9 | *
10 | * - 1.做统一的code码处理
11 | * - 2.未进行统一处理的code码会被转换为自定义异常[ResponseException]抛出
12 | *
13 | * 使用方式为:进行统一处理的异常进行抛出[ResponseEmptyException],未进行处理的code抛出[ResponseException],成功状态下执行[successBlock]
14 | *
15 | * @param code Int code码
16 | * @param msg String? 错误信息
17 | * @param successBlock suspend () -> Unit 没有异常的情况下执行的方法体 可以在此处进行数据的发射
18 | * @throws ResponseException 未进行处理的异常会进行抛出,让ViewModel去做进一步处理
19 | */
20 | @Throws(ResponseException::class)
21 | suspend fun responseCodeExceptionHandler(
22 | code: Int,
23 | msg: String?,
24 | successBlock: suspend () -> Unit
25 | ) {
26 | // 进行异常的处理
27 | when (code) {
28 | ResponseCodeEnum.ERROR.getCode() -> {
29 | toast(ResponseCodeEnum.ERROR.getMessage())
30 | throw ResponseEmptyException()
31 | }
32 | ResponseCodeEnum.SUCCESS.getCode() -> successBlock.invoke()
33 | }
34 | }
--------------------------------------------------------------------------------
/lib_common/build.gradle:
--------------------------------------------------------------------------------
1 | //****************************************
2 | //********* lib_common 的配置文件 **********
3 | //****************************************
4 |
5 | plugins {
6 | alias(libs.plugins.library)
7 | alias(libs.plugins.kotlin)
8 | alias(libs.plugins.hilt)
9 | id "kotlin-kapt"
10 | }
11 |
12 | apply from: '../base_lib.gradle'
13 |
14 | import com.quyunshuo.androidbaseframemvvm.buildsrc.*
15 |
16 | android {
17 |
18 | defaultConfig {
19 | resValue "string", "BUGLY_APP_ID", SDKKeyConfig.BUGLY_APP_ID
20 | }
21 |
22 | buildFeatures {
23 | viewBinding = true
24 | }
25 |
26 | resourcePrefix "common_"
27 | namespace 'com.quyunshuo.androidbaseframemvvm.common'
28 | }
29 |
30 | dependencies {
31 | implementation fileTree(dir: "libs", include: ["*.jar"])
32 |
33 | api project(path: ':lib_base')
34 |
35 | api DependencyConfig.JetPack.HiltCore
36 |
37 | kapt DependencyConfig.GitHub.ARouteCompiler
38 | kapt DependencyConfig.GitHub.EventBusAPT
39 | kapt DependencyConfig.GitHub.AutoServiceAnnotations
40 | kapt DependencyConfig.JetPack.HiltApt
41 | kapt DependencyConfig.JetPack.LifecycleCompilerAPT
42 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/AppUtils.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils
2 |
3 | import android.content.pm.PackageInfo
4 | import android.os.Build
5 | import com.quyunshuo.androidbaseframemvvm.base.BaseApplication
6 |
7 | /**
8 | * App 相关工具类
9 | *
10 | * @author Qu Yunshuo
11 | * @sine 2023/2/13 23:15
12 | */
13 | class AppUtils {
14 |
15 | /**
16 | * 获取当前 App 版本号
17 | *
18 | * @return Long
19 | */
20 | fun getAppVersionCode(): Long {
21 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
22 | getAppPackageInfo().longVersionCode
23 | } else {
24 | getAppPackageInfo().versionCode.toLong()
25 | }
26 | }
27 |
28 | /**
29 | * 获取当前 App 版本名
30 | *
31 | * @return String
32 | */
33 | fun getAppVersionName(): String = getAppPackageInfo().versionName
34 |
35 | /**
36 | * 获取当前 App 的 [PackageInfo]
37 | *
38 | * @return PackageInfo
39 | */
40 | fun getAppPackageInfo(): PackageInfo {
41 | return BaseApplication.context
42 | .packageManager
43 | .getPackageInfo(BaseApplication.context.packageName, 0)
44 | }
45 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/network/AutoRegisterNetListener.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils.network
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleObserver
5 | import androidx.lifecycle.OnLifecycleEvent
6 |
7 | /**
8 | * 自动注册网络状态监听
9 | * 使用的是[androidx.lifecycle.LifecycleObserver]来同步生命周期
10 | *
11 | * @author Qu Yunshuo
12 | * @since 2021/7/11 4:56 下午
13 | */
14 | class AutoRegisterNetListener constructor(listener: NetworkStateChangeListener) :
15 | LifecycleObserver {
16 |
17 | /**
18 | * 当前需要自动注册的监听器
19 | */
20 | private var mListener: NetworkStateChangeListener? = null
21 |
22 | init {
23 | mListener = listener
24 | }
25 |
26 | @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
27 | fun register() {
28 | mListener?.run { NetworkStateClient.setListener(this) }
29 | }
30 |
31 | @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
32 | fun unregister() {
33 | NetworkStateClient.removeListener()
34 | }
35 |
36 | @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
37 | fun clean() {
38 | NetworkStateClient.removeListener()
39 | mListener = null
40 | }
41 | }
--------------------------------------------------------------------------------
/lib_common/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
19 |
20 |
21 |
25 |
26 |
27 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/ktx/ViewPager2Ktx.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.ktx
2 |
3 | import android.view.View
4 | import androidx.recyclerview.widget.RecyclerView
5 | import androidx.viewpager2.widget.ViewPager2
6 |
7 | /**
8 | * 设置ViewPager2的过度滚动模式为绝不允许用户过度滚动此视图
9 | * @receiver ViewPager2
10 | */
11 | fun ViewPager2.setOverScrollModeToNever() {
12 | val childView: View = this.getChildAt(0)
13 | if (childView is RecyclerView) {
14 | childView.overScrollMode = RecyclerView.OVER_SCROLL_NEVER
15 | }
16 | }
17 |
18 | /**
19 | * 设置ViewPager2的过度滚动模式为始终允许用户过度滚动此视图,前提是它是可以滚动的视图
20 | * @receiver ViewPager2
21 | */
22 | fun ViewPager2.setOverScrollModeToAlways() {
23 | val childView: View = this.getChildAt(0)
24 | if (childView is RecyclerView) {
25 | childView.overScrollMode = RecyclerView.OVER_SCROLL_ALWAYS
26 | }
27 | }
28 |
29 | /**
30 | * 设置ViewPager2的过度滚动模式为仅当内容大到足以有意义地滚动时,才允许用户过度滚动此视图,前提是它是可以滚动的视图。
31 | * @receiver ViewPager2
32 | */
33 | fun ViewPager2.setOverScrollModeToIfContentScrolls() {
34 | val childView: View = this.getChildAt(0)
35 | if (childView is RecyclerView) {
36 | childView.overScrollMode = RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS
37 | }
38 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the ART/Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | out/
15 |
16 | # Gradle files
17 | .gradle/
18 | build/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Proguard folder generated by Eclipse
24 | proguard/
25 |
26 | # Log Files
27 | *.log
28 |
29 | # Android Studio Navigation editor temp files
30 | .navigation/
31 |
32 | # Android Studio captures folder
33 | captures/
34 |
35 | # IntelliJ
36 | *.iml
37 | .idea/workspace.xml
38 | .idea/tasks.xml
39 | .idea/gradle.xml
40 | .idea/assetWizardSettings.xml
41 | .idea/dictionaries
42 | .idea/libraries
43 | .idea/caches
44 | .idea/*
45 |
46 | # Keystore files
47 | # Uncomment the following line if you do not want to check your keystore files in.
48 | #*.jks
49 |
50 | # External native build folder generated in Android Studio 2.2 and later
51 | .externalNativeBuild
52 |
53 | # Google Services (e.g. APIs or Firebase)
54 | google-services.json
55 |
56 | # Freeline
57 | freeline.py
58 | freeline/
59 | freeline_project_description.json
60 |
61 | # fastlane
62 | fastlane/report.xml
63 | fastlane/Preview.html
64 | fastlane/screenshots
65 | fastlane/test_output
66 | fastlane/readme.md
67 |
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/status/imp/BaseFrameViewStatusHelperImp.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils.status.imp
2 |
3 | import android.os.Bundle
4 | import com.quyunshuo.androidbaseframemvvm.base.utils.status.ViewStatusHelper
5 |
6 | /**
7 | * @author DBoy 2021/7/8
8 | *
9 | * - 文件描述 : 视图,activity,fragment重建帮助类
10 | */
11 | open class BaseFrameViewStatusHelperImp(parentViewStatusHelper: ViewStatusHelper? = null) : ViewStatusHelper(parentViewStatusHelper) {
12 | /**
13 | * 重建标记key 以包名保存数据可以防止嵌套层级出现重复Key
14 | */
15 | private val KEY_RECREATE = "com.quyunshuo.androidbaseframemvvm.base.utils.status.BaseFrameViewStatusHelperImp.Recreate"
16 |
17 | /**
18 | * 是否重建
19 | */
20 | var isRecreate = false
21 | private set
22 |
23 |
24 | /**
25 | * 恢复状态
26 | */
27 | override fun onRestoreInstanceStatus(savedInstanceState: Bundle?) {
28 | super.onRestoreInstanceStatus(savedInstanceState)
29 | isRecreate = savedInstanceState?.getBoolean(KEY_RECREATE) ?: false
30 | }
31 |
32 | /**
33 | * 保存状态
34 | */
35 | override fun onSaveInstanceState(bundle: Bundle) {
36 | super.onSaveInstanceState(bundle)
37 | bundle.putBoolean(KEY_RECREATE, true)
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/module_home/src/main/java/com/quyunshuo/module/home/activity/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.module.home.activity
2 |
3 | import android.graphics.Color
4 | import androidx.activity.viewModels
5 | import com.quyunshuo.androidbaseframemvvm.base.ktx.observeLiveData
6 | import com.quyunshuo.androidbaseframemvvm.common.ui.BaseActivity
7 | import com.quyunshuo.module.home.databinding.HomeActivityMainBinding
8 | import dagger.hilt.android.AndroidEntryPoint
9 |
10 | /**
11 | * 首页
12 | *
13 | * @author Qu Yunshuo
14 | * @since 5/22/21 2:26 PM
15 | */
16 | @AndroidEntryPoint
17 | class MainActivity : BaseActivity() {
18 |
19 | /**
20 | * 通过 viewModels() + Hilt 获取 ViewModel 实例
21 | */
22 | override val mViewModel by viewModels()
23 |
24 | override fun createVB() = HomeActivityMainBinding.inflate(layoutInflater)
25 |
26 | override fun HomeActivityMainBinding.initView() {}
27 |
28 | override fun initObserve() {
29 | observeLiveData(mViewModel.data, ::processData)
30 | }
31 |
32 | private fun processData(data: String) {
33 | mBinding.vTvHello.text = data
34 | mBinding.vTvHello.setTextColor(Color.BLUE)
35 | }
36 |
37 | override fun initRequestData() {
38 | // 模拟获取数据
39 | mViewModel.getData()
40 | }
41 | }
--------------------------------------------------------------------------------
/lib_common/src/main/java/com/quyunshuo/androidbaseframemvvm/common/ui/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.common.ui
2 |
3 | import android.util.Log
4 | import androidx.viewbinding.ViewBinding
5 | import com.quyunshuo.androidbaseframemvvm.base.mvvm.v.BaseFrameActivity
6 | import com.quyunshuo.androidbaseframemvvm.base.mvvm.vm.BaseViewModel
7 | import com.quyunshuo.androidbaseframemvvm.base.utils.ActivityStackManager
8 | import com.quyunshuo.androidbaseframemvvm.base.utils.AndroidBugFixUtils
9 | import com.quyunshuo.androidbaseframemvvm.base.utils.BarUtils
10 |
11 | /**
12 | * Activity基类
13 | *
14 | * @author Qu Yunshuo
15 | * @since 8/27/20
16 | */
17 | abstract class BaseActivity : BaseFrameActivity() {
18 |
19 | /**
20 | * 设置状态栏
21 | * 子类需要自定义时重写该方法即可
22 | * @return Unit
23 | */
24 | override fun setStatusBar() {
25 | BarUtils.transparentStatusBar(this)
26 | BarUtils.setStatusBarLightMode(this, true)
27 | }
28 |
29 | override fun onResume() {
30 | super.onResume()
31 | Log.d("ActivityLifecycle", "ActivityStack: ${ActivityStackManager.activityStack}")
32 | }
33 |
34 | override fun onDestroy() {
35 | super.onDestroy()
36 | // 解决某些特定机型会触发的Android本身的Bug
37 | AndroidBugFixUtils().fixSoftInputLeaks(this)
38 | }
39 | }
--------------------------------------------------------------------------------
/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
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 | android.defaults.buildfeatures.buildconfig=true
23 | android.nonTransitiveRClass=false
24 | android.nonFinalResIds=false
25 | android.enableR8.fullMode=false
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
23 |
24 |
25 |
28 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils
2 |
3 | import android.util.Log
4 | import com.alibaba.android.arouter.launcher.ARouter
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.delay
7 | import kotlinx.coroutines.flow.catch
8 | import kotlinx.coroutines.flow.flow
9 | import kotlinx.coroutines.flow.flowOn
10 |
11 | /**
12 | * 使用 Flow 做的简单的轮询
13 | * 请使用单独的协程来进行管理该 Flow
14 | * Flow 仍有一些操作符是实验性的 使用时需添加 @InternalCoroutinesApi 注解
15 | * @param intervals 轮询间隔时间/毫秒
16 | * @param block 需要执行的代码块
17 | */
18 | suspend fun startPolling(intervals: Long, block: () -> Unit) {
19 | flow {
20 | while (true) {
21 | delay(intervals)
22 | emit(0)
23 | }
24 | }
25 | .catch { Log.e("flow", "startPolling: $it") }
26 | .flowOn(Dispatchers.Main)
27 | .collect { block.invoke() }
28 | }
29 | /**************************************************************************************************/
30 |
31 | /**
32 | * 发送普通EventBus事件
33 | */
34 | fun sendEvent(event: Any) = EventBusUtils.postEvent(event)
35 |
36 | /**************************************************************************************************/
37 | /**
38 | * 阿里路由不带参数跳转
39 | * @param routerUrl String 路由地址
40 | */
41 | fun aRouterJump(routerUrl: String) {
42 | ARouter.getInstance().build(routerUrl).navigation()
43 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/ktx/VideoViewKtx.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.ktx
2 |
3 | import android.media.MediaPlayer
4 | import android.view.ViewGroup
5 | import android.widget.VideoView
6 |
7 | /**
8 | * 根据视频的尺寸与容器尺寸比例,动态调整 [VideoView] 的尺寸以适应视频的尺寸
9 | * 解决 [VideoView] 尺寸比例与视频尺寸比例不一致导致视频拉伸的问题
10 | *
11 | * 容器可以是屏幕或者 [VideoView]
12 | *
13 | * 使用方式:
14 | * 1. 通过 [VideoView.setOnPreparedListener] 向 [VideoView] 设置 [MediaPlayer.OnPreparedListener] 监听
15 | * 2. 通过 [MediaPlayer.OnPreparedListener] 回调获取到视频的真实宽高,调用该方法传入参数进行适配
16 | * 3. 如果需要考虑横竖屏切换,请在横竖屏改变监听回调中再次调用该方法进行适配
17 | *
18 | * @receiver [VideoView]
19 | * @param containerW Float 容器的真实宽
20 | * @param containerH Float 容器的真实高
21 | * @param videoW Float 视频的真实宽
22 | * @param videoH Float 视频的真实高
23 | */
24 | fun VideoView.resetVideoViewDimensions(
25 | containerW: Float,
26 | containerH: Float,
27 | videoW: Float,
28 | videoH: Float,
29 | ) {
30 | // 计算宽高比进行调整宽高
31 | this.layoutParams = if (videoW / containerW < videoH / containerH) {
32 | this.layoutParams.apply {
33 | width = ViewGroup.LayoutParams.WRAP_CONTENT
34 | height = ViewGroup.LayoutParams.MATCH_PARENT
35 | }
36 | } else {
37 | this.layoutParams.apply {
38 | width = ViewGroup.LayoutParams.MATCH_PARENT
39 | height = ViewGroup.LayoutParams.WRAP_CONTENT
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/ToastUtils.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils
2 |
3 | import android.os.Build
4 | import android.os.Handler
5 | import android.os.Looper
6 | import android.widget.Toast
7 | import androidx.annotation.StringRes
8 | import com.quyunshuo.androidbaseframemvvm.base.BaseApplication
9 |
10 | private val mToastHandler by lazy { Handler(Looper.getMainLooper()) }
11 |
12 | private var mToast: Toast? = null
13 |
14 | @JvmOverloads
15 | fun toast(text: String, duration: Int = Toast.LENGTH_SHORT) {
16 | postToast(text, duration)
17 | }
18 |
19 | @JvmOverloads
20 | fun toast(@StringRes id: Int, duration: Int = Toast.LENGTH_SHORT) {
21 | postToast(getString(id), duration)
22 | }
23 |
24 | private fun postToast(text: String, duration: Int) {
25 | mToastHandler.post {
26 | setToast(text, duration)
27 | mToast?.show()
28 | }
29 | }
30 |
31 | private fun setToast(text: String, duration: Int) {
32 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
33 | if (mToast == null) {
34 | mToast = Toast.makeText(BaseApplication.context, text, duration)
35 | } else {
36 | mToast?.duration = duration
37 | mToast?.setText(text)
38 | }
39 | } else {
40 | if (mToast != null) {
41 | mToast?.cancel()
42 | mToast = null
43 | }
44 | mToast = Toast.makeText(BaseApplication.context, text, duration)
45 | }
46 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/EventBusUtils.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils
2 |
3 | import org.greenrobot.eventbus.EventBus
4 |
5 | /**
6 | * @Author: QuYunShuo
7 | * @Time: 2020/8/29
8 | * @Class: EventBusUtil
9 | * @Remark: EventBus工具类
10 | */
11 | object EventBusUtils {
12 |
13 | /**
14 | * 订阅
15 | * @param subscriber 订阅者
16 | */
17 | fun register(subscriber: Any) = EventBus.getDefault().register(subscriber)
18 |
19 | /**
20 | * 解除注册
21 | * @param subscriber 订阅者
22 | */
23 | fun unRegister(subscriber: Any) = EventBus.getDefault().unregister(subscriber)
24 |
25 | /**
26 | * 发送普通事件
27 | * @param event 事件
28 | */
29 | fun postEvent(event: Any) = EventBus.getDefault().post(event)
30 |
31 | /**
32 | * 发送粘性事件
33 | * @param stickyEvent 粘性事件
34 | */
35 | fun postStickyEvent(stickyEvent: Any) = EventBus.getDefault().postSticky(stickyEvent)
36 |
37 | /**
38 | * 手动获取粘性事件
39 | * @param stickyEventType 粘性事件
40 | * @param 事件泛型
41 | * @return 返回给定事件类型的最近粘性事件
42 | */
43 | fun getStickyEvent(stickyEventType: Class): T =
44 | EventBus.getDefault().getStickyEvent(stickyEventType)
45 |
46 | /**
47 | * 手动删除粘性事件
48 | * @param stickyEventType 粘性事件
49 | * @param 事件泛型
50 | * @return 返回给定事件类型的最近粘性事件
51 | */
52 | fun removeStickyEvent(stickyEventType: Class): T =
53 | EventBus.getDefault().removeStickyEvent(stickyEventType)
54 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/RegisterEventBus.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils
2 |
3 | /**
4 | * 辅助注册 EventBus 的注解
5 | *
6 | * - **使用方式:**
7 | * 在基类中的 `onCreate()`、`onDestroy()` 生命周期回调中去判断当前 Class 对象是否使用了该注解,
8 | * 然后根据结果去注册或反注册
9 | *
10 | * - **为什么不统一注册:**
11 | * 统一注册会在 EventBus 内部集合中留存,每次发送事件时,会遍历集合,过多无用的注册会导致速度变慢,
12 | * 所以最好的方式就是根据需要进行注册,避免无意义的全部注册
13 | *
14 | * - **sample:**
15 | * ```
16 | * abstract class BaseActivity : AppCompatActivity() {
17 | *
18 | * // 是否有 [RegisterEventBus] 注解 , 避免重复调用 [Class.isAnnotation]
19 | * private var mHaveRegisterEventBus = false
20 | * override fun onCreate(savedInstanceState: Bundle?) {
21 | * super.onCreate(savedInstanceState)
22 | * // 根据子类是否有 RegisterEventBus 注解決定是否进行注册 EventBus
23 | * if (javaClass.isAnnotationPresent(RegisterEventBus::class.java)) {
24 | * mHaveRegisterEventBus = true
25 | * EventBusUtils.register(this)
26 | * }
27 | * }
28 | *
29 | * override fun onDestroy() {
30 | * // 根据子类是否有 RegisterEventBus 注解决定是否进行注册 EventBus
31 | * if (mHaveRegisterEventBus) {
32 | * EventBusUtils.unRegister(this)
33 | * }
34 | * super.onDestroy()
35 | * }
36 | * }
37 | *
38 | * // 子类:
39 | * @RegisterEventBus
40 | * class SampleActivity : BaseActivity()
41 | * ```
42 | *
43 | * @author Qu Yunshuo
44 | * @since 2020/8/29
45 | */
46 | @Target(AnnotationTarget.CLASS)
47 | @Retention(AnnotationRetention.RUNTIME)
48 | annotation class RegisterEventBus
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/AndroidBugFixUtils.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.view.View
6 | import android.view.inputmethod.InputMethodManager
7 | import com.quyunshuo.androidbaseframemvvm.base.BaseApplication
8 | import java.lang.reflect.Field
9 |
10 | /**
11 | * 解决 Android 自身的 Bug
12 | *
13 | * @author Qu Yunshuo
14 | * @since 2020/10/22
15 | */
16 | class AndroidBugFixUtils {
17 |
18 | /**
19 | * 解决 InputMethodManager 造成的内存泄露
20 | *
21 | * 使用方式:
22 | * ```
23 | * override fun onDestroy() {
24 | * AndroidBugFixUtils().fixSoftInputLeaks(this)
25 | * super.onDestroy()
26 | * }
27 | * ```
28 | */
29 | fun fixSoftInputLeaks(activity: Activity) {
30 | val imm =
31 | BaseApplication.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
32 | val leakViews = arrayOf("mLastSrvView", "mCurRootView", "mServedView", "mNextServedView")
33 | for (leakView in leakViews) {
34 | try {
35 | val leakViewField: Field =
36 | InputMethodManager::class.java.getDeclaredField(leakView) ?: continue
37 | if (!leakViewField.isAccessible) leakViewField.isAccessible = true
38 | val view: Any? = leakViewField.get(imm)
39 | if (view !is View) continue
40 | if (view.rootView == activity.window.decorView.rootView) {
41 | leakViewField.set(imm, null)
42 | }
43 | } catch (t: Throwable) {
44 | }
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/app/LoadModuleProxy.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.app
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.util.Log
6 | import java.util.*
7 |
8 | /**
9 | * 加载组件代理类
10 | * 组件初始化的工作将由该代理类代理实现
11 | *
12 | * @author Qu Yunshuo
13 | * @since 4/23/21 5:37 PM
14 | */
15 | class LoadModuleProxy : ApplicationLifecycle {
16 |
17 | private var mLoader: ServiceLoader =
18 | ServiceLoader.load(ApplicationLifecycle::class.java)
19 |
20 | /**
21 | * 同[Application.attachBaseContext]
22 | * @param context Context
23 | */
24 | override fun onAttachBaseContext(context: Context) {
25 | mLoader.forEach {
26 | Log.d("ApplicationInit", it.toString())
27 | it.onAttachBaseContext(context)
28 | }
29 | }
30 |
31 | /**
32 | * 同[Application.onCreate]
33 | * @param application Application
34 | */
35 | override fun onCreate(application: Application) {
36 | mLoader.forEach { it.onCreate(application) }
37 | }
38 |
39 | /**
40 | * 同[Application.onTerminate]
41 | * @param application Application
42 | */
43 | override fun onTerminate(application: Application) {
44 | mLoader.forEach { it.onTerminate(application) }
45 | }
46 |
47 | /**
48 | * 主线程前台初始化
49 | * @return MutableList<() -> String> 初始化方法集合
50 | */
51 | override fun initByFrontDesk(): MutableList<() -> String> {
52 | val list: MutableList<() -> String> = mutableListOf()
53 | mLoader.forEach { list.addAll(it.initByFrontDesk()) }
54 | return list
55 | }
56 |
57 | /**
58 | * 不需要立即初始化的放在这里进行后台初始化
59 | */
60 | override fun initByBackstage() {
61 | mLoader.forEach { it.initByBackstage() }
62 | }
63 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/ktx/SizeUnitKtx.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.ktx
2 |
3 | import android.content.Context
4 | import androidx.fragment.app.Fragment
5 |
6 | /**
7 | * @Author: QuYunShuo
8 | * @Time: 2020/9/17
9 | * @Class: SizeUnitKtx
10 | * @Remark: 尺寸单位换算相关扩展属性
11 | */
12 |
13 | /**
14 | * dp 转 px
15 | */
16 | fun Context.dp2px(dpValue: Float): Int {
17 | val scale = resources.displayMetrics.density
18 | return (dpValue * scale + 0.5f).toInt()
19 | }
20 |
21 | /**
22 | * px 转 dp
23 | */
24 | fun Context.px2dp(pxValue: Float): Int {
25 | val scale = resources.displayMetrics.density
26 | return (pxValue / scale + 0.5f).toInt()
27 | }
28 |
29 | /**
30 | * sp 转 px
31 | */
32 | fun Context.sp2px(spValue: Float): Int {
33 | val scale = resources.displayMetrics.scaledDensity
34 | return (spValue * scale + 0.5f).toInt()
35 | }
36 |
37 | /**
38 | * px 转 sp
39 | */
40 | fun Context.px2sp(pxValue: Float): Int {
41 | val scale = resources.displayMetrics.scaledDensity
42 | return (pxValue / scale + 0.5f).toInt()
43 | }
44 |
45 | /**
46 | * dp 转 px
47 | */
48 | fun Fragment.dp2px(dpValue: Float): Int {
49 | val scale = resources.displayMetrics.density
50 | return (dpValue * scale + 0.5f).toInt()
51 | }
52 |
53 | /**
54 | * px 转 dp
55 | */
56 | fun Fragment.px2dp(pxValue: Float): Int {
57 | val scale = resources.displayMetrics.density
58 | return (pxValue / scale + 0.5f).toInt()
59 | }
60 |
61 | /**
62 | * sp 转 px
63 | */
64 | fun Fragment.sp2px(spValue: Float): Int {
65 | val scale = resources.displayMetrics.scaledDensity
66 | return (spValue * scale + 0.5f).toInt()
67 | }
68 |
69 | /**
70 | * px 转 sp
71 | */
72 | fun Fragment.px2sp(pxValue: Float): Int {
73 | val scale = resources.displayMetrics.scaledDensity
74 | return (pxValue / scale + 0.5f).toInt()
75 | }
--------------------------------------------------------------------------------
/module_home/src/main/res/layout/home_activity_internal_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
14 |
15 |
16 |
22 |
23 |
32 |
33 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/mvvm/vm/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.mvvm.vm
2 |
3 | import androidx.lifecycle.MutableLiveData
4 | import androidx.lifecycle.ViewModel
5 | import com.quyunshuo.androidbaseframemvvm.base.utils.StateLayoutEnum
6 | import kotlin.jvm.Throws
7 |
8 | /**
9 | * ViewModel 基类
10 | *
11 | * @author Qu Yunshuo
12 | * @since 8/27/20
13 | */
14 | abstract class BaseViewModel : ViewModel() {
15 |
16 | /**
17 | * 控制状态视图的LiveData
18 | */
19 | val stateViewLD = MutableLiveData()
20 |
21 | /**
22 | * 更改状态视图的状态
23 | *
24 | * @param hide Boolean 是否进行隐藏状态视图
25 | * @param loading Boolean 是否显示加载中视图
26 | * @param error Boolean 是否显示错误视图
27 | * @param noData Boolean 是否显示没有数据视图
28 | * @return Unit
29 | * @throws IllegalArgumentException 如果入参没有传入任何参数或者为true的参数 >1 时,会抛出[IllegalArgumentException]
30 | */
31 | @Throws(IllegalArgumentException::class)
32 | protected fun changeStateView(
33 | hide: Boolean = false,
34 | loading: Boolean = false,
35 | error: Boolean = false,
36 | noData: Boolean = false
37 | ) {
38 | // 对参数进行校验
39 | var count = 0
40 | if (hide) count++
41 | if (loading) count++
42 | if (error) count++
43 | if (noData) count++
44 | when {
45 | count == 0 -> throw IllegalArgumentException("必须设置一个参数为true")
46 | count > 1 -> throw IllegalArgumentException("只能有一个参数为true")
47 | }
48 |
49 | // 修改状态
50 | when {
51 | hide -> stateViewLD.postValue(StateLayoutEnum.HIDE)
52 | loading -> stateViewLD.postValue(StateLayoutEnum.LOADING)
53 | error -> stateViewLD.postValue(StateLayoutEnum.ERROR)
54 | noData -> stateViewLD.postValue(StateLayoutEnum.NO_DATA)
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/network/NetworkStateClient.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils.network
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.NetworkCapabilities
6 | import android.net.NetworkRequest
7 | import androidx.annotation.RequiresPermission
8 | import com.quyunshuo.androidbaseframemvvm.base.BaseApplication
9 |
10 | /**
11 | * 网络状态监听
12 | *
13 | * @author Qu Yunshuo
14 | * @since 2021/7/11 3:58 下午
15 | */
16 | object NetworkStateClient {
17 |
18 | private val mNetworkCallback = NetworkCallbackImpl()
19 |
20 | /**
21 | * 注册网络监听客户端
22 | * @return Unit
23 | */
24 | @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
25 | fun register() {
26 | val build = NetworkRequest.Builder().build()
27 | val cm =
28 | BaseApplication.context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
29 | cm.registerNetworkCallback(build, mNetworkCallback)
30 | }
31 |
32 | /**
33 | * 设置监听器
34 | * @param listener NetworkStateChangeListener 监听器
35 | * @return Unit
36 | */
37 | fun setListener(listener: NetworkStateChangeListener) {
38 | mNetworkCallback.changeCall = listener
39 | }
40 |
41 | /**
42 | * 移除监听器
43 | * @return Unit
44 | */
45 | fun removeListener() {
46 | mNetworkCallback.changeCall = null
47 | }
48 |
49 | /**
50 | * 获取网络类型
51 | * 当前网络类型是缓存的最近一次连接的网络类型,当无网络连接时其实拿到的是上一次的
52 | * 所以网络是否连接应该作为第一判断,确定网络是连接状态时再获取当前的网络类型,因为网络类型中没有设定无网
53 | * @return NetworkTypeEnum 参照[NetworkTypeEnum]
54 | */
55 | fun getNetworkType(): NetworkTypeEnum = mNetworkCallback.currentNetworkType
56 |
57 | /**
58 | * 网络是否连接
59 | * @return Boolean
60 | */
61 | fun isConnected(): Boolean = mNetworkCallback.isConnected
62 | }
--------------------------------------------------------------------------------
/base_lib.gradle:
--------------------------------------------------------------------------------
1 | //****************************************
2 | //********* lib 模块的公共脚本配置 **********
3 | //****************************************
4 |
5 | import com.quyunshuo.androidbaseframemvvm.buildsrc.*
6 |
7 | android {
8 | compileSdkVersion ProjectBuildConfig.compileSdkVersion
9 |
10 | defaultConfig {
11 | minSdkVersion ProjectBuildConfig.minSdkVersion
12 | targetSdkVersion ProjectBuildConfig.targetSdkVersion
13 | versionCode ProjectBuildConfig.versionCode
14 | versionName ProjectBuildConfig.versionName
15 |
16 | consumerProguardFiles "consumer-rules.pro"
17 |
18 | ndk {
19 | // 设置支持的SO库架构
20 | //abiFilters 'armeabi', 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
21 | abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86'
22 | }
23 | }
24 |
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_17
27 | targetCompatibility JavaVersion.VERSION_17
28 | }
29 |
30 | kotlinOptions {
31 | jvmTarget = "17"
32 | }
33 |
34 | buildTypes {
35 | // 对应 ALPHA 版本
36 | debug {
37 | buildConfigField "String", "VERSION_TYPE", "\"${ProjectBuildConfig.Version.ALPHA}\""
38 | minifyEnabled false
39 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
40 | }
41 | beta {
42 | buildConfigField "String", "VERSION_TYPE", "\"${ProjectBuildConfig.Version.BETA}\""
43 | minifyEnabled false
44 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
45 | }
46 | release {
47 | buildConfigField "String", "VERSION_TYPE", "\"${ProjectBuildConfig.Version.RELEASE}\""
48 | minifyEnabled false
49 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
50 | }
51 | }
52 | }
53 |
54 | kapt {
55 | arguments {
56 | arg("AROUTER_MODULE_NAME", project.getName())
57 | }
58 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/app/ActivityLifecycleCallbacksImpl.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.app
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.os.Bundle
6 | import android.util.Log
7 | import com.quyunshuo.androidbaseframemvvm.base.utils.ActivityStackManager
8 | import com.quyunshuo.androidbaseframemvvm.base.utils.ForegroundBackgroundHelper
9 |
10 | /**
11 | * Activity生命周期监听
12 | *
13 | * @author Qu Yunshuo
14 | * @since 4/20/21 9:10 AM
15 | */
16 | class ActivityLifecycleCallbacksImpl : Application.ActivityLifecycleCallbacks {
17 |
18 | private val TAG = "ActivityLifecycle"
19 |
20 | override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
21 | ActivityStackManager.addActivityToStack(activity)
22 | Log.e(TAG, "${activity.javaClass.simpleName} --> onActivityCreated")
23 | }
24 |
25 | override fun onActivityStarted(activity: Activity) {
26 | Log.e(TAG, "${activity.javaClass.simpleName} --> onActivityStarted")
27 | ForegroundBackgroundHelper.onActivityStarted()
28 | }
29 |
30 | override fun onActivityResumed(activity: Activity) {
31 | Log.e(TAG, "${activity.javaClass.simpleName} --> onActivityResumed")
32 | }
33 |
34 | override fun onActivityPaused(activity: Activity) {
35 | Log.e(TAG, "${activity.javaClass.simpleName} --> onActivityPaused")
36 | }
37 |
38 | override fun onActivityStopped(activity: Activity) {
39 | Log.e(TAG, "${activity.javaClass.simpleName} --> onActivityStopped")
40 | ForegroundBackgroundHelper.onActivityStopped()
41 | }
42 |
43 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
44 | Log.e(TAG, "${activity.javaClass.simpleName} --> onActivitySaveInstanceState")
45 | }
46 |
47 | override fun onActivityDestroyed(activity: Activity) {
48 | ActivityStackManager.popActivityToStack(activity)
49 | Log.e(TAG, "${activity.javaClass.simpleName} --> onActivityDestroyed")
50 | }
51 | }
--------------------------------------------------------------------------------
/module_home/src/main/java/com/quyunshuo/module/home/fragment/InternalViewModel.kt:
--------------------------------------------------------------------------------
1 | //package com.quyunshuo.module.home.fragment
2 | //
3 | //import android.util.Log
4 | //import androidx.lifecycle.MutableLiveData
5 | //import androidx.lifecycle.viewModelScope
6 | //import com.quyunshuo.androidbaseframemvvm.base.mvvm.vm.BaseViewModel
7 | //import com.quyunshuo.androidbaseframemvvm.base.utils.status.ViewStatusHelper
8 | //import dagger.hilt.android.lifecycle.HiltViewModel
9 | //import kotlinx.coroutines.Dispatchers
10 | //import kotlinx.coroutines.delay
11 | //import kotlinx.coroutines.flow.catch
12 | //import kotlinx.coroutines.flow.collect
13 | //import kotlinx.coroutines.flow.onStart
14 | //import kotlinx.coroutines.launch
15 | //import javax.inject.Inject
16 | //
17 | ///**
18 | // * @author DBoy 2021/7/6
19 | // * - 文件描述 : ViewModel再ViewPager2的Fragment中会随着Fragment执行[Fragment.onDestory]一同销毁。
20 | // * 所以一些需要长期保存的变量数据,不适合保存再ViewModel,考虑使用[ViewStatusHelper]保存页面上部分数据,
21 | // * 页面恢复的时候再交给ViewModel处理,例如[recreatedCont]
22 | // */
23 | //@HiltViewModel
24 | //class InternalViewModel @Inject constructor() :
25 | // BaseViewModel() {
26 | //
27 | // @Inject
28 | // lateinit var repository: InternalRepository
29 | //
30 | // /**
31 | // * 重建计数
32 | // */
33 | // val recreatedCont = MutableLiveData()
34 | //
35 | // /**
36 | // * 首个数据
37 | // */
38 | // val firstData = MutableLiveData()
39 | //
40 | // /**
41 | // * 累加重建次数
42 | // */
43 | // fun increase(size: Int) {
44 | // recreatedCont.value = size
45 | // }
46 | //
47 | // /**
48 | // * 获取数据
49 | // */
50 | // fun getData() {
51 | // viewModelScope.launch(Dispatchers.IO) {
52 | // repository.getData()
53 | // .catch {
54 | // Log.d("DJC", "getData: ")
55 | // }
56 | // .onStart { changeStateView(loading = true) }
57 | // .collect {
58 | // changeStateView(hide = true)
59 | // delay(200)
60 | // firstData.postValue(it)
61 | // }
62 | // }
63 | // }
64 | //
65 | //}
66 |
--------------------------------------------------------------------------------
/lib_common/src/main/java/com/quyunshuo/androidbaseframemvvm/common/di/DINetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.common.di
2 |
3 | import com.quyunshuo.androidbaseframemvvm.base.BuildConfig
4 | import com.quyunshuo.androidbaseframemvvm.base.constant.VersionStatus
5 | import com.quyunshuo.androidbaseframemvvm.common.constant.NetBaseUrlConstant
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.components.SingletonComponent
10 | import okhttp3.OkHttpClient
11 | import okhttp3.logging.HttpLoggingInterceptor
12 | import retrofit2.Retrofit
13 | import java.util.concurrent.TimeUnit
14 | import okhttp3.logging.HttpLoggingInterceptor.Level.NONE
15 | import okhttp3.logging.HttpLoggingInterceptor.Level.BODY
16 | import retrofit2.converter.gson.GsonConverterFactory
17 | import javax.inject.Singleton
18 |
19 | /**
20 | * 全局作用域的网络层的依赖注入模块
21 | *
22 | * @author Qu Yunshuo
23 | * @since 6/4/21 8:58 AM
24 | */
25 | @Module
26 | @InstallIn(SingletonComponent::class)
27 | class DINetworkModule {
28 |
29 | /**
30 | * [OkHttpClient]依赖提供方法
31 | *
32 | * @return OkHttpClient
33 | */
34 | @Singleton
35 | @Provides
36 | fun provideOkHttpClient(): OkHttpClient {
37 | // 日志拦截器部分
38 | val level = if (BuildConfig.VERSION_TYPE != VersionStatus.RELEASE) BODY else NONE
39 | val logInterceptor = HttpLoggingInterceptor().setLevel(level)
40 |
41 | return OkHttpClient.Builder()
42 | .connectTimeout(15L * 1000L, TimeUnit.MILLISECONDS)
43 | .readTimeout(20L * 1000L, TimeUnit.MILLISECONDS)
44 | .addInterceptor(logInterceptor)
45 | .retryOnConnectionFailure(true)
46 | .build()
47 | }
48 |
49 | /**
50 | * 项目主要服务器地址的[Retrofit]依赖提供方法
51 | *
52 | * @param okHttpClient OkHttpClient OkHttp客户端
53 | * @return Retrofit
54 | */
55 | @Singleton
56 | @Provides
57 | fun provideMainRetrofit(okHttpClient: OkHttpClient): Retrofit {
58 | return Retrofit.Builder()
59 | .baseUrl(NetBaseUrlConstant.MAIN_URL)
60 | .addConverterFactory(GsonConverterFactory.create())
61 | .client(okHttpClient)
62 | .build()
63 | }
64 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/ForegroundBackgroundHelper.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils
2 |
3 | /**
4 | * 前后台切换帮助类,该类实现了前后台监听以及支持注册变化响应监听
5 | *
6 | * @see ForegroundBackgroundObserver
7 | * @see ForegroundBackgroundSubject
8 | *
9 | * @author Qu Yunshuo
10 | * @since 2023/5/31 14:22
11 | */
12 | object ForegroundBackgroundHelper : ForegroundBackgroundSubject {
13 |
14 | private var mActivityStartCount = 0
15 |
16 | private var mIsForeground = false
17 |
18 | private val mObservers = mutableListOf()
19 |
20 | fun onActivityStarted() {
21 | mActivityStartCount++
22 | if (mActivityStartCount == 1) {
23 | mIsForeground = true
24 | notifyObservers()
25 | }
26 | }
27 |
28 | fun onActivityStopped() {
29 | mActivityStartCount--
30 | if (mActivityStartCount == 0) {
31 | mIsForeground = false
32 | notifyObservers()
33 | }
34 | }
35 |
36 | /**
37 | * 通知所有订阅者状态变化
38 | */
39 | override fun notifyObservers() {
40 | mObservers.forEach {
41 | it.foregroundBackgroundNotify(mIsForeground)
42 | }
43 | }
44 |
45 | /**
46 | * 添加订阅者
47 | *
48 | * @param observer ForegroundBackgroundObserver
49 | */
50 | override fun addObserve(observer: ForegroundBackgroundObserver) {
51 | mObservers.add(observer)
52 | }
53 |
54 | /**
55 | * 移除订阅者
56 | *
57 | * @param observer ForegroundBackgroundObserver
58 | */
59 | override fun removeObserver(observer: ForegroundBackgroundObserver) {
60 | mObservers.remove(observer)
61 | }
62 | }
63 |
64 | /**
65 | * 订阅者需要实现的接口
66 | *
67 | * @author Qu Yunshuo
68 | * @since 2023/5/31 14:23
69 | */
70 | interface ForegroundBackgroundObserver {
71 | fun foregroundBackgroundNotify(isForeground: Boolean)
72 | }
73 |
74 | /**
75 | * 被观察者抽象主题
76 | *
77 | * @author Qu Yunshuo
78 | * @since 2023/5/31 14:24
79 | */
80 | interface ForegroundBackgroundSubject {
81 | fun notifyObservers()
82 | fun addObserve(observer: ForegroundBackgroundObserver)
83 | fun removeObserver(observer: ForegroundBackgroundObserver)
84 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/SpUtils.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils
2 |
3 | import android.content.Context
4 | import com.tencent.mmkv.MMKV
5 |
6 | /**
7 | * MMKV使用封装
8 | *
9 | * @author Qu Yunshuo
10 | * @since 8/28/20
11 | */
12 | object SpUtils {
13 |
14 | /**
15 | * 初始化
16 | */
17 | fun initMMKV(context: Context): String? = MMKV.initialize(context)
18 |
19 | /**
20 | * 保存数据(简化)
21 | * 根据value类型自动匹配需要执行的方法
22 | */
23 | fun put(key: String, value: Any) =
24 | when (value) {
25 | is Int -> putInt(key, value)
26 | is Long -> putLong(key, value)
27 | is Float -> putFloat(key, value)
28 | is Double -> putDouble(key, value)
29 | is String -> putString(key, value)
30 | is Boolean -> putBoolean(key, value)
31 | else -> false
32 | }
33 |
34 | fun putString(key: String, value: String): Boolean? = MMKV.defaultMMKV()?.encode(key, value)
35 |
36 | fun getString(key: String, defValue: String): String? =
37 | MMKV.defaultMMKV()?.decodeString(key, defValue)
38 |
39 | fun putInt(key: String, value: Int): Boolean? = MMKV.defaultMMKV()?.encode(key, value)
40 |
41 | fun getInt(key: String, defValue: Int): Int? = MMKV.defaultMMKV()?.decodeInt(key, defValue)
42 |
43 | fun putLong(key: String, value: Long): Boolean? = MMKV.defaultMMKV()?.encode(key, value)
44 |
45 | fun getLong(key: String, defValue: Long): Long? = MMKV.defaultMMKV()?.decodeLong(key, defValue)
46 |
47 | fun putDouble(key: String, value: Double): Boolean? = MMKV.defaultMMKV()?.encode(key, value)
48 |
49 | fun getDouble(key: String, defValue: Double): Double? =
50 | MMKV.defaultMMKV()?.decodeDouble(key, defValue)
51 |
52 | fun putFloat(key: String, value: Float): Boolean? = MMKV.defaultMMKV()?.encode(key, value)
53 |
54 | fun getFloat(key: String, defValue: Float): Float? =
55 | MMKV.defaultMMKV()?.decodeFloat(key, defValue)
56 |
57 | fun putBoolean(key: String, value: Boolean): Boolean? = MMKV.defaultMMKV()?.encode(key, value)
58 |
59 | fun getBoolean(key: String, defValue: Boolean): Boolean? =
60 | MMKV.defaultMMKV()?.decodeBool(key, defValue)
61 |
62 | fun contains(key: String): Boolean? = MMKV.defaultMMKV()?.contains(key)
63 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/mvvm/v/BaseFrameFragment.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.mvvm.v
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.Fragment
8 | import androidx.viewbinding.ViewBinding
9 | import com.alibaba.android.arouter.launcher.ARouter
10 | import com.quyunshuo.androidbaseframemvvm.base.mvvm.vm.BaseViewModel
11 | import com.quyunshuo.androidbaseframemvvm.base.utils.RegisterEventBus
12 | import com.quyunshuo.androidbaseframemvvm.base.utils.EventBusUtils
13 |
14 | /**
15 | * Fragment基类
16 | *
17 | * @author Qu Yunshuo
18 | * @since 8/27/20
19 | */
20 | abstract class BaseFrameFragment : Fragment(),
21 | FrameView {
22 |
23 | /**
24 | * 私有的 ViewBinding 此写法来自 Google Android 官方
25 | */
26 | private var _binding: VB? = null
27 |
28 | protected val mBinding get() = _binding!!
29 |
30 | protected abstract val mViewModel: VM
31 |
32 | /**
33 | * 是否有 [RegisterEventBus] 注解,避免重复调用 [Class.isAnnotation]
34 | */
35 | private var mHaveRegisterEventBus = false
36 |
37 | override fun onCreateView(
38 | inflater: LayoutInflater,
39 | container: ViewGroup?,
40 | savedInstanceState: Bundle?
41 | ): View? {
42 | _binding = createVB()
43 | return _binding?.root
44 | }
45 |
46 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
47 | super.onViewCreated(view, savedInstanceState)
48 | // ARouter 依赖注入
49 | ARouter.getInstance().inject(this)
50 |
51 | // 根据子类是否有 RegisterEventBus 注解決定是否进行注册 EventBus
52 | if (javaClass.isAnnotationPresent(RegisterEventBus::class.java)) {
53 | mHaveRegisterEventBus = true
54 | EventBusUtils.register(this)
55 | }
56 | _binding?.initView()
57 | initObserve()
58 | initRequestData()
59 | }
60 |
61 | override fun onDestroyView() {
62 | super.onDestroyView()
63 | _binding = null
64 | }
65 |
66 | override fun onDestroy() {
67 | // 根据子类是否有 RegisterEventBus 注解决定是否进行注册 EventBus
68 | if (mHaveRegisterEventBus) {
69 | EventBusUtils.unRegister(this)
70 | }
71 | super.onDestroy()
72 | }
73 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/ProcessUtils.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils
2 |
3 | import android.app.ActivityManager
4 | import android.content.Context
5 | import android.content.pm.PackageManager
6 | import android.os.Process
7 | import kotlin.jvm.Throws
8 |
9 | /**
10 | * 进程工具类
11 | *
12 | * @author Qu Yunshuo
13 | * @since 3/16/21 9:06 AM
14 | */
15 | object ProcessUtils {
16 |
17 | /**
18 | * 获取当前所有进程
19 | *
20 | * @param context Context 上下文
21 | * @return List 当前所有进程
22 | */
23 | fun getRunningAppProcessList(context: Context): List {
24 | val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
25 | return activityManager.runningAppProcesses
26 | }
27 |
28 | /**
29 | * 判断该进程id是否属于该进程名的进程
30 | *
31 | * @param context Context 上下文
32 | * @param processId Int 进程Id
33 | * @param processName String 进程名
34 | * @return Boolean
35 | */
36 | fun isPidOfProcessName(context: Context, processId: Int, processName: String): Boolean {
37 | // 遍历所有进程找到该进程id对应的进程
38 | for (process in getRunningAppProcessList(context)) {
39 | if (process.pid == processId) {
40 | // 判断该进程id是否和进程名一致
41 | return (process.processName == processName)
42 | }
43 | }
44 | return false
45 | }
46 |
47 | /**
48 | * 获取主进程名
49 | *
50 | * @param context Context 上下文
51 | * @return String 主进程名
52 | * @throws PackageManager.NameNotFoundException if a package with the given name cannot be found on the system.
53 | */
54 | @Throws(PackageManager.NameNotFoundException::class)
55 | fun getMainProcessName(context: Context): String {
56 | val applicationInfo = context.packageManager.getApplicationInfo(context.packageName, 0)
57 | return applicationInfo.processName
58 | }
59 |
60 | /**
61 | * 判断当前进程是否是主进程
62 | *
63 | * @param context Context 上下文
64 | * @return Boolean
65 | * @throws PackageManager.NameNotFoundException if a package with the given name cannot be found on the system.
66 | */
67 | @Throws(PackageManager.NameNotFoundException::class)
68 | fun isMainProcess(context: Context): Boolean {
69 | val processId = Process.myPid()
70 | val mainProcessName = getMainProcessName(context)
71 | return isPidOfProcessName(context, processId, mainProcessName)
72 | }
73 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/BaseApplication.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.util.Log
6 | import androidx.multidex.MultiDexApplication
7 | import com.quyunshuo.androidbaseframemvvm.base.app.ActivityLifecycleCallbacksImpl
8 | import com.quyunshuo.androidbaseframemvvm.base.app.LoadModuleProxy
9 | import kotlinx.coroutines.*
10 | import kotlin.system.measureTimeMillis
11 |
12 | /**
13 | * Application 基类
14 | *
15 | * @author Qu Yunshuo
16 | * @since 4/24/21 5:30 PM
17 | */
18 | open class BaseApplication : MultiDexApplication() {
19 |
20 | private val mCoroutineScope by lazy(mode = LazyThreadSafetyMode.NONE) { MainScope() }
21 |
22 | private val mLoadModuleProxy by lazy(mode = LazyThreadSafetyMode.NONE) { LoadModuleProxy() }
23 |
24 | companion object {
25 | // 全局Context
26 | @SuppressLint("StaticFieldLeak")
27 | lateinit var context: Context
28 |
29 | @SuppressLint("StaticFieldLeak")
30 | lateinit var application: BaseApplication
31 | }
32 |
33 | override fun attachBaseContext(base: Context) {
34 | super.attachBaseContext(base)
35 | context = base
36 | application = this
37 | mLoadModuleProxy.onAttachBaseContext(base)
38 | }
39 |
40 | override fun onCreate() {
41 | super.onCreate()
42 |
43 | // 全局监听 Activity 生命周期
44 | registerActivityLifecycleCallbacks(ActivityLifecycleCallbacksImpl())
45 |
46 | mLoadModuleProxy.onCreate(this)
47 |
48 | // 策略初始化第三方依赖
49 | initDepends()
50 | }
51 |
52 | /**
53 | * 初始化第三方依赖
54 | */
55 | private fun initDepends() {
56 | // 开启一个 Default Coroutine 进行初始化不会立即使用的第三方
57 | mCoroutineScope.launch(Dispatchers.Default) {
58 | mLoadModuleProxy.initByBackstage()
59 | }
60 |
61 | // 前台初始化
62 | val allTimeMillis = measureTimeMillis {
63 | val depends = mLoadModuleProxy.initByFrontDesk()
64 | var dependInfo: String
65 | depends.forEach {
66 | val dependTimeMillis = measureTimeMillis { dependInfo = it() }
67 | Log.d("BaseApplication", "initDepends: $dependInfo : $dependTimeMillis ms")
68 | }
69 | }
70 | Log.d("BaseApplication", "初始化完成 $allTimeMillis ms")
71 | }
72 |
73 | override fun onTerminate() {
74 | super.onTerminate()
75 | mLoadModuleProxy.onTerminate(this)
76 | mCoroutineScope.cancel()
77 | }
78 | }
--------------------------------------------------------------------------------
/lib_base/build.gradle:
--------------------------------------------------------------------------------
1 | //****************************************
2 | //********** lib_base 的配置文件 ***********
3 | //****************************************
4 |
5 | plugins {
6 | alias(libs.plugins.library)
7 | alias(libs.plugins.kotlin)
8 | alias(libs.plugins.hilt)
9 | id "kotlin-kapt"
10 | }
11 |
12 | apply from: '../base_lib.gradle'
13 |
14 | import com.quyunshuo.androidbaseframemvvm.buildsrc.*
15 |
16 | android {
17 |
18 | buildFeatures {
19 | viewBinding = true
20 | }
21 |
22 | resourcePrefix "base_"
23 | namespace 'com.quyunshuo.androidbaseframemvvm.base'
24 | }
25 |
26 | dependencies {
27 | implementation fileTree(dir: "libs", include: ["*.jar"])
28 |
29 | api DependencyConfig.AndroidX.CoreKtx
30 | api DependencyConfig.AndroidX.AppCompat
31 | api DependencyConfig.AndroidX.ConstraintLayout
32 | api DependencyConfig.AndroidX.ActivityKtx
33 | api DependencyConfig.AndroidX.FragmentKtx
34 | api DependencyConfig.AndroidX.MultiDex
35 |
36 | api DependencyConfig.Android.Material
37 |
38 | api DependencyConfig.Kotlin.Kotlin
39 | api DependencyConfig.Kotlin.CoroutinesCore
40 | api DependencyConfig.Kotlin.CoroutinesAndroid
41 |
42 | api DependencyConfig.JetPack.ViewModel
43 | api DependencyConfig.JetPack.ViewModelSavedState
44 | api DependencyConfig.JetPack.LiveData
45 | api DependencyConfig.JetPack.Lifecycle
46 | api DependencyConfig.JetPack.HiltCore
47 |
48 | api DependencyConfig.GitHub.Gson
49 | api DependencyConfig.GitHub.MMKV
50 | api DependencyConfig.GitHub.AutoSize
51 | api DependencyConfig.GitHub.ARoute
52 | api DependencyConfig.GitHub.RecyclerViewAdapter
53 | api DependencyConfig.GitHub.EventBus
54 | api DependencyConfig.GitHub.PermissionX
55 | api DependencyConfig.GitHub.AutoService
56 | api DependencyConfig.GitHub.OkHttp
57 | api DependencyConfig.GitHub.OkHttpInterceptorLogging
58 | api DependencyConfig.GitHub.Retrofit
59 | api DependencyConfig.GitHub.RetrofitConverterGson
60 | api DependencyConfig.GitHub.Coil
61 | api DependencyConfig.GitHub.CoilGIF
62 | api DependencyConfig.GitHub.CoilSVG
63 | api DependencyConfig.GitHub.CoilVideo
64 |
65 | api DependencyConfig.SDK.TencentBugly
66 | api DependencyConfig.SDK.TencentBuglyNative
67 | api DependencyConfig.SDK.TencentTBSX5
68 |
69 | kapt DependencyConfig.GitHub.ARouteCompiler
70 | kapt DependencyConfig.GitHub.EventBusAPT
71 | kapt DependencyConfig.GitHub.AutoServiceAnnotations
72 | kapt DependencyConfig.JetPack.HiltApt
73 | kapt DependencyConfig.JetPack.LifecycleCompilerAPT
74 |
75 | debugApi DependencyConfig.GitHub.LeakCanary
76 | }
--------------------------------------------------------------------------------
/module_home/src/main/res/layout/home_fragment_internal_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
17 |
18 |
28 |
29 |
34 |
35 |
36 |
37 |
46 |
47 |
48 |
57 |
58 |
67 |
68 |
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/network/NetworkCallbackImpl.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils.network
2 |
3 | import android.net.ConnectivityManager
4 | import android.net.Network
5 | import android.net.NetworkCapabilities
6 |
7 |
8 | /**
9 | * 实时监听网络状态变化的[ConnectivityManager.NetworkCallback]实现类
10 | *
11 | * @author Qu Yunshuo
12 | * @since 2021/7/10 5:38 下午
13 | */
14 | class NetworkCallbackImpl : ConnectivityManager.NetworkCallback() {
15 |
16 | /**
17 | * 当前网络类型
18 | */
19 | var currentNetworkType: NetworkTypeEnum = NetworkTypeEnum.OTHER
20 |
21 | /**
22 | * 当前网络是否已连接
23 | */
24 | var isConnected = false
25 |
26 | /**
27 | * 注册的监听
28 | */
29 | var changeCall: NetworkStateChangeListener? = null
30 |
31 | override fun onAvailable(network: Network) {
32 | super.onAvailable(network)
33 | isConnected = true
34 | changeCall?.networkConnectChange(isConnected)
35 | }
36 |
37 | override fun onLost(network: Network) {
38 | super.onLost(network)
39 | isConnected = false
40 | changeCall?.networkConnectChange(isConnected)
41 | }
42 |
43 | override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
44 | super.onCapabilitiesChanged(network, networkCapabilities)
45 | if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
46 | currentNetworkType = networkTypeConvert(networkCapabilities)
47 | changeCall?.networkTypeChange(currentNetworkType)
48 | }
49 | }
50 |
51 | /**
52 | * 网络类型转换
53 | */
54 | private fun networkTypeConvert(networkCapabilities: NetworkCapabilities): NetworkTypeEnum {
55 | return when {
56 | networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {
57 | NetworkTypeEnum.TRANSPORT_CELLULAR
58 | }
59 | networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> {
60 | NetworkTypeEnum.TRANSPORT_WIFI
61 | }
62 | networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> {
63 | NetworkTypeEnum.TRANSPORT_BLUETOOTH
64 | }
65 | networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> {
66 | NetworkTypeEnum.TRANSPORT_ETHERNET
67 | }
68 | networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> {
69 | NetworkTypeEnum.TRANSPORT_VPN
70 | }
71 | networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE) -> {
72 | NetworkTypeEnum.TRANSPORT_WIFI_AWARE
73 | }
74 | networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_LOWPAN) -> {
75 | NetworkTypeEnum.TRANSPORT_LOWPAN
76 | }
77 | else -> NetworkTypeEnum.OTHER
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/base_module.gradle:
--------------------------------------------------------------------------------
1 | //****************************************
2 | //******** module模块的公共脚本配置 *********
3 | //****************************************
4 |
5 | import com.quyunshuo.androidbaseframemvvm.buildsrc.*
6 |
7 | android {
8 | compileSdk ProjectBuildConfig.compileSdkVersion
9 |
10 | defaultConfig {
11 | minSdk ProjectBuildConfig.minSdkVersion
12 | targetSdk ProjectBuildConfig.targetSdkVersion
13 | versionCode ProjectBuildConfig.versionCode
14 | versionName ProjectBuildConfig.versionName
15 | testInstrumentationRunner DependencyConfig.AndroidX.AndroidJUnitRunner
16 |
17 | ndk {
18 | // 设置支持的SO库架构
19 | //abiFilters 'armeabi', 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
20 | abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86'
21 | }
22 | }
23 |
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_17
26 | targetCompatibility JavaVersion.VERSION_17
27 | }
28 |
29 | kotlinOptions {
30 | jvmTarget = "17"
31 | }
32 |
33 | buildFeatures {
34 | viewBinding = true
35 | }
36 |
37 | sourceSets {
38 | main {
39 | manifest.srcFile 'src/main/AndroidManifest.xml'
40 | java {
41 | //排除debug文件夹下的所有文件
42 | exclude 'debug/**'
43 | }
44 | }
45 | }
46 |
47 | buildTypes {
48 | // 对应 ALPHA 版本
49 | debug {
50 | buildConfigField "String", "VERSION_TYPE", "\"${ProjectBuildConfig.Version.ALPHA}\""
51 | minifyEnabled false
52 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
53 | }
54 | beta {
55 | buildConfigField "String", "VERSION_TYPE", "\"${ProjectBuildConfig.Version.BETA}\""
56 | minifyEnabled false
57 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
58 | }
59 | release {
60 | buildConfigField "String", "VERSION_TYPE", "\"${ProjectBuildConfig.Version.RELEASE}\""
61 | minifyEnabled false
62 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
63 | }
64 | }
65 | }
66 |
67 | kapt {
68 | arguments {
69 | arg("AROUTER_MODULE_NAME", project.name)
70 | arg("eventBusIndex", "${ProjectBuildConfig.applicationId}.eventbus.index.${project.name}EventIndex")
71 | }
72 | }
73 |
74 | dependencies {
75 | implementation fileTree(dir: 'libs', include: ['*.jar'])
76 |
77 | api project(path: ':lib_common')
78 |
79 | testImplementation DependencyConfig.Android.Junit
80 | androidTestImplementation DependencyConfig.AndroidX.TestExtJunit
81 | androidTestImplementation DependencyConfig.AndroidX.TestEspresso
82 | implementation DependencyConfig.JetPack.HiltCore
83 |
84 | kapt DependencyConfig.GitHub.ARouteCompiler
85 | kapt DependencyConfig.GitHub.EventBusAPT
86 | kapt DependencyConfig.GitHub.AutoServiceAnnotations
87 | kapt DependencyConfig.JetPack.HiltApt
88 | kapt DependencyConfig.JetPack.LifecycleCompilerAPT
89 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/ActivityStackManager.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils
2 |
3 | import android.app.Activity
4 | import java.util.*
5 |
6 | /**
7 | * @Author: QuYunShuo
8 | * @Time: 2020/9/11
9 | * @Class: ActivityStackManager
10 | * @Remark: Activity 栈管理类
11 | */
12 | object ActivityStackManager {
13 |
14 | // 管理栈
15 | val activityStack by lazy { Stack() }
16 |
17 | /**
18 | * 添加 Activity 到管理栈
19 | * @param activity Activity
20 | */
21 | fun addActivityToStack(activity: Activity) {
22 | activityStack.push(activity)
23 | }
24 |
25 | /**
26 | * 弹出栈内指定Activity 不finish
27 | * @param activity Activity
28 | */
29 | fun popActivityToStack(activity: Activity) {
30 | if (!activityStack.empty()) {
31 | activityStack.forEach {
32 | if (it == activity) {
33 | activityStack.remove(activity)
34 | return
35 | }
36 | }
37 | }
38 | }
39 |
40 | /**
41 | * 返回到上一个 Activity 并结束当前 Activity
42 | */
43 | fun backToPreviousActivity() {
44 | if (!activityStack.empty()) {
45 | val activity = activityStack.pop()
46 | if (!activity.isFinishing) activity.finish()
47 | }
48 | }
49 |
50 | /**
51 | * 根据类名 判断是否是当前的 Activity
52 | * @param cls Class<*> 类名
53 | * @return Boolean
54 | */
55 | fun isCurrentActivity(cls: Class<*>): Boolean {
56 | val currentActivity = getCurrentActivity()
57 | return if (currentActivity != null) currentActivity.javaClass == cls else false
58 | }
59 |
60 | /**
61 | * 获取当前的 Activity
62 | */
63 | fun getCurrentActivity(): Activity? =
64 | if (!activityStack.empty()) activityStack.lastElement() else null
65 |
66 | /**
67 | * 结束一个栈内指定类名的 Activity
68 | * @param cls Class<*>
69 | */
70 | fun finishActivity(cls: Class<*>) {
71 | activityStack.forEach {
72 | if (it.javaClass == cls) {
73 | if (!it.isFinishing) it.finish()
74 | return
75 | }
76 | }
77 | }
78 |
79 | /**
80 | * 弹出其他 Activity
81 | */
82 | fun popOtherActivity() {
83 | val activityList = activityStack.toList()
84 | getCurrentActivity()?.run {
85 | activityList.forEach { activity ->
86 | if (this != activity) {
87 | activityStack.remove(activity)
88 | activity.finish()
89 | }
90 | }
91 | }
92 | }
93 |
94 | /**
95 | * 返回到指定 Activity
96 | */
97 | fun backToSpecifyActivity(activityClass: Class<*>) {
98 | val activityList = activityStack.toList().reversed()
99 | activityList.forEach {
100 | if (it.javaClass == activityClass) {
101 | return
102 | } else {
103 | activityStack.pop()
104 | it.finish()
105 | }
106 | }
107 | }
108 | }
--------------------------------------------------------------------------------
/module_home/src/main/java/com/quyunshuo/module/home/fragment/InternalFragment.kt:
--------------------------------------------------------------------------------
1 | //package com.quyunshuo.module.home.fragment
2 | //
3 | //import android.os.Bundle
4 | //import android.util.Log
5 | //import androidx.fragment.app.viewModels
6 | //import com.quyunshuo.androidbaseframemvvm.base.utils.status.ViewStatusHelper
7 | //import com.quyunshuo.androidbaseframemvvm.common.ui.BaseFragment
8 | //import com.quyunshuo.module.home.databinding.HomeFragmentInternalLayoutBinding
9 | //import dagger.hilt.android.AndroidEntryPoint
10 | //
11 | ///**
12 | // * @author DBoy 2021/7/6
13 | // * - 文件描述 : 测试fragment
14 | // */
15 | //@AndroidEntryPoint
16 | //class InternalFragment : BaseFragment() {
17 | //
18 | // override val mViewModel by viewModels()
19 | //
20 | // /**
21 | // * 页面状态数据管理帮助类
22 | // */
23 | // private lateinit var mInternalFragmentStatusHelper: InternalFragmentStatusHelper
24 | //
25 | // /***
26 | // * 注册帮助类
27 | // */
28 | // override fun onRegisterStatusHelper(): ViewStatusHelper? {
29 | // mInternalFragmentStatusHelper = InternalFragmentStatusHelper(super.onRegisterStatusHelper())
30 | // return mInternalFragmentStatusHelper
31 | // }
32 | //
33 | // override fun HomeFragmentInternalLayoutBinding.initView() {}
34 | //
35 | //
36 | // override fun initObserve() {
37 | // mViewModel.increase(mInternalFragmentStatusHelper.rebuildSize)
38 | // val s = arguments?.getString("What") ?: ""
39 | // mBinding.toolBarTitle.text = s
40 | // mViewModel.recreatedCont.observe(viewLifecycleOwner) {
41 | // mBinding.recreateContTv.text = "重建次数 $it"
42 | // }
43 | // mViewModel.firstData.observe(viewLifecycleOwner) {
44 | // mBinding.loadDataTv.text = it
45 | // }
46 | //// mViewModel.isLoading.observe(viewLifecycleOwner) {
47 | //// mBinding.loadingStatusTv.text = if (it) {
48 | //// "正在加载..."
49 | //// } else {
50 | //// "加载完成!"
51 | //// }
52 | //// }
53 | // }
54 | //
55 | // override fun initRequestData() {
56 | // //当页面重建的时候不再重新请求数据,且当前页面数据数据有且没有刷新逻辑的情况下不再请求数据。
57 | // if (isRecreate() && mViewModel.firstData.value != null) {
58 | // return
59 | // }
60 | // mViewModel.getData()
61 | // }
62 | //
63 | // /**
64 | // * 当前Fragment重建帮助类
65 | // */
66 | // internal class InternalFragmentStatusHelper(parentViewStatusHelper: ViewStatusHelper?) : ViewStatusHelper(parentViewStatusHelper) {
67 | // /**
68 | // * 重建次数
69 | // */
70 | // var rebuildSize = 0
71 | //
72 | // private val KEY_REBUILD = "com.quyunshuo.module.home.fragment.InternalFragment.InternalFragmentStatusHelper.rebuild"
73 | //
74 | // override fun onRestoreInstanceStatus(savedInstanceState: Bundle?) {
75 | // super.onRestoreInstanceStatus(savedInstanceState)
76 | // rebuildSize = (savedInstanceState?.getInt(KEY_REBUILD) ?: 0) + 1
77 | // }
78 | //
79 | // override fun onSaveInstanceState(bundle: Bundle) {
80 | // super.onSaveInstanceState(bundle)
81 | // bundle.putInt(KEY_REBUILD, rebuildSize)
82 | // }
83 | // }
84 | //
85 | //}
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/ktx/ViewModelKtx.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.ktx
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import kotlinx.coroutines.*
6 |
7 | /**
8 | * 开启一个线程调度模式为[Dispatchers.IO]的协程 有默认的异常处理器
9 | *
10 | * **sample:**
11 | * ```
12 | * class SampleViewModel : ViewModel() {
13 | *
14 | * fun sample() {
15 | * launchIO {
16 | * // 协程体
17 | * }
18 | * launchIO(exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
19 | * // exception handling
20 | * }) {
21 | * // 协程体
22 | * }
23 | * }
24 | * }
25 | * ```
26 | *
27 | * @receiver ViewModel
28 | *
29 | * @param exceptionHandler CoroutineExceptionHandler 异常处理器
30 | * @param block suspend CoroutineScope.() -> Unit 协程体
31 | * @return Job
32 | */
33 | fun ViewModel.launchIO(
34 | exceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
35 | throwable.printStackTrace()
36 | },
37 | block: suspend CoroutineScope.() -> Unit
38 | ): Job = viewModelScope.launch(Dispatchers.IO + exceptionHandler, block = block)
39 |
40 | /**
41 | * 开启一个线程调度模式为[Dispatchers.Default]的协程 有默认的异常处理器
42 | *
43 | * **sample:**
44 | * ```
45 | * class SampleViewModel : ViewModel() {
46 | *
47 | * fun sample() {
48 | * launchDefault {
49 | * // 协程体
50 | * }
51 | * launchDefault(exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
52 | * // exception handling
53 | * }) {
54 | * // 协程体
55 | * }
56 | * }
57 | * }
58 | * ```
59 | *
60 | * @receiver ViewModel
61 | *
62 | * @param exceptionHandler CoroutineExceptionHandler 异常处理器
63 | * @param block suspend CoroutineScope.() -> Unit 协程体
64 | * @return Job
65 | */
66 | fun ViewModel.launchDefault(
67 | exceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
68 | throwable.printStackTrace()
69 | },
70 | block: suspend CoroutineScope.() -> Unit
71 | ): Job = viewModelScope.launch(Dispatchers.Default + exceptionHandler, block = block)
72 |
73 | /**
74 | * 开启一个线程调度模式为[Dispatchers.Main]的协程 有默认的异常处理器
75 | *
76 | * **sample:**
77 | * ```
78 | * class SampleViewModel : ViewModel() {
79 | *
80 | * fun sample() {
81 | * launchMain {
82 | * // 协程体
83 | * }
84 | * launchMain(exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
85 | * // exception handling
86 | * }) {
87 | * // 协程体
88 | * }
89 | * }
90 | * }
91 | * ```
92 | *
93 | * @receiver ViewModel
94 | *
95 | * @param exceptionHandler CoroutineExceptionHandler 异常处理器
96 | * @param block suspend CoroutineScope.() -> Unit 协程体
97 | * @return Job
98 | */
99 | fun ViewModel.launchMain(
100 | exceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
101 | throwable.printStackTrace()
102 | },
103 | block: suspend CoroutineScope.() -> Unit
104 | ): Job = viewModelScope.launch(Dispatchers.Main + exceptionHandler, block = block)
--------------------------------------------------------------------------------
/module_home/src/main/java/com/quyunshuo/module/home/activity/InternalPagerActivity.kt:
--------------------------------------------------------------------------------
1 | //package com.quyunshuo.module.home.activity
2 | //
3 | //import android.os.Bundle
4 | //import androidx.activity.viewModels
5 | //import androidx.fragment.app.Fragment
6 | //import androidx.fragment.app.FragmentActivity
7 | //import com.quyunshuo.androidbaseframemvvm.base.mvvm.vm.EmptyViewModel
8 | //import com.quyunshuo.androidbaseframemvvm.common.ui.BaseActivity
9 | //import com.quyunshuo.androidbaseframemvvm.common.ui.BaseFragmentStateAdapter
10 | //import com.quyunshuo.module.home.databinding.HomeActivityInternalLayoutBinding
11 | //import com.quyunshuo.module.home.fragment.InternalFragment
12 | //import dagger.hilt.android.AndroidEntryPoint
13 | //import kotlin.random.Random
14 | //
15 | ///**
16 | // * @author DBoy 2021/7/6
17 | // * - 文件描述 : ViewPager2+fragment 模拟Fragment页面重建。
18 | // */
19 | //@AndroidEntryPoint
20 | //class InternalPagerActivity : BaseActivity() {
21 | //
22 | // override val mViewModel: EmptyViewModel by viewModels()
23 | //
24 | // private val mCreateFragmentData = mutableListOf()
25 | //
26 | // private var mAdapter: InternalPagerFragmentAdapter? = null
27 | //
28 | //
29 | // override fun HomeActivityInternalLayoutBinding.initView() {
30 | // addFragment.setOnClickListener {
31 | // //添加一个随机页面
32 | // mAdapter?.addData("Pager ID:${Random.nextInt()}")
33 | // }
34 | // removeFragment.setOnClickListener {
35 | // //移除当前展示页面
36 | //// mAdapter?.removeData("更多")
37 | // mAdapter?.removeData(viewPager.currentItem)
38 | // }
39 | // initPager()
40 | // }
41 | //
42 | // private fun initPager() {
43 | // mCreateFragmentData.add("首页")
44 | // mCreateFragmentData.add("我的")
45 | // mCreateFragmentData.add("设置")
46 | // mCreateFragmentData.add("更多")
47 | // mCreateFragmentData.add("动态")
48 | // mAdapter = InternalPagerFragmentAdapter(this, mCreateFragmentData)
49 | // mBinding.viewPager.adapter = mAdapter
50 | //
51 | // }
52 | //
53 | // override fun initObserve() {}
54 | //
55 | // override fun initRequestData() {}
56 | //
57 | // class InternalPagerFragmentAdapter(activity: FragmentActivity, data: MutableList = mutableListOf()) :
58 | // BaseFragmentStateAdapter(activity, data) {
59 | // override fun createFragment(item: String, position: Int): Fragment {
60 | // val bundle = Bundle().apply {
61 | // putString("What", item)
62 | // }
63 | // return when (item) {
64 | // "首页" -> {
65 | // //假装首页
66 | // InternalFragment()
67 | // }
68 | // "我的" -> {
69 | // //假装我的
70 | // InternalFragment()
71 | // }
72 | // "设置" -> {
73 | // //假装设置
74 | // InternalFragment()
75 | // }
76 | // "更多" -> {
77 | // //假装更多
78 | // InternalFragment()
79 | // }
80 | // else -> {
81 | // //另外动态item创建类型
82 | // InternalFragment()
83 | // }
84 | // }.apply {
85 | // //设置传递参数bundle
86 | // arguments = bundle
87 | // }
88 | // }
89 | //
90 | //
91 | // }
92 | //
93 | //}
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/mvvm/v/BaseFrameActivity.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.mvvm.v
2 |
3 | import android.content.res.Resources
4 | import android.os.Bundle
5 | import android.os.Looper
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.viewbinding.ViewBinding
8 | import com.alibaba.android.arouter.launcher.ARouter
9 | import com.quyunshuo.androidbaseframemvvm.base.R
10 | import com.quyunshuo.androidbaseframemvvm.base.mvvm.vm.BaseViewModel
11 | import com.quyunshuo.androidbaseframemvvm.base.utils.*
12 | import com.quyunshuo.androidbaseframemvvm.base.utils.network.AutoRegisterNetListener
13 | import com.quyunshuo.androidbaseframemvvm.base.utils.network.NetworkStateChangeListener
14 | import com.quyunshuo.androidbaseframemvvm.base.utils.network.NetworkTypeEnum
15 | import me.jessyan.autosize.AutoSizeCompat
16 |
17 | /**
18 | * Activity基类
19 | *
20 | * @author Qu Yunshuo
21 | * @since 8/27/20
22 | */
23 | abstract class BaseFrameActivity : AppCompatActivity(),
24 | FrameView, NetworkStateChangeListener {
25 |
26 | protected abstract val mViewModel: VM
27 |
28 | protected val mBinding: VB by lazy(mode = LazyThreadSafetyMode.NONE) { createVB() }
29 |
30 | /**
31 | * 是否有 [RegisterEventBus] 注解,避免重复调用 [Class.isAnnotation]
32 | */
33 | private var mHaveRegisterEventBus = false
34 |
35 | override fun onCreate(savedInstanceState: Bundle?) {
36 | super.onCreate(savedInstanceState)
37 | setContentView(mBinding.root)
38 | // ARouter 依赖注入
39 | ARouter.getInstance().inject(this)
40 |
41 | // 根据子类是否有 RegisterEventBus 注解決定是否进行注册 EventBus
42 | if (javaClass.isAnnotationPresent(RegisterEventBus::class.java)) {
43 | mHaveRegisterEventBus = true
44 | EventBusUtils.register(this)
45 | }
46 |
47 | setStatusBar()
48 | mBinding.initView()
49 | initNetworkListener()
50 | initObserve()
51 | initRequestData()
52 | }
53 |
54 | /**
55 | * 初始化网络状态监听
56 | * @return Unit
57 | */
58 | private fun initNetworkListener() {
59 | lifecycle.addObserver(AutoRegisterNetListener(this))
60 | }
61 |
62 | /**
63 | * 设置状态栏
64 | * 子类需要自定义时重写该方法即可
65 | * @return Unit
66 | */
67 | open fun setStatusBar() {}
68 |
69 | /**
70 | * 网络类型更改回调
71 | * @param type Int 网络类型
72 | * @return Unit
73 | */
74 | override fun networkTypeChange(type: NetworkTypeEnum) {}
75 |
76 | /**
77 | * 网络连接状态更改回调
78 | * @param isConnected Boolean 是否已连接
79 | * @return Unit
80 | */
81 | override fun networkConnectChange(isConnected: Boolean) {
82 | toast(if (isConnected) getString(R.string.base_network_connected) else getString(R.string.base_network_disconnected))
83 | }
84 |
85 | override fun onDestroy() {
86 | // 根据子类是否有 RegisterEventBus 注解决定是否进行注册 EventBus
87 | if (mHaveRegisterEventBus) {
88 | EventBusUtils.unRegister(this)
89 | }
90 | super.onDestroy()
91 | }
92 |
93 | override fun getResources(): Resources {
94 | // 主要是为了解决 AndroidAutoSize 在横屏切换时导致适配失效的问题
95 | // 但是 AutoSizeCompat.autoConvertDensity() 对线程做了判断 导致Coil等图片加载框架在子线程访问的时候会异常
96 | // 所以在这里加了线程的判断 如果是非主线程 就取消单独的适配
97 | if (Looper.myLooper() == Looper.getMainLooper()) {
98 | AutoSizeCompat.autoConvertDensityOfGlobal((super.getResources()))
99 | }
100 | return super.getResources()
101 | }
102 | }
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | //****************************************
2 | //************ app 壳的配置文件 ************
3 | //****************************************
4 |
5 | import com.quyunshuo.androidbaseframemvvm.buildsrc.*
6 |
7 | plugins {
8 | alias(libs.plugins.application)
9 | alias(libs.plugins.kotlin)
10 | alias(libs.plugins.hilt)
11 | id "kotlin-kapt"
12 | }
13 |
14 | android {
15 | namespace 'com.quyunshuo.androidbaseframemvvm'
16 | compileSdk ProjectBuildConfig.compileSdkVersion
17 |
18 | defaultConfig {
19 | applicationId ProjectBuildConfig.applicationId
20 | minSdk ProjectBuildConfig.minSdkVersion
21 | targetSdk ProjectBuildConfig.targetSdkVersion
22 | versionCode ProjectBuildConfig.versionCode
23 | versionName ProjectBuildConfig.versionName
24 |
25 | testInstrumentationRunner DependencyConfig.AndroidX.AndroidJUnitRunner
26 | multiDexKeepProguard file("multidexKeep.pro")
27 |
28 | ndk {
29 | // 设置支持的SO库架构
30 | //abiFilters 'armeabi', 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
31 | abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
32 | }
33 | }
34 |
35 | // signingConfigs {
36 | // releaseConfig {
37 | // storeFile file('')
38 | // storePassword ""
39 | // keyAlias ""
40 | // keyPassword ""
41 | // }
42 | // }
43 |
44 | buildTypes {
45 | // 对应 ALPHA 版本
46 | debug {
47 | buildConfigField "String", "VERSION_TYPE", "\"${ProjectBuildConfig.Version.ALPHA}\""
48 | // signingConfig signingConfigs.releaseConfig
49 | minifyEnabled false
50 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
51 | }
52 | beta {
53 | buildConfigField "String", "VERSION_TYPE", "\"${ProjectBuildConfig.Version.BETA}\""
54 | // signingConfig signingConfigs.releaseConfig
55 | minifyEnabled false
56 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
57 | }
58 | release {
59 | buildConfigField "String", "VERSION_TYPE", "\"${ProjectBuildConfig.Version.RELEASE}\""
60 | // signingConfig signingConfigs.releaseConfig
61 | minifyEnabled false
62 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
63 | }
64 | }
65 |
66 | // 自定义打包apk的文件名
67 | android.applicationVariants.all { variant ->
68 | variant.outputs.all { output ->
69 | if (outputFileName != null && outputFileName.endsWith('.apk')) {
70 | outputFileName = "${ProjectBuildConfig.applicationId}" +
71 | "_${ProjectBuildConfig.versionCode}" +
72 | "(${ProjectBuildConfig.versionName})" +
73 | "_${variant.buildType.name}" +
74 | ".apk"
75 | }
76 | }
77 | }
78 |
79 | compileOptions {
80 | sourceCompatibility JavaVersion.VERSION_17
81 | targetCompatibility JavaVersion.VERSION_17
82 | }
83 |
84 | kotlinOptions {
85 | jvmTarget = '17'
86 | }
87 | }
88 |
89 | dependencies {
90 | implementation fileTree(dir: "libs", include: ["*.jar"])
91 |
92 | if (!ProjectBuildConfig.isAppMode) {
93 | // 有业务组件时 把这个去掉 这里只是为了使用base里的依赖库
94 | implementation project(path: ':module_home')
95 | } else {
96 | implementation project(path: ':lib_common')
97 | }
98 | implementation DependencyConfig.JetPack.HiltCore
99 |
100 | kapt DependencyConfig.GitHub.AutoServiceAnnotations
101 | kapt DependencyConfig.JetPack.HiltApt
102 | }
--------------------------------------------------------------------------------
/lib_common/src/main/java/com/quyunshuo/androidbaseframemvvm/common/CommonApplication.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.common
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Application
5 | import android.content.Context
6 | import android.util.Log
7 | import com.alibaba.android.arouter.launcher.ARouter
8 | import com.google.auto.service.AutoService
9 | import com.quyunshuo.androidbaseframemvvm.base.app.ApplicationLifecycle
10 | import com.quyunshuo.androidbaseframemvvm.base.BaseApplication
11 | import com.quyunshuo.androidbaseframemvvm.base.constant.VersionStatus
12 | import com.quyunshuo.androidbaseframemvvm.base.utils.ForegroundBackgroundObserver
13 | import com.quyunshuo.androidbaseframemvvm.base.utils.ProcessUtils
14 | import com.quyunshuo.androidbaseframemvvm.base.utils.SpUtils
15 | import com.quyunshuo.androidbaseframemvvm.base.utils.network.NetworkStateClient
16 | import com.tencent.bugly.crashreport.CrashReport
17 | import com.tencent.smtt.export.external.TbsCoreSettings
18 | import com.tencent.smtt.sdk.QbSdk
19 | import com.tencent.smtt.sdk.QbSdk.PreInitCallback
20 |
21 | /**
22 | * 项目相关的Application
23 | *
24 | * @author Qu Yunshuo
25 | * @since 4/16/21 3:37 PM
26 | */
27 | @AutoService(ApplicationLifecycle::class)
28 | class CommonApplication : ApplicationLifecycle, ForegroundBackgroundObserver {
29 |
30 | companion object {
31 | // 全局CommonApplication
32 | @SuppressLint("StaticFieldLeak")
33 | lateinit var mCommonApplication: CommonApplication
34 | }
35 |
36 | /**
37 | * 同[Application.attachBaseContext]
38 | * @param context Context
39 | */
40 | override fun onAttachBaseContext(context: Context) {
41 | mCommonApplication = this
42 | }
43 |
44 | /**
45 | * 同[Application.onCreate]
46 | * @param application Application
47 | */
48 | override fun onCreate(application: Application) {}
49 |
50 | /**
51 | * 同[Application.onTerminate]
52 | * @param application Application
53 | */
54 | override fun onTerminate(application: Application) {}
55 |
56 | /**
57 | * 主线程前台初始化
58 | * @return MutableList<() -> String> 初始化方法集合
59 | */
60 | override fun initByFrontDesk(): MutableList<() -> String> {
61 | val list = mutableListOf<() -> String>()
62 | // 以下只需要在主进程当中初始化 按需要调整
63 | if (ProcessUtils.isMainProcess(BaseApplication.context)) {
64 | list.add { initMMKV() }
65 | list.add { initARouter() }
66 | list.add { initNetworkStateClient() }
67 | }
68 | list.add { initTencentBugly() }
69 | return list
70 | }
71 |
72 | /**
73 | * 不需要立即初始化的放在这里进行后台初始化
74 | */
75 | override fun initByBackstage() {
76 | initX5WebViewCore()
77 | }
78 |
79 | /**
80 | * 初始化网络状态监听客户端
81 | * @return Unit
82 | */
83 | private fun initNetworkStateClient(): String {
84 | NetworkStateClient.register()
85 | return "NetworkStateClient -->> init complete"
86 | }
87 |
88 | /**
89 | * 腾讯TBS WebView X5 内核初始化
90 | */
91 | private fun initX5WebViewCore() {
92 | // dex2oat优化方案
93 | val map = HashMap()
94 | map[TbsCoreSettings.TBS_SETTINGS_USE_SPEEDY_CLASSLOADER] = true
95 | map[TbsCoreSettings.TBS_SETTINGS_USE_DEXLOADER_SERVICE] = true
96 | QbSdk.initTbsSettings(map)
97 |
98 | // 允许使用非wifi网络进行下载
99 | QbSdk.setDownloadWithoutWifi(true)
100 |
101 | // 初始化
102 | QbSdk.initX5Environment(BaseApplication.context, object : PreInitCallback {
103 |
104 | override fun onCoreInitFinished() {
105 | Log.d("ApplicationInit", " TBS X5 init finished")
106 | }
107 |
108 | override fun onViewInitFinished(p0: Boolean) {
109 | // 初始化完成的回调,为true表示x5内核加载成功,否则表示x5内核加载失败,会自动切换到系统内核
110 | Log.d("ApplicationInit", " TBS X5 init is $p0")
111 | }
112 | })
113 | }
114 |
115 | /**
116 | * 腾讯 MMKV 初始化
117 | */
118 | private fun initMMKV(): String {
119 | val result = SpUtils.initMMKV(BaseApplication.context)
120 | return "MMKV -->> $result"
121 | }
122 |
123 | /**
124 | * 阿里路由 ARouter 初始化
125 | */
126 | private fun initARouter(): String {
127 | // 测试环境下打开ARouter的日志和调试模式 正式环境需要关闭
128 | if (BuildConfig.VERSION_TYPE != VersionStatus.RELEASE) {
129 | ARouter.openLog() // 打印日志
130 | ARouter.openDebug() // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
131 | }
132 | ARouter.init(BaseApplication.application)
133 | return "ARouter -->> init complete"
134 | }
135 |
136 | /**
137 | * 初始化 腾讯Bugly
138 | * 测试环境应该与正式环境的日志收集渠道分隔开
139 | * 目前有两个渠道 测试版本/正式版本
140 | */
141 | private fun initTencentBugly(): String {
142 | // 第三个参数为SDK调试模式开关
143 | CrashReport.initCrashReport(
144 | BaseApplication.context,
145 | BaseApplication.context.getString(R.string.BUGLY_APP_ID),
146 | BuildConfig.VERSION_TYPE != VersionStatus.RELEASE
147 | )
148 | return "Bugly -->> init complete"
149 | }
150 |
151 | override fun foregroundBackgroundNotify(isForeground: Boolean) {
152 | Log.d("ForegroundBackground", "isForeground: $isForeground")
153 | }
154 | }
--------------------------------------------------------------------------------
/lib_common/src/main/java/com/quyunshuo/androidbaseframemvvm/common/ui/BaseFragmentStateAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.common.ui
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.fragment.app.FragmentActivity
5 | import androidx.recyclerview.widget.DiffUtil
6 | import androidx.recyclerview.widget.RecyclerView
7 | import androidx.viewpager2.adapter.FragmentStateAdapter
8 | import kotlin.random.Random
9 |
10 | /**
11 | * @author DBoy 2021/8/5
12 | * - 文件描述 : ViewPager2 FragmentAdapter封装
13 | * - 对于元数据[mData]的增加删除操作只能通过内部提供的4种方式进行操作:
14 | * - [setNewData]
15 | * - [addNewData]
16 | * - [addData]
17 | * - [removeData]
18 | * - 内部使用[DiffUtil]工具实现更新UI,不需调用[notifyDataSetChanged]等一系列方法。
19 | */
20 | abstract class BaseFragmentStateAdapter : FragmentStateAdapter {
21 |
22 | private val TAG = "FragmentAdapter"
23 |
24 | /**
25 | * 记录生成的Fragment id列表
26 | */
27 | private var mFragmentIdMap = mutableMapOf()
28 |
29 | /**
30 | * 需要生成页面的数据
31 | */
32 | var mData: MutableList
33 | set(value) {
34 | field = value
35 | mFragmentIdMap.clear()
36 | createFragmentsIds(field)
37 | }
38 |
39 | constructor(fragment: Fragment, data: MutableList = mutableListOf()) : super(fragment) {
40 | mData = data
41 | }
42 |
43 | constructor(activity: FragmentActivity, data: MutableList = mutableListOf()) : super(
44 | activity
45 | ) {
46 | mData = data
47 | }
48 |
49 | /**
50 | * 获取需要创建几个Fragment
51 | */
52 | override fun getItemCount(): Int = mData.size
53 |
54 | /**
55 | * 创建Fragment
56 | */
57 | final override fun createFragment(position: Int): Fragment {
58 | return createFragment(mData[position], position)
59 | }
60 |
61 | /**
62 | * 创建fragment 传递数据
63 | */
64 | abstract fun createFragment(item: T, position: Int): Fragment
65 |
66 | /**
67 | * 获取fragment 对应 id
68 | */
69 | override fun getItemId(position: Int): Long {
70 | if (position >= mData.size) return RecyclerView.NO_ID
71 | return mFragmentIdMap[mData[position]] ?: return RecyclerView.NO_ID
72 | }
73 |
74 | /**
75 | * 判断是否包含这个id的数
76 | */
77 | override fun containsItem(itemId: Long): Boolean {
78 | return mFragmentIdMap.values.contains(itemId)
79 | }
80 |
81 | /**
82 | * 设置新数据
83 | */
84 | fun setNewData(data: MutableList = mutableListOf()) {
85 | val oldData = copyData()
86 | mData = data
87 | diffNotifyDataSetChanged(oldData, mData)
88 | }
89 |
90 | /**
91 | * 累加新数据
92 | */
93 | fun addNewData(data: MutableList = mutableListOf()) {
94 | val oldData = copyData()
95 | mData.addAll(data)
96 | //创建新的对应位置的id
97 | createFragmentsIds(data)
98 | diffNotifyDataSetChanged(oldData, mData)
99 |
100 | }
101 |
102 | /**
103 | * 添加数据
104 | */
105 | fun addData(data: T) {
106 | val oldData = copyData()
107 | mData.add(data)
108 | //随机一个id对应当前位置Fragment,两次随机确保同id率为最低概率
109 | mFragmentIdMap[data] = Random.nextLong() - Random.nextInt()
110 | diffNotifyDataSetChanged(oldData, mData)
111 | }
112 |
113 | /**
114 | * 移除某个数据
115 | */
116 | fun removeData(data: T): Boolean {
117 | val oldData = copyData()
118 | if (mData.remove(data)) {
119 | mFragmentIdMap.remove(data)
120 | diffNotifyDataSetChanged(oldData, mData)
121 | return true
122 | }
123 | return false
124 | }
125 |
126 | /**
127 | * 移除某个位置的数据
128 | */
129 | fun removeData(position: Int): Boolean {
130 | if (position < mData.size && position < mFragmentIdMap.size) {
131 | val oldData = copyData()
132 | val removeItem = mData.removeAt(position)
133 | mFragmentIdMap.remove(removeItem)
134 | diffNotifyDataSetChanged(oldData, mData)
135 | return true
136 | }
137 | return false
138 | }
139 |
140 | /**
141 | * 拷贝原数据
142 | */
143 | private fun copyData(): MutableList {
144 | val oldData = mutableListOf()
145 | oldData.addAll(mData)
146 | return oldData
147 | }
148 |
149 | /**
150 | * 使用diff工具更新UI,当前diff工具对比使用的方式是 [==] 所以如果需要精确对比不同item数据,可以重写[T]的[equals]方法.
151 | */
152 | private fun diffNotifyDataSetChanged(oldData: MutableList, newData: MutableList) {
153 | DiffUtil.calculateDiff(object : DiffUtil.Callback() {
154 |
155 | override fun getOldListSize(): Int = oldData.size
156 |
157 | override fun getNewListSize(): Int = newData.size
158 |
159 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = oldData[oldItemPosition] == newData[newItemPosition]
160 |
161 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = oldData[oldItemPosition] == newData[newItemPosition]
162 |
163 | }, true).dispatchUpdatesTo(this)
164 | }
165 |
166 |
167 | /**
168 | * 创建[mData]对应Fragment的id
169 | */
170 | private fun createFragmentsIds(data: MutableList) {
171 | for (item in data) {
172 | mFragmentIdMap[item] = Random.nextLong() - Random.nextInt()
173 | }
174 | }
175 |
176 | }
177 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/ktx/ViewKtx.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.ktx
2 |
3 | import android.animation.Animator
4 | import android.animation.IntEvaluator
5 | import android.animation.ValueAnimator
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import com.quyunshuo.androidbaseframemvvm.base.view.OnSingleClickListener
9 |
10 | /**
11 | * @Author: QuYunShuo
12 | * @Time: 2020/9/1
13 | * @Class: ViewKtx
14 | * @Remark: View相关的扩展方法
15 | */
16 |
17 | /*************************************** View可见性相关 ********************************************/
18 | /**
19 | * 隐藏View
20 | * @receiver View
21 | */
22 | fun View.gone() {
23 | visibility = View.GONE
24 | }
25 |
26 | /**
27 | * 显示View
28 | * @receiver View
29 | */
30 | fun View.visible() {
31 | visibility = View.VISIBLE
32 | }
33 |
34 | /**
35 | * View不可见但存在原位置
36 | * @receiver View
37 | */
38 | fun View.invisible() {
39 | visibility = View.INVISIBLE
40 | }
41 |
42 | /**
43 | * 设置 View 为 [View.VISIBLE]
44 | * 如果 [isVisible] 值为true,将 [View.setVisibility] 设置为 [View.VISIBLE],反之为 [View.GONE]
45 | *
46 | * @receiver View
47 | * @param isVisible Boolean 是否显示
48 | */
49 | fun View.setVisible(isVisible: Boolean) {
50 | if (isVisible) visible() else gone()
51 | }
52 |
53 | /**
54 | * 设置 View 为 [View.GONE]
55 | * 如果 [isGone] 值为true,将 [View.setVisibility] 设置为 [View.GONE],反之为 [View.VISIBLE]
56 | *
57 | * @receiver View
58 | * @param isGone Boolean 是否隐藏
59 | */
60 | fun View.setGone(isGone: Boolean) {
61 | if (isGone) visible() else gone()
62 | }
63 |
64 | /*************************************** View宽高相关 ********************************************/
65 | /**
66 | * 设置 View 的高度
67 | * @receiver View
68 | * @param height Int 目标高度
69 | * @return View
70 | */
71 | fun View.height(height: Int): View {
72 | val params = layoutParams ?: ViewGroup.LayoutParams(
73 | ViewGroup.LayoutParams.MATCH_PARENT,
74 | ViewGroup.LayoutParams.WRAP_CONTENT
75 | )
76 | params.height = height
77 | layoutParams = params
78 | return this
79 | }
80 |
81 | /**
82 | * 设置View的宽度
83 | * @receiver View
84 | * @param width Int 目标宽度
85 | * @return View
86 | */
87 | fun View.width(width: Int): View {
88 | val params = layoutParams ?: ViewGroup.LayoutParams(
89 | ViewGroup.LayoutParams.MATCH_PARENT,
90 | ViewGroup.LayoutParams.WRAP_CONTENT
91 | )
92 | params.width = width
93 | layoutParams = params
94 | return this
95 | }
96 |
97 | /**
98 | * 设置View的宽度和高度
99 | * @receiver View
100 | * @param width Int 要设置的宽度
101 | * @param height Int 要设置的高度
102 | * @return View
103 | */
104 | fun View.widthAndHeight(width: Int, height: Int): View {
105 | val params = layoutParams ?: ViewGroup.LayoutParams(
106 | ViewGroup.LayoutParams.MATCH_PARENT,
107 | ViewGroup.LayoutParams.WRAP_CONTENT
108 | )
109 | params.width = width
110 | params.height = height
111 | layoutParams = params
112 | return this
113 | }
114 |
115 | /**
116 | * 设置宽度,带有过渡动画
117 | * @param targetValue 目标宽度
118 | * @param duration 时长
119 | * @param action 可选行为
120 | * @return 动画
121 | */
122 | fun View.animateWidth(
123 | targetValue: Int, duration: Long = 400, listener: Animator.AnimatorListener? = null,
124 | action: ((Float) -> Unit)? = null
125 | ): ValueAnimator? {
126 | var animator: ValueAnimator? = null
127 | post {
128 | animator = ValueAnimator.ofInt(width, targetValue).apply {
129 | addUpdateListener {
130 | width(it.animatedValue as Int)
131 | action?.invoke((it.animatedFraction))
132 | }
133 | if (listener != null) addListener(listener)
134 | setDuration(duration)
135 | start()
136 | }
137 | }
138 | return animator
139 | }
140 |
141 | /**
142 | * 设置高度,带有过渡动画
143 | * @param targetValue 目标高度
144 | * @param duration 时长
145 | * @param action 可选行为
146 | * @return 动画
147 | */
148 | fun View.animateHeight(
149 | targetValue: Int,
150 | duration: Long = 400,
151 | listener: Animator.AnimatorListener? = null,
152 | action: ((Float) -> Unit)? = null
153 | ): ValueAnimator? {
154 | var animator: ValueAnimator? = null
155 | post {
156 | animator = ValueAnimator.ofInt(height, targetValue).apply {
157 | addUpdateListener {
158 | height(it.animatedValue as Int)
159 | action?.invoke((it.animatedFraction))
160 | }
161 | if (listener != null) addListener(listener)
162 | setDuration(duration)
163 | start()
164 | }
165 | }
166 | return animator
167 | }
168 |
169 | /**
170 | * 设置宽度和高度,带有过渡动画
171 | * @param targetWidth 目标宽度
172 | * @param targetHeight 目标高度
173 | * @param duration 时长
174 | * @param action 可选行为
175 | * @return 动画
176 | */
177 | fun View.animateWidthAndHeight(
178 | targetWidth: Int,
179 | targetHeight: Int,
180 | duration: Long = 400,
181 | listener: Animator.AnimatorListener? = null,
182 | action: ((Float) -> Unit)? = null
183 | ): ValueAnimator? {
184 | var animator: ValueAnimator? = null
185 | post {
186 | val startHeight = height
187 | val evaluator = IntEvaluator()
188 | animator = ValueAnimator.ofInt(width, targetWidth).apply {
189 | addUpdateListener {
190 | widthAndHeight(
191 | it.animatedValue as Int,
192 | evaluator.evaluate(it.animatedFraction, startHeight, targetHeight)
193 | )
194 | action?.invoke((it.animatedFraction))
195 | }
196 | if (listener != null) addListener(listener)
197 | setDuration(duration)
198 | start()
199 | }
200 | }
201 | return animator
202 | }
203 |
204 | /*************************************** View其他 ********************************************/
205 | /**
206 | * 获取View id
207 | */
208 | fun View.getViewId(): Int {
209 | var id = id
210 | if (id == View.NO_ID) {
211 | id = View.generateViewId()
212 | }
213 | return id
214 | }
215 |
216 | /**
217 | * 给 [View] 设置带有防抖效果的点击事件
218 | *
219 | * @receiver [View]
220 | * @param delayTime Int 防抖间隔时间,单位是毫秒,默认值 500ms
221 | * @param listener (v: View) -> Unit 具体的点击事件
222 | * @see OnSingleClickListener
223 | */
224 | fun View.setOnSingleClickListener(delayTime: Int = 500, listener: (v: View) -> Unit) {
225 | setOnClickListener(OnSingleClickListener(delayTime, listener))
226 | }
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/DateUtils.kt:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils
2 |
3 | import java.text.ParseException
4 | import java.text.SimpleDateFormat
5 | import java.util.*
6 |
7 | /**
8 | * 时间工具类
9 | * ________________________________________________________________________________________
10 | * |字母 |日期或时间元素 | 表示 | 示例 |
11 | * |:--:|:--------------------:|:-----------------:|:------------------------------------:|
12 | * |G |Era 标志符 | Text | AD |
13 | * |y |年 | Year | 1996; 96 |
14 | * |M |年中的月份 | Month | July; Jul; 07 |
15 | * |w |年中的周数 | Number | 27 |
16 | * |W |月份中的周数 | Number | 2 |
17 | * |D |年中的天数 | Number | 189 |
18 | * |d |月份中的天数 | Number | 10 |
19 | * |F |月份中的星期 | Number | 2 |
20 | * |E |星期中的天数 | Text | Tuesday; Tue |
21 | * |a |Am/pm 标记 | Text | PM |
22 | * |H |一天中的小时数(0-23) | Number | 0 |
23 | * |k |一天中的小时数(1-24) | Number | 24 |
24 | * |K |am/pm 中的小时数(0-11) | Number | 0 |
25 | * |h |am/pm 中的小时数(1-12) | Number | 12 |
26 | * |m |小时中的分钟数 | Number | 30 |
27 | * |s |分钟中的秒数 | Number | 55 |
28 | * |S |毫秒数 | Number | 978 |
29 | * |z |时区 | General time zone | Pacific Standard Time; PST; GMT-08:00|
30 | * |Z |时区 | RFC 822 time zone | -0800 |
31 | *  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
32 | * @author Qu Yunshuo
33 | * @since 2020/9/8
34 | */
35 | object DateUtils {
36 |
37 | /**
38 | * 获取时间格式化String
39 | * @param timestamp 时间戳
40 | * @param dateFormat 日期格式
41 | * @return 格式化后的字符串
42 | */
43 | fun getDateFormatString(timestamp: Long, dateFormat: String): String =
44 | SimpleDateFormat(dateFormat, Locale.CHINESE).format(Date(timestamp))
45 |
46 | /**
47 | * 将固定格式[dateFormat]的时间字符串[dateString]转换为时间值
48 | */
49 | fun getDateStringToDate(dateString: String, dateFormat: String): Long? {
50 | val simpleDateFormat = SimpleDateFormat(dateFormat, Locale.CHINESE)
51 | var date: Date? = null
52 | try {
53 | date = simpleDateFormat.parse(dateString)
54 | } catch (e: ParseException) {
55 | e.printStackTrace()
56 | }
57 | return date?.time
58 | }
59 |
60 | /**
61 | * 将计时毫秒值[millisecond]转换为时分秒
62 | */
63 | fun getGapTime(millisecond: Long): String {
64 | val hours = millisecond / (1000 * 60 * 60)
65 | val minutes = (millisecond - hours * (1000 * 60 * 60)) / (1000 * 60)
66 | val second = (millisecond - hours * (1000 * 60 * 60) - minutes * (1000 * 60)) / 1000
67 | var diffTime: String
68 | diffTime = if (minutes < 10) {
69 | "$hours:0$minutes"
70 | } else {
71 | "$hours:$minutes"
72 | }
73 | diffTime = if (second < 10) {
74 | "$diffTime:0$second"
75 | } else {
76 | "$diffTime:$second"
77 | }
78 | return diffTime
79 | }
80 |
81 | /**
82 | * 获取以当前日期为基准的某一时间段的日期
83 | * @param isFuture Boolean 真为未来时间 假为以前的时间
84 | * @param interval Int 间隔时间 以当前时间为基准 距今天前n天或后n天开始 就是n 0是当前日期
85 | * @param size String 时间区间长度 比如获取五天的时间 就是5 当前日期也算一天
86 | * @return List 日期集合 顺序为日期的新旧程度
87 | * @throws RuntimeException 如果[interval]小于0或者[size]小于1会抛出[RuntimeException]
88 | *
89 | * 示例:获取后天开始 为期七天的时间就是 getExcerptDate(true, 2, 7)
90 | * 获取昨天开始再往前7天的时间 getExcerptDate(false, 1, 7)
91 | */
92 | fun getExcerptDate(
93 | isFuture: Boolean,
94 | interval: Int,
95 | size: Int,
96 | dateFormat: String
97 | ): List {
98 | if (interval < 0) throw RuntimeException("\"interval\" it can't be less than 0")
99 | if (size < 1) throw RuntimeException("\"size\" it can't be less than 1")
100 | val simpleDateFormat = SimpleDateFormat(dateFormat, Locale.CHINESE)
101 | val calendar = Calendar.getInstance()
102 | val currentDayOfYear = calendar.get(Calendar.DAY_OF_YEAR)
103 | val currentYear = calendar.get(Calendar.YEAR)
104 | val dateList = mutableListOf()
105 | if (isFuture) {
106 | (interval until interval + size).forEach {
107 | val timestamp = getSomedayDate(it, calendar, currentDayOfYear, currentYear)
108 | dateList.add(simpleDateFormat.format(timestamp))
109 | }
110 | } else {
111 | (-interval downTo -interval - size + 1).forEach {
112 | val timestamp = getSomedayDate(it, calendar, currentDayOfYear, currentYear)
113 | dateList.add(simpleDateFormat.format(timestamp))
114 | }
115 | }
116 | return dateList
117 | }
118 |
119 | /**
120 | * 获取距离今天的某一天的时间戳
121 | * @param numberOfDaysBetween Int 间隔今天的天数 正数为未来时间 负数为以前的时间
122 | * @param calendar Calendar Calendar对象 使用依赖注入方式 提高对象的复用性
123 | * @param currentDayOfYear Int 当前时间在当年的天 使用Calendar获取
124 | * @param currentYear Int 当前年 使用Calendar获取
125 | * @return Long 时间戳
126 | */
127 | fun getSomedayDate(
128 | numberOfDaysBetween: Int,
129 | calendar: Calendar,
130 | currentDayOfYear: Int,
131 | currentYear: Int
132 | ): Long {
133 | calendar.set(Calendar.DAY_OF_YEAR, currentDayOfYear)
134 | calendar.set(Calendar.YEAR, currentYear)
135 | calendar.set(
136 | Calendar.DAY_OF_YEAR,
137 | calendar.get(Calendar.DAY_OF_YEAR) + numberOfDaysBetween
138 | )
139 | return calendar.time.time
140 | }
141 |
142 | /**
143 | * String 转化 Calendar
144 | * @param string String
145 | * @param format String
146 | */
147 | fun stringToCalendar(string: String, format: String): Calendar? {
148 | val sdf = SimpleDateFormat(format, Locale.CHINESE)
149 | var calendar: Calendar
150 | try {
151 | val date: Date = sdf.parse(string) ?: return null
152 | calendar = Calendar.getInstance()
153 | calendar.time = date
154 | } catch (e: ParseException) {
155 | e.printStackTrace()
156 | calendar = Calendar.getInstance()
157 | }
158 | return calendar
159 | }
160 |
161 | /**
162 | * String 转化Date
163 | * @param str String
164 | * @param format String
165 | * @return Date
166 | */
167 | fun strToDate(str: String, format: String): Date? {
168 | val sdf = SimpleDateFormat(format, Locale.CHINESE)
169 | return try {
170 | sdf.parse(str)
171 | } catch (e: ParseException) {
172 | e.printStackTrace()
173 | null
174 | }
175 | }
176 |
177 | /**
178 | * 判断两个时间是否是同一天
179 | * @param cal1 Calendar
180 | * @param cal2 Calendar
181 | * @return Boolean
182 | */
183 | fun isSameDay(cal1: Calendar, cal2: Calendar): Boolean {
184 | return cal1[0] == cal2[0] && cal1[1] == cal2[1] && cal1[6] == cal2[6]
185 | }
186 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AndroidBaseFrameMVVM 🐽
2 |
3 | > **AndroidBaseFrameMVVM** 是一个Android工程框架,所使用技术栈为:**组件化、Kotlin、MVVM、Jetpack、Repository、Kotlin-Coroutine-Flow**,本框架既是一个可以开箱即用的工程框架基础层,也是一个很好的学习资源,文档下面会对框架中所使用的一些核心技术进行阐述。该框架作为个人技术积累的产物,会一直更新维护,如果有技术方面的谈论或者框架中的错误点,可以在 **GitHub** 上提 **Issues**,我会及时进行回应。希望这个框架项目能给大家带来帮助,喜欢可以Start🌟。
4 | >
5 | > 项目地址:[AndroidBaseFrameMVVM](https://github.com/Quyunshuo/AndroidBaseFrameMVVM)
6 |
7 | ## Demo
8 |
9 | 以鸿洋大神的玩安卓开放Api做了简单的页面示例,仓库地址:[WanAndroidMVVM](https://github.com/Quyunshuo/WanAndroidMVVM)
10 |
11 | ## 框架图示
12 |
13 | **谷歌 Android 团队 Jetpack 视图模型:**
14 |
15 |
16 |
17 | ## 模块
18 |
19 | - **app:**
20 |
21 | **app壳** 工程,是依赖所有组件的壳,该模块不应该包含任何代码,它只作为一个空壳存在,由于项目中使用了EventBusAPT技术,需要索引到各业务组件的对应的APT生成类,所以在 **app壳** 内有这一部分的代码。
22 |
23 |
24 | - **buildSrc:**
25 |
26 | 这是一个特殊的文件夹,负责项目的构建,里面存放着一些项目构建时用到的东西,比如项目配置,依赖。这里面还是存放 **Gradle** 插件的地方,一些自定义的 **Gradle** 的插件都需要放在此处。
27 |
28 | - **lib_base:**
29 |
30 | 项目的基础公共模块,存放着各种基类封装、对远程库的依赖、以及工具类、三方库封装,该组件是和项目业务无关的,和项目业务相关的公共部分需要放在 **lib_common** 中。
31 |
32 | - **lib_common:**
33 |
34 | 项目的业务公共模块,这里面存放着项目里各个业务组件的公共部分,还有一些项目特定需要的一些文件等,该组件是和项目业务有关系的。
35 |
36 | ## 组件化相关
37 |
38 | ### 组件初始化
39 |
40 | > 为了更好的代码隔离与解耦,在特定组件内使用的SDK及三方库,应该只在该组件内依赖,不应该让该组件的特定SDK及三方库的API暴露给其他不需要用的组件。有一个问题就出现了,SDK及三方库常常需要手动去初始化,而且一般都需要在项目一启动(即 **Application** 中)初始化,但是一个项目肯定只能有一个自定义的 **Application**,该项目中的自定义 **Application** 在 **lib_base** 模块中,并且也是在 **lib_base** 模块中的清单文件中声明的,那其他组件该如何初始化呢?带着这个问题我们一起来深入研究下。
41 |
42 | **常见的组件初始化解决方案:**
43 |
44 | 在我的了解范围内,目前有两种最为常见的解决方案:
45 |
46 | - **面向接口编程 + 反射扫描实现类:**
47 |
48 | 该方案是基于接口编程,自定义 **Application** 去实现一个自定义的接口(**interface**),这个接口中定一些和 **Application** 生命周期相对应的抽象方法及其他自定义的抽象方法,每个组件去编写一个实现类,该实现类就类似于一个假的自定义 **Application**,然后在真正的自定义 **Application** 中去通过反射去动态查找当前运行时环境中所有该接口的实现类,并且去进行实例化,然后将这些实现类收集到一个集合中,在 **Application** 的对应声明周期方法中去逐一调用对应方法,以实现各实现类能够和 **Application** 生命周期相同步,并且持有 **Application** 的引用及 **context** 上下文对象,这样我们就可以在组件内模拟 **Application** 的生命周期并初始化SDK和三方库。使用反射还需要做一些异常的处理。该方案是我见过的最常见的方案,在一些商业项目中也见到过。
49 |
50 | - **面向接口编程 + meta-data + 反射:**
51 |
52 | 该方案的后半部分也是和第一种方法一样,通过接口编程实现 **Application** 的生命周期同步,其实这一步是避免不了的,在我的方案中,后半部分也是这样实现的。不同的是前半部分,也就是如何找到接口的实现类,该方案使用的是 **AndroidManifest** 的 **meta-data** 标签,通过每个组件内的 **AndroidManifest** 内去声明一个 **meta-data** 标签,包含该组件实现类的信息,然后在 **Application** 中去找到这些配置信息,然后通过反射去创建这些实现类的实例,再将它们收集到一个集合中,剩下的操作基本相同了。该方案和第一种方案一样都需要处理很多的异常。这种方案我在一些开源项目中见到过,个人认为过于繁琐,还要处理很多的异常。
53 |
54 | **本项目中所使用的方案:**
55 |
56 | - **面向接口编程 + Java的SPI机制(ServiceLoader)+AutoService:**
57 |
58 | 先来认识下 **Java** 的 **SPI** 机制:面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。**JavaSPI** 就是提供这样的一个机制:为某个接口寻找服务实现的机制。这有点类似 **IOC** 的思想,将装配的控制权移到了程序之外。这段话也是我复制的别人的,听起来很懵逼,大致意思就是我们可以通过 **SPI** 机制将实现类暴露出去。关于如何使用 **SPI**,这里不在陈述,总之是我们在各组件内通过 **SPI** 去将实现类暴露出去,在 **Application** 中我们通过 **Java** 提供的 **SPI** **API** 去获取这些暴露的服务,这样我们就拿到了这些类的实例,剩下的步骤就和上面的方案一样了,通过一个集合遍历实现类调用其相应的方法完成初始化的工作。由于使用 **SPI** 需要在每个模块创建对应的文件配置,这比较麻烦,所以我们使用 **Google** 的 **AutoService** 库来帮助我们自动创建这些配置文件,使用方式也非常的简单,就是在实现类添加一个 **AutoService** 注解。本框架中的核心类是这几个:**lib_base-LoadModuleProxy**、**lib_base-ApplicationLifecycle**。这种方案是我请教的一个米哈游的大佬,这位大佬告诉我在组件化中组件的初始化可以使用 **ServiceLoader** 来做,于是我就去研究了下,最后发现这种方案还不错,比前面提到的两种方案都要简单、安全。
59 |
60 | ### 资源命名冲突
61 |
62 | 在组件化方案中,资源命名冲突是一个比较严重的问题,由于在打包时会进行资源的合并,如果两个模块中有两个相同名字的文件,那么最后只会保留一份,如果不知道这个问题的小伙伴,在遇到这个问题时肯定是一脸懵逼的状态。问题既然已经出现,那我们就要去解决,解决办法就是每个组件都用固定的命名前缀,这样就不会出现两个相同的文件的现象了,我们可以在 **build.gradle** 配置文件中去配置前缀限定,如果不按该前缀进行命名,**AS** 就会进行警告提示,配置如下:
63 |
64 | ```Groovy
65 | android {
66 | resourcePrefix "前缀_"
67 | }
68 | ```
69 |
70 | ### 组件划分
71 |
72 | 其实组件的划分一直是一个比较难的部分,这里其实也给不到一些非常适合的建议,看是看具体项目而定。
73 |
74 | 关于基础组件通常要以独立可直接复用的角度出现,比如网络模块、二维码识别模块等。
75 |
76 | 关于业务组件,业务组件一般可以进行单独调试,也就是可以作为 **app** 运行,这样才能发挥组件化的一大用处,当项目越来越大,业务组件越来越多时,编译耗时将会是一个非常棘手的问题,但是如果每个业务模块都可以进行的单独调试,那就大大减少了编译时间,同时,开发人员也不需要关注其他组件。
77 |
78 | 关于公共模块,**lib_base** 放一些基础性代码,属于框架基础层,不应该和项目业务有牵扯,而和项目业务相关的公共部分则应该放在 **lib_common** 中,不要污染 **lib_base**。
79 |
80 | ### 依赖版本控制
81 |
82 | 组件化常见的一个问题就是依赖版本,每个组件都有可能自己的依赖库,那我们应该统一管理各种依赖库及其版本,使项目所有使用的依赖都是同一个版本,而不是不同版本。本项目中使用 **buildSrc** 中的几个kt文件进行依赖版本统一性的管理,及其项目的一些配置。
83 |
84 | ## **MVVM相关**
85 |
86 | * **MVVM** 采用 **Jetpack** 组件 + **Repository** 设计模式 实现,所使用的 **Jetpack** 并不是很多,像 **DataBinding**、**Paging 3**、**Room** 等并没有使用,如果需要可以添加。采用架构模式目的就是为了解偶代码,对代码进行分层,各模块各司其职,所以既然使用了架构模式那就要遵守好规范。
87 | * **Repository** 仓库层负责数据的提供,**ViewModel** 无需关心数据的来源,**Repository** 内避免使用 **LiveData**,框架里使用了 **Kotlin** 协程的 **Flow** 进行处理请求或访问数据库,**Repository** 的函数会返回一个 **Flow** 给 **ViewModel** 的调用函数,**Flow** 上游负责提供数据,下游也就是 **ViewModel** 获取到数据使用 **LiveData** 进行存储,**View** 层订阅 **LiveData**,实现数据驱动视图
88 | * 三者的依赖都是单向依赖,**View** -> **ViewModel** -> **Repository**
89 |
90 | ## 项目使用的三方库及其简单示例和资料
91 |
92 | * [Kotlin](https://github.com/JetBrains/kotlin)
93 | * [Kotlin-Coroutines-Flow](https://github.com/JetBrains/kotlin)
94 | * [Lifecycle](https://developer.android.com/jetpack/androidx/releases/lifecycle)
95 | * [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel)
96 | * [LiveData](https://developer.android.com/topic/libraries/architecture/livedata)
97 | * [ViewBinding](https://developer.android.com/topic/libraries/view-binding)
98 | * [Hilt](https://developer.android.com/jetpack/androidx/releases/hilt)
99 | * [OkHttp](https://github.com/square/okhttp):网络请求
100 | * [Retrofit](https://github.com/square/retrofit):网络请求
101 | * [MMKV](https://github.com/Tencent/MMKV):腾讯基于 **mmap** 内存映射的 **key-value** 本地存储组件
102 | * [Coil](https://github.com/coil-kt/coil):一个 Android 图片加载库,通过 Kotlin 协程的方式加载图片
103 | * [ARoute](https://github.com/alibaba/ARouter):阿里用于帮助 **Android App** 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦
104 | * [BaseRecyclerViewAdapterHelper](https://github.com/CymChad/BaseRecyclerViewAdapterHelper):一个强大并且灵活的 **RecyclerViewAdapter**
105 | * [EventBus](https://github.com/greenrobot/EventBus):适用于 **Android** 和 **Java** 的发布/订阅事件总线
106 | * [Bugly](https://bugly.qq.com/v2/index):腾讯异常上报及热更新(只集成了异常上报)
107 | * [PermissionX](https://github.com/guolindev/PermissionX):郭霖权限请求框架
108 | * [LeakCanary](https://github.com/square/leakcanary):**Android** 的内存泄漏检测库
109 | * [AndroidAutoSize](https://github.com/JessYanCoding/AndroidAutoSize):**JessYan** 大佬的 今日头条屏幕适配方案终极版
110 |
111 | ### **Kotlin协程**
112 |
113 | 关于 **Kotlin 协程**,是真的香,具体教程可以看我的一篇文章:
114 |
115 | - [万字长文 - Kotlin 协程进阶](https://juejin.cn/post/6950616789390721037)
116 |
117 | **Flow** 类似于 **RxJava**,它也有一系列的操作符,资料:
118 |
119 | - [Google 推荐在 MVVM 架构中使用 Kotlin Flow: ](https://juejin.im/post/6854573211930066951)
120 | - [即学即用Kotlin - 协程:](https://juejin.im/post/6854573211418361864)
121 | - [Kotlin Coroutines Flow 系列(1-5):](https://juejin.im/post/6844904057530908679)
122 |
123 | ### **PermissionX**
124 |
125 | **PermissionX** 是郭霖的一个权限申请框架
126 | **使用方式:**
127 |
128 | ```
129 | PermissionX.init(this)
130 | .permissions("需要申请的权限")
131 | .request { allGranted, grantedList, deniedList -> }
132 | ```
133 |
134 | **资料:**
135 |
136 | GitHub: [https://github.com/guolindev/PermissionX](https://github.com/guolindev/PermissionX)
137 |
138 | ### EventBus APT
139 |
140 | 事件总线这里选择的还是 **EventBus**,也有很多比较新的事件总线框架,还是选择了这个直接上手的
141 | 在框架内我对 **EventBus** 进行了基类封装,自动注册和解除注册,在需要注册的类上添加 **@EventBusRegister** 注解即可,无需关心内存泄漏及没及时解除注册的情况,基类里已经做了处理
142 |
143 | ```kotlin
144 | @EventBusRegister
145 | class MainActivity : AppCompatActivity() {}
146 | ```
147 |
148 | 很多使用 **EventBus** 的开发者其实都没有发现 **APT** 的功能,这是 **EventBus3.0** 的重大更新,使用 **EventBus APT** 可以在编译期生成订阅类,这样就可以避免使用低效率的反射,很多人不知道这个更新,用着**3.0**的版本,实际上却是**2.0**的效率。
149 | 项目中已经在各模块中开启了 **EventBus APT**,**EventBus** 会在编译器对各模块生成订阅类,需要我们手动编写代码去注册这些订阅类:
150 |
151 | ```kotlin
152 | // 在APP壳的AppApplication类中
153 | EventBus
154 | .builder()
155 | .addIndex("各模块生成的订阅类的实例 类名在base_module.gradle脚本中进行了设置 比如 module_home 生成的订阅类就是 module_homeIndex")
156 | .installDefaultEventBus()
157 | ```
158 |
159 | ### 屏幕适配 AndroidAutoSize
160 |
161 | 屏幕适配使用的是 **JessYan** 大佬的 今日头条屏幕适配方案终极版
162 |
163 | GitHub: [https://github.com/JessYanCoding/AndroidAutoSize](https://github.com/JessYanCoding/AndroidAutoSize)
164 |
165 | **使用方式:**
166 |
167 | ```
168 | // 在清单文件中声明
169 |
170 |
171 | // 主单位使用dp 没设置副单位
172 |
175 |
178 |
179 |
180 |
181 | // 默认是以竖屏的宽度为基准进行适配
182 | // 如果是横屏项目要适配Pad(Pad适配尽量使用两套布局 因为手机和Pad屏幕宽比差距很大 无法完美适配)
183 |
184 |
185 | // 以高度为基准进行适配 (还需要手动代码设置以高度为基准进行适配) 目前以高度适配比宽度为基准适配 效果要好
186 |
189 |
190 |
191 |
192 | // 在Application 中设置
193 | // 屏幕适配 AndroidAutoSize 以横屏高度为基准进行适配
194 | AutoSizeConfig.getInstance().isBaseOnWidth = false
195 | ```
196 |
197 | ### ARoute
198 |
199 | **ARoute** 是阿里巴巴的一个用于帮助 **Android App** 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦
200 |
201 | **使用方式:**
202 |
203 | ```
204 | // 1.在需要进行路由跳转的Activity或Fragment上添加 @Route 注解
205 | @Route(path = "/test/activity")
206 | public class YourActivity extend Activity {
207 | ...
208 | }
209 |
210 | // 2.发起路由跳转
211 | ARouter.getInstance()
212 | .build("目标路由地址")
213 | .navigation()
214 |
215 | // 3.携带参数跳转
216 | ARouter.getInstance()
217 | .build("目标路由地址")
218 | .withLong("key1", 666L)
219 | .withString("key3", "888")
220 | .withObject("key4", new Test("Jack", "Rose"))
221 | .navigation()
222 |
223 | // 4.接收参数
224 | @Route(path = RouteUrl.MainActivity2)
225 | class MainActivity : AppCompatActivity() {
226 |
227 | // 通过name来映射URL中的不同参数
228 | @Autowired(name = "key")
229 | lateinit var name: String
230 |
231 | override fun onCreate(savedInstanceState: Bundle?) {
232 | super.onCreate(savedInstanceState)
233 | setContentView(mBinding.root)
234 | // ARouter 依赖注入 ARouter会自动对字段进行赋值,无需主动获取
235 | ARouter.getInstance().inject(this)
236 | }
237 | }
238 |
239 | // 5.获取Fragment
240 | Fragment fragment = (Fragment) ARouter.getInstance().build("/test/fragment").navigation();
241 | ```
242 |
243 | **资料:**
244 |
245 | 官方文档:[https://github.com/alibaba/ARouter](https://github.com/alibaba/ARouter)
246 |
247 | ### ViewBinding
248 |
249 | 通过视图绑定功能,可以更轻松地编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个 **XML** 布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有 **ID** 的所有视图的直接引用。
250 | 在大多数情况下,视图绑定会替代 **findViewById**
251 |
252 | **使用方式:**
253 |
254 | 按模块启用**ViewBinding**
255 |
256 | ```groovy
257 | // 模块下的build.gradle文件
258 | android {
259 | // 开启ViewBinding
260 | // 高版本AS
261 | buildFeatures {
262 | viewBinding = true
263 | }
264 | // 低版本AS 最低3.6
265 | viewBinding {
266 | enabled = true
267 | }
268 | }
269 | ```
270 |
271 | **Activity** 中 **ViewBinding** 的使用
272 |
273 | ```kotlin
274 | // 之前设置视图的方法
275 | setContentView(R.layout.activity_main)
276 |
277 | // 使用ViewBinding后的方法
278 | val mBinding = ActivityMainBinding.inflate(layoutInflater)
279 | setContentView(mBinding.root)
280 |
281 | // ActivityMainBinding类是根据布局自动生成的 如果没有请先build一下项目
282 | // ViewBinding会将控件id转换为小驼峰命名法,所以为了保持一致规范,在xml里声明id时也请使用小驼峰命名法
283 | // 比如你有一个id为mText的控件,可以这样使用
284 | mBinding.mText.text = "ViewBinding"
285 | ```
286 |
287 | **Fragment** 中 **ViewBinding** 的使用
288 |
289 | ```kotlin
290 | // 原来的写法
291 | return inflater.inflate(R.layout.fragment_blank, container, false)
292 |
293 | // 使用ViewBinding的写法
294 | mBinding = FragmentBlankBinding.inflate(inflater)
295 | return mBinding.root
296 | ```
297 |
298 | **资料:**
299 |
300 | 官方文档: [https://developer.android.com/topic/libraries/view-binding](https://developer.android.com/topic/libraries/view-binding)
301 |
302 | CSDN: [https://blog.csdn.net/u010976213/article/details/104501830?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-5&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-5](https://blog.csdn.net/u010976213/article/details/104501830?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-5&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-5)
303 |
304 | ### ViewModel
305 |
306 | **ViewModel** 类旨在以注重生命周期的方式存储和管理界面相关的数据。**ViewModel** 类让数据可在发生屏幕旋转等配置更改后继续留存。
307 |
308 | **使用方式:**
309 |
310 | ```kotlin
311 | class MainViewModel : ViewModel(){}
312 |
313 | class MainActivity : AppCompatActivity() {
314 | // 获取无参构造的ViewModel实例
315 | val mViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
316 | }
317 | ```
318 |
319 | **资料:**
320 |
321 | 官方文档: [https://developer.android.com/topic/libraries/architecture/viewmodel](https://developer.android.com/topic/libraries/architecture/viewmodel)
322 |
323 | Android ViewModel,再学不会你砍我: [https://juejin.im/post/6844903919064186888](https://juejin.im/post/6844903919064186888)
324 |
325 | ### LiveData
326 |
327 | **LiveData** 是一种可观察的数据存储器类。与常规的可观察类不同,**LiveData** 具有生命周期感知能力,意指它遵循其他应用组件(如 **Activity**、**Fragment** 或 **Service**)的生命周期。这种感知能力可确保 **LiveData** 仅更新处于活跃生命周期状态的应用组件观察者
328 |
329 | **LiveData** 分为可变值的 **MutableLiveData** 和不可变值的 **LiveData**
330 |
331 | **常用方法:**
332 |
333 | ```kotlin
334 | fun test() {
335 | val liveData = MutableLiveData()
336 | // 设置更新数据源
337 | liveData.value = "LiveData"
338 | // 将任务发布到主线程以设置给定值
339 | liveData.postValue("LiveData")
340 | // 获取值
341 | val value = liveData.value
342 | // 观察数据源更改(第一个参数应是owner:LifecycleOwner 比如实现了LifecycleOwner接口的Activity)
343 | liveData.observe(this, {
344 | // 数据源更改后触发的逻辑
345 | })
346 | }
347 | ```
348 |
349 | **资料:**
350 |
351 | 官方文档: [https://developer.android.com/topic/libraries/architecture/livedata](https://developer.android.com/topic/libraries/architecture/livedata)
352 |
353 | ### Lifecycle
354 |
355 | **Lifecycle** 是一个类,用于存储有关组件(如 **Activity** 或 **Fragment**)的生命周期状态的信息,并允许其他对象观察此状态。**LifecycleOwner** 是单一方法接口,表示类具有 **Lifecycle**。它具有一种方法(即 **getLifecycle()**),该方法必须由类实现。实现 **LifecycleObserver** 的组件可与实现 **LifecycleOwner** 的组件无缝协同工作,因为所有者可以提供生命周期,而观察者可以注册以观察生命周期。
356 |
357 | **资料:**
358 |
359 | 官方文档: [https://developer.android.com/topic/libraries/architecture/lifecycle](https://developer.android.com/topic/libraries/architecture/lifecycle)
360 |
361 | ### Hilt
362 |
363 | **Hilt** 是 **Android** 的依赖项注入库,可减少在项目中执行手动依赖项注入的样板代码。执行手动依赖项注入要求您手动构造每个类及其依赖项,并借助容器重复使用和管理依赖项。
364 |
365 | **Hilt** 通过为项目中的每个 **Android** 类提供容器并自动管理其生命周期,提供了一种在应用中使用 **DI(依赖项注入)**的标准方法。**Hilt** 在热门 **DI** 库 **Dagger** 的基础上构建而成,因而能够受益于 **Dagger** 的编译时正确性、运行时性能、可伸缩性和 **Android Studio** 支持。
366 |
367 | **资料:**
368 |
369 | 目前官方文档还没有更新正式版的,还是 **alpha** 版本的文档:[使用 Hilt 实现依赖项注入](https://developer.android.com/training/dependency-injection/hilt-android)
370 |
371 | **Dagger** 的 **Hilt** 文档目前是最新的:[Dagger-Hilt](https://dagger.dev/hilt/)
372 |
373 | ### Coil
374 |
375 | **Coil** 是一个 Android 图片加载库,通过 Kotlin 协程的方式加载图片。特点如下:
376 |
377 | - **更快**: Coil 在性能上有很多优化,包括内存缓存和磁盘缓存,把缩略图存保存在内存中,循环利用 bitmap,自动暂停和取消图片网络请求等。
378 | - **更轻量级**: Coil 只有2000个方法(前提是你的 APP 里面集成了 OkHttp 和 Coroutines),Coil 和 Picasso 的方法数差不多,相比 Glide 和 Fresco 要轻量很多。
379 | - **更容易使用**: Coil 的 API 充分利用了 Kotlin 语言的新特性,简化和减少了很多样板代码。
380 | - **更流行**: Coil 首选 Kotlin 语言开发并且使用包含 Coroutines, OkHttp, Okio 和 AndroidX Lifecycles 在内最流行的开源库。
381 |
382 | **Coil** 名字的由来:取 **Co**routine **I**mage **L**oader 首字母得来。
383 |
384 | **资料:**
385 |
386 | 官方文档: [https://coil-kt.github.io/coil/](https://coil-kt.github.io/coil/)
387 |
388 | 三方库源码笔记(13)-可能是全网第一篇 Coil 的源码分析文章:[https://juejin.cn/post/6897872882051842061](https://juejin.cn/post/6897872882051842061)
389 |
390 | 【奇技淫巧】新的图片加载库?基于Kotlin协程的图片加载库——Coil:[https://juejin.cn/post/6844904159527829518](https://juejin.cn/post/6844904159527829518)
--------------------------------------------------------------------------------
/lib_base/src/main/java/com/quyunshuo/androidbaseframemvvm/base/utils/BarUtils.java:
--------------------------------------------------------------------------------
1 | package com.quyunshuo.androidbaseframemvvm.base.utils;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.app.Activity;
5 | import android.content.Context;
6 | import android.content.res.Resources;
7 | import android.graphics.Color;
8 | import android.graphics.Point;
9 | import android.os.Build;
10 | import android.util.TypedValue;
11 | import android.view.Display;
12 | import android.view.KeyCharacterMap;
13 | import android.view.KeyEvent;
14 | import android.view.View;
15 | import android.view.ViewConfiguration;
16 | import android.view.ViewGroup;
17 | import android.view.ViewGroup.MarginLayoutParams;
18 | import android.view.Window;
19 | import android.view.WindowManager;
20 |
21 | import androidx.annotation.ColorInt;
22 | import androidx.annotation.NonNull;
23 | import androidx.annotation.RequiresApi;
24 | import androidx.annotation.RequiresPermission;
25 |
26 | import java.lang.reflect.Method;
27 |
28 | import static android.Manifest.permission.EXPAND_STATUS_BAR;
29 |
30 | import com.quyunshuo.androidbaseframemvvm.base.BaseApplication;
31 |
32 | /**
33 | * 各种栏的工具类
34 | *
35 | * getStatusBarHeight : 获取状态栏高度(px)
36 | * setStatusBarVisibility : 设置状态栏是否可见
37 | * isStatusBarVisible : 判断状态栏是否可见
38 | * setStatusBarLightMode : 设置状态栏是否为浅色模式
39 | * isStatusBarLightMode : 判断状态栏是否为浅色模式
40 | * addMarginTopEqualStatusBarHeight : 为 view 增加 MarginTop 为状态栏高度
41 | * subtractMarginTopEqualStatusBarHeight: 为 view 减少 MarginTop 为状态栏高度
42 | * setStatusBarColor : 设置状态栏颜色
43 | * setStatusBarColor4Drawer : 为 DrawerLayout 设置状态栏颜色
44 | * transparentStatusBar : 透明状态栏
45 | * getActionBarHeight : 获取 ActionBar 高度
46 | * setNotificationBarVisibility : 设置通知栏是否可见
47 | * getNavBarHeight : 获取导航栏高度
48 | * setNavBarVisibility : 设置导航栏是否可见
49 | * isNavBarVisible : 判断导航栏是否可见
50 | * setNavBarColor : 设置导航栏颜色
51 | * getNavBarColor : 获取导航栏颜色
52 | * isSupportNavBar : 判断是否支持导航栏
53 | * setNavBarLightMode : 设置导航栏是否为浅色模式
54 | * isNavBarLightMode : 判断导航栏是否为浅色模式
55 | *
56 | * @author Qu Yunshuo
57 | * @since 2021/7/15 10:42 上午
58 | */
59 | public final class BarUtils {
60 |
61 | ///////////////////////////////////////////////////////////////////////////
62 | // status bar
63 | ///////////////////////////////////////////////////////////////////////////
64 |
65 | private static final String TAG_STATUS_BAR = "TAG_STATUS_BAR";
66 | private static final String TAG_OFFSET = "TAG_OFFSET";
67 | private static final int KEY_OFFSET = -123;
68 |
69 | private BarUtils() {
70 | throw new UnsupportedOperationException("u can't instantiate me...");
71 | }
72 |
73 | /**
74 | * Return the status bar's height.
75 | *
76 | * @return the status bar's height
77 | */
78 | public static int getStatusBarHeight() {
79 | Resources resources = BaseApplication.context.getResources();
80 | int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
81 | return resources.getDimensionPixelSize(resourceId);
82 | }
83 |
84 | /**
85 | * Set the status bar's visibility.
86 | *
87 | * @param activity The activity.
88 | * @param isVisible True to set status bar visible, false otherwise.
89 | */
90 | public static void setStatusBarVisibility(@NonNull final Activity activity,
91 | final boolean isVisible) {
92 | setStatusBarVisibility(activity.getWindow(), isVisible);
93 | }
94 |
95 | /**
96 | * Set the status bar's visibility.
97 | *
98 | * @param window The window.
99 | * @param isVisible True to set status bar visible, false otherwise.
100 | */
101 | public static void setStatusBarVisibility(@NonNull final Window window,
102 | final boolean isVisible) {
103 | if (isVisible) {
104 | window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
105 | showStatusBarView(window);
106 | addMarginTopEqualStatusBarHeight(window);
107 | } else {
108 | window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
109 | hideStatusBarView(window);
110 | subtractMarginTopEqualStatusBarHeight(window);
111 | }
112 | }
113 |
114 | /**
115 | * Return whether the status bar is visible.
116 | *
117 | * @param activity The activity.
118 | * @return {@code true}: yes
{@code false}: no
119 | */
120 | public static boolean isStatusBarVisible(@NonNull final Activity activity) {
121 | int flags = activity.getWindow().getAttributes().flags;
122 | return (flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0;
123 | }
124 |
125 | /**
126 | * Set the status bar's light mode.
127 | *
128 | * @param activity The activity.
129 | * @param isLightMode True to set status bar light mode, false otherwise.
130 | */
131 | public static void setStatusBarLightMode(@NonNull final Activity activity,
132 | final boolean isLightMode) {
133 | setStatusBarLightMode(activity.getWindow(), isLightMode);
134 | }
135 |
136 | /**
137 | * Set the status bar's light mode.
138 | *
139 | * @param window The window.
140 | * @param isLightMode True to set status bar light mode, false otherwise.
141 | */
142 | public static void setStatusBarLightMode(@NonNull final Window window,
143 | final boolean isLightMode) {
144 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
145 | View decorView = window.getDecorView();
146 | int vis = decorView.getSystemUiVisibility();
147 | if (isLightMode) {
148 | vis |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
149 | } else {
150 | vis &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
151 | }
152 | decorView.setSystemUiVisibility(vis);
153 | }
154 | }
155 |
156 | /**
157 | * Is the status bar light mode.
158 | *
159 | * @param activity The activity.
160 | * @return {@code true}: yes
{@code false}: no
161 | */
162 | public static boolean isStatusBarLightMode(@NonNull final Activity activity) {
163 | return isStatusBarLightMode(activity.getWindow());
164 | }
165 |
166 | /**
167 | * Is the status bar light mode.
168 | *
169 | * @param window The window.
170 | * @return {@code true}: yes
{@code false}: no
171 | */
172 | public static boolean isStatusBarLightMode(@NonNull final Window window) {
173 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
174 | View decorView = window.getDecorView();
175 | int vis = decorView.getSystemUiVisibility();
176 | return (vis & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0;
177 | }
178 | return false;
179 | }
180 |
181 | /**
182 | * Add the top margin size equals status bar's height for view.
183 | *
184 | * @param view The view.
185 | */
186 | public static void addMarginTopEqualStatusBarHeight(@NonNull View view) {
187 |
188 | view.setTag(TAG_OFFSET);
189 | Object haveSetOffset = view.getTag(KEY_OFFSET);
190 | if (haveSetOffset != null && (Boolean) haveSetOffset) return;
191 | MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams();
192 | layoutParams.setMargins(layoutParams.leftMargin,
193 | layoutParams.topMargin + getStatusBarHeight(),
194 | layoutParams.rightMargin,
195 | layoutParams.bottomMargin);
196 | view.setTag(KEY_OFFSET, true);
197 | }
198 |
199 | /**
200 | * Subtract the top margin size equals status bar's height for view.
201 | *
202 | * @param view The view.
203 | */
204 | public static void subtractMarginTopEqualStatusBarHeight(@NonNull View view) {
205 |
206 | Object haveSetOffset = view.getTag(KEY_OFFSET);
207 | if (haveSetOffset == null || !(Boolean) haveSetOffset) return;
208 | MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams();
209 | layoutParams.setMargins(layoutParams.leftMargin,
210 | layoutParams.topMargin - getStatusBarHeight(),
211 | layoutParams.rightMargin,
212 | layoutParams.bottomMargin);
213 | view.setTag(KEY_OFFSET, false);
214 | }
215 |
216 | private static void addMarginTopEqualStatusBarHeight(@NonNull final Window window) {
217 | View withTag = window.getDecorView().findViewWithTag(TAG_OFFSET);
218 | if (withTag == null) return;
219 | addMarginTopEqualStatusBarHeight(withTag);
220 | }
221 |
222 | private static void subtractMarginTopEqualStatusBarHeight(@NonNull final Window window) {
223 | View withTag = window.getDecorView().findViewWithTag(TAG_OFFSET);
224 | if (withTag == null) return;
225 | subtractMarginTopEqualStatusBarHeight(withTag);
226 | }
227 |
228 | /**
229 | * Set the status bar's color.
230 | *
231 | * @param activity The activity.
232 | * @param color The status bar's color.
233 | */
234 | public static View setStatusBarColor(@NonNull final Activity activity,
235 | @ColorInt final int color) {
236 | return setStatusBarColor(activity, color, false);
237 | }
238 |
239 | /**
240 | * Set the status bar's color.
241 | *
242 | * @param activity The activity.
243 | * @param color The status bar's color.
244 | * @param isDecor True to add fake status bar in DecorView,
245 | * false to add fake status bar in ContentView.
246 | */
247 | public static View setStatusBarColor(@NonNull final Activity activity,
248 | @ColorInt final int color,
249 | final boolean isDecor) {
250 | transparentStatusBar(activity);
251 | return applyStatusBarColor(activity, color, isDecor);
252 | }
253 |
254 |
255 | /**
256 | * Set the status bar's color.
257 | *
258 | * @param window The window.
259 | * @param color The status bar's color.
260 | */
261 | public static View setStatusBarColor(@NonNull final Window window,
262 | @ColorInt final int color) {
263 | return setStatusBarColor(window, color, false);
264 | }
265 |
266 | /**
267 | * Set the status bar's color.
268 | *
269 | * @param window The window.
270 | * @param color The status bar's color.
271 | * @param isDecor True to add fake status bar in DecorView,
272 | * false to add fake status bar in ContentView.
273 | */
274 | public static View setStatusBarColor(@NonNull final Window window,
275 | @ColorInt final int color,
276 | final boolean isDecor) {
277 | transparentStatusBar(window);
278 | return applyStatusBarColor(window, color, isDecor);
279 | }
280 |
281 | /**
282 | * Set the status bar's color.
283 | *
284 | * @param fakeStatusBar The fake status bar view.
285 | * @param color The status bar's color.
286 | */
287 | public static void setStatusBarColor(Activity activity, @NonNull final View fakeStatusBar,
288 | @ColorInt final int color) {
289 | if (activity == null) return;
290 | transparentStatusBar(activity);
291 | fakeStatusBar.setVisibility(View.VISIBLE);
292 | ViewGroup.LayoutParams layoutParams = fakeStatusBar.getLayoutParams();
293 | layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
294 | layoutParams.height = getStatusBarHeight();
295 | fakeStatusBar.setBackgroundColor(color);
296 | }
297 |
298 | /**
299 | * Set the custom status bar.
300 | *
301 | * @param fakeStatusBar The fake status bar view.
302 | */
303 | public static void setStatusBarCustom(Activity activity, @NonNull final View fakeStatusBar) {
304 | if (activity == null) return;
305 | transparentStatusBar(activity);
306 | fakeStatusBar.setVisibility(View.VISIBLE);
307 | ViewGroup.LayoutParams layoutParams = fakeStatusBar.getLayoutParams();
308 | if (layoutParams == null) {
309 | layoutParams = new ViewGroup.LayoutParams(
310 | ViewGroup.LayoutParams.MATCH_PARENT,
311 | getStatusBarHeight()
312 | );
313 | fakeStatusBar.setLayoutParams(layoutParams);
314 | } else {
315 | layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
316 | layoutParams.height = getStatusBarHeight();
317 | }
318 | }
319 |
320 | private static View applyStatusBarColor(@NonNull final Activity activity,
321 | final int color,
322 | boolean isDecor) {
323 | return applyStatusBarColor(activity.getWindow(), color, isDecor);
324 | }
325 |
326 | private static View applyStatusBarColor(@NonNull final Window window,
327 | final int color,
328 | boolean isDecor) {
329 | ViewGroup parent = isDecor ?
330 | (ViewGroup) window.getDecorView() :
331 | (ViewGroup) window.findViewById(android.R.id.content);
332 | View fakeStatusBarView = parent.findViewWithTag(TAG_STATUS_BAR);
333 | if (fakeStatusBarView != null) {
334 | if (fakeStatusBarView.getVisibility() == View.GONE) {
335 | fakeStatusBarView.setVisibility(View.VISIBLE);
336 | }
337 | fakeStatusBarView.setBackgroundColor(color);
338 | } else {
339 | fakeStatusBarView = createStatusBarView(window.getContext(), color);
340 | parent.addView(fakeStatusBarView);
341 | }
342 | return fakeStatusBarView;
343 | }
344 |
345 | private static void hideStatusBarView(@NonNull final Activity activity) {
346 | hideStatusBarView(activity.getWindow());
347 | }
348 |
349 | private static void hideStatusBarView(@NonNull final Window window) {
350 | ViewGroup decorView = (ViewGroup) window.getDecorView();
351 | View fakeStatusBarView = decorView.findViewWithTag(TAG_STATUS_BAR);
352 | if (fakeStatusBarView == null) return;
353 | fakeStatusBarView.setVisibility(View.GONE);
354 | }
355 |
356 | private static void showStatusBarView(@NonNull final Window window) {
357 | ViewGroup decorView = (ViewGroup) window.getDecorView();
358 | View fakeStatusBarView = decorView.findViewWithTag(TAG_STATUS_BAR);
359 | if (fakeStatusBarView == null) return;
360 | fakeStatusBarView.setVisibility(View.VISIBLE);
361 | }
362 |
363 | private static View createStatusBarView(@NonNull final Context context,
364 | final int color) {
365 | View statusBarView = new View(context);
366 | statusBarView.setLayoutParams(new ViewGroup.LayoutParams(
367 | ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight()));
368 | statusBarView.setBackgroundColor(color);
369 | statusBarView.setTag(TAG_STATUS_BAR);
370 | return statusBarView;
371 | }
372 |
373 | public static void transparentStatusBar(@NonNull final Activity activity) {
374 | transparentStatusBar(activity.getWindow());
375 | }
376 |
377 | public static void transparentStatusBar(@NonNull final Window window) {
378 |
379 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
380 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
381 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
382 | int option = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
383 | int vis = window.getDecorView().getSystemUiVisibility();
384 | window.getDecorView().setSystemUiVisibility(option | vis);
385 | window.setStatusBarColor(Color.TRANSPARENT);
386 | } else {
387 | window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
388 | }
389 | }
390 |
391 | ///////////////////////////////////////////////////////////////////////////
392 | // action bar
393 | ///////////////////////////////////////////////////////////////////////////
394 |
395 | /**
396 | * Return the action bar's height.
397 | *
398 | * @return the action bar's height
399 | */
400 | public static int getActionBarHeight() {
401 | TypedValue tv = new TypedValue();
402 | if (BaseApplication.context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
403 | return TypedValue.complexToDimensionPixelSize(
404 | tv.data, BaseApplication.context.getResources().getDisplayMetrics()
405 | );
406 | }
407 | return 0;
408 | }
409 |
410 | ///////////////////////////////////////////////////////////////////////////
411 | // notification bar
412 | ///////////////////////////////////////////////////////////////////////////
413 |
414 | /**
415 | * Set the notification bar's visibility.
416 | *
Must hold {@code }
417 | *
418 | * @param isVisible True to set notification bar visible, false otherwise.
419 | */
420 | @RequiresPermission(EXPAND_STATUS_BAR)
421 | public static void setNotificationBarVisibility(final boolean isVisible) {
422 | String methodName;
423 | if (isVisible) {
424 | methodName = (Build.VERSION.SDK_INT <= 16) ? "expand" : "expandNotificationsPanel";
425 | } else {
426 | methodName = (Build.VERSION.SDK_INT <= 16) ? "collapse" : "collapsePanels";
427 | }
428 | invokePanels(methodName);
429 | }
430 |
431 | private static void invokePanels(final String methodName) {
432 | try {
433 | @SuppressLint("WrongConstant")
434 | Object service = BaseApplication.context.getSystemService("statusbar");
435 | @SuppressLint("PrivateApi")
436 | Class> statusBarManager = Class.forName("android.app.StatusBarManager");
437 | Method expand = statusBarManager.getMethod(methodName);
438 | expand.invoke(service);
439 | } catch (Exception e) {
440 | e.printStackTrace();
441 | }
442 | }
443 |
444 | ///////////////////////////////////////////////////////////////////////////
445 | // navigation bar
446 | ///////////////////////////////////////////////////////////////////////////
447 |
448 | /**
449 | * Return the navigation bar's height.
450 | *
451 | * @return the navigation bar's height
452 | */
453 | public static int getNavBarHeight() {
454 | Resources res = BaseApplication.context.getResources();
455 | int resourceId = res.getIdentifier("navigation_bar_height", "dimen", "android");
456 | if (resourceId != 0) {
457 | return res.getDimensionPixelSize(resourceId);
458 | } else {
459 | return 0;
460 | }
461 | }
462 |
463 | /**
464 | * Set the navigation bar's visibility.
465 | *
466 | * @param activity The activity.
467 | * @param isVisible True to set navigation bar visible, false otherwise.
468 | */
469 | public static void setNavBarVisibility(@NonNull final Activity activity, boolean isVisible) {
470 |
471 | setNavBarVisibility(activity.getWindow(), isVisible);
472 |
473 | }
474 |
475 | /**
476 | * Set the navigation bar's visibility.
477 | *
478 | * @param window The window.
479 | * @param isVisible True to set navigation bar visible, false otherwise.
480 | */
481 | public static void setNavBarVisibility(@NonNull final Window window, boolean isVisible) {
482 |
483 | final ViewGroup decorView = (ViewGroup) window.getDecorView();
484 | for (int i = 0, count = decorView.getChildCount(); i < count; i++) {
485 | final View child = decorView.getChildAt(i);
486 | final int id = child.getId();
487 | if (id != View.NO_ID) {
488 | String resourceEntryName = getResNameById(id);
489 | if ("navigationBarBackground".equals(resourceEntryName)) {
490 | child.setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE);
491 | }
492 | }
493 | }
494 | final int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
495 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
496 | | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
497 | if (isVisible) {
498 | decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() & ~uiOptions);
499 | } else {
500 | decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | uiOptions);
501 | }
502 | }
503 |
504 | /**
505 | * Return whether the navigation bar visible.
506 | * Call it in onWindowFocusChanged will get right result.
507 | *
508 | * @param activity The activity.
509 | * @return {@code true}: yes
{@code false}: no
510 | */
511 | public static boolean isNavBarVisible(@NonNull final Activity activity) {
512 | return isNavBarVisible(activity.getWindow());
513 | }
514 |
515 | /**
516 | * Return whether the navigation bar visible.
517 | * Call it in onWindowFocusChanged will get right result.
518 | *
519 | * @param window The window.
520 | * @return {@code true}: yes
{@code false}: no
521 | */
522 | public static boolean isNavBarVisible(@NonNull final Window window) {
523 | boolean isVisible = false;
524 | ViewGroup decorView = (ViewGroup) window.getDecorView();
525 | for (int i = 0, count = decorView.getChildCount(); i < count; i++) {
526 | final View child = decorView.getChildAt(i);
527 | final int id = child.getId();
528 | if (id != View.NO_ID) {
529 | String resourceEntryName = getResNameById(id);
530 | if ("navigationBarBackground".equals(resourceEntryName)
531 | && child.getVisibility() == View.VISIBLE) {
532 | isVisible = true;
533 | break;
534 | }
535 | }
536 | }
537 | if (isVisible) {
538 | int visibility = decorView.getSystemUiVisibility();
539 | isVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0;
540 | }
541 |
542 | return isVisible;
543 | }
544 |
545 | private static String getResNameById(int id) {
546 | try {
547 | return BaseApplication.context.getResources().getResourceEntryName(id);
548 | } catch (Exception ignore) {
549 | return "";
550 | }
551 | }
552 |
553 | /**
554 | * Set the navigation bar's color.
555 | *
556 | * @param activity The activity.
557 | * @param color The navigation bar's color.
558 | */
559 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
560 | public static void setNavBarColor(@NonNull final Activity activity, @ColorInt final int color) {
561 | setNavBarColor(activity.getWindow(), color);
562 | }
563 |
564 | /**
565 | * Set the navigation bar's color.
566 | *
567 | * @param window The window.
568 | * @param color The navigation bar's color.
569 | */
570 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
571 | public static void setNavBarColor(@NonNull final Window window, @ColorInt final int color) {
572 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
573 | window.setNavigationBarColor(color);
574 | }
575 |
576 | /**
577 | * Return the color of navigation bar.
578 | *
579 | * @param activity The activity.
580 | * @return the color of navigation bar
581 | */
582 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
583 | public static int getNavBarColor(@NonNull final Activity activity) {
584 | return getNavBarColor(activity.getWindow());
585 | }
586 |
587 | /**
588 | * Return the color of navigation bar.
589 | *
590 | * @param window The window.
591 | * @return the color of navigation bar
592 | */
593 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
594 | public static int getNavBarColor(@NonNull final Window window) {
595 | return window.getNavigationBarColor();
596 | }
597 |
598 | /**
599 | * Return whether the navigation bar visible.
600 | *
601 | * @return {@code true}: yes
{@code false}: no
602 | */
603 | public static boolean isSupportNavBar() {
604 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
605 | WindowManager wm = (WindowManager) BaseApplication.context.getSystemService(Context.WINDOW_SERVICE);
606 | if (wm == null) return false;
607 | Display display = wm.getDefaultDisplay();
608 | Point size = new Point();
609 | Point realSize = new Point();
610 | display.getSize(size);
611 | display.getRealSize(realSize);
612 | return realSize.y != size.y || realSize.x != size.x;
613 | }
614 | boolean menu = ViewConfiguration.get(BaseApplication.context).hasPermanentMenuKey();
615 | boolean back = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK);
616 | return !menu && !back;
617 | }
618 |
619 | /**
620 | * Set the nav bar's light mode.
621 | *
622 | * @param activity The activity.
623 | * @param isLightMode True to set nav bar light mode, false otherwise.
624 | */
625 | public static void setNavBarLightMode(@NonNull final Activity activity,
626 | final boolean isLightMode) {
627 | setNavBarLightMode(activity.getWindow(), isLightMode);
628 | }
629 |
630 | /**
631 | * Set the nav bar's light mode.
632 | *
633 | * @param window The window.
634 | * @param isLightMode True to set nav bar light mode, false otherwise.
635 | */
636 | public static void setNavBarLightMode(@NonNull final Window window,
637 | final boolean isLightMode) {
638 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
639 | View decorView = window.getDecorView();
640 | int vis = decorView.getSystemUiVisibility();
641 | if (isLightMode) {
642 | vis |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
643 | } else {
644 | vis &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
645 | }
646 | decorView.setSystemUiVisibility(vis);
647 | }
648 | }
649 |
650 | /**
651 | * Is the nav bar light mode.
652 | *
653 | * @param activity The activity.
654 | * @return {@code true}: yes
{@code false}: no
655 | */
656 | public static boolean isNavBarLightMode(@NonNull final Activity activity) {
657 | return isNavBarLightMode(activity.getWindow());
658 | }
659 |
660 | /**
661 | * Is the nav bar light mode.
662 | *
663 | * @param window The window.
664 | * @return {@code true}: yes
{@code false}: no
665 | */
666 | public static boolean isNavBarLightMode(@NonNull final Window window) {
667 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
668 | View decorView = window.getDecorView();
669 | int vis = decorView.getSystemUiVisibility();
670 | return (vis & View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0;
671 | }
672 | return false;
673 | }
674 | }
--------------------------------------------------------------------------------