Home > Back-end >  Save and show profile picture with Jetpack/Coil on app startup
Save and show profile picture with Jetpack/Coil on app startup

Time:04-28

My idea was to render a profile picture there and allow the user to change it. In order to save the selected picture I am using the SharedPreferences (saving the Uri as string). The problem is the on each startup the image does not show up. The value retrieved by the Shared manager seems correct, but the SubComposeAsyncImageContent does not correctly show the pic.

Profile Picture composable:

@Composable
fun ProfilePicture(
    imageUri: String?,
    size: Dp = 50.dp,
    onClick: (String) -> Unit,
) {

    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.GetContent())  { uri: Uri? ->
        onClick(uri.toString())
    }

    if (imageUri != null) {
        Log.e("ProfilePicture", imageUri)
    }

    SubcomposeAsyncImage(
        model = imageUri,
        contentDescription = "",
        modifier = Modifier.clickable { launcher.launch("image/*") }
    ) {
        val state = painter.state
        Log.e("ProfilePicState", "${state}")
        if (state is AsyncImagePainter.State.Loading || state is AsyncImagePainter.State.Error) {
            CircularProgressIndicator()
        } else {
            SubcomposeAsyncImageContent()
        }

    }
}

The idea was that the imageUri is passed as parameter from the profile screen (that contains a ProfilePicture). The profile screen gets this value from the viewModel, which has access to the sharedPreferences.

ProfileScreen.kt:

@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {

    var profileUri by rememberSaveable {
        mutableStateOf(viewModel.getProfilePicURI())
    }

    Log.w("ProfileScreen", profileUri)

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        ProfilePicture(
            imageUri = profileUri,
            size = 150.dp,
            onClick = {
                viewModel.onEvent(ProfileEvents.OnUpdateProfilePic(it))
                profileUri = viewModel.getProfilePicURI()
            }
        )
    }
}

Lastly, the viewModel:

class ProfileViewModel(val preferenceManager: PreferenceManager): ViewModel() {

    fun getProfilePicURI(): String {
        return preferenceManager.getProfilePic()
    }

    fun onEvent(event: ProfileEvents) {
        when (event) {
            is ProfileEvents.OnUpdateProfilePic -> {
                // update the sharedpreference
                preferenceManager.setProfilePic(event.newUri)
                Log.e("ProfileVM", "uri stored: ${getProfilePicURI()}")

            }
        }
    }
}

As said, the code works within the app, i.e. I can change the profile pic and it gets remembered even when I go back to the profile screen, but at each startup the painter fails to load the image even though the right resource seems to be sent.

The log looks as follows:

2022-04-27 09:29:45.174 12502-12502/com.example.insurance W/ProfileScreen: content://com.android.providers.media.documents/document/image:96
2022-04-27 09:29:45.182 12502-12502/com.example.insurance E/ProfilePicture: content://com.android.providers.media.documents/document/image:96
2022-04-27 09:29:45.258 12502-12502/com.example.insurance E/ProfilePicState: Loading(painter=null)
2022-04-27 09:29:45.274 12502-12502/com.example.insurance E/ProfilePicState: Loading(painter=null)
2022-04-27 09:29:45.278 12502-12502/com.example.insurance E/ProfilePicState: Error(painter=null, result=coil.request.ErrorResult@bfc77785)

Exiting the app with the back button works, on recreation the profile pic is there. Destroying it via the task task manager triggers the wrong behaviour.

When I start the app the only way to show a profile pic is to select a different image, i.e. if I select the img that was previously selected it doesn't show. As soon as I pick a new one it shows again

CodePudding user response:

The latest versions of Android provide access to the file only during the current application session - after restarting you no longer have the rights to read the same URI.

The file is still there: if you pick it again, you'll get the same URI, and will have access too, but in your case the same URI doesn't trigger recomposition, that's why you can't see the update.

Even when you have access to the URI, you don't have direct access, you can only get the input stream from context.contentResolver. This is what Coil does for you under the hood when displaying it, but to copy it, you have to manage it yourself.

Output file should be in App-specific storage.

val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.GetContent()
)  { newUri: Uri? ->
    if (newUri == null) return@rememberLauncherForActivityResult

    val input = context.contentResolver.openInputStream(newUri) ?: return@rememberLauncherForActivityResult
    val outputFile = context.filesDir.resolve("profilePic.jpg")
    input.copyTo(outputFile.outputStream())
    uri = outputFile.toUri()
}

Note that I'm using the same filename here, so if you select two different images in a row, you won't see an update from the first to the second, since the output URI is still the same. How to solve this problem is up to you, for example add a UUID to the filename so it's a unique URI each time, just don't forget to delete the old one.

  • Related