@@ -3,42 +3,48 @@ package com.steamclock.steamock
33import android.os.Bundle
44import androidx.activity.ComponentActivity
55import androidx.activity.compose.setContent
6- import androidx.compose.animation.AnimatedVisibility
7- import androidx.compose.animation.core.tween
8- import androidx.compose.animation.fadeIn
9- import androidx.compose.animation.fadeOut
10- import androidx.compose.foundation.clickable
6+ import androidx.compose.foundation.background
7+ import androidx.compose.foundation.border
8+ import androidx.compose.foundation.layout.Box
119import androidx.compose.foundation.layout.Column
1210import androidx.compose.foundation.layout.Row
13- import androidx.compose.foundation.layout.Spacer
11+ import androidx.compose.foundation.layout.fillMaxHeight
1412import androidx.compose.foundation.layout.fillMaxSize
1513import androidx.compose.foundation.layout.fillMaxWidth
16- import androidx.compose.foundation.layout.height
1714import androidx.compose.foundation.layout.padding
15+ import androidx.compose.foundation.layout.wrapContentHeight
1816import androidx.compose.foundation.layout.wrapContentSize
19- import androidx.compose.foundation.lazy.LazyColumn
20- import androidx.compose.material.*
21- import androidx.compose.material.icons.Icons
22- import androidx.compose.material.icons.filled.KeyboardArrowDown
23- import androidx.compose.material.icons.filled.KeyboardArrowUp
17+ import androidx.compose.foundation.rememberScrollState
18+ import androidx.compose.foundation.verticalScroll
19+ import androidx.compose.material.AlertDialog
20+ import androidx.compose.material.Button
21+ import androidx.compose.material.DropdownMenuItem
22+ import androidx.compose.material.ExperimentalMaterialApi
23+ import androidx.compose.material.ExposedDropdownMenuBox
24+ import androidx.compose.material.ExposedDropdownMenuDefaults
25+ import androidx.compose.material.MaterialTheme
26+ import androidx.compose.material.OutlinedTextField
27+ import androidx.compose.material.Surface
28+ import androidx.compose.material.Text
29+ import androidx.compose.material.TextButton
2430import androidx.compose.runtime.Composable
2531import androidx.compose.runtime.collectAsState
26- import androidx.compose.ui.Modifier
27- import androidx.compose.ui.unit.dp
28- import androidx.lifecycle.lifecycleScope
29- import com.steamclock.steamock.lib.PostmanMockConfig
30- import com.steamclock.steamock.lib.repo.PostmanMockRepo
31- import kotlinx.coroutines.launch
3232import androidx.compose.runtime.getValue
3333import androidx.compose.runtime.mutableStateOf
3434import androidx.compose.runtime.remember
3535import androidx.compose.runtime.rememberCoroutineScope
3636import androidx.compose.runtime.setValue
3737import androidx.compose.ui.Alignment
38- import androidx.compose.ui.unit.sp
38+ import androidx.compose.ui.Modifier
39+ import androidx.compose.ui.graphics.Color
40+ import androidx.compose.ui.unit.dp
41+ import androidx.compose.ui.window.Dialog
42+ import com.steamclock.steamock.lib.PostmanMockConfig
3943import com.steamclock.steamock.lib.repo.MockState
40- import com.steamclock.steamock.lib.ui.ContentLoadViewState
44+ import com.steamclock.steamock.lib.repo.MockedAPI
45+ import com.steamclock.steamock.lib.repo.PostmanMockRepo
4146import com.steamclock.steamock.lib.ui.AvailableMocks
47+ import kotlinx.coroutines.launch
4248
4349class MainActivity : ComponentActivity () {
4450
@@ -80,95 +86,137 @@ class MainActivity : ComponentActivity() {
8086 super .onCreate(savedInstanceState)
8187
8288 setContent {
83- val coroutineScope = rememberCoroutineScope()
84- val mockCollectionState by postmanRepo.mockCollectionState.collectAsState()
8589 val exampleAPIResponse by exampleApiRepo.apiResponse.collectAsState()
86- var exampleAPIUrl by remember { mutableStateOf( BuildConfig .exampleDefaultUrl) }
87-
90+ val mockedAPIs by postmanRepo.mockedAPIs.collectAsState()
91+ val coroutineScope = rememberCoroutineScope()
8892 var openAlertDialog by remember { mutableStateOf(true ) }
89- var showingStubs by remember { mutableStateOf(false ) }
90- var showingExample by remember { mutableStateOf(false ) }
9193
92- val stateText = when (val immutableState = mockCollectionState) {
93- is ContentLoadViewState .Error -> immutableState.throwable.localizedMessage
94- ContentLoadViewState .Loading -> " Fetching available Postman mocks..."
95- else -> null
94+ if (openAlertDialog) {
95+ WelcomeDialog { openAlertDialog = false }
96+ }
97+
98+ if (exampleAPIResponse.isNotEmpty()) {
99+ ResponseDialog (
100+ text = exampleAPIResponse,
101+ onDismiss = { exampleApiRepo.clearLastResponse() }
102+ )
96103 }
97104
98105 // A surface container using the 'background' color from the theme
99106 Surface (
100107 modifier = Modifier .fillMaxSize(),
101108 color = MaterialTheme .colors.background
102109 ) {
103- LazyColumn {
104- when (mockCollectionState) {
105- is ContentLoadViewState .Error ,
106- is ContentLoadViewState .Loading -> {
107- stateText?.let {
108- item {
109- Text (modifier = Modifier .padding(16 .dp), text = stateText)
110- Spacer (modifier = Modifier .height(16 .dp))
111- }
112- }
113- }
114- is ContentLoadViewState .Success -> {
115- // List all mocks
116- item {
117- CollapsableContent (
118- title = " Available Postman Mocks" ,
119- isExpanded = showingStubs,
120- onRowClicked = { showingStubs = ! showingStubs },
121- content = { AvailableMocks (mockRepo = postmanRepo) }
122- )
123- }
124-
125- // Setup intercept requests
126- item {
127- Spacer (modifier = Modifier .height(16 .dp))
128-
129- CollapsableContent (
130- title = " Intercept Requests" ,
131- isExpanded = showingExample,
132- onRowClicked = { showingExample = ! showingExample },
133- content = {
134- Column (
135- modifier = Modifier .padding(16 .dp)
136- ) {
137- OutlinedTextField (
138- value = exampleAPIUrl,
139- onValueChange = { exampleAPIUrl = it }
140- )
141-
142- Button (
143- onClick = {
144- coroutineScope.launch {
145- exampleApiRepo.makeRequest(exampleAPIUrl)
146- }
147- }
148- ) {
149- Text (text = " Send Request" )
150- }
151-
152- Text (
153- text = exampleAPIResponse
154- )
155- }
156- }
157- )
158- }
110+ Column {
111+ // Input to allow us to test the interception
112+ RequestSimulator (mockedAPIs) { url ->
113+ coroutineScope.launch {
114+ exampleApiRepo.makeRequest(url)
159115 }
160116 }
161117
162- }
163-
164- if (openAlertDialog) {
165- WelcomeDialog { openAlertDialog = false }
118+ // Available mocks takes up the rest of the page
119+ Box (modifier = Modifier
120+ .fillMaxWidth()
121+ .weight(1f )
122+ .border(4 .dp, MaterialTheme .colors.primary)) {
123+ AvailableMocks (
124+ mockRepo = postmanRepo
125+ )
126+ }
166127 }
167128 }
168129 }
130+ }
131+ }
132+
133+ /* *
134+ * Shows a dropdown list of all the available API/endpoints in the Postman collection
135+ * that have at least one mock setup on them. Allows a user to select an API, and then
136+ * press a button to simulate making a request to that URL.
137+ */
138+ @Composable
139+ private fun RequestSimulator (
140+ mockedAPIs : List <MockedAPI >,
141+ onSendRequest : (String ) -> Unit
142+ ) {
143+ var selectedAPIUrl by remember { mutableStateOf(" " ) }
144+
145+ // Input to allow us to test the interception
146+ Column (
147+ modifier = Modifier
148+ .padding(16 .dp)
149+ .wrapContentSize()
150+ ) {
151+ Text (
152+ text = " Request Simulator" ,
153+ style = MaterialTheme .typography.h6
154+ )
155+
156+ MockedAPIDropdownMenu (mockedAPIs) {
157+ it.url?.let { url -> selectedAPIUrl = url }
158+ }
159+
160+ Text (
161+ modifier = Modifier .padding(vertical = 8 .dp),
162+ text = selectedAPIUrl,
163+ style = MaterialTheme .typography.caption
164+ )
165+
166+ Button (
167+ onClick = { onSendRequest(selectedAPIUrl) }
168+ ) {
169+ Text (text = " Send Request" )
170+ }
171+ }
172+ }
173+
174+ @OptIn(ExperimentalMaterialApi ::class ) // ExposedDropdownMenuBox
175+ @Composable
176+ private fun MockedAPIDropdownMenu (
177+ options : List <MockedAPI >,
178+ onSelected : (MockedAPI ) -> Unit
179+ ) {
180+ var expanded by remember { mutableStateOf(false ) }
181+ var selectedOption by remember { mutableStateOf(" Select an endpoint" ) }
182+
183+ Column {
184+ ExposedDropdownMenuBox (
185+ expanded = expanded,
186+ onExpandedChange = {
187+ expanded = ! expanded
188+ }
189+ ) {
190+ OutlinedTextField (
191+ value = selectedOption,
192+ onValueChange = { },
193+ modifier = Modifier
194+ .fillMaxWidth()
195+ .exposedDropdownSize(true ),
196+ label = { Text (" Endpoint" ) },
197+ readOnly = true ,
198+ trailingIcon = {
199+ ExposedDropdownMenuDefaults .TrailingIcon (
200+ expanded = expanded
201+ )
202+ },
203+ colors = ExposedDropdownMenuDefaults .textFieldColors()
204+ )
169205
170- lifecycleScope.launch {
171- postmanRepo.requestCollectionUpdate()
206+ ExposedDropdownMenu (
207+ expanded = expanded,
208+ onDismissRequest = { expanded = false }
209+ ) {
210+ options.forEach { option ->
211+ DropdownMenuItem (onClick = {
212+ selectedOption = option.name
213+ onSelected(option)
214+ expanded = false
215+ }) {
216+ Text (text = option.name)
217+ }
218+ }
219+ }
172220 }
173221 }
174222}
@@ -185,8 +233,8 @@ private fun WelcomeDialog(
185233 Text (
186234 text = " This app is meant to demonstrate how to use Postman to mock APIs in your Android app." +
187235 " To get started, you will need to update the local properties for the sample app with your mocking environment setup.\n\n " +
188- " Once setup, you can view and enable all mocks in the \" Available Postman Mocks \" section below." +
189- " The \" Intercept Requests \" section can be used to test how calls are intercepted and mocks are returned." ,
236+ " Once setup, you can view and enable all mocks in the \" Mocks available for... \" section below." +
237+ " The \" Request Simulator \" section can be used to test how calls are intercepted and mocks are returned." ,
190238 modifier = Modifier .padding(16 .dp)
191239 )
192240 },
@@ -201,39 +249,40 @@ private fun WelcomeDialog(
201249 )
202250}
203251
252+ /* *
253+ * Shows the response from the API call in a dialog.
254+ */
204255@Composable
205- private fun CollapsableContent (
206- title : String ,
207- isExpanded : Boolean ,
208- onRowClicked : () -> Unit ,
209- content : @Composable () -> Unit
256+ private fun ResponseDialog (
257+ text : String ,
258+ onDismiss : () -> Unit
210259) {
211- Divider ()
212-
213- Row (
214- modifier = Modifier
215- .fillMaxWidth()
216- .clickable { onRowClicked() },
217- verticalAlignment = Alignment .CenterVertically
218- ) {
219- Text (
220- modifier = Modifier .weight(1f ).padding(8 .dp),
221- fontSize = 24 .sp,
222- text = title,
223- )
224- Icon (
225- modifier = Modifier .wrapContentSize().padding(8 .dp),
226- imageVector = if (isExpanded) Icons .Default .KeyboardArrowUp else Icons .Default .KeyboardArrowDown ,
227- contentDescription = null
228- )
229- }
230- Divider ()
260+ Dialog (
261+ onDismissRequest = { onDismiss() },
262+ content = {
263+ Column (
264+ modifier = Modifier .fillMaxHeight(0.7f ).fillMaxWidth().background(Color .White )
265+ ) {
266+ Box (
267+ modifier = Modifier .weight(1f ).verticalScroll(rememberScrollState())
268+ ) {
269+ Text (
270+ text = text,
271+ modifier = Modifier .padding(16 .dp)
272+ )
273+ }
231274
232- AnimatedVisibility (
233- visible = isExpanded,
234- enter = fadeIn(animationSpec = tween(200 )),
235- exit = fadeOut(animationSpec = tween(200 ))
236- ) {
237- content()
238- }
275+ Row (
276+ modifier = Modifier
277+ .wrapContentHeight()
278+ .align(Alignment .End )
279+ .padding(horizontal = 8 .dp, vertical = 2 .dp)
280+ ) {
281+ TextButton (onClick = { onDismiss() }) {
282+ Text (" Ok" )
283+ }
284+ }
285+ }
286+ }
287+ )
239288}
0 commit comments