Skip to content

Commit

Permalink
[Jetsurvey] Simplification of survey screens (#1058)
Browse files Browse the repository at this point in the history
This eliminates the survey repository and related data models in favor
of simplifying the code base. Each of the different question types has a
generic composable such as MultipleChoiceQuestion, which takes all of
the relevant strings/resources to generate the UI. The actual survey
data is baked into a specific composable in Survey.kt, such as
FreeTimeQuestion, instead of coming from a data model.

The SurveyViewModel is now much more explicit with methods for each
question such as onFreeTimeResponse. In a production app, you'd likely
want this to be more flexible to handle server-side questions, but this
code pattern makes the sample easier to skim. Every response is
maintained in SurveyViewModel as a separate state. This increases
verbosity in favor of better readability.

Note: This does not animate between the final survey question and the
survey results. When the work to move the app to Compose Navigation is
done, the results will be a separate destination that is navigated to
and animated that way.

This fixes #1054.
  • Loading branch information
IanGClifton authored Jan 4, 2023
2 parents 20e4e51 + 124745c commit 203a397
Show file tree
Hide file tree
Showing 20 changed files with 943 additions and 1,199 deletions.
1 change: 0 additions & 1 deletion Jetsurvey/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ dependencies {
implementation(libs.androidx.compose.material.iconsExtended)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.compose.runtime.livedata)
debugImplementation(libs.androidx.compose.ui.tooling)

implementation(libs.accompanist.permissions)
Expand Down
3 changes: 1 addition & 2 deletions Jetsurvey/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@

<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name">
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2023 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
*
* https://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.compose.jetsurvey.survey

import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.example.compose.jetsurvey.theme.slightlyDeemphasizedAlpha
import com.example.compose.jetsurvey.theme.stronglyDeemphasizedAlpha

/**
* A scrollable container with the question's title, direction, and dynamic content.
*
* @param titleResourceId String resource to use for the question's title
* @param modifier Modifier to apply to the entire wrapper
* @param directionsResourceId String resource to use for the question's directions; the direction
* UI will be omitted if null is passed
* @param content Composable to display after the title and option directions
*/
@Composable
fun QuestionWrapper(
@StringRes titleResourceId: Int,
modifier: Modifier = Modifier,
@StringRes directionsResourceId: Int? = null,
content: @Composable () -> Unit,
) {
Column(
modifier = modifier
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState())
) {
Spacer(Modifier.height(32.dp))
QuestionTitle(titleResourceId)
directionsResourceId?.let {
Spacer(Modifier.height(18.dp))
QuestionDirections(it)
}
Spacer(Modifier.height(18.dp))

content()
}
}

@Composable
private fun QuestionTitle(
@StringRes title: Int,
modifier: Modifier = Modifier,
) {
Text(
text = stringResource(id = title),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = slightlyDeemphasizedAlpha),
modifier = modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.inverseOnSurface,
shape = MaterialTheme.shapes.small
)
.padding(vertical = 24.dp, horizontal = 16.dp)
)
}

@Composable
private fun QuestionDirections(
@StringRes directionsResourceId: Int,
modifier: Modifier = Modifier,
) {
Text(
text = stringResource(id = directionsResourceId),
color = MaterialTheme.colorScheme.onSurface
.copy(alpha = stronglyDeemphasizedAlpha),
style = MaterialTheme.typography.bodySmall,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,83 +17,102 @@
package com.example.compose.jetsurvey.survey

import android.net.Uri
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.example.compose.jetsurvey.R
import com.example.compose.jetsurvey.survey.question.DateQuestion
import com.example.compose.jetsurvey.survey.question.MultipleChoiceQuestion
import com.example.compose.jetsurvey.survey.question.PhotoQuestion
import com.example.compose.jetsurvey.survey.question.SingleChoiceQuestion
import com.example.compose.jetsurvey.survey.question.SliderQuestion
import com.example.compose.jetsurvey.survey.question.Superhero

data class SurveyResult(
val library: String,
@StringRes val result: Int,
@StringRes val description: Int
)

data class Survey(
@StringRes val title: Int,
val questions: List<Question>
)

data class Question(
val id: Int,
@StringRes val questionText: Int,
val answer: PossibleAnswer,
@StringRes val description: Int? = null,
val permissionsRequired: List<String> = emptyList(),
@StringRes val permissionsRationaleText: Int? = null
)

/**
* Type of supported actions for a survey
*/
enum class SurveyActionType { PICK_DATE, TAKE_PHOTO, SELECT_CONTACT }

sealed class SurveyActionResult {
data class Date(val dateMillis: Long) : SurveyActionResult()
data class Photo(val uri: Uri) : SurveyActionResult()
data class Contact(val contact: String) : SurveyActionResult()
@Composable
fun FreeTimeQuestion(
selectedAnswers: List<Int>,
onOptionSelected: (selected: Boolean, answer: Int) -> Unit,
modifier: Modifier = Modifier,
) {
MultipleChoiceQuestion(
titleResourceId = R.string.in_my_free_time,
directionsResourceId = R.string.select_all,
possibleAnswers = listOf(
R.string.read,
R.string.work_out,
R.string.draw,
R.string.play_games,
R.string.dance,
R.string.watch_movies,
),
selectedAnswers = selectedAnswers,
onOptionSelected = onOptionSelected,
modifier = modifier,
)
}

data class AnswerOption(@StringRes val textRes: Int, @DrawableRes val iconRes: Int? = null)

sealed class PossibleAnswer {
data class SingleChoice(val options: List<AnswerOption>) : PossibleAnswer()
data class MultipleChoice(val options: List<AnswerOption>) : PossibleAnswer()
data class Action(
@StringRes val label: Int,
val actionType: SurveyActionType
) : PossibleAnswer()

data class Slider(
val range: ClosedFloatingPointRange<Float>,
val steps: Int,
@StringRes val startText: Int,
@StringRes val endText: Int,
@StringRes val neutralText: Int,
val defaultValue: Float = 5.5f
) : PossibleAnswer()
@Composable
fun SuperheroQuestion(
selectedAnswer: Superhero?,
onOptionSelected: (Superhero) -> Unit,
modifier: Modifier = Modifier,
) {
SingleChoiceQuestion(
titleResourceId = R.string.pick_superhero,
directionsResourceId = R.string.select_one,
possibleAnswers = listOf(
Superhero(R.string.spark, R.drawable.spark),
Superhero(R.string.lenz, R.drawable.lenz),
Superhero(R.string.bugchaos, R.drawable.bug_of_chaos),
Superhero(R.string.frag, R.drawable.frag),
),
selectedAnswer = selectedAnswer,
onOptionSelected = onOptionSelected,
modifier = modifier,
)
}

sealed class Answer<T : PossibleAnswer> {
object PermissionsDenied : Answer<Nothing>()
data class SingleChoice(@StringRes val answer: Int) : Answer<PossibleAnswer.SingleChoice>()
data class MultipleChoice(val answersStringRes: Set<Int>) :
Answer<PossibleAnswer.MultipleChoice>()
@Composable
fun TakeawayQuestion(
dateInMillis: Long?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
DateQuestion(
titleResourceId = R.string.takeaway,
directionsResourceId = R.string.select_date,
dateInMillis = dateInMillis,
onClick = onClick,
modifier = modifier,
)
}

data class Action(val result: SurveyActionResult) : Answer<PossibleAnswer.Action>()
data class Slider(val answerValue: Float) : Answer<PossibleAnswer.Slider>()
@Composable
fun FeelingAboutSelfiesQuestion(
value: Float?,
onValueChange: (Float) -> Unit,
modifier: Modifier = Modifier,
) {
SliderQuestion(
titleResourceId = R.string.selfies,
value = value,
onValueChange = onValueChange,
startTextResource = R.string.strongly_dislike,
neutralTextResource = R.string.neutral,
endTextResource = R.string.strongly_like,
modifier = modifier,
)
}

/**
* Add or remove an answer from the list of selected answers depending on whether the answer was
* selected or deselected.
*/
fun Answer.MultipleChoice.withAnswerSelected(
@StringRes answer: Int,
selected: Boolean
): Answer.MultipleChoice {
val newStringRes = answersStringRes.toMutableSet()
if (!selected) {
newStringRes.remove(answer)
} else {
newStringRes.add(answer)
}
return Answer.MultipleChoice(newStringRes)
@Composable
fun TakeSelfieQuestion(
imageUri: Uri?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
PhotoQuestion(
titleResourceId = R.string.selfie_skills,
imageUri = imageUri,
onClick = onClick,
modifier = modifier,
)
}
Loading

0 comments on commit 203a397

Please sign in to comment.