,
83 | val admin_closed_comment: Boolean, //false
84 | val href: String, ///api/posts/33716448
85 | val excerptTitle: String,
86 | val author: Author,
87 | val content: String, //我们最近在和一些面向设计师、产品经理的工具软件团队合作,觉得有一些很有意思的模式,希望能够和更多的团队一起合作。
如果有这个方面相关的开发者,产品在国内拥有相当用户量的,欢迎联系我。
我们覆盖了非常大的设计师、产品经理用户群体,在帮助更多的人在专业方面成长,同时也希望能够帮到更多致力于做这个领域工具产品、为专业人群服务的团队。
直接在知乎私信我就可以。
88 | val state: String, //published
89 | val sourceUrl: String,
90 | val pageCommentsCount: Int, //0
91 | val canComment: Boolean, //false
92 | val snapshotUrl: String,
93 | val slug: Int, //33716448
94 | val publishedTime: String, //2018-02-09T11:52:11+08:00
95 | val url: String, ///p/33716448
96 | val title: String, //寻找面向设计师、产品经理的工具软件、App 合作
97 | val summary: String, //我们最近在和一些面向设计师、产品经理的工具软件团队合作,觉得有一些很有意思的模式,希望能够和更多的团队一起合作。如果有这个方面相关的开发者,产品在国内拥有相当用户量的,欢迎联系我。我们覆盖了非常大的设计师、产品经理用户群体,在帮助更多的人…
98 | val reviewingCommentsCount: Int, //0
99 | val meta: Meta,
100 | val commentPermission: String, //anyone
101 | val commentsCount: Int, //3
102 | val likesCount: Int //21
103 | ) {
104 | data class Links(
105 | val comments: String ///api/posts/33716448/comments
106 | )
107 |
108 | data class Topic(
109 | val url: String, //https://www.zhihu.com/topic/19551325
110 | val id: String, //19551325
111 | val name: String //产品经理
112 | )
113 |
114 | data class Author(
115 | val bio: String, //马力在招聘:zhuanlan.zhihu.com/p/31904197
116 | val isFollowing: Boolean, //false
117 | val hash: String, //c6e85ba5d5999df4c5ce2f2903b1ce0e
118 | val uid: Long, //26680571723776
119 | val isOrg: Boolean, //false
120 | val slug: String, //mali
121 | val isFollowed: Boolean, //false
122 | val description: String, //最美应用创始人 产品经理 设计师,欢迎关注微博:@Ma_Li | 他在好奇的注视着这个饶有趣味的世界 | 感谢在评选知乎年度荣誉会员时为我投票的各位,一起认真!| 马力的互联网学习圈:http://mali.brixd.com | 文章索引: https://zhuanlan.zhihu.com/p/25493627
123 | val name: String, //马力
124 | val profileUrl: String, //https://www.zhihu.com/people/mali
125 | val avatar: Avatar,
126 | val isOrgWhiteList: Boolean, //false
127 | val isBanned: Boolean //false
128 | ) {
129 | data class Avatar(
130 | val id: String, //ba332a401
131 | val template: String //https://pic2.zhimg.com/{id}_{size}.jpg
132 | )
133 | }
134 |
135 | data class Meta(
136 | val previous: Any, //null
137 | val next: Any //null
138 | )
139 | }
140 | }
141 |
142 | data class Author(
143 | val bio: String, //无法定义
144 | val isFollowing: Boolean, //false
145 | val hash: String, //0ddb38d93b1c1d6c91cfb94a1d46ef99
146 | val uid: Long, //33147160887296
147 | val isOrg: Boolean, //false
148 | val slug: String, //christinehu
149 | val isFollowed: Boolean, //false
150 | val description: String, //好好活下去……每天都有新打击
151 | val name: String, //夏漱香菜
152 | val badge: Badge,
153 | val profileUrl: String, //https://www.zhihu.com/people/christinehu
154 | val avatar: Avatar,
155 | val isOrgWhiteList: Boolean, //false
156 | val isBanned: Boolean //false
157 | ) {
158 | data class Avatar(
159 | val id: String, //v2-0efada2f1686480f2163224252b8f824
160 | val template: String //https://pic3.zhimg.com/{id}_{size}.jpg
161 | )
162 |
163 | data class Badge(
164 | val identity: Any, //null
165 | val best_answerer: Any //null
166 | )
167 | }
168 |
169 | data class Topic(
170 | val url: String, //https://www.zhihu.com/topic/19551771
171 | val id: String, //19551771
172 | val name: String //求职
173 | )
174 |
175 | data class Column(
176 | val slug: String, //design
177 | val name: String //可能性 | 产品与大设计
178 | )
179 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/bean/PostsListBean.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.bean
2 |
3 | /**
4 | * Created by Meiji on 2018/2/9.
5 | */
6 |
7 | data class PostsListBean(
8 | val isTitleImageFullScreen: Boolean, //false
9 | val rating: String, //none
10 | val sourceUrl: String,
11 | val publishedTime: String, //2018-01-29T15:45:38+08:00
12 | val links: Links,
13 | val author: Author,
14 | val url: String, ///p/33398689
15 | val title: String, //发布了一款叫做「抽奖助手」的小程序
16 | val titleImage: String, //https://pic4.zhimg.com/v2-de4b8114e8408d5265503c8b41f59f85_r.jpg
17 | val summary: String,
18 | val content: String, //无码科技团队前不久发布了一款叫做「抽奖助手」的小程序。目前在用户那边反馈还不错。
这个小程序主要解决如下三个场景的需求:
先说第一个需求,也是我自己的需求。有的时候想做个活动,回馈一下公众号读者,怎么做,又公平又简单呢?现在有些公众号做活动,都是写评论,点赞最多的算中奖。可是呢,你还要提示参与者写快递信息什么的。不优雅,很麻烦。
微信群里,如果抽奖,该怎么弄呢?现在也没看到很好的方式。当然,你可以用发红包,然后选最大红包或是最小红包的方式抽奖,不过,红包小了金额会重复,也不好,不好玩。
公司年会抽奖,怎么搞?不但需要考虑公平性的问题,还要考虑弄这个抽奖活动要花多少成本,我还真见过有的公司为了年会单独写程序抽奖,发短信之类的,无形中多了很多成本,弄不好还当众掉链子。
有没有一个小工具解决这些问题,并且可以用完即走,还灵活优雅呢?我们发布的这个「抽奖助手」小程序或许可以解决上面提到的需求场景。
当然,这个小程序产品还在改进优化中,还可以有很多玩法。懂得运营的用户会发现它的更多场景,比如线下活动,要搞个抽奖活动,只需要把小程序的活动页面打印出来贴那儿就行,就这么简单。
无码科技团队为什么又做小程序了呢?
微信团队对小程序寄予厚望,作为创业团队,也得积极探索可能性。
无码科技团队现在做的几款小程序,除了想触达更多用户,也是看看我们整体的技术协作能力,展示一下产品创新方面的能力。
更重要的是,这个事情很有趣啊。
咋使用?微信里搜索小程序「抽奖助手」即可。
19 | val state: String, //published
20 | val href: String, ///api/posts/33398689
21 | val meta: Meta,
22 | val commentPermission: String, //review
23 | val snapshotUrl: String,
24 | val canComment: Boolean, //false
25 | val slug: Int, //33398689
26 | val commentsCount: Int, //23
27 | val likesCount: Int //27
28 | ) {
29 | data class Links(
30 | val comments: String ///api/posts/33398689/comments
31 | )
32 |
33 | data class Author(
34 | val bio: String, //「小道消息」,微信号:WebNotes
35 | val isFollowing: Boolean, //false
36 | val hash: String, //99953853cc4219fabe8327301058357c
37 | val uid: Long, //26676083818496
38 | val isOrg: Boolean, //false
39 | val slug: String, //fenng
40 | val isFollowed: Boolean, //false
41 | val description: String, //手艺人.知乎74号用户.微信公众帐号「小道消息」, 微信号: 「WebNotes」https://readhub.me
42 | val name: String, //Fenng
43 | val profileUrl: String, //https://www.zhihu.com/people/fenng
44 | val avatar: Avatar,
45 | val isOrgWhiteList: Boolean, //false
46 | val isBanned: Boolean //false
47 | ) {
48 | data class Avatar(
49 | val id: String, //9ee82f3f07623303aa42164b523ac267
50 | val template: String //https://pic3.zhimg.com/{id}_{size}.jpg
51 | )
52 | }
53 |
54 | data class Meta(
55 | val previous: Any, //null
56 | val next: Any //null
57 | )
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/bean/ZhuanlanBean.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.bean
2 |
3 | import android.arch.persistence.room.Entity
4 | import android.arch.persistence.room.PrimaryKey
5 |
6 |
7 | /**
8 | * Created by Meiji on 2018/2/9.
9 | */
10 | @Entity(tableName = "zhuanlans")
11 | data class ZhuanlanBean(
12 | var followersCount: Int, //28569
13 | var creator: Creator,
14 | var topics: List,
15 | var activateState: String, //activated
16 | var href: String, ///api/columns/design
17 | var acceptSubmission: Boolean, //true
18 | var firstTime: Boolean, //false
19 | // var postTopics: List,
20 | var pendingName: String,
21 | var avatar: Avatar,
22 | var canManage: Boolean, //false
23 | var description: String, //关于用户体验、产品 、技术 和创业的干货集中地 | 最美应用:zuimeia.com
24 | // var pendingTopics: List,
25 | var nameCanEditUntil: Int, //0
26 | var reason: String,
27 | var banUntil: Int, //0
28 | @PrimaryKey var slug: String, //design
29 | var name: String, //可能性 | 产品与大设计
30 | var url: String, ///design
31 | var intro: String, //马力的互联网产品设计与用户体验专栏
32 | var topicsCanEditUntil: Int, //0
33 | var activateAuthorRequested: String, //none
34 | var commentPermission: String, //anyone
35 | var following: Boolean, //false
36 | var postsCount: Int, //200
37 | var canPost: Boolean, //false
38 | var type: Int = 0
39 | ) {
40 | data class Topic(
41 | var url: String, //https://www.zhihu.com/topic/19550517
42 | var id: String, //19550517
43 | var name: String //互联网
44 | )
45 |
46 | data class PostTopic(
47 | var postsCount: Int, //1
48 | var id: Int, //2
49 | var name: String //知乎
50 | )
51 |
52 | data class Creator(
53 | var bio: String, //马力在招聘:zhuanlan.zhihu.com/p/31904197
54 | var isFollowing: Boolean, //false
55 | var hash: String, //c6e85ba5d5999df4c5ce2f2903b1ce0e
56 | var uid: Long, //26680571723776
57 | var isOrg: Boolean, //false
58 | var slug: String, //mali
59 | var isFollowed: Boolean, //false
60 | var description: String, //最美应用创始人 产品经理 设计师,欢迎关注微博:@Ma_Li | 他在好奇的注视着这个饶有趣味的世界 | 感谢在评选知乎年度荣誉会员时为我投票的各位,一起认真!| 马力的互联网学习圈:http://mali.brixd.com | 文章索引: https://zhuanlan.zhihu.com/p/25493627
61 | var name: String, //马力
62 | var profileUrl: String, //https://www.zhihu.com/people/mali
63 | var avatar: Avatar,
64 | var isOrgWhiteList: Boolean, //false
65 | var isBanned: Boolean //false
66 | ) {
67 | data class Avatar(
68 | var id: String, //ba332a401
69 | var template: String //https://pic2.zhimg.com/{id}_{size}.jpg
70 | )
71 | }
72 |
73 | data class Avatar(
74 | var id: String, //v2-5410cdcdc7fb1556a27d0ddc4734e64b
75 | var template: String //https://pic2.zhimg.com/{id}_{size}.jpg
76 | )
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/binder/FooterViewBinder.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.binder
2 |
3 | import android.graphics.PorterDuff
4 | import android.os.Build
5 | import android.support.v4.graphics.drawable.DrawableCompat
6 | import android.support.v7.widget.RecyclerView
7 | import android.view.LayoutInflater
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import android.widget.ProgressBar
11 | import com.meiji.daily.App
12 | import com.meiji.daily.R
13 | import com.meiji.daily.bean.FooterBean
14 | import com.meiji.daily.util.SettingHelper
15 | import dagger.Lazy
16 | import kotlinx.android.synthetic.main.item_loading.view.*
17 | import me.drakeet.multitype.ItemViewBinder
18 | import javax.inject.Inject
19 |
20 | /**
21 | * Created by Meiji on 2017/6/7.
22 | */
23 |
24 | internal class FooterViewBinder : ItemViewBinder() {
25 |
26 | @Inject
27 | internal lateinit var mSettingHelper: Lazy
28 |
29 | override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): FooterViewBinder.ViewHolder {
30 | DaggerItemViewComponent.builder()
31 | .appComponent(App.sAppComponent)
32 | .build().inject(this)
33 | val view = inflater.inflate(R.layout.item_loading, parent, false)
34 | return ViewHolder(view)
35 | }
36 |
37 | override fun onBindViewHolder(holder: ViewHolder, item: FooterBean) {
38 | val color = mSettingHelper.get().color
39 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
40 | val wrapDrawable = DrawableCompat.wrap(holder.mProgressBar.indeterminateDrawable)
41 | DrawableCompat.setTint(wrapDrawable, color)
42 | holder.mProgressBar.indeterminateDrawable = DrawableCompat.unwrap(wrapDrawable)
43 | } else {
44 | holder.mProgressBar.indeterminateDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN)
45 | }
46 | }
47 |
48 | inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
49 |
50 | val mProgressBar: ProgressBar = itemView.loading
51 |
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/binder/ItemViewComponent.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.binder
2 |
3 | import com.meiji.daily.di.component.AppComponent
4 | import com.meiji.daily.di.scope.FragmentScoped
5 |
6 | import dagger.Component
7 |
8 | /**
9 | * Created by Meiji on 2017/12/28.
10 | */
11 | @FragmentScoped
12 | @Component(dependencies = arrayOf(AppComponent::class))
13 | internal interface ItemViewComponent {
14 |
15 | fun inject(footerViewBinder: FooterViewBinder)
16 |
17 | fun inject(zhuanlanViewBinder: ZhuanlanViewBinder)
18 |
19 | fun inject(postsListViewBinder: PostsListViewBinder)
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/binder/PostsListViewBinder.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.binder
2 |
3 | import android.support.v7.widget.CardView
4 | import android.support.v7.widget.RecyclerView
5 | import android.text.TextUtils
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.widget.ImageView
10 | import android.widget.TextView
11 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
12 | import com.meiji.daily.GlideApp
13 | import com.meiji.daily.R
14 | import com.meiji.daily.bean.PostsListBean
15 | import com.meiji.daily.module.postscontent.PostsContentActivity
16 | import kotlinx.android.synthetic.main.item_postlist.view.*
17 | import me.drakeet.multitype.ItemViewBinder
18 |
19 | /**
20 | * Created by Meiji on 2017/6/6.
21 | */
22 |
23 | internal class PostsListViewBinder : ItemViewBinder() {
24 |
25 | override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): PostsListViewBinder.ViewHolder {
26 | val view = inflater.inflate(R.layout.item_postlist, parent, false)
27 | return ViewHolder(view)
28 | }
29 |
30 | override fun onBindViewHolder(holder: PostsListViewBinder.ViewHolder, item: PostsListBean) {
31 | val context = holder.itemView.context
32 | val publishedTime = item.publishedTime.substring(0, 10)
33 | val likesCount = item.likesCount.toString() + "赞"
34 | val commentsCount = item.commentsCount.toString() + "条评论"
35 | var titleImage = item.titleImage
36 | val title = item.title
37 |
38 | if (!TextUtils.isEmpty(titleImage)) {
39 | titleImage = item.titleImage.replace("r.jpg", "b.jpg")
40 | GlideApp.with(context)
41 | .load(titleImage)
42 | .centerCrop()
43 | .error(R.color.viewBackground)
44 | .transition(DrawableTransitionOptions().crossFade())
45 | .into(holder.iv_titleImage)
46 | } else {
47 | holder.iv_titleImage.setImageResource(R.drawable.error_image)
48 | holder.iv_titleImage.scaleType = ImageView.ScaleType.CENTER_CROP
49 | }
50 | holder.tv_publishedTime.text = publishedTime
51 | holder.tv_likesCount.text = likesCount
52 | holder.tv_commentsCount.text = commentsCount
53 | holder.tv_title.text = title
54 | holder.root.setOnClickListener { PostsContentActivity.start(context, item.titleImage, item.title, item.slug.toString()) }
55 | }
56 |
57 | inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
58 |
59 | val root: CardView = itemView.cardview
60 | val tv_publishedTime: TextView = itemView.tv_publishedTime
61 | val tv_likesCount: TextView = itemView.tv_likesCount
62 | val tv_commentsCount: TextView = itemView.tv_commentsCount
63 | val iv_titleImage: ImageView = itemView.iv_titleImage
64 | val tv_title: TextView = itemView.tv_title
65 |
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/binder/ZhuanlanViewBinder.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.binder
2 |
3 | import android.support.v7.widget.CardView
4 | import android.support.v7.widget.RecyclerView
5 | import android.text.TextUtils
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.widget.TextView
10 | import com.meiji.daily.GlideApp
11 | import com.meiji.daily.R
12 | import com.meiji.daily.bean.ZhuanlanBean
13 | import com.meiji.daily.module.postslist.PostsListActivity
14 | import de.hdodenhof.circleimageview.CircleImageView
15 | import kotlinx.android.synthetic.main.item_zhuanlan.view.*
16 | import me.drakeet.multitype.ItemViewBinder
17 |
18 | /**
19 | * Created by Meiji on 2017/6/6.
20 | */
21 |
22 | internal class ZhuanlanViewBinder : ItemViewBinder() {
23 |
24 | override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ZhuanlanViewBinder.ViewHolder {
25 | val view = inflater.inflate(R.layout.item_zhuanlan, parent, false)
26 | return ViewHolder(view)
27 | }
28 |
29 | override fun onBindViewHolder(holder: ZhuanlanViewBinder.ViewHolder, item: ZhuanlanBean) {
30 | val context = holder.itemView.context
31 | val followersCount = item.followersCount.toString() + "人关注TA"
32 | val postsCount = item.postsCount.toString() + "篇文章"
33 | var avatarUrl = item.avatar.template
34 | if (!TextUtils.isEmpty(avatarUrl)) {
35 | // 拼凑avatar链接
36 | avatarUrl = avatarUrl.replace("{id}", item.avatar.id.toString()).replace("{size}", "m")
37 |
38 | GlideApp.with(context)
39 | .load(avatarUrl)
40 | .centerCrop()
41 | .error(R.color.viewBackground)
42 | .into(holder.cv_avatar)
43 | }
44 | holder.tv_name.text = item.name
45 | holder.tv_followersCount.text = followersCount
46 | holder.tv_postsCount.text = postsCount
47 | holder.tv_intro.text = item.intro
48 | holder.cardView.setOnClickListener { PostsListActivity.start(context, item.slug, item.name, item.postsCount) }
49 | }
50 |
51 | inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
52 |
53 | val cardView: CardView = itemView.cardview
54 | val cv_avatar: CircleImageView = itemView.cv_avatar
55 | val tv_name: TextView = itemView.tv_name
56 | val tv_followersCount: TextView = itemView.tv_followersCount
57 | val tv_postsCount: TextView = itemView.tv_postsCount
58 | val tv_intro: TextView = itemView.tv_intro
59 |
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/data/local/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.data.local
2 |
3 | import android.arch.persistence.room.Database
4 | import android.arch.persistence.room.Room
5 | import android.arch.persistence.room.RoomDatabase
6 | import android.arch.persistence.room.TypeConverters
7 | import android.content.Context
8 | import android.support.annotation.VisibleForTesting
9 |
10 | import com.meiji.daily.bean.ZhuanlanBean
11 | import com.meiji.daily.data.local.converter.ZhuanlanBeanConverter
12 | import com.meiji.daily.data.local.dao.ZhuanlanDao
13 |
14 | /**
15 | * Created by Meiji on 2017/11/28.
16 | */
17 | @Database(entities = [(ZhuanlanBean::class)], version = 1, exportSchema = false)
18 | @TypeConverters(ZhuanlanBeanConverter::class)
19 | abstract class AppDatabase : RoomDatabase() {
20 |
21 | abstract fun zhuanlanDao(): ZhuanlanDao
22 |
23 | companion object {
24 |
25 | @VisibleForTesting
26 | val DATABASE_NAME = "Daily"
27 | @Volatile
28 | private var sInstance: AppDatabase? = null
29 |
30 | fun getInstance(context: Context): AppDatabase {
31 | if (sInstance == null) {
32 | synchronized(AppDatabase::class.java) {
33 | if (sInstance == null) {
34 | sInstance = Room.databaseBuilder(context,
35 | AppDatabase::class.java,
36 | DATABASE_NAME
37 | ).build()
38 | }
39 | }
40 | }
41 | return sInstance!!
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/data/local/converter/ZhuanlanBeanConverter.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.data.local.converter
2 |
3 | import android.arch.persistence.room.TypeConverter
4 | import com.meiji.daily.bean.ZhuanlanBean
5 | import com.squareup.moshi.JsonAdapter
6 | import com.squareup.moshi.Moshi
7 | import com.squareup.moshi.Types
8 |
9 | /**
10 | * Created by Meiji on 2017/11/28.
11 | */
12 |
13 | class ZhuanlanBeanConverter {
14 |
15 | private val mMoshi = Moshi.Builder().build()
16 | private val mCreatorAdapter: JsonAdapter
17 | private var mAvatarAdapter: JsonAdapter
18 | private var mTopicAdapter: JsonAdapter>
19 |
20 | init {
21 | mCreatorAdapter = mMoshi.adapter(ZhuanlanBean.Creator::class.java)
22 | mAvatarAdapter = mMoshi.adapter(ZhuanlanBean.Avatar::class.java)
23 |
24 | val type = Types.newParameterizedType(List::class.java, ZhuanlanBean.Topic::class.java)
25 | mTopicAdapter = mMoshi.adapter>(type)
26 | }
27 |
28 | @TypeConverter
29 | fun fromCreator(c: ZhuanlanBean.Creator): String = mCreatorAdapter.toJson(c)
30 |
31 | @TypeConverter
32 | fun toCreator(s: String): ZhuanlanBean.Creator = mCreatorAdapter.fromJson(s)!!
33 |
34 | @TypeConverter
35 | fun fromTopicList(l: List): String = mTopicAdapter.toJson(l)
36 |
37 | @TypeConverter
38 | fun toTopicList(s: String): List = mTopicAdapter.fromJson(s)!!
39 |
40 | @TypeConverter
41 | fun fromAvatar(a: ZhuanlanBean.Avatar): String = mAvatarAdapter.toJson(a)
42 |
43 | @TypeConverter
44 | fun toAvatar(s: String): ZhuanlanBean.Avatar = mAvatarAdapter.fromJson(s)!!
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/data/local/dao/ZhuanlanDao.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.data.local.dao
2 |
3 | /**
4 | * Created by Meiji on 2017/11/28.
5 | */
6 |
7 | import android.arch.persistence.room.Dao
8 | import android.arch.persistence.room.Insert
9 | import android.arch.persistence.room.OnConflictStrategy
10 | import android.arch.persistence.room.Query
11 |
12 | import com.meiji.daily.bean.ZhuanlanBean
13 |
14 | import io.reactivex.Maybe
15 |
16 | @Dao
17 | interface ZhuanlanDao {
18 |
19 | @Insert(onConflict = OnConflictStrategy.IGNORE)
20 | fun insert(zhuanlanBean: ZhuanlanBean): Long
21 |
22 | @Insert(onConflict = OnConflictStrategy.IGNORE)
23 | fun insert(list: MutableList)
24 |
25 | @Query("SELECT * FROM zhuanlans WHERE type = :type")
26 | fun query(type: Int): Maybe>
27 |
28 | @Query("DELETE FROM zhuanlans WHERE slug = :slug")
29 | fun delete(slug: String)
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/data/remote/IApi.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.data.remote
2 |
3 | import com.meiji.daily.bean.PostsContentBean
4 | import com.meiji.daily.bean.PostsListBean
5 | import com.meiji.daily.bean.ZhuanlanBean
6 |
7 | import io.reactivex.Maybe
8 | import retrofit2.http.GET
9 | import retrofit2.http.Path
10 | import retrofit2.http.Query
11 |
12 | /**
13 | * Created by Meiji on 2016/11/16.
14 | */
15 |
16 | interface IApi {
17 |
18 | /**
19 | * 获取专栏信息 Retrofit + RxJava
20 | * https://zhuanlan.zhihu.com/api/columns/design
21 | *
22 | * @param slug
23 | * @return
24 | */
25 | @GET("api/columns/{slug}")
26 | fun getZhuanlanBean(@Path("slug") slug: String): Maybe
27 |
28 | /**
29 | * 获取专栏文章 Retrofit + RxJava
30 | * https://zhuanlan.zhihu.com/api/columns/design/posts?limit=10&offset=10
31 | *
32 | * @param slug 专栏ID
33 | * @param offset 偏移量
34 | * @return
35 | */
36 | @GET("api/columns/{slug}/posts?limit=10")
37 | fun getPostsList(
38 | @Path("slug") slug: String,
39 | @Query("offset") offset: Int): Maybe>
40 |
41 | /**
42 | * 获取文章内容 Retrofit + RxJava
43 | * https://zhuanlan.zhihu.com/api/posts/25982605
44 | *
45 | * @param slug 文章ID
46 | * @return
47 | */
48 | @GET("api/posts/{slug}")
49 | fun getPostsContentBean(@Path("slug") slug: String): Maybe
50 |
51 | companion object {
52 |
53 | const val COLUMN_URL = "https://zhuanlan.zhihu.com/api/columns/"
54 |
55 | const val POST_URL = "https://zhuanlan.zhihu.com/api/posts/"
56 |
57 | const val API_BASE = "https://zhuanlan.zhihu.com/"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/di/component/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.di.component
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 | import com.meiji.daily.App
7 | import com.meiji.daily.data.local.AppDatabase
8 | import com.meiji.daily.di.module.AppModule
9 | import com.meiji.daily.util.RxBusHelper
10 | import com.meiji.daily.util.SettingHelper
11 | import dagger.BindsInstance
12 | import dagger.Component
13 | import retrofit2.Retrofit
14 | import javax.inject.Named
15 | import javax.inject.Singleton
16 |
17 | /**
18 | * Created by Meiji on 2017/12/21.
19 | */
20 | @Singleton
21 | @Component(modules = arrayOf(AppModule::class))
22 | interface AppComponent {
23 |
24 | @get:Named("application")
25 | val application: Application
26 |
27 | @get:Named("context")
28 | val context: Context
29 |
30 | val appDatabase: AppDatabase
31 |
32 | val settingHelper: SettingHelper
33 |
34 | val retrofit: Retrofit
35 |
36 | val sharedPreferences: SharedPreferences
37 |
38 | val rxBus: RxBusHelper
39 |
40 | fun inject(app: App)
41 |
42 | @Component.Builder
43 | interface Builder {
44 | @BindsInstance
45 | fun application(application: Application): Builder
46 |
47 | @BindsInstance
48 | fun context(context: Context): Builder
49 |
50 | fun build(): AppComponent
51 | }
52 |
53 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/di/component/CommonActivityComponent.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.di.component
2 |
3 | import com.meiji.daily.MainActivity
4 | import com.meiji.daily.di.scope.ActivityScoped
5 | import com.meiji.daily.module.base.BaseActivity
6 |
7 | import dagger.Component
8 |
9 | /**
10 | * Created by Meiji on 2017/12/28.
11 | */
12 |
13 | @ActivityScoped
14 | @Component(dependencies = arrayOf(AppComponent::class))
15 | interface CommonActivityComponent {
16 |
17 | fun inject(baseActivity: BaseActivity)
18 |
19 | fun inject(mainActivity: MainActivity)
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/di/module/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.di.module
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 | import android.preference.PreferenceManager
7 | import com.meiji.daily.data.local.AppDatabase
8 | import com.meiji.daily.util.RetrofitHelper
9 | import dagger.Module
10 | import dagger.Provides
11 | import retrofit2.Retrofit
12 | import javax.inject.Named
13 | import javax.inject.Singleton
14 |
15 | /**
16 | * Created by Meiji on 2017/12/21.
17 | */
18 | @Module
19 | class AppModule {
20 |
21 | @Named("context")
22 | @Provides
23 | @Singleton
24 | internal fun provideContext(context: Context): Context {
25 | return context
26 | }
27 |
28 | @Named("application")
29 | @Provides
30 | @Singleton
31 | internal fun provideApplication(application: Application): Application {
32 | return application
33 | }
34 |
35 | @Singleton
36 | @Provides
37 | internal fun provideAppDatabase(context: Context): AppDatabase {
38 | return AppDatabase.getInstance(context)
39 | }
40 |
41 | @Singleton
42 | @Provides
43 | internal fun provideSharedPreferences(context: Context): SharedPreferences {
44 | return PreferenceManager.getDefaultSharedPreferences(context)
45 | }
46 |
47 | @Singleton
48 | @Provides
49 | internal fun provideRetrofit(context: Context): Retrofit {
50 | return RetrofitHelper(context).retrofit
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/di/scope/ActivityScoped.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.di.scope
2 |
3 | import com.meiji.daily.di.component.AppComponent
4 |
5 | import java.lang.annotation.Documented
6 | import java.lang.annotation.Retention
7 | import java.lang.annotation.RetentionPolicy
8 |
9 | import javax.inject.Scope
10 |
11 | /**
12 | * In Dagger, an unscoped component cannot depend on a scoped component. As
13 | * [AppComponent] is a scoped component (`@Singleton`, we create a custom
14 | * scope to be used by all fragment components. Additionally, a component with a specific scope
15 | * cannot have a sub component with the same scope.
16 | */
17 | @Documented
18 | @Scope
19 | @Retention(RetentionPolicy.RUNTIME)
20 | annotation class ActivityScoped
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/di/scope/FragmentScoped.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.di.scope
2 |
3 | import java.lang.annotation.Retention
4 | import java.lang.annotation.RetentionPolicy
5 |
6 | import javax.inject.Scope
7 |
8 | @Scope
9 | @Retention(RetentionPolicy.RUNTIME)
10 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FILE, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
11 | annotation class FragmentScoped
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/listener/IOnItemClickListener.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.listener
2 |
3 | import android.view.View
4 |
5 | /**
6 | * Created by Meiji on 2016/11/18.
7 | */
8 |
9 | interface IOnItemClickListener {
10 |
11 | /**
12 | * RecyclerView Item点击事件
13 | *
14 | * @param view
15 | * @param position
16 | */
17 | fun onClick(view: View, position: Int)
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.base
2 |
3 | import android.graphics.drawable.ColorDrawable
4 | import android.os.Build
5 | import android.os.Bundle
6 | import android.support.v7.app.AppCompatActivity
7 | import android.support.v7.widget.Toolbar
8 | import android.view.MenuItem
9 |
10 | import com.afollestad.materialdialogs.color.CircleView
11 | import com.meiji.daily.App
12 | import com.meiji.daily.R
13 | import com.meiji.daily.di.component.DaggerCommonActivityComponent
14 | import com.meiji.daily.util.SettingHelper
15 |
16 | import javax.inject.Inject
17 |
18 | /**
19 | * Created by Meiji on 2017/12/5.
20 | */
21 |
22 | abstract class BaseActivity : AppCompatActivity() {
23 |
24 | @Inject
25 | lateinit var mSettingHelper: SettingHelper
26 |
27 | /**
28 | * 绑定布局文件
29 | *
30 | * @return 布局文件ID
31 | */
32 | protected abstract fun attachLayoutId(): Int
33 |
34 | /**
35 | * 初始化视图控件
36 | */
37 | protected abstract fun initViews()
38 |
39 | /**
40 | * 初始化数据
41 | *
42 | * @param savedInstanceState
43 | */
44 | protected open fun initData(savedInstanceState: Bundle?) {}
45 |
46 | protected fun initTheme() {
47 | val isNightMode = mSettingHelper.isNightMode
48 | if (isNightMode) {
49 | setTheme(R.style.DarkTheme)
50 | } else {
51 | setTheme(R.style.LightTheme)
52 | }
53 | }
54 |
55 | /**
56 | * 初始化 Toolbar
57 | *
58 | * @param toolbar
59 | * @param homeAsUpEnabled
60 | * @param title
61 | */
62 | fun initToolBar(toolbar: Toolbar, homeAsUpEnabled: Boolean, title: String?) {
63 | toolbar.title = title
64 | setSupportActionBar(toolbar)
65 | if (supportActionBar != null) {
66 | supportActionBar!!.setDisplayHomeAsUpEnabled(homeAsUpEnabled)
67 | }
68 | }
69 |
70 | override fun onCreate(savedInstanceState: Bundle?) {
71 | DaggerCommonActivityComponent.builder()
72 | .appComponent(App.sAppComponent)
73 | .build().inject(this)
74 | super.onCreate(savedInstanceState)
75 | initTheme()
76 | setContentView(attachLayoutId())
77 | initViews()
78 | initData(savedInstanceState)
79 | }
80 |
81 | override fun onResume() {
82 | super.onResume()
83 | val color = mSettingHelper.color
84 | if (supportActionBar != null)
85 | supportActionBar!!.setBackgroundDrawable(ColorDrawable(color))
86 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
87 | window.statusBarColor = CircleView.shiftColorDown(color)
88 | window.navigationBarColor = color
89 | }
90 | }
91 |
92 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
93 | val id = item.itemId
94 | if (id == android.R.id.home) {
95 | onBackPressed()
96 | }
97 | return super.onOptionsItemSelected(item)
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/base/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.base
2 |
3 | import android.os.Bundle
4 | import android.support.v4.app.Fragment
5 | import android.support.v7.widget.Toolbar
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 |
10 | /**
11 | * Created by Meiji on 2017/12/4.
12 | */
13 |
14 | abstract class BaseFragment : Fragment() {
15 |
16 | /**
17 | * 绑定布局文件
18 | *
19 | * @return 布局文件ID
20 | */
21 | protected abstract fun attachLayoutId(): Int
22 |
23 | /**
24 | * 初始化视图控件
25 | */
26 | protected abstract fun initViews(view: View)
27 |
28 | /**
29 | * 初始化数据
30 | */
31 | protected fun initData() {}
32 |
33 | /**
34 | * 订阅UI组件
35 | */
36 | protected open fun subscribeUI() {}
37 |
38 | /**
39 | * 初始化 Dagger
40 | */
41 | protected open fun initInject() {}
42 |
43 | /**
44 | * 初始化 Toolbar
45 | *
46 | * @param toolbar
47 | * @param homeAsUpEnabled
48 | * @param title
49 | */
50 | protected fun initToolBar(toolbar: Toolbar, homeAsUpEnabled: Boolean, title: String) {
51 | if (activity != null) {
52 | (activity as BaseActivity).initToolBar(toolbar, homeAsUpEnabled, title)
53 | }
54 | }
55 |
56 | override fun onCreate(savedInstanceState: Bundle?) {
57 | super.onCreate(savedInstanceState)
58 | initInject()
59 | }
60 |
61 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
62 | val view = inflater.inflate(attachLayoutId(), container, false)
63 | initViews(view)
64 | initData()
65 | return view
66 | }
67 |
68 | override fun onActivityCreated(savedInstanceState: Bundle?) {
69 | super.onActivityCreated(savedInstanceState)
70 | if (!isAdded) {
71 | return
72 | }
73 | subscribeUI()
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/postscontent/PostsContentActivity.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.postscontent
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 |
7 | import com.meiji.daily.R
8 | import com.meiji.daily.module.base.BaseActivity
9 |
10 | /**
11 | * Created by Meiji on 2017/12/6.
12 | */
13 |
14 | class PostsContentActivity : BaseActivity() {
15 |
16 | override fun attachLayoutId() = R.layout.container
17 |
18 | override fun initViews() {
19 |
20 | }
21 |
22 | override fun initData(savedInstanceState: Bundle?) {
23 | if (intent == null) {
24 | finish()
25 | return
26 | }
27 | val titleImage = intent.getStringExtra(EXTRA_TITLEIMAGE)
28 | val title = intent.getStringExtra(EXTRA_TITLE)
29 | val slug = intent.getStringExtra(EXTRA_SLUG)
30 | // if (supportActionBar != null) {
31 | // supportActionBar!!.title = title
32 | // }
33 | if (savedInstanceState == null) {
34 | supportFragmentManager
35 | .beginTransaction()
36 | .replace(R.id.container, PostsContentView.newInstance(titleImage, title, slug))
37 | .commit()
38 | }
39 | }
40 |
41 | companion object {
42 |
43 | internal val EXTRA_TITLEIMAGE = "EXTRA_TITLEIMAGE"
44 | internal val EXTRA_TITLE = "EXTRA_TITLE"
45 | internal val EXTRA_SLUG = "EXTRA_SLUG"
46 |
47 | fun start(context: Context, titleImage: String, title: String, slug: String) {
48 | val starter = Intent(context, PostsContentActivity::class.java)
49 | starter.putExtra(EXTRA_TITLEIMAGE, titleImage)
50 | starter.putExtra(EXTRA_TITLE, title)
51 | starter.putExtra(EXTRA_SLUG, slug)
52 | context.startActivity(starter)
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/postscontent/PostsContentComponent.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.postscontent
2 |
3 | import com.meiji.daily.di.component.AppComponent
4 | import com.meiji.daily.di.scope.FragmentScoped
5 |
6 | import dagger.Component
7 |
8 | /**
9 | * Created by Meiji on 2017/12/28.
10 | */
11 |
12 | @FragmentScoped
13 | @Component(modules = [(PostsContentModule::class)], dependencies = [(AppComponent::class)])
14 | interface PostsContentComponent {
15 |
16 | fun inject(postsContentView: PostsContentView)
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/postscontent/PostsContentModule.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.postscontent
2 |
3 | import android.app.Application
4 | import android.arch.lifecycle.ViewModelProviders
5 | import com.meiji.daily.di.scope.FragmentScoped
6 | import dagger.Module
7 | import dagger.Provides
8 | import retrofit2.Retrofit
9 | import javax.inject.Named
10 |
11 | /**
12 | * Created by Meiji on 2017/12/28.
13 | */
14 |
15 | @Module
16 | class PostsContentModule(private val mPostsContentView: PostsContentView) {
17 |
18 | @FragmentScoped
19 | @Provides
20 | internal fun provideModel(@Named("application") application: Application,
21 | @Named("slug") slug: String,
22 | retrofit: Retrofit): PostsContentViewModel {
23 | val factory = PostsContentViewModel.Factory(application, slug, retrofit)
24 | return ViewModelProviders.of(mPostsContentView, factory).get(PostsContentViewModel::class.java)
25 | }
26 |
27 | @Provides
28 | @FragmentScoped
29 | @Named("image")
30 | internal fun provideImage(): String {
31 | val bundle = mPostsContentView.arguments
32 | return if (bundle != null) {
33 | bundle.getString(PostsContentView.ARGUMENT_TITLEIMAGE)
34 | } else ""
35 | }
36 |
37 | @Provides
38 | @FragmentScoped
39 | @Named("title")
40 | internal fun provideTitle(): String {
41 | val bundle = mPostsContentView.arguments
42 | return if (bundle != null) {
43 | bundle.getString(PostsContentView.ARGUMENT_TITLE)
44 | } else ""
45 | }
46 |
47 | @Provides
48 | @FragmentScoped
49 | @Named("slug")
50 | internal fun provideSlug(): String {
51 | val bundle = mPostsContentView.arguments
52 | return bundle?.getString(PostsContentView.ARGUMENT_SLUG, "") ?: ""
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/postscontent/PostsContentView.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.postscontent
2 |
3 | import android.annotation.SuppressLint
4 | import android.arch.lifecycle.Observer
5 | import android.content.Intent
6 | import android.content.res.ColorStateList
7 | import android.os.Bundle
8 | import android.support.design.widget.Snackbar
9 | import android.text.TextUtils
10 | import android.view.KeyEvent
11 | import android.view.View
12 | import android.webkit.WebSettings
13 | import android.webkit.WebView
14 | import android.webkit.WebViewClient
15 | import android.widget.ImageView
16 | import com.afollestad.materialdialogs.MaterialDialog
17 | import com.afollestad.materialdialogs.Theme
18 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
19 | import com.meiji.daily.App
20 | import com.meiji.daily.GlideApp
21 | import com.meiji.daily.R
22 | import com.meiji.daily.data.remote.IApi
23 | import com.meiji.daily.module.base.BaseFragment
24 | import com.meiji.daily.util.SettingHelper
25 | import kotlinx.android.synthetic.main.activity_postscontent.view.*
26 | import javax.inject.Inject
27 | import javax.inject.Named
28 |
29 | /**
30 | * Created by Meiji on 2017/12/6.
31 | */
32 |
33 | class PostsContentView : BaseFragment() {
34 |
35 | @field:[Inject Named("image")]
36 | lateinit var mImage: String
37 |
38 | @field:[Inject Named("title")]
39 | lateinit var mTitle: String
40 |
41 | @field:[Inject Named("slug")]
42 | lateinit var mSlug: String
43 |
44 | @Inject
45 | lateinit var mModel: PostsContentViewModel
46 | @Inject
47 | lateinit var mSettingHelper: SettingHelper
48 |
49 | private lateinit var mWebView: WebView
50 | private var mDialog: MaterialDialog? = null
51 |
52 | override fun initInject() {
53 | DaggerPostsContentComponent.builder()
54 | .appComponent(App.sAppComponent)
55 | .postsContentModule(com.meiji.daily.module.postscontent.PostsContentModule(this))
56 | .build().inject(this)
57 | }
58 |
59 | fun onSetWebView(url: String?) {
60 | mWebView.loadDataWithBaseURL(null, url, "text/html", "utf-8", null)
61 | }
62 |
63 | @SuppressLint("SetJavaScriptEnabled")
64 | private fun initWebClient() {
65 | val settings = mWebView.settings
66 | settings.javaScriptEnabled = true
67 | // 缩放,设置为不能缩放可以防止页面上出现放大和缩小的图标
68 | settings.builtInZoomControls = false
69 | // 缓存
70 | settings.cacheMode = WebSettings.LOAD_DEFAULT
71 | // 开启DOM storage API功能
72 | settings.domStorageEnabled = true
73 | // 开启application Cache功能
74 | settings.setAppCacheEnabled(false)
75 | // 不调用第三方浏览器即可进行页面反应
76 | mWebView.webViewClient = object : WebViewClient() {
77 | override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
78 | view.loadUrl(url)
79 | return true
80 | }
81 | }
82 |
83 | mWebView.setOnKeyListener(View.OnKeyListener { view, i, keyEvent ->
84 | if (keyEvent.keyCode == KeyEvent.KEYCODE_BACK && mWebView.canGoBack()) {
85 | mWebView.goBack()
86 | return@OnKeyListener true
87 | }
88 | false
89 | })
90 | }
91 |
92 | override fun attachLayoutId() = R.layout.activity_postscontent
93 |
94 | override fun initViews(view: View) {
95 | mWebView = view.webview_content
96 |
97 | initToolBar(view.toolbar_title, true, mTitle)
98 |
99 | view.toolbar_title.setOnClickListener { view.scrollView.smoothScrollTo(0, 0) }
100 |
101 | view.fab_share.backgroundTintList = ColorStateList.valueOf(mSettingHelper.color)
102 | view.fab_share.setOnClickListener {
103 | val shareIntent = Intent().also { it.action = Intent.ACTION_SEND; it.type = "text/plain" }
104 | val shareText = mTitle + " " + IApi.POST_URL + mSlug
105 | shareIntent.putExtra(Intent.EXTRA_TEXT, shareText)
106 | startActivity(Intent.createChooser(shareIntent, getString(R.string.share_to)))
107 | }
108 |
109 | view.collapsing_layout.let {
110 | it.setExpandedTitleTextAppearance(R.style.ExpandedAppBar)
111 | it.setCollapsedTitleTextAppearance(R.style.CollapsedAppBar)
112 | it.title = mTitle
113 | }
114 |
115 | mDialog = MaterialDialog.Builder(context!!)
116 | .progress(true, 0)
117 | .content(R.string.md_loading)
118 | .theme(if (mSettingHelper.isNightMode) Theme.DARK else Theme.LIGHT)
119 | .cancelable(true)
120 | .build()
121 |
122 | if (TextUtils.isEmpty(mImage)) {
123 | view.iv_titleimage.setImageResource(R.drawable.error_image)
124 | view.iv_titleimage.scaleType = ImageView.ScaleType.CENTER_CROP
125 | } else {
126 | GlideApp.with(this)
127 | .load(mImage)
128 | .centerCrop()
129 | .error(R.color.viewBackground)
130 | .transition(DrawableTransitionOptions().crossFade())
131 | .into(view.iv_titleimage)
132 | }
133 |
134 | initWebClient()
135 | }
136 |
137 | override fun subscribeUI() {
138 | mModel.html.observe(this, Observer { s ->
139 | if (!TextUtils.isEmpty(s)) {
140 | onSetWebView(s)
141 | } else {
142 | onShowNetError()
143 | }
144 | })
145 | mModel.isLoading.observe(this, Observer { aBoolean ->
146 | if (aBoolean!!) {
147 | onShowLoading()
148 | } else {
149 | onHideLoading()
150 | }
151 | })
152 | }
153 |
154 | fun onShowLoading() {
155 | mDialog!!.show()
156 | }
157 |
158 | fun onHideLoading() {
159 | mDialog!!.dismiss()
160 | }
161 |
162 | fun onShowNetError() {
163 | mDialog!!.dismiss()
164 | Snackbar.make(mWebView, R.string.network_error, Snackbar.LENGTH_SHORT).show()
165 | }
166 |
167 | companion object {
168 |
169 | internal val ARGUMENT_TITLEIMAGE = "ARGUMENT_TITLEIMAGE"
170 | internal val ARGUMENT_TITLE = "ARGUMENT_TITLE"
171 | internal val ARGUMENT_SLUG = "ARGUMENT_SLUG"
172 |
173 | fun newInstance(titleImage: String, title: String, slug: String): PostsContentView {
174 | val args = Bundle()
175 | args.putString(ARGUMENT_TITLEIMAGE, titleImage)
176 | args.putString(ARGUMENT_TITLE, title)
177 | args.putString(ARGUMENT_SLUG, slug)
178 | val fragment = com.meiji.daily.module.postscontent.PostsContentView()
179 | fragment.arguments = args
180 | return fragment
181 | }
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/postscontent/PostsContentViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.postscontent
2 |
3 | import android.app.Application
4 | import android.arch.lifecycle.AndroidViewModel
5 | import android.arch.lifecycle.MutableLiveData
6 | import android.arch.lifecycle.ViewModel
7 | import android.arch.lifecycle.ViewModelProvider
8 | import com.meiji.daily.data.remote.IApi
9 | import com.meiji.daily.io
10 | import com.meiji.daily.mainThread
11 | import com.meiji.daily.util.ErrorAction
12 | import io.reactivex.disposables.CompositeDisposable
13 | import io.reactivex.functions.Consumer
14 | import retrofit2.Retrofit
15 |
16 | /**
17 | * Created by Meiji on 2017/12/5.
18 | */
19 |
20 | class PostsContentViewModel
21 | internal constructor(application: Application,
22 | slug: String,
23 | private val mRetrofit: Retrofit) : AndroidViewModel(application) {
24 | private val mDisposable: CompositeDisposable
25 |
26 | var isLoading: MutableLiveData
27 | private set
28 | var html: MutableLiveData
29 | private set
30 |
31 | init {
32 | isLoading = MutableLiveData()
33 | html = MutableLiveData()
34 | mDisposable = CompositeDisposable()
35 | handleData(slug)
36 | }
37 |
38 | private fun handleData(slug: String) {
39 | isLoading.value = true
40 |
41 | mRetrofit.create(IApi::class.java).getPostsContentBean(slug)
42 | .subscribeOn(io)
43 | .observeOn(mainThread)
44 | .subscribe(Consumer { bean ->
45 | isLoading.value = false
46 | html.value = parserHTML(bean.content)
47 | }, object : ErrorAction() {
48 | override fun doAction() {
49 | isLoading.value = false
50 | html.value = null
51 | }
52 | }.action()).let { mDisposable.add(it) }
53 | }
54 |
55 | private fun parserHTML(content: String): String {
56 | val css = ""
57 |
58 | return ("\n"
59 | + "\n"
60 | + "\n"
61 | + "\t\n\n"
62 | + css
63 | + "\n"
64 | + content
65 | + "\n")
66 | }
67 |
68 | override fun onCleared() {
69 | mDisposable.clear()
70 | super.onCleared()
71 | }
72 |
73 | class Factory internal constructor(private val mApplication: Application,
74 | private val mSlug: String,
75 | private val mRetrofit: Retrofit) : ViewModelProvider.NewInstanceFactory() {
76 |
77 | override fun create(modelClass: Class): T {
78 | return com.meiji.daily.module.postscontent.PostsContentViewModel(mApplication, mSlug, mRetrofit) as T
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/postslist/PostsListActivity.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.postslist
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 |
7 | import com.meiji.daily.R
8 | import com.meiji.daily.module.base.BaseActivity
9 |
10 | /**
11 | * Created by Meiji on 2017/12/5.
12 | */
13 |
14 | class PostsListActivity : BaseActivity() {
15 |
16 | override fun attachLayoutId() = R.layout.container
17 |
18 | override fun initViews() {}
19 |
20 | override fun initData(savedInstanceState: Bundle?) {
21 | if (intent == null) {
22 | finish()
23 | return
24 | }
25 | val slug = intent.getStringExtra(EXTRA_SLUG)
26 | val title = intent.getStringExtra(EXTRA_NAME)
27 | val postCount = intent.getIntExtra(EXTRA_POSTSCOUNT, 0)
28 | // supportActionBar?.title = title
29 | if (savedInstanceState == null) {
30 | supportFragmentManager
31 | .beginTransaction()
32 | .replace(R.id.container, PostsListView.newInstance(slug, title, postCount))
33 | .commit()
34 | }
35 | }
36 |
37 | companion object {
38 |
39 | private val EXTRA_SLUG = "EXTRA_SLUG"
40 | private val EXTRA_NAME = "EXTRA_NAME"
41 | private val EXTRA_POSTSCOUNT = "EXTRA_POSTSCOUNT"
42 |
43 | fun start(context: Context, slug: String, title: String, postsCount: Int) {
44 | val starter = Intent(context, PostsListActivity::class.java)
45 | .putExtra(EXTRA_SLUG, slug)
46 | .putExtra(EXTRA_NAME, title)
47 | .putExtra(EXTRA_POSTSCOUNT, postsCount)
48 | context.startActivity(starter)
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/postslist/PostsListComponent.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.postslist
2 |
3 | import com.meiji.daily.di.component.AppComponent
4 | import com.meiji.daily.di.scope.FragmentScoped
5 |
6 | import dagger.Component
7 |
8 | /**
9 | * Created by Meiji on 2017/12/28.
10 | */
11 | @FragmentScoped
12 | @Component(modules = [(PostsListModule::class)], dependencies = [(AppComponent::class)])
13 | interface PostsListComponent {
14 |
15 | fun inject(postsListView: PostsListView)
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/postslist/PostsListModule.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.postslist
2 |
3 | import android.app.Application
4 | import android.arch.lifecycle.ViewModelProviders
5 | import com.meiji.daily.di.scope.FragmentScoped
6 | import dagger.Module
7 | import dagger.Provides
8 | import retrofit2.Retrofit
9 | import javax.inject.Named
10 |
11 | /**
12 | * Created by Meiji on 2017/12/28.
13 | */
14 |
15 | @Module
16 | class PostsListModule(private val mPostsListView: PostsListView) {
17 |
18 | @FragmentScoped
19 | @Provides
20 | internal fun provideModule(@Named("application") application: Application,
21 | @Named("slug") slug: String,
22 | @Named("count") postCount: Int,
23 | retrofit: Retrofit): PostsListViewModel {
24 | val factory = PostsListViewModel.Factory(application, slug, postCount, retrofit)
25 | return ViewModelProviders.of(mPostsListView, factory).get(PostsListViewModel::class.java)
26 | }
27 |
28 | @FragmentScoped
29 | @Provides
30 | @Named("slug")
31 | internal fun provideSlug(): String {
32 | val bundle = mPostsListView.arguments
33 | return if (bundle != null) {
34 | bundle.getString(PostsListView.ARGUMENT_SLUG)
35 | } else ""
36 | }
37 |
38 | @FragmentScoped
39 | @Provides
40 | @Named("count")
41 | internal fun providePostCount(): Int {
42 | val bundle = mPostsListView.arguments
43 | return bundle?.getInt(PostsListView.ARGUMENT_POSTSCOUNT, 0) ?: 0
44 | }
45 |
46 | @FragmentScoped
47 | @Provides
48 | @Named("title")
49 | internal fun provideTitle(): String {
50 | val bundle = mPostsListView.arguments
51 | return if (bundle != null) {
52 | bundle.getString(PostsListView.ARGUMENT_NAME)
53 | } else ""
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/postslist/PostsListView.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.postslist
2 |
3 | import android.arch.lifecycle.Observer
4 | import android.os.Bundle
5 | import android.support.design.widget.Snackbar
6 | import android.support.v4.widget.SwipeRefreshLayout
7 | import android.support.v7.widget.LinearLayoutManager
8 | import android.view.View
9 | import com.meiji.daily.App
10 | import com.meiji.daily.R
11 | import com.meiji.daily.bean.FooterBean
12 | import com.meiji.daily.bean.PostsListBean
13 | import com.meiji.daily.binder.FooterViewBinder
14 | import com.meiji.daily.binder.PostsListViewBinder
15 | import com.meiji.daily.module.base.BaseFragment
16 | import com.meiji.daily.module.postslist.PostsListModule
17 | import com.meiji.daily.util.DiffCallback
18 | import com.meiji.daily.util.OnLoadMoreListener
19 | import com.meiji.daily.util.SettingHelper
20 | import kotlinx.android.synthetic.main.activity_postslist.*
21 | import kotlinx.android.synthetic.main.activity_postslist.view.*
22 | import me.drakeet.multitype.Items
23 | import me.drakeet.multitype.MultiTypeAdapter
24 | import javax.inject.Inject
25 | import javax.inject.Named
26 |
27 |
28 | /**
29 | * Created by Meiji on 2017/12/5.
30 | */
31 |
32 | class PostsListView : BaseFragment(), SwipeRefreshLayout.OnRefreshListener {
33 |
34 | @field:[Inject Named("title")]
35 | lateinit var mTitle: String
36 | @Inject
37 | lateinit var mModel: PostsListViewModel
38 | @Inject
39 | lateinit var mSettingHelper: SettingHelper
40 |
41 | private val mOldItems = Items()
42 | private var mAdapter: MultiTypeAdapter? = null
43 | private var mCanloadmore: Boolean = false
44 |
45 | override fun initInject() {
46 | DaggerPostsListComponent.builder()
47 | .appComponent(App.sAppComponent)
48 | .postsListModule(PostsListModule(this))
49 | .build().inject(this)
50 | }
51 |
52 | override fun attachLayoutId() = R.layout.activity_postslist
53 |
54 | override fun initViews(view: View) {
55 | initToolBar(view.toolbar_title, true, mTitle)
56 | view.toolbar_title.setOnClickListener { view.recycler_view.smoothScrollToPosition(0) }
57 | view.recycler_view.layoutManager = LinearLayoutManager(context)
58 | view.recycler_view.setHasFixedSize(true)
59 | // 设置下拉刷新的按钮的颜色
60 | view.refresh_layout.setColorSchemeColors(mSettingHelper.color)
61 | view.refresh_layout.setOnRefreshListener(this)
62 |
63 | mAdapter = MultiTypeAdapter()
64 | mAdapter!!.register(PostsListBean::class.java, PostsListViewBinder())
65 | mAdapter!!.register(FooterBean::class.java, FooterViewBinder())
66 | mAdapter!!.items = mOldItems
67 | view.recycler_view.adapter = mAdapter
68 | }
69 |
70 | override fun subscribeUI() {
71 | mModel.mListLiveData.observe(this, Observer> { list ->
72 | if (null != list && list.isNotEmpty()) {
73 | onSetAdapter(list)
74 | } else {
75 | onShowNetError()
76 | }
77 | })
78 | mModel.isLoading.observe(this, Observer { aBoolean ->
79 | if (aBoolean!!) {
80 | onShowLoading()
81 | } else {
82 | onHideLoading()
83 | }
84 | })
85 | mModel.isEnd.observe(this, Observer {
86 | Snackbar.make(refresh_layout, R.string.no_more, Snackbar.LENGTH_SHORT).show()
87 | if (mOldItems.size > 0) {
88 | mOldItems.removeAt(mOldItems.size - 1)
89 | mAdapter!!.notifyDataSetChanged()
90 | }
91 | })
92 | }
93 |
94 | fun onSetAdapter(list: List?) {
95 | val newItems = Items(list!!)
96 | newItems.add(FooterBean())
97 |
98 | DiffCallback.create(mOldItems, newItems, mAdapter!!)
99 | mOldItems.clear()
100 | mOldItems.addAll(newItems)
101 |
102 | mCanloadmore = true
103 |
104 | recycler_view.addOnScrollListener(object : OnLoadMoreListener() {
105 | override fun onLoadMore() {
106 | if (mCanloadmore) {
107 | mCanloadmore = false
108 | mModel.loadMore()
109 | }
110 | }
111 | })
112 | }
113 |
114 | override fun onRefresh() {
115 | mModel.doRefresh()
116 | }
117 |
118 | fun onShowLoading() {
119 | refresh_layout.isRefreshing = true
120 | }
121 |
122 | fun onHideLoading() {
123 | refresh_layout.isRefreshing = false
124 | }
125 |
126 | fun onShowNetError() {
127 | Snackbar.make(refresh_layout, R.string.network_error, Snackbar.LENGTH_SHORT).show()
128 | }
129 |
130 | companion object {
131 |
132 | const val ARGUMENT_SLUG = "ARGUMENT_SLUG"
133 | const val ARGUMENT_NAME = "ARGUMENT_NAME"
134 | const val ARGUMENT_POSTSCOUNT = "ARGUMENT_POSTSCOUNT"
135 | const val TAG = "PostsListView"
136 |
137 | fun newInstance(slug: String, title: String, postsCount: Int): PostsListView {
138 | val args = Bundle()
139 | args.putString(ARGUMENT_SLUG, slug)
140 | args.putString(ARGUMENT_NAME, title)
141 | args.putInt(ARGUMENT_POSTSCOUNT, postsCount)
142 | val fragment = PostsListView()
143 | fragment.arguments = args
144 | return fragment
145 | }
146 | }
147 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/postslist/PostsListViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.postslist
2 |
3 | import android.app.Application
4 | import android.arch.lifecycle.*
5 | import com.meiji.daily.bean.PostsListBean
6 | import com.meiji.daily.data.remote.IApi
7 | import com.meiji.daily.io
8 | import com.meiji.daily.mainThread
9 | import com.meiji.daily.util.ErrorAction
10 | import io.reactivex.disposables.CompositeDisposable
11 | import io.reactivex.functions.Consumer
12 | import retrofit2.Retrofit
13 | import java.util.*
14 |
15 | /**
16 | * Created by Meiji on 2017/12/5.
17 | */
18 |
19 | class PostsListViewModel
20 | constructor(application: Application,
21 | private val mSlug: String,
22 | private val mPostCount: Int,
23 | private val mRetrofit: Retrofit) : AndroidViewModel(application) {
24 | private val mDisposable: CompositeDisposable
25 | private var mList: MutableList
26 | var isLoading: MutableLiveData
27 | private set
28 | var isEnd: MutableLiveData
29 | private set
30 | var mOffset: MutableLiveData
31 | var mListLiveData: LiveData>
32 | private set
33 |
34 | init {
35 | mList = ArrayList()
36 | mDisposable = CompositeDisposable()
37 | isLoading = MutableLiveData()
38 | isEnd = MutableLiveData()
39 | mListLiveData = MutableLiveData()
40 | mOffset = MutableLiveData()
41 |
42 | isLoading.value = true
43 | mOffset.value = 0
44 |
45 | // 当 mOffset 的值发生改变,就会执行 apply
46 | mListLiveData = Transformations.switchMap(mOffset) { offset -> handleData(offset!!) }
47 | }
48 |
49 | private fun handleData(offset: Int): LiveData> {
50 |
51 | val liveData = MutableLiveData>()
52 |
53 | mRetrofit.create(IApi::class.java).getPostsList(mSlug, offset)
54 | .subscribeOn(io)
55 | .observeOn(mainThread)
56 | .subscribe(Consumer { list ->
57 | mList.addAll(list)
58 | liveData.value = mList
59 | isLoading.value = false
60 | }, object : ErrorAction() {
61 | override fun doAction() {
62 | liveData.value = null
63 | }
64 | }.action()).let { mDisposable.add(it) }
65 | return liveData
66 | }
67 |
68 | internal fun doRefresh() {
69 | mList.clear()
70 | mOffset.value = 0
71 | }
72 |
73 | internal fun loadMore() {
74 | if (mOffset.value != null && mOffset.value!! >= mPostCount - 1) {
75 | isEnd.value = true
76 | return
77 | }
78 |
79 | mOffset.value = mList.size
80 | }
81 |
82 | override fun onCleared() {
83 | mDisposable.clear()
84 | super.onCleared()
85 | }
86 |
87 | class Factory internal constructor(private val mApplication: Application,
88 | private val mSlug: String,
89 | private val mPostCount: Int,
90 | private val mRetrofit: Retrofit) : ViewModelProvider.NewInstanceFactory() {
91 |
92 | override fun create(modelClass: Class): T {
93 | return com.meiji.daily.module.postslist.PostsListViewModel(mApplication, mSlug, mPostCount, mRetrofit) as T
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/shareadd/ShareAddActivity.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.shareadd
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.os.Handler
6 | import android.support.v7.app.AppCompatActivity
7 | import android.text.TextUtils
8 | import android.widget.Toast
9 | import com.afollestad.materialdialogs.MaterialDialog
10 | import com.afollestad.materialdialogs.Theme
11 | import com.meiji.daily.*
12 | import com.meiji.daily.data.local.AppDatabase
13 | import com.meiji.daily.data.remote.IApi
14 | import com.meiji.daily.util.SettingHelper
15 | import io.reactivex.disposables.CompositeDisposable
16 | import io.reactivex.functions.Consumer
17 | import retrofit2.Retrofit
18 | import java.util.regex.Pattern
19 | import javax.inject.Inject
20 |
21 | /**
22 | * Created by Meiji on 2016/12/1.
23 | */
24 |
25 | class ShareAddActivity : AppCompatActivity() {
26 | private val mDisposable: CompositeDisposable = CompositeDisposable()
27 | @Inject
28 | lateinit var mSettingHelper: SettingHelper
29 | @Inject
30 | lateinit var mAppDatabase: AppDatabase
31 | @Inject
32 | lateinit var mRetrofit: Retrofit
33 | private lateinit var mDialog: MaterialDialog
34 |
35 | override fun onCreate(savedInstanceState: Bundle?) {
36 | DaggerShareAddComponent.builder()
37 | .appComponent(App.sAppComponent)
38 | .build().inject(this)
39 | super.onCreate(savedInstanceState)
40 | mDialog = MaterialDialog.Builder(this)
41 | .progress(true, 0)
42 | .content(R.string.md_loading)
43 | .theme(if (mSettingHelper.isNightMode) Theme.DARK else Theme.LIGHT)
44 | .cancelable(true)
45 | .build()
46 | mDialog.show()
47 |
48 | val intent = intent
49 | val action = intent.action
50 | val type = intent.type
51 | val shareText = intent.getStringExtra(Intent.EXTRA_TEXT)
52 | if (action == Intent.ACTION_SEND && type == "text/plain" && !TextUtils.isEmpty(shareText)) {
53 | handleSendText(shareText)
54 | } else {
55 | onFinish(getString(R.string.formal_incorrect))
56 | }
57 | }
58 |
59 | private fun handleSendText(shareText: String) {
60 |
61 | val regex = "^.*http.*://zhuanlan.zhihu.com/(.*)$"
62 | val matcher = Pattern.compile(regex).matcher(shareText)
63 | if (matcher.find()) {
64 | val slug = matcher.group(1).toLowerCase()
65 | mAppDatabase.zhuanlanDao().query(Constant.TYPE_USERADD)
66 | .subscribeOn(io)
67 | .observeOn(mainThread)
68 | .subscribe(Consumer { list ->
69 | for (bean in list) {
70 | if (bean.slug == slug) {
71 | onFinish(getString(R.string.has_been_added))
72 | return@Consumer
73 | }
74 | }
75 | }).let { mDisposable.add(it) }
76 |
77 | mRetrofit.create(IApi::class.java).getZhuanlanBean(slug)
78 | .map { bean ->
79 | bean.type = Constant.TYPE_USERADD
80 | return@map mAppDatabase.zhuanlanDao().insert(bean).toInt() != -1
81 | }
82 | .subscribeOn(io)
83 | .observeOn(mainThread)
84 | .subscribe({ isSuccess ->
85 | if (isSuccess!!) {
86 | onFinish(getString(R.string.add_zhuanlan_id_success))
87 | } else {
88 | onFinish(getString(R.string.add_zhuanlan_id_error))
89 | }
90 | }) { onFinish(getString(R.string.add_zhuanlan_id_error)) }
91 | .let { mDisposable.add(it) }
92 | } else {
93 | onFinish(getString(R.string.incorrect_link))
94 | }
95 | }
96 |
97 | override fun onDestroy() {
98 | mDisposable.clear()
99 | super.onDestroy()
100 | }
101 |
102 | private fun onFinish(message: String) {
103 | mDialog.dismiss()
104 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
105 | Handler().postDelayed({ finish() }, 800)
106 | }
107 |
108 | companion object {
109 |
110 | internal val TAG = "ShareAddActivity"
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/shareadd/ShareAddComponent.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.shareadd
2 |
3 | import com.meiji.daily.di.component.AppComponent
4 | import com.meiji.daily.di.scope.ActivityScoped
5 |
6 | import dagger.Component
7 |
8 | /**
9 | * Created by Meiji on 2017/12/28.
10 | */
11 | @ActivityScoped
12 | @Component(dependencies = arrayOf(AppComponent::class))
13 | interface ShareAddComponent {
14 |
15 | fun inject(shareAddActivity: ShareAddActivity)
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/useradd/UserAddComponent.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.useradd
2 |
3 | import com.meiji.daily.di.component.AppComponent
4 | import com.meiji.daily.di.scope.FragmentScoped
5 |
6 | import dagger.Component
7 |
8 | /**
9 | * Created by Meiji on 2017/12/28.
10 | */
11 | @FragmentScoped
12 | @Component(modules = arrayOf(UserAddModule::class), dependencies = arrayOf(AppComponent::class))
13 | interface UserAddComponent {
14 |
15 | fun inject(userAddView: UserAddView)
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/useradd/UserAddModule.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.useradd
2 |
3 | import android.app.Application
4 | import android.arch.lifecycle.ViewModelProviders
5 | import com.meiji.daily.data.local.AppDatabase
6 | import com.meiji.daily.di.scope.FragmentScoped
7 | import com.meiji.daily.util.RxBusHelper
8 | import dagger.Module
9 | import dagger.Provides
10 | import retrofit2.Retrofit
11 | import javax.inject.Named
12 |
13 | /**
14 | * Created by Meiji on 2017/12/28.
15 | */
16 | @Module
17 | class UserAddModule(private val mUserAddView: UserAddView) {
18 |
19 | @FragmentScoped
20 | @Provides
21 | internal fun provideModel(@Named("application") application: Application,
22 | appDatabase: AppDatabase,
23 | retrofit: Retrofit,
24 | rxBusHelper: RxBusHelper): UserAddViewModel {
25 | val factory = UserAddViewModel.Factory(application, appDatabase, retrofit, rxBusHelper)
26 | return ViewModelProviders.of(mUserAddView, factory).get(UserAddViewModel::class.java)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/useradd/UserAddViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.useradd
2 |
3 | import android.app.Application
4 | import android.arch.lifecycle.AndroidViewModel
5 | import android.arch.lifecycle.MutableLiveData
6 | import android.arch.lifecycle.ViewModel
7 | import android.arch.lifecycle.ViewModelProvider
8 | import com.meiji.daily.Constant
9 | import com.meiji.daily.bean.ZhuanlanBean
10 | import com.meiji.daily.data.local.AppDatabase
11 | import com.meiji.daily.data.remote.IApi
12 | import com.meiji.daily.io
13 | import com.meiji.daily.mainThread
14 | import com.meiji.daily.util.ErrorAction
15 | import com.meiji.daily.util.RxBusHelper
16 | import io.reactivex.Flowable
17 | import io.reactivex.Single
18 | import io.reactivex.SingleOnSubscribe
19 | import io.reactivex.disposables.CompositeDisposable
20 | import io.reactivex.functions.Consumer
21 | import retrofit2.Retrofit
22 |
23 | /**
24 | * Created by Meiji on 2017/12/4.
25 | */
26 |
27 | class UserAddViewModel
28 | constructor(application: Application,
29 | private val mAppDatabase: AppDatabase,
30 | private val mRetrofit: Retrofit,
31 | private val mRxBusHelper: RxBusHelper)
32 | : AndroidViewModel(application) {
33 |
34 | private val mDisposable: CompositeDisposable
35 |
36 | private lateinit var mRxBus: Flowable
37 | var isLoading: MutableLiveData
38 | private set
39 | var isRefreshUI: MutableLiveData
40 | private set
41 | var isAddResult: MutableLiveData
42 | private set
43 | var mList: MutableLiveData>
44 | private set
45 |
46 | init {
47 | isLoading = MutableLiveData()
48 | isRefreshUI = MutableLiveData()
49 | isAddResult = MutableLiveData()
50 | mList = MutableLiveData()
51 | mDisposable = CompositeDisposable()
52 |
53 | isLoading.value = true
54 | isRefreshUI.value = true
55 | }
56 |
57 | init {
58 | handleData()
59 | subscribeTheme()
60 | }
61 |
62 | fun handleData() {
63 | isLoading.value = true
64 |
65 | mAppDatabase.zhuanlanDao().query(Constant.TYPE_USERADD)
66 | .subscribeOn(io)
67 | .observeOn(mainThread)
68 | .subscribe(Consumer> { list ->
69 | mList.value = list
70 | }, ErrorAction.error()).let { mDisposable.add(it) }
71 | isLoading.value = false
72 | }
73 |
74 | internal fun addItem(input: String) {
75 | isLoading.value = true
76 |
77 | mRetrofit.create(IApi::class.java).getZhuanlanBean(input)
78 | .doOnSuccess { bean ->
79 | bean?.let {
80 | it.type = Constant.TYPE_USERADD
81 | mAppDatabase.zhuanlanDao().insert(it)
82 | }
83 | }
84 | .subscribeOn(io)
85 | .observeOn(mainThread)
86 | .subscribe(Consumer {
87 | isAddResult.value = true
88 | handleData()
89 | }, object : ErrorAction() {
90 | override fun doAction() {
91 | isAddResult.value = false
92 | isLoading.value = false
93 | }
94 | }.action()).let { mDisposable.add(it) }
95 | }
96 |
97 | private fun subscribeTheme() {
98 | mRxBus = mRxBusHelper.register(Constant.RxBusEvent.REFRESHUI)
99 | mRxBus.subscribe(Consumer {
100 | isRefreshUI.setValue(!(isRefreshUI.value)!!)
101 | }, ErrorAction.error()).let { mDisposable.add(it) }
102 | }
103 |
104 | internal fun deleteItem(bean: ZhuanlanBean) {
105 | Single.create(SingleOnSubscribe {
106 | mAppDatabase.zhuanlanDao().delete(bean.slug)
107 | }).subscribeOn(io)
108 | .subscribe().let { mDisposable.add(it) }
109 |
110 | }
111 |
112 | override fun onCleared() {
113 | mRxBusHelper.unregister(Constant.RxBusEvent.REFRESHUI, mRxBus)
114 | mDisposable.clear()
115 | super.onCleared()
116 | }
117 |
118 | class Factory internal constructor(private val mApplication: Application,
119 | private val mAppDatabase: AppDatabase,
120 | private val mRetrofit: Retrofit,
121 | private val mRxBusHelper: RxBusHelper)
122 | : ViewModelProvider.NewInstanceFactory() {
123 |
124 | override fun create(modelClass: Class): T {
125 | return UserAddViewModel(mApplication, mAppDatabase, mRetrofit, mRxBusHelper) as T
126 | }
127 | }
128 | }
129 |
130 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/zhuanlan/ZhuanlanComponent.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.zhuanlan
2 |
3 | import com.meiji.daily.di.component.AppComponent
4 | import com.meiji.daily.di.scope.FragmentScoped
5 |
6 | import dagger.Component
7 |
8 | /**
9 | * Created by Meiji on 2017/12/21.
10 | */
11 | @FragmentScoped
12 | @Component(modules = arrayOf(ZhuanlanModule::class), dependencies = arrayOf(AppComponent::class))
13 | interface ZhuanlanComponent {
14 |
15 | // 与 dependencies 有冲突
16 | // @Component.Builder
17 | // interface Builder {
18 | // Builder injectView(ZhuanlanView view);
19 | //
20 | // Builder injectType(int type);
21 | //
22 | // ZhuanlanComponent build();
23 | // }
24 |
25 | fun inject(view: ZhuanlanView)
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/zhuanlan/ZhuanlanModule.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.zhuanlan
2 |
3 | import android.app.Application
4 | import android.arch.lifecycle.ViewModelProviders
5 | import com.meiji.daily.Constant
6 | import com.meiji.daily.data.local.AppDatabase
7 | import com.meiji.daily.di.scope.FragmentScoped
8 | import com.meiji.daily.util.RxBusHelper
9 | import dagger.Module
10 | import dagger.Provides
11 | import retrofit2.Retrofit
12 | import javax.inject.Named
13 |
14 | /**
15 | * Created by Meiji on 2017/12/21.
16 | */
17 | @Module
18 | class ZhuanlanModule(private val mZhuanlanView: ZhuanlanView) {
19 |
20 | @FragmentScoped
21 | @Provides
22 | internal fun provideModel(@Named("application") application: Application,
23 | @Named("type") type: Int,
24 | appDatabase: AppDatabase,
25 | retrofit: Retrofit,
26 | rxBusHelper: RxBusHelper): ZhuanlanViewModel {
27 | val factory = ZhuanlanViewModel.Factory(application, type, appDatabase, retrofit, rxBusHelper)
28 | return ViewModelProviders.of(mZhuanlanView, factory).get(ZhuanlanViewModel::class.java)
29 | }
30 |
31 | @FragmentScoped
32 | @Provides
33 | @Named("type")
34 | internal fun provideType(): Int {
35 | val bundle = mZhuanlanView.arguments
36 | var type = Constant.TYPE_PRODUCT
37 | bundle?.run {
38 | type = bundle.getInt(ZhuanlanView.ARGUMENT_TYPE, Constant.TYPE_PRODUCT)
39 | }
40 | return type
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/zhuanlan/ZhuanlanView.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.zhuanlan
2 |
3 | import android.arch.lifecycle.Observer
4 | import android.content.SharedPreferences
5 | import android.os.Bundle
6 | import android.support.design.widget.Snackbar
7 | import android.support.v4.widget.SwipeRefreshLayout
8 | import android.support.v7.widget.LinearLayoutManager
9 | import android.util.TypedValue
10 | import android.view.View
11 | import com.meiji.daily.App
12 | import com.meiji.daily.R
13 | import com.meiji.daily.bean.ZhuanlanBean
14 | import com.meiji.daily.binder.ZhuanlanViewBinder
15 | import com.meiji.daily.module.base.BaseFragment
16 | import com.meiji.daily.util.RecyclerViewUtil
17 | import com.meiji.daily.util.SettingHelper
18 | import kotlinx.android.synthetic.main.fragment_zhuanlan.*
19 | import kotlinx.android.synthetic.main.fragment_zhuanlan.view.*
20 | import kotlinx.android.synthetic.main.item_zhuanlan.view.*
21 | import me.drakeet.multitype.MultiTypeAdapter
22 | import javax.inject.Inject
23 |
24 |
25 | /**
26 | * Created by Meiji on 2017/11/29.
27 | */
28 |
29 | class ZhuanlanView : BaseFragment(), SwipeRefreshLayout.OnRefreshListener, SharedPreferences.OnSharedPreferenceChangeListener {
30 |
31 | @Inject
32 | lateinit var mModel: ZhuanlanViewModel
33 | @Inject
34 | lateinit var mSettingHelper: SettingHelper
35 | @Inject
36 | lateinit var mSharedPreferences: SharedPreferences
37 |
38 | private var mAdapter: MultiTypeAdapter? = null
39 | @Suppress("deprecation")
40 | private fun refreshUI() {
41 | val theme = activity!!.theme
42 | val rootViewBackground = TypedValue()
43 | val itemViewBackground = TypedValue()
44 | val textColorPrimary = TypedValue()
45 | theme.resolveAttribute(R.attr.rootViewBackground, rootViewBackground, true)
46 | theme.resolveAttribute(R.attr.itemViewBackground, itemViewBackground, true)
47 | theme.resolveAttribute(R.attr.textColorPrimary, textColorPrimary, true)
48 | ll_root!!.setBackgroundResource(rootViewBackground.resourceId)
49 |
50 | val resources = resources
51 | val childCount = recycler_view!!.childCount
52 | for (i in 0 until childCount) {
53 | val cardView = recycler_view!!.getChildAt(i).cardview
54 | cardView.setBackgroundResource(itemViewBackground.resourceId)
55 |
56 | cardView.tv_name.setTextColor(resources.getColor(textColorPrimary.resourceId))
57 | cardView.tv_followersCount.setTextColor(resources.getColor(textColorPrimary.resourceId))
58 | cardView.tv_postsCount.setTextColor(resources.getColor(textColorPrimary.resourceId))
59 | cardView.tv_intro.setTextColor(resources.getColor(textColorPrimary.resourceId))
60 | }
61 | RecyclerViewUtil.invalidateCacheItem(recycler_view)
62 | }
63 |
64 | override fun initInject() {
65 | DaggerZhuanlanComponent.builder()
66 | .appComponent(App.sAppComponent)
67 | .zhuanlanModule(ZhuanlanModule(this))
68 | .build().inject(this)
69 | }
70 |
71 | override fun attachLayoutId() = R.layout.fragment_zhuanlan
72 |
73 | override fun initViews(view: View) {
74 | view.recycler_view.setHasFixedSize(true)
75 | view.recycler_view.layoutManager = LinearLayoutManager(activity)
76 | view.refresh_layout.setColorSchemeColors(mSettingHelper.color)
77 | view.refresh_layout.setOnRefreshListener(this)
78 | }
79 |
80 | override fun onStop() {
81 | super.onStop()
82 | mSharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
83 | }
84 |
85 | override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
86 | refresh_layout.setColorSchemeColors(mSettingHelper.color)
87 | }
88 |
89 | override fun subscribeUI() {
90 | mModel.mList.observe(this, Observer> { list ->
91 | if (null != list && list.size > 0) {
92 | onSetAdapter(list)
93 | } else {
94 | onShowNetError()
95 | }
96 | })
97 | mModel.isLoading.observe(this, Observer { aBoolean ->
98 | if (aBoolean!!) {
99 | onShowLoading()
100 | } else {
101 | onHideLoading()
102 | }
103 | })
104 | mModel.isRefreshUI.observe(this, Observer { refreshUI() })
105 | mSharedPreferences.registerOnSharedPreferenceChangeListener(this)
106 | }
107 |
108 | private fun onSetAdapter(list: List) {
109 | if (mAdapter == null) {
110 | mAdapter = MultiTypeAdapter(list)
111 | mAdapter?.register(ZhuanlanBean::class.java, ZhuanlanViewBinder())
112 | recycler_view.adapter = mAdapter
113 | } else {
114 | mAdapter?.notifyDataSetChanged()
115 | }
116 | }
117 |
118 | override fun onRefresh() {
119 | mModel.handleData()
120 | }
121 |
122 | private fun onShowLoading() {
123 | refresh_layout.isRefreshing = true
124 | recycler_view.visibility = View.GONE
125 | }
126 |
127 | private fun onHideLoading() {
128 | refresh_layout.isRefreshing = false
129 | recycler_view.visibility = View.VISIBLE
130 | }
131 |
132 | private fun onShowNetError() {
133 | Snackbar.make(refresh_layout, R.string.network_error, Snackbar.LENGTH_SHORT).show()
134 | refresh_layout.isEnabled = true
135 | }
136 |
137 | companion object {
138 |
139 | internal val ARGUMENT_TYPE = "ARGUMENT_TYPE"
140 | internal val TAG = "ZhuanlanView"
141 |
142 | fun newInstance(type: Int): ZhuanlanView {
143 | val args = Bundle()
144 | args.putInt(ARGUMENT_TYPE, type)
145 | val fragment = ZhuanlanView()
146 | fragment.arguments = args
147 | return fragment
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/module/zhuanlan/ZhuanlanViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.module.zhuanlan
2 |
3 | import android.app.Application
4 | import android.arch.lifecycle.AndroidViewModel
5 | import android.arch.lifecycle.MutableLiveData
6 | import android.arch.lifecycle.ViewModel
7 | import android.arch.lifecycle.ViewModelProvider
8 | import com.meiji.daily.Constant
9 | import com.meiji.daily.R
10 | import com.meiji.daily.bean.ZhuanlanBean
11 | import com.meiji.daily.data.local.AppDatabase
12 | import com.meiji.daily.data.remote.IApi
13 | import com.meiji.daily.io
14 | import com.meiji.daily.mainThread
15 | import com.meiji.daily.util.ErrorAction
16 | import com.meiji.daily.util.RxBusHelper
17 | import io.reactivex.Flowable
18 | import io.reactivex.Maybe
19 | import io.reactivex.disposables.CompositeDisposable
20 | import io.reactivex.functions.Consumer
21 | import retrofit2.Retrofit
22 | import java.util.*
23 |
24 | /**
25 | * Created by Meiji on 2017/11/29.
26 | */
27 |
28 | class ZhuanlanViewModel
29 | constructor(application: Application,
30 | private val mType: Int,
31 | private val mAppDatabase: AppDatabase,
32 | private val mRetrofit: Retrofit,
33 | private val mRxBusHelper: RxBusHelper) : AndroidViewModel(application) {
34 |
35 | private val mDisposable: CompositeDisposable
36 | private lateinit var mIdArr: Array
37 | private lateinit var mRxBus: Flowable
38 | var isLoading: MutableLiveData
39 | private set
40 | var isRefreshUI: MutableLiveData
41 | private set
42 | var mList: MutableLiveData>
43 | private set
44 |
45 | init {
46 | isLoading = MutableLiveData()
47 | isRefreshUI = MutableLiveData()
48 | mList = MutableLiveData()
49 | mDisposable = CompositeDisposable()
50 |
51 | isLoading.value = true
52 | isRefreshUI.value = true
53 | }
54 |
55 | init {
56 | handleData()
57 | subscribeTheme()
58 | }
59 |
60 | private fun subscribeTheme() {
61 | mRxBus = mRxBusHelper.register(Constant.RxBusEvent.REFRESHUI)
62 | mRxBus.subscribe(Consumer {
63 | isRefreshUI.setValue(!(isRefreshUI.value)!!)
64 | }, ErrorAction.error()).let { mDisposable.add(it) }
65 | }
66 |
67 | internal fun handleData() {
68 | mAppDatabase.zhuanlanDao().query(mType)
69 | .flatMap {
70 | if (it.size > 0) {
71 | return@flatMap Maybe.just(it)
72 | } else {
73 | val l = retrofitRequest()
74 | return@flatMap Maybe.just(l)
75 | }
76 | }
77 | .subscribeOn(io)
78 | .observeOn(mainThread)
79 | .subscribe(Consumer> { list ->
80 | mList.value = list
81 | isLoading.setValue(false)
82 | }, object : ErrorAction() {
83 | override fun doAction() {
84 | mList.value = null
85 | isLoading.value = false
86 | }
87 | }.action()).let { mDisposable.add(it) }
88 | }
89 |
90 | override fun onCleared() {
91 | mRxBusHelper.unregister(Constant.RxBusEvent.REFRESHUI, mRxBus)
92 | mDisposable.clear()
93 | super.onCleared()
94 | }
95 |
96 | private fun retrofitRequest(): List {
97 |
98 | val resources = getApplication().resources
99 |
100 | when (mType) {
101 | Constant.TYPE_PRODUCT -> mIdArr = resources.getStringArray(R.array.product)
102 | Constant.TYPE_MUSIC -> mIdArr = resources.getStringArray(R.array.music)
103 | Constant.TYPE_LIFE -> mIdArr = resources.getStringArray(R.array.life)
104 | Constant.TYPE_EMOTION -> mIdArr = resources.getStringArray(R.array.emotion)
105 | Constant.TYPE_FINANCE -> mIdArr = resources.getStringArray(R.array.profession)
106 | Constant.TYPE_ZHIHU -> mIdArr = resources.getStringArray(R.array.zhihu)
107 | }
108 |
109 | val list = ArrayList()
110 | val api = mRetrofit.create(IApi::class.java)
111 |
112 | val maybeList = mIdArr.map { api.getZhuanlanBean(it) }
113 |
114 | Maybe.merge(maybeList)
115 | .doOnComplete { mAppDatabase.zhuanlanDao().insert(list) }
116 | .subscribe(Consumer { bean ->
117 | if (bean != null) {
118 | bean.type = mType
119 | list.add(bean)
120 | }
121 | }, ErrorAction.error()).let { mDisposable.add(it) }
122 |
123 | return list
124 | }
125 |
126 | class Factory(private val mApplication: Application,
127 | private val mType: Int,
128 | private val mAppDatabase: AppDatabase,
129 | private val mRetrofit: Retrofit,
130 | private val mRxBusHelper: RxBusHelper) : ViewModelProvider.NewInstanceFactory() {
131 |
132 | override fun create(modelClass: Class): T {
133 | return ZhuanlanViewModel(mApplication, mType, mAppDatabase, mRetrofit, mRxBusHelper) as T
134 | }
135 | }
136 |
137 | companion object {
138 |
139 | internal val TAG = "ZhuanlanViewModel"
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/util/DiffCallback.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.util
2 |
3 | import android.support.v7.util.DiffUtil
4 |
5 | import me.drakeet.multitype.Items
6 | import me.drakeet.multitype.MultiTypeAdapter
7 |
8 | /**
9 | * Created by Meiji on 2018/1/25.
10 | */
11 |
12 | class DiffCallback private constructor(private val oldItems: Items?, private val newItems: Items?) : DiffUtil.Callback() {
13 |
14 | override fun getOldListSize(): Int {
15 | return oldItems?.size ?: 0
16 | }
17 |
18 | override fun getNewListSize(): Int {
19 | return newItems?.size ?: 0
20 | }
21 |
22 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
23 | return oldItems!![oldItemPosition] == newItems!![newItemPosition]
24 | }
25 |
26 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
27 | return oldItems!![oldItemPosition].hashCode() == newItems!![newItemPosition].hashCode()
28 | }
29 |
30 | companion object {
31 |
32 | fun create(oldItems: Items, newItems: Items, adapter: MultiTypeAdapter) {
33 | val diffCallback = DiffCallback(oldItems, newItems)
34 | val result = DiffUtil.calculateDiff(diffCallback, true)
35 | result.dispatchUpdatesTo(adapter)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/util/ErrorAction.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.util
2 |
3 | import com.crashlytics.android.Crashlytics
4 | import com.meiji.daily.BuildConfig
5 |
6 | import io.reactivex.annotations.NonNull
7 | import io.reactivex.functions.Consumer
8 |
9 | /**
10 | * Created by Meiji on 2018/1/25.
11 | */
12 |
13 | abstract class ErrorAction {
14 |
15 | fun action(): Consumer {
16 | return Consumer { throwable ->
17 | print(throwable)
18 | doAction()
19 | }
20 | }
21 |
22 | abstract fun doAction()
23 |
24 | companion object {
25 |
26 | fun error(): Consumer {
27 | return Consumer { throwable -> print(throwable) }
28 | }
29 |
30 | fun print(@NonNull throwable: Throwable) {
31 | if (BuildConfig.DEBUG) {
32 | throwable.printStackTrace()
33 | } else {
34 | Crashlytics.logException(throwable)
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/util/JsonUtil.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.util
2 |
3 | import org.json.JSONArray
4 | import org.json.JSONException
5 | import org.json.JSONObject
6 |
7 | /**
8 | * Created by Meiji on 2018/2/6.
9 | */
10 |
11 | object JsonUtil {
12 |
13 | private const val JSON_INDENT = 2
14 |
15 | /**
16 | * @param jsonStr string param expect a json string
17 | * @return formatted json string if param is a json string,otherwise return the param
18 | */
19 | @Throws(JSONException::class)
20 | fun convert(jsonStr: String): String {
21 | val json = jsonStr.trim()
22 | return when {
23 | json.startsWith("{") && json.endsWith("}") -> {
24 | JSONObject(json).toString(JSON_INDENT)
25 | }
26 |
27 | json.startsWith("[") && json.endsWith("]") -> {
28 | JSONArray(json).toString(JSON_INDENT)
29 | }
30 |
31 | else -> json
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/util/NetWorkUtil.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.util
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 |
6 | /**
7 | * Created by Meiji on 2017/5/2.
8 | */
9 |
10 | object NetWorkUtil {
11 |
12 | /**
13 | * 判断是否有网络连接
14 | */
15 | fun isNetworkConnected(context: Context): Boolean {
16 | // 获取手机所有连接管理对象(包括对wi-fi,net等连接的管理)
17 | val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
18 | // 获取NetworkInfo对象
19 | val networkInfo = manager.activeNetworkInfo
20 | //判断NetworkInfo对象是否为空
21 | return networkInfo?.isAvailable ?: false
22 | }
23 |
24 | /**
25 | * 判断WIFI网络是否可用
26 | */
27 | fun isWifiConnected(context: Context): Boolean {
28 | // 获取手机所有连接管理对象(包括对wi-fi,net等连接的管理)
29 | val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
30 | // 获取NetworkInfo对象
31 | val networkInfo = manager.activeNetworkInfo
32 | //判断NetworkInfo对象是否为空 并且类型是否为WIFI
33 | return networkInfo?.isAvailable ?: false && networkInfo?.type == ConnectivityManager.TYPE_WIFI
34 | }
35 |
36 | /**
37 | * 判断MOBILE网络是否可用
38 | */
39 | fun isMobileConnected(context: Context): Boolean {
40 | //获取手机所有连接管理对象(包括对wi-fi,net等连接的管理)
41 | val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
42 | //获取NetworkInfo对象
43 | val networkInfo = manager.activeNetworkInfo
44 | //判断NetworkInfo对象是否为空 并且类型是否为MOBILE
45 | return networkInfo?.isAvailable ?: false && networkInfo?.type == ConnectivityManager.TYPE_MOBILE
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/util/OnLoadMoreListener.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.util
2 |
3 | import android.support.v7.widget.LinearLayoutManager
4 | import android.support.v7.widget.RecyclerView
5 | import android.util.Log
6 |
7 | /**
8 | * Created by Meiji on 2017/6/8.
9 | */
10 |
11 | abstract class OnLoadMoreListener : RecyclerView.OnScrollListener() {
12 |
13 | private var layoutManager: LinearLayoutManager? = null
14 | private var itemCount: Int = 0
15 | private var lastPosition: Int = 0
16 | private var lastItemCount: Int = 0
17 |
18 | abstract fun onLoadMore()
19 |
20 | override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
21 | if (recyclerView!!.layoutManager is LinearLayoutManager) {
22 | layoutManager = recyclerView.layoutManager as LinearLayoutManager
23 |
24 | itemCount = layoutManager!!.itemCount
25 | lastPosition = layoutManager!!.findLastCompletelyVisibleItemPosition()
26 | } else {
27 | Log.e("OnLoadMoreListener", "The OnLoadMoreListener only support LinearLayoutManager")
28 | return
29 | }
30 |
31 | if (lastItemCount != itemCount && lastPosition == itemCount - 1) {
32 | lastItemCount = itemCount
33 | this.onLoadMore()
34 | }
35 | }
36 |
37 | // @Override
38 | // public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
39 | // super.onScrollStateChanged(recyclerView, newState);
40 | // if (recyclerView.getLayoutManager() instanceof LinearLayoutManager) {
41 | // if (newState == RecyclerView.SCROLL_STATE_IDLE) {
42 | // if (!recyclerView.canScrollVertically(1)) {
43 | // this.doAction();
44 | // }
45 | // }
46 | // }
47 | // }
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/util/RecyclerViewUtil.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.util
2 |
3 | import android.support.v7.widget.RecyclerView
4 |
5 | /**
6 | * Created by Meiji on 2017/7/13.
7 | */
8 |
9 | object RecyclerViewUtil {
10 |
11 | /**
12 | * 使RecyclerView缓存中在pool中的Item失效
13 | */
14 | fun invalidateCacheItem(recyclerView: RecyclerView) {
15 | val recyclerViewClass = RecyclerView::class.java
16 | try {
17 | val declaredField = recyclerViewClass.getDeclaredField("mRecycler")
18 | declaredField.isAccessible = true
19 | val declaredMethod = Class.forName(RecyclerView.Recycler::class.java.name)
20 | .getDeclaredMethod("clear", *arrayOfNulls>(0) as Array>)
21 | declaredMethod.isAccessible = true
22 | declaredMethod.invoke(declaredField.get(recyclerView))
23 | val recycledViewPool = recyclerView.recycledViewPool
24 | recycledViewPool.clear()
25 | } catch (e: Exception) {
26 | e.printStackTrace()
27 | }
28 |
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/util/RetrofitFactory.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.util
2 |
3 | import com.franmontiel.persistentcookiejar.PersistentCookieJar
4 | import com.franmontiel.persistentcookiejar.cache.SetCookieCache
5 | import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
6 | import com.meiji.daily.App
7 | import com.meiji.daily.BuildConfig
8 | import com.meiji.daily.SdkManager
9 | import com.meiji.daily.data.remote.IApi
10 | import okhttp3.Cache
11 | import okhttp3.CacheControl
12 | import okhttp3.Interceptor
13 | import okhttp3.OkHttpClient
14 | import retrofit2.Retrofit
15 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
16 | import retrofit2.converter.moshi.MoshiConverterFactory
17 | import java.io.File
18 | import java.util.concurrent.TimeUnit
19 |
20 | /**
21 | * Created by Meiji on 2017/4/22.
22 | */
23 | @Suppress("deprecation")
24 | @Deprecated("")
25 | class RetrofitFactory private constructor() {
26 |
27 | companion object {
28 | val instance: Retrofit by lazy { RetrofitFactory().init() }
29 | }
30 |
31 | /**
32 | * 缓存机制
33 | * 在响应请求之后在 data/data/<包名>/cache 下建立一个response 文件夹,保持缓存数据。
34 | * 这样我们就可以在请求的时候,如果判断到没有网络,自动读取缓存的数据。
35 | * 同样这也可以实现,在我们没有网络的情况下,重新打开App可以浏览的之前显示过的内容。
36 | * 也就是:判断网络,有网络,则从网络获取,并保存到缓存中,无网络,则从缓存中获取。
37 | * https://werb.github.io/2016/07/29/%E4%BD%BF%E7%94%A8Retrofit2+OkHttp3%E5%AE%9E%E7%8E%B0%E7%BC%93%E5%AD%98%E5%A4%84%E7%90%86/
38 | 包名> */
39 | private val cacheControlInterceptor = Interceptor { chain ->
40 | var request = chain.request()
41 | if (!NetWorkUtil.isNetworkConnected(App.sAppContext)) {
42 | request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build()
43 | }
44 |
45 | val originalResponse = chain.proceed(request)
46 | if (NetWorkUtil.isNetworkConnected(App.sAppContext)) {
47 | // 有网络时 设置缓存为默认值
48 | val cacheControl = request.cacheControl().toString()
49 | originalResponse.newBuilder()
50 | .header("Cache-Control", cacheControl)
51 | .removeHeader("Pragma") // 清除头信息,因为服务器如果不支持,会返回一些干扰信息,不清除下面无法生效
52 | .build()
53 | } else {
54 | // 无网络时 设置超时为1周
55 | val maxStale = 60 * 60 * 24 * 7
56 | originalResponse.newBuilder()
57 | .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
58 | .removeHeader("Pragma")
59 | .build()
60 | }
61 | }
62 |
63 | fun init(): Retrofit {
64 | // 指定缓存路径,缓存大小 50Mb
65 | val cache = Cache(File(App.sAppContext.cacheDir, "HttpCache"),
66 | (1024 * 1024 * 50).toLong())
67 | // Cookie 持久化
68 | val cookieJar = PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(App.sAppContext))
69 |
70 | var builder: OkHttpClient.Builder = OkHttpClient.Builder()
71 | .cookieJar(cookieJar)
72 | .cache(cache)
73 | .addInterceptor(cacheControlInterceptor)
74 | .connectTimeout(10, TimeUnit.SECONDS)
75 | .readTimeout(15, TimeUnit.SECONDS)
76 | .writeTimeout(15, TimeUnit.SECONDS)
77 | .retryOnConnectionFailure(true)
78 |
79 | // Log 拦截器
80 | if (BuildConfig.DEBUG) {
81 | builder = SdkManager.initInterceptor(builder)
82 | }
83 |
84 | return Retrofit.Builder()
85 | .baseUrl(IApi.API_BASE)
86 | .client(builder.build())
87 | .addConverterFactory(MoshiConverterFactory.create())
88 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
89 | .build()
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/util/RetrofitHelper.kt:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.util
2 |
3 | import android.content.Context
4 | import com.franmontiel.persistentcookiejar.PersistentCookieJar
5 | import com.franmontiel.persistentcookiejar.cache.SetCookieCache
6 | import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
7 | import com.meiji.daily.BuildConfig
8 | import com.meiji.daily.SdkManager
9 | import com.meiji.daily.data.remote.IApi
10 | import okhttp3.Cache
11 | import okhttp3.CacheControl
12 | import okhttp3.Interceptor
13 | import okhttp3.OkHttpClient
14 | import retrofit2.Retrofit
15 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
16 | import retrofit2.converter.moshi.MoshiConverterFactory
17 | import java.io.File
18 | import java.util.concurrent.TimeUnit
19 | import javax.inject.Inject
20 | import javax.inject.Singleton
21 |
22 | /**
23 | * Created by Meiji on 2017/12/28.
24 | */
25 | @Singleton
26 | class RetrofitHelper
27 | @Inject
28 | constructor(private val mContext: Context) {
29 | /**
30 | * 缓存机制
31 | * 在响应请求之后在 data/data/<包名>/cache 下建立一个response 文件夹,保持缓存数据。
32 | * 这样我们就可以在请求的时候,如果判断到没有网络,自动读取缓存的数据。
33 | * 同样这也可以实现,在我们没有网络的情况下,重新打开App可以浏览的之前显示过的内容。
34 | * 也就是:判断网络,有网络,则从网络获取,并保存到缓存中,无网络,则从缓存中获取。
35 | * https://werb.github.io/2016/07/29/%E4%BD%BF%E7%94%A8Retrofit2+OkHttp3%E5%AE%9E%E7%8E%B0%E7%BC%93%E5%AD%98%E5%A4%84%E7%90%86/
36 | 包名> */
37 | private val cacheControlInterceptor = Interceptor { chain ->
38 | var request = chain.request()
39 | if (!NetWorkUtil.isNetworkConnected(mContext)) {
40 | request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build()
41 | }
42 |
43 | val originalResponse = chain.proceed(request)
44 | if (NetWorkUtil.isNetworkConnected(mContext)) {
45 | // 有网络时 设置缓存为默认值
46 | val cacheControl = request.cacheControl().toString()
47 | originalResponse.newBuilder()
48 | .header("Cache-Control", cacheControl)
49 | .removeHeader("Pragma") // 清除头信息,因为服务器如果不支持,会返回一些干扰信息,不清除下面无法生效
50 | .build()
51 | } else {
52 | // 无网络时 设置超时为1周
53 | val maxStale = 60 * 60 * 24 * 7
54 | originalResponse.newBuilder()
55 | .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
56 | .removeHeader("Pragma")
57 | .build()
58 | }
59 | }
60 |
61 | // 指定缓存路径,缓存大小 50Mb
62 | // Cookie 持久化
63 | // Log 拦截器
64 | val retrofit: Retrofit
65 | get() {
66 | val cache = Cache(File(mContext.cacheDir, "HttpCache"),
67 | (1024 * 1024 * 50).toLong())
68 | val cookieJar = PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(mContext))
69 |
70 | var builder: OkHttpClient.Builder = OkHttpClient.Builder()
71 | .cookieJar(cookieJar)
72 | .cache(cache)
73 | .addInterceptor(cacheControlInterceptor)
74 | .connectTimeout(10, TimeUnit.SECONDS)
75 | .readTimeout(15, TimeUnit.SECONDS)
76 | .writeTimeout(15, TimeUnit.SECONDS)
77 | .retryOnConnectionFailure(true)
78 | if (BuildConfig.DEBUG) {
79 | builder = SdkManager.initInterceptor(builder)
80 | }
81 |
82 | return Retrofit.Builder()
83 | .baseUrl(IApi.API_BASE)
84 | .client(builder.build())
85 | .addConverterFactory(MoshiConverterFactory.create())
86 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
87 | .build()
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/src/main/java/com/meiji/daily/util/RxBus.java:
--------------------------------------------------------------------------------
1 | package com.meiji.daily.util;
2 |
3 | import android.support.annotation.NonNull;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 | import java.util.concurrent.ConcurrentHashMap;
8 |
9 | import io.reactivex.Flowable;
10 | import io.reactivex.processors.PublishProcessor;
11 |
12 | /**
13 | * Created by Meiji on 2017/12/14.
14 | * 参考 :https://juejin.im/entry/58ff2e26a0bb9f0065d2c5f2
15 | */
16 | @SuppressWarnings("all")
17 | @Deprecated
18 | public class RxBus {
19 |
20 | private ConcurrentHashMap