I have this composable that used to work fine, but now after some libraries update it doesn't.
I'm using a ViewModel to save an image returned from ActivityResultContracts.TakePicture() and show it in an Image within a Box.
The PhotoButton composable is working fine and return the correct image url, but the image string in my main composable is always null.
SamplePage.kt
#Composable
fun SamplePage(navController: NavController) {
val inputViewModel = InputViewModel()
val context = LocalContext.current
Column{
InputFields(inputViewModel, navController, setPerm)
}
}
#Composable
fun InputFields(inputViewModel: InputViewModel, navController: NavController) {
val image: String by inputViewModel.image.observeAsState("")
Column() {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(contentAlignment = Alignment.Center) {
val painter = rememberImagePainter(data = image)
Image(
painter = painter,
contentScale = ContentScale.FillWidth,
contentDescription = null
)
if (painter.state !is ImagePainter.State.Success) {
Icon(
painter = painterResource(id = R.drawable.icon),
contentDescription = null
)
}
}
PhotoButton() {
inputViewModel.onImageChange(it)
}
}
}
}
class InputViewModel : ViewModel() {
private val _image: MutableLiveData<String> = MutableLiveData("")
val image: LiveData<String> = _image
fun onImageChange(newImage: String) {
_image.value = newImage
}
}
PhotoButton.kt
#Composable
fun PhotoButton(onValChange: ((String) -> Unit)?){
val context = LocalContext.current
val storageDir: File? = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val file = File(storageDir, "picFromCamera")
val uri = FileProvider.getUriForFile(
context,
context.packageName.toString() + ".provider",
file
)
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) {
if (onValChange != null) {
onValChange(uri.toString())
}
}
FAB() {
launcher.launch(uri)
}
}
You're creating a new view model on each recomposition:
val inputViewModel = InputViewModel()
Instead you should use viewModel(): it'll create a new view model on the first call, and store it for the future calls:
val inputViewModel = viewModel<InputViewModel>()
Check out more about view models usage in compose state documentation.
Related
I am working on trying to integrate Google Sign in API with my application. It is part of a university project. I followed the following website: https://proandroiddev.com/google-signin-compose-a9afa67b7519
But, when I try, it simply does not work. I have setup everything even in the Google Cloud APIs & Services, OAuth 2.0 Client IDs.
#HiltViewModel
class UserSessionViewModel #Inject constructor(
application: Application,
) : ViewModel() {
private val _user: MutableStateFlow<User?> = MutableStateFlow(null)
val user: StateFlow<User?> = _user
companion object {}
init {
verifySignedInUser(application.applicationContext)
}
fun signIn(email: String, name: String){
viewModelScope.launch {
_user.value = User(
email = email,
displayName = name
)
}
}
fun signOut(){}
private fun verifySignedInUser(applicationContext: Context){
val gsa = GoogleSignIn.getLastSignedInAccount(applicationContext)
if(gsa != null){
_user.value = User(
email = gsa.email.toString(),
displayName = gsa.givenName.toString()
)
Log.d("User", _user.value.toString())
}
}
}
class SignInGoogleViewModelFactory(
private val application: Application
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(UserSessionViewModel::class.java)) {
return UserSessionViewModel(application) as T
}
throw IllegalArgumentException("Unknown view-model class")
}
}
I have the following in JSON:
{"web":
{
"client_id":"",
"project_id":"",
"auth_uri":"https://accounts.google.com/o/oauth2/auth",
"token_uri":"https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs",
"client_secret":""
}
}
EDIT:
It isn't clear, how I am supposed to use this from my Android Application. If, anyone has any suggestions please share. The tutorial doesn't mention anything about this, more precisely.
Login Screen:
#SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun LoginScreen(
rootNavHostController: NavHostController? = null) {
val coroutine = rememberCoroutineScope()
val systemUiController = rememberSystemUiController()
val context = LocalContext.current
val signInViewModel : UserSessionViewModel = viewModel(
factory = SignInGoogleViewModelFactory(context.applicationContext as Application)
)
(context as? Activity)?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
val state = signInViewModel.user.collectAsState()
val user = state.value
val isError = rememberSaveable { mutableStateOf(false) }
SideEffect {
systemUiController.setStatusBarColor(
color = Color.Transparent,
darkIcons = false
)
systemUiController.setNavigationBarColor(
color = Color.Transparent,
darkIcons = false,
navigationBarContrastEnforced = false
)
}
val authResult = rememberLauncherForActivityResult(contract = GoogleApiContract()) { task ->
try {
val gsa = task?.getResult(ApiException::class.java)
if(gsa != null){
coroutine.launch {
signInViewModel.signIn(gsa.email.toString(), gsa.givenName.toString())
}
} else {
isError.value = true
}
} catch(e: ApiException){ Log.d("App", e.toString()) }
}
Scaffold(modifier = Modifier.fillMaxSize(),
content = {
Content(
onClick = { authResult.launch(1) },
isError = isError.value,
signInViewModel
)
}
)
user?.let { rootNavHostController?.navigate("Home") }
}
#Composable
fun Content(
onClick: () -> Unit,
isError: Boolean = false,
_signInViewModel: UserSessionViewModel
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter)
{
Image(
painter = painterResource(R.drawable.background),
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.FillBounds
)
Box(modifier = Modifier
.fillMaxSize()
.padding(top = 70.dp, bottom = 0.dp),
contentAlignment = Alignment.TopCenter,
content = {
Column(modifier = Modifier
.fillMaxWidth()
.padding(0.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
content = {
Image(
painter = painterResource(R.drawable.app_logo),
contentDescription = null,
modifier = Modifier.padding(0.dp),
contentScale = ContentScale.None
)
Text(
text = stringResource(R.string.app_description),
modifier = Modifier.padding(16.dp),
color = Color.White,
fontSize = 14.sp,
fontStyle = FontStyle.Italic,
lineHeight = 22.sp
)
}
)
}
)
Column(horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.75f)) {
Row(
content = {
ElevatedButton(
onClick = { onClick() },
content = {
Text("Sign in with Google", color = Color.Black)
}
)
}
)
Row(content = {
Info(
text = stringResource(R.string.info),
)
})
}
}
}
GoogleApiContract:
class GoogleApiContract : ActivityResultContract<Int, Task<GoogleSignInAccount>?>(){
override fun createIntent(context: Context, input: Int): Intent {
val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken("")
.requestEmail()
.requestProfile()
.build()
val intent = GoogleSignIn.getClient(context, signInOptions)
return intent.signInIntent
}
override fun parseResult(resultCode: Int, intent: Intent?): Task<GoogleSignInAccount>? {
return when (resultCode) {
Activity.RESULT_OK -> {
GoogleSignIn.getSignedInAccountFromIntent(intent)
}
else -> null
}
}
}
EDIT: I think, the issue is here. But, I have no idea how to use the client_id and the client_secret it isn't clear from the tutorial nor the documentation.
Removing .requestIdToken("") completely resolved the issue. Wasn't required to invoke this method. Strangely.
Note: this .requestIdToken("") line, even with correct signature, it still didn't work.
I have some suggestions after reading your question and comments i think it will clear your issue if you use requireActivity() as context in your code other than application context and if still its not working then try to create new credentials with Web client (Auto-created for Google Sign-in) and use that in your code i think iam right if not please reply
I was developing an App where I try to implement some new technologies, as Jetpack Compose. And in general, it's a great tool, except the fact that it has hard pre-visualize system (#Preview) thn the regular xml design files.
My problem comes when I try to create a #Preview of the component which represent the different rows, where I load my data recover from network.
In my case I made this:
#Preview(
name ="ListScreenPreview ",
showSystemUi = true,
showBackground = true,
device = Devices.NEXUS_9)
#Composable
fun myPokemonRowPreview(
#PreviewParameter(PokemonListScreenProvider::class) pokemonMokData: PokedexListModel
) {
PokedexEntry(
model = pokemonMokData,
navController = rememberNavController(),
viewModel = hiltViewModel())
}
class PokemonListScreenProvider: PreviewParameterProvider<PokedexListModel> {
override val values: Sequence<PokedexListModel> = sequenceOf(
PokedexListModel(
pokemonName = "Cacamon",
number = 0,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png"
),
PokedexListModel(
pokemonName = "Tontaro",
number = 73,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png"
)
)
}
To represent this #Composable:
#Composable
fun PokemonListScreen(
navController: NavController,
viewModel: PokemonListViewModel
) {
Surface(
color = MaterialTheme.colors.background,
modifier = Modifier.fillMaxSize()
)
{
Column {
Spacer(modifier = Modifier.height(20.dp))
Image(
painter = painterResource(id = R.drawable.ic_international_pok_mon_logo),
contentDescription = "Pokemon",
modifier = Modifier
.fillMaxWidth()
.align(CenterHorizontally)
)
SearchBar(
hint = "Search...",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
viewModel.searchPokemonList(it)
}
Spacer(modifier = Modifier.height(16.dp))
PokemonList(navController = navController,
viewModel = viewModel)
}
}
}
#Composable
fun SearchBar(
modifier: Modifier = Modifier,
hint: String = " ",
onSearch: (String) -> Unit = { }
) {
var text by remember {
mutableStateOf("")
}
var isHintDisplayed by remember {
mutableStateOf(hint != "")
}
Box(modifier = modifier) {
BasicTextField(value = text,
onValueChange = {
text = it
onSearch(it)
},
maxLines = 1,
singleLine = true,
textStyle = TextStyle(color = Color.Black),
modifier = Modifier
.fillMaxWidth()
.shadow(5.dp, CircleShape)
.background(Color.White, CircleShape)
.padding(horizontal = 20.dp, vertical = 12.dp)
.onFocusChanged {
isHintDisplayed = !it.isFocused
}
)
if (isHintDisplayed) {
Text(
text = hint,
color = Color.LightGray,
modifier = Modifier
.padding(horizontal = 20.dp, vertical = 12.dp)
)
}
}
}
#Composable
fun PokemonList(
navController: NavController,
viewModel: PokemonListViewModel
) {
val pokemonList by remember { viewModel.pokemonList }
val endReached by remember { viewModel.endReached }
val loadError by remember { viewModel.loadError }
val isLoading by remember { viewModel.isLoading }
val isSearching by remember { viewModel.isSearching }
LazyColumn(contentPadding = PaddingValues(16.dp)) {
val itemCount = if (pokemonList.size % 2 == 0) {
pokemonList.size / 2
} else {
pokemonList.size / 2 + 1
}
items(itemCount) {
if (it >= itemCount - 1 && !endReached && !isLoading && !isSearching) {
viewModel.loadPokemonPaginated()
}
PokedexRow(rowIndex = it, models = pokemonList, navController = navController, viewModel = viewModel)
}
}
Box(
contentAlignment = Center,
modifier = Modifier.fillMaxSize()
) {
if (isLoading) {
CircularProgressIndicator(color = MaterialTheme.colors.primary)
}
if (loadError.isNotEmpty()) {
RetrySection(error = loadError) {
viewModel.loadPokemonPaginated()
}
}
}
}
#SuppressLint("LogNotTimber")
#Composable
fun PokedexEntry(
model: PokedexListModel,
navController: NavController,
modifier: Modifier = Modifier,
viewModel: PokemonListViewModel
) {
val defaultDominantColor = MaterialTheme.colors.surface
var dominantColor by remember {
mutableStateOf(defaultDominantColor)
}
Box(
contentAlignment = Center,
modifier = modifier
.shadow(5.dp, RoundedCornerShape(10.dp))
.clip(RoundedCornerShape(10.dp))
.aspectRatio(1f)
.background(
Brush.verticalGradient(
listOf(dominantColor, defaultDominantColor)
)
)
.clickable {
navController.navigate(
"pokemon_detail_screen/${dominantColor.toArgb()}/${model.pokemonName}/${model.number}"
)
}
) {
Column {
CoilImage(
imageRequest = ImageRequest.Builder(LocalContext.current)
.data(model.imageUrl)
.target {
viewModel.calcDominantColor(it) { color ->
dominantColor = color
}
}.build(),
imageLoader = ImageLoader.Builder(LocalContext.current)
.availableMemoryPercentage(0.25)
.crossfade(true)
.build(),
contentDescription = model.pokemonName,
modifier = Modifier
.size(120.dp)
.align(CenterHorizontally),
loading = {
ConstraintLayout(
modifier = Modifier.fillMaxSize()
) {
val indicator = createRef()
CircularProgressIndicator(
//Set constrains dynamically
modifier = Modifier.constrainAs(indicator) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
)
}
},
// shows an error text message when request failed.
failure = {
Text(text = "image request failed.")
}
)
Log.d("pokemonlist", model.imageUrl)
Text(
text = model.pokemonName,
fontFamily = RobotoCondensed,
fontSize = 20.sp,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
#Composable
fun PokedexRow(
rowIndex: Int,
models: List<PokedexListModel>,
navController: NavController,
viewModel: PokemonListViewModel
) {
Column {
Row {
PokedexEntry(
model = models[rowIndex * 2],
navController = navController,
modifier = Modifier.weight(1f),
viewModel = viewModel
)
Spacer(modifier = Modifier.width(16.dp))
if (models.size >= rowIndex * 2 + 2) {
PokedexEntry(
model = models[rowIndex * 2 + 1],
navController = navController,
modifier = Modifier.weight(1f),
viewModel = viewModel
)
} else {
Spacer(modifier = Modifier.weight(1f))
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
#Composable
fun RetrySection(
error: String,
onRetry: () -> Unit,
) {
Column() {
Text(error, color = Color.Red, fontSize = 18.sp)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { onRetry() },
modifier = Modifier.align(CenterHorizontally)
) {
Text(text = "Retry")
}
}
}
I try to annotate with the #Nullable navController and viewmodel of the PokemonListScreen #Composable, but doesn't work either. I'm still seeing an empty screen:
So I try to search into the Jetpack documentation but, it's just defining quite simple Composables.
So if you have some more knowledge about it and can help, thanks in advance !
The main problem is if I wanna Preview that #Composable, although I made #Nullable to the viewmodel parameter, which I guess it's the problem here, AS still demand to initialize. Because I guess the right way to pass argument to a preview is by #PreviewArgument annotation.
[EDIT]
After some digging, I found AS is returning the following error under the Preview Screen:
So, there anyway to avoid viewmodel error??
[SOLUTION]
Finally a apply the following solution which make works, because the cause of the problem is due to Hilt have some inconpatibilities with Jetpack Compose previews:
Create an interface of the your ViewModel which recover all the variables and methods.
Make yourcurrent viemodel class extends of the interface.
Create a 2ยบ class which extends on the interface and pass that to your #Preview
#SuppressLint("UnrememberedMutableState")
#Preview(
name ="ListScreenPreview",
showSystemUi = true,
showBackground = true,
device = Devices.PIXEL)
#Composable
fun MyPokemonRowPreview(
#PreviewParameter(PokemonListScreenProvider::class) pokemonMokData: PokedexListModel
) {
JetpackComposePokedexTheme {
PokedexRow(
rowIndex = 0,
models = PokemonListScreenProvider().values.toList(),
navController = rememberNavController(),
viewModel = PokemonListViewModelMock(
0, mutableStateOf(""), mutableStateOf(value = false),
mutableStateOf(false), mutableStateOf(listOf(pokemonMokData))
)
)
}
}
class PokemonListScreenProvider: PreviewParameterProvider<PokedexListModel> {
override val values: Sequence<PokedexListModel> = sequenceOf(
PokedexListModel(
pokemonName = "Machasaurio",
number = 0,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png"
),
PokedexListModel(
pokemonName = "Tontaro",
number = 73,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png"
)
)
}
PokemonListViewModelInterface
interface PokemonListViewModelInterface {
var curPage : Int
var loadError: MutableState<String>
var isLoading: MutableState<Boolean>
var endReached: MutableState<Boolean>
var pokemonList: MutableState<List<PokedexListModel>>
fun searchPokemonList(query: String)
fun loadPokemonPaginated()
fun calcDominantColor(drawable: Drawable, onFinish: (Color) -> Unit)
}
PokemonListViewModelMock
class PokemonListViewModelMock (
override var curPage: Int,
override var loadError: MutableState<String>,
override var isLoading: MutableState<Boolean>,
override var endReached: MutableState<Boolean>,
override var pokemonList: MutableState<List<PokedexListModel>>
): PokemonListViewModelInterface{
override fun searchPokemonList(query: String) {
TODO("Not yet implemented")
}
override fun loadPokemonPaginated() {
TODO("Not yet implemented")
}
override fun calcDominantColor(drawable: Drawable, onFinish: (Color) -> Unit) {
TODO("Not yet implemented")
}
}
The actual Preview is the following, and although the image doesn't display, is shown correctly:
You could create another composable which invokes the viewmodel logic via lambda functions instead of using the viewmodel itself. Extract your uiState to a separate class, so it can be used as a StateFlow in your viewmodel, which in turn can be observed from the composable.
#Composable
fun PokemonListScreen(
navController: NavController,
viewModel: PokemonListViewModel
) {
/*
rememberStateWithLifecyle is an extension function based on
https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
*/
val uiState by rememberStateWithLifecycle(viewModel.uiState)
PokemonListScreen(
uiState = uiState,
onLoadPokemons = viewModel::loadPokemons,
onSearchPokemon = {viewModel.searchPokemon(it)},
onCalculateDominantColor = {viewModel.calcDominantColor(it)},
onNavigate = {route -> navController.navigate(route, null, null)},
)
}
#Composable
private fun PokemonListScreen(
uiState: PokemonUiState,
onLoadPokemons:()->Unit,
onSearchPokemon: (String) -> Unit,
onCalculateDominantColor: (Drawable) -> Color,
onNavigate:(String)->Unit,
) {
}
#HiltViewModel
class PokemonListViewModel #Inject constructor(/*your datasources*/) {
private val loading = MutableStateFlow(false)
private val loadError = MutableStateFlow(false)
private val endReached = MutableStateFlow(false)
private val searching = MutableStateFlow(false)
private val pokemons = MutableStateFlow<Pokemon?>(null)
val uiState: StateFlow<PokemonUiState> = combine(
loading,
loadError,
endReached,
searching,
pokemons
) { loading, error, endReached, searching, pokemons ->
PokemonUiState(
isLoading = loading,
loadError = error,
endReached = endReached,
isSearching = searching,
pokemonList = pokemons,
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = PokemonUiState.Empty,
)
}
data class PokemonUiState(
val pokemonList: List<Pokemon> = emptyList(),
val endReached: Boolean = false,
val loadError: Boolean = false,
val isLoading: Boolean = false,
val isSearching: Boolean = false,
) {
companion object {
val Empty = PokemonUiState()
}
}
I'm not sure of the depth of this application, but a potential idea would be to code to an interface and not an implementation.
That is, create an interface with all of the functions you need (that may already exist in your ViewModel), have your PokemonListViewModel implement it, and create another mock class that implements it as well. Pass the mock into your preview and leave the real implementation with PokemonListViewModel
interface PokeListViewModel {
...
// your other val's
val isLoading: Boolean
fun searchPokemonList(pokemon: String)
fun loadPokemonPaginated()
// your other functions
...
}
Once you create your interface you can simply update your composables to be expecting an object that "is a" PokeListViewModel, for example.
Hopefully this helps
I want to make each card clickable and navigate from screen A to B. On Screen A it contains list view of card already. At screen B, I want to display each information in detail about each card from this Api.
Api Link : https://api.test.dev3.coolbeans.studio/books
This is my API Class
interface ApiInterface {
#GET("/books")
suspend fun getBooks(): Response
companion object {
private var apiInterface: ApiInterface? = null
fun getInstance(): ApiInterface {
if (apiInterface == null) {
apiInterface = Retrofit.Builder()
.baseUrl("https://api.test.dev3.coolbeans.studio")
.addConverterFactory(GsonConverterFactory.create()).build()
.create(ApiInterface::class.java)
}
return apiInterface!!
}
}}
This is my HomeScreen
#Composable
fun HomeScreen (book: BookDetail) {
Card(
backgroundColor = Color.White, elevation = 2.dp, modifier = Modifier.padding(all = 10.dp)
) {
Column(
modifier = Modifier
.padding(all = 10.dp)
.fillMaxWidth()
) {
Column {
Text(
text = "Id: ${book.id}",
style = TextStyle(fontSize = 14.sp),
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp)
)
Text(
text = "Title: ${book.title}",
style = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium),
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp)
)
Text(
text = "Description: ${book.description}",
style = TextStyle(fontSize = 14.sp, textAlign = TextAlign.Justify),
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp)
)
Text(
text = "Author: ${book.author}",
style = TextStyle(fontSize = 14.sp),
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp)
)
}
}
}
}
This is my MainActivity
class MainActivity : ComponentActivity() {
private val bookViewModel by viewModels<BookViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BookList(bookList = bookViewModel.bookListResponse)
bookViewModel.getBookList()
}
}
}
#Composable
fun BookList(bookList: List<BookDetail>) {
LazyColumn {
itemsIndexed(items = bookList) { index, item ->
HomeScreen(book = item)
}
}
}
This is my BookViewModel
class BookViewModel : ViewModel() {
var bookListResponse: List<BookDetail> by mutableStateOf(listOf())
var errorMessage: String by mutableStateOf("")
fun getBookList() {
viewModelScope.launch {
val apiInterface = ApiInterface.getInstance()
try {
apiInterface.getBooks().let{
bookListResponse = it.rows
}
} catch (e: Exception) {
errorMessage = e.message.toString()
}
}
}
}
This is my BookDetail (Model class)
data class BookDetail(
val author: String,
val description: String,
val id: String,
val thumbnailUrl: String,
val title: String
)
data class Response(
val count: Int,
val rows: List<BookDetail>
)
You can follow along this tutorial to show this data on recycler view. https://www.section.io/engineering-education/handling-recyclerview-clicks-the-right-way/
Once You have setup the recycler view and adapter then you can go ahead with the below implementation.
Now First, make sure the BookDetail class implements the Serializable interface:
class BookDetail : Serializable {
// your stuff
}
then on each item's click you can start the activity B and send BookDetails to it using intents. eg:
fun launchNextScreen(context: Context, book: BookDetail): Intent {
val intent = Intent(context, NextScreenActivity::class.java)
intent.putExtra("book_details", book)
return intent
}
startActivity(launchNextScreen())
To receive BookDetail back from on Activity B Intent you'll need to call:
val bookDetail = intent.getSerializableExtra("book_details") as? BookDetail
For my pet app, I have a composable that looks like this:
#Composable
fun PokeBrowser(model: PokeBrowserViewModel) {
val pokemonDataState by model.pokemonDataList.observeAsState()
pokemonDataState?.let {
val staticState = pokemonDataState ?: listOf()
LazyColumn() {
itemsIndexed(staticState) { i, p ->
if (i == staticState.lastIndex) {
model.loadPokemonData()
}
val pokeImagePainter = rememberImagePainter(
data = p.imageUrl
)
Row(
horizontalArrangement = Arrangement.Start,
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
) {
Image(
painter = pokeImagePainter,
contentDescription = "Pokemon name"
)
Text(text = p.name)
}
}
}
}
}
I populate the lazy load of data in this way:
private fun getPokemonData(nextPokeList: List<GetPokemonListResponse.PokemonUrl>) {
viewModelScope.launch {
val requests = nextPokeList.map { urlData ->
async {
pokeClient.getPokemonData(urlData.url)
}
}
val responses = requests.awaitAll()
val newPokemonDataList = _pokemonDataList.value as ArrayList
newPokemonDataList.addAll(
pokeClientMapper.mapPokeDataResponseToDomainModel(responses)
)
_pokemonDataList.postValue(newPokemonDataList)
}
}
I can see that more data is coming in on this line: _pokemonDataList.postValue(newPokemonDataList)
But, the composable does not update. Am I missing something?
Try this:
val pokemonDataState by model.pokemonDataList.observeAsState().value
I suspected that to postValue correctly I needed a brand new collection pointer. So, I rewrote the code in this way, which fixed the problem:
private fun getPokemonData(nextPokeList: List<GetPokemonListResponse.PokemonUrl>) {
viewModelScope.launch {
val requests = nextPokeList.map { urlData ->
async {
pokeClient.getPokemonData(urlData.url)
}
}
val responses = requests.awaitAll()
val oldPokemonData = _pokemonDataList.value as ArrayList
val newPokemonData = ArrayList<Pokemon>(oldPokemonData.size + responses.size)
newPokemonData.addAll(oldPokemonData.toList())
newPokemonData.addAll(pokeClientMapper.mapPokeDataResponseToDomainModel(responses))
_pokemonDataList.postValue(newPokemonData)
}
}
I am using accompanist-coil:0.12.0. I want to load image from a url and then pass the drawable to a method. I am using this:
val painter = rememberCoilPainter(
request = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.target {
viewModel.calcDominantColor(it) { color ->
dominantColor = color
}
}
.build(),
fadeIn = true
)
and then passing the painter to Image like this:
Image(
painter = painter,
contentDescription = "Some Image",
)
The image loads without any problem but the method calcDominantColor is never called.
Am I doing it the wrong way?
UPDATE:
I was able to call the method using Transformation in requestBuilder but I am not sure, if this is how it is supposed to be done because I am not actually transforming the Bitmap itself:
val painter = rememberCoilPainter(
request = entry.imageUrl,
requestBuilder = {
transformations(
object: Transformation{
override fun key(): String {
return entry.imageUrl
}
override suspend fun transform(
pool: BitmapPool,
input: Bitmap,
size: Size
): Bitmap {
viewModel.calcDominantColor(input) { color ->
dominantColor = color
}
return input
}
}
)
}
)
This works fine for first time but when the composable recomposes, transformation is returned from cache and my method doesn't run.
I think you want to use LaunchedEffect along with an ImageLoader to access the bitmap from the loader result.
val context = LocalContext.current
val imageLoader = ImageLoader(context)
val request = ImageRequest.Builder(context)
.transformations(RoundedCornersTransformation(12.dp.value))
.data(imageUrl)
.build()
val imagePainter = rememberCoilPainter(
request = request,
imageLoader = imageLoader
)
LaunchedEffect(key1 = imagePainter) {
launch {
val result = (imageLoader.execute(request) as SuccessResult).drawable
val bitmap = (result as BitmapDrawable).bitmap
val vibrant = Palette.from(bitmap)
.generate()
.getVibrantColor(defaultColor)
// do something with vibrant color
}
}
I would suggest using the new coil-compose library. Just copy the following and add it to the app build.gradle file:
implementation "io.coil-kt:coil-compose:1.4.0"
I was also following the tutorial and got stuck at this point. I would suggest copying and pasting the following code:
Column {
val painter = rememberImagePainter(
data = entry.imageUrl
)
val painterState = painter.state
Image(
painter = painter,
contentDescription = entry.pokemonName,
modifier = Modifier
.size(120.dp)
.align(CenterHorizontally),
)
if (painterState is ImagePainter.State.Loading) {
CircularProgressIndicator(
color = MaterialTheme.colors.primary,
modifier = Modifier
.scale(0.5f)
.align(CenterHorizontally)
)
}
else if (painterState is ImagePainter.State.Success) {
LaunchedEffect(key1 = painter) {
launch {
val image = painter.imageLoader.execute(painter.request).drawable
viewModel.calcDominantColor(image!!) {
dominantColor = it
}
}
}
}
Text(
text = entry.pokemonName,
fontFamily = RobotoCondensed,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
Replace your "AsyncImage" with "AsyncImageWithDrawable" :
#Composable
fun AsyncImageWithDrawable(
model: Any?,
contentDescription: String?,
modifier: Modifier = Modifier,
placeholderResId: Int? = null,
errorResId: Int? = null,
fallbackResId: Int? = errorResId,
contentScale: ContentScale,
onDrawableLoad: (Drawable?) -> Unit) {
val painter = rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current).data(data = model)
.apply(block = fun ImageRequest.Builder.() {
crossfade(true)
placeholderResId?.let { placeholder(it) }
errorResId?.let { error(it) }
fallbackResId?.let { fallback(it) }
allowHardware(false)
}).build()
)
val state = painter.state
Image(
painter = painter,
contentDescription = contentDescription,
modifier = modifier,
contentScale = contentScale
)
when (state) {
is AsyncImagePainter.State.Success -> {
LaunchedEffect(key1 = painter) {
launch {
val drawable: Drawable? =
painter.imageLoader.execute(painter.request).drawable
onDrawableLoad(drawable)
}
}
}
else -> {}
}
}
I've created this Composable inspired by #Aknk answer and Coil source code.
Hint: You can use this Composable to Load and Render your Image from Url and get your Image Palette by the returned Drawable.
You can use the onSuccess callback to get the drawable:
AsyncImage(
model = url,
contentDescription = null,
onSuccess = { success ->
val drawable = success.result.drawable
}
)
You can use the same approach also with rememberAsyncImagePainter.