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
@@ -0,0 +1 @@
/build
@@ -0,0 +1,105 @@
def isBuildModule = rootProject.ext.module.isBuildModule
if (Boolean.valueOf(isBuildModule)) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-parcelize'
apply plugin: 'com.google.dagger.hilt.android'
android {
compileSdkVersion rootProject.ext.android.compileSdkVersion
buildToolsVersion rootProject.ext.android.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode rootProject.ext.android.versionCode
versionName rootProject.ext.android.versionName
multiDexEnabled true
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation":"$projectDir/schemas".toString(),
"room.incremental":"true",
"room.expandProjection":"true",
AROUTER_MODULE_NAME: project.getName()
]
}
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
buildFeatures {
viewBinding true
}
sourceSets {
main {
if (Boolean.valueOf(isBuildModule)) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
java {
//排除java/debug文件夹下的所有文件
exclude '*module'
}
}
}
}
lintOptions {
checkReleaseBuilds false
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
abortOnError false
}
}
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
generateStubs = true
useBuildCache = true
javacOptions {
option("-Xmaxerrs", 500)
}
}
dependencies {
implementation project(":common:common-base")
implementation project(":common:common-service")
kapt rootProject.ext.compiler["arouterCompiler"]
implementation rootProject.ext.roomLibs
kapt rootProject.ext.compiler["roomCompiler"]
compileOnly(rootProject.ext.jetpack["hilt"])
kapt rootProject.ext.compiler["hiltAndroidCompiler"]
if (Boolean.valueOf(isBuildModule)) {
implementation project(":modules:module-collect")
implementation project(":modules:module-content")
implementation project(":modules:module-login")
}
}
@@ -0,0 +1,2 @@
-keep class com.bbgo.module_home.bean.** {*;}
@@ -0,0 +1,23 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# 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
-keep class com.bbgo.module_home.bean.** {*;}
@@ -0,0 +1,24 @@
package com.bbgo.module_home
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.bbgo.module_home", appContext.packageName)
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.bbgo.module_home">
<application>
<activity android:name=".activity.HomeActivity">
</activity>
</application>
</manifest>
@@ -0,0 +1,13 @@
package com.bbgo.module_home
import com.bbgo.common_base.BaseApplication
import dagger.hilt.android.HiltAndroidApp
/**
* @Description:
* @Author: wangyuebin
* @Date: 2021/9/10 5:12 下午
*/
//@HiltAndroidApp
class HomeApp : BaseApplication() {
}
@@ -0,0 +1,12 @@
package com.bbgo.module_home.activity
import com.bbgo.common_base.base.BaseActivity
import com.bbgo.module_home.R
import com.bbgo.module_home.databinding.FragmentHomeBinding
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class HomeActivity : BaseActivity<FragmentHomeBinding>() {
override fun inflateViewBinding() = FragmentHomeBinding.inflate(layoutInflater)
}
@@ -0,0 +1,19 @@
package com.bbgo.module_home.bean
import androidx.annotation.Keep
/**
* @Description:
* @Author: wangyuebin
* @Date: 2021/10/22 8:59 下午
*/
@Keep
data class ArticleData(
val curPage: Int,
val datas: MutableList<ArticleDetail>,
val offset: Int,
val over: Boolean,
val pageCount: Int,
val size: Int,
val total: Int
)
@@ -0,0 +1,46 @@
package com.bbgo.module_home.bean
import androidx.annotation.Keep
/**
* @Description:
* @Author: wangyuebin
* @Date: 2021/10/22 9:00 下午
*/
@Keep
data class ArticleDetail(
val apkLink: String,
val audit: Int,
val author: String,
val canEdit: Boolean,
val chapterId: Int,
val chapterName: String,
var collect: Boolean,
val courseId: Int,
val desc: String,
val descMd: String,
val envelopePic: String,
val fresh: Boolean,
val host: String,
val id: Int,
val link: String,
val niceDate: String,
val niceShareDate: String,
val origin: String,
val prefix: String,
val projectLink: String,
val publishTime: Long,
val realSuperChapterId: Int,
val selfVisible: Int,
val shareDate: Long,
val shareUser: String,
val superChapterId: Int,
val superChapterName: String,
val tags: List<Tag>,
val title: String,
val type: Int,
val userId: Int,
val visible: Int,
val zan: Int,
var top: String,
)
@@ -0,0 +1,19 @@
package com.bbgo.module_home.bean
import androidx.annotation.Keep
import androidx.room.Entity
import androidx.room.PrimaryKey
@Keep
@Entity
data class Banner(
val desc: String,
@PrimaryKey
val id: Int,
val imagePath: String,
val isVisible: Int,
val order: Int,
val title: String,
val type: Int,
val url: String
)
@@ -0,0 +1,14 @@
package com.bbgo.module_home.bean
import androidx.annotation.Keep
/**
* @Description:
* @Author: wangyuebin
* @Date: 2021/10/22 9:00 下午
*/
@Keep
data class Tag(
val name: String,
val url: String
)
@@ -0,0 +1,16 @@
package com.bbgo.module_home.local
import androidx.room.Database
import androidx.room.RoomDatabase
import com.bbgo.module_home.bean.Banner
import com.bbgo.module_home.local.dao.BannerDao
/**
* author: wangyb
* date: 4/8/21 7:56 PM
* description: todo
*/
@Database(entities = [Banner::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun bannerDao(): BannerDao
}
@@ -0,0 +1,28 @@
package com.bbgo.module_home.local
import androidx.room.Room
import com.bbgo.common_base.BaseApplication
/**
* author: wangyb
* date: 2021/5/24 5:56 下午
* description: todo
*/
class DBUtil {
companion object {
@Volatile
private var db: AppDatabase? = null
fun getInstance(): AppDatabase {
return db ?: synchronized(AppDatabase::class.java) {
val roomDB = Room.databaseBuilder(
BaseApplication.getContext().applicationContext,
AppDatabase::class.java, "home.db").build()
db = roomDB
roomDB
}
}
}
}
@@ -0,0 +1,17 @@
package com.bbgo.module_home.local.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import com.bbgo.module_home.bean.Banner
/**
* author: wangyb
* date: 2021/6/2 11:07 上午
* description: todo
*/
@Dao
interface BannerDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(vararg data: Banner)
}
@@ -0,0 +1,37 @@
package com.bbgo.module_home.net.api
import com.bbgo.common_base.bean.HttpResult
import com.bbgo.module_home.bean.ArticleData
import com.bbgo.module_home.bean.ArticleDetail
import com.bbgo.module_home.bean.Banner
import kotlinx.coroutines.flow.Flow
import retrofit2.http.GET
import retrofit2.http.Path
/**
* author: wangyb
* date: 4/7/21 9:24 PM
* description: http api
*/
interface HttpHomeApiService {
@GET("banner/json")
fun getBanners(): Flow<HttpResult<List<Banner>>>
/**
* 获取首页置顶文章列表
* http://www.wanandroid.com/article/top/json
*/
@GET("article/top/json")
fun getTopArticles(): Flow<HttpResult<List<ArticleDetail>>>
/**
* 获取文章列表
* http://www.wanandroid.com/article/list/0/json
* @param pageNum
*/
@GET("article/list/{pageNum}/json")
fun getArticles(@Path("pageNum") pageNum: Int): Flow<HttpResult<ArticleData>>
}
@@ -0,0 +1,24 @@
package com.bbgo.module_home.repository
import com.bbgo.module_home.bean.Banner
import com.bbgo.module_home.local.DBUtil
import dagger.hilt.android.scopes.ActivityRetainedScoped
import javax.inject.Inject
/**
* author: wangyb
* date: 3/30/21 2:36 PM
* description: todo
*/
@ActivityRetainedScoped
class HomeLocalRepository @Inject constructor(){
fun insertBanners(banners: List<Banner>) {
DBUtil.getInstance().runInTransaction {
banners.forEach {
DBUtil.getInstance().bannerDao().insertAll(it)
}
}
}
}
@@ -0,0 +1,35 @@
package com.bbgo.module_home.repository
import com.bbgo.common_base.bean.HttpResult
import com.bbgo.common_base.net.ServiceCreators
import com.bbgo.module_home.bean.ArticleData
import com.bbgo.module_home.bean.ArticleDetail
import com.bbgo.module_home.bean.Banner
import com.bbgo.module_home.net.api.HttpHomeApiService
import dagger.hilt.android.scopes.ActivityRetainedScoped
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
/**
* author: wangyb
* date: 3/30/21 2:35 PM
* description: todo
*/
@ActivityRetainedScoped
class HomeRemoteRepository @Inject constructor(){
private val service = ServiceCreators.create(HttpHomeApiService::class.java)
fun getTopArticles() : Flow<HttpResult<List<ArticleDetail>>> {
return service.getTopArticles()
}
fun getArticles(pageNum: Int) : Flow<HttpResult<ArticleData>> {
return service.getArticles(pageNum)
}
fun getBanners(): Flow<HttpResult<List<Banner>>> {
return service.getBanners()
}
}
@@ -0,0 +1,34 @@
package com.bbgo.module_home.repository
import com.bbgo.common_base.bean.HttpResult
import com.bbgo.module_home.bean.ArticleData
import com.bbgo.module_home.bean.ArticleDetail
import com.bbgo.module_home.bean.Banner
import dagger.hilt.android.scopes.ActivityRetainedScoped
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
/**
* author: wangyb
* date: 3/29/21 9:32 PM
* description: todo
*/
@ActivityRetainedScoped
class HomeRepository @Inject constructor(private val remoteRepository: HomeRemoteRepository, private val localRepository: HomeLocalRepository) {
fun getTopArticles() : Flow<HttpResult<List<ArticleDetail>>> {
return remoteRepository.getTopArticles()
}
fun getArticles(pageNum: Int) : Flow<HttpResult<ArticleData>> {
return remoteRepository.getArticles(pageNum)
}
fun getBanners(): Flow<HttpResult<List<Banner>>> {
return remoteRepository.getBanners()
}
fun insertBanners(banners: List<Banner>) {
localRepository.insertBanners(banners)
}
}
@@ -0,0 +1,64 @@
package com.bbgo.module_home.ui
import android.text.Html
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.bbgo.common_base.util.ImageLoader
import com.bbgo.module_home.R
import com.bbgo.module_home.bean.ArticleDetail
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
class HomeAdapter(datas: MutableList<ArticleDetail>)
: BaseQuickAdapter<ArticleDetail, BaseViewHolder>(R.layout.item_home_list, datas) {
override fun convert(holder: BaseViewHolder, item: ArticleDetail) {
val authorStr = if (item.author.isNotEmpty()) item.author else item.shareUser
holder.setText(R.id.tv_article_title, Html.fromHtml(item.title))
.setText(R.id.tv_article_author, authorStr)
.setText(R.id.tv_article_date, item.niceDate)
.setImageResource(R.id.iv_like,
if (item.collect) R.drawable.ic_like else R.drawable.ic_like_not
)
val chapterName = when {
item.superChapterName.isNotEmpty() and item.chapterName.isNotEmpty() ->
"${item.superChapterName} / ${item.chapterName}"
item.superChapterName.isNotEmpty() -> item.superChapterName
item.chapterName.isNotEmpty() -> item.chapterName
else -> ""
}
holder.setText(R.id.tv_article_chapterName, chapterName)
if (item.envelopePic.isNotEmpty()) {
holder.getView<ImageView>(R.id.iv_article_thumbnail)
.visibility = View.VISIBLE
context?.let {
ImageLoader.load(it, item.envelopePic, holder.getView(R.id.iv_article_thumbnail))
}
} else {
holder.getView<ImageView>(R.id.iv_article_thumbnail)
.visibility = View.GONE
}
val tv_fresh = holder.getView<TextView>(R.id.tv_article_fresh)
if (item.fresh) {
tv_fresh.visibility = View.VISIBLE
} else {
tv_fresh.visibility = View.GONE
}
val tv_top = holder.getView<TextView>(R.id.tv_article_top)
if (item.top == "1") {
tv_top.visibility = View.VISIBLE
} else {
tv_top.visibility = View.GONE
}
val tv_article_tag = holder.getView<TextView>(R.id.tv_article_tag)
if (item.tags.size > 0) {
tv_article_tag.visibility = View.VISIBLE
tv_article_tag.text = item.tags[0].name
} else {
tv_article_tag.visibility = View.GONE
}
}
}
@@ -0,0 +1,293 @@
package com.bbgo.module_home.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import cn.bingoogolapple.bgabanner.BGABanner
import com.alibaba.android.arouter.facade.annotation.Autowired
import com.alibaba.android.arouter.facade.annotation.Route
import com.alibaba.android.arouter.launcher.ARouter
import com.bbgo.common_base.base.BaseFragment
import com.bbgo.common_base.bus.BusKey
import com.bbgo.common_base.bus.LiveDataBus
import com.bbgo.common_base.constants.Constants
import com.bbgo.common_base.constants.RouterPath
import com.bbgo.common_base.databinding.LayoutLoadingBinding
import com.bbgo.common_base.event.MessageEvent
import com.bbgo.common_base.event.ScrollEvent
import com.bbgo.common_base.ext.Resource
import com.bbgo.common_base.ext.collectWithLifeCycle
import com.bbgo.common_base.ext.showToast
import com.bbgo.common_base.util.ImageLoader
import com.bbgo.common_base.util.log.Logs
import com.bbgo.common_base.widget.SpaceItemDecoration
import com.bbgo.common_service.collect.CollectService
import com.bbgo.module_home.R
import com.bbgo.module_home.bean.ArticleDetail
import com.bbgo.module_home.bean.Banner
import com.bbgo.module_home.databinding.FragmentHomeBinding
import com.bbgo.module_home.databinding.ItemHomeBannerBinding
import com.bbgo.module_home.viewmodel.HomeViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* author: wangyb
* date: 2021/5/20 3:00 下午
* description: todo
*/
@Route(path = RouterPath.Home.PAGE_HOME)
@AndroidEntryPoint
class HomeFragment : BaseFragment<FragmentHomeBinding>() {
companion object {
private const val TAG = "HomeFragment"
fun getInstance(): HomeFragment {
return HomeFragment()
}
}
private lateinit var bannerBinding: ItemHomeBannerBinding
private lateinit var loadingBinding: LayoutLoadingBinding
@Inject
lateinit var homeViewModel: HomeViewModel
@Autowired(name = RouterPath.Collect.SERVICE_COLLECT)
lateinit var collectService: CollectService
/**
* RecyclerView Divider
*/
private val recyclerViewItemDecoration by lazy {
activity?.let {
SpaceItemDecoration(it)
}
}
/**
* LinearLayoutManager
*/
private val linearLayoutManager: LinearLayoutManager by lazy {
LinearLayoutManager(activity)
}
/**
* datas
*/
private val articleList = mutableListOf<ArticleDetail>()
/**
* Home Adapter
*/
private val homeAdapter: HomeAdapter by lazy {
HomeAdapter(articleList)
}
/**
* is Refresh
*/
private var isRefresh = false
/**
* RefreshListener
*/
private val onRefreshListener = SwipeRefreshLayout.OnRefreshListener {
isRefresh = true
homeViewModel.getArticles(0)
}
/**
* Banner Adapter
*/
private val bannerAdapter: BGABanner.Adapter<ImageView, String> by lazy {
BGABanner.Adapter<ImageView, String> { bgaBanner, imageView, feedImageUrl, position ->
activity?.let { ImageLoader.load(it, feedImageUrl, imageView) }
}
}
override fun initView() {
bannerBinding = ItemHomeBannerBinding.inflate(layoutInflater)
loadingBinding = LayoutLoadingBinding.bind(binding.root)
ARouter.getInstance().inject(this)
initBus()
binding.swipeRefreshLayout.run {
setOnRefreshListener(onRefreshListener)
}
binding.recyclerView.run {
layoutManager = linearLayoutManager
adapter = homeAdapter
itemAnimator = DefaultItemAnimator()
recyclerViewItemDecoration?.let { addItemDecoration(it) }
}
homeAdapter.run {
addHeaderView(bannerBinding.root)
setOnItemClickListener { adapter, view, position ->
val article = articleList[position]
ARouter.getInstance().build(RouterPath.Content.PAGE_CONTENT)
.withString(Constants.POSITION, position.toString())
.withString(Constants.CONTENT_ID_KEY, article.id.toString())
.withString(Constants.CONTENT_TITLE_KEY, article.title)
.withString(Constants.CONTENT_URL_KEY, article.link)
.withString(Constants.COLLECT, article.collect.toString())
.navigation()
}
addChildClickViewIds(R.id.iv_like)
setOnItemChildClickListener { adapter, view, position ->
if (view.id == R.id.iv_like) {
val article = articleList[position]
if (article.collect) {
collectService.unCollect(
Constants.FragmentIndex.HOME_INDEX,
position,
articleList[position].id
)
return@setOnItemChildClickListener
}
collectService.collect(
Constants.FragmentIndex.HOME_INDEX,
position,
articleList[position].id
)
}
}
}
}
/**
* 初始化事件总线,和eventbus效果相同
*/
private fun initBus() {
LiveDataBus.get().with(BusKey.COLLECT, MessageEvent::class.java).observe(viewLifecycleOwner) {
if (it.indexPage == Constants.FragmentIndex.HOME_INDEX) {
handleCollect(it)
}
}
LiveDataBus.get().with(BusKey.SCROLL_TOP, ScrollEvent::class.java).observe(viewLifecycleOwner) {
if (it.index == 0) {
scrollToTop()
}
}
}
override fun lazyLoad() {
homeViewModel.getArticles(0)
homeViewModel.getBanner()
}
override fun observe() {
collectWithLifeCycle(this) {
/**
* flow默认情况下是不管页面处于哪个生命周期都会订阅数据,不会像livedata一样,
* 在生命周期处于DESTROYED时,移除观察者。因此需要在start生命周期启动协程达到
* 和livedata一样的效果
*
* 收集多个stateflow,需要launch多次
*/
launch {
homeViewModel.articleUiState.collectLatest {
handleInfo(it)
}
}
launch {
homeViewModel.bannerUiState.collectLatest {
handleBanner(it)
}
}
}
}
private fun handleBanner(status: Resource<List<Banner>>) {
when(status) {
is Resource.Loading -> {
}
is Resource.Error -> {
Logs.e(status.exception)
}
is Resource.Success -> {
val bannerFeedList = ArrayList<String>()
val bannerTitleList = ArrayList<String>()
status.data.forEach { banner ->
bannerFeedList.add(banner.imagePath)
bannerTitleList.add(banner.title)
}
bannerBinding.banner.setDelegate { banner, imageView, model, position ->
}
bannerBinding.banner.run {
setAutoPlayAble(bannerFeedList.size > 1)
setData(bannerFeedList, bannerTitleList)
setAdapter(bannerAdapter)
}
}
}
}
private fun handleInfo(articles: Resource<List<ArticleDetail>>) {
when(articles) {
is Resource.Loading -> {
loadingBinding.progressBar.visibility = View.VISIBLE
}
is Resource.Error -> {
loadingBinding.progressBar.visibility = View.GONE
binding.swipeRefreshLayout.isRefreshing = false
}
is Resource.Success -> {
loadingBinding.progressBar.visibility = View.GONE
binding.swipeRefreshLayout.isRefreshing = false
if (isRefresh) {
articleList.clear()
articleList.addAll(articles.data)
homeAdapter.setList(articleList)
} else {
articleList.addAll(articles.data)
homeAdapter.addData(articleList)
}
}
}
}
private fun handleCollect(event: MessageEvent) {
when (event.type) {
Constants.CollectType.UNKNOWN -> {
ARouter.getInstance().build(RouterPath.LoginRegister.PAGE_LOGIN).navigation()
}
else -> {
if (event.type == Constants.CollectType.COLLECT) {
showToast(getString(R.string.collect_success))
articleList[event.position].collect = true
homeAdapter.setList(articleList)
return
}
articleList[event.position].collect = false
homeAdapter.setList(articleList)
showToast(getString(R.string.cancel_collect_success))
}
}
}
private fun scrollToTop() {
binding.recyclerView.run {
if (linearLayoutManager.findFirstVisibleItemPosition() > 20) {
scrollToPosition(0)
} else {
smoothScrollToPosition(0)
}
}
}
override fun inflateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?
) = FragmentHomeBinding.inflate(inflater, container, false)
}
@@ -0,0 +1,67 @@
package com.bbgo.module_home.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.bbgo.common_base.ext.Resource
import com.bbgo.common_base.util.log.Logs
import com.bbgo.module_home.bean.ArticleDetail
import com.bbgo.module_home.bean.Banner
import com.bbgo.module_home.repository.HomeRepository
import dagger.hilt.android.scopes.ActivityRetainedScoped
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* author: wangyb
* date: 3/29/21 9:31 PM
* description: todo
*/
@ActivityRetainedScoped
class HomeViewModel @Inject constructor(private val repository: HomeRepository) : ViewModel() {
val articleUiState = MutableStateFlow<Resource<List<ArticleDetail>>>(Resource.Loading())
val bannerUiState = MutableStateFlow<Resource<List<Banner>>>(Resource.Loading())
fun getArticles(pageNum: Int) = viewModelScope.launch {
repository.getTopArticles()
.zip(repository.getArticles(pageNum)) { topArticles, articles ->
{
val allArticles = mutableListOf<ArticleDetail>()
topArticles.data.forEach {
it.top = "1"
}
allArticles.addAll(topArticles.data)
allArticles.addAll(articles.data.datas)
allArticles
}
}
.catch {
Logs.e(it, it.message)
}
.collectLatest {
articleUiState.value = Resource.Success(it.invoke())
}
}
fun getBanner() = viewModelScope.launch(Dispatchers.IO){
repository.getBanners()
.map {
repository.insertBanners(it.data)
it
}
.catch {
Logs.e("${it.message}")
bannerUiState.value = Resource.Error(it)
}
.collectLatest {
bannerUiState.value = Resource.Success(it.data)
}
}
companion object {
private const val TAG = "HomeViewModel"
}
}
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.bbgo.module_home">
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <!-- 配置权限,用来记录应用配置信息 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- 重力加速度传感器权限 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <!-- Android8.0安装apk需要请求安装权限 -->
<uses-permission android:name="android.permission.VIBRATE" /> <!-- 小米push -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REORDER_TASKS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.hardware.sensor.accelerometer" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FLASHLIGHT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<application
android:name=".HomeApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activity.HomeActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator">
<scale android:fromXScale="2.0" android:toXScale="1.0"
android:fromYScale="2.0" android:toYScale="1.0"
android:pivotX="50%p" android:pivotY="50%p"
android:duration="@android:integer/config_mediumAnimTime" />
</set>
@@ -0,0 +1,10 @@
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:zAdjustment="top">
<scale android:fromXScale="1.0" android:toXScale=".5"
android:fromYScale="1.0" android:toYScale=".5"
android:pivotX="50%p" android:pivotY="50%p"
android:duration="@android:integer/config_mediumAnimTime" />
<alpha android:fromAlpha="1.0" android:toAlpha="0"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="1000"
android:fromAlpha="0"
android:interpolator="@android:anim/decelerate_interpolator"
android:toAlpha="1.0"/>
</set>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="1500"
android:fromAlpha="1.0"
android:interpolator="@android:anim/decelerate_interpolator"
android:toAlpha="0"/>
</set>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="-30.0%p"
android:toXDelta="0.0" />
</set>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="0.0"
android:toXDelta="-30.0%p" />
</set>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromYDelta="-50%p" android:toYDelta="0" android:duration="1000"/>
<alpha android:fromAlpha="0.0" android:toAlpha="1.0" android:duration="1000" />
</set>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromYDelta="0" android:toYDelta="-50%p" android:duration="1000"/>
<alpha android:fromAlpha="1.0" android:toAlpha="0.0" android:duration="1000" />
</set>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="100.0%p"
android:toXDelta="0.0" />
</set>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="0.0"
android:toXDelta="100.0%p" />
</set>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/list_divider" />
</shape>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke android:color="@color/Red"
android:width="@dimen/dp_05"/>
<corners android:radius="@dimen/dp_2" />
</shape>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="@dimen/dp_05"
android:color="@color/colorAccent" />
<corners android:radius="@dimen/dp_2" />
</shape>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/colorAccent"/>
<corners android:radius="@dimen/dp_2" />
</shape>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape>
<solid android:color="#A6000000" />
<padding android:left="8dp" android:top="4dp" android:right="8dp" android:bottom="4dp" />
<corners android:radius="2dp" />
</shape>
</item>
<item>
<shape>
<solid android:color="#99000000" />
<padding android:left="8dp" android:top="4dp" android:right="8dp" android:bottom="4dp" />
<corners android:radius="2dp" />
</shape>
</item>
</selector>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#00BCD4"
android:alpha="0.8">
<path
android:fillColor="@android:color/white"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
</vector>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#9E9E9E"
android:alpha="0.8">
<path
android:fillColor="@android:color/white"
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:name="com.bbgo.module_home.ui.HomeFragment"
android:layout_width="match_parent"
android:layout_height="match_parent">
</fragment>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/viewBackground">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
style="@style/RecyclerViewStyle"
tools:listitem="@layout/item_home_list" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<include layout="@layout/layout_loading"/>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:theme="@style/AppTheme.AppBarOverlay"
app:elevation="0dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:titleTextAppearance="@style/Toolbar.TitleText">
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:textColor="@color/viewBackground"
android:textSize="@dimen/sp_18" />
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<cn.bingoogolapple.bgabanner.BGABanner
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_230"
android:scaleType="fitXY"
app:banner_indicatorGravity="bottom|right"
app:banner_placeholderDrawable="@drawable/placeholder_banner"
app:banner_tipTextColor="@color/color_title_bg"
app:banner_tipTextSize="@dimen/textSize_titleSmall"
app:banner_transitionEffect="alpha" />
</LinearLayout>
@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/cardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
app:cardBackgroundColor="@color/viewBackground"
app:cardCornerRadius="@dimen/dp_1"
app:cardElevation="@dimen/dp_1">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/item_content_padding"
android:paddingRight="@dimen/item_content_padding"
android:paddingBottom="@dimen/item_content_padding">
<TextView
android:id="@+id/tv_article_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/dp_10"
android:background="@drawable/bg_fresh"
android:paddingLeft="@dimen/dp_4"
android:paddingTop="@dimen/dp_2"
android:paddingRight="@dimen/dp_4"
android:paddingBottom="@dimen/dp_2"
android:text="@string/top_tip"
android:textColor="@color/Red"
android:textSize="@dimen/sp_10"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/tv_article_fresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/dp_10"
android:layout_toRightOf="@+id/tv_article_top"
android:background="@drawable/bg_fresh"
android:paddingLeft="@dimen/dp_4"
android:paddingTop="@dimen/dp_2"
android:paddingRight="@dimen/dp_4"
android:paddingBottom="@dimen/dp_2"
android:text="@string/new_fresh"
android:textColor="@color/Red"
android:textSize="@dimen/sp_10"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/tv_article_tag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/dp_10"
android:layout_toRightOf="@+id/tv_article_fresh"
android:background="@drawable/bg_tag"
android:paddingLeft="@dimen/dp_4"
android:paddingTop="@dimen/dp_2"
android:paddingRight="@dimen/dp_4"
android:paddingBottom="@dimen/dp_2"
android:textColor="@color/colorAccent"
android:textSize="@dimen/sp_10"
android:visibility="gone"
tools:text="@string/app_name"
tools:visibility="visible" />
<TextView
android:id="@+id/tv_article_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/tv_article_top"
android:layout_marginLeft="@dimen/dp_10"
android:layout_toRightOf="@+id/tv_article_tag"
android:textColor="@color/item_author"
android:textSize="@dimen/item_tv_author"
tools:text="@string/app_name" />
<TextView
android:id="@+id/tv_article_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/tv_article_top"
android:layout_alignParentRight="true"
android:textColor="@color/item_date"
android:textSize="@dimen/item_tv_date"
tools:text="@string/app_name" />
<ImageView
android:id="@+id/iv_article_thumbnail"
android:layout_width="@dimen/item_img_width"
android:layout_height="@dimen/item_img_height"
android:layout_below="@+id/tv_article_author"
android:layout_marginLeft="@dimen/dp_10"
android:layout_marginTop="@dimen/dp_8"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/tv_article_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_article_author"
android:layout_marginLeft="@dimen/dp_10"
android:layout_marginTop="@dimen/dp_8"
android:layout_toRightOf="@+id/iv_article_thumbnail"
android:ellipsize="end"
android:gravity="top|start"
android:lineSpacingExtra="2dp"
android:maxLines="2"
android:paddingBottom="@dimen/dp_6"
android:textColor="@color/item_title"
android:textSize="@dimen/item_tv_title" />
<TextView
android:id="@+id/tv_article_chapterName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_article_title"
android:layout_alignParentBottom="true"
android:layout_marginStart="@dimen/dp_10"
android:layout_marginLeft="@dimen/dp_10"
android:layout_marginTop="@dimen/dp_10"
android:layout_marginEnd="@dimen/dp_10"
android:layout_marginRight="@dimen/dp_10"
android:layout_toRightOf="@+id/iv_article_thumbnail"
android:gravity="center"
android:textColor="@color/item_chapter"
android:textSize="@dimen/item_tv_tag"
tools:text="@string/app_name" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_like"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_like_not" />
</LinearLayout>
</RelativeLayout>
</androidx.cardview.widget.CardView>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/colorPrimary"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,16 @@
<resources>
<string name="app_name">首页模块</string>
<string name="new_fresh"></string>
<string name="top_tip">置顶</string>
<string name="collect_success">收藏成功</string>
<string name="cancel_collect_success">已取消收藏</string>
<string name="login">登录</string>
<string name="register">注册</string>
<string name="login_ing">登录中...</string>
<string name="register_ing">注册中...</string>
<string name="username_not_empty">用户名不能为空</string>
<string name="password_not_empty">密码不能为空</string>
<string name="confirm_password_not_empty">确认密码不能为空</string>
<string name="password_cannot_match">两次密码不相同</string>
</resources>
@@ -0,0 +1,17 @@
package com.bbgo.module_home
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}