This commit is contained in:
coco
2026-07-03 16:23:31 +08:00
commit 7a4fb0e6ae
1979 changed files with 101570 additions and 0 deletions
+168
View File
@@ -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
+55
View File
@@ -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>
#### 截图展示
----
| ![](screenshots/light_home.jpg) | ![](screenshots/light_project.jpg) | ![](screenshots/light_navigation.jpg) | ![](screenshots/light_profile.jpg) |
| --- | --- | --- | --- |
| ![](screenshots/dark_home.jpg) | ![](screenshots/dark_project.jpg) | ![](screenshots/dark_navigation.jpg) | ![](screenshots/dark_profile.jpg) |
| ![](screenshots/color1.png) | ![](screenshots/color2.png) | ![](screenshots/color3.png) | ![](screenshots/color4.png) |
----
默认主题色采用[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 WanAndroidWanAndroid的最佳可使用的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
+2
View File
@@ -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)
}
@@ -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
View File
@@ -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>;
}
@@ -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
)
}
@@ -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() }
}
}
@@ -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
}
}
)
}
@@ -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
}
@@ -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)
@@ -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>&nbsp;</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()
}
}
}
@@ -0,0 +1,6 @@
package com.lowe.wanandroid.ui.about
import com.lowe.common.base.BaseViewModel
class AboutViewModel : BaseViewModel() {
}
@@ -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
}
@@ -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
}
}
}
@@ -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)
}
@@ -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
)
)
}
@@ -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
}
}
}
@@ -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
}
@@ -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)
}
@@ -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
)
)
}
@@ -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
}
}
}
@@ -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)
}
@@ -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
)
)
}
@@ -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)
}
@@ -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)
}
}
}
@@ -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
}
@@ -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
}
}
@@ -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
}
}
}
@@ -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)
}
}
@@ -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),
)
}
@@ -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
}
}
@@ -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
}
}
}
@@ -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)
}
@@ -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
}
}
}
@@ -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)
}
@@ -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
}
}
}
@@ -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)
}
@@ -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
}
@@ -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) }
}
}
}
@@ -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)
}
}
@@ -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
}
}
@@ -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)
}
}
}
@@ -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
}
}
@@ -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),
)
}
@@ -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
@@ -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
}
@@ -0,0 +1,5 @@
package com.lowe.wanandroid.ui.message
import com.lowe.common.base.BaseViewModel
class MessageViewModel : BaseViewModel()
@@ -0,0 +1,5 @@
package com.lowe.wanandroid.ui.message
data class UnreadMessage(
val count: Int = 0
)
@@ -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))
}
}
@@ -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
}
}
}
@@ -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()
}
}
}
@@ -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()
}
@@ -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
@@ -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)
)
}
@@ -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
}
@@ -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
}
}
@@ -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)
)
}
}
}
@@ -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
)
)
}
@@ -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)
}
}
}
}
@@ -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)
}
}
@@ -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
)
)
}
@@ -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])
}
@@ -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
}
}
}
@@ -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
}
}
@@ -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
}
}
}
@@ -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)
}
}
@@ -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)
}
}
}
}
@@ -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)
})
}
}
@@ -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
)
)
}
@@ -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)
}
}
}
}
@@ -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()
}
}
}
@@ -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)
)
}
}
@@ -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()
)
}
}
}
@@ -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
}
}
}
@@ -0,0 +1,23 @@
package com.lowe.wanandroid.ui.profile
/**
* CollapsingToolBar状态枚举
*/
enum class CollapsingToolBarState{
/**
* 展开
*/
EXPANDED,
/**
* 折叠
*/
COLLAPSED,
/**
* 过渡态
*/
INTERMEDIATE
}
@@ -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)
}
}
@@ -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
}
}
@@ -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()
}
@@ -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) }
}
}
}
@@ -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)
}
}
@@ -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)
}
}
}
@@ -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
}
@@ -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 = "最新项目"
)
}
@@ -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
}
}
}
@@ -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)
}
}
@@ -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()
}
}
}
@@ -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)
}
}
@@ -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