Skip to content

Instantly share code, notes, and snippets.

@BlakeBarrett
Created August 21, 2023 00:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BlakeBarrett/58ce9d62e510ee726b035143e4941d7e to your computer and use it in GitHub Desktop.
Save BlakeBarrett/58ce9d62e510ee726b035143e4941d7e to your computer and use it in GitHub Desktop.
JSON USER DATA EXAMPLE: Fake Users JSON array generated by ChatGPT
[
{
"id": 1,
"first_name": "Alice",
"last_name": "Smith",
"email": "alice@example.com",
"age": 28,
"last_login_date": "2023-08-09T10:30:00",
"friends_user_ids": [2, 3, 5, 7],
"favorite_animal": "Dog",
"least_favorite_smell": "Durian",
"hobbies": ["Painting", "Hiking"],
"birthplace": "Cityville",
"favorite_color": "Purple",
"unread_messages": 3,
"has_pets": true,
"shoe_size": 7.5,
"coffee_preference": "Black",
"dream_destination": "Bora Bora",
"music_genre": "Indie Rock",
"is_vegan": false,
"fitness_level": "Intermediate",
"lucky_number": 17
},
{
"id": 2,
"first_name": "Bob",
"last_name": "Johnson",
"email": "bob@example.com",
"age": 35,
"last_login_date": "2023-08-08T15:20:00",
"friends_user_ids": [1, 4, 8],
"favorite_animal": "Cat",
"least_favorite_smell": "Sulfur",
"hobbies": ["Reading", "Gardening"],
"birthplace": "Villageville",
"favorite_color": "Blue",
"unread_messages": 0,
"has_pets": true,
"shoe_size": 9.0,
"coffee_preference": "Latte",
"dream_destination": "Paris",
"music_genre": "Classical",
"is_vegan": true,
"fitness_level": "Beginner",
"lucky_number": 5
},
{
"id": 3,
"first_name": "Charlie",
"last_name": "Brown",
"email": "charlie@example.com",
"age": 22,
"last_login_date": "2023-08-09T08:45:00",
"friends_user_ids": [1, 5, 9],
"favorite_animal": "Elephant",
"least_favorite_smell": "Fish",
"hobbies": ["Cooking", "Playing Guitar"],
"birthplace": "Townsville",
"favorite_color": "Green",
"unread_messages": 10,
"has_pets": false,
"shoe_size": 8.5,
"coffee_preference": "Cappuccino",
"dream_destination": "Tokyo",
"music_genre": "Pop",
"is_vegan": false,
"fitness_level": "Advanced",
"lucky_number": 12
},
{
"id": 4,
"first_name": "David",
"last_name": "Lee",
"email": "david@example.com",
"age": 31,
"last_login_date": "2023-08-07T18:10:00",
"friends_user_ids": [2, 8, 10],
"favorite_animal": "Lion",
"least_favorite_smell": "Garlic",
"hobbies": ["Soccer", "Photography"],
"birthplace": "Countryville",
"favorite_color": "Yellow",
"unread_messages": 5,
"has_pets": true,
"shoe_size": 10.5,
"coffee_preference": "Espresso",
"dream_destination": "New York",
"music_genre": "Rock",
"is_vegan": false,
"fitness_level": "Intermediate",
"lucky_number": 9
},
{
"id": 5,
"first_name": "Eve",
"last_name": "Williams",
"email": "eve@example.com",
"age": 27,
"last_login_date": "2023-08-09T12:05:00",
"friends_user_ids": [1, 3, 9, 11],
"favorite_animal": "Dolphin",
"least_favorite_smell": "Onion",
"hobbies": ["Yoga", "Traveling"],
"birthplace": "Seaville",
"favorite_color": "Turquoise",
"unread_messages": 8,
"has_pets": true,
"shoe_size": 8.0,
"coffee_preference": "Mocha",
"dream_destination": "Maldives",
"music_genre": "Electronic",
"is_vegan": true,
"fitness_level": "Advanced",
"lucky_number": 3
},
{
"id": 6,
"first_name": "Frank",
"last_name": "Davis",
"email": "frank@example.com",
"age": 45,
"last_login_date": "2023-08-08T09:40:00",
"friends_user_ids": [12, 13, 14],
"favorite_animal": "Giraffe",
"least_favorite_smell": "Vinegar",
"hobbies": ["Golf", "Painting"],
"birthplace": "Mountainville",
"favorite_color": "Orange",
"unread_messages": 2,
"has_pets": false,
"shoe_size": 11.0,
"coffee_preference": "Americano",
"dream_destination": "Switzerland",
"music_genre": "Jazz",
"is_vegan": false,
"fitness_level": "Beginner",
"lucky_number": 8
},
{
"id": 7,
"first_name": "Grace",
"last_name": "Martinez",
"email": "grace@example.com",
"age": 29,
"last_login_date": "2023-08-09T16:15:00",
"friends_user_ids": [1, 15, 16],
"favorite_animal": "Penguin",
"least_favorite_smell": "Cabbage",
"hobbies": ["Singing", "Cooking"],
"birthplace": "Snowville",
"favorite_color": "White",
"unread_messages": 1,
"has_pets": true,
"shoe_size": 6.5,
"coffee_preference": "Latte",
"dream_destination": "Hawaii",
"music_genre": "Reggae",
"is_vegan": true,
"fitness_level": "Intermediate",
"lucky_number": 13
},
{
"id": 8,
"first_name": "Henry",
"last_name": "Taylor",
"email": "henry@example.com",
"age": 32,
"last_login_date": "2023-08-08T13:30:00",
"friends_user_ids": [2, 4, 9, 17],
"favorite_animal": "Tiger",
"least_favorite_smell": "Mold",
"hobbies": ["Running", "Chess"],
"birthplace": "Desertville",
"favorite_color": "Red",
"unread_messages": 12,
"has_pets": true,
"shoe_size": 10.0,
"coffee_preference": "Cappuccino",
"dream_destination": "Australia",
"music_genre": "Country",
"is_vegan": false,
"fitness_level": "Advanced",
"lucky_number": 21
},
{
"id": 9,
"first_name": "Ivy",
"last_name": "Anderson",
"email": "ivy@example.com",
"age": 24,
"last_login_date": "2023-08-09T07:55:00",
"friends_user_ids": [3, 5, 8, 18],
"favorite_animal": "Kangaroo",
"least_favorite_smell": "Smoked Fish",
"hobbies": ["Dancing", "Biking"],
"birthplace": "Jungleville",
"favorite_color": "Yellow",
"unread_messages": 6,
"has_pets": true,
"shoe_size": 8.0,
"coffee_preference": "Espresso",
"dream_destination": "Brazil",
"music_genre": "Hip Hop",
"is_vegan": true,
"fitness_level": "Beginner",
"lucky_number": 6
},
{
"id": 10,
"first_name": "Jack",
"last_name": "Moore",
"email": "jack@example.com",
"age": 30,
"last_login_date": "2023-08-07T21:20:00",
"friends_user_ids": [4, 7, 9, 19],
"favorite_animal": "Horse",
"least_favorite_smell": "Burnt Rubber",
"hobbies": ["Photography", "Travelling"],
"birthplace": "Farmville",
"favorite_color": "Brown",
"unread_messages": 9,
"has_pets": true,
"shoe_size": 10.5,
"coffee_preference": "Black",
"dream_destination": "New Zealand",
"music_genre": "Alternative",
"is_vegan": false,
"fitness_level": "Intermediate",
"lucky_number": 11
},
{
"id": 11,
"first_name": "Karen",
"last_name": "Jackson",
"email": "karen@example.com",
"age": 26,
"last_login_date": "2023-08-09T14:50:00",
"friends_user_ids": [5, 10, 20],
"favorite_animal": "Panda",
"least_favorite_smell": "Rotting Fruit",
"hobbies": ["Yoga", "Reading"],
"birthplace": "Cityville",
"favorite_color": "Pink",
"unread_messages": 4,
"has_pets": false,
"shoe_size": 7.0,
"coffee_preference": "Mocha",
"dream_destination": "Thailand",
"music_genre": "R&B",
"is_vegan": true,
"fitness_level": "Advanced",
"lucky_number": 15
},
{
"id": 12,
"first_name": "Liam",
"last_name": "Garcia",
"email": "liam@example.com",
"age": 21,
"last_login_date": "2023-08-08T08:00:00",
"friends_user_ids": [6, 13, 15],
"favorite_animal": "Sloth",
"least_favorite_smell": "Dirty Socks",
"hobbies": ["Gaming", "Cooking"],
"birthplace": "Villageville",
"favorite_color": "Black",
"unread_messages": 7,
"has_pets": true,
"shoe_size": 9.5,
"coffee_preference": "Latte",
"dream_destination": "Greece",
"music_genre": "Metal",
"is_vegan": false,
"fitness_level": "Beginner",
"lucky_number": 14
},
{
"id": 13,
"first_name": "Mia",
"last_name": "Rodriguez",
"email": "mia@example.com",
"age": 33,
"last_login_date": "2023-08-09T11:40:00",
"friends_user_ids": [6, 12, 14, 16],
"favorite_animal": "Owl",
"least_favorite_smell": "Stale Beer",
"hobbies": ["Singing", "Painting"],
"birthplace": "Countryville",
"favorite_color": "Blue",
"unread_messages": 5,
"has_pets": true,
"shoe_size": 8.5,
"coffee_preference": "Cappuccino",
"dream_destination": "Italy",
"music_genre": "Pop",
"is_vegan": true,
"fitness_level": "Intermediate",
"lucky_number": 8
},
{
"id": 14,
"first_name": "Noah",
"last_name": "Martinez",
"email": "noah@example.com",
"age": 40,
"last_login_date": "2023-08-08T17:55:00",
"friends_user_ids": [6, 13, 15],
"favorite_animal": "Wolf",
"least_favorite_smell": "Mothballs",
"hobbies": ["Running", "Cooking"],
"birthplace": "Cityville",
"favorite_color": "Green",
"unread_messages": 2,
"has_pets": false,
"shoe_size": 10.0,
"coffee_preference": "Espresso",
"dream_destination": "Australia",
"music_genre": "Rock",
"is_vegan": false,
"fitness_level": "Advanced",
"lucky_number": 19
},
{
"id": 15,
"first_name": "Olivia",
"last_name": "Thompson",
"email": "olivia@example.com",
"age": 29,
"last_login_date": "2023-08-09T10:15:00",
"friends_user_ids": [7, 13, 14, 17],
"favorite_animal": "Duck",
"least_favorite_smell": "Wet Dog",
"hobbies": ["Soccer", "Gardening"],
"birthplace": "Townsville",
"favorite_color": "Red",
"unread_messages": 1,
"has_pets": true,
"shoe_size": 7.0,
"coffee_preference": "Black",
"dream_destination": "Japan",
"music_genre": "Electronic",
"is_vegan": true,
"fitness_level": "Intermediate",
"lucky_number": 10
},
{
"id": 16,
"first_name": "Peter",
"last_name": "Wright",
"email": "peter@example.com",
"age": 37,
"last_login_date": "2023-08-08T12:30:00",
"friends_user_ids": [6, 15, 18],
"favorite_animal": "Bear",
"least_favorite_smell": "Gasoline",
"hobbies": ["Reading", "Biking"],
"birthplace": "Seaville",
"favorite_color": "Blue",
"unread_messages": 0,
"has_pets": false,
"shoe_size": 9.5,
"coffee_preference": "Mocha",
"dream_destination": "New Zealand",
"music_genre": "Indie Rock",
"is_vegan": false,
"fitness_level": "Advanced",
"lucky_number": 7
},
{
"id": 17,
"first_name": "Quinn",
"last_name": "Hernandez",
"email": "quinn@example.com",
"age": 25,
"last_login_date": "2023-08-09T09:05:00",
"friends_user_ids": [8, 16, 19],
"favorite_animal": "Fox",
"least_favorite_smell": "Wet Carpet",
"hobbies": ["Painting", "Singing"],
"birthplace": "Mountainville",
"favorite_color": "Orange",
"unread_messages": 4,
"has_pets": true,
"shoe_size": 8.0,
"coffee_preference": "Latte",
"dream_destination": "Greece",
"music_genre": "Pop",
"is_vegan": true,
"fitness_level": "Beginner",
"lucky_number": 22
},
{
"id": 18,
"first_name": "Ryan",
"last_name": "Adams",
"email": "ryan@example.com",
"age": 32,
"last_login_date": "2023-08-08T14:25:00",
"friends_user_ids": [9, 15, 20],
"favorite_animal": "Gorilla",
"least_favorite_smell": "Burnt Popcorn",
"hobbies": ["Gaming", "Hiking"],
"birthplace": "Villageville",
"favorite_color": "Green",
"unread_messages": 6,
"has_pets": true,
"shoe_size": 10.5,
"coffee_preference": "Black",
"dream_destination": "Hawaii",
"music_genre": "Electronic",
"is_vegan": false,
"fitness_level": "Intermediate",
"lucky_number": 18
},
{
"id": 19,
"first_name": "Sophia",
"last_name": "Gonzalez",
"email": "sophia@example.com",
"age": 28,
"last_login_date": "2023-08-09T13:00:00",
"friends_user_ids": [10, 17, 20],
"favorite_animal": "Llama",
"least_favorite_smell": "Public Restrooms",
"hobbies": ["Reading", "Dancing"],
"birthplace": "Cityville",
"favorite_color": "Purple",
"unread_messages": 2,
"has_pets": true,
"shoe_size": 8.0,
"coffee_preference": "Latte",
"dream_destination": "Paris",
"music_genre": "Pop",
"is_vegan": true,
"fitness_level": "Advanced",
"lucky_number": 4
},
{
"id": 20,
"first_name": "Thomas",
"last_name": "Walker",
"email": "thomas@example.com",
"age": 30,
"last_login_date": "2023-08-08T19:45:00",
"friends_user_ids": [11, 19],
"favorite_animal": "Monkey",
"least_favorite_smell": "Sewage",
"hobbies": ["Cooking", "Traveling"],
"birthplace": "Townsville",
"favorite_color": "Blue",
"unread_messages": 7,
"has_pets": false,
"shoe_size": 9.5,
"coffee_preference": "Cappuccino",
"dream_destination": "Maldives",
"music_genre": "Rock",
"is_vegan": false,
"fitness_level": "Intermediate",
"lucky_number": 16
}
]
@BlakeBarrett
Copy link
Author

Sample Kotlin parser, to get you unblocked.
You'll need to copy/paste the sample data above into sampleDataJson: String.

import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

@Serializable
data class User(
    val id: Int,
    val first_name: String,
    val last_name: String,
    val email: String,
    val age: Int,
    val last_login_date: String,
    val friends_user_ids: List<Int>,
    val favorite_animal: String,
    val least_favorite_smell: String,
    val hobbies: List<String>,
    val birthplace: String,
    val favorite_color: String,
    val unread_messages: Int,
    val has_pets: Boolean,
    val shoe_size: Double,
    val coffee_preference: String,
    val dream_destination: String,
    val music_genre: String,
    val is_vegan: Boolean,
    val fitness_level: String,
    val lucky_number: Int
) {
    companion object {
        fun parse(source: String): List<User> {
            return Json.decodeFromString(source)
        }
    }
}

fun main() {
    val sampleDataJson = """[...]""" // Your sample JSON data
    val users = User.parse(sampleDataJson)
    
    for (user in users) {
        println("User ID: ${user.id}")
        println("Name: ${user.first_name} ${user.last_name}")
        // ... print other fields ...
        println("Lucky Number: ${user.lucky_number}")
        println()
    }
}

@BlakeBarrett
Copy link
Author

BlakeBarrett commented Aug 21, 2023

Create a user profile UI using Compose's @composable functions to display information about a selected user.
Your task is to design and implement a user interface that showcases some of the user's details, you don't need to feature them all.
To fulfill the requirements, you need to use at least three different @composable functions, utilize a ViewModel to manage data for this view, ensure data is passed from the top-level down to the components, and make use of the provided user data.

@BlakeBarrett
Copy link
Author

BlakeBarrett commented Aug 21, 2023

A sample response to the prompt:

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

data class UserViewModel(
    val first_name: String,
    val last_name: String,
    val age: Int,
    val email: String,
    val favorite_animal: String,
    val hobbies: List<String>
)

class UserProfileViewModel(
    private val userRepository: List<User> = User.parse(data)
) : ViewModel() {

    fun getUserViewModel(userId: Int): UserViewModel? {
        val user = userRepository.find { it.id == userId }
        return user?.let {
            UserViewModel(
                first_name = it.first_name,
                last_name = it.last_name,
                age = it.age,
                email = it.email,
                favorite_animal = it.favorite_animal,
                hobbies = it.hobbies
            )
        }
    }
}

@Composable
fun UserProfileScreen(userId: Int) {
    val viewModel: UserProfileViewModel = viewModel()
    val userViewModel = remember {
        mutableStateOf(viewModel.getUserViewModel(userId))
    }

    userViewModel.value?.let { user ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
        ) {
            Text(
                text = "User Profile",
                style = MaterialTheme.typography.headlineSmall
            )
            Spacer(modifier = Modifier.height(16.dp))
            UserDetailCard(user)
            Spacer(modifier = Modifier.height(16.dp))
            UserHobbies(user.hobbies)
        }
    } ?: run {
        Text(
            text = "User not found",
            style = MaterialTheme.typography.headlineMedium
        )
    }
}

@Composable
fun UserDetailCard(user: UserViewModel) {
    Card(
        modifier = Modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(
            defaultElevation = 10.dp
        )
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Text(
                text = "${user.first_name} ${user.last_name}",
                style = MaterialTheme.typography.headlineMedium
            )
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = "Age: ${user.age}",
                style = MaterialTheme.typography.bodyMedium
            )
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = "Email: ${user.email}",
                style = MaterialTheme.typography.bodySmall
            )
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = "Favorite Animal: ${user.favorite_animal}",
                style = MaterialTheme.typography.bodySmall
            )
            Spacer(modifier = Modifier.height(8.dp))
            // Other user details...
        }
    }
}

@Composable
fun UserHobbies(hobbies: List<String>) {
    Column(
        modifier = Modifier.semantics { contentDescription = "User's hobbies" }
    ) {
        Text(
            text = "Hobbies",
            style = MaterialTheme.typography.labelSmall
        )
        Spacer(modifier = Modifier.height(8.dp))
        Column {
            hobbies.forEach { hobby ->
                Text(
                    text = "- $hobby",
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }
    }
}

@Composable
fun UserProfileApp() {
    val userId = (1..20).random()
    UserProfileScreen(userId = userId) // Pass the desired user ID
}

@Preview(showBackground = true)
@Composable
fun UserProfileAppPreview() {
    UserProfileApp()
}

Which ends up looking like:

UserProfileAppPreview

@BlakeBarrett
Copy link
Author

MainAcitivy.kt

package com.blakebarrett.myapplication

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Text
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.blakebarrett.myapplication.ui.theme.DIALTheme
import kotlinx.coroutines.launch

/**
 * This app is a simple phone dialer example written in Jetpack Compose.
 * There are three main screens:
 *  1. Dialer screen
 *  2. Call history screen
 *  3. Contact list screen
 */
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            DIALTheme {
//                DialerApp()
                UserProfileApp()
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    DIALTheme {
//        DialerApp()
        UserProfileApp()
    }
}

/**
 * The main app composable.
 * This will either render a Scaffold on phones or a SideNav on tablets.
 */

@Composable
fun DialerApp() {
    var isDialing = rememberSaveable {
        mutableStateOf(false)
    }

    var dialEntry = rememberSaveable {
        mutableStateOf("867-5309")
    }

    DialerAppPhone(
        modifier = Modifier
            .background(MaterialTheme.colorScheme.primaryContainer)
    ) { modifier ->
        Box(modifier = modifier) {
            DialerScreen(
                currentEntry = dialEntry.value,
                stateChange = { updatedEntry ->
                    dialEntry.value = updatedEntry
                },
                dial = { entry ->
                    isDialing.value = true
                }
            )
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DialerAppPhone(
    modifier: Modifier = Modifier,
    content: @Composable (modifier: Modifier) -> Unit = {}
) {
    val coroutineScope = rememberCoroutineScope()
    val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
        bottomSheetState = SheetState(
            initialValue = SheetValue.Hidden,
            skipPartiallyExpanded = true
        )
    )
    BottomSheetScaffold(
        scaffoldState = bottomSheetScaffoldState,
        sheetContent = {
            Row(
                modifier = Modifier
                    .padding(8.dp)
                    .fillMaxWidth()
                    .wrapContentWidth()
            ) {
                Button(
                    onClick = {},
                ) {
                    Text(text = "Dial Pad")
                }
                Button(
                    modifier = Modifier.padding(horizontal = 8.dp),
                    onClick = {},
                ) {
                    Text(text = "Call History")
                }
                Button(
                    onClick = {},
                ) {
                    Text(text = "Contacts")
                }
            }
        },
        sheetPeekHeight = 32.dp,
        sheetShape = MaterialTheme.shapes.extraLarge,
        sheetShadowElevation = 10.dp,
        sheetTonalElevation = 10.dp,
        containerColor = MaterialTheme.colorScheme.primaryContainer,
        modifier = Modifier
            .clickable {
                coroutineScope.launch {
                    if (bottomSheetScaffoldState.bottomSheetState.isVisible) {
                        bottomSheetScaffoldState.bottomSheetState.hide()
                    } else {
                        bottomSheetScaffoldState.bottomSheetState.expand()
                    }
                }
            }
            .background(MaterialTheme.colorScheme.primaryContainer)
    ) { padding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
        ) {
            content(
                modifier = modifier
            )
        }
    }
}

/*
@Composable
private fun SetupBottomSheet() {
    val coroutineScope = rememberCoroutineScope()
    val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
        bottomSheetState = SheetState(
            initialValue = SheetValue.Hidden,
            skipPartiallyExpanded = true
        )
    )
    BottomSheetScaffold(
        scaffoldState = bottomSheetScaffoldState,
        sheetContent = {
            Row (
                modifier = Modifier
                    .padding(8.dp)
                    .fillMaxWidth()
                    .wrapContentWidth()
            ) {
                Button(
                    onClick = {},
                ) {
                    Text(text = "Dial Pad")
                }
                Button(
                    modifier = Modifier.padding(horizontal = 8.dp),
                    onClick = {},
                ) {
                    Text(text = "Call History")
                }
                Button(
                    onClick = {},
                ) {
                    Text(text = "Contacts")
                }
            }
        },
        sheetPeekHeight = 0.dp,
        sheetShape = MaterialTheme.shapes.extraLarge,
        sheetShadowElevation = 10.dp,
        sheetTonalElevation = 10.dp,
    ) {
        Column {
            // Main Content
            Button(onClick = {
                coroutineScope.launch {
                    if (bottomSheetScaffoldState.bottomSheetState.isVisible) {
                        bottomSheetScaffoldState.bottomSheetState.hide()
                    } else {
                        bottomSheetScaffoldState.bottomSheetState.expand()
                    }
                }
            }) {
                Text(text = "Open Bottom Sheet")
            }
        }
    }
}
*/
/**
 * Composable for the Dialer screen.
 * This has two main parts:
 *  1. The dialer pad
 *  2. The current number that was entered.
 *    - This is a text field that is updated when the user presses a number on the dialer pad.
 *    - This is also updated when the user presses the backspace button.
 *    - State is remembered when the user navigates away from the screen.
 *    - State is remembered through recompositions (e.g. screen rotation).
 */
@Composable
fun DialerScreen(
    currentEntry: String,
    stateChange: (currentEntry: String) -> Unit,
    dial: (entry: String) -> Unit,
    modifier: Modifier = Modifier
) {
    Column (
        modifier = modifier
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth(),
            horizontalArrangement = Arrangement.Center,
        ) {
            Text(
                text = currentEntry,
                modifier = Modifier
                    .padding(8.dp),
                fontSize = MaterialTheme.typography.headlineLarge.fontSize,
                maxLines = 1
            )
        }
        DialerPad(
            currentEntry = currentEntry,
            stateChange = stateChange,
        )
        Button(
            modifier = Modifier
                .padding(8.dp)
                .fillMaxSize(), // take up the rest of the space
            colors = ButtonDefaults.buttonColors(
                containerColor = MaterialTheme.colorScheme.primary,
            ),
            onClick = {
                dial(currentEntry)
            }) {
            Text(
                text = "Call",
                fontSize = MaterialTheme.typography.headlineLarge.fontSize,
                fontFamily = MaterialTheme.typography.headlineMedium.fontFamily,
                fontWeight = MaterialTheme.typography.headlineMedium.fontWeight,
                fontStyle = MaterialTheme.typography.headlineMedium.fontStyle,
                color = MaterialTheme.colorScheme.onPrimary,
            )
        }
    }
}

/**
 *  Composable that returns a grid of buttons for the dialer pad.
 *  This is a 4x3 grid of buttons with the numbers 1-9, 0, and backspace.
 *  The backspace button is a special case because it is a square button that spans two columns.
 *  The backspace button is also disabled when there is no current entry.
 */
@Composable
fun DialerPad(
    currentEntry: String,
    stateChange: (newState: String) -> Unit,
    modifier: Modifier = Modifier
) {
    LazyVerticalGrid(
        modifier = modifier,
        columns = GridCells.Fixed(3),
    ) {
        for (i in 1..9) {
            item {
                DialerButton(
                    text = i.toString(),
                    onClick = {
                        stateChange(currentEntry + i.toString())
                    }
                )
            }
        }
        item {
            DialerButton(
                text = "*",
                onClick = {
                    stateChange("$currentEntry * ")
                }
            )
        }
        item {
            DialerButton(
                text = "0",
                onClick = {
                    stateChange(currentEntry + "0")
                }
            )
        }
        item {
            DialerButton(
                text = "del",
                onClick = {
                    if (currentEntry.isNotEmpty()) {
                        stateChange(currentEntry.dropLast(1))
                    }
                }
            )
        }
    }
}


@Composable
fun DialerButton(
    text: String,
    onClick: () -> Unit,
) {
    Box(
        modifier = Modifier
            .aspectRatio(1f)
            .fillMaxSize(),
    ) {
        Button(
            onClick = onClick,
            modifier = Modifier
                .padding(8.dp)
                .fillMaxSize(),
        ) {
            Text(
                text = text
            )
        }
    }
}

@BlakeBarrett
Copy link
Author

BlakeBarrett commented Aug 25, 2023

UserProfile.kt

package com.blakebarrett.myapplication

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.blakebarrett.myapplication.ui.theme.DIALTheme
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

@Preview(showBackground = true)
@Composable
fun UserProfileScreenPreview() {
    DIALTheme {
        InitAppWithNavigation()
    }
}

@Composable
fun UserProfileApp() {
    DIALTheme {
        InitAppWithNavigation()
    }
}

object Routes {
    const val FRIENDS_LIST = "friendsList"
    private const val PROFILE = "profile"
    fun profile(userId: Int) = "$PROFILE/$userId"
}

@Composable
fun InitAppWithNavigation() {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = Routes.FRIENDS_LIST
    ) {
        composable(
            "profile/{userId}",
            arguments = listOf(navArgument("userId") { type = NavType.IntType })
        ) { backStackEntry ->
            val userId = backStackEntry.arguments?.getInt("userId")
            userId?.let {
                Scaffold(
                    bottomBar = {
                        Row(
                            modifier = Modifier
                                .fillMaxWidth()
                                .height(56.dp)
                                .background(Color.Magenta),
                            verticalAlignment = Alignment.CenterVertically,
                            horizontalArrangement = Arrangement.SpaceEvenly
                        ) {
                            Text(
                                text = "All Users",
                                modifier = Modifier
                                    .weight(1f)
                                    .clickable {
                                        navController.navigate(Routes.FRIENDS_LIST)
                                    }
                                    .padding(16.dp),
                                fontStyle = MaterialTheme.typography.bodyMedium.fontStyle
                            )
                            Text(
                                text = "Back",
                                modifier = Modifier
                                    .weight(1f)
                                    .clickable {
                                        navController.popBackStack()
                                    }
                                    .padding(16.dp),
                                fontStyle = MaterialTheme.typography.bodyMedium.fontStyle
                            )
                        }
                    }
                ) { padding ->
                    UserProfileScreen(
                        modifier = Modifier.padding(padding),
                        userId = userId,
                        navigateToUserId = { friendId ->
                            navController.navigate(Routes.profile(friendId))
                        }
                    )
                }
            }
        }
        composable(Routes.FRIENDS_LIST) {
            LazyColumn (
                modifier = Modifier
                    .fillMaxSize()
                    .padding(16.dp)
            ) {
                val allUsers = User.parse(data)
                items(allUsers.count()) {index ->
                    val user = allUsers[index]
                    Box(
                        modifier = Modifier
                            .fillMaxWidth()
                            .background(Color.LightGray, MaterialTheme.shapes.medium)
                            .clickable {
                                navController.navigate(Routes.profile(user.id))
                            }
                            .padding(16.dp)
                    ) {
                        Text("${user.first_name}  ${user.last_name}")
                    }
                }
            }
        }
    }
}

@Composable
fun UserProfileScreen(modifier: Modifier = Modifier,
                      userId: Int,
                      navigateToUserId: (userId: Int) -> Unit = { _ -> }) {
    val model = viewModel<UserViewModelLocator>()
    model.updateUiStateWithUserViewModel(userId)
    model.uiState.collectAsStateWithLifecycle().let { user ->
        Column(
            modifier = modifier
        ) {
            Text(
                text = "User Profile",
                style = MaterialTheme.typography.headlineSmall
            )
            Spacer(modifier = Modifier.height(16.dp))
            user.value?.let {
                UserDetailCard(it, navigateToUserId)
            }
            Spacer(modifier = Modifier.height(16.dp))
        }
    } ?: run {
        Text(
            text = "User not found",
            style = MaterialTheme.typography.headlineMedium
        )
    }
}

@Composable
fun UserDetailCard(
    user: UserViewModelUiState,
    friendClick: (Int) -> Unit = { _ -> }
) {
    Card(
        modifier = Modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(
            defaultElevation = 10.dp
        )
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Text(
                text = "${user.first_name} ${user.last_name}",
                style = MaterialTheme.typography.headlineMedium
            )
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = "Age: ${user.age}",
                style = MaterialTheme.typography.bodyMedium
            )
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = "Email: ${user.email}",
                style = MaterialTheme.typography.bodySmall
            )
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = "Favorite Animal: ${user.favorite_animal}",
                style = MaterialTheme.typography.bodySmall
            )
            Spacer(modifier = Modifier.height(8.dp))
            UserFriendsList(
                user.friends_user_ids,
                onFriendClicked = friendClick
            )
            Spacer(modifier = Modifier.height(8.dp))
            UserHobbies(user.hobbies)
        }
    }
}

@Composable
fun UserHobbies(hobbies: List<String>) {
    Column {
        Text(
            text = "Hobbies",
            style = MaterialTheme.typography.labelSmall
        )
        Spacer(modifier = Modifier.height(8.dp))
        Column {
            hobbies.forEach { hobby ->
                Text(
                    text = "- $hobby",
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }
    }
}

@Composable
fun UserFriendsList(
    friends_user_ids: List<Int>,
    onFriendClicked: (Int) -> Unit = {}
) {
    Column {
        Text(
            text = "Friends",
            style = MaterialTheme.typography.labelSmall
        )
        Spacer(modifier = Modifier.height(8.dp))
        Column {
            friends_user_ids.forEach { friendId ->
                viewModel<UserViewModelLocator>()
                    .getUserById(friendId)?.let { user ->
                    Text(
                        text = "- ${user.first_name} ${user.last_name}",
                        style = MaterialTheme.typography.bodyMedium,
                        modifier = Modifier.clickable {
                            onFriendClicked(friendId)
                        }
                    )
                }
            }
        }
    }
}

data class UserViewModelUiState(
    val first_name: String,
    val last_name: String,
    val age: Int,
    val email: String,
    val favorite_animal: String,
    val hobbies: List<String>,
    val friends_user_ids: List<Int>
)

class UserViewModelLocator : ViewModel() {
    private val _allUsers: List<User> = User.parse(data)
    private val _uiState = MutableStateFlow<UserViewModelUiState?>(null)
    val uiState: StateFlow<UserViewModelUiState?> = _uiState.asStateFlow()

    fun getUserById(userId: Int): UserViewModelUiState? {
        return _allUsers.find { it.id == userId }?.let { user ->
            UserViewModelUiState(
                first_name = user.first_name,
                last_name = user.last_name,
                age = user.age,
                email = user.email,
                favorite_animal = user.favorite_animal,
                hobbies = user.hobbies,
                friends_user_ids = user.friends_user_ids
            )
        }
    }

    fun updateUiStateWithUserViewModel(userId: Int) {
        getUserById(userId)?.let { user ->
            _uiState.update {
                it?.copy(
                    first_name = user.first_name,
                    last_name = user.last_name,
                    age = user.age,
                    email = user.email,
                    favorite_animal = user.favorite_animal,
                    hobbies = user.hobbies,
                    friends_user_ids = user.friends_user_ids
                ) ?: run {
                    UserViewModelUiState(
                        first_name = user.first_name,
                        last_name = user.last_name,
                        age = user.age,
                        email = user.email,
                        favorite_animal = user.favorite_animal,
                        hobbies = user.hobbies,
                        friends_user_ids = user.friends_user_ids
                    )
                }
            }
        }
    }
}

@Serializable
data class User(
    val id: Int,
    val first_name: String,
    val last_name: String,
    val email: String,
    val age: Int,
    val last_login_date: String,
    val friends_user_ids: List<Int>,
    val favorite_animal: String,
    val least_favorite_smell: String,
    val hobbies: List<String>,
    val birthplace: String,
    val favorite_color: String,
    val unread_messages: Int,
    val has_pets: Boolean,
    val shoe_size: Double,
    val coffee_preference: String,
    val dream_destination: String,
    val music_genre: String,
    val is_vegan: Boolean,
    val fitness_level: String,
    val lucky_number: Int
) {
    companion object {
        fun parse(source: String): List<User> {
            return Json.decodeFromString(source)
        }
    }
}

const val data = """
[
    {
        "id": 1,
        "first_name": "Alice",
        "last_name": "Smith",
        "email": "alice@example.com",
        "age": 28,
        "last_login_date": "2023-08-09T10:30:00",
        "friends_user_ids": [2, 3, 5, 7],
        "favorite_animal": "Dog",
        "least_favorite_smell": "Durian",
        "hobbies": ["Painting", "Hiking"],
        "birthplace": "Cityville",
        "favorite_color": "Purple",
        "unread_messages": 3,
        "has_pets": true,
        "shoe_size": 7.5,
        "coffee_preference": "Black",
        "dream_destination": "Bora Bora",
        "music_genre": "Indie Rock",
        "is_vegan": false,
        "fitness_level": "Intermediate",
        "lucky_number": 17
    },
    {
        "id": 2,
        "first_name": "Bob",
        "last_name": "Johnson",
        "email": "bob@example.com",
        "age": 35,
        "last_login_date": "2023-08-08T15:20:00",
        "friends_user_ids": [1, 4, 8],
        "favorite_animal": "Cat",
        "least_favorite_smell": "Sulfur",
        "hobbies": ["Reading", "Gardening"],
        "birthplace": "Villageville",
        "favorite_color": "Blue",
        "unread_messages": 0,
        "has_pets": true,
        "shoe_size": 9.0,
        "coffee_preference": "Latte",
        "dream_destination": "Paris",
        "music_genre": "Classical",
        "is_vegan": true,
        "fitness_level": "Beginner",
        "lucky_number": 5
    },
    {
        "id": 3,
        "first_name": "Charlie",
        "last_name": "Brown",
        "email": "charlie@example.com",
        "age": 22,
        "last_login_date": "2023-08-09T08:45:00",
        "friends_user_ids": [1, 5, 9],
        "favorite_animal": "Elephant",
        "least_favorite_smell": "Fish",
        "hobbies": ["Cooking", "Playing Guitar"],
        "birthplace": "Townsville",
        "favorite_color": "Green",
        "unread_messages": 10,
        "has_pets": false,
        "shoe_size": 8.5,
        "coffee_preference": "Cappuccino",
        "dream_destination": "Tokyo",
        "music_genre": "Pop",
        "is_vegan": false,
        "fitness_level": "Advanced",
        "lucky_number": 12
    },
    {
        "id": 4,
        "first_name": "David",
        "last_name": "Lee",
        "email": "david@example.com",
        "age": 31,
        "last_login_date": "2023-08-07T18:10:00",
        "friends_user_ids": [2, 8, 10],
        "favorite_animal": "Lion",
        "least_favorite_smell": "Garlic",
        "hobbies": ["Soccer", "Photography"],
        "birthplace": "Countryville",
        "favorite_color": "Yellow",
        "unread_messages": 5,
        "has_pets": true,
        "shoe_size": 10.5,
        "coffee_preference": "Espresso",
        "dream_destination": "New York",
        "music_genre": "Rock",
        "is_vegan": false,
        "fitness_level": "Intermediate",
        "lucky_number": 9
    },
    {
        "id": 5,
        "first_name": "Eve",
        "last_name": "Williams",
        "email": "eve@example.com",
        "age": 27,
        "last_login_date": "2023-08-09T12:05:00",
        "friends_user_ids": [1, 3, 9, 11],
        "favorite_animal": "Dolphin",
        "least_favorite_smell": "Onion",
        "hobbies": ["Yoga", "Traveling"],
        "birthplace": "Seaville",
        "favorite_color": "Turquoise",
        "unread_messages": 8,
        "has_pets": true,
        "shoe_size": 8.0,
        "coffee_preference": "Mocha",
        "dream_destination": "Maldives",
        "music_genre": "Electronic",
        "is_vegan": true,
        "fitness_level": "Advanced",
        "lucky_number": 3
    },
    {
        "id": 6,
        "first_name": "Frank",
        "last_name": "Davis",
        "email": "frank@example.com",
        "age": 45,
        "last_login_date": "2023-08-08T09:40:00",
        "friends_user_ids": [12, 13, 14],
        "favorite_animal": "Giraffe",
        "least_favorite_smell": "Vinegar",
        "hobbies": ["Golf", "Painting"],
        "birthplace": "Mountainville",
        "favorite_color": "Orange",
        "unread_messages": 2,
        "has_pets": false,
        "shoe_size": 11.0,
        "coffee_preference": "Americano",
        "dream_destination": "Switzerland",
        "music_genre": "Jazz",
        "is_vegan": false,
        "fitness_level": "Beginner",
        "lucky_number": 8
    },
    {
        "id": 7,
        "first_name": "Grace",
        "last_name": "Martinez",
        "email": "grace@example.com",
        "age": 29,
        "last_login_date": "2023-08-09T16:15:00",
        "friends_user_ids": [1, 15, 16],
        "favorite_animal": "Penguin",
        "least_favorite_smell": "Cabbage",
        "hobbies": ["Singing", "Cooking"],
        "birthplace": "Snowville",
        "favorite_color": "White",
        "unread_messages": 1,
        "has_pets": true,
        "shoe_size": 6.5,
        "coffee_preference": "Latte",
        "dream_destination": "Hawaii",
        "music_genre": "Reggae",
        "is_vegan": true,
        "fitness_level": "Intermediate",
        "lucky_number": 13
    },
    {
        "id": 8,
        "first_name": "Henry",
        "last_name": "Taylor",
        "email": "henry@example.com",
        "age": 32,
        "last_login_date": "2023-08-08T13:30:00",
        "friends_user_ids": [2, 4, 9, 17],
        "favorite_animal": "Tiger",
        "least_favorite_smell": "Mold",
        "hobbies": ["Running", "Chess"],
        "birthplace": "Desertville",
        "favorite_color": "Red",
        "unread_messages": 12,
        "has_pets": true,
        "shoe_size": 10.0,
        "coffee_preference": "Cappuccino",
        "dream_destination": "Australia",
        "music_genre": "Country",
        "is_vegan": false,
        "fitness_level": "Advanced",
        "lucky_number": 21
    },
    {
        "id": 9,
        "first_name": "Ivy",
        "last_name": "Anderson",
        "email": "ivy@example.com",
        "age": 24,
        "last_login_date": "2023-08-09T07:55:00",
        "friends_user_ids": [3, 5, 8, 18],
        "favorite_animal": "Kangaroo",
        "least_favorite_smell": "Smoked Fish",
        "hobbies": ["Dancing", "Biking"],
        "birthplace": "Jungleville",
        "favorite_color": "Yellow",
        "unread_messages": 6,
        "has_pets": true,
        "shoe_size": 8.0,
        "coffee_preference": "Espresso",
        "dream_destination": "Brazil",
        "music_genre": "Hip Hop",
        "is_vegan": true,
        "fitness_level": "Beginner",
        "lucky_number": 6
    },
    {
        "id": 10,
        "first_name": "Jack",
        "last_name": "Moore",
        "email": "jack@example.com",
        "age": 30,
        "last_login_date": "2023-08-07T21:20:00",
        "friends_user_ids": [4, 7, 9, 19],
        "favorite_animal": "Horse",
        "least_favorite_smell": "Burnt Rubber",
        "hobbies": ["Photography", "Travelling"],
        "birthplace": "Farmville",
        "favorite_color": "Brown",
        "unread_messages": 9,
        "has_pets": true,
        "shoe_size": 10.5,
        "coffee_preference": "Black",
        "dream_destination": "New Zealand",
        "music_genre": "Alternative",
        "is_vegan": false,
        "fitness_level": "Intermediate",
        "lucky_number": 11
    },
    {
        "id": 11,
        "first_name": "Karen",
        "last_name": "Jackson",
        "email": "karen@example.com",
        "age": 26,
        "last_login_date": "2023-08-09T14:50:00",
        "friends_user_ids": [5, 10, 20],
        "favorite_animal": "Panda",
        "least_favorite_smell": "Rotting Fruit",
        "hobbies": ["Yoga", "Reading"],
        "birthplace": "Cityville",
        "favorite_color": "Pink",
        "unread_messages": 4,
        "has_pets": false,
        "shoe_size": 7.0,
        "coffee_preference": "Mocha",
        "dream_destination": "Thailand",
        "music_genre": "R&B",
        "is_vegan": true,
        "fitness_level": "Advanced",
        "lucky_number": 15
    },
    {
        "id": 12,
        "first_name": "Liam",
        "last_name": "Garcia",
        "email": "liam@example.com",
        "age": 21,
        "last_login_date": "2023-08-08T08:00:00",
        "friends_user_ids": [6, 13, 15],
        "favorite_animal": "Sloth",
        "least_favorite_smell": "Dirty Socks",
        "hobbies": ["Gaming", "Cooking"],
        "birthplace": "Villageville",
        "favorite_color": "Black",
        "unread_messages": 7,
        "has_pets": true,
        "shoe_size": 9.5,
        "coffee_preference": "Latte",
        "dream_destination": "Greece",
        "music_genre": "Metal",
        "is_vegan": false,
        "fitness_level": "Beginner",
        "lucky_number": 14
    },
    {
        "id": 13,
        "first_name": "Mia",
        "last_name": "Rodriguez",
        "email": "mia@example.com",
        "age": 33,
        "last_login_date": "2023-08-09T11:40:00",
        "friends_user_ids": [6, 12, 14, 16],
        "favorite_animal": "Owl",
        "least_favorite_smell": "Stale Beer",
        "hobbies": ["Singing", "Painting"],
        "birthplace": "Countryville",
        "favorite_color": "Blue",
        "unread_messages": 5,
        "has_pets": true,
        "shoe_size": 8.5,
        "coffee_preference": "Cappuccino",
        "dream_destination": "Italy",
        "music_genre": "Pop",
        "is_vegan": true,
        "fitness_level": "Intermediate",
        "lucky_number": 8
    },
    {
        "id": 14,
        "first_name": "Noah",
        "last_name": "Martinez",
        "email": "noah@example.com",
        "age": 40,
        "last_login_date": "2023-08-08T17:55:00",
        "friends_user_ids": [6, 13, 15],
        "favorite_animal": "Wolf",
        "least_favorite_smell": "Mothballs",
        "hobbies": ["Running", "Cooking"],
        "birthplace": "Cityville",
        "favorite_color": "Green",
        "unread_messages": 2,
        "has_pets": false,
        "shoe_size": 10.0,
        "coffee_preference": "Espresso",
        "dream_destination": "Australia",
        "music_genre": "Rock",
        "is_vegan": false,
        "fitness_level": "Advanced",
        "lucky_number": 19
    },
    {
        "id": 15,
        "first_name": "Olivia",
        "last_name": "Thompson",
        "email": "olivia@example.com",
        "age": 29,
        "last_login_date": "2023-08-09T10:15:00",
        "friends_user_ids": [7, 13, 14, 17],
        "favorite_animal": "Duck",
        "least_favorite_smell": "Wet Dog",
        "hobbies": ["Soccer", "Gardening"],
        "birthplace": "Townsville",
        "favorite_color": "Red",
        "unread_messages": 1,
        "has_pets": true,
        "shoe_size": 7.0,
        "coffee_preference": "Black",
        "dream_destination": "Japan",
        "music_genre": "Electronic",
        "is_vegan": true,
        "fitness_level": "Intermediate",
        "lucky_number": 10
    },
    {
        "id": 16,
        "first_name": "Peter",
        "last_name": "Wright",
        "email": "peter@example.com",
        "age": 37,
        "last_login_date": "2023-08-08T12:30:00",
        "friends_user_ids": [6, 15, 18],
        "favorite_animal": "Bear",
        "least_favorite_smell": "Gasoline",
        "hobbies": ["Reading", "Biking"],
        "birthplace": "Seaville",
        "favorite_color": "Blue",
        "unread_messages": 0,
        "has_pets": false,
        "shoe_size": 9.5,
        "coffee_preference": "Mocha",
        "dream_destination": "New Zealand",
        "music_genre": "Indie Rock",
        "is_vegan": false,
        "fitness_level": "Advanced",
        "lucky_number": 7
    },
    {
        "id": 17,
        "first_name": "Quinn",
        "last_name": "Hernandez",
        "email": "quinn@example.com",
        "age": 25,
        "last_login_date": "2023-08-09T09:05:00",
        "friends_user_ids": [8, 16, 19],
        "favorite_animal": "Fox",
        "least_favorite_smell": "Wet Carpet",
        "hobbies": ["Painting", "Singing"],
        "birthplace": "Mountainville",
        "favorite_color": "Orange",
        "unread_messages": 4,
        "has_pets": true,
        "shoe_size": 8.0,
        "coffee_preference": "Latte",
        "dream_destination": "Greece",
        "music_genre": "Pop",
        "is_vegan": true,
        "fitness_level": "Beginner",
        "lucky_number": 22
    },
    {
        "id": 18,
        "first_name": "Ryan",
        "last_name": "Adams",
        "email": "ryan@example.com",
        "age": 32,
        "last_login_date": "2023-08-08T14:25:00",
        "friends_user_ids": [9, 15, 20],
        "favorite_animal": "Gorilla",
        "least_favorite_smell": "Burnt Popcorn",
        "hobbies": ["Gaming", "Hiking"],
        "birthplace": "Villageville",
        "favorite_color": "Green",
        "unread_messages": 6,
        "has_pets": true,
        "shoe_size": 10.5,
        "coffee_preference": "Black",
        "dream_destination": "Hawaii",
        "music_genre": "Electronic",
        "is_vegan": false,
        "fitness_level": "Intermediate",
        "lucky_number": 18
    },
    {
        "id": 19,
        "first_name": "Sophia",
        "last_name": "Gonzalez",
        "email": "sophia@example.com",
        "age": 28,
        "last_login_date": "2023-08-09T13:00:00",
        "friends_user_ids": [10, 17, 20],
        "favorite_animal": "Llama",
        "least_favorite_smell": "Public Restrooms",
        "hobbies": ["Reading", "Dancing"],
        "birthplace": "Cityville",
        "favorite_color": "Purple",
        "unread_messages": 2,
        "has_pets": true,
        "shoe_size": 8.0,
        "coffee_preference": "Latte",
        "dream_destination": "Paris",
        "music_genre": "Pop",
        "is_vegan": true,
        "fitness_level": "Advanced",
        "lucky_number": 4
    },
    {
        "id": 20,
        "first_name": "Thomas",
        "last_name": "Walker",
        "email": "thomas@example.com",
        "age": 30,
        "last_login_date": "2023-08-08T19:45:00",
        "friends_user_ids": [11, 19],
        "favorite_animal": "Monkey",
        "least_favorite_smell": "Sewage",
        "hobbies": ["Cooking", "Traveling"],
        "birthplace": "Townsville",
        "favorite_color": "Blue",
        "unread_messages": 7,
        "has_pets": false,
        "shoe_size": 9.5,
        "coffee_preference": "Cappuccino",
        "dream_destination": "Maldives",
        "music_genre": "Rock",
        "is_vegan": false,
        "fitness_level": "Intermediate",
        "lucky_number": 16
    }
]
"""

@BlakeBarrett
Copy link
Author

MyComposableUnitTest.kt

import androidx.compose.material3.Text
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import org.junit.Rule
import org.junit.Test

class MyComposableTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun testMyComposable() {
        composeTestRule.setContent {
            // Call your Composable function here
            Text("Hello, World!")
        }

        // Use onNodeWithText or other assertions if needed
        composeTestRule.onNodeWithText("Hello, World!").assertExists()
    }
}

@BlakeBarrett
Copy link
Author

@BlakeBarrett
Copy link
Author

@BlakeBarrett
Copy link
Author

build.gradle

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    kotlin("plugin.serialization") version "1.8.10"
}

android {
    namespace = "com.blakebarrett.myapplication"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.blakebarrett.myapplication"
        minSdk = 24
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary = true
        }
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
    buildFeatures {
        compose = true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.4.3"
    }
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
}

dependencies {

    val lifecycle_version = "2.6.1"
    val compose_version = "1.5.0"

    implementation("androidx.core:core-ktx:1.10.1")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
    implementation("androidx.activity:activity-compose:1.7.2")
    implementation(platform("androidx.compose:compose-bom:2023.08.00"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-graphics")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3:1.1.1")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00"))
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-tooling")
    debugImplementation("androidx.compose.ui:ui-test-manifest:1.5.0")
    // Kotlin serialization
    implementation("org.jetbrains.kotlin:kotlin-serialization-compiler-plugin-embeddable:1.9.0")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")

    // ViewModel
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
    // ViewModel utilities for Compose
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version")
    // LiveData
    implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
    // Lifecycles only (without ViewModel or LiveData)
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version")
    // Lifecycle utilities for Compose
    implementation("androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version")
    // Saved state module for ViewModel
    implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version")

    implementation("androidx.compose.runtime:runtime:$compose_version")
    implementation("androidx.compose.runtime:runtime-livedata:$compose_version")
    // Test rules and transitive dependencies:
    testImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
    // Needed for createAndroidComposeRule, but not createComposeRule:
    testImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")

    // Test rules and transitive dependencies:
    androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
    // Needed for createAndroidComposeRule, but not createComposeRule:
    debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")

    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")

    // or Material Design 2
    implementation("androidx.compose.material:material")
    // or skip Material Design and build directly on top of foundational components
    implementation("androidx.compose.foundation:foundation")

    val nav_version = "2.7.1"
    implementation("androidx.navigation:navigation-compose:$nav_version")
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment