ViewPager in Android with XML and Compose - kotlin

I'm working with Compose but have Fragments. I need to switch to second page when user click IconButton.
With only xml:
_binding = FragmentFirstScreenBinding.inflate(inflater, container, false)
val viewPager = activity?.findViewById<ViewPager2>(R.id.viewPager)
binding.firstFragmentButton.setOnClickListener{
viewPager?.currentItem = 1
}
In Compose:
AndroidView(factory = { context ->
val viewPager = activity?.findViewById<ViewPager2>(R.id.viewPager)
val view = LayoutInflater.from(context)
.inflate(R.layout.fragment_view_pager, null, false)
viewPager?.currentItem = 1
view
})
Box(
modifier = Modifier
.fillMaxSize()
) {
IconButton(
onClick = { },
modifier = Modifier
.height(90.dp)
.width(90.dp)
.align(Alignment.TopEnd)
) {
Icon(
painter = painterResource(id = R.drawable.ic_baseline_keyboard_arrow_right_24),
contentDescription = "",
)
}
}
Problem with this code is that automatically I switch to second page.
I tried to put viewPager in IconButton on click but I dont get view back and got null when user clicks icon. My question is how to get that viewPager into compose part.
EDIT:
Just put viewPager in onCreateView before
val viewPager = activity?.findViewById<ViewPager2>(R.id.viewPager)
return ComposeView(requireContext()).apply {
setContent {

Related

Jetpack compose custom snackbar material 3

How to achieve a custom snackbar in compose using material 3? I want to change the alignment of the snackbar. Also I want dynamic icon on the snackbar either on left or right side.
You can customize your snackbar using SnackBar composable and can change alignment using SnackbarHost alignment inside a Box if that's what you mean.
val snackState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
Box(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
) {
Column(modifier = Modifier.fillMaxSize()) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
coroutineScope.launch {
snackState.showSnackbar("Custom Snackbar")
}
}) {
Text("Show Snackbar")
}
}
SnackbarHost(
modifier=Modifier.align(Alignment.BottomStart),
hostState = snackState
) { snackbarData: SnackbarData ->
CustomSnackBar(
R.drawable.baseline_swap_horiz_24,
snackbarData.visuals.message,
isRtl = true,
containerColor = Color.Gray
)
}
}
#Composable
fun CustomSnackBar(
#DrawableRes drawableRes: Int,
message: String,
isRtl: Boolean = true,
containerColor: Color = Color.Black
) {
Snackbar(containerColor = containerColor) {
CompositionLocalProvider(
LocalLayoutDirection provides
if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
) {
Row {
Icon(
painterResource(id = drawableRes),
contentDescription = null
)
Text(message)
}
}
}
}
Validated answer does not answer the question properly because the icon is provided in a static way, not dynamic. The icon is not passed to showSnackbar.
You can do it by having your own SnackbarVisuals :
// Your custom visuals
// Default values are the same than SnackbarHostState.showSnackbar
data class SnackbarVisualsCustom(
override val message: String,
override val actionLabel: String? = null,
override val withDismissAction: Boolean = false,
override val duration: SnackbarDuration = if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite
// You can add custom things here (for you it's an icon)
#DrawableRes val drawableRes: Int
) : SnackbarVisuals
// The way you decide how to display your custom visuals
SnackbarHost(hostState = snackbarHostState) {
val customVisuals = it.visuals as SnackbarVisualsCustom
Snackbar {
// Here is your custom snackbar where you use your icon
}
}
// To display the custom snackbar
snackbarHostStateScope.launch {
snackbarHostState.showSnackbar(
SnackbarVisualsCustom(
message = "The message",
drawableRes = R.drawable.your_icon_id
)
)
}

How to send and get argument in composable in jetpack compose?

I'm new in jetpack compose. I developed an app by XML and Kotlin and want to move it to jetpack compose. The problem is that I have one activity and one fragment that there are several CardViews in the activity and when clicking on them they send a specific key to the fragment and the fragment shows specific information related to that CardView (I just have used one fragment), now in jetpack compose I don't know how to implement it. Could anyone help me, please? I have no problem with navigation I want to know how to change an Image and some Texts. I wonder that how can I use
when (key) {"key" -> text = ... imageResource = ...} in compose or not?
this is my code for XML
class BaseFragment : Fragment() {
internal lateinit var view: View
private lateinit var imgMain : ImageView
private lateinit var txtIngredients : TextView
private lateinit var txtMaking : TextView
private lateinit var txtBeCareful : TextView
#Suppress("UNREACHABLE_CODE")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
view = inflater.inflate(R.layout.base_fragment, container,
false)
setupViews()
return view
}
private fun setupViews() {
imgMain = view.findViewById(R.id.img_BaseFragment_mainImg)
txtIngredients =
view.findViewById(R.id.txt_BaseFragment_ingredients)
txtMaking = view.findViewById(R.id.txt_BaseFragment_making)
txtBeCareful =
view.findViewById(R.id.txt_BaseFragment_beCareFullText)
this.arguments?.let {
when (it.getString(key: "itemTitle", defaultVlue :"")) {
"mas1" -> {
makeContent(R.drawable.mas1,
R.string.mas1,
R.string.mas1_making,
R.string.mas1_text)
}
"mas2" -> {
makeContent(R.drawable.mas2,
R.string.mas2,
R.string.mas2_making,
R.string.mas2_text)
}
"mas3" -> {
makeContent(R.drawable.mas3,
R.string.mas3,
R.string.mas3_making,
R.string.mas3_text)
}
}
private fun makeContent(imgCont : Int, txtIngredientsCont : Int,
txtMakingCont : Int, txtBeCarCont : Int ){
imgMain.setImageResource(imgCont)
txtIngredients.setText(txtIngredientsCont)
txtMaking.setText(txtMakingCont)
txtBeCareful.setText(txtBeCarCont)
}
}
and this is my code in jetpack compose but it doesn't work
#SuppressLint("UnusedMaterialScaffoldPaddingParameter")
#Composable
fun FinalShowScreen(itemTitle: String? = null, navController:
NavController) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(3.dp)
.verticalScroll(rememberScrollState())
) {
var imageId: Int
var textIngredientsId: Int
var textMakingId: Int
var textBeCarefulId: Int
when (maskArg) {
"mas1" -> {
imageId = R.drawable.mas1
textIngredientsId = R.string.mas1
textMakingId = R.string.mas1making
textBeCarefulId = R.string.mas1_text
}
"mas2" -> {
imageId = R.drawable.mas2
textIngredientsId = R.string.mas2
textMakingId = R.string.mas2making
textBeCarefulId = R.string.mas2_text
}
"mas3" -> {
imageId = R.drawable.mas3
textIngredientsId = R.string.mas3
textMakingId = R.string.mas3making
textBeCarefulId = R.string.mas3_text
}
Image(
painter = painterResource(id =
imageId),
contentDescription = "",
modifier = Modifier
.padding(bottom = 8.dp)
.height(200.dp)
.fillMaxSize()
)
Text(
text = stringResource(id = textIngredientsId),
style = typography.h1,
color = MaterialTheme.colors.primary,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(8.dp)
)
Text(
text = stringResource(id =
textMakingId),
style = typography.h2,
color = MaterialTheme.colors.primaryVariant,
textAlign = TextAlign.Start,
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, bottom = 15.dp,
top = 8.dp)
)
Text(
text = stringResource(id =
textBeCarefulId),
style = typography.h2,
color = MaterialTheme.colors.primaryVariant,
textAlign = TextAlign.Start,
modifier = Modifier
.padding(8.dp)
)
}
}
Finally, my problem was solved when, I changed the variables to
#DrawableRes imageId: Int,
#StringRes textIngredientsId: Int,
#StringRes textMakingId: Int,
#StringRes textBeCarefulId: Int
and I changed the navigation method in the Navgragh to this
class MainActions(navController: NavController) {
val gotoFinalShow: (String) -> Unit = { maskArg ->
navController.navigate("${Screens.FinalShow.route}/$maskArg"){
launchSingleTop = true
restoreState = true
}
}
}

Kotlin Jetpack Compose, Card, change color on click

I cant get the Card in the LazyColumn to check if the number is in val guestNumbers and then change the color on the Card.
But the numbers in val guestNumbers changes on startup.
Is the Card the right stuff to use or should i use buttons?
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val guestNumbers = rememberSaveable {
mutableStateOf(mutableSetOf<Int>(10,11,2,22))
}
NumberGuessingGameTheme {
Scaffold(
topBar = {
TopAppBar {
}
}
) {
Row(modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxSize()
.weight(4f)
.background(color = Color.LightGray)
) {
Text(
text = "1F",
style = MaterialTheme.typography.caption
)
}
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f)
.background(color = Color.LightGray)
) {
LazyColumn {
items(1000 + 1) {
Card(modifier = Modifier
.fillMaxSize()
.padding(5.dp)
//.background(if ("$it".toInt() !in guestNumbers.value) Color.Green else Color.Red)
.clickable {
guestNumbers.value.add("$it".toInt())
Log.d("Tag", "${guestNumbers.value}")
},
elevation = 10.dp,
backgroundColor = if ("$it".toInt() in guestNumbers.value) Color.Red else Color.LightGray
) {
Text(text = "$it", fontSize = 28.sp, textAlign = TextAlign.Center)
}
}
}
}
}
}
}
}
}
}
mutableStateOf cannot track changes of the underlaying object. It can only trigger recomposition when you replace its value with an other object.
You can store an immutable set, create a modifiable copy to change it, and set the new value to your mutable state. This will create a new object.
var guestNumbers by rememberSaveable {
mutableStateOf(setOf(10,11,2,22))
}
Button({
val mutableSet = guestNumbers.toMutableSet()
mutableSet.add(if (Random.nextBoolean()) 2 else Random.nextInt())
guestNumbers = mutableSet.toSet()
}) {
Text(guestNumbers.toString())
}
Note, that if you'll face same problem with a list, you gonna need to call .toImmutableList(), because .toList() is only erasing the type and not actually creating a new object if called on a mutable list.

LazyColumn that respects MotionEvent.ACTION_SCROLL

How to force compose' LazyColumn to act like traditional scrollable elements like RecyclerView or ListView?
Useful when want to scroll with mouse, e.g. with vysor.
The solution is to add filter to modifier
const val VERTICAL_SCROLL_MULTIPLIER = -4
// Helps with scrolling with mouse wheel (just like recycler view/list view without compose)
#ExperimentalComposeUiApi
#Composable
fun MyLazyColumn(
modifier: Modifier = Modifier,
state: LazyListState, // rememberLazyListState()
content: LazyListScope.() -> Unit
) {
LazyColumn(
state = state,
modifier = modifier
.pointerInteropFilter {
if (it.action == MotionEvent.ACTION_SCROLL) {
state.dispatchRawDelta(it.getAxisValue(MotionEvent.AXIS_VSCROLL) * VERTICAL_SCROLL_MULTIPLIER)
}
false
},
content = content
)
}

Compose navigation title is not updating

I am trying to update the title of the TopAppBar based on a live data in the ViewModel, which I update on different screens. It looks like the live data is getting updated properly, but the update is not getting reflected on the title of the TopAppBar. Here is the code:
class MainActivity : ComponentActivity() {
#ExperimentalFoundationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LazyVerticalGridActivityScreen()
}
}
}
#ExperimentalFoundationApi
#Composable
fun LazyVerticalGridActivityScreen(destinationViewModel: DestinationViewModel = viewModel()) {
val navController = rememberNavController()
var canPop by remember { mutableStateOf(false) }
// getting the latest title value from the view model
val title: String by destinationViewModel.title.observeAsState("")
Log.d("MainActivity_title", title) // not getting called
navController.addOnDestinationChangedListener { controller, _, _ ->
canPop = controller.previousBackStackEntry != null
}
val navigationIcon: (#Composable () -> Unit)? =
if (canPop) {
{
IconButton(onClick = { navController.popBackStack() }) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null)
}
}
} else {
null
}
Scaffold(
topBar = {
TopAppBar(title = { Text(title) }, navigationIcon = navigationIcon) // updating the title
},
content = {
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("details/{listId}") { backStackEntry ->
backStackEntry.arguments?.getString("listId")?.let { DetailsScreen(it, navController) }
}
}
}
)
}
#ExperimentalFoundationApi
#Composable
fun HomeScreen(navController: NavHostController, destinationViewModel: DestinationViewModel = viewModel()) {
val destinations = DestinationDataSource().loadData()
// updating the title in the view model
destinationViewModel.setTitle("Lazy Grid Layout")
LazyVerticalGrid(
cells = GridCells.Adaptive(minSize = 140.dp),
contentPadding = PaddingValues(8.dp)
) {
itemsIndexed(destinations) { index, destination ->
Row(Modifier.padding(8.dp)) {
ItemLayout(destination, index, navController)
}
}
}
}
#Composable
fun ItemLayout(
destination: Destination,
index: Int,
navController: NavHostController
) {
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier
.background(MaterialTheme.colors.primaryVariant)
.fillMaxWidth()
.clickable {
navController.navigate("details/$index")
}
) {
Image(
painter = painterResource(destination.photoId),
contentDescription = stringResource(destination.nameId),
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.Crop
)
Text(
text = stringResource(destination.nameId),
color = Color.White,
fontSize = 14.sp,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp)
)
}
}
#Composable
fun DetailsScreen(
index: String,
navController: NavController,
destinationViewModel: DestinationViewModel = viewModel()
) {
val dataSource = DestinationDataSource().loadData()
val destination = dataSource[index.toInt()]
val destinationName = stringResource(destination.nameId)
val destinationDesc = stringResource(destination.descriptionId)
val destinationImage = painterResource(destination.photoId)
// updating the title in the view model
destinationViewModel.setTitle("Destination Details")
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
Image(
painter = destinationImage,
contentDescription = destinationName,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth()
)
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
Text(
text = destinationName,
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(vertical = 16.dp)
)
Text(text = destinationDesc, lineHeight = 24.sp)
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
OutlinedButton(
onClick = {
navController.navigate("home") {
popUpTo("home") { inclusive = true }
}
},
modifier = Modifier.padding(top = 24.dp)
) {
Image(
imageVector = Icons.Filled.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colors.primaryVariant),
modifier = Modifier.size(20.dp)
)
Text("Back to Destinations", modifier = Modifier.padding(start = 16.dp))
}
}
}
}
}
EDIT: The ViewModel
class DestinationViewModel : ViewModel() {
private var _title = MutableLiveData("")
val title: LiveData<String>
get() = _title
fun setTitle(newTitle: String) {
_title.value = newTitle
Log.d("ViewModel_title", _title.value.toString())
Log.d("ViewModelTitle", title.value.toString())
}
}
Can anyone please help to find the bug? Thanks!
Edit:
Here is the GitHub link of the project: https://github.com/rawhasan/compose-exercise-lazy-vertical-grid
The reason it's not working is because those are different objects created in different scopes.
When you're using a navController, each destination will have it's own scope for viewModel() creation. By the design you may have a view model for each destination, like HomeViewModel, DestinationViewModel, etc
You can't access an other destination view model from current destination scope, as well as you can't access view model from the outer scope(which you're trying to do)
What you can do, is instead of trying to retrieve it with viewModel(), you can pass outer scope view model to your composable:
composable("details/{listId}") { backStackEntry ->
backStackEntry.arguments?.getString("listId")?.let { DetailsScreen(it, navController, destinationViewModel) }
}
Check out more details about viewModel() in the documentation
Another problem with your code is that you're calling destinationViewModel.setTitle("Lazy Grid Layout") inside composable function. So this code will be called many times, which may lead to recomposition.
To call any actions inside composable, you need to use side-effects. LaunchedEffect in this case:
LaunchedEffect(Unit) {
destinationViewModel.setTitle("Destination Details")
}
This will be called only once after view appear. If you need to call it more frequently, you need to specify key instead of Unit, so it'll be recalled each time when the key changes
You might wanna have a look here https://stackoverflow.com/a/68671477/15880865
Also, you do not need to paste all the code in the question. Just the necessary bit. We shall ask for it if something is missing in the question. Just change your LiveData to mutableStateOf and then let me know if that fixed your problem. Also, you do not need to call observeAsState after modifying the type. Just refer to the link it contains all the info.
Thanks