/
ArticleScreen.kt
262 lines (247 loc) · 8.74 KB
/
ArticleScreen.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.jetnews.ui.article
import android.content.Context
import android.content.Intent
import androidx.compose.foundation.Icon
import androidx.compose.foundation.Text
import androidx.compose.foundation.contentColor
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.material.AlertDialog
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.Share
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.savedinstancestate.savedInstanceState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ContextAmbient
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.viewModel
import androidx.ui.tooling.preview.Preview
import com.example.jetnews.R
import com.example.jetnews.data.Result
import com.example.jetnews.data.posts.PostsRepository
import com.example.jetnews.data.posts.impl.BlockingFakePostsRepository
import com.example.jetnews.data.posts.impl.post3
import com.example.jetnews.model.Post
import com.example.jetnews.ui.ThemedPreview
import com.example.jetnews.ui.home.BookmarkButton
import com.example.jetnews.ui.state.UiState
import kotlinx.coroutines.runBlocking
/**
* Stateful Article Screen that uses [ArticleViewModel] to manage its state.
*
* @param postId (state) the post to show
* @param postsRepository data source for [ArticleViewModel]
* @param onBack (event) request back navigation
*/
@Composable
fun ArticleScreen(
postId: String,
postsRepository: PostsRepository,
onBack: () -> Unit
) {
// viewModel() is scoped to the Application or Fragment Lifecycle that is displaying this
// composable by default. Callers of this composable can modify this by providing a new scope
// through [ViewModelStoreOwnerAmbient]. Navigation controller is expected to scope ViewModel in
// this manner.
val articleViewModel =
viewModel<ArticleViewModel>(factory = ArticleViewModelFactory(postId, postsRepository))
// [observeAsState] will automatically observe a LiveData<T> and return a State<T> object that
// updates whenever the LiveData emits a value. observation of the LiveData will stop when
// [observeAsState] is removed from the composition tree
val post by articleViewModel.post.observeAsState(UiState())
// TODO: handle errors when repository gains ability to cause them
val postData = post.data ?: return
// [collectAsState] will automatically collect a Flow<T> and return a State<T> object that
// updates whenever the Flow emits a value. Collection is cancelled when [collectAsState] is
// removed from the composition tree.
val favorites by articleViewModel.favorites.collectAsState(setOf())
val isFavorite = favorites.contains(postId)
ArticleScreen(
post = postData,
onBack = onBack,
isFavorite = isFavorite,
onToggleFavorite = articleViewModel::toggleFavorite
)
}
/**
* Stateless Article Screen that displays a single post.
*
* @param post (state) item to display
* @param onBack (event) request navigate back
* @param isFavorite (state) is this item currently a favorite
* @param onToggleFavorite (event) request that this post toggle it's favorite state
*/
@Composable
fun ArticleScreen(
post: Post,
onBack: () -> Unit,
isFavorite: Boolean,
onToggleFavorite: () -> Unit
) {
var showDialog by savedInstanceState { false }
if (showDialog) {
FunctionalityNotAvailablePopup { showDialog = false }
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = "Published in: ${post.publication?.name}",
style = MaterialTheme.typography.subtitle2,
color = contentColor()
)
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.Filled.ArrowBack)
}
}
)
},
bodyContent = { innerPadding ->
val modifier = Modifier.padding(innerPadding)
PostContent(post, modifier)
},
bottomBar = {
BottomBar(
post = post,
onUnimplementedAction = { showDialog = true },
isFavorite = isFavorite,
onToggleFavorite = onToggleFavorite
)
}
)
}
/**
* Bottom bar for Article screen
*
* @param post (state) used in share sheet to share the post
* @param onUnimplementedAction (event) called when the user performs an unimplemented action
* @param isFavorite (state) if this post is currently a favorite
* @param onToggleFavorite (event) request this post toggle it's favorite status
*/
@Composable
private fun BottomBar(
post: Post,
onUnimplementedAction: () -> Unit,
isFavorite: Boolean,
onToggleFavorite: () -> Unit
) {
Surface(elevation = 2.dp) {
Row(
verticalGravity = Alignment.CenterVertically,
modifier = Modifier
.preferredHeight(56.dp)
.fillMaxWidth()
) {
IconButton(onClick = onUnimplementedAction) {
Icon(Icons.Filled.FavoriteBorder)
}
BookmarkButton(
isBookmarked = isFavorite,
onClick = onToggleFavorite
)
val context = ContextAmbient.current
IconButton(onClick = { sharePost(post, context) }) {
Icon(Icons.Filled.Share)
}
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onUnimplementedAction) {
Icon(vectorResource(R.drawable.ic_text_settings))
}
}
}
}
/**
* Display a popup explaining functionality not available.
*
* @param onDismiss (event) request the popup be dismissed
*/
@Composable
private fun FunctionalityNotAvailablePopup(onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
text = {
Text(
text = "Functionality not available \uD83D\uDE48",
style = MaterialTheme.typography.body2
)
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text(text = "CLOSE")
}
}
)
}
/**
* Show a share sheet for a post
*
* @param post to share
* @param context Android context to show the share sheet in
*/
private fun sharePost(post: Post, context: Context) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, post.title)
putExtra(Intent.EXTRA_TEXT, post.url)
}
context.startActivity(Intent.createChooser(intent, "Share post"))
}
@Preview("Article screen")
@Composable
fun PreviewArticle() {
ThemedPreview {
val post = loadFakePost(post3.id)
ArticleScreen(post, {}, false, {})
}
}
@Preview("Article screen dark theme")
@Composable
fun PreviewArticleDark() {
ThemedPreview(darkTheme = true) {
val post = loadFakePost(post3.id)
ArticleScreen(post, {}, false, {})
}
}
@Composable
private fun loadFakePost(postId: String): Post {
val context = ContextAmbient.current
val post = runBlocking {
(BlockingFakePostsRepository(context).getPost(postId) as Result.Success).data
}
return post
}