Mustafa Abdullah
Mustafa Abdullah

Reputation: 171

Image picker from gallery for Jetpack compose - Android/Kotlin

I want to implement an image picker using Jetpack compose I was searching for solutions and I found some tutorials like this one https://ngengesenior.medium.com/pick-image-from-gallery-in-jetpack-compose-5fa0d0a8ddaf I used the code they explained and it worked fine but I have a problem!

My app includes one activity "MainActivity" which start rendering compose components, one of my screens is a form with a field to select image, and other fields, when I used the code below it opens the gallery and I select an image and when clicking OK, it goes to the MainActivity, but I need instead to stay in the same screen of the form so user could continue completing the form, I will list the code and I hope someone could help me with this

val launcher = rememberLauncherForActivityResult(contract =
    ActivityResultContracts.GetContent()) { uri: Uri? ->
        imageUri = uri
    }
    Column() {
        Button(onClick = {
            launcher.launch("image/*")
        }) {
            Text(text = "Pick image")
        }

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

        imageUri?.let {
            if (Build.VERSION.SDK_INT < 28) {
                bitmap.value = MediaStore.Images
                    .Media.getBitmap(context.contentResolver,it)

            } else {
                val source = ImageDecoder
                    .createSource(context.contentResolver,it)
                bitmap.value = ImageDecoder.decodeBitmap(source)
            }

            bitmap.value?.let {  btm ->
                Image(bitmap = btm.asImageBitmap(),
                    contentDescription =null,
                    modifier = Modifier.size(400.dp))
            }
        }

    }
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            Button(
                onClick = {
                    navController.navigate(AppScreens.FormScreen.route)
                },
                ) {
                Text(text = "Go to form screen" )
            }
        }
    }
}
@Composable
fun FormScreen() {
    var imageUri by remember { mutableStateOf<Uri?>(null) }
    val context = LocalContext.current
    var bitmap by remember { mutableStateOf<Bitmap?>(null) }

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

    Column {
        // some text field in the form
        // another number field in the form
        // select image filed in the form
        CustomInputFieldContainer(
            label = "select image"
        ) {
            Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
                Column(horizontalAlignment = Alignment.CenterHorizontally) {

                    imageUri?.let {
                        if (Build.VERSION.SDK_INT < 28) {
                            bitmap = MediaStore.Images
                                .Media.getBitmap(context.contentResolver,it)

                        } else {
                            val source = ImageDecoder
                                .createSource(context.contentResolver,it)
                            bitmap = ImageDecoder.decodeBitmap(source)
                        }

                        bitmap.let {  btm ->
                            Image(bitmap = btm.asImageBitmap(),
                                contentDescription =null,
                                modifier = Modifier.size(400.dp))
                        }

                    }

                    Button(
                        onClick = { launcher.launch("image/*") },
                        contentPadding = PaddingValues(),
                        modifier = Modifier.background(Color.Yellow)
                    ) {
                        Row(
                            modifier = Modifier
                                .fillMaxWidth()
                                .wrapContentSize(Alignment.BottomCenter)
                                .padding(vertical = 10.dp),
                            verticalAlignment = Alignment.CenterVertically
                        ) {
                            Icon(imageVector = Icons.Filled.AddAPhoto, contentDescription = null)
                            Spacer(modifier = Modifier.width(8.dp))
                            Text(text = "Add Photo")
                        }
                    }
                }
            }
        }
    }


}

now I want to select an image from the gallery when clicking on select image button and back to my form to complete the rest of the fields, when I tried the above code I could select an image but it goes to the main activity which makes me lost the data

any help for solving this problem?

Upvotes: 4

Views: 7980

Answers (3)

PAF
PAF

Reputation: 43

I was in the same situation than you if I understood well your problem. Here's how I did it. It's a bit hacky but it works.

I have this

//The clickable composable
    ProfilePictureView(
                    avatarUrl, firstname, lastname, isUpdatingProfileImage,
                    Modifier
                        .padding(top = 50.dp)
                        .size(110.dp)
                        .align(Alignment.CenterHorizontally),
                onClick = {
                        scope.launch {
                            openImagePicker(permissionManager, LocalContext.current as MainActivity)
                        }
                }) 



fun openImagePicker(permissionManager: PermissionManager, activity: AppCompatActivity) {
    permissionManager.requestStoragePermission {
        if (it) {
            val intent = Intent(Intent.ACTION_PICK)
            intent.type = "image/*"
            startActivityForResult(activity, intent, MainActivity.IMAGE_PICK_CODE, null)
        }
    }
}

A class to handle permissions in the app

class PermissionManager(private val activity: AppCompatActivity) {

    private val errorHandler = CoroutineExceptionHandler { _, throwable ->
        Timber.e("$throwable")
    }

    private val scope = CoroutineScope(Job() + Dispatchers.IO + errorHandler)

    fun requestStoragePermission(onResult: (Boolean) -> Unit) {
        requestPermission(
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Permissions.STORAGE.code,
            onResult
        )
    }

    fun requestPermission(permission: String, permissionCode: Int, onResult: (Boolean) -> Unit) {
        if (
            ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_DENIED
        ) {
            val permissions = arrayOf(permission)

            scope.launch {
                activity.requestPermissions(permissions, permissionCode)

                LiveEventBus.listen<EventPermissionResult>().collect {
                    onResult.invoke(it.isGranted)
                }
            }
        } else {
            onResult.invoke(true)
        }
    }


    enum class Permissions(val code: Int) {
        STORAGE(1001)
    }
}

Then in the MainActivity when we get the image, we trigger the call with the eventBus

class MainActivity : AppCompatActivity() {
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    if (resultCode == RESULT_OK) {
        lifecycleScope.launch {
            when (requestCode) {
                IMAGE_PICK_CODE -> data?.data?.let {
                    LiveEventBus.send(
                        EventImageSelected(it)
                    )
                }
            }
        }
    }
  }
}

There's probably a better solution

It might be worth checking the Jetpack Compose Accompanist lib for that It's still experimental at the moment https://github.com/google/accompanist/tree/main/permissions and the doc here : https://google.github.io/accompanist/permissions/

Upvotes: 0

Koch
Koch

Reputation: 594

Indeed it doesn't seem it has to do with how you're using the launcher. I understand the issue is that the screen comes back to the main activity after you select your image file from the image picker, at this point I would check how the backStackEntry is being managed. Try to comment out the "compose" body from mainActivity and observe if that does anything. ---> comment out this block.

{backStackEntry -> backStackEntry.arguments?.getString("form)?.let { _form ->
            val form = Gson().fromJson(_form, FormModel::class.java)
            FormDetailScreen(
                navController = navController,
                form = form,
                formViewModel = formViewModel
            )
        }
    }

Upvotes: 0

Koch
Koch

Reputation: 594

When you say "it worked fine" do you mean you checked that " val imageUri'" have the correct Uri of the file picked ?

Do you get any error from the logcat after selecting an image from the Image-Picker ?

Try with this and let us know if it does anything

var imageUri = remember { mutableStateOf<Uri?>(null) } // UPDATE
val context = LocalContext.current
var bitmap by remember { mutableStateOf<Bitmap?>(null) }

val launcher = rememberLauncherForActivityResult(contract =
ActivityResultContracts.GetContent()) { uri: Uri? -> 
    imageUri.value = uri // UPDATE
}

Upvotes: 1

Related Questions