a
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/android,androidstudio
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=android,androidstudio
|
||||
|
||||
### Android ###
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Log/OS Files
|
||||
*.log
|
||||
|
||||
# Android Studio generated files and folders
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
output.json
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/
|
||||
misc.xml
|
||||
deploymentTargetDropDown.xml
|
||||
render.experimental.xml
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
### Android Patch ###
|
||||
gen-external-apklibs
|
||||
|
||||
# Replacement of .externalNativeBuild directories introduced
|
||||
# with Android Studio 3.5.
|
||||
|
||||
### AndroidStudio ###
|
||||
# Covers files to be ignored for android development using Android Studio.
|
||||
|
||||
# Built application files
|
||||
*.ap_
|
||||
*.aab
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
bin/
|
||||
gen/
|
||||
out/
|
||||
|
||||
# Gradle files
|
||||
.gradle
|
||||
|
||||
# Signing files
|
||||
.signing/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
|
||||
# Android Studio
|
||||
/*/build/
|
||||
/*/local.properties
|
||||
/*/out
|
||||
/*/*/build
|
||||
/*/*/production
|
||||
.navigation/
|
||||
*.ipr
|
||||
*~
|
||||
*.swp
|
||||
|
||||
# Keystore files
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
# google-services.json
|
||||
|
||||
# Android Patch
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
|
||||
# NDK
|
||||
obj/
|
||||
|
||||
# IntelliJ IDEA
|
||||
*.iws
|
||||
/out/
|
||||
|
||||
# User-specific configurations
|
||||
.idea/caches/
|
||||
.idea/libraries/
|
||||
.idea/shelf/
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/.name
|
||||
.idea/compiler.xml
|
||||
.idea/copyright/profiles_settings.xml
|
||||
.idea/encodings.xml
|
||||
.idea/misc.xml
|
||||
.idea/modules.xml
|
||||
.idea/scopes/scope_settings.xml
|
||||
.idea/dictionaries
|
||||
.idea/vcs.xml
|
||||
.idea/jsLibraryMappings.xml
|
||||
.idea/datasources.xml
|
||||
.idea/dataSources.ids
|
||||
.idea/sqlDataSources.xml
|
||||
.idea/dynamic.xml
|
||||
.idea/uiDesigner.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/gradle.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/navEditor.xml
|
||||
|
||||
# Legacy Eclipse project files
|
||||
.classpath
|
||||
.project
|
||||
.cproject
|
||||
.settings/
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
|
||||
hs_err_pid*
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/mongoSettings.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
### AndroidStudio Patch ###
|
||||
|
||||
!/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/android,androidstudio
|
||||
|
||||
/buildSrc/build
|
||||
@@ -0,0 +1,55 @@
|
||||
# 🦄Design WanAndroid
|
||||
|
||||
|
||||
## Compose化部分页面
|
||||
目前暂有:主题选择界面
|
||||
|
||||
## 界面:
|
||||
**原WanAndroid开放的Api功能均已实现**<p>
|
||||
App内通篇全采用[Material Design 3](https://m3.material.io/)风格,拒绝半完成式Material带来的UI的割裂感。<p>
|
||||
所有Icon取自[Material Symbols](https://fonts.google.com/icons),统一而规范的设计。<p>
|
||||
主题色遵循[Material3 Color system](https://m3.material.io/styles/color/the-color-system/key-colors-tones)。
|
||||
- PrimaryColor, On-primary, Primary container, On-primary container
|
||||
- SecondaryColor 同上
|
||||
- TertiaryColor
|
||||
<p>
|
||||
|
||||
#### 截图展示
|
||||
----
|
||||
|
||||
|  |  |  |  |
|
||||
| --- | --- | --- | --- |
|
||||
|  |  |  |  |
|
||||
|  |  |  |  |
|
||||
|
||||
----
|
||||
|
||||
默认主题色采用[Material Theme Builder](https://material-foundation.github.io/material-theme-builder/)从图片取色而成。<p>
|
||||
实现[Dynamic Colors](https://m3.material.io/styles/color/dynamic-color/overview),开启动态主题色后,App主题色自动跟随系统主题色且适配深色模式,保持一贯的视觉体验(Android 12及以上支持)
|
||||
|
||||
所以可交互的UI均带有Ripple效果,明确表示这是个可交互控件,且Ripple颜色支持取自当前Dynamic colors的主题色
|
||||
|
||||
## 逻辑:
|
||||
**详细细节可转到[Design WanAndroid(WanAndroid的最佳可使用的Android客户端)](https://juejin.cn/post/7117594416235151367)**<p>
|
||||
使用buildSrc,实现全局且统一的依赖管理。<p>
|
||||
严格遵循[Android Architecture Components](https://developer.android.com/topic/libraries/architecture/),逻辑分为:
|
||||
- 界面层(UI Layer)
|
||||
- APP内实现:视图(Activity/Fragment等) + 数据驱动及处理逻辑的状态容器(ViewModel等)
|
||||
- 网域层(Domain Layer) 可选项,用于处理复杂逻辑或支持可重用性吗,当你需要从不同数据源获取数据时如需要同时从数据库和接口请求数据时,推荐使用UseCase进行组合。
|
||||
- App内实现:组合或复用数据源(UseCase)
|
||||
- 数据层(Data Layer)
|
||||
- App内实现:数据源(Repository)
|
||||
|
||||
当你采用某项东西,应是为了解决某些特定的问题,不能单纯为了用而用。在该架构下:<p>
|
||||
- 对于网络请求的需要,引入通用的网络请求库,[Retrofit](https://github.com/square/retrofit) + [OkHttp](https://github.com/square/okhttp)。<p>
|
||||
- 对于网络异常处理的需要,自定义Retrofit [NetworkResponseAdapterFactory](https://github.com/Lowae/Design-WanAndroid/tree/main/app/src/main/java/com/lowe/wanandroid/base/http/adapter)和[GsonConverterFactory](https://github.com/Lowae/Design-WanAndroid/tree/main/app/src/main/java/com/lowe/wanandroid/base/http/converter),包装接口返回,自定义解析区分业务code,实现全局的接口错误或业务逻辑错误处理,同时下游也可按需获取错误类型。
|
||||
- 针对数据层Repository需要以及UseCase需要复用并组合各类Service,引入[Hilt](https://developer.android.com/training/dependency-injection/hilt-android),解决依赖注入问题,提高可重用性且避免强依赖。
|
||||
- 对于网络请求的线程切换使用[Kotlin协程](https://developer.android.com/kotlin/coroutines?hl=zh-cn),针对复杂且需要进行各类转换处理的数据流使用[Flow](https://developer.android.com/kotlin/flow?hl=zh-cn),对于One-shot数据使用[LiveData](https://developer.android.com/topic/libraries/architecture/livedata?hl=zh-cn),因为LiveData设计初衷并非用于处理复杂的响应数据流。
|
||||
- 对于App内的部分需要持久化数据如[登陆状态的Cookie](https://github.com/Lowae/Design-WanAndroid/tree/main/app/src/main/java/com/lowe/wanandroid/base/http/cookie)、KV数据等小型数据引入[DataStore](https://developer.android.com/topic/libraries/architecture/datastore?hl=zh-cn)和[Kotlin Serialization](https://kotlinlang.org/docs/serialization.html)
|
||||
- 对于RecyclerView引入[Paging3](https://developer.android.com/topic/libraries/architecture/paging/v3-overview?hl=zh-cn)列表的加载及状态处理
|
||||
- 针对列表的多类型Item,导入并自定义修改MultiType实现[PagingMultiTypeAdapter](https://github.com/Lowae/Design-WanAndroid/blob/main/multitype/src/main/java/com/lowe/multitype/PagingMultiTypeAdapter.kt)使其能够支持配合Paging3使用
|
||||
|
||||
除以上主要依赖外,其他引入有[Banner](https://github.com/youth5201314/banner)和[AgentWeb](https://github.com/Justson/AgentWeb),除此之外其余功能均自己实现。
|
||||
|
||||
## 最后:
|
||||
有任何问题欢迎提Issue,如果喜欢的话也可以给个⭐Star
|
||||
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
/release
|
||||
@@ -0,0 +1,98 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
id("kotlin-parcelize")
|
||||
kotlin("kapt")
|
||||
id("dagger.hilt.android.plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = Version.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
applicationId = Version.applicationId
|
||||
minSdk = Version.minSdk
|
||||
targetSdk = Version.targetSdk
|
||||
versionCode = Version.versionCode
|
||||
versionName = Version.versionName
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
outputs.all {
|
||||
(this as? com.android.build.gradle.internal.api.ApkVariantOutputImpl)?.outputFileName =
|
||||
"Design WanAndroid-${Version.versionName}-${name}.apk"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
}
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
lint {
|
||||
baseline = File("lint-baseline.xml")
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
compileOptions {
|
||||
targetCompatibility(JavaVersion.VERSION_11)
|
||||
sourceCompatibility(JavaVersion.VERSION_11)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree("dir" to "libs", "include" to listOf("*.jar", "*.aar")))
|
||||
implementation(project(mapOf("path" to ":multitype")))
|
||||
implementation(project(mapOf("path" to ":resource")))
|
||||
implementation(project(mapOf("path" to ":common")))
|
||||
implementation(project(mapOf("path" to ":compose")))
|
||||
implementation(Deps.coreKtx)
|
||||
implementation(Deps.appcompat)
|
||||
implementation(Deps.activity)
|
||||
implementation(Deps.fragment)
|
||||
implementation(Deps.material)
|
||||
implementation(Deps.constraintlayout)
|
||||
implementation(Deps.lifecycleLiveDataKtx)
|
||||
implementation(Deps.lifecycleViewModelKtx)
|
||||
implementation(Deps.lifecucleRuntimeKtx)
|
||||
implementation(Deps.navigationFragmentKtx)
|
||||
implementation(Deps.navigationUiKtx)
|
||||
implementation(Deps.swiperefreshlayout)
|
||||
implementation(Deps.recyclerview)
|
||||
implementation(Deps.paging)
|
||||
implementation(Deps.pagingKtx)
|
||||
implementation(Deps.dataStore)
|
||||
implementation(Deps.preferences)
|
||||
implementation(Deps.hiltAndroid)
|
||||
kapt(Deps.kaptHiltAndroidCompiler)
|
||||
kapt(Deps.kaptHiltCompiler)
|
||||
|
||||
// implementation(Deps.okhttp)
|
||||
// implementation(Deps.okhttpLoggingInterceptor)
|
||||
implementation(Deps.gson)
|
||||
implementation(Deps.fresco)
|
||||
|
||||
implementation(Deps.banner)
|
||||
implementation(Deps.flexbox)
|
||||
|
||||
// implementation(Deps.kotlinSerial)
|
||||
debugImplementation(Deps.DebugDependency.debugLeakCanary)
|
||||
|
||||
testImplementation(Deps.testJunit)
|
||||
androidTestImplementation(Deps.androidTestJunit)
|
||||
androidTestImplementation(Deps.androidTestEspresso)
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 7.2.1" type="baseline" client="gradle" dependencies="false" name="AGP (7.2.1)" variant="fatal" version="7.2.1">
|
||||
|
||||
<issue
|
||||
id="NullSafeMutableLiveData"
|
||||
message="Expected non-nullable value"
|
||||
errorLine1=" _loginLiveData.value = login(userInfo)"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/lowe/wanandroid/ui/login/LoginViewModel.kt"
|
||||
line="43"
|
||||
column="36"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="NullSafeMutableLiveData"
|
||||
message="Expected non-nullable value"
|
||||
errorLine1=" _registerLiveData.value = register(registerInfo)"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/lowe/wanandroid/ui/login/LoginViewModel.kt"
|
||||
line="49"
|
||||
column="39"/>
|
||||
</issue>
|
||||
|
||||
</issues>
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
|
||||
-optimizationpasses 5
|
||||
-verbose
|
||||
|
||||
# 保留代码行号
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# Preserve some attributes that may be required for reflection.
|
||||
-keepattributes AnnotationDefault,
|
||||
EnclosingMethod,
|
||||
InnerClasses,
|
||||
RuntimeVisibleAnnotations,
|
||||
RuntimeVisibleParameterAnnotations,
|
||||
RuntimeVisibleTypeAnnotations,
|
||||
Signature
|
||||
|
||||
# Fragment
|
||||
-keep class * extends androidx.fragment.app.Fragment{}
|
||||
|
||||
# Preserve annotated Javascript interface methods.
|
||||
-keepclassmembers class * {
|
||||
@android.webkit.JavascriptInterface <methods>;
|
||||
}
|
||||
|
||||
# The support libraries contains references to newer platform versions.
|
||||
# Don't warn about those in case this app is linking against an older
|
||||
# platform version. We know about them, and they are safe.
|
||||
-dontnote android.support.**
|
||||
-dontnote androidx.**
|
||||
-dontwarn android.support.**
|
||||
-dontwarn androidx.**
|
||||
|
||||
## Android architecture components: Lifecycle
|
||||
# LifecycleObserver's empty constructor is considered to be unused by proguard
|
||||
-keepclassmembers class * implements androidx.lifecycle.LifecycleObserver {
|
||||
<init>(...);
|
||||
}
|
||||
# ViewModel's empty constructor is considered to be unused by proguard
|
||||
-keepclassmembers class * extends androidx.lifecycle.ViewModel {
|
||||
<init>(...);
|
||||
}
|
||||
# keep methods annotated with @OnLifecycleEvent even if they seem to be unused
|
||||
# (Mostly for LiveData.LifecycleBoundObserver.onStateChange(), but who knows)
|
||||
-keepclassmembers class * {
|
||||
@androidx.lifecycle.OnLifecycleEvent *;
|
||||
}
|
||||
|
||||
# ViewBinding
|
||||
-keep public class * extends androidx.viewbinding.ViewBinding {*;}
|
||||
|
||||
# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
|
||||
-dontnote org.apache.http.**
|
||||
-dontnote android.net.http.**
|
||||
|
||||
# 保持自定义控件类不被混淆
|
||||
-keepclasseswithmembers class * {
|
||||
public <init>(android.content.Context, android.util.AttributeSet);
|
||||
}
|
||||
-keepclasseswithmembers class * {
|
||||
public <init>(android.content.Context, android.util.AttributeSet, int);
|
||||
}
|
||||
|
||||
# AgentWeb
|
||||
-keep class com.just.agentweb.** {*;}
|
||||
-dontwarn com.just.agentweb.**
|
||||
|
||||
# Databinding
|
||||
-dontwarn android.databinding.**
|
||||
-keep class android.databinding.** { *; }
|
||||
|
||||
# Gson
|
||||
-keep class com.google.gson.** {*;}
|
||||
-keep class com.google.gson.stream.** {*;}
|
||||
-keep class com.google.** {
|
||||
<fields>;
|
||||
<methods>;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.lowe.wanandroid
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.lowe.wanandroid", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.lowe.wanandroid">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!-- 由于WanAndroid接口部分下发的链接是http,所以为了兼容只能设置usesCleartextTraffic = true 开放对http链接的支持 -->
|
||||
<application
|
||||
android:name=".app.BaseApp"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.web.WebActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.navigator.child.tutorial.list.TutorialChapterListActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.navigator.child.series.detail.SeriesDetailListActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.login.LoginActivity"
|
||||
android:exported="false"
|
||||
android:windowSoftInputMode="adjustPan" />
|
||||
<activity
|
||||
android:name=".ui.message.MessageActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.share.ShareListActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.collect.CollectActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.tools.ToolListActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.search.SearchActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.setting.SettingActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.coin.MyCoinInfoActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.coin.ranking.CoinRankingActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.about.AboutActivity"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.lowe.wanandroid
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.lowe.common.base.BaseThemeActivity
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
|
||||
abstract class BaseActivity<VM : BaseViewModel, VD : ViewDataBinding> : BaseThemeActivity() {
|
||||
|
||||
protected abstract val viewDataBinding: VD
|
||||
protected abstract val viewModel: VM
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.lowe.wanandroid
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
|
||||
abstract class BaseFragment<VM : BaseViewModel, VD : ViewDataBinding>(@LayoutRes private val layoutResId: Int) :
|
||||
Fragment() {
|
||||
|
||||
protected lateinit var viewDataBinding: VD
|
||||
protected abstract val viewModel: VM
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
) = DataBindingUtil.inflate<VD>(inflater, layoutResId, container, false)
|
||||
.also {
|
||||
it.lifecycleOwner = viewLifecycleOwner
|
||||
viewDataBinding = it
|
||||
}
|
||||
.root
|
||||
|
||||
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
onViewCreated(savedInstanceState)
|
||||
}
|
||||
|
||||
protected abstract fun onViewCreated(savedInstanceState: Bundle?)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.lowe.wanandroid
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.viewModels
|
||||
import com.lowe.resource.extension.getPrimaryColor
|
||||
import com.lowe.wanandroid.databinding.ActivityMainBinding
|
||||
import com.lowe.wanandroid.ui.ActivityDataBindingDelegate
|
||||
import com.lowe.wanandroid.ui.group.GroupFragment
|
||||
import com.lowe.wanandroid.ui.home.HomeFragment
|
||||
import com.lowe.wanandroid.ui.home.child.explore.ExploreFragment
|
||||
import com.lowe.wanandroid.ui.navigator.NavigatorFragment
|
||||
import com.lowe.wanandroid.ui.profile.ProfileFragment
|
||||
import com.lowe.wanandroid.ui.project.ProjectFragment
|
||||
import com.lowe.common.utils.launchRepeatOnStarted
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : BaseActivity<MainViewModel, ActivityMainBinding>() {
|
||||
|
||||
companion object {
|
||||
private const val KEY_CURRENT_FRAGMENT_INDEX = "key_current_fragment_index"
|
||||
}
|
||||
|
||||
override val viewDataBinding: ActivityMainBinding by ActivityDataBindingDelegate(R.layout.activity_main)
|
||||
|
||||
private var fragmentList =
|
||||
listOf(
|
||||
HomeFragment(),
|
||||
ProjectFragment(),
|
||||
NavigatorFragment(),
|
||||
GroupFragment(),
|
||||
ProfileFragment()
|
||||
)
|
||||
private var activeFragmentIndex = -1
|
||||
|
||||
override val viewModel: MainViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewDataBinding.navView.setOnItemSelectedListener(
|
||||
NavBottomViewDoubleClickListener(
|
||||
this::onBottomItemSelect,
|
||||
this::onBottomDoubleClick
|
||||
)
|
||||
)
|
||||
if (savedInstanceState == null) {
|
||||
switchFragment(0)
|
||||
}
|
||||
launchRepeatOnStarted {
|
||||
launch {
|
||||
viewModel.profileUnread.collect(this@MainActivity::changeProfileDot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putInt(KEY_CURRENT_FRAGMENT_INDEX, activeFragmentIndex)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
fragmentList = fragmentList.map {
|
||||
supportFragmentManager.findFragmentByTag(it.javaClass.simpleName) as? BaseFragment<*, *>
|
||||
?: it
|
||||
}
|
||||
switchFragment(savedInstanceState.getInt(KEY_CURRENT_FRAGMENT_INDEX, 0))
|
||||
}
|
||||
|
||||
private fun onBottomItemSelect(item: MenuItem): Boolean {
|
||||
switchFragment(getFragmentIndexFromItemId(item.itemId))
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onBottomDoubleClick(item: MenuItem) {
|
||||
viewModel.bottomDoubleClick(getTagFromItemId(item.itemId))
|
||||
}
|
||||
|
||||
private fun switchFragment(fragmentIndex: Int) {
|
||||
if (fragmentIndex != activeFragmentIndex) {
|
||||
val fragmentTransaction = supportFragmentManager.beginTransaction()
|
||||
val fragment = fragmentList[fragmentIndex]
|
||||
fragmentList.getOrNull(activeFragmentIndex)?.apply(fragmentTransaction::hide)
|
||||
if (!fragment.isAdded) {
|
||||
fragmentTransaction
|
||||
.add(
|
||||
R.id.nav_host_fragment_activity_main,
|
||||
fragment,
|
||||
fragment.javaClass.simpleName
|
||||
)
|
||||
.show(fragment)
|
||||
} else {
|
||||
fragmentTransaction.show(fragment)
|
||||
}
|
||||
fragmentTransaction.commitAllowingStateLoss()
|
||||
activeFragmentIndex = fragmentIndex
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTagFromItemId(itemId: Int) = fragmentList[getFragmentIndexFromItemId(itemId)].tag
|
||||
?: ExploreFragment::class.java.simpleName
|
||||
|
||||
private fun getFragmentIndexFromItemId(itemId: Int): Int {
|
||||
return when (itemId) {
|
||||
R.id.navigation_home -> 0
|
||||
R.id.navigation_project -> 1
|
||||
R.id.navigation_navigator -> 2
|
||||
R.id.navigation_we_chat_group -> 3
|
||||
R.id.navigation_profile -> 4
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeProfileDot(isShown: Boolean) {
|
||||
viewDataBinding.navView.getOrCreateBadge(R.id.navigation_profile).also { badge ->
|
||||
badge.backgroundColor = getPrimaryColor()
|
||||
badge.isVisible = isShown
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.lowe.wanandroid
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.wanandroid.ui.message.UnreadMessageManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MainViewModel @Inject constructor(unreadMessageManager: UnreadMessageManager) :
|
||||
BaseViewModel() {
|
||||
|
||||
/**
|
||||
* 首页NavigationBottomTab双击事件
|
||||
*/
|
||||
private val _mainTabDoubleClickLiveData = MutableLiveData<String>()
|
||||
val mainTabDoubleClickLiveData: LiveData<String> = _mainTabDoubleClickLiveData
|
||||
|
||||
fun bottomDoubleClick(tag: String) {
|
||||
_mainTabDoubleClickLiveData.value = tag
|
||||
}
|
||||
|
||||
val profileUnread = unreadMessageManager.profileUnreadState.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.Lazily,
|
||||
false
|
||||
)
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.lowe.wanandroid
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.view.MenuItem
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
|
||||
class NavBottomViewDoubleClickListener(
|
||||
private val onItemSelected: ((MenuItem) -> Boolean),
|
||||
private val onItemDoubleClick: ((MenuItem) -> Unit)
|
||||
) : NavigationBarView.OnItemSelectedListener {
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_QUICK_CLICK_DURATION = 200L
|
||||
}
|
||||
|
||||
private var timestampLastClick = 0L
|
||||
private var lastClickItemId = 0
|
||||
|
||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||
if (SystemClock.elapsedRealtime() - timestampLastClick < DEFAULT_QUICK_CLICK_DURATION && item.itemId == lastClickItemId) {
|
||||
onItemDoubleClick(item)
|
||||
}
|
||||
lastClickItemId = item.itemId
|
||||
timestampLastClick = SystemClock.elapsedRealtime()
|
||||
return onItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.lowe.wanandroid.app
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import com.facebook.drawee.backends.pipeline.Fresco
|
||||
import com.lowe.common.base.app.ApplicationProxy
|
||||
import com.lowe.common.base.app.CommonApplicationProxy
|
||||
import com.lowe.common.base.http.DataStoreFactory
|
||||
import com.lowe.common.base.http.RetrofitManager
|
||||
import com.lowe.common.base.http.cookie.UserCookieJarImpl
|
||||
import com.lowe.common.constant.SettingConstants
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* App
|
||||
*/
|
||||
@HiltAndroidApp
|
||||
open class BaseApp : Application() {
|
||||
|
||||
@Inject
|
||||
lateinit var cookieJarImpl: UserCookieJarImpl
|
||||
|
||||
private val proxies = listOf<ApplicationProxy>(CommonApplicationProxy)
|
||||
|
||||
companion object {
|
||||
lateinit var appContext: Context
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
appContext = applicationContext
|
||||
proxies.forEach { it.onCreate(this) }
|
||||
Fresco.initialize(this)
|
||||
DataStoreFactory.init(this)
|
||||
RetrofitManager.init(cookieJarImpl)
|
||||
AppCompatDelegate.setDefaultNightMode(SettingConstants.preferenceDarkMode)
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
proxies.forEach { it.onTerminate() }
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.lowe.wanandroid.ui
|
||||
|
||||
import com.lowe.common.base.SimpleDiffCallback
|
||||
import com.lowe.common.base.SimpleDiffItemCallback
|
||||
import com.lowe.common.services.model.*
|
||||
|
||||
object ArticleDiffCalculator {
|
||||
|
||||
fun getCommonDiffCallback(oldList: List<Any>, newList: List<Any>) =
|
||||
SimpleDiffCallback(
|
||||
oldList,
|
||||
newList,
|
||||
{ oldItem: Any, newItem: Any ->
|
||||
when {
|
||||
oldItem is Navigation && newItem is Navigation -> oldItem.name == newItem.name
|
||||
oldItem is Article && newItem is Article -> oldItem.id == newItem.id
|
||||
oldItem is Series && newItem is Series -> oldItem.id == newItem.id
|
||||
oldItem is Classify && newItem is Classify -> oldItem.id == newItem.id
|
||||
else -> oldItem.javaClass == newItem.javaClass
|
||||
}
|
||||
},
|
||||
{ oldItem: Any, newItem: Any ->
|
||||
when {
|
||||
oldItem is Navigation && newItem is Navigation -> oldItem == newItem
|
||||
oldItem is Series && newItem is Series -> oldItem == newItem
|
||||
oldItem is Classify && newItem is Classify -> oldItem == newItem
|
||||
oldItem is Article && newItem is Article -> oldItem == newItem
|
||||
else -> oldItem.javaClass == newItem.javaClass && oldItem == newItem
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
fun getCommonDiffItemCallback() =
|
||||
SimpleDiffItemCallback(
|
||||
areItemSame = { oldItem: Any, newItem: Any ->
|
||||
when {
|
||||
oldItem is Article && newItem is Article -> oldItem.id == newItem.id
|
||||
oldItem is Banners && newItem is Banners -> true
|
||||
oldItem is CollectBean && newItem is CollectBean -> oldItem.id == newItem.id
|
||||
oldItem is MsgBean && newItem is MsgBean -> oldItem.id == newItem.id
|
||||
else -> oldItem::class.java == newItem::class.java
|
||||
}
|
||||
},
|
||||
areContentSame = { oldItem: Any, newItem: Any ->
|
||||
when {
|
||||
oldItem is Article && newItem is Article -> oldItem == newItem
|
||||
oldItem is Banners && newItem is Banners -> oldItem == newItem
|
||||
oldItem is CollectBean && newItem is CollectBean -> oldItem == newItem && oldItem.collect == newItem.collect
|
||||
oldItem is MsgBean && newItem is MsgBean -> oldItem == newItem
|
||||
else -> oldItem == newItem
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.lowe.wanandroid.ui
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* Activity的[ViewDataBinding]代理类
|
||||
*/
|
||||
class ActivityDataBindingDelegate<out VD : ViewDataBinding>(@LayoutRes private val layoutRes: Int) :
|
||||
ReadOnlyProperty<Activity, VD> {
|
||||
|
||||
private var binding: VD? = null
|
||||
|
||||
override fun getValue(thisRef: Activity, property: KProperty<*>): VD =
|
||||
binding ?: DataBindingUtil.setContentView<VD>(thisRef, layoutRes)
|
||||
.also { binding = it }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.lowe.wanandroid.ui
|
||||
|
||||
enum class ItemClickType {
|
||||
|
||||
/**
|
||||
* 主体点击
|
||||
*/
|
||||
CONTENT,
|
||||
|
||||
/**
|
||||
* 收藏点击
|
||||
*/
|
||||
COLLECT
|
||||
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.lowe.wanandroid.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.paging.LoadState
|
||||
import com.lowe.multitype.FooterStateItemBinder
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ItemFooterLayoutBinding
|
||||
|
||||
open class SimpleFooterItemBinder :
|
||||
FooterStateItemBinder<ViewBindingHolder<ItemFooterLayoutBinding>>() {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
context: Context,
|
||||
parent: ViewGroup
|
||||
): ViewBindingHolder<ItemFooterLayoutBinding> =
|
||||
ViewBindingHolder(
|
||||
DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context),
|
||||
R.layout.item_footer_layout,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewBindingHolder<ItemFooterLayoutBinding>,
|
||||
state: LoadState
|
||||
) {
|
||||
holder.binding.apply {
|
||||
if (state is LoadState.Error) {
|
||||
errorMsg.text = state.error.localizedMessage
|
||||
}
|
||||
loadingProgress.isVisible = state is LoadState.Loading
|
||||
retryButton.isVisible = state is LoadState.Error
|
||||
errorMsg.isVisible = state is LoadState.Error
|
||||
endTips.isVisible = state.endOfPaginationReached
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.lowe.wanandroid.ui
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
/**
|
||||
* [ViewDataBinding] ViewHolder
|
||||
*/
|
||||
open class ViewBindingHolder<VD : ViewDataBinding>(open val binding: VD) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.lowe.wanandroid.ui.about
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import androidx.activity.viewModels
|
||||
import com.lowe.wanandroid.BaseActivity
|
||||
import com.lowe.common.utils.fromHtml
|
||||
import com.lowe.wanandroid.BuildConfig
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ActivityAboutBinding
|
||||
import com.lowe.wanandroid.ui.ActivityDataBindingDelegate
|
||||
|
||||
/**
|
||||
* 关于页面
|
||||
*/
|
||||
class AboutActivity : BaseActivity<AboutViewModel, ActivityAboutBinding>() {
|
||||
|
||||
override val viewDataBinding: ActivityAboutBinding by ActivityDataBindingDelegate(R.layout.activity_about)
|
||||
|
||||
override val viewModel: AboutViewModel by viewModels()
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewDataBinding.toolbar.setNavigationOnClickListener { finish() }
|
||||
viewDataBinding.version.text = getString(
|
||||
R.string.app_version,
|
||||
BuildConfig.VERSION_NAME,
|
||||
BuildConfig.VERSION_CODE.toString()
|
||||
)
|
||||
viewDataBinding.aboutText.apply {
|
||||
text = """
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='UTF-8'><meta name='viewport' content='width=device-width initial-scale=1'>
|
||||
<title></title>
|
||||
</head>
|
||||
<body><h2 id='应用介绍'>应用介绍</h2>
|
||||
<p>🦄<a href='https://github.com/Lowae/WanAndroid'>Design WanAndroid</a>,本应用是<a href='https://www.wanandroid.com/'>WanAndroid</a>网站的Android客户端。是<strong>Material Design</strong> + <strong>Jetpack</strong>最佳实践,严格遵循<strong>Material3</strong>设计,且完美支持其Dynamic Colors等新特性,贯彻<strong>MVVM</strong>架构,保证UI风格、逻辑设计的一致性。</p>
|
||||
<p> </p>
|
||||
<h2 id='网站内容'>网站内容</h2>
|
||||
<p>本网站每天新增20~30篇优质文章,并加入到现有分类中,力求整理出一份优质而又详尽的知识体系,闲暇时间不妨上来学习下知识; 除此以外,并为大家提供平时开发过程中常用的工具以及常用的网址导航。</p>
|
||||
<p>当然这只是我们目前的功能,未来我们将提供更多更加便捷的功能...</p>
|
||||
<p>如果您有任何好的建议:</p>
|
||||
<ul>
|
||||
<li>关于网站排版</li>
|
||||
<li>关于新增常用网址以及工具</li>
|
||||
<li>未来你希望增加的功能等</li>
|
||||
|
||||
</ul>
|
||||
<p>可以在 <a href='https://github.com/hongyangAndroid/wanandroid'>https://github.com/hongyangAndroid/xueandroid</a> 项目中以issue的形式提出,我将及时跟进。</p>
|
||||
</body>
|
||||
</html>
|
||||
""".trimIndent().fromHtml()
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.lowe.wanandroid.ui.about
|
||||
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
|
||||
class AboutViewModel : BaseViewModel() {
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.lowe.wanandroid.ui.coin
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.base.IntKeyPagingSource
|
||||
import com.lowe.common.base.http.adapter.getOrNull
|
||||
import com.lowe.common.services.CoinService
|
||||
import javax.inject.Inject
|
||||
|
||||
class CoinRepository @Inject constructor(private val service: CoinService) {
|
||||
|
||||
fun getCoinHistoryFlow() = Pager(
|
||||
PagingConfig(
|
||||
pageSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
initialLoadSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
enablePlaceholders = false
|
||||
)
|
||||
) {
|
||||
IntKeyPagingSource(service = service) { service, page, _ ->
|
||||
service.getMyCoinList(page).getOrNull()?.datas ?: emptyList()
|
||||
}
|
||||
}.flow
|
||||
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package com.lowe.wanandroid.ui.coin
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.paging.CombinedLoadStates
|
||||
import androidx.paging.LoadState
|
||||
import com.lowe.wanandroid.BaseActivity
|
||||
import com.lowe.common.base.SimpleDiffItemCallback
|
||||
import com.lowe.common.base.http.exception.ApiException
|
||||
import com.lowe.common.services.model.CoinHistory
|
||||
import com.lowe.common.services.model.UserBaseInfo
|
||||
import com.lowe.common.utils.*
|
||||
import com.lowe.multitype.PagingLoadStateAdapter
|
||||
import com.lowe.multitype.PagingMultiTypeAdapter
|
||||
import com.lowe.wanandroid.BR
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ActivityMyCoinInfoBinding
|
||||
import com.lowe.wanandroid.ui.ActivityDataBindingDelegate
|
||||
import com.lowe.wanandroid.ui.SimpleFooterItemBinder
|
||||
import com.lowe.wanandroid.ui.coin.item.CoinHistoryItemBinder
|
||||
import com.lowe.wanandroid.ui.coin.ranking.CoinRankingActivity
|
||||
import com.lowe.wanandroid.ui.web.WebActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 我的积分记录页面
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class MyCoinInfoActivity : BaseActivity<MyCoinInfoViewModel, ActivityMyCoinInfoBinding>() {
|
||||
|
||||
companion object {
|
||||
val COIN_DIFF_CALLBACK =
|
||||
SimpleDiffItemCallback<CoinHistory>(
|
||||
{ old, new -> old.id == new.id },
|
||||
{ old, new -> old == new }
|
||||
)
|
||||
}
|
||||
|
||||
private val coinHistoryAdapter = PagingMultiTypeAdapter(COIN_DIFF_CALLBACK).apply {
|
||||
register(CoinHistoryItemBinder())
|
||||
}
|
||||
|
||||
override val viewDataBinding: ActivityMyCoinInfoBinding by ActivityDataBindingDelegate(
|
||||
R.layout.activity_my_coin_info
|
||||
)
|
||||
override val viewModel: MyCoinInfoViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
initView()
|
||||
initEvent()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
with(viewDataBinding.coinList) {
|
||||
adapter = coinHistoryAdapter.withLoadStateFooter(
|
||||
PagingLoadStateAdapter(
|
||||
SimpleFooterItemBinder(),
|
||||
coinHistoryAdapter.types
|
||||
)
|
||||
)
|
||||
setHasFixedSize(true)
|
||||
}
|
||||
with(viewDataBinding.toolbar) {
|
||||
setNavigationOnClickListener { finish() }
|
||||
}
|
||||
viewDataBinding.coinRulesHelp.setOnClickListener {
|
||||
WebActivity.loadUrl(this, "https://www.wanandroid.com/blog/show/2653")
|
||||
}
|
||||
viewDataBinding.coinRanking.setOnClickListener {
|
||||
startActivity(Intent(this, CoinRankingActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvent() {
|
||||
launchRepeatOnStarted {
|
||||
launch {
|
||||
coinHistoryAdapter.loadStateFlow.collectLatest(this@MyCoinInfoActivity::updateLoadStates)
|
||||
}
|
||||
launch {
|
||||
viewModel.userBaseInfoFlow.collectLatest(this@MyCoinInfoActivity::updateUserCoinInfo)
|
||||
}
|
||||
launch {
|
||||
viewModel.coinHistoryFlow.collectLatest(coinHistoryAdapter::submitData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUserCoinInfo(info: UserBaseInfo) {
|
||||
viewDataBinding.userInfo = info
|
||||
viewDataBinding.notifyPropertyChanged(BR.userInfo)
|
||||
}
|
||||
|
||||
private fun updateLoadStates(loadStates: CombinedLoadStates) {
|
||||
loadStates.whenError {
|
||||
(it.error as? ApiException)?.message?.showShortToast()
|
||||
}
|
||||
viewDataBinding.loadingContainer.apply {
|
||||
emptyLayout.isVisible =
|
||||
loadStates.refresh is LoadState.NotLoading && coinHistoryAdapter.isEmpty()
|
||||
loadingProgress.isVisible = coinHistoryAdapter.isEmpty() && loadStates.isRefreshing
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.lowe.wanandroid.ui.coin
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.cachedIn
|
||||
import com.lowe.common.account.IAccountViewModelDelegate
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MyCoinInfoViewModel @Inject constructor(
|
||||
repository: CoinRepository,
|
||||
accountViewModelDelegate: IAccountViewModelDelegate
|
||||
) :
|
||||
BaseViewModel(), IAccountViewModelDelegate by accountViewModelDelegate {
|
||||
|
||||
val userBaseInfoFlow = accountViewModelDelegate.accountInfo
|
||||
|
||||
val coinHistoryFlow = repository.getCoinHistoryFlow().cachedIn(viewModelScope)
|
||||
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.lowe.wanandroid.ui.coin.item
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import com.lowe.multitype.PagingItemViewBinder
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ItemCoinHistoryLayoutBinding
|
||||
import com.lowe.common.services.model.CoinHistory
|
||||
import com.lowe.wanandroid.ui.ViewBindingHolder
|
||||
|
||||
class CoinHistoryItemBinder :
|
||||
PagingItemViewBinder<CoinHistory, ViewBindingHolder<ItemCoinHistoryLayoutBinding>>() {
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewBindingHolder<ItemCoinHistoryLayoutBinding>,
|
||||
item: CoinHistory
|
||||
) {
|
||||
holder.binding.apply {
|
||||
coinHistory = item
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup
|
||||
): ViewBindingHolder<ItemCoinHistoryLayoutBinding> =
|
||||
ViewBindingHolder(
|
||||
DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.item_coin_history_layout,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.lowe.wanandroid.ui.coin.ranking
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.paging.CombinedLoadStates
|
||||
import androidx.paging.LoadState
|
||||
import com.lowe.common.base.SimpleDiffItemCallback
|
||||
import com.lowe.common.services.model.CoinInfo
|
||||
import com.lowe.common.utils.isEmpty
|
||||
import com.lowe.common.utils.isRefreshing
|
||||
import com.lowe.common.utils.launchRepeatOnStarted
|
||||
import com.lowe.multitype.PagingLoadStateAdapter
|
||||
import com.lowe.multitype.PagingMultiTypeAdapter
|
||||
import com.lowe.wanandroid.BaseActivity
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ActivityCoinRankingBinding
|
||||
import com.lowe.wanandroid.ui.ActivityDataBindingDelegate
|
||||
import com.lowe.wanandroid.ui.SimpleFooterItemBinder
|
||||
import com.lowe.wanandroid.ui.coin.ranking.item.CoinInfoItemBinder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CoinRankingActivity : BaseActivity<CoinRankingViewModel, ActivityCoinRankingBinding>() {
|
||||
|
||||
companion object {
|
||||
private val COIN_DIFF_ITEM_CALLBACK =
|
||||
SimpleDiffItemCallback<CoinInfo>(
|
||||
{ old, new -> old.userId == new.userId },
|
||||
{ old, new -> old == new }
|
||||
)
|
||||
}
|
||||
|
||||
private val rankingAdapter = PagingMultiTypeAdapter(COIN_DIFF_ITEM_CALLBACK).apply {
|
||||
register(CoinInfoItemBinder())
|
||||
}
|
||||
|
||||
override val viewDataBinding: ActivityCoinRankingBinding by ActivityDataBindingDelegate(R.layout.activity_coin_ranking)
|
||||
|
||||
override val viewModel: CoinRankingViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
initView()
|
||||
initEvent()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
with(viewDataBinding.rankList) {
|
||||
adapter = rankingAdapter.withLoadStateFooter(
|
||||
PagingLoadStateAdapter(
|
||||
SimpleFooterItemBinder(),
|
||||
rankingAdapter.types
|
||||
)
|
||||
)
|
||||
setHasFixedSize(true)
|
||||
}
|
||||
viewDataBinding.toolbar.setNavigationOnClickListener { finish() }
|
||||
}
|
||||
|
||||
private fun initEvent() {
|
||||
launchRepeatOnStarted {
|
||||
launch {
|
||||
viewModel.coinRankingFlow.collectLatest(rankingAdapter::submitData)
|
||||
}
|
||||
launch {
|
||||
rankingAdapter.loadStateFlow.collectLatest(this@CoinRankingActivity::updateLoadStates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLoadStates(loadStates: CombinedLoadStates) {
|
||||
viewDataBinding.loadingContainer.apply {
|
||||
emptyLayout.isVisible =
|
||||
loadStates.refresh is LoadState.NotLoading && rankingAdapter.isEmpty()
|
||||
loadingProgress.isVisible = rankingAdapter.isEmpty() && loadStates.isRefreshing
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.lowe.wanandroid.ui.coin.ranking
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.base.IntKeyPagingSource
|
||||
import com.lowe.common.base.http.adapter.getOrNull
|
||||
import com.lowe.common.services.CoinService
|
||||
import javax.inject.Inject
|
||||
|
||||
class CoinRankingRepository @Inject constructor(private val service: CoinService) {
|
||||
|
||||
val coinRankingFlow = Pager(
|
||||
PagingConfig(
|
||||
pageSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
initialLoadSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
enablePlaceholders = false
|
||||
)
|
||||
) {
|
||||
IntKeyPagingSource(service = service) { service, page, _ ->
|
||||
service.getCoinRanking(page).getOrNull()?.datas ?: emptyList()
|
||||
}
|
||||
}.flow
|
||||
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.lowe.wanandroid.ui.coin.ranking
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.cachedIn
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class CoinRankingViewModel @Inject constructor(repository: CoinRankingRepository) :
|
||||
BaseViewModel() {
|
||||
|
||||
val coinRankingFlow = repository.coinRankingFlow.cachedIn(viewModelScope)
|
||||
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.lowe.wanandroid.ui.coin.ranking.item
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import com.lowe.common.services.model.CoinInfo
|
||||
import com.lowe.multitype.PagingItemViewBinder
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ItemCoinInfoLayoutBinding
|
||||
import com.lowe.wanandroid.ui.ViewBindingHolder
|
||||
|
||||
class CoinInfoItemBinder :
|
||||
PagingItemViewBinder<CoinInfo, ViewBindingHolder<ItemCoinInfoLayoutBinding>>() {
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewBindingHolder<ItemCoinInfoLayoutBinding>,
|
||||
item: CoinInfo
|
||||
) {
|
||||
holder.binding.apply {
|
||||
coinInfo = item
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup
|
||||
): ViewBindingHolder<ItemCoinInfoLayoutBinding> =
|
||||
ViewBindingHolder(
|
||||
DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.item_coin_info_layout,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package com.lowe.wanandroid.ui.collect
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.paging.CombinedLoadStates
|
||||
import androidx.paging.LoadState
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.lowe.common.base.app.AppViewModel
|
||||
import com.lowe.common.services.model.CollectBean
|
||||
import com.lowe.common.services.model.CollectEvent
|
||||
import com.lowe.common.utils.*
|
||||
import com.lowe.multitype.PagingLoadStateAdapter
|
||||
import com.lowe.multitype.PagingMultiTypeAdapter
|
||||
import com.lowe.resource.extension.getPrimaryColor
|
||||
import com.lowe.wanandroid.BaseActivity
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ActivityCollectBinding
|
||||
import com.lowe.wanandroid.ui.ActivityDataBindingDelegate
|
||||
import com.lowe.wanandroid.ui.ArticleDiffCalculator
|
||||
import com.lowe.wanandroid.ui.ItemClickType
|
||||
import com.lowe.wanandroid.ui.SimpleFooterItemBinder
|
||||
import com.lowe.wanandroid.ui.collect.item.CollectItemBinder
|
||||
import com.lowe.wanandroid.ui.web.WebActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 收藏页面
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class CollectActivity : BaseActivity<CollectViewModel, ActivityCollectBinding>() {
|
||||
|
||||
@Inject
|
||||
lateinit var appViewModel: AppViewModel
|
||||
|
||||
private val collectPagingAdapter =
|
||||
PagingMultiTypeAdapter(ArticleDiffCalculator.getCommonDiffItemCallback()).apply {
|
||||
register(CollectItemBinder(this@CollectActivity::onCollectClick))
|
||||
}
|
||||
|
||||
override val viewDataBinding: ActivityCollectBinding by ActivityDataBindingDelegate(R.layout.activity_collect)
|
||||
|
||||
override val viewModel: CollectViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.apply {
|
||||
with(collectList) {
|
||||
adapter = collectPagingAdapter.withLoadStateFooter(
|
||||
PagingLoadStateAdapter(
|
||||
SimpleFooterItemBinder(),
|
||||
collectPagingAdapter.types
|
||||
)
|
||||
)
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
setHasFixedSize(true)
|
||||
}
|
||||
with(toolbar) {
|
||||
setNavigationOnClickListener {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
with(collectSwipeRefresh) {
|
||||
setColorSchemeColors(context.getPrimaryColor())
|
||||
setOnRefreshListener {
|
||||
collectPagingAdapter.refresh()
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
launchRepeatOnStarted {
|
||||
launch {
|
||||
collectPagingAdapter.loadStateFlow.collectLatest { loadState ->
|
||||
loadState.whenError { it.error.message?.showShortToast() }
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
collectPagingAdapter.loadStateFlow.collectLatest(this@CollectActivity::updateLoadStates)
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.collectFlow.collectLatest(collectPagingAdapter::submitData)
|
||||
}
|
||||
}
|
||||
appViewModel.collectArticleEvent.observe(this) { event ->
|
||||
collectPagingAdapter.snapshot().run {
|
||||
val index = indexOfFirst { it is CollectBean && it.originId == event.id }
|
||||
if (index >= 0) {
|
||||
(this[index] as? CollectBean)?.collect = event.isCollected
|
||||
index
|
||||
} else null
|
||||
}?.apply(collectPagingAdapter::notifyItemChanged)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCollectClick(position: Int, collectBean: CollectBean, type: ItemClickType) {
|
||||
when (type) {
|
||||
ItemClickType.CONTENT -> WebActivity.loadUrl(
|
||||
this,
|
||||
Activities.Web.WebIntent(
|
||||
collectBean.link,
|
||||
collectBean.originId,
|
||||
collectBean.collect
|
||||
)
|
||||
)
|
||||
ItemClickType.COLLECT -> {
|
||||
/**
|
||||
* 这里需要使用的是[CollectBean.originId],而不是id
|
||||
*/
|
||||
appViewModel.articleCollectAction(
|
||||
CollectEvent(
|
||||
collectBean.originId,
|
||||
collectBean.link,
|
||||
collectBean.collect.not()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLoadStates(loadStates: CombinedLoadStates) {
|
||||
viewDataBinding.loadingContainer.apply {
|
||||
emptyLayout.isVisible =
|
||||
loadStates.refresh is LoadState.NotLoading && collectPagingAdapter.isEmpty()
|
||||
loadingProgress.isVisible = collectPagingAdapter.isEmpty() && loadStates.isRefreshing
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.lowe.wanandroid.ui.collect
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.cachedIn
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.services.repository.CollectRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class CollectViewModel @Inject constructor(repository: CollectRepository) :
|
||||
BaseViewModel() {
|
||||
|
||||
/**
|
||||
* 收藏文章列表Flow
|
||||
*/
|
||||
val collectFlow = repository.getCollectFlow().cachedIn(viewModelScope)
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.lowe.wanandroid.ui.collect.item
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import com.lowe.common.services.model.CollectBean
|
||||
import com.lowe.multitype.PagingItemViewBinder
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ItemCollectListArticleLayoutBinding
|
||||
import com.lowe.wanandroid.ui.ItemClickType
|
||||
import com.lowe.wanandroid.ui.ViewBindingHolder
|
||||
|
||||
class CollectItemBinder(private val onClick: (Int, CollectBean, type: ItemClickType) -> Unit) :
|
||||
PagingItemViewBinder<CollectBean, ViewBindingHolder<ItemCollectListArticleLayoutBinding>>() {
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewBindingHolder<ItemCollectListArticleLayoutBinding>,
|
||||
item: CollectBean
|
||||
) {
|
||||
holder.binding.apply {
|
||||
collect = item
|
||||
root.setOnClickListener { onClick(holder.bindingAdapterPosition, item, ItemClickType.CONTENT) }
|
||||
ivCollect.setOnClickListener { onClick(holder.bindingAdapterPosition, item, ItemClickType.COLLECT) }
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup
|
||||
): ViewBindingHolder<ItemCollectListArticleLayoutBinding> = ViewBindingHolder(
|
||||
DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.item_collect_list_article_layout,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.lowe.wanandroid.ui.group
|
||||
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.lowe.common.services.model.Classify
|
||||
import com.lowe.wanandroid.ui.group.child.GroupChildFragment
|
||||
|
||||
class GroupChildFragmentAdapter(
|
||||
var items: List<Classify>,
|
||||
fragmentManager: FragmentManager,
|
||||
lifecycle: Lifecycle
|
||||
) : FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun createFragment(position: Int) = GroupChildFragment.newInstance(items[position].id)
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.lowe.wanandroid.ui.group
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.lowe.wanandroid.BaseFragment
|
||||
import com.lowe.wanandroid.MainViewModel
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.FragmentGroupBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
/**
|
||||
* 公众号Tab Fragment
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class GroupFragment :
|
||||
BaseFragment<GroupViewModel, FragmentGroupBinding>(R.layout.fragment_group) {
|
||||
|
||||
private lateinit var childAdapter: GroupChildFragmentAdapter
|
||||
private lateinit var tabLayoutMediator: TabLayoutMediator
|
||||
private val mainViewModel by activityViewModels<MainViewModel>()
|
||||
|
||||
override val viewModel: GroupViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(savedInstanceState: Bundle?) {
|
||||
childAdapter =
|
||||
GroupChildFragmentAdapter(emptyList(), this.childFragmentManager, lifecycle)
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
viewModel.authorsNameLiveData.observe(viewLifecycleOwner) {
|
||||
childAdapter.items = it
|
||||
childAdapter.notifyDataSetChanged()
|
||||
}
|
||||
mainViewModel.mainTabDoubleClickLiveData.observe(viewLifecycleOwner) {
|
||||
if (childAdapter.items.isEmpty() || it != this.tag) return@observe
|
||||
viewModel.scrollToTopEvent(childAdapter.items[viewDataBinding.groupViewPager2.currentItem].id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.apply {
|
||||
with(groupViewPager2) {
|
||||
adapter = childAdapter
|
||||
}
|
||||
with(groupSwipeRefresh) {
|
||||
setOnRefreshListener {
|
||||
this@GroupFragment.viewModel.parentRefreshEvent(childAdapter.items[groupViewPager2.currentItem].id)
|
||||
this.isRefreshing = false
|
||||
}
|
||||
}
|
||||
tabLayoutMediator = TabLayoutMediator(
|
||||
viewDataBinding.groupTabLayout,
|
||||
viewDataBinding.groupViewPager2
|
||||
) { tab: TabLayout.Tab, position: Int ->
|
||||
tab.text = childAdapter.items[position].name
|
||||
}.apply(TabLayoutMediator::attach)
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.lowe.wanandroid.ui.group
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.base.IntKeyPagingSource
|
||||
import com.lowe.common.base.http.adapter.getOrNull
|
||||
import com.lowe.common.services.GroupService
|
||||
import javax.inject.Inject
|
||||
|
||||
class GroupRepository @Inject constructor(private val service: GroupService) {
|
||||
|
||||
suspend fun getAuthorTitleList() = service.getAuthorTitleList()
|
||||
|
||||
fun getAuthorArticles(id: Int) = Pager(
|
||||
PagingConfig(
|
||||
pageSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
initialLoadSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
enablePlaceholders = false
|
||||
)
|
||||
) {
|
||||
IntKeyPagingSource(service = service) { service, page, size ->
|
||||
service.getAuthorArticles(id, page, size).getOrNull()?.datas ?: emptyList()
|
||||
}
|
||||
}.flow
|
||||
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.lowe.wanandroid.ui.group
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.base.http.adapter.getOrElse
|
||||
import com.lowe.common.services.model.Classify
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class GroupViewModel @Inject constructor(private val repository: GroupRepository) :
|
||||
BaseViewModel() {
|
||||
|
||||
val authorsNameLiveData: LiveData<List<Classify>> = liveData {
|
||||
emit(
|
||||
repository.getAuthorTitleList().getOrElse { emptyList() }
|
||||
)
|
||||
}
|
||||
private val _parentRefreshLiveData = MutableLiveData<Int>()
|
||||
val parentRefreshLiveData: LiveData<Int> = _parentRefreshLiveData
|
||||
private val _scrollToTopLiveData = MutableLiveData<Int>()
|
||||
val scrollToTopLiveData: LiveData<Int> = _scrollToTopLiveData
|
||||
|
||||
/**
|
||||
* 用于公众号子Fragment滚到顶部
|
||||
*/
|
||||
fun scrollToTopEvent(id: Int) {
|
||||
_scrollToTopLiveData.value = id
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于公众号子Fragment触发刷新
|
||||
*/
|
||||
fun parentRefreshEvent(id: Int) {
|
||||
_parentRefreshLiveData.value = id
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package com.lowe.wanandroid.ui.group.child
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.paging.CombinedLoadStates
|
||||
import androidx.paging.LoadState
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.lowe.wanandroid.BaseFragment
|
||||
import com.lowe.common.base.app.AppViewModel
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.common.services.model.CollectEvent
|
||||
import com.lowe.common.utils.*
|
||||
import com.lowe.multitype.PagingLoadStateAdapter
|
||||
import com.lowe.multitype.PagingMultiTypeAdapter
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.FragmentChildGroupBinding
|
||||
import com.lowe.wanandroid.ui.ArticleDiffCalculator
|
||||
import com.lowe.wanandroid.ui.SimpleFooterItemBinder
|
||||
import com.lowe.wanandroid.ui.group.GroupViewModel
|
||||
import com.lowe.wanandroid.ui.home.item.ArticleAction
|
||||
import com.lowe.wanandroid.ui.home.item.HomeArticleItemBinderV2
|
||||
import com.lowe.wanandroid.ui.web.WebActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 公众号TabLayout下的子Fragment
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class GroupChildFragment :
|
||||
BaseFragment<GroupChildViewModel, FragmentChildGroupBinding>(R.layout.fragment_child_group) {
|
||||
|
||||
companion object {
|
||||
|
||||
const val KEY_BUNDLE_GROUP_CHILD_ID = "key_bundle_group_child_id"
|
||||
|
||||
fun newInstance(id: Int) =
|
||||
with(GroupChildFragment()) {
|
||||
arguments = bundleOf(
|
||||
KEY_BUNDLE_GROUP_CHILD_ID to id
|
||||
)
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var appViewModel: AppViewModel
|
||||
|
||||
private val authorId by unsafeLazy {
|
||||
arguments?.getInt(
|
||||
KEY_BUNDLE_GROUP_CHILD_ID, 0
|
||||
) ?: 0
|
||||
}
|
||||
|
||||
private val articlesAdapter =
|
||||
PagingMultiTypeAdapter(ArticleDiffCalculator.getCommonDiffItemCallback()).apply {
|
||||
register(HomeArticleItemBinderV2(this@GroupChildFragment::onArticleClick))
|
||||
}
|
||||
private val groupViewModel by viewModels<GroupViewModel>(this::requireParentFragment)
|
||||
|
||||
override val viewModel: GroupChildViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initEvents()
|
||||
viewModel.fetch(authorId)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.apply {
|
||||
with(childList) {
|
||||
adapter = articlesAdapter.withLoadStateFooter(
|
||||
PagingLoadStateAdapter(
|
||||
SimpleFooterItemBinder(),
|
||||
articlesAdapter.types
|
||||
)
|
||||
)
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
setHasFixedSize(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
launchRepeatOnStarted {
|
||||
launch {
|
||||
viewModel.groupArticlesFlow.collectLatest(articlesAdapter::submitData)
|
||||
}
|
||||
launch {
|
||||
articlesAdapter.loadStateFlow.collectLatest(this@GroupChildFragment::updateLoadStates)
|
||||
}
|
||||
}
|
||||
groupViewModel.apply {
|
||||
scrollToTopLiveData.observe(viewLifecycleOwner) {
|
||||
if (it == authorId) scrollToTop()
|
||||
}
|
||||
parentRefreshLiveData.observe(viewLifecycleOwner) {
|
||||
if (it == authorId) articlesAdapter.refresh()
|
||||
}
|
||||
}
|
||||
appViewModel.collectArticleEvent.observe(viewLifecycleOwner) { event ->
|
||||
articlesAdapter.snapshot().run {
|
||||
val index = indexOfFirst { it is Article && it.id == event.id }
|
||||
if (index >= 0) {
|
||||
(this[index] as? Article)?.collect = event.isCollected
|
||||
index
|
||||
} else null
|
||||
}?.apply(articlesAdapter::notifyItemChanged)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onArticleClick(articleAction: ArticleAction) {
|
||||
when (articleAction) {
|
||||
is ArticleAction.ItemClick -> WebActivity.loadUrl(
|
||||
requireContext(),
|
||||
Activities.Web.WebIntent(
|
||||
articleAction.article.link,
|
||||
articleAction.article.id,
|
||||
articleAction.article.collect
|
||||
)
|
||||
)
|
||||
is ArticleAction.CollectClick -> {
|
||||
appViewModel.articleCollectAction(
|
||||
CollectEvent(
|
||||
articleAction.article.id,
|
||||
articleAction.article.link,
|
||||
articleAction.article.collect.not()
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToTop() {
|
||||
viewDataBinding.childList.scrollToPosition(0)
|
||||
}
|
||||
|
||||
private fun updateLoadStates(loadStates: CombinedLoadStates) {
|
||||
viewDataBinding.loadingContainer.apply {
|
||||
emptyLayout.isVisible =
|
||||
loadStates.refresh is LoadState.NotLoading && articlesAdapter.isEmpty()
|
||||
loadingProgress.isVisible = articlesAdapter.isEmpty() && loadStates.isRefreshing
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.lowe.wanandroid.ui.group.child
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.cachedIn
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.utils.tryOffer
|
||||
import com.lowe.wanandroid.ui.group.GroupRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class GroupChildViewModel @Inject constructor(private val repository: GroupRepository) :
|
||||
BaseViewModel() {
|
||||
|
||||
private val _fetchGroupArticles = Channel<Int>(Channel.CONFLATED)
|
||||
|
||||
/**
|
||||
* 公众号文章Flow
|
||||
*/
|
||||
val groupArticlesFlow = _fetchGroupArticles.receiveAsFlow().flatMapLatest {
|
||||
repository.getAuthorArticles(it)
|
||||
}.cachedIn(viewModelScope)
|
||||
|
||||
fun fetch(id: Int) {
|
||||
_fetchGroupArticles.tryOffer(id)
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.lowe.wanandroid.ui.home
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.lowe.wanandroid.ui.home.child.answer.AnswerFragment
|
||||
import com.lowe.wanandroid.ui.home.child.explore.ExploreFragment
|
||||
import com.lowe.wanandroid.ui.home.child.square.SquareFragment
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
class HomeChildFragmentAdapter(
|
||||
var items: List<HomeTabBean>,
|
||||
fragmentManager: FragmentManager,
|
||||
lifecycle: Lifecycle
|
||||
) : FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||
|
||||
companion object {
|
||||
const val HOME_TAB_EXPLORE = "首页"
|
||||
const val HOME_TAB_SQUARE = "广场"
|
||||
const val HOME_TAB_ANSWER = "问答"
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (items[position].title) {
|
||||
HOME_TAB_EXPLORE -> ExploreFragment.newInstance(items[position])
|
||||
HOME_TAB_SQUARE -> SquareFragment.newInstance(items[position])
|
||||
HOME_TAB_ANSWER -> AnswerFragment.newInstance(items[position])
|
||||
else -> ExploreFragment.newInstance(items[position])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class HomeTabBean(
|
||||
val title: String
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.lowe.wanandroid.ui.home
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.lowe.wanandroid.BaseFragment
|
||||
import com.lowe.wanandroid.MainViewModel
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.FragmentHomeBinding
|
||||
import com.lowe.wanandroid.ui.search.SearchActivity
|
||||
|
||||
/**
|
||||
* 首页Tab
|
||||
*/
|
||||
class HomeFragment : BaseFragment<HomeViewModel, FragmentHomeBinding>(R.layout.fragment_home) {
|
||||
|
||||
companion object {
|
||||
|
||||
const val KEY_CHILD_HOME_TAB_PARCELABLE = "key_child_tab_parcelable"
|
||||
|
||||
}
|
||||
|
||||
private lateinit var childAdapter: HomeChildFragmentAdapter
|
||||
|
||||
private val mainViewModel by activityViewModels<MainViewModel>()
|
||||
|
||||
override val viewModel: HomeViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
childAdapter =
|
||||
HomeChildFragmentAdapter(generateHomeTabs(), this.childFragmentManager, lifecycle)
|
||||
viewDataBinding.apply {
|
||||
with(homeViewPager2) {
|
||||
adapter = childAdapter
|
||||
}
|
||||
with(searchIcon) {
|
||||
setOnClickListener {
|
||||
startActivity(Intent(this@HomeFragment.context, SearchActivity::class.java))
|
||||
}
|
||||
}
|
||||
TabLayoutMediator(
|
||||
homeTabLayout,
|
||||
homeViewPager2
|
||||
) { tab: TabLayout.Tab, position: Int ->
|
||||
tab.text = childAdapter.items[position].title
|
||||
}.apply(TabLayoutMediator::attach)
|
||||
with(swipeRefreshLayout) {
|
||||
setOnRefreshListener {
|
||||
this@HomeFragment.viewModel.refreshEvent(childAdapter.items[viewDataBinding.homeViewPager2.currentItem])
|
||||
this.isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
mainViewModel.apply {
|
||||
mainTabDoubleClickLiveData.observe(viewLifecycleOwner) {
|
||||
if (it == this@HomeFragment.tag){
|
||||
viewModel.scrollToTopEvent(childAdapter.items[viewDataBinding.homeViewPager2.currentItem])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateHomeTabs() = listOf(
|
||||
HomeTabBean(HomeChildFragmentAdapter.HOME_TAB_EXPLORE),
|
||||
HomeTabBean(HomeChildFragmentAdapter.HOME_TAB_SQUARE),
|
||||
HomeTabBean(HomeChildFragmentAdapter.HOME_TAB_ANSWER),
|
||||
)
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package com.lowe.wanandroid.ui.home
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.base.IntKeyPagingSource
|
||||
import com.lowe.common.base.http.adapter.getOrElse
|
||||
import com.lowe.common.base.http.adapter.getOrNull
|
||||
import com.lowe.common.services.BaseService
|
||||
import com.lowe.common.services.HomeService
|
||||
import com.lowe.common.services.model.Banners
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 首页Tab Repository
|
||||
*/
|
||||
class HomeRepository @Inject constructor(private val service: HomeService) {
|
||||
|
||||
private suspend fun getBanner() = service.getBanner()
|
||||
|
||||
private suspend fun getArticleTopList() = service.getArticleTopList()
|
||||
|
||||
fun getArticlePageList(pageSize: Int) =
|
||||
Pager(
|
||||
PagingConfig(
|
||||
pageSize = pageSize,
|
||||
initialLoadSize = pageSize,
|
||||
enablePlaceholders = false
|
||||
)
|
||||
) {
|
||||
IntKeyPagingSource(
|
||||
BaseService.DEFAULT_PAGE_START_NO,
|
||||
service = service
|
||||
) { service, page, size ->
|
||||
// 根据Page来区分是否需要拉取Banner和置顶文章
|
||||
if (page == BaseService.DEFAULT_PAGE_START_NO) {
|
||||
// 使用async并行请求
|
||||
val (articlesDeferred, topsDeferred, bannersDeferred) =
|
||||
supervisorScope {
|
||||
Triple(
|
||||
async { service.getArticlePageList(page, size) },
|
||||
async { getArticleTopList() },
|
||||
async { getBanner() })
|
||||
}
|
||||
val articles = articlesDeferred.await().getOrNull()?.datas ?: emptyList()
|
||||
val tops = topsDeferred.await().getOrElse { emptyList() }
|
||||
val banners = bannersDeferred.await().getOrElse { emptyList() }
|
||||
with(ArrayList<Any>(1 + tops.size + articles.size)) {
|
||||
if (banners.isNotEmpty()) {
|
||||
add(Banners(banners))
|
||||
}
|
||||
addAll(tops)
|
||||
addAll(articles)
|
||||
this
|
||||
}
|
||||
} else {
|
||||
// page不为0则只是加载更多即可
|
||||
service.getArticlePageList(page, size).getOrNull()?.datas ?: emptyList()
|
||||
}
|
||||
}
|
||||
}.flow
|
||||
|
||||
fun getSquarePageList(pageSize: Int) =
|
||||
Pager(
|
||||
PagingConfig(
|
||||
pageSize = pageSize,
|
||||
initialLoadSize = pageSize,
|
||||
enablePlaceholders = false
|
||||
)
|
||||
) {
|
||||
IntKeyPagingSource(
|
||||
BaseService.DEFAULT_PAGE_START_NO,
|
||||
service = service
|
||||
) { service, page, size ->
|
||||
service.getSquarePageList(page, size).getOrNull()?.datas ?: emptyList()
|
||||
}
|
||||
}.flow
|
||||
|
||||
fun getAnswerPageList() =
|
||||
Pager(
|
||||
PagingConfig(
|
||||
pageSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
initialLoadSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
enablePlaceholders = false
|
||||
)
|
||||
) {
|
||||
IntKeyPagingSource(
|
||||
BaseService.DEFAULT_PAGE_START_NO_1,
|
||||
service = service
|
||||
) { service, page, _ ->
|
||||
service.getAnswerPageList(page).getOrNull()?.datas ?: emptyList()
|
||||
}
|
||||
}.flow
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.lowe.wanandroid.ui.home
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
|
||||
class HomeViewModel : BaseViewModel() {
|
||||
|
||||
private val _scrollToTopLiveData = MutableLiveData<HomeTabBean>()
|
||||
val scrollToTopLiveData: LiveData<HomeTabBean> = _scrollToTopLiveData
|
||||
private val _refreshLiveData = MutableLiveData<HomeTabBean>()
|
||||
val refreshLiveData: LiveData<HomeTabBean> = _refreshLiveData
|
||||
|
||||
fun scrollToTopEvent(tabBean: HomeTabBean) {
|
||||
_scrollToTopLiveData.value = tabBean
|
||||
}
|
||||
|
||||
fun refreshEvent(tabBean: HomeTabBean) {
|
||||
_refreshLiveData.value = tabBean
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package com.lowe.wanandroid.ui.home.child.answer
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.paging.CombinedLoadStates
|
||||
import androidx.paging.LoadState
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.lowe.wanandroid.BaseFragment
|
||||
import com.lowe.common.base.app.AppViewModel
|
||||
import com.lowe.common.compat.BundleCompat
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.common.services.model.CollectEvent
|
||||
import com.lowe.common.utils.*
|
||||
import com.lowe.multitype.PagingLoadStateAdapter
|
||||
import com.lowe.multitype.PagingMultiTypeAdapter
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.FragmentHomeChildAnswerBinding
|
||||
import com.lowe.wanandroid.ui.ArticleDiffCalculator
|
||||
import com.lowe.wanandroid.ui.SimpleFooterItemBinder
|
||||
import com.lowe.wanandroid.ui.home.HomeChildFragmentAdapter
|
||||
import com.lowe.wanandroid.ui.home.HomeFragment
|
||||
import com.lowe.wanandroid.ui.home.HomeTabBean
|
||||
import com.lowe.wanandroid.ui.home.HomeViewModel
|
||||
import com.lowe.wanandroid.ui.home.item.ArticleAction
|
||||
import com.lowe.wanandroid.ui.home.item.HomeArticleItemBinderV2
|
||||
import com.lowe.wanandroid.ui.web.WebActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 问答页面
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class AnswerFragment :
|
||||
BaseFragment<AnswerViewModel, FragmentHomeChildAnswerBinding>(R.layout.fragment_home_child_answer) {
|
||||
|
||||
companion object {
|
||||
fun newInstance(homeTabBean: HomeTabBean) = with(AnswerFragment()) {
|
||||
arguments = bundleOf(
|
||||
HomeFragment.KEY_CHILD_HOME_TAB_PARCELABLE to homeTabBean
|
||||
)
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var appViewModel: AppViewModel
|
||||
|
||||
private val homeViewModel by viewModels<HomeViewModel>(this::requireParentFragment)
|
||||
private val squareTabBean by unsafeLazy {
|
||||
BundleCompat.getParcelable(arguments, HomeFragment.KEY_CHILD_HOME_TAB_PARCELABLE)
|
||||
?: HomeTabBean(HomeChildFragmentAdapter.HOME_TAB_ANSWER)
|
||||
}
|
||||
private val answerAdapter =
|
||||
PagingMultiTypeAdapter(ArticleDiffCalculator.getCommonDiffItemCallback()).apply {
|
||||
register(HomeArticleItemBinderV2(this@AnswerFragment::onItemClick))
|
||||
}
|
||||
|
||||
override val viewModel: AnswerViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.apply {
|
||||
with(answerList) {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = answerAdapter.withLoadStateFooter(PagingLoadStateAdapter(SimpleFooterItemBinder(), answerAdapter.types))
|
||||
setHasFixedSize(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
launchRepeatOnStarted {
|
||||
launch {
|
||||
viewModel.getAnswerListFlow.collectLatest(answerAdapter::submitData)
|
||||
}
|
||||
launch {
|
||||
answerAdapter.loadStateFlow.collectLatest(this@AnswerFragment::updateLoadStates)
|
||||
}
|
||||
}
|
||||
homeViewModel.apply {
|
||||
scrollToTopLiveData.observe(viewLifecycleOwner) {
|
||||
if (it.title == squareTabBean.title) scrollToTop()
|
||||
}
|
||||
refreshLiveData.observe(viewLifecycleOwner) {
|
||||
if (it.title == squareTabBean.title) answerAdapter.refresh()
|
||||
}
|
||||
}
|
||||
appViewModel.collectArticleEvent.observe(viewLifecycleOwner) { event ->
|
||||
answerAdapter.snapshot().run {
|
||||
val index = indexOfFirst { it is Article && it.id == event.id }
|
||||
if (index >= 0) {
|
||||
(this[index] as? Article)?.collect = event.isCollected
|
||||
index
|
||||
} else null
|
||||
}?.apply(answerAdapter::notifyItemChanged)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToTop() {
|
||||
viewDataBinding.answerList.scrollToPosition(0)
|
||||
}
|
||||
|
||||
private fun onItemClick(articleAction: ArticleAction) {
|
||||
when (articleAction) {
|
||||
is ArticleAction.ItemClick -> WebActivity.loadUrl(
|
||||
requireContext(),
|
||||
Activities.Web.WebIntent(
|
||||
articleAction.article.link,
|
||||
articleAction.article.id,
|
||||
articleAction.article.collect
|
||||
)
|
||||
)
|
||||
is ArticleAction.CollectClick -> {
|
||||
appViewModel.articleCollectAction(
|
||||
CollectEvent(
|
||||
articleAction.article.id,
|
||||
articleAction.article.link,
|
||||
articleAction.article.collect.not()
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLoadStates(loadStates: CombinedLoadStates) {
|
||||
viewDataBinding.loadingContainer.apply {
|
||||
emptyLayout.isVisible =
|
||||
loadStates.refresh is LoadState.NotLoading && answerAdapter.isEmpty()
|
||||
loadingProgress.isVisible = answerAdapter.isEmpty() && loadStates.isRefreshing
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.lowe.wanandroid.ui.home.child.answer
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.cachedIn
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.wanandroid.ui.home.HomeRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AnswerViewModel @Inject constructor(repository: HomeRepository) :
|
||||
BaseViewModel() {
|
||||
|
||||
/**
|
||||
* 问答列表数据Flow
|
||||
*/
|
||||
val getAnswerListFlow = repository.getAnswerPageList().cachedIn(viewModelScope)
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
package com.lowe.wanandroid.ui.home.child.explore
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.paging.CombinedLoadStates
|
||||
import androidx.paging.LoadState
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.lowe.wanandroid.BaseFragment
|
||||
import com.lowe.common.base.app.AppViewModel
|
||||
import com.lowe.common.compat.BundleCompat
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.common.services.model.Banner
|
||||
import com.lowe.common.services.model.CollectEvent
|
||||
import com.lowe.common.utils.*
|
||||
import com.lowe.multitype.PagingLoadStateAdapter
|
||||
import com.lowe.multitype.PagingMultiTypeAdapter
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.FragmentHomeChildExploreBinding
|
||||
import com.lowe.wanandroid.ui.ArticleDiffCalculator
|
||||
import com.lowe.wanandroid.ui.SimpleFooterItemBinder
|
||||
import com.lowe.wanandroid.ui.home.HomeChildFragmentAdapter
|
||||
import com.lowe.wanandroid.ui.home.HomeFragment
|
||||
import com.lowe.wanandroid.ui.home.HomeTabBean
|
||||
import com.lowe.wanandroid.ui.home.HomeViewModel
|
||||
import com.lowe.wanandroid.ui.home.item.ArticleAction
|
||||
import com.lowe.wanandroid.ui.home.item.HomeArticleItemBinderV2
|
||||
import com.lowe.wanandroid.ui.home.item.HomeBannerItemBinder
|
||||
import com.lowe.wanandroid.ui.web.WebActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 首页页面
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class ExploreFragment :
|
||||
BaseFragment<ExploreViewModel, FragmentHomeChildExploreBinding>(R.layout.fragment_home_child_explore) {
|
||||
|
||||
companion object {
|
||||
private const val KEY_HOME_FRAGMENT_LIST_SAVE_STATE = "key_home_fragment_list_save_state"
|
||||
|
||||
fun newInstance(homeTabBean: HomeTabBean): ExploreFragment = with(ExploreFragment()) {
|
||||
arguments = bundleOf(
|
||||
HomeFragment.KEY_CHILD_HOME_TAB_PARCELABLE to homeTabBean
|
||||
)
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var appViewModel: AppViewModel
|
||||
|
||||
private val homeAdapter =
|
||||
PagingMultiTypeAdapter(ArticleDiffCalculator.getCommonDiffItemCallback()).apply {
|
||||
register(HomeBannerItemBinder(this@ExploreFragment::onBannerItemClick))
|
||||
register(HomeArticleItemBinderV2(this@ExploreFragment::onItemClick))
|
||||
}
|
||||
private val homeViewModel by viewModels<HomeViewModel>(this::requireParentFragment)
|
||||
private val exploreTabBean by unsafeLazy {
|
||||
BundleCompat.getParcelable(arguments, HomeFragment.KEY_CHILD_HOME_TAB_PARCELABLE)
|
||||
?: HomeTabBean(HomeChildFragmentAdapter.HOME_TAB_EXPLORE)
|
||||
}
|
||||
|
||||
override val viewModel: ExploreViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initEvents()
|
||||
if (savedInstanceState == null) {
|
||||
onRefresh()
|
||||
} else {
|
||||
viewDataBinding.homeList.layoutManager?.onRestoreInstanceState(
|
||||
BundleCompat.getParcelable(
|
||||
savedInstanceState,
|
||||
KEY_HOME_FRAGMENT_LIST_SAVE_STATE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putParcelable(
|
||||
KEY_HOME_FRAGMENT_LIST_SAVE_STATE,
|
||||
viewDataBinding.homeList.layoutManager?.onSaveInstanceState()
|
||||
)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.homeList.apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = homeAdapter.withLoadStateFooter(
|
||||
PagingLoadStateAdapter(
|
||||
SimpleFooterItemBinder(),
|
||||
homeAdapter.types
|
||||
)
|
||||
)
|
||||
setHasFixedSize(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
launchRepeatOnStarted {
|
||||
launch {
|
||||
viewModel.getArticlesFlow.collectLatest(homeAdapter::submitData)
|
||||
}
|
||||
launch {
|
||||
homeAdapter.loadStateFlow.collectLatest(this@ExploreFragment::updateLoadStates)
|
||||
}
|
||||
}
|
||||
homeViewModel.apply {
|
||||
scrollToTopLiveData.observe(viewLifecycleOwner) {
|
||||
if (it.title == exploreTabBean.title) scrollToTop()
|
||||
}
|
||||
refreshLiveData.observe(viewLifecycleOwner) {
|
||||
if (it.title == exploreTabBean.title) onRefresh()
|
||||
}
|
||||
}
|
||||
appViewModel.collectArticleEvent.observe(viewLifecycleOwner) { event ->
|
||||
homeAdapter.snapshot().run {
|
||||
val index = indexOfFirst { it is Article && it.id == event.id }
|
||||
if (index >= 0) {
|
||||
(this[index] as? Article)?.collect = event.isCollected
|
||||
index
|
||||
} else null
|
||||
}?.apply(homeAdapter::notifyItemChanged)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToTop() {
|
||||
viewDataBinding.homeList.scrollToPosition(0)
|
||||
}
|
||||
|
||||
private fun onRefresh() {
|
||||
homeAdapter.refresh()
|
||||
}
|
||||
|
||||
private fun onBannerItemClick(data: Banner, position: Int) {
|
||||
WebActivity.loadUrl(this.requireContext(), data.url)
|
||||
}
|
||||
|
||||
private fun onItemClick(articleAction: ArticleAction) {
|
||||
when (articleAction) {
|
||||
is ArticleAction.ItemClick -> WebActivity.loadUrl(
|
||||
requireContext(),
|
||||
Activities.Web.WebIntent(
|
||||
articleAction.article.link,
|
||||
articleAction.article.id,
|
||||
articleAction.article.collect,
|
||||
)
|
||||
)
|
||||
is ArticleAction.CollectClick -> {
|
||||
appViewModel.articleCollectAction(
|
||||
CollectEvent(
|
||||
articleAction.article.id,
|
||||
articleAction.article.link,
|
||||
articleAction.article.collect.not()
|
||||
)
|
||||
)
|
||||
}
|
||||
is ArticleAction.AuthorClick -> {
|
||||
startActivity(intentTo(Activities.ShareList(bundle = bundleOf(Activities.ShareList.KEY_SHARE_LIST_USER_ID to articleAction.article.userId.toString()))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLoadStates(loadStates: CombinedLoadStates) {
|
||||
viewDataBinding.loadingContainer.apply {
|
||||
emptyLayout.isVisible =
|
||||
loadStates.refresh is LoadState.NotLoading && homeAdapter.isEmpty()
|
||||
loadingProgress.isVisible = homeAdapter.isEmpty() && loadStates.isRefreshing
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.lowe.wanandroid.ui.home.child.explore
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.cachedIn
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.wanandroid.ui.home.HomeRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ExploreViewModel @Inject constructor(repository: HomeRepository) :
|
||||
BaseViewModel() {
|
||||
|
||||
/**
|
||||
* 首页列表数据Flow
|
||||
*/
|
||||
val getArticlesFlow = repository.getArticlePageList(20).cachedIn(viewModelScope)
|
||||
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
package com.lowe.wanandroid.ui.home.child.square
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.paging.CombinedLoadStates
|
||||
import androidx.paging.LoadState
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.lowe.wanandroid.BaseFragment
|
||||
import com.lowe.common.base.app.AppViewModel
|
||||
import com.lowe.common.compat.BundleCompat
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.common.services.model.CollectEvent
|
||||
import com.lowe.common.utils.*
|
||||
import com.lowe.multitype.PagingLoadStateAdapter
|
||||
import com.lowe.multitype.PagingMultiTypeAdapter
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.FragmentHomeChildSquareBinding
|
||||
import com.lowe.wanandroid.ui.ArticleDiffCalculator
|
||||
import com.lowe.wanandroid.ui.SimpleFooterItemBinder
|
||||
import com.lowe.wanandroid.ui.home.HomeChildFragmentAdapter
|
||||
import com.lowe.wanandroid.ui.home.HomeFragment
|
||||
import com.lowe.wanandroid.ui.home.HomeTabBean
|
||||
import com.lowe.wanandroid.ui.home.HomeViewModel
|
||||
import com.lowe.wanandroid.ui.home.item.ArticleAction
|
||||
import com.lowe.wanandroid.ui.home.item.HomeArticleItemBinderV2
|
||||
import com.lowe.wanandroid.ui.web.WebActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 广场页面
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class SquareFragment :
|
||||
BaseFragment<SquareViewModel, FragmentHomeChildSquareBinding>(R.layout.fragment_home_child_square) {
|
||||
|
||||
companion object {
|
||||
fun newInstance(homeTabBean: HomeTabBean) = with(SquareFragment()) {
|
||||
arguments = bundleOf(
|
||||
HomeFragment.KEY_CHILD_HOME_TAB_PARCELABLE to homeTabBean
|
||||
)
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var appViewModel: AppViewModel
|
||||
|
||||
private val homeViewModel by viewModels<HomeViewModel>(this::requireParentFragment)
|
||||
private val squareTabBean by unsafeLazy {
|
||||
BundleCompat.getParcelable(arguments, HomeFragment.KEY_CHILD_HOME_TAB_PARCELABLE)
|
||||
?: HomeTabBean(HomeChildFragmentAdapter.HOME_TAB_SQUARE)
|
||||
}
|
||||
private val squareAdapter =
|
||||
PagingMultiTypeAdapter(ArticleDiffCalculator.getCommonDiffItemCallback()).apply {
|
||||
register(HomeArticleItemBinderV2(this@SquareFragment::onItemClick))
|
||||
}
|
||||
|
||||
override val viewModel: SquareViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.apply {
|
||||
with(squareList) {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = squareAdapter.withLoadStateFooter(
|
||||
PagingLoadStateAdapter(
|
||||
SimpleFooterItemBinder(),
|
||||
squareAdapter.types
|
||||
)
|
||||
)
|
||||
setHasFixedSize(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
launchRepeatOnStarted {
|
||||
launch {
|
||||
viewModel.getSquareFlow.collectLatest(squareAdapter::submitData)
|
||||
}
|
||||
launch {
|
||||
squareAdapter.loadStateFlow.collectLatest(this@SquareFragment::updateLoadStates)
|
||||
}
|
||||
}
|
||||
homeViewModel.apply {
|
||||
scrollToTopLiveData.observe(viewLifecycleOwner) {
|
||||
if (it.title == squareTabBean.title) scrollToTop()
|
||||
}
|
||||
refreshLiveData.observe(viewLifecycleOwner) {
|
||||
if (it.title == squareTabBean.title) onRefresh()
|
||||
}
|
||||
}
|
||||
appViewModel.collectArticleEvent.observe(viewLifecycleOwner) { event ->
|
||||
squareAdapter.snapshot().run {
|
||||
val index = indexOfFirst { it is Article && it.id == event.id }
|
||||
if (index >= 0) {
|
||||
(this[index] as? Article)?.collect = event.isCollected
|
||||
index
|
||||
} else null
|
||||
}?.apply(squareAdapter::notifyItemChanged)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToTop() {
|
||||
viewDataBinding.squareList.scrollToPosition(0)
|
||||
}
|
||||
|
||||
private fun onRefresh() {
|
||||
squareAdapter.refresh()
|
||||
}
|
||||
|
||||
private fun onItemClick(articleAction: ArticleAction) {
|
||||
when (articleAction) {
|
||||
is ArticleAction.ItemClick -> WebActivity.loadUrl(
|
||||
requireContext(),
|
||||
Activities.Web.WebIntent(
|
||||
articleAction.article.link,
|
||||
articleAction.article.id,
|
||||
articleAction.article.collect,
|
||||
)
|
||||
)
|
||||
is ArticleAction.CollectClick -> {
|
||||
appViewModel.articleCollectAction(
|
||||
CollectEvent(
|
||||
articleAction.article.id,
|
||||
articleAction.article.link,
|
||||
articleAction.article.collect.not()
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLoadStates(loadStates: CombinedLoadStates) {
|
||||
viewDataBinding.loadingContainer.apply {
|
||||
emptyLayout.isVisible =
|
||||
loadStates.refresh is LoadState.NotLoading && squareAdapter.isEmpty()
|
||||
loadingProgress.isVisible = squareAdapter.isEmpty() && loadStates.isRefreshing
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.lowe.wanandroid.ui.home.child.square
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.cachedIn
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.wanandroid.ui.home.HomeRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SquareViewModel @Inject constructor(repository: HomeRepository) :
|
||||
BaseViewModel() {
|
||||
|
||||
val getSquareFlow = repository.getSquarePageList(DEFAULT_PAGE_SIZE).cachedIn(viewModelScope)
|
||||
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package com.lowe.wanandroid.ui.home.item
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.multitype.PagingItemViewBinder
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ItemHomeArticleLayoutV2Binding
|
||||
import com.lowe.wanandroid.ui.ViewBindingHolder
|
||||
|
||||
/**
|
||||
* 文章[PagingItemViewBinder]
|
||||
*/
|
||||
class HomeArticleItemBinderV2(private val onClick: (ArticleAction) -> Unit) :
|
||||
PagingItemViewBinder<Article, ViewBindingHolder<ItemHomeArticleLayoutV2Binding>>() {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup
|
||||
): ViewBindingHolder<ItemHomeArticleLayoutV2Binding> {
|
||||
return ViewBindingHolder(
|
||||
DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.item_home_article_layout_v2,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewBindingHolder<ItemHomeArticleLayoutV2Binding>,
|
||||
item: Article
|
||||
) {
|
||||
holder.binding.apply {
|
||||
root.setOnClickListener {
|
||||
onClick(
|
||||
ArticleAction.ItemClick(
|
||||
holder.bindingAdapterPosition,
|
||||
item
|
||||
)
|
||||
)
|
||||
}
|
||||
ivCollect.setOnClickListener {
|
||||
onClick(
|
||||
ArticleAction.CollectClick(
|
||||
holder.bindingAdapterPosition,
|
||||
item
|
||||
)
|
||||
)
|
||||
}
|
||||
tvAuthor.setOnClickListener {
|
||||
onClick(
|
||||
ArticleAction.AuthorClick(
|
||||
holder.bindingAdapterPosition,
|
||||
item
|
||||
)
|
||||
)
|
||||
}
|
||||
article = item
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章行为
|
||||
*/
|
||||
sealed interface ArticleAction {
|
||||
|
||||
/**
|
||||
* 主体点击
|
||||
*/
|
||||
data class ItemClick(val position: Int, val article: Article) : ArticleAction
|
||||
|
||||
/**
|
||||
* 收藏点击
|
||||
*/
|
||||
data class CollectClick(val position: Int, val article: Article) : ArticleAction
|
||||
|
||||
/**
|
||||
* 作者点击
|
||||
*/
|
||||
data class AuthorClick(val position: Int, val article: Article) : ArticleAction
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.lowe.wanandroid.ui.home.item
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.facebook.drawee.view.SimpleDraweeView
|
||||
import com.lowe.common.services.model.Banner
|
||||
import com.youth.banner.adapter.BannerAdapter
|
||||
|
||||
class HomeBannerAdapter(items: List<Banner>, private val onClick: (Banner, Int) -> Unit) :
|
||||
BannerAdapter<Banner, HomeBannerAdapter.BannerViewHolder>(items) {
|
||||
|
||||
class BannerViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
||||
|
||||
override fun onCreateHolder(parent: ViewGroup, viewType: Int): BannerViewHolder {
|
||||
return BannerViewHolder(SimpleDraweeView(parent.context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onBindView(holder: BannerViewHolder, data: Banner, position: Int, size: Int) {
|
||||
(holder.view as? SimpleDraweeView)?.apply {
|
||||
setImageURI(data.imagePath)
|
||||
setOnClickListener { onClick(data, position) }
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.lowe.wanandroid.ui.home.item
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import com.lowe.common.services.model.Banner
|
||||
import com.lowe.common.services.model.Banners
|
||||
import com.lowe.multitype.PagingItemViewBinder
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ItemHomeBannerLayoutBinding
|
||||
import com.lowe.wanandroid.ui.ViewBindingHolder
|
||||
import com.youth.banner.indicator.CircleIndicator
|
||||
|
||||
class HomeBannerItemBinder(onClick: (Banner, Int) -> Unit) :
|
||||
PagingItemViewBinder<Banners, ViewBindingHolder<ItemHomeBannerLayoutBinding>>() {
|
||||
|
||||
private val bannerAdapter = HomeBannerAdapter(emptyList(), onClick)
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup
|
||||
): ViewBindingHolder<ItemHomeBannerLayoutBinding> {
|
||||
return ViewBindingHolder(
|
||||
DataBindingUtil.inflate<ItemHomeBannerLayoutBinding?>(
|
||||
inflater,
|
||||
R.layout.item_home_banner_layout,
|
||||
parent,
|
||||
false
|
||||
).apply {
|
||||
with(banner) {
|
||||
setAdapter(bannerAdapter)
|
||||
indicator = CircleIndicator(context)
|
||||
addBannerLifecycleObserver(findViewTreeLifecycleOwner())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewBindingHolder<ItemHomeBannerLayoutBinding>,
|
||||
item: Banners
|
||||
) {
|
||||
bannerAdapter.setDatas(item.banners)
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.lowe.wanandroid.ui.login
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.isVisible
|
||||
import com.lowe.common.account.LocalUserInfo
|
||||
import com.lowe.common.base.http.adapter.isSuccess
|
||||
import com.lowe.common.utils.unsafeLazy
|
||||
import com.lowe.wanandroid.BaseActivity
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ActivityLoginBinding
|
||||
import com.lowe.wanandroid.ui.ActivityDataBindingDelegate
|
||||
import com.lowe.wanandroid.ui.login.register.RegisterDialog
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
/**
|
||||
* 登录页面
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class LoginActivity : BaseActivity<LoginViewModel, ActivityLoginBinding>() {
|
||||
|
||||
private val registerDialog by unsafeLazy {
|
||||
RegisterDialog(
|
||||
this,
|
||||
viewModel
|
||||
)
|
||||
}
|
||||
|
||||
override val viewDataBinding: ActivityLoginBinding by ActivityDataBindingDelegate(R.layout.activity_login)
|
||||
override val viewModel: LoginViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
overridePendingTransition(R.anim.activity_anim_login_in, R.anim.activity_anim_dummy)
|
||||
super.onCreate(savedInstanceState)
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.viewModel = viewModel
|
||||
viewDataBinding.loginButton.setOnClickListener {
|
||||
updateLoginLoadingStatus(true)
|
||||
viewModel.userLogin(
|
||||
LocalUserInfo(
|
||||
viewModel.userNameObservable.get().toString(),
|
||||
viewModel.passwordObservable.get().toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
viewDataBinding.register.setOnClickListener { registerDialog.show() }
|
||||
viewDataBinding.backIcon.setOnClickListener { finish() }
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
viewModel.apply {
|
||||
loginLiveData.observe(this@LoginActivity) {
|
||||
updateLoginLoadingStatus(false)
|
||||
if (it.isSuccess) finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLoginLoadingStatus(isLoading: Boolean) {
|
||||
viewDataBinding.loginLoading.isVisible = isLoading
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.lowe.wanandroid.ui.login
|
||||
|
||||
import androidx.databinding.ObservableBoolean
|
||||
import androidx.databinding.ObservableField
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.lowe.common.account.IAccountViewModelDelegate
|
||||
import com.lowe.common.account.LocalUserInfo
|
||||
import com.lowe.common.account.RegisterInfo
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.base.http.adapter.NetworkResponse
|
||||
import com.lowe.common.services.model.User
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LoginViewModel @Inject constructor(
|
||||
private val accountViewModelDelegate: IAccountViewModelDelegate
|
||||
) :
|
||||
BaseViewModel(), IAccountViewModelDelegate by accountViewModelDelegate {
|
||||
|
||||
private val _loginLiveData = MutableLiveData<NetworkResponse<User>>()
|
||||
val loginLiveData: LiveData<NetworkResponse<User>> = _loginLiveData
|
||||
|
||||
private val _registerLiveData = MutableLiveData<NetworkResponse<Any>>()
|
||||
val registerLiveData: LiveData<NetworkResponse<Any>> = _registerLiveData
|
||||
|
||||
/**
|
||||
* 数据绑定
|
||||
*/
|
||||
val userNameObservable = ObservableField<String>()
|
||||
val passwordObservable = ObservableField<String>()
|
||||
val loginEnable = object : ObservableBoolean(userNameObservable, passwordObservable) {
|
||||
|
||||
override fun get() =
|
||||
userNameObservable.get()?.trim().isNullOrBlank().not() && passwordObservable.get()
|
||||
?.trim().isNullOrBlank().not()
|
||||
}
|
||||
|
||||
fun userLogin(userInfo: LocalUserInfo) {
|
||||
viewModelScope.launch {
|
||||
_loginLiveData.value = accountViewModelDelegate.login(userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
fun userRegister(registerInfo: RegisterInfo) {
|
||||
viewModelScope.launch {
|
||||
_registerLiveData.value = accountViewModelDelegate.register(registerInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package com.lowe.wanandroid.ui.login.register
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ObservableBoolean
|
||||
import androidx.databinding.ObservableField
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.lowe.common.account.RegisterInfo
|
||||
import com.lowe.common.base.http.adapter.isSuccess
|
||||
import com.lowe.common.utils.showShortToast
|
||||
import com.lowe.common.utils.unsafeLazy
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.DialogRegisterAccountLayoutBinding
|
||||
import com.lowe.wanandroid.ui.login.LoginActivity
|
||||
import com.lowe.wanandroid.ui.login.LoginViewModel
|
||||
|
||||
class RegisterDialog(activity: LoginActivity, viewModel: LoginViewModel) {
|
||||
|
||||
val userNameObservable = ObservableField<String>()
|
||||
val passwordObservable = ObservableField<String>()
|
||||
val confirmPasswordObservable = ObservableField<String>()
|
||||
val loginEnable = object :
|
||||
ObservableBoolean(userNameObservable, passwordObservable, confirmPasswordObservable) {
|
||||
override fun get() =
|
||||
userNameObservable.get()?.trim().isNullOrBlank().not()
|
||||
&& passwordObservable.get()?.trim().isNullOrBlank().not()
|
||||
&& confirmPasswordObservable.get()?.trim().isNullOrBlank().not()
|
||||
}
|
||||
|
||||
private val dialogViewDataBinding: DialogRegisterAccountLayoutBinding =
|
||||
DataBindingUtil.inflate<DialogRegisterAccountLayoutBinding?>(
|
||||
LayoutInflater.from(activity),
|
||||
R.layout.dialog_register_account_layout,
|
||||
null,
|
||||
false
|
||||
).also {
|
||||
it.lifecycleOwner = activity
|
||||
}
|
||||
|
||||
private val registerDialog by unsafeLazy {
|
||||
MaterialAlertDialogBuilder(activity).setView(dialogViewDataBinding.root)
|
||||
.setCancelable(true)
|
||||
.create()
|
||||
}
|
||||
|
||||
init {
|
||||
dialogViewDataBinding.apply {
|
||||
dialog = this@RegisterDialog
|
||||
loginButton.setOnClickListener {
|
||||
val name = userNameObservable.get()?.trim().toString()
|
||||
val password = passwordObservable.get()?.trim().toString()
|
||||
val confirmPassword = confirmPasswordObservable.get()?.trim().toString()
|
||||
if (checkRegisterStatus(name, password, confirmPassword)) {
|
||||
viewModel.userRegister(RegisterInfo(name, password, confirmPassword))
|
||||
dialogViewDataBinding.Loading.isVisible = true
|
||||
}
|
||||
}
|
||||
userName.doAfterTextChanged {
|
||||
if (userNameInputLayout.error.isNullOrBlank().not()) userNameInputLayout.error = ""
|
||||
}
|
||||
password.doAfterTextChanged {
|
||||
if (passwordInputLayout.error.isNullOrBlank().not()) passwordInputLayout.error = ""
|
||||
}
|
||||
confirmPassword.doAfterTextChanged {
|
||||
if (confirmPasswordInputLayout.error.isNullOrBlank()
|
||||
.not()
|
||||
) confirmPasswordInputLayout.error = ""
|
||||
}
|
||||
|
||||
}
|
||||
viewModel.registerLiveData.observe(activity) {
|
||||
if (it.isSuccess) {
|
||||
registerDialog.dismiss()
|
||||
activity.getString(R.string.register_success).showShortToast()
|
||||
}
|
||||
dialogViewDataBinding.Loading.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
fun show() {
|
||||
registerDialog.show()
|
||||
}
|
||||
|
||||
private fun checkRegisterStatus(
|
||||
username: String,
|
||||
password: String,
|
||||
confirmPassword: String
|
||||
): Boolean {
|
||||
if (username.length < 3) {
|
||||
dialogViewDataBinding.userNameInputLayout.error = "用户名最少3位"
|
||||
return false
|
||||
}
|
||||
if (password.length < 6) {
|
||||
dialogViewDataBinding.passwordInputLayout.error = "密码至少 6 位"
|
||||
return false
|
||||
}
|
||||
if (confirmPassword.length < 6) {
|
||||
dialogViewDataBinding.confirmPasswordInputLayout.error = "密码至少 6 位"
|
||||
return false
|
||||
}
|
||||
if (password != confirmPassword) {
|
||||
dialogViewDataBinding.confirmPasswordInputLayout.error = "确认密码与密码不符"
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.lowe.wanandroid.ui.message
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.lowe.wanandroid.BaseActivity
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ActivityMessageBinding
|
||||
import com.lowe.wanandroid.ui.ActivityDataBindingDelegate
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
/**
|
||||
* 消息页面
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class MessageActivity : BaseActivity<MessageViewModel, ActivityMessageBinding>() {
|
||||
|
||||
private lateinit var childAdapter: MessageChildFragmentAdapter
|
||||
|
||||
override val viewDataBinding: ActivityMessageBinding by ActivityDataBindingDelegate(R.layout.activity_message)
|
||||
|
||||
override val viewModel: MessageViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
initView()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
childAdapter =
|
||||
MessageChildFragmentAdapter(generateMessageTabs(), supportFragmentManager, lifecycle)
|
||||
|
||||
viewDataBinding.apply {
|
||||
with(messageViewPager2) {
|
||||
adapter = childAdapter
|
||||
}
|
||||
TabLayoutMediator(
|
||||
messageTabLayout,
|
||||
messageViewPager2
|
||||
) { tab: TabLayout.Tab, position: Int ->
|
||||
tab.text = childAdapter.items[position].title
|
||||
}.apply(TabLayoutMediator::attach)
|
||||
backIcon.setOnClickListener { finish() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateMessageTabs() = listOf(
|
||||
MessageTabBean(MessageChildFragmentAdapter.MESSAGE_TAB_NEW),
|
||||
MessageTabBean(MessageChildFragmentAdapter.MESSAGE_TAB_HISTORY),
|
||||
)
|
||||
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.lowe.wanandroid.ui.message
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.lowe.wanandroid.ui.message.child.MessageTabChildFragment
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
class MessageChildFragmentAdapter(
|
||||
var items: List<MessageTabBean>,
|
||||
fragmentManager: FragmentManager,
|
||||
lifecycle: Lifecycle
|
||||
) : FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||
|
||||
companion object {
|
||||
const val MESSAGE_TAB_NEW = "新消息"
|
||||
const val MESSAGE_TAB_HISTORY = "历史消息"
|
||||
|
||||
const val KEY_MESSAGE_CHILD_TAB_PARCELABLE = "key_message_child_tab_parcelable"
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return MessageTabChildFragment.newInstance(items[position])
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class MessageTabBean(
|
||||
val title: String
|
||||
) : Parcelable
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.lowe.wanandroid.ui.message
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.base.IntKeyPagingSource
|
||||
import com.lowe.common.base.http.adapter.getOrNull
|
||||
import com.lowe.common.services.ProfileService
|
||||
import javax.inject.Inject
|
||||
|
||||
class MessageRepository @Inject constructor(private val service: ProfileService) {
|
||||
|
||||
/**
|
||||
* 已读消息Flow
|
||||
*/
|
||||
fun getReadiedMsgFlow() = Pager(
|
||||
PagingConfig(
|
||||
pageSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
initialLoadSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
enablePlaceholders = false
|
||||
)
|
||||
) {
|
||||
IntKeyPagingSource(service = service) { service, page, _ ->
|
||||
service.getReadiedMessageList(page).getOrNull()?.datas ?: emptyList()
|
||||
}
|
||||
}.flow
|
||||
|
||||
/**
|
||||
* 未读消息Flow
|
||||
*/
|
||||
fun getUnreadMsgFlow() = Pager(
|
||||
PagingConfig(
|
||||
pageSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
initialLoadSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
enablePlaceholders = false
|
||||
)
|
||||
) {
|
||||
IntKeyPagingSource(service = service) { service, page, _ ->
|
||||
service.getUnReadMessageList(page).getOrNull()?.datas ?: emptyList()
|
||||
}
|
||||
}.flow
|
||||
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.lowe.wanandroid.ui.message
|
||||
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
|
||||
class MessageViewModel : BaseViewModel()
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.lowe.wanandroid.ui.message
|
||||
|
||||
data class UnreadMessage(
|
||||
val count: Int = 0
|
||||
)
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.lowe.wanandroid.ui.message
|
||||
|
||||
import com.lowe.common.base.http.adapter.getOrNull
|
||||
import com.lowe.common.di.ApplicationScope
|
||||
import com.lowe.common.di.IoDispatcher
|
||||
import com.lowe.common.services.usecase.UnreadMessageCountUseCase
|
||||
import com.lowe.common.account.IAccountViewModelDelegate
|
||||
import com.lowe.common.account.isLogin
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class UnreadMessageManager @Inject constructor(
|
||||
private val unreadMessageCountUseCase: UnreadMessageCountUseCase,
|
||||
private val accountViewModelDelegate: IAccountViewModelDelegate,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
|
||||
) : IAccountViewModelDelegate by accountViewModelDelegate {
|
||||
|
||||
|
||||
private val _unreadMessage: MutableStateFlow<UnreadMessage> = MutableStateFlow(UnreadMessage(0))
|
||||
val unreadMessage: StateFlow<UnreadMessage> = _unreadMessage
|
||||
|
||||
private val profileUnread = MutableStateFlow(true)
|
||||
|
||||
val profileUnreadState =
|
||||
unreadMessage.map { it.count != 0 }.combine(profileUnread) { unread, profile ->
|
||||
unread && profile
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
accountViewModelDelegate.accountState.collect {
|
||||
_unreadMessage.emit(
|
||||
UnreadMessage(
|
||||
if (it.isLogin) unreadMessageCountUseCase(Unit).getOrNull() ?: 0 else 0
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun clearProfileUnread() {
|
||||
profileUnread.tryEmit(false)
|
||||
}
|
||||
|
||||
fun clearUnreadMessage() {
|
||||
_unreadMessage.tryEmit(UnreadMessage(0))
|
||||
}
|
||||
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package com.lowe.wanandroid.ui.message.child
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.paging.CombinedLoadStates
|
||||
import androidx.paging.LoadState
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.lowe.wanandroid.BaseFragment
|
||||
import com.lowe.common.compat.BundleCompat
|
||||
import com.lowe.common.services.model.MsgBean
|
||||
import com.lowe.common.utils.*
|
||||
import com.lowe.multitype.PagingMultiTypeAdapter
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.FragmentMessageChildListBinding
|
||||
import com.lowe.wanandroid.ui.ArticleDiffCalculator
|
||||
import com.lowe.wanandroid.ui.message.MessageChildFragmentAdapter
|
||||
import com.lowe.wanandroid.ui.message.MessageTabBean
|
||||
import com.lowe.wanandroid.ui.web.WebActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 消息子Fragment
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class MessageTabChildFragment :
|
||||
BaseFragment<MessageTabChildViewModel, FragmentMessageChildListBinding>(R.layout.fragment_message_child_list) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance(tabBean: MessageTabBean) =
|
||||
with(MessageTabChildFragment()) {
|
||||
arguments =
|
||||
bundleOf(MessageChildFragmentAdapter.KEY_MESSAGE_CHILD_TAB_PARCELABLE to tabBean)
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
private val messagesAdapter =
|
||||
PagingMultiTypeAdapter(ArticleDiffCalculator.getCommonDiffItemCallback()).apply {
|
||||
register(MessageTabChildItemBinder(this@MessageTabChildFragment::onMsgItemClick))
|
||||
}
|
||||
private val messageTabBean by unsafeLazy {
|
||||
BundleCompat.getParcelable(
|
||||
arguments,
|
||||
MessageChildFragmentAdapter.KEY_MESSAGE_CHILD_TAB_PARCELABLE
|
||||
) ?: MessageTabBean(MessageChildFragmentAdapter.MESSAGE_TAB_NEW)
|
||||
}
|
||||
|
||||
override val viewModel: MessageTabChildViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.apply {
|
||||
with(messageList) {
|
||||
adapter = messagesAdapter
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
setHasFixedSize(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
launchRepeatOnStarted {
|
||||
launch {
|
||||
messagesAdapter.loadStateFlow.collectLatest { loadState ->
|
||||
loadState.whenError { it.error.message?.showShortToast() }
|
||||
updateLoadStates(loadState)
|
||||
}
|
||||
}
|
||||
launch {
|
||||
if (messageTabBean.title == MessageChildFragmentAdapter.MESSAGE_TAB_NEW) {
|
||||
viewModel.getUnreadMsgFlow.onStart {
|
||||
viewModel.clearUnreadMessage()
|
||||
}
|
||||
} else {
|
||||
viewModel.getReadiedMsgFlow
|
||||
}.collectLatest(messagesAdapter::submitData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMsgItemClick(position: Int, msgBean: MsgBean) {
|
||||
WebActivity.loadUrl(requireContext(), msgBean.fullLink)
|
||||
}
|
||||
|
||||
private fun updateLoadStates(loadStates: CombinedLoadStates) {
|
||||
viewDataBinding.loadingContainer.apply {
|
||||
emptyLayout.isVisible =
|
||||
loadStates.refresh is LoadState.NotLoading && messagesAdapter.isEmpty()
|
||||
loadingProgress.isVisible = messagesAdapter.isEmpty() && loadStates.isRefreshing
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.lowe.wanandroid.ui.message.child
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import com.lowe.common.services.model.MsgBean
|
||||
import com.lowe.multitype.PagingItemViewBinder
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ItemMessageChildLayoutBinding
|
||||
import com.lowe.wanandroid.ui.ViewBindingHolder
|
||||
|
||||
class MessageTabChildItemBinder(private val onClick: (Int, MsgBean) -> Unit) :
|
||||
PagingItemViewBinder<MsgBean, ViewBindingHolder<ItemMessageChildLayoutBinding>>() {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup
|
||||
): ViewBindingHolder<ItemMessageChildLayoutBinding> = ViewBindingHolder(
|
||||
DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.item_message_child_layout,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewBindingHolder<ItemMessageChildLayoutBinding>,
|
||||
item: MsgBean
|
||||
) {
|
||||
holder.binding.apply {
|
||||
onClickFunc = onClick
|
||||
msg = item
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.lowe.wanandroid.ui.message.child
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.cachedIn
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.wanandroid.ui.message.MessageRepository
|
||||
import com.lowe.wanandroid.ui.message.UnreadMessageManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MessageTabChildViewModel @Inject constructor(
|
||||
repository: MessageRepository,
|
||||
private val unreadMessageManager: UnreadMessageManager
|
||||
) :
|
||||
BaseViewModel() {
|
||||
|
||||
val getReadiedMsgFlow = repository.getReadiedMsgFlow().cachedIn(viewModelScope)
|
||||
|
||||
val getUnreadMsgFlow = repository.getUnreadMsgFlow().cachedIn(viewModelScope)
|
||||
|
||||
fun clearUnreadMessage() = unreadMessageManager.clearUnreadMessage()
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.lowe.wanandroid.ui.navigator
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.lowe.wanandroid.ui.navigator.child.navigator.NavigatorChildFragment
|
||||
import com.lowe.wanandroid.ui.navigator.child.series.SeriesChildFragment
|
||||
import com.lowe.wanandroid.ui.navigator.child.tutorial.TutorialChildFragment
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
class NavigatorChildFragmentAdapter(
|
||||
var items: List<NavigatorTabBean>,
|
||||
fragmentManager: FragmentManager,
|
||||
lifecycle: Lifecycle
|
||||
) : FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||
|
||||
companion object {
|
||||
const val NAVIGATOR_TAB_NAVIGATOR = "导航"
|
||||
const val NAVIGATOR_TAB_SERIES = "体系"
|
||||
const val NAVIGATOR_TAB_TUTORIAL = "教程"
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (items[position].title) {
|
||||
NAVIGATOR_TAB_NAVIGATOR -> NavigatorChildFragment.newInstance(items[position])
|
||||
NAVIGATOR_TAB_SERIES -> SeriesChildFragment.newInstance(items[position])
|
||||
NAVIGATOR_TAB_TUTORIAL -> TutorialChildFragment.newInstance(items[position])
|
||||
else -> NavigatorChildFragment.newInstance(items[position])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class NavigatorTabBean(
|
||||
val title: String
|
||||
) : Parcelable
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.lowe.wanandroid.ui.navigator
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.lowe.wanandroid.BaseFragment
|
||||
import com.lowe.wanandroid.MainViewModel
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.FragmentNotificationsBinding
|
||||
|
||||
/**
|
||||
* 导航Fragment
|
||||
*/
|
||||
class NavigatorFragment :
|
||||
BaseFragment<NavigatorViewModel, FragmentNotificationsBinding>(R.layout.fragment_notifications) {
|
||||
|
||||
companion object {
|
||||
const val KEY_NAVIGATOR_CHILD_HOME_TAB_PARCELABLE = "key_navigator_child_tab_parcelable"
|
||||
}
|
||||
|
||||
private lateinit var childAdapter: NavigatorChildFragmentAdapter
|
||||
private val mainViewModel by activityViewModels<MainViewModel>()
|
||||
|
||||
override val viewModel: NavigatorViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
childAdapter =
|
||||
NavigatorChildFragmentAdapter(
|
||||
generateNavigatorTabs(),
|
||||
this.childFragmentManager,
|
||||
lifecycle
|
||||
)
|
||||
viewDataBinding.apply {
|
||||
with(navigatorPager2) {
|
||||
adapter = childAdapter
|
||||
}
|
||||
TabLayoutMediator(
|
||||
navigatorTabLayout,
|
||||
navigatorPager2
|
||||
) { tab: TabLayout.Tab, position: Int ->
|
||||
tab.text = childAdapter.items[position].title
|
||||
}.apply(TabLayoutMediator::attach)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
mainViewModel.apply {
|
||||
mainTabDoubleClickLiveData.observe(viewLifecycleOwner) {
|
||||
if (it == this@NavigatorFragment.tag) {
|
||||
viewModel.scrollToTopEvent(childAdapter.items[viewDataBinding.navigatorPager2.currentItem])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateNavigatorTabs() = listOf(
|
||||
NavigatorTabBean(NavigatorChildFragmentAdapter.NAVIGATOR_TAB_NAVIGATOR),
|
||||
NavigatorTabBean(NavigatorChildFragmentAdapter.NAVIGATOR_TAB_SERIES),
|
||||
NavigatorTabBean(NavigatorChildFragmentAdapter.NAVIGATOR_TAB_TUTORIAL)
|
||||
)
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.lowe.wanandroid.ui.navigator
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import com.lowe.common.base.IntKeyPagingSource
|
||||
import com.lowe.common.base.http.adapter.getOrNull
|
||||
import com.lowe.common.services.BaseService
|
||||
import com.lowe.common.services.NavigatorService
|
||||
import javax.inject.Inject
|
||||
|
||||
class NavigatorRepository @Inject constructor(private val service: NavigatorService) {
|
||||
|
||||
suspend fun getNavigationList() = service.getNavigationList()
|
||||
|
||||
suspend fun getTreeList() = service.getTreeList()
|
||||
|
||||
suspend fun getTutorialList() = service.getTutorialList()
|
||||
|
||||
suspend fun getTutorialChapterList(id: Int, orderType: Int = 1) =
|
||||
service.getTutorialChapterList(id, orderType)
|
||||
|
||||
fun getSeriesDetailList(id: Int, size: Int) =
|
||||
Pager(
|
||||
PagingConfig(
|
||||
pageSize = size,
|
||||
initialLoadSize = size,
|
||||
enablePlaceholders = false
|
||||
)
|
||||
) {
|
||||
IntKeyPagingSource(
|
||||
BaseService.DEFAULT_PAGE_START_NO,
|
||||
service = service
|
||||
) { service, page, size ->
|
||||
service.getSeriesDetailList(page, id, size).getOrNull()?.datas ?: emptyList()
|
||||
}
|
||||
}.flow
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.lowe.wanandroid.ui.navigator
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
|
||||
class NavigatorViewModel : BaseViewModel() {
|
||||
|
||||
private val _scrollToTopLiveData = MutableLiveData<NavigatorTabBean>()
|
||||
val scrollToTopLiveData: LiveData<NavigatorTabBean> = _scrollToTopLiveData
|
||||
|
||||
fun scrollToTopEvent(tabBean: NavigatorTabBean) {
|
||||
_scrollToTopLiveData.value = tabBean
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.navigator
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.lowe.wanandroid.BaseFragment
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.common.services.model.Navigation
|
||||
import com.lowe.common.utils.Activities
|
||||
import com.lowe.common.utils.smoothSnapToPosition
|
||||
import com.lowe.multitype.MultiTypeAdapter
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.FragmentNavigatorChildNavigatorBinding
|
||||
import com.lowe.wanandroid.ui.navigator.NavigatorFragment
|
||||
import com.lowe.wanandroid.ui.navigator.NavigatorTabBean
|
||||
import com.lowe.wanandroid.ui.navigator.child.navigator.item.NavigatorChildTagChildrenItemBinder
|
||||
import com.lowe.wanandroid.ui.navigator.widgets.NavigatorTagOnScrollListener
|
||||
import com.lowe.wanandroid.ui.web.WebActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 导航子Fragment页面
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class NavigatorChildFragment :
|
||||
BaseFragment<NavigatorChildViewModel, FragmentNavigatorChildNavigatorBinding>(R.layout.fragment_navigator_child_navigator) {
|
||||
|
||||
companion object {
|
||||
fun newInstance(navigatorTabBean: NavigatorTabBean): NavigatorChildFragment =
|
||||
with(NavigatorChildFragment()) {
|
||||
arguments =
|
||||
bundleOf(NavigatorFragment.KEY_NAVIGATOR_CHILD_HOME_TAB_PARCELABLE to navigatorTabBean)
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
private val tagChildrenAdapter = MultiTypeAdapter()
|
||||
private val tagOnScrollListener = NavigatorTagOnScrollListener()
|
||||
|
||||
/**
|
||||
* 延迟到verticalTagList已经加载完成后再启动协程
|
||||
*/
|
||||
private val tagChildrenFirstCompletelyVisiblePosChange: Job =
|
||||
lifecycleScope.launch(start = CoroutineStart.LAZY) {
|
||||
tagOnScrollListener.firstCompletelyVisiblePosChange.collect(this@NavigatorChildFragment::tagSelectedChange)
|
||||
}
|
||||
override val viewModel: NavigatorChildViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.apply {
|
||||
with(tagChildrenList) {
|
||||
tagChildrenAdapter.register(NavigatorChildTagChildrenItemBinder(this@NavigatorChildFragment::onTagChildrenItemClick))
|
||||
adapter = tagChildrenAdapter
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
addOnScrollListener(tagOnScrollListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
viewModel.apply {
|
||||
navigationTagListLiveData.observe(viewLifecycleOwner) {
|
||||
dispatchToAdapter(it, tagChildrenAdapter)
|
||||
generateVerticalScrollChipGroup(it.first)
|
||||
if (tagChildrenFirstCompletelyVisiblePosChange.isActive.not()) {
|
||||
tagChildrenFirstCompletelyVisiblePosChange.start()
|
||||
}
|
||||
viewDataBinding.loadingContainer.loadingProgress.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成竖向列表Chip
|
||||
*/
|
||||
private fun generateVerticalScrollChipGroup(tags: List<Any>) {
|
||||
viewDataBinding.verticalTagList.setViews(
|
||||
tags.map {
|
||||
val navigation = it as Navigation
|
||||
Chip(
|
||||
this.requireContext(),
|
||||
null,
|
||||
com.google.android.material.R.style.Widget_MaterialComponents_Chip_Choice
|
||||
).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
setChipBackgroundColorResource(R.color.choice_chip_background_color)
|
||||
text = navigation.name
|
||||
textSize = 13F
|
||||
chipStartPadding = 0F
|
||||
chipEndPadding = 0F
|
||||
textStartPadding = 0F
|
||||
textEndPadding = 0F
|
||||
isCheckable = true
|
||||
isCheckedIconVisible = false
|
||||
gravity = Gravity.CENTER
|
||||
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
if (paint.measureText(text.toString()) > viewDataBinding.verticalTagList.width) {
|
||||
textSize = 11F
|
||||
}
|
||||
}
|
||||
}.onEach { chip ->
|
||||
chip.setOnClickListener {
|
||||
onTagClick(
|
||||
viewDataBinding.verticalTagList.getChipGroup().indexOfChild(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun dispatchToAdapter(
|
||||
result: Pair<List<Any>, DiffUtil.DiffResult>,
|
||||
adapter: MultiTypeAdapter
|
||||
) {
|
||||
adapter.items = result.first
|
||||
result.second.dispatchUpdatesTo(adapter)
|
||||
}
|
||||
|
||||
private fun onTagClick(pos: Int) {
|
||||
viewDataBinding.tagChildrenList.smoothSnapToPosition(pos)
|
||||
}
|
||||
|
||||
private fun tagSelectedChange(pos: Int) {
|
||||
viewDataBinding.verticalTagList.checkByPosition(pos)
|
||||
}
|
||||
|
||||
private fun onTagChildrenItemClick(article: Article) {
|
||||
context?.apply {
|
||||
WebActivity.loadUrl(
|
||||
this, Activities.Web.WebIntent(article.link)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.navigator
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.base.http.adapter.getOrElse
|
||||
import com.lowe.wanandroid.ui.ArticleDiffCalculator
|
||||
import com.lowe.wanandroid.ui.navigator.NavigatorRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NavigatorChildViewModel @Inject constructor(private val repository: NavigatorRepository) :
|
||||
BaseViewModel() {
|
||||
|
||||
/**
|
||||
* 获取导航数据
|
||||
*/
|
||||
val navigationTagListLiveData: LiveData<Pair<List<Any>, DiffUtil.DiffResult>> = liveData {
|
||||
emit(
|
||||
getDiffResultPair(
|
||||
this.latestValue?.first ?: emptyList(),
|
||||
repository.getNavigationList().getOrElse { emptyList() }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDiffResultPair(
|
||||
oldList: List<Any>,
|
||||
newList: List<Any>
|
||||
) = newList to DiffUtil.calculateDiff(
|
||||
ArticleDiffCalculator.getCommonDiffCallback(
|
||||
oldList,
|
||||
newList
|
||||
)
|
||||
)
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.navigator.item
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.common.services.model.Navigation
|
||||
import com.lowe.common.utils.fromHtml
|
||||
import com.lowe.multitype.ItemViewBinder
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ItemNavigatorChildTagChildrenLayoutBinding
|
||||
import com.lowe.wanandroid.ui.ViewBindingHolder
|
||||
import com.lowe.wanandroid.widgets.CommonTagChipWidget
|
||||
|
||||
class NavigatorChildTagChildrenItemBinder(private val onTagChildrenClick: (Article) -> Unit) :
|
||||
ItemViewBinder<Navigation, ViewBindingHolder<ItemNavigatorChildTagChildrenLayoutBinding>>() {
|
||||
override fun onCreateViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup
|
||||
): ViewBindingHolder<ItemNavigatorChildTagChildrenLayoutBinding> {
|
||||
return ViewBindingHolder(
|
||||
DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.item_navigator_child_tag_children_layout,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewBindingHolder<ItemNavigatorChildTagChildrenLayoutBinding>,
|
||||
item: Navigation
|
||||
) {
|
||||
holder.binding.apply {
|
||||
name = item.name
|
||||
executePendingBindings()
|
||||
tagChildrenLayout.removeAllViews()
|
||||
item.articles.forEach { article ->
|
||||
val tv = CommonTagChipWidget.generateTextViewChip(
|
||||
this.root.context,
|
||||
ViewGroup.MarginLayoutParams(
|
||||
ViewGroup.MarginLayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.MarginLayoutParams.WRAP_CONTENT
|
||||
)
|
||||
)
|
||||
tv.text = article.title.fromHtml()
|
||||
tv.setOnClickListener { onTagChildrenClick(article) }
|
||||
tagChildrenLayout.addView(tv)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.series
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.lowe.wanandroid.BaseFragment
|
||||
import com.lowe.common.services.model.Classify
|
||||
import com.lowe.common.services.model.Series
|
||||
import com.lowe.common.utils.smoothSnapToPosition
|
||||
import com.lowe.multitype.MultiTypeAdapter
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.FragmentNavigatorChildSeriesBinding
|
||||
import com.lowe.wanandroid.ui.navigator.NavigatorFragment
|
||||
import com.lowe.wanandroid.ui.navigator.NavigatorTabBean
|
||||
import com.lowe.wanandroid.ui.navigator.child.series.detail.SeriesDetailListActivity
|
||||
import com.lowe.wanandroid.ui.navigator.child.series.item.SeriesChildTagChildrenItemBinder
|
||||
import com.lowe.wanandroid.ui.navigator.widgets.NavigatorTagOnScrollListener
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 体系在导航Tab下的子Fragment页面
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class SeriesChildFragment :
|
||||
BaseFragment<SeriesChildViewModel, FragmentNavigatorChildSeriesBinding>(R.layout.fragment_navigator_child_series) {
|
||||
|
||||
companion object {
|
||||
fun newInstance(navigatorTabBean: NavigatorTabBean): SeriesChildFragment = with(
|
||||
SeriesChildFragment()
|
||||
) {
|
||||
arguments =
|
||||
bundleOf(NavigatorFragment.KEY_NAVIGATOR_CHILD_HOME_TAB_PARCELABLE to navigatorTabBean)
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
private val tagChildrenAdapter = MultiTypeAdapter()
|
||||
private val tagOnScrollListener = NavigatorTagOnScrollListener()
|
||||
private val tagChildrenFirstCompletelyVisiblePosChange: Job =
|
||||
lifecycleScope.launch(start = CoroutineStart.LAZY) {
|
||||
tagOnScrollListener.firstCompletelyVisiblePosChange.collect(this@SeriesChildFragment::tagSelectedChange)
|
||||
}
|
||||
|
||||
override val viewModel: SeriesChildViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.apply {
|
||||
with(seriesTagChildrenList) {
|
||||
tagChildrenAdapter.register(SeriesChildTagChildrenItemBinder(this@SeriesChildFragment::onTagChildrenItemClick))
|
||||
adapter = tagChildrenAdapter
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
addOnScrollListener(tagOnScrollListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
viewModel.apply {
|
||||
seriesListLiveData.observe(viewLifecycleOwner) {
|
||||
dispatchToAdapter(it, tagChildrenAdapter)
|
||||
generateVerticalScrollChipGroup(it.first)
|
||||
if (tagChildrenFirstCompletelyVisiblePosChange.isActive.not()) {
|
||||
tagChildrenFirstCompletelyVisiblePosChange.start()
|
||||
}
|
||||
viewDataBinding.loadingContainer.loadingProgress.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatchToAdapter(
|
||||
result: Pair<List<Any>, DiffUtil.DiffResult>,
|
||||
adapter: MultiTypeAdapter
|
||||
) {
|
||||
adapter.items = result.first
|
||||
result.second.dispatchUpdatesTo(adapter)
|
||||
}
|
||||
|
||||
private fun generateVerticalScrollChipGroup(tags: List<Any>) {
|
||||
viewDataBinding.seriesTagList.setViews(
|
||||
tags.map {
|
||||
val series = it as Series
|
||||
Chip(
|
||||
this.requireContext(),
|
||||
null,
|
||||
com.google.android.material.R.style.Widget_MaterialComponents_Chip_Choice
|
||||
).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
setChipBackgroundColorResource(R.color.choice_chip_background_color)
|
||||
text = series.name
|
||||
textSize = 13F
|
||||
chipStartPadding = 0F
|
||||
chipEndPadding = 0F
|
||||
textStartPadding = 0F
|
||||
textEndPadding = 0F
|
||||
isCheckable = true
|
||||
isCheckedIconVisible = false
|
||||
gravity = Gravity.CENTER
|
||||
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
if (paint.measureText(text.toString()) > viewDataBinding.seriesTagList.width) {
|
||||
textSize = 11F
|
||||
}
|
||||
}
|
||||
}.onEach { chip ->
|
||||
chip.setOnClickListener {
|
||||
onTagClick(
|
||||
viewDataBinding.seriesTagList.getChipGroup().indexOfChild(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onTagClick(pos: Int) {
|
||||
viewDataBinding.seriesTagChildrenList.smoothSnapToPosition(pos)
|
||||
}
|
||||
|
||||
private fun onTagChildrenItemClick(classify: Classify) {
|
||||
val series = viewModel.getCurrentList()
|
||||
.find { it is Series && it.id == classify.parentChapterId } as? Series ?: return
|
||||
startActivity(Intent(this.context, SeriesDetailListActivity::class.java).apply {
|
||||
putParcelableArrayListExtra(
|
||||
SeriesDetailListActivity.KEY_BUNDLE_CLASSIFY_LIST_TAB,
|
||||
series.children as ArrayList<out Parcelable>
|
||||
)
|
||||
putExtra(
|
||||
SeriesDetailListActivity.KEY_BUNDLE_INIT_TAB_INDEX,
|
||||
series.children.indexOf(classify)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private fun tagSelectedChange(pos: Int) {
|
||||
viewDataBinding.seriesTagList.checkByPosition(pos)
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.series
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.lowe.common.base.http.adapter.getOrElse
|
||||
import com.lowe.wanandroid.ui.ArticleDiffCalculator
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.wanandroid.ui.navigator.NavigatorRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SeriesChildViewModel @Inject constructor(private val repository: NavigatorRepository) :
|
||||
BaseViewModel() {
|
||||
|
||||
val seriesListLiveData: LiveData<Pair<List<Any>, DiffUtil.DiffResult>> = liveData {
|
||||
val series = repository.getTreeList().getOrElse { emptyList() }
|
||||
val oldList = this.latestValue?.first ?: emptyList()
|
||||
emit(getDiffResultPair(oldList, oldList + series))
|
||||
}
|
||||
|
||||
fun getCurrentList() = seriesListLiveData.value?.first ?: emptyList()
|
||||
|
||||
private fun getDiffResultPair(oldList: List<Any>, newList: List<Any>) =
|
||||
newList to DiffUtil.calculateDiff(
|
||||
ArticleDiffCalculator.getCommonDiffCallback(
|
||||
oldList,
|
||||
newList
|
||||
)
|
||||
)
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.series.detail
|
||||
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.lowe.common.services.model.Classify
|
||||
import com.lowe.wanandroid.ui.navigator.child.series.detail.child.SeriesDetailChildFragment
|
||||
|
||||
class SeriesDetailFragmentStateAdapter(
|
||||
var items: List<Classify>,
|
||||
fragmentManager: FragmentManager,
|
||||
lifecycle: Lifecycle
|
||||
) : FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun createFragment(position: Int) =
|
||||
SeriesDetailChildFragment.newInstance(items[position])
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.series.detail
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ActivitySeriesDetailListLayoutBinding
|
||||
import com.lowe.common.services.model.Classify
|
||||
import com.lowe.wanandroid.ui.ActivityDataBindingDelegate
|
||||
import com.lowe.wanandroid.BaseActivity
|
||||
import com.lowe.common.compat.IntentCompat
|
||||
import com.lowe.common.utils.unsafeLazy
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
/**
|
||||
* 体系tag页面
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class SeriesDetailListActivity :
|
||||
BaseActivity<SeriesDetailListViewModel, ActivitySeriesDetailListLayoutBinding>() {
|
||||
|
||||
companion object {
|
||||
|
||||
const val KEY_BUNDLE_CLASSIFY_LIST_TAB = "key_bundle_classify_list_Tab"
|
||||
const val KEY_BUNDLE_INIT_TAB_INDEX = "key_bundle_init_tab_index"
|
||||
|
||||
}
|
||||
|
||||
private lateinit var detailFragmentAdapter: SeriesDetailFragmentStateAdapter
|
||||
private lateinit var tabLayoutMediator: TabLayoutMediator
|
||||
|
||||
private val classifyList: List<Classify> by unsafeLazy {
|
||||
IntentCompat.getParcelableArrayListExtra(intent, KEY_BUNDLE_CLASSIFY_LIST_TAB)
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
private val initIndex: Int by unsafeLazy {
|
||||
intent.getIntExtra(KEY_BUNDLE_INIT_TAB_INDEX, -1)
|
||||
}
|
||||
|
||||
override val viewDataBinding: ActivitySeriesDetailListLayoutBinding by ActivityDataBindingDelegate(
|
||||
R.layout.activity_series_detail_list_layout
|
||||
)
|
||||
|
||||
override val viewModel: SeriesDetailListViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
detailFragmentAdapter =
|
||||
SeriesDetailFragmentStateAdapter(classifyList, supportFragmentManager, lifecycle)
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.apply {
|
||||
with(seriesDetailPager2) {
|
||||
adapter = detailFragmentAdapter
|
||||
setCurrentItem(initIndex.takeIf { it >= 0 } ?: 0, false)
|
||||
}
|
||||
tabLayoutMediator = TabLayoutMediator(
|
||||
seriesDetailTabLayout,
|
||||
seriesDetailPager2
|
||||
) { tab: TabLayout.Tab, position: Int ->
|
||||
tab.text = detailFragmentAdapter.items[position].name
|
||||
}.apply(TabLayoutMediator::attach)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
viewDataBinding.swipeRefreshLayout.setOnRefreshListener {
|
||||
viewModel.onRefreshEvent(detailFragmentAdapter.items[viewDataBinding.seriesDetailPager2.currentItem])
|
||||
viewDataBinding.swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.series.detail
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.services.model.Classify
|
||||
|
||||
class SeriesDetailListViewModel : BaseViewModel() {
|
||||
|
||||
private val _onRefreshLiveData = MutableLiveData<Classify>()
|
||||
val onRefreshLiveData: LiveData<Classify> = _onRefreshLiveData
|
||||
|
||||
fun onRefreshEvent(classify: Classify) {
|
||||
_onRefreshLiveData.value = classify
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.series.detail.child
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.paging.CombinedLoadStates
|
||||
import androidx.paging.LoadState
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.lowe.wanandroid.BaseFragment
|
||||
import com.lowe.common.base.app.AppViewModel
|
||||
import com.lowe.common.compat.BundleCompat
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.common.services.model.Classify
|
||||
import com.lowe.common.services.model.CollectEvent
|
||||
import com.lowe.common.utils.*
|
||||
import com.lowe.multitype.PagingLoadStateAdapter
|
||||
import com.lowe.multitype.PagingMultiTypeAdapter
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.FragmentSeriesDetailChildBinding
|
||||
import com.lowe.wanandroid.ui.ArticleDiffCalculator
|
||||
import com.lowe.wanandroid.ui.SimpleFooterItemBinder
|
||||
import com.lowe.wanandroid.ui.home.item.ArticleAction
|
||||
import com.lowe.wanandroid.ui.home.item.HomeArticleItemBinderV2
|
||||
import com.lowe.wanandroid.ui.navigator.child.series.detail.SeriesDetailListViewModel
|
||||
import com.lowe.wanandroid.ui.web.WebActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 体系tag下的子Fragment
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class SeriesDetailChildFragment :
|
||||
BaseFragment<SeriesDetailChildViewModel, FragmentSeriesDetailChildBinding>(R.layout.fragment_series_detail_child) {
|
||||
|
||||
companion object {
|
||||
|
||||
const val KEY_SERIES_DETAIL_CHILD_TAB = "key_series_detail_child_tab"
|
||||
|
||||
fun newInstance(classify: Classify) =
|
||||
with(SeriesDetailChildFragment()) {
|
||||
arguments = bundleOf(KEY_SERIES_DETAIL_CHILD_TAB to classify)
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var appViewModel: AppViewModel
|
||||
|
||||
private val classify by unsafeLazy {
|
||||
BundleCompat.getParcelable(arguments, KEY_SERIES_DETAIL_CHILD_TAB) ?: Classify()
|
||||
}
|
||||
private val detailsAdapter =
|
||||
PagingMultiTypeAdapter(ArticleDiffCalculator.getCommonDiffItemCallback()).apply {
|
||||
register(HomeArticleItemBinderV2(this@SeriesDetailChildFragment::onItemClick))
|
||||
}
|
||||
private val seriesDetailViewModel by activityViewModels<SeriesDetailListViewModel>()
|
||||
|
||||
override val viewModel: SeriesDetailChildViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initEvents()
|
||||
viewModel.fetch(classify.id)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.apply {
|
||||
with(seriesDetailList) {
|
||||
setHasFixedSize(true)
|
||||
adapter = detailsAdapter.withLoadStateFooter(
|
||||
PagingLoadStateAdapter(
|
||||
SimpleFooterItemBinder(), detailsAdapter.types
|
||||
)
|
||||
)
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
launchRepeatOnStarted {
|
||||
launch {
|
||||
viewModel.seriesDetailListFlow.collectLatest(detailsAdapter::submitData)
|
||||
}
|
||||
|
||||
launch {
|
||||
detailsAdapter.loadStateFlow.collectLatest(this@SeriesDetailChildFragment::updateLoadStates)
|
||||
}
|
||||
}
|
||||
seriesDetailViewModel.onRefreshLiveData.observe(viewLifecycleOwner) {
|
||||
if (it.id == classify.id) detailsAdapter.refresh()
|
||||
}
|
||||
appViewModel.collectArticleEvent.observe(viewLifecycleOwner) { event ->
|
||||
detailsAdapter.snapshot().run {
|
||||
val index = indexOfFirst { it is Article && it.id == event.id }
|
||||
if (index >= 0) {
|
||||
(this[index] as? Article)?.collect = event.isCollected
|
||||
index
|
||||
} else null
|
||||
}?.apply(detailsAdapter::notifyItemChanged)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onItemClick(articleAction: ArticleAction) {
|
||||
when (articleAction) {
|
||||
is ArticleAction.ItemClick -> WebActivity.loadUrl(
|
||||
requireContext(),
|
||||
Activities.Web.WebIntent(
|
||||
articleAction.article.link,
|
||||
articleAction.article.id,
|
||||
articleAction.article.collect,
|
||||
)
|
||||
)
|
||||
is ArticleAction.CollectClick -> {
|
||||
appViewModel.articleCollectAction(
|
||||
CollectEvent(
|
||||
articleAction.article.id,
|
||||
articleAction.article.link,
|
||||
articleAction.article.collect.not()
|
||||
)
|
||||
)
|
||||
}
|
||||
is ArticleAction.AuthorClick -> {
|
||||
startActivity(
|
||||
intentTo(
|
||||
Activities.ShareList(bundle = bundleOf(Activities.ShareList.KEY_SHARE_LIST_USER_ID to articleAction.article.userId.toString()))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLoadStates(loadStates: CombinedLoadStates) {
|
||||
viewDataBinding.loadingContainer.apply {
|
||||
emptyLayout.isVisible =
|
||||
loadStates.refresh is LoadState.NotLoading && detailsAdapter.isEmpty()
|
||||
loadingProgress.isVisible = detailsAdapter.isEmpty() && loadStates.isRefreshing
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.series.detail.child
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.cachedIn
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.utils.tryOffer
|
||||
import com.lowe.wanandroid.ui.navigator.NavigatorRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SeriesDetailChildViewModel @Inject constructor(private val repository: NavigatorRepository) :
|
||||
BaseViewModel() {
|
||||
|
||||
private val _fetchSeriesDetails = Channel<Int>(Channel.CONFLATED)
|
||||
|
||||
/**
|
||||
* 获取对应Tag的体系文章列表
|
||||
*/
|
||||
val seriesDetailListFlow = _fetchSeriesDetails.receiveAsFlow().flatMapLatest {
|
||||
repository.getSeriesDetailList(it, DEFAULT_PAGE_SIZE)
|
||||
}.cachedIn(viewModelScope)
|
||||
|
||||
fun fetch(id: Int) {
|
||||
_fetchSeriesDetails.tryOffer(id)
|
||||
}
|
||||
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.series.item
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import com.lowe.common.services.model.Classify
|
||||
import com.lowe.common.services.model.Series
|
||||
import com.lowe.common.utils.fromHtml
|
||||
import com.lowe.multitype.ItemViewBinder
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ItemNavigatorChildTagChildrenLayoutBinding
|
||||
import com.lowe.wanandroid.ui.ViewBindingHolder
|
||||
import com.lowe.wanandroid.widgets.CommonTagChipWidget
|
||||
|
||||
class SeriesChildTagChildrenItemBinder(private val onTagChildrenClick: (Classify) -> Unit) :
|
||||
ItemViewBinder<Series, ViewBindingHolder<ItemNavigatorChildTagChildrenLayoutBinding>>() {
|
||||
override fun onCreateViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup
|
||||
): ViewBindingHolder<ItemNavigatorChildTagChildrenLayoutBinding> {
|
||||
return ViewBindingHolder(
|
||||
DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.item_navigator_child_tag_children_layout,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewBindingHolder<ItemNavigatorChildTagChildrenLayoutBinding>,
|
||||
item: Series
|
||||
) {
|
||||
holder.binding.apply {
|
||||
name = item.name
|
||||
executePendingBindings()
|
||||
tagChildrenLayout.removeAllViews()
|
||||
item.children.forEach { classify ->
|
||||
val tv = CommonTagChipWidget.generateTextViewChip(
|
||||
this.root.context,
|
||||
ViewGroup.MarginLayoutParams(
|
||||
ViewGroup.MarginLayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.MarginLayoutParams.WRAP_CONTENT
|
||||
)
|
||||
)
|
||||
tv.text = classify.name.fromHtml()
|
||||
tv.setOnClickListener { onTagChildrenClick(classify) }
|
||||
tagChildrenLayout.addView(tv)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.tutorial
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.lowe.common.compat.BundleCompat
|
||||
import com.lowe.common.services.model.Classify
|
||||
import com.lowe.common.utils.unsafeLazy
|
||||
import com.lowe.multitype.MultiTypeAdapter
|
||||
import com.lowe.wanandroid.BaseFragment
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.FragmentNavigatorChildTutorialBinding
|
||||
import com.lowe.wanandroid.ui.navigator.NavigatorChildFragmentAdapter
|
||||
import com.lowe.wanandroid.ui.navigator.NavigatorFragment
|
||||
import com.lowe.wanandroid.ui.navigator.NavigatorTabBean
|
||||
import com.lowe.wanandroid.ui.navigator.child.tutorial.item.TutorialChildItemBinder
|
||||
import com.lowe.wanandroid.ui.navigator.child.tutorial.list.TutorialChapterListActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
/**
|
||||
* 教程子Fragment页面
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class TutorialChildFragment :
|
||||
BaseFragment<TutorialChildViewModel, FragmentNavigatorChildTutorialBinding>(R.layout.fragment_navigator_child_tutorial) {
|
||||
|
||||
companion object {
|
||||
fun newInstance(navigatorTabBean: NavigatorTabBean): TutorialChildFragment = with(
|
||||
TutorialChildFragment()
|
||||
) {
|
||||
arguments =
|
||||
bundleOf(NavigatorFragment.KEY_NAVIGATOR_CHILD_HOME_TAB_PARCELABLE to navigatorTabBean)
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
private val navigatorTabBean by unsafeLazy {
|
||||
BundleCompat.getParcelable(
|
||||
arguments,
|
||||
NavigatorFragment.KEY_NAVIGATOR_CHILD_HOME_TAB_PARCELABLE
|
||||
) ?: NavigatorTabBean(NavigatorChildFragmentAdapter.NAVIGATOR_TAB_TUTORIAL)
|
||||
}
|
||||
private val tutorialAdapter = MultiTypeAdapter()
|
||||
override val viewModel: TutorialChildViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.apply {
|
||||
with(tutorialList) {
|
||||
tutorialAdapter.register(TutorialChildItemBinder(this@TutorialChildFragment::onTutorialItemClick))
|
||||
adapter = tutorialAdapter
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
viewModel.apply {
|
||||
tutorialListLiveData.observe(viewLifecycleOwner) {
|
||||
dispatchToAdapter(it)
|
||||
viewDataBinding.loadingContainer.loadingProgress.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatchToAdapter(result: Pair<List<Any>, DiffUtil.DiffResult>) {
|
||||
tutorialAdapter.items = result.first
|
||||
result.second.dispatchUpdatesTo(tutorialAdapter)
|
||||
}
|
||||
|
||||
private fun onTutorialItemClick(position: Int, classify: Classify) {
|
||||
startActivity(Intent(this.context, TutorialChapterListActivity::class.java).apply {
|
||||
putExtra(TutorialChapterListActivity.KEY_INTENT_TUTORIAL_TITLE, classify.name)
|
||||
putExtra(TutorialChapterListActivity.KEY_INTENT_TUTORIAL_ID, classify.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.tutorial
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.lowe.common.base.http.adapter.getOrElse
|
||||
import com.lowe.wanandroid.ui.ArticleDiffCalculator
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.wanandroid.ui.navigator.NavigatorRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class TutorialChildViewModel @Inject constructor(private val repository: NavigatorRepository) :
|
||||
BaseViewModel() {
|
||||
|
||||
val tutorialListLiveData: LiveData<Pair<List<Any>, DiffUtil.DiffResult>> = liveData {
|
||||
emit(
|
||||
getDiffResultPair(
|
||||
this.latestValue?.first ?: emptyList(),
|
||||
repository.getTutorialList().getOrElse { emptyList() }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDiffResultPair(oldList: List<Any>, newList: List<Any>) =
|
||||
newList to DiffUtil.calculateDiff(
|
||||
ArticleDiffCalculator.getCommonDiffCallback(
|
||||
oldList,
|
||||
newList
|
||||
)
|
||||
)
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.tutorial.item
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import com.lowe.common.services.model.Classify
|
||||
import com.lowe.multitype.ItemViewBinder
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ItemNavigatorChildTutorialLayoutBinding
|
||||
import com.lowe.wanandroid.ui.ViewBindingHolder
|
||||
|
||||
class TutorialChildItemBinder(private val onClick: (Int, Classify) -> Unit) :
|
||||
ItemViewBinder<Classify, ViewBindingHolder<ItemNavigatorChildTutorialLayoutBinding>>() {
|
||||
override fun onCreateViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup
|
||||
): ViewBindingHolder<ItemNavigatorChildTutorialLayoutBinding> {
|
||||
return ViewBindingHolder(
|
||||
DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.item_navigator_child_tutorial_layout,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewBindingHolder<ItemNavigatorChildTutorialLayoutBinding>,
|
||||
item: Classify
|
||||
) {
|
||||
holder.binding.apply {
|
||||
classify = item
|
||||
executePendingBindings()
|
||||
root.setOnClickListener {
|
||||
onClick(holder.bindingAdapterPosition, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.tutorial.list
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import com.lowe.multitype.ItemViewBinder
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.ui.ViewBindingHolder
|
||||
import com.lowe.wanandroid.databinding.ItemHomeArticleLayoutV2Binding
|
||||
import com.lowe.common.services.model.Article
|
||||
|
||||
class TutorialChapterItemBinder(private val onClick: (Int, Article) -> Unit) :
|
||||
ItemViewBinder<Article, ViewBindingHolder<ItemHomeArticleLayoutV2Binding>>() {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup
|
||||
): ViewBindingHolder<ItemHomeArticleLayoutV2Binding> {
|
||||
return ViewBindingHolder(
|
||||
DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.item_home_article_layout_v2,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewBindingHolder<ItemHomeArticleLayoutV2Binding>,
|
||||
item: Article
|
||||
) {
|
||||
holder.binding.apply {
|
||||
ivCollect.isVisible = false
|
||||
root.setOnClickListener { onClick(holder.bindingAdapterPosition, item) }
|
||||
article = item
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.tutorial.list
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.lowe.multitype.MultiTypeAdapter
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ActivityTutorialChapterListLayoutBinding
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.wanandroid.ui.ActivityDataBindingDelegate
|
||||
import com.lowe.wanandroid.BaseActivity
|
||||
import com.lowe.wanandroid.ui.web.WebActivity
|
||||
import com.lowe.common.utils.Activities
|
||||
import com.lowe.common.utils.launchRepeatOnStarted
|
||||
import com.lowe.common.utils.unsafeLazy
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TutorialChapterListActivity :
|
||||
BaseActivity<TutorialChapterListViewModel, ActivityTutorialChapterListLayoutBinding>() {
|
||||
|
||||
companion object {
|
||||
const val KEY_INTENT_TUTORIAL_ID = "key_intent_tutorial_id"
|
||||
const val KEY_INTENT_TUTORIAL_TITLE = "key_intent_tutorial_title"
|
||||
}
|
||||
|
||||
private val chapterAdapter = MultiTypeAdapter()
|
||||
private val tutorialId by unsafeLazy {
|
||||
intent.getIntExtra(KEY_INTENT_TUTORIAL_ID, -1)
|
||||
}
|
||||
private val tutorialTitle by unsafeLazy {
|
||||
intent.getStringExtra(KEY_INTENT_TUTORIAL_TITLE).orEmpty()
|
||||
}
|
||||
|
||||
|
||||
override val viewDataBinding: ActivityTutorialChapterListLayoutBinding by ActivityDataBindingDelegate(
|
||||
R.layout.activity_tutorial_chapter_list_layout
|
||||
)
|
||||
|
||||
override val viewModel: TutorialChapterListViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
initView()
|
||||
initEvents()
|
||||
if (savedInstanceState == null) {
|
||||
viewModel.fetchChapterList(tutorialId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
chapterAdapter.register(TutorialChapterItemBinder(this::onItemClick))
|
||||
viewDataBinding.apply {
|
||||
with(chapterList) {
|
||||
adapter = chapterAdapter
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
with(toolbar) {
|
||||
title = tutorialTitle
|
||||
setNavigationOnClickListener { this@TutorialChapterListActivity.finish() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
launchRepeatOnStarted {
|
||||
viewModel.chaptersFlow.collect {
|
||||
chapterAdapter.items = it
|
||||
chapterAdapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onItemClick(position: Int, article: Article) {
|
||||
WebActivity.loadUrl(
|
||||
this, Activities.Web.WebIntent(article.link)
|
||||
)
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.lowe.wanandroid.ui.navigator.child.tutorial.list
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.lowe.common.base.http.adapter.getOrNull
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.wanandroid.ui.navigator.NavigatorRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class TutorialChapterListViewModel @Inject constructor(private val repository: NavigatorRepository) :
|
||||
BaseViewModel() {
|
||||
|
||||
private val _chaptersSharedFlow = MutableSharedFlow<List<Article>>(replay = 1)
|
||||
val chaptersFlow: Flow<List<Article>> = _chaptersSharedFlow
|
||||
|
||||
fun fetchChapterList(tutorialId: Int) {
|
||||
viewModelScope.launch {
|
||||
_chaptersSharedFlow.emit(
|
||||
repository.getTutorialChapterList(tutorialId).getOrNull()?.datas ?: emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.lowe.wanandroid.ui.navigator.widgets
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* 滚动联动效果实现
|
||||
*/
|
||||
class NavigatorTagOnScrollListener : RecyclerView.OnScrollListener() {
|
||||
|
||||
private val _firstCompletelyVisiblePosChange = MutableStateFlow(0)
|
||||
val firstCompletelyVisiblePosChange: StateFlow<Int> = _firstCompletelyVisiblePosChange
|
||||
|
||||
/**
|
||||
* 滚动起始原因
|
||||
* startState == [RecyclerView.SCROLL_STATE_DRAGGING] 说明是用户操作
|
||||
* startState == [RecyclerView.SCROLL_STATE_SETTLING] 说明是smoothScroll等外部调用滚动,不再回调[firstCompletelyVisiblePosChange]
|
||||
*/
|
||||
private var startState = RecyclerView.SCROLL_STATE_IDLE
|
||||
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
if (startState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
startState = newState
|
||||
}
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
startState = newState
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
/**
|
||||
* 意味着非用户操作导致的滚动,忽略
|
||||
*/
|
||||
if (startState == RecyclerView.SCROLL_STATE_SETTLING) return
|
||||
(recyclerView.layoutManager as? LinearLayoutManager)?.findFirstCompletelyVisibleItemPosition()
|
||||
?.takeIf { it >= 0 }
|
||||
?.also { pos ->
|
||||
_firstCompletelyVisiblePosChange.value = pos
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.lowe.wanandroid.ui.profile
|
||||
|
||||
/**
|
||||
* CollapsingToolBar状态枚举
|
||||
*/
|
||||
enum class CollapsingToolBarState{
|
||||
|
||||
/**
|
||||
* 展开
|
||||
*/
|
||||
EXPANDED,
|
||||
|
||||
/**
|
||||
* 折叠
|
||||
*/
|
||||
COLLAPSED,
|
||||
|
||||
/**
|
||||
* 过渡态
|
||||
*/
|
||||
INTERMEDIATE
|
||||
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
package com.lowe.wanandroid.ui.profile
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.lowe.common.account.AccountState
|
||||
import com.lowe.common.account.checkLogin
|
||||
import com.lowe.common.services.model.UserBaseInfo
|
||||
import com.lowe.common.utils.Activities
|
||||
import com.lowe.common.utils.intentTo
|
||||
import com.lowe.common.utils.launchRepeatOnCreated
|
||||
import com.lowe.common.utils.launchRepeatOnStarted
|
||||
import com.lowe.multitype.MultiTypeAdapter
|
||||
import com.lowe.wanandroid.BR
|
||||
import com.lowe.wanandroid.BaseFragment
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.FragmentProfileBinding
|
||||
import com.lowe.wanandroid.ui.coin.MyCoinInfoActivity
|
||||
import com.lowe.wanandroid.ui.collect.CollectActivity
|
||||
import com.lowe.wanandroid.ui.message.MessageActivity
|
||||
import com.lowe.wanandroid.ui.message.UnreadMessage
|
||||
import com.lowe.wanandroid.ui.profile.item.ProfileItemBinder
|
||||
import com.lowe.wanandroid.ui.tools.ToolListActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* 个人页面
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class ProfileFragment :
|
||||
BaseFragment<ProfileViewModel, FragmentProfileBinding>(R.layout.fragment_profile) {
|
||||
|
||||
private val profileItemAdapter = MultiTypeAdapter()
|
||||
private var collapsingToolBarStateFlow =
|
||||
MutableStateFlow(CollapsingToolBarState.EXPANDED)
|
||||
|
||||
override val viewModel: ProfileViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initItems()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
profileItemAdapter.register(ProfileItemBinder(this::onOptionClick))
|
||||
viewDataBinding.apply {
|
||||
with(this.itemContainer.profileItemList) {
|
||||
adapter = profileItemAdapter
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
with(appBarLayout) {
|
||||
addOnOffsetChangedListener { appBarLayout, verticalOffset ->
|
||||
when {
|
||||
verticalOffset == 0 -> collapsingToolBarStateFlow.value =
|
||||
CollapsingToolBarState.EXPANDED
|
||||
abs(verticalOffset) >= appBarLayout.totalScrollRange -> collapsingToolBarStateFlow.value =
|
||||
CollapsingToolBarState.COLLAPSED
|
||||
else -> collapsingToolBarStateFlow.value =
|
||||
CollapsingToolBarState.INTERMEDIATE
|
||||
}
|
||||
}
|
||||
}
|
||||
settingFabIcon.setOnClickListener {
|
||||
startActivity(intentTo(Activities.Setting))
|
||||
}
|
||||
arrayOf(userAvatar, userName, userId, userCoinCount).forEach {
|
||||
it.setOnClickListener {
|
||||
viewModel.accountState.value.checkLogin(requireContext()) {}
|
||||
}
|
||||
}
|
||||
userCoinCount.setOnClickListener {
|
||||
viewModel.accountState.value.checkLogin(requireContext()) {
|
||||
startActivity(Intent(requireContext(), MyCoinInfoActivity::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initItems() {
|
||||
profileItemAdapter.items = listOf(
|
||||
ProfileItemBean(
|
||||
R.drawable.ic_notification_48dp,
|
||||
getString(R.string.profile_item_title_message)
|
||||
),
|
||||
ProfileItemBean(R.drawable.ic_share_48dp, getString(R.string.profile_item_title_share)),
|
||||
ProfileItemBean(R.drawable.ic_collect, getString(R.string.profile_item_title_favorite)),
|
||||
ProfileItemBean(R.drawable.ic_tool_48dp, getString(R.string.profile_item_title_tools))
|
||||
)
|
||||
profileItemAdapter.notifyItemRangeChanged(0, profileItemAdapter.itemCount)
|
||||
}
|
||||
|
||||
|
||||
private fun initEvents() {
|
||||
launchRepeatOnCreated {
|
||||
launch {
|
||||
viewModel.accountState.collect {
|
||||
when (it) {
|
||||
is AccountState.LogIn -> {
|
||||
viewModel.fetchUserInfo()
|
||||
}
|
||||
AccountState.LogOut -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
viewModel.accountInfo.collect(this@ProfileFragment::userInfoGot)
|
||||
}
|
||||
}
|
||||
launchRepeatOnStarted {
|
||||
|
||||
viewModel.clearProfileUnread()
|
||||
|
||||
launch {
|
||||
collapsingToolBarStateFlow
|
||||
.distinctUntilChanged { old, new ->
|
||||
old == new
|
||||
}.collectLatest {
|
||||
if (it == CollapsingToolBarState.COLLAPSED) {
|
||||
viewDataBinding.collapsingToolbarLayout.title =
|
||||
viewDataBinding.user?.userInfo?.nickname
|
||||
} else viewDataBinding.collapsingToolbarLayout.title = ""
|
||||
}
|
||||
}
|
||||
launch {
|
||||
viewModel.messageUnreadState.collect(this@ProfileFragment::updateMessageBadge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOptionClick(position: Int, item: ProfileItemBean) {
|
||||
when (item.title) {
|
||||
getString(R.string.profile_item_title_message) -> {
|
||||
viewModel.accountState.value.checkLogin(requireContext()) {
|
||||
startActivity(Intent(requireContext(), MessageActivity::class.java))
|
||||
}
|
||||
}
|
||||
getString(R.string.profile_item_title_share) -> {
|
||||
viewModel.accountState.value.checkLogin(requireContext()) {
|
||||
startActivity(
|
||||
intentTo(
|
||||
Activities.ShareList(
|
||||
bundle = bundleOf(Activities.ShareList.KEY_SHARE_LIST_USER_ID to viewModel.userId)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
getString(R.string.profile_item_title_favorite) -> {
|
||||
viewModel.accountState.value.checkLogin(requireContext()) {
|
||||
startActivity(Intent(requireContext(), CollectActivity::class.java))
|
||||
}
|
||||
}
|
||||
getString(R.string.profile_item_title_tools) -> {
|
||||
startActivity(Intent(requireContext(), ToolListActivity::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun userInfoGot(response: UserBaseInfo) {
|
||||
viewDataBinding.user = response
|
||||
viewDataBinding.notifyPropertyChanged(BR.user)
|
||||
}
|
||||
|
||||
private fun updateMessageBadge(unreadMessage: UnreadMessage) {
|
||||
val index =
|
||||
profileItemAdapter.items.indexOfFirst { it is ProfileItemBean && it.title == getString(R.string.profile_item_title_message) }
|
||||
(profileItemAdapter.items[index] as ProfileItemBean).badge =
|
||||
if (unreadMessage.count > 0) Badge(
|
||||
Badge.BadgeType.NUMBER,
|
||||
unreadMessage.count
|
||||
) else Badge(Badge.BadgeType.NONE)
|
||||
profileItemAdapter.notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.lowe.wanandroid.ui.profile
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
|
||||
class ProfileItemBean(
|
||||
@DrawableRes
|
||||
val iconRes: Int,
|
||||
val title: String,
|
||||
var badge: Badge = Badge()
|
||||
)
|
||||
|
||||
class Badge(
|
||||
val type: BadgeType = BadgeType.NONE,
|
||||
val number: Int = 0
|
||||
) {
|
||||
enum class BadgeType {
|
||||
NONE,
|
||||
DOT,
|
||||
NUMBER
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.lowe.wanandroid.ui.profile
|
||||
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.account.IAccountViewModelDelegate
|
||||
import com.lowe.wanandroid.ui.message.UnreadMessageManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ProfileViewModel @Inject constructor(
|
||||
private val unreadMessageManager: UnreadMessageManager,
|
||||
private val accountViewModelDelegate: IAccountViewModelDelegate
|
||||
) : BaseViewModel(), IAccountViewModelDelegate by accountViewModelDelegate {
|
||||
|
||||
|
||||
val messageUnreadState = unreadMessageManager.unreadMessage
|
||||
|
||||
fun clearProfileUnread() = unreadMessageManager.clearProfileUnread()
|
||||
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package com.lowe.wanandroid.ui.profile.item
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import com.google.android.material.badge.BadgeDrawable
|
||||
import com.google.android.material.badge.BadgeUtils
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.lowe.multitype.ItemViewBinder
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ItemProfileOptionsLayoutBinding
|
||||
import com.lowe.resource.extension.getPrimaryColor
|
||||
import com.lowe.wanandroid.ui.ViewBindingHolder
|
||||
import com.lowe.wanandroid.ui.profile.Badge
|
||||
import com.lowe.wanandroid.ui.profile.ProfileItemBean
|
||||
|
||||
class ProfileItemBinder(private val onClick: (Int, ProfileItemBean) -> Unit) :
|
||||
ItemViewBinder<ProfileItemBean, ProfileItemBinder.ProfileItemViewHolder>() {
|
||||
|
||||
class ProfileItemViewHolder(override val binding: ItemProfileOptionsLayoutBinding) :
|
||||
ViewBindingHolder<ItemProfileOptionsLayoutBinding>(binding) {
|
||||
|
||||
private var badgeDrawable: BadgeDrawable? = null
|
||||
|
||||
fun updateBadge(badge: Badge) {
|
||||
if (badgeDrawable == null) {
|
||||
badgeDrawable = BadgeDrawable.create(binding.root.context).apply {
|
||||
backgroundColor = MaterialColors.getColor(
|
||||
binding.profileItemIcon,
|
||||
com.google.android.material.R.attr.colorSecondary
|
||||
)
|
||||
badgeTextColor = MaterialColors.getColor(
|
||||
binding.profileItemIcon,
|
||||
com.google.android.material.R.attr.colorOnSecondary
|
||||
)
|
||||
number = 0
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
badgeDrawable?.also { drawable ->
|
||||
when (badge.type) {
|
||||
Badge.BadgeType.NONE -> {
|
||||
BadgeUtils.detachBadgeDrawable(drawable, binding.profileItemIcon)
|
||||
badgeDrawable = null
|
||||
}
|
||||
Badge.BadgeType.DOT -> {
|
||||
drawable.number = 0
|
||||
drawable.isVisible = true
|
||||
BadgeUtils.attachBadgeDrawable(drawable, binding.profileItemIcon)
|
||||
}
|
||||
Badge.BadgeType.NUMBER -> {
|
||||
drawable.number = badge.number
|
||||
drawable.isVisible = true
|
||||
BadgeUtils.attachBadgeDrawable(drawable, binding.profileItemIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup
|
||||
): ProfileItemViewHolder = ProfileItemViewHolder(
|
||||
DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.item_profile_options_layout,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ProfileItemViewHolder,
|
||||
item: ProfileItemBean
|
||||
) {
|
||||
holder.binding.apply {
|
||||
this.item = item
|
||||
executePendingBindings()
|
||||
profileItemIcon.setImageDrawable(
|
||||
AppCompatResources.getDrawable(root.context, item.iconRes)?.apply {
|
||||
setTint(root.context.getPrimaryColor())
|
||||
}
|
||||
)
|
||||
profileItemIcon.post {
|
||||
holder.updateBadge(item.badge)
|
||||
}
|
||||
root.setOnClickListener { onClick(holder.bindingAdapterPosition, item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.lowe.wanandroid.ui.project
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.lowe.common.services.model.ProjectTitle
|
||||
import com.lowe.wanandroid.ui.project.child.ProjectChildFragment
|
||||
|
||||
class ProjectChildFragmentAdapter(
|
||||
var items: List<ProjectTitle>,
|
||||
fragmentManager: FragmentManager,
|
||||
lifecycle: Lifecycle
|
||||
) : FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return ProjectChildFragment.newInstance(items[position].id)
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.lowe.wanandroid.ui.project
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.lowe.common.utils.fromHtml
|
||||
import com.lowe.wanandroid.BaseFragment
|
||||
import com.lowe.wanandroid.MainViewModel
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.FragmentProjectBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
/**
|
||||
* 项目Tab
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class ProjectFragment :
|
||||
BaseFragment<ProjectViewModel, FragmentProjectBinding>(R.layout.fragment_project) {
|
||||
|
||||
private lateinit var childAdapter: ProjectChildFragmentAdapter
|
||||
private lateinit var tabLayoutMediator: TabLayoutMediator
|
||||
private val mainViewModel by activityViewModels<MainViewModel>()
|
||||
|
||||
override val viewModel: ProjectViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(savedInstanceState: Bundle?) {
|
||||
childAdapter =
|
||||
ProjectChildFragmentAdapter(emptyList(), this.childFragmentManager, lifecycle)
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
viewModel.projectTitleListLiveData.observe(viewLifecycleOwner) {
|
||||
childAdapter.items = it
|
||||
childAdapter.notifyDataSetChanged()
|
||||
}
|
||||
mainViewModel.mainTabDoubleClickLiveData.observe(viewLifecycleOwner) {
|
||||
if (childAdapter.items.isEmpty() || it != this.tag) return@observe
|
||||
viewModel.scrollToTopLiveData.value =
|
||||
childAdapter.items[viewDataBinding.projectViewPager2.currentItem].id
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.apply {
|
||||
with(projectViewPager2) {
|
||||
adapter = childAdapter
|
||||
}
|
||||
with(projectSwipeRefresh) {
|
||||
setOnRefreshListener {
|
||||
this@ProjectFragment.viewModel.parentRefreshLiveData.value =
|
||||
childAdapter.items[projectViewPager2.currentItem].id
|
||||
this.isRefreshing = false
|
||||
}
|
||||
}
|
||||
tabLayoutMediator = TabLayoutMediator(
|
||||
viewDataBinding.projectTabLayout,
|
||||
viewDataBinding.projectViewPager2
|
||||
) { tab: TabLayout.Tab, position: Int ->
|
||||
tab.text = childAdapter.items[position].name.fromHtml()
|
||||
}.apply(TabLayoutMediator::attach)
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.lowe.wanandroid.ui.project
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import com.lowe.common.base.IntKeyPagingSource
|
||||
import com.lowe.common.base.http.adapter.getOrNull
|
||||
import com.lowe.common.services.BaseService
|
||||
import com.lowe.common.services.ProjectService
|
||||
import javax.inject.Inject
|
||||
|
||||
class ProjectRepository @Inject constructor(private val service: ProjectService) {
|
||||
|
||||
suspend fun getProjectTitleList() = service.getProjectTitleList()
|
||||
|
||||
/**
|
||||
* 项目列表Flow
|
||||
*/
|
||||
fun getProjectListFlow(pageSize: Int, categoryId: Int) =
|
||||
Pager(
|
||||
PagingConfig(
|
||||
pageSize = pageSize,
|
||||
initialLoadSize = pageSize,
|
||||
enablePlaceholders = false
|
||||
)
|
||||
) {
|
||||
IntKeyPagingSource(
|
||||
service = service
|
||||
) { service, page, size ->
|
||||
service.getProjectPageList(page, size, categoryId).getOrNull()?.datas ?: emptyList()
|
||||
}
|
||||
}.flow
|
||||
|
||||
/**
|
||||
* 最新项目列表Flow
|
||||
*/
|
||||
fun getNewProjectListFlow(pageSize: Int) =
|
||||
Pager(
|
||||
PagingConfig(
|
||||
pageSize = pageSize,
|
||||
initialLoadSize = pageSize,
|
||||
enablePlaceholders = false
|
||||
)
|
||||
) {
|
||||
IntKeyPagingSource(
|
||||
BaseService.DEFAULT_PAGE_START_NO,
|
||||
service
|
||||
) { service, page, size ->
|
||||
service.getNewProjectPageList(page, size).getOrNull()?.datas ?: emptyList()
|
||||
}
|
||||
}.flow
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.lowe.wanandroid.ui.project
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import com.lowe.common.base.http.adapter.getOrElse
|
||||
import com.lowe.common.services.model.ProjectTitle
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.wanandroid.ui.project.child.ProjectChildFragment
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ProjectViewModel @Inject constructor(private val repository: ProjectRepository) :
|
||||
BaseViewModel() {
|
||||
|
||||
val projectTitleListLiveData = liveData<List<ProjectTitle>> {
|
||||
emit(
|
||||
mutableListOf<ProjectTitle>().apply {
|
||||
add(generateNewestProjectBean())
|
||||
addAll(repository.getProjectTitleList().getOrElse { emptyList() })
|
||||
}
|
||||
)
|
||||
}
|
||||
val parentRefreshLiveData = MutableLiveData<Int>()
|
||||
val scrollToTopLiveData = MutableLiveData<Int>()
|
||||
|
||||
private fun generateNewestProjectBean() = ProjectTitle(
|
||||
id = ProjectChildFragment.CATEGORY_ID_NEWEST_PROJECT, name = "最新项目"
|
||||
)
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
package com.lowe.wanandroid.ui.project.child
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.paging.CombinedLoadStates
|
||||
import androidx.paging.LoadState
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.lowe.common.base.app.AppViewModel
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.common.services.model.CollectEvent
|
||||
import com.lowe.common.utils.Activities
|
||||
import com.lowe.common.utils.isEmpty
|
||||
import com.lowe.common.utils.isRefreshing
|
||||
import com.lowe.common.utils.launchRepeatOnStarted
|
||||
import com.lowe.multitype.PagingLoadStateAdapter
|
||||
import com.lowe.multitype.PagingMultiTypeAdapter
|
||||
import com.lowe.wanandroid.BaseFragment
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.FragmentChildProjectBinding
|
||||
import com.lowe.wanandroid.ui.ArticleDiffCalculator
|
||||
import com.lowe.wanandroid.ui.SimpleFooterItemBinder
|
||||
import com.lowe.wanandroid.ui.home.item.ArticleAction
|
||||
import com.lowe.wanandroid.ui.project.ProjectViewModel
|
||||
import com.lowe.wanandroid.ui.project.child.item.ProjectChildItemBinder
|
||||
import com.lowe.wanandroid.ui.web.WebActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 项目子Fragment
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class ProjectChildFragment :
|
||||
BaseFragment<ProjectChildViewModel, FragmentChildProjectBinding>(R.layout.fragment_child_project) {
|
||||
|
||||
companion object {
|
||||
const val CATEGORY_ID_NEWEST_PROJECT = 0
|
||||
const val KEY_PROJECT_CHILD_CATEGORY_ID = "key_project_child_category_id"
|
||||
|
||||
fun newInstance(categoryId: Int) = ProjectChildFragment().apply {
|
||||
arguments = bundleOf(KEY_PROJECT_CHILD_CATEGORY_ID to categoryId)
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var appViewModel: AppViewModel
|
||||
|
||||
private val projectAdapter =
|
||||
PagingMultiTypeAdapter(ArticleDiffCalculator.getCommonDiffItemCallback()).apply {
|
||||
register(ProjectChildItemBinder(this@ProjectChildFragment::onItemClick))
|
||||
}
|
||||
private val projectViewModel by viewModels<ProjectViewModel>(this::requireParentFragment)
|
||||
private val categoryId by lazy { arguments?.getInt(KEY_PROJECT_CHILD_CATEGORY_ID, -1) ?: -1 }
|
||||
|
||||
override val viewModel: ProjectChildViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initEvents()
|
||||
if (savedInstanceState == null) {
|
||||
viewModel.fetch(categoryId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.apply {
|
||||
with(childList) {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = projectAdapter.withLoadStateFooter(
|
||||
PagingLoadStateAdapter(
|
||||
SimpleFooterItemBinder(),
|
||||
projectAdapter.types
|
||||
)
|
||||
)
|
||||
setHasFixedSize(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
launchRepeatOnStarted {
|
||||
launch {
|
||||
viewModel.getProjectListFlow.collectLatest(projectAdapter::submitData)
|
||||
}
|
||||
launch {
|
||||
projectAdapter.loadStateFlow.collectLatest(this@ProjectChildFragment::updateLoadStates)
|
||||
}
|
||||
}
|
||||
projectViewModel.parentRefreshLiveData.observe(viewLifecycleOwner, this::onParentRefresh)
|
||||
projectViewModel.scrollToTopLiveData.observe(viewLifecycleOwner, this::scrollToTop)
|
||||
appViewModel.collectArticleEvent.observe(viewLifecycleOwner, this::refreshCollectStatus)
|
||||
}
|
||||
|
||||
private fun onParentRefresh(categoryId: Int) {
|
||||
if (categoryId != this.categoryId) return
|
||||
projectAdapter.refresh()
|
||||
}
|
||||
|
||||
private fun scrollToTop(categoryId: Int) {
|
||||
if (categoryId != this.categoryId) return
|
||||
viewDataBinding.childList.scrollToPosition(0)
|
||||
}
|
||||
|
||||
private fun refreshCollectStatus(event: CollectEvent) {
|
||||
projectAdapter.snapshot().run {
|
||||
val index = indexOfFirst { it is Article && it.id == event.id }
|
||||
if (index >= 0) {
|
||||
(this[index] as? Article)?.collect = event.isCollected
|
||||
index
|
||||
} else null
|
||||
}?.apply(projectAdapter::notifyItemChanged)
|
||||
}
|
||||
|
||||
private fun onItemClick(articleAction: ArticleAction) {
|
||||
when (articleAction) {
|
||||
is ArticleAction.ItemClick -> WebActivity.loadUrl(
|
||||
requireContext(),
|
||||
Activities.Web.WebIntent(
|
||||
articleAction.article.link,
|
||||
articleAction.article.id,
|
||||
articleAction.article.collect,
|
||||
)
|
||||
)
|
||||
is ArticleAction.CollectClick -> {
|
||||
appViewModel.articleCollectAction(
|
||||
CollectEvent(
|
||||
articleAction.article.id,
|
||||
articleAction.article.link,
|
||||
articleAction.article.collect.not()
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLoadStates(loadStates: CombinedLoadStates) {
|
||||
viewDataBinding.loadingContainer.apply {
|
||||
emptyLayout.isVisible =
|
||||
loadStates.refresh is LoadState.NotLoading && projectAdapter.isEmpty()
|
||||
loadingProgress.isVisible = projectAdapter.isEmpty() && loadStates.isRefreshing
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.lowe.wanandroid.ui.project.child
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.cachedIn
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.utils.tryOffer
|
||||
import com.lowe.wanandroid.ui.project.ProjectRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ProjectChildViewModel @Inject constructor(private val repository: ProjectRepository) :
|
||||
BaseViewModel() {
|
||||
|
||||
private val _fetchProjects = Channel<Int>(Channel.CONFLATED)
|
||||
|
||||
val getProjectListFlow = _fetchProjects.receiveAsFlow().flatMapLatest {
|
||||
if (it == ProjectChildFragment.CATEGORY_ID_NEWEST_PROJECT) repository.getNewProjectListFlow(
|
||||
DEFAULT_PAGE_SIZE
|
||||
) else repository.getProjectListFlow(DEFAULT_PAGE_SIZE, it)
|
||||
}.cachedIn(viewModelScope)
|
||||
|
||||
fun fetch(categoryId: Int) {
|
||||
_fetchProjects.tryOffer(categoryId)
|
||||
}
|
||||
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.lowe.wanandroid.ui.project.child.item
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import com.lowe.common.services.model.Article
|
||||
import com.lowe.multitype.PagingItemViewBinder
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ItemProjectArticleLayoutBinding
|
||||
import com.lowe.wanandroid.ui.ViewBindingHolder
|
||||
import com.lowe.wanandroid.ui.home.item.ArticleAction
|
||||
|
||||
class ProjectChildItemBinder(private val onClick: (ArticleAction) -> Unit) :
|
||||
PagingItemViewBinder<Article, ViewBindingHolder<ItemProjectArticleLayoutBinding>>() {
|
||||
override fun onCreateViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup
|
||||
): ViewBindingHolder<ItemProjectArticleLayoutBinding> {
|
||||
return ViewBindingHolder(
|
||||
DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.item_project_article_layout,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewBindingHolder<ItemProjectArticleLayoutBinding>,
|
||||
item: Article
|
||||
) {
|
||||
holder.binding.apply {
|
||||
article = item
|
||||
root.setOnClickListener {
|
||||
onClick(
|
||||
ArticleAction.ItemClick(
|
||||
holder.bindingAdapterPosition,
|
||||
item
|
||||
)
|
||||
)
|
||||
}
|
||||
projectCollect.setOnClickListener {
|
||||
onClick(
|
||||
ArticleAction.CollectClick(
|
||||
holder.bindingAdapterPosition,
|
||||
item
|
||||
)
|
||||
)
|
||||
}
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package com.lowe.wanandroid.ui.search
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.activity.viewModels
|
||||
import com.lowe.wanandroid.BaseActivity
|
||||
import com.lowe.common.utils.hideSoftKeyboard
|
||||
import com.lowe.common.utils.launchRepeatOnStarted
|
||||
import com.lowe.wanandroid.R
|
||||
import com.lowe.wanandroid.databinding.ActivitySearchBinding
|
||||
import com.lowe.wanandroid.ui.ActivityDataBindingDelegate
|
||||
import com.lowe.wanandroid.ui.search.begin.SearchBeginFragment
|
||||
import com.lowe.wanandroid.ui.search.result.SearchListFragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SearchActivity : BaseActivity<SearchViewModel, ActivitySearchBinding>() {
|
||||
|
||||
private var searchBeginFragment = SearchBeginFragment()
|
||||
private var searchListFragment = SearchListFragment()
|
||||
|
||||
override val viewDataBinding: ActivitySearchBinding by ActivityDataBindingDelegate(R.layout.activity_search)
|
||||
|
||||
override val viewModel: SearchViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(
|
||||
R.id.searchFragmentContainer,
|
||||
searchBeginFragment,
|
||||
searchBeginFragment.javaClass.simpleName
|
||||
)
|
||||
.commit()
|
||||
} else {
|
||||
searchBeginFragment =
|
||||
supportFragmentManager.findFragmentByTag(SearchBeginFragment::class.java.simpleName) as? SearchBeginFragment
|
||||
?: searchBeginFragment
|
||||
|
||||
searchListFragment =
|
||||
supportFragmentManager.findFragmentByTag(SearchListFragment::class.java.simpleName) as? SearchListFragment
|
||||
?: searchListFragment
|
||||
}
|
||||
initView()
|
||||
initEvent()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
viewDataBinding.apply {
|
||||
with(backIcon) {
|
||||
setOnClickListener {
|
||||
if (supportFragmentManager.backStackEntryCount != 0) {
|
||||
supportFragmentManager.popBackStack()
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
with(searchIcon) {
|
||||
setOnClickListener {
|
||||
search(searchEdit.text?.trim().toString())
|
||||
}
|
||||
}
|
||||
with(searchEdit) {
|
||||
setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_GO) {
|
||||
search(searchEdit.text?.trim().toString())
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
setOnKeyListener { _, keyCode, event ->
|
||||
if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||
search(searchEdit.text?.trim().toString())
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvent() {
|
||||
launchRepeatOnStarted {
|
||||
launch {
|
||||
viewModel.shortcutSearch.collect(this@SearchActivity::search)
|
||||
}
|
||||
|
||||
launch {
|
||||
viewModel.searchState
|
||||
.map { it.keywords }
|
||||
.distinctUntilChanged()
|
||||
.collect(this@SearchActivity::setSearchText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun search(keywords: String) {
|
||||
viewDataBinding.searchEdit.hideSoftKeyboard()
|
||||
if (keywords.isBlank()) return
|
||||
if (searchListFragment.isAdded.not()) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.hide(searchBeginFragment)
|
||||
.add(
|
||||
R.id.searchFragmentContainer,
|
||||
searchListFragment,
|
||||
searchListFragment.javaClass.simpleName
|
||||
)
|
||||
.addToBackStack(null).commit()
|
||||
}
|
||||
viewModel.search(keywords)
|
||||
}
|
||||
|
||||
private fun setSearchText(text: String) {
|
||||
viewDataBinding.searchEdit.setText(text)
|
||||
viewDataBinding.searchEdit.setSelection(text.length)
|
||||
}
|
||||
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.lowe.wanandroid.ui.search
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import com.lowe.common.base.BaseViewModel
|
||||
import com.lowe.common.base.IntKeyPagingSource
|
||||
import com.lowe.common.base.http.SearchHistoryPreference
|
||||
import com.lowe.common.base.http.adapter.getOrNull
|
||||
import com.lowe.common.services.SearchService
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
class SearchRepository @Inject constructor(
|
||||
private val searchService: SearchService,
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
) {
|
||||
|
||||
suspend fun getHotKeyList() = searchService.getSearchHotKey()
|
||||
|
||||
fun search(keywords: String) =
|
||||
Pager(
|
||||
PagingConfig(
|
||||
pageSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
initialLoadSize = BaseViewModel.DEFAULT_PAGE_SIZE,
|
||||
enablePlaceholders = false
|
||||
)
|
||||
) {
|
||||
IntKeyPagingSource(service = searchService) { service, page, _ ->
|
||||
service.queryBySearchKey(page, keywords).getOrNull()?.datas ?: emptyList()
|
||||
}
|
||||
}.flow
|
||||
|
||||
fun searchHistoryCache() = dataStore.data.map {
|
||||
(it[SearchHistoryPreference.searchHistoryPreferences]
|
||||
?: emptySet()).map { SearchState(it) }
|
||||
}
|
||||
|
||||
suspend fun updateSearchHistory(set: Set<String>) {
|
||||
dataStore.edit {
|
||||
it[SearchHistoryPreference.searchHistoryPreferences] = set
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user