Build a Compose Rating Card
Company: OpenAI
Role: Android Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Quick Answer: This question evaluates proficiency in Android UI development with Kotlin and Jetpack Compose, emphasizing custom composable design (a stateless star-rating component), MVVM-style state management via a ViewModel, form validation, and handling asynchronous submission and success states.
Part 1: Simulate a ViewModel-Driven Rating Card
Constraints
- 0 <= len(events) <= 10000
- Ratings are intended to be integers from 1 to 5
- Comment length is at most 1000 characters per event
- All event names are strings; unknown event names should be ignored
Examples
Input: ([],)
Expected Output: ['0', '', 'idle', 'false', '']
Explanation: With no events, no rating or comment exists, so submit is disabled.
Input: ([('rate', '5'), ('comment', 'Great app'), ('submit',)],)
Expected Output: ['5', 'Great app', 'success', 'false', 'Thanks for rating 5 stars!']
Explanation: Both required fields are present, so submit succeeds and the card enters success state.
Input: ([('comment', ' '), ('rate', '3'), ('submit',)],)
Expected Output: ['3', ' ', 'idle', 'false', '']
Explanation: Whitespace-only comments do not count as non-empty.
Input: ([('rate', '6'), ('comment', 'ok'), ('submit',)],)
Expected Output: ['0', 'ok', 'idle', 'false', '']
Explanation: Rating 6 is invalid and ignored, so submission is blocked.
Input: ([('rate', '2'), ('comment', 'ok'), ('submit',), ('comment', 'updated')],)
Expected Output: ['2', 'updated', 'idle', 'true', '']
Explanation: Editing after success resets the card to idle, and the new valid state can be submitted.
Hints
- Keep only one source of truth: rating, comment, and status. Derive canSubmit instead of storing it.
- Treat submit as a state transition that only happens when the current derived validation passes.
Part 2: Restore Rating Card State After Rotation and Process Death
Constraints
- 0 <= len(actions) <= 10000
- Saved rating is always an integer from 0 to 5
- Comment length is at most 1000 characters per event
- Process death restores only rating and comment, not transient success status
Examples
Input: ([],)
Expected Output: ['0', '', 'idle', 'false']
Explanation: Initial state has no saved input.
Input: ([('rate', '4'), ('comment', 'nice'), ('rotate',)],)
Expected Output: ['4', 'nice', 'idle', 'true']
Explanation: Rotation does not lose ViewModel state.
Input: ([('rate', '5'), ('comment', 'Done'), ('submit',), ('process_death',)],)
Expected Output: ['5', 'Done', 'idle', 'true']
Explanation: Rating and comment are restored, but success status is transient and resets to idle.
Input: ([('rate', '3'), ('comment', ' '), ('process_death',)],)
Expected Output: ['3', ' ', 'idle', 'false']
Explanation: Whitespace comments survive, but still do not satisfy validation.
Input: ([('rate', '2'), ('comment', 'ok'), ('process_death',), ('rate', '9'), ('process_death',)],)
Expected Output: ['2', 'ok', 'idle', 'true']
Explanation: Invalid rating 9 is ignored and does not overwrite the saved rating.
Hints
- Think of two copies of state: the live ViewModel state and the persisted saved-state bundle.
- On process death, rebuild live state from the saved bundle and reset transient submission fields.
Part 3: Run Deterministic Tests for the Rating ViewModel and Star Bar
Constraints
- 0 <= number of scenarios <= 1000
- 0 <= total steps across all scenarios <= 10000
- Ratings/taps outside 1 to 5 are ignored
- Each scenario starts from rating 0, empty comment, and idle status
Examples
Input: ([],)
Expected Output: []
Explanation: No scenarios means no assertions can fail.
Input: ([[('expect', 'canSubmit', 'false'), ('tap', '4'), ('expect', 'selectedStars', '11110'), ('comment', 'Good'), ('expect', 'canSubmit', 'true'), ('submit',), ('expect', 'status', 'success'), ('expect', 'canSubmit', 'false')]],)
Expected Output: []
Explanation: All assertions match the expected state transitions and star rendering.
Input: ([[('tap', '3'), ('expect', 'canSubmit', 'true')]],)
Expected Output: [0]
Explanation: A rating alone is not enough; the comment is missing.
Input: ([[('comment', 'ok'), ('expect', 'canSubmit', 'false')], [('tap', '6'), ('expect', 'rating', '0'), ('tap', '2'), ('expect', 'selectedStars', '10000')]],)
Expected Output: [2]
Explanation: The third assertion fails because rating 2 renders '11000', not '10000'.
Input: ([[('comment', ' '), ('tap', '1'), ('submit',), ('expect', 'status', 'idle'), ('expect', 'canSubmit', 'false')]],)
Expected Output: []
Explanation: Whitespace-only comment blocks submission, so status remains idle.
Hints
- Separate action execution from assertion evaluation.
- For selectedStars, star i is selected exactly when i <= current rating.
Part 4: Convert Tap and Drag Positions to Half-Star Ratings
Constraints
- 0 <= max_stars <= 100000
- 1 <= star_width <= 100000
- 0 <= gap <= 100000
- 0 <= len(coordinates) <= 100000
- Coordinates may be negative or beyond the end of the bar
Examples
Input: (5, 20, 4, [-5, 0, 9, 10, 19, 20, 23, 24, 34, 200])
Expected Output: [0.0, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0, 1.5, 2.0, 5.0]
Explanation: Coordinates in the first gap keep rating 1.0; coordinate 24 starts the second star.
Input: (5, 20, 0, [0, 10, 20, 30, 99, 100])
Expected Output: [0.5, 1.0, 1.5, 2.0, 5.0, 5.0]
Explanation: With no gaps, every 20 pixels begins a new star except the final boundary, which is full max rating.
Input: (5, 20, 4, [])
Expected Output: []
Explanation: No interactions produce no ratings.
Input: (0, 20, 4, [-1, 0, 100])
Expected Output: [0.0, 0.0, 0.0]
Explanation: A bar with zero stars cannot select a positive rating.
Input: (3, 10, 5, [9, 10, 14, 15])
Expected Output: [1.0, 1.0, 1.0, 1.5]
Explanation: Coordinates 10 through 14 are in the gap after the first star.
Hints
- Each star plus following gap forms a repeated cell of size star_width + gap, except there is no gap after the last star.
- First clamp positions before the bar and beyond the total width, then handle star area versus gap area.
Part 5: Simulate Network Submission With Retry and Double-Submit Prevention
Constraints
- 0 <= len(events) <= 10000
- Valid ratings are integers from 1 to 5
- Comment length is at most 1000 characters per event
- While submitting, rating/comment edits and extra submit events are ignored
Examples
Input: ([('submit',), ('rate', '5'), ('submit',)],)
Expected Output: ['5', '', 'idle', 'false', '0', 'none', '']
Explanation: Both submits are invalid because the comment is missing.
Input: ([('rate', '4'), ('comment', 'ok'), ('submit',), ('submit',), ('resolve', '1', 'success')],)
Expected Output: ['4', 'ok', 'success', 'false', '1', 'none', '']
Explanation: The second submit during loading is ignored, so only one request starts.
Input: ([('rate', '3'), ('comment', 'ok'), ('submit',), ('resolve', '1', 'error'), ('submit',), ('resolve', '2', 'success')],)
Expected Output: ['3', 'ok', 'success', 'false', '2', 'none', '']
Explanation: After an error, retry starts request 2 and it succeeds.
Input: ([('rate', '3'), ('comment', 'ok'), ('submit',), ('resolve', '2', 'error')],)
Expected Output: ['3', 'ok', 'submitting', 'false', '1', '1', '']
Explanation: Resolution for request 2 is stale because only request 1 exists, so it is ignored.
Input: ([('rate', '1'), ('comment', 'a'), ('submit',), ('comment', 'b'), ('resolve', '1', 'error')],)
Expected Output: ['1', 'a', 'error', 'true', '1', 'none', 'Submission failed']
Explanation: Edits while submitting are ignored. The failed valid request can be retried.
Hints
- Model submission status as idle, submitting, success, or error rather than multiple loose booleans.
- Store the current pending request id so stale network completions cannot change state.
Part 6: Render a Reusable Configurable Rating Component
Constraints
- 0 <= max_stars <= 100000
- rating may be below 0 or above max_stars and must be clamped
- step is either 1.0 or 0.5
- symbols contains only string keys and string values
Examples
Input: (5, 3, 1.0, True, {'full': 'X', 'half': '/', 'empty': '-'})
Expected Output: ['X|true|1|Rate 1 of 5', 'X|true|2|Rate 2 of 5', 'X|true|3|Rate 3 of 5', '-|false|4|Rate 4 of 5', '-|false|5|Rate 5 of 5']
Explanation: Full-star mode ignores fractional visuals and renders three selected stars.
Input: (5, 3.5, 0.5, True, {'full': 'X', 'half': '/', 'empty': '-'})
Expected Output: ['X|true|1|Rate 1 of 5', 'X|true|2|Rate 2 of 5', 'X|true|3|Rate 3 of 5', '/|true|4|Rate 4 of 5', '-|false|5|Rate 5 of 5']
Explanation: Half-star mode renders the fourth star as half-selected.
Input: (1, 0, 1.0, False, {'full': 'F', 'empty': 'E'})
Expected Output: ['E|false|none|empty star 1 of 1']
Explanation: Disabled/read-only mode has no click value and uses a read-only label.
Input: (3, 10, 0.5, True, {'full': 'F', 'half': 'H', 'empty': 'E'})
Expected Output: ['F|true|1|Rate 1 of 3', 'F|true|2|Rate 2 of 3', 'F|true|3|Rate 3 of 3']
Explanation: Rating is clamped to max_stars.
Input: (0, 2, 1.0, True, {'full': 'F', 'empty': 'E'})
Expected Output: []
Explanation: A component configured with zero stars renders nothing.
Hints
- Make rendering a pure function of configuration and current rating.
- For half-star mode, star i is half-filled when rating is at least i - 0.5 but less than i.