Creating Jetpack Compose Profile Screen with ChatGPT

I want a profile screen with three fields:

  • Profile picture
  • Name
  • Email

I want the full functionality, so I am going to ask for these parts of the architecture:

  • The view with the composables
  • The view model
  • The data access using room

View

I started with a simple pompt like this

This is the code that it gave me

package com.example.profile_screen.screens

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.material3.icons.Icons
import androidx.compose.material3.icons.filled.MailOutline
import androidx.compose.material3.icons.filled.Person
import androidx.compose.material3.icons.filled.Save
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

data class UserProfile(val name: String, val email: String, val profileImageRes: Int)

@Composable
fun ProfileScreen(userProfile: UserProfile) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        // Profile Image
        Image(
            painter = painterResource(id = userProfile.profileImageRes),
            contentDescription = null,
            modifier = Modifier
                .size(120.dp)
                .clip(CircleShape)
                .background(MaterialTheme.colorScheme.primary)
        )

        Spacer(modifier = Modifier.height(16.dp))

        // Name Field
        OutlinedTextField(
            value = userProfile.name,
            onValueChange = { /*TODO*/ },
            label = { Text("Name") },
            leadingIcon = { Icon(imageVector = Icons.Default.Person, contentDescription = null) },
            modifier = Modifier
                .fillMaxWidth()
                .padding(8.dp)
        )

        Spacer(modifier = Modifier.height(16.dp))

        // Email Field
        OutlinedTextField(
            value = userProfile.email,
            onValueChange = { /*TODO*/ },
            label = { Text("Email") },
            leadingIcon = { Icon(imageVector = Icons.Default.MailOutline, contentDescription = null) },
            modifier = Modifier
                .fillMaxWidth()
                .padding(8.dp)
        )

        Spacer(modifier = Modifier.height(16.dp))

        // Save Button
        Button(
            onClick = { /*TODO*/ },
            modifier = Modifier
                .fillMaxWidth()
                .height(50.dp)
        ) {
            Icon(imageVector = Icons.Default.Save, contentDescription = null)
            Spacer(modifier = Modifier.width(8.dp))
            Text("Save")
        }
    }
}

@Preview(showBackground = true)
@Composable
fun ProfileScreenPreview() {
    val userProfile = UserProfile(
        name = "John Doe",
        email = "john.doe@example.com",
        profileImageRes = R.drawable.ic_launcher_foreground // Replace with your actual image resource
    )
    ProfileScreen(userProfile = userProfile)
}

I had to fix the imports of the icons, one of the icon didn’t exists. Fixing that this is how to looks like:


Now I want the the user select the profile picture.

It changed the profile image field and add a functión to select an image. This is the new function:

var userProfile by remember { mutableStateOf(userProfile) }
val getContent = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.GetContent()) { uri: Uri? ->
    uri?.let {
        userProfile = userProfile.copy(profileImageUri = it)
    }
}

And the profile image field now calls the getContent function

    Box(
            modifier = Modifier
                .size(120.dp)
                .clip(CircleShape)
                .background(MaterialTheme.colorScheme.primary)
                .clickable {
                    getContent.launch("image/*") // Calls getContent
                },
            contentAlignment = Alignment.Center
        ) {
            if (userProfile.profileImageUri != null) {
                Image(
                    painter = painterResource(id = R.drawable.ic_launcher_foreground),
                    contentDescription = null,
                    contentScale = ContentScale.Crop,
                    modifier = Modifier
                        .fillMaxSize()
                        .clip(CircleShape)
                )
            } else {
                Icon(
                    imageVector = Icons.Default.Upload,
                    contentDescription = null,
                    modifier = Modifier.size(48.dp),
                    tint = MaterialTheme.colorScheme.onPrimary
                )
            }
        }

The code did not show the selected image, so I added a new dependency and changed some code

implementation("io.coil-kt:coil-compose:2.5.0")
if (userProfile.profileImageUri != null) {
    Image(
        // I replace this line
        //painter = painterResource(id = R.drawable.ic_launcher_foreground),
        painter = rememberAsyncImagePainter(
            model = userProfile.profileImageUri   // or ht
        ),
        contentDescription = null,
        contentScale = ContentScale.Crop,
        modifier = Modifier
            .fillMaxSize()
            .clip(CircleShape)
    )
}

It was necesary to add permissions to read the image, so I added a line when the image is picked

uri?.let {
  //Permissions to read the uri
  context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION )

  //userProfile = userProfile.copy(profileImageUri = it)
  viewModel.setProfileImageUri(it.toString())
}

View Model

For the ViewModel, this was my prompt

And ChatGPT gave me this code

import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

data class UserProfileState(
    val name: String = "",
    val email: String = "",
    val profileImageUri: Uri? = null
)

class ProfileViewModel : ViewModel() {

    private val _userProfileState = MutableStateFlow(UserProfileState())
    val userProfileState: StateFlow<UserProfileState> get() = _userProfileState

    fun setName(name: String) {
        _userProfileState.value = _userProfileState.value.copy(name = name)
    }

    fun setEmail(email: String) {
        _userProfileState.value = _userProfileState.value.copy(email = email)
    }

    fun setProfileImageUri(uri: Uri?) {
        _userProfileState.value = _userProfileState.value.copy(profileImageUri = uri)
    }

    fun saveUserProfile() {
        // Implement your save logic here
        viewModelScope.launch {
            // For example, you can save the user profile to a repository or database
            // userRepository.saveUserProfile(_userProfileState.value)
        }
    }
}

Nowadays the prefered way to update the view model is using the update method, so I changed it. I made some renames too, and I change the Uri parameter for a string because it is easier to store in the database.


class ProfileViewModel : ViewModel() {

    private val _state = MutableStateFlow(UserProfileState())
    val state: StateFlow<UserProfileState> = _state

    fun setName(name: String) {
        _state.update { it.copy(name = name) }
    }

    fun setEmail(email: String) {
        _state.update { it.copy(email = email) }
    }

    fun setProfileImageUri(uri: Uri?) {
        _state.update { it.copy(image = uri.toString()) }
    }

    fun saveUserProfile() {
        // Implement your save logic here
        viewModelScope.launch {
            // For example, you can save the user profile to a repository or database
            // userRepository.saveUserProfile(_userProfileState.value)
        }
    }
}

Data access

First I’m going to create the data model and and data access objects

ChatGPT did a great job! I didn’t have to ask for the Dao neither for the database class. It gave me this code


@Entity(tableName = "user_profiles")
data class UserProfileEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val name: String,
    val image: String? = null,
    val email: String
)

@Dao
interface UserProfileDao {

    @Insert
    suspend fun insertUserProfile(userProfile: UserProfileEntity)

    @Query("SELECT * FROM user_profiles WHERE id = :userId")
    suspend fun getUserProfile(userId: Long): UserProfileEntity?


}


@Database(entities = [UserProfileEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userProfileDao(): UserProfileDao
}

I’m going to make changes acoording my needs

@Entity(tableName = "profile")
data class Profile(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val name: String,
    val image: String? = null,
    val email: String
)

@Dao
interface ProfileDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun save(profile: Profile)

    @Query("SELECT * FROM profile")
    suspend fun getAll(): List<Profile>
}

@Database(entities = [Profile::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun profileDao(): ProfileDao
}

Glue the parts

To add the database to the ViewModel just add two simple methods

    fun load() {
        viewModelScope.launch {
            val profiles = database.profileDao().getAll()

            if (profiles.isNotEmpty()) {
                val profile = profiles.first()

                _state.update {
                    it.copy(
                        name = profile.name,
                        image = null,
                        email = profile.email,
                        id = profile.id
                    )
                }

                setProfileImageUri(profile.image)
            }

        }
    }
    
    fun saveUserProfile() {
        viewModelScope.launch {
            val state = state.value
            var profile = Profile(
                id = state.id,
                name = state.name,
                email = state.email,
                image = state.image
            )

            database.profileDao().save(profile)
        }
    }

And I pass the database in the constructor

class ProfileViewModel(private val database: AppDatabase) : ViewModel()

Then I add the view model in the screen. For example to bind the email field with the view model I change the value and onValueChange properties of the composable

val state by viewModel.state.collectAsState()

OutlinedTextField(
    value = state.email, // Bind view Model
    onValueChange = {
        viewModel.setEmail(it)
    },
    label = { Text("Email") },
    leadingIcon = { Icon(
        imageVector = Icons.Default.MailOutline, 
        contentDescription = null) 
                  },
    modifier = Modifier
        .fillMaxWidth()
        .padding(8.dp)
)

GitHub Code: https://github.com/FractalCodeRicardo/profile-screen

Leave a Reply

Your email address will not be published. Required fields are marked *