I'm trying to launch an Activity by clicking on a button set on a BottomNavBar. There's a Compose Navigation set up using NavGraphBuilder.navigation() with a composable() call for each Compose screen like:
navigation(
startDestination = "home",
route = "main"
) {
composable("home") {
HomeScreen(...)
}
// Several more Screens
}
I've found out about NavGraphBuilder.activity(), so I was thinking something like:
activity("tickets") {
this.activityClass = ExternalActivity::class
}
And it works, if ExternalActivity doens't need any data to be passed to it. But it does.
The only viable alternative that comes to mind is using a composable() and launching the activity from there:
composable("tickets") { backStackEntry ->
val config = // get config from arguments
context.startActivity(
Intent(context, ExternalActivity::class.java).apply {
putExtra("config", config)
}
)
}
Or something along those lines. But it's kind of messy and has some side effects, so I'd like to avoid it.
Is there any way to use the activity() call and pass data to the Activity being launched?
I'm limited by the architecture of the codebase in which I'm working, so yes, it needs to be an Activity (it's actually from an external library).
Thanks.
The only thing that remotely resembles what you are trying to do would be data.
activity("tickets") {
this.activityClass = ExternalActivity::class
this.data = "Hello World".toUri() <--
}
[ExternalActivity]
override fun onCreate(savedInstanceState: Bundle?) {
...
val data = intent.data
But data is Uri, so it might not suit your needs, especially if you are dealing with an external library. Then context.startActivity() would be the next choice, as in your second approach.
One thing to note is that when you use context.startActivity() (instead of NavGraphBuilder.activity()), you need to set the "current" destination correctly when the Activity closes (e.g. call navController.navigateUp() or navController.popBackStack()). If not, it will jump back to the Activity you just closed, because as far as NavController is concerned, the Activity you started (and now closed) is the current destination.
Related
I am using a file picker inside a HorizontalPager in jetpack compose. When the corresponding screen is loaded while tapping the button, the launcher is triggered 2 times.
Code snippet
var openFileManager by remember {
mutableStateOf(false)
}
if (openFileManager) {
launcher.launch("*/*")
}
Button(text = "Upload",
onClick = {
openFileManager = true
})
Edited: First of all Ian's point is valid why not just launch it in the onClick directly? I also assumed that maybe you want to do something more with your true false value. If you want nothing but launch then all these are useless.
The screen can draw multiple times when you click and make openFileManager true so using only condition won't prevent it from calling multiple times.
You can wrap your code with LaunchedEffect with openFileManager as a key. The LaunchedEffect block will run only when your openFileManager change.
if (openFileManager) {
LaunchedEffect(openFileManager) {
launcher.launch("*/*")
}
}
You should NEVER store such important state inside a #Composable. Such important business logic is meant to be stored in a more robust holder like the ViewModel.
ViewModel{
var launch by mutableStateOf (false)
private set
fun updateLaunchValue(newValue: Boolean){
launch = newValue
}
}
Pass these to the Composable from the main activity
MyComposable(
launchValue = viewModel.launch
updateLaunchValue = viewModel::updateLaunchValue
)
Create the parameters in the Composable as necessary
#Comoosable
fun Uploader(launchValue: Boolean, onUpdateLaunchValue: (Boolean) -> Unit){
LaunchedEffect (launchValue){
if (launchValue)
launcher.launch(...)
}
Button { // This is onClick
onUpdateLaunchValue(true) // makes the value true in the vm, updating state
}
}
If you think it is overcomplicated, you're in the wrong paradigm. This is the recommended AND CORRECT way of handling state in Compose, or any declarative paradigm, really afaik. This keeps the code clean, while completely separating UI and data layers, allowing controlled interaction between UI and state to achieve just the perfect behaviour for the app.
I am trying to figure out how jobs with coroutines work. Basically, I want to launch this coroutine from FirstFragment and after that navigate to SecondFragment and get notified when this job is done. I call getData() in FirstFragment onViewCreated() and navigate to SecondFragment. Whether I write getData().isCompleted or getData().invokeOnCompletion { } in SecondFragment nothing happens. I don't know if I am missing something or not starting job correctly or something else.
private val _data = MutableStateFlow<GetResource<String>?>(null)
val data: StateFlow<GetResource<String>?> = _data
fun getData() = viewModelScope.launch {
repository.getData().collect {
_data.value = it
}
}
A Flow from a database never completes because it is supposed to monitor the database for changes indefinitely. It only stops when the coroutine is cancelled. Therefore the Job that collects such a Flow will never complete. Also, if you call getData() on the repo again, you are getting a new Flow instance each time.
Regardless of what you're doing, you need to be sure you are using the same ViewModel instance between both fragments by scoping it to the Activity. (Use by activityViewModels() for example.) This is so the viewModelScope won't be cancelled during the transition between Fragments.
If all you need is a single item from the repo one time, probably the simplest thing to do would be to expose a suspend function from the repo instead of a Flow. Then turn it into a Deferred. Maybe by making it a Lazy, you can selectively decide when to start retrieving the value. Omit the lazy if you just want to start retrieving the value immediately when the first Fragment starts.
// In the shared view model:
val data: Deferred<GetResource<String>> by lazy {
viewModelScope.async {
repository.getData() // suspend function returning GetResource<String>
}
}
fun startDataRetrieval() { data } // access the lazy property to start its coroutine
// In second fragment:
lifecycleScope.launch {
val value = mySharedViewModel.data.await()
// do something with value
}
But if you have to have the Flow because you’re using it for other purposes:
If you just want the first available value from the Flow, have the second Fragment monitor your data StateFlow for its first valid value.
lifecycleScope.launch {
val value = mySharedViewModel.data.filterNotNull().first()
// do something with first arrived value
}
And you can use SharedFlow so you don’t have to make the data type nullable. If you do this you can omit filterNotNull() above. In your ViewModel, it’s easier to do this with shareIn than your code that has to use a backing property and manually collect the source.
val data: SharedFlow<GetResource<String>> = repository.getData()
.shareIn(viewModelScope, replay = 1, SharingStarted.Eagerly)
If you need to wait before starting the collection to the SharedFlow, then you could make the property lazy.
Agreed with #Tenfour04 's answer, I would like to contribute a little more.
If you really want to control over the jobs or Structured Concurrency, i would suggest use custom way of handling the coroutine rather than coupled your code with the viewModelScope.
There are couple of things you need to make sure:
1- What happen when cancellation or exception occurrs
2- you have to manage the lifecycle of the coroutine(CoroutineScope)
3- Cancelling scope, depends on usecase like problem facing you are right now
4- Scope of ViewModel e.g: Either it is tied to activity(Shared ViewModel) or for specific fragment.
If you are not handling either of these carefully specifically first 3, your are more likely to leaking the coroutine your are gurenteed gonna get misbehavior of you app.
Whenever you start any coroutine in Custom way you have to make sure, what is going to be the lifecycle, when it gonna end, This is so important, it can cause real problems
Luckily, i have this sample of CustomViewModel using Jobs: Structured Concurrency sample code
In the case of using Di, the way it is written on the official Android website is as follows
// import androidx.hilt.navigation.compose.hiltViewModel
#Composable
fun MyApp() {
NavHost(navController, startDestination = startRoute) {
composable("example") { backStackEntry ->
// Creates a ViewModel from the current BackStackEntry
// Available in the androidx.hilt:hilt-navigation-compose artifact
val exampleViewModel = hiltViewModel<ExampleViewModel>()
ExampleScreen(exampleViewModel)
}
/* ... */
}
}
Then if there are a lot of other #Composable functions in the ExampleScreen, like this
ExampleScreen() {
A()
B()
}
A() {
TopBar()
BottomBar()
....
}
B() ...
If both A() and its sub-functions need to use things in vm, don't you have to pass the vm parameters one by one? Because if vm is created in these functions, it is not a singleton(Because navigation compose affects the viewModel, each time you switch the page, these viewModels will be recreated as a new one). When I was puzzled, I saw this design idea on the official website again:
Pass explicit parameters
The general idea is that I should pass the logic code of the child function in the parent function, e.g. in ExampleScreen write:
ExampleScreen() {
val vm = hilt<VM>()
A(onClick = vm.onClick, ...)
B(...)
}
So my question is, if I have a lot of nested functions, don't I need to write a logical parameter in each function? So if I want to create a vm directly in each function, but it is not a singleton, what should I do? Im confused
You've done the right thing by injecting the viewmodel at the top-level. It's now up to you to decide how you want to pass it down. They're just functions in the end.
You can pass the viewmodel down everywhere, pass down only specific members or pass nothing down.
Do what makes sense and iterate if it doesn't work.
I am trying to create an app that display random challenges in different levels of difficulty to build a gamified self-development app. As i am a pretty unexperienced developer(this is my first app that is not part of any course), I didn't used fragments but i actually created a layout that correspond to every challenge. In my app, i am displaying as MainActivity the different levels of difficulty which are represented by buttons. Each of those level button create an intent to an introductory page to the level which also contains a button at the bottom that should randomly select one of the layouts(categorized as part of this level) and display it to the user. My problem is that i don't know the code to do this kind of selection and previous answers didn't worked very well.
I tried to do this based on the answers:
private val SafeChallenges = listOf(
DeclutterPhone::class,
Drink2glasses::class,
TodoList::class
)
private fun startRandomActivity() {
startActivity(Intent(this, SafeChallenges.random().java))
// If this s in a Fragment, use requireContext() instead of this
}
But it didn't worked out. I created 3 classes to have each one with a setContentView to a specific layout(challenge) I know, there is no setOnClickListener but when i put one it either show me an error or doesn't do anything.
I also tried this type of loop but my button doesn't respond to it.
override fun onClick(view: View){
Log.d(TAG,"onclick: called")
category12_challenge_button.setOnClickListener {
Log.d(TAG,"button clicked")
val myRandomChallenges = Random.nextInt(1..3)
when(myRandomChallenges){
1->startActivity(Intent(this,DeclutterPhone::class.java))
2->startActivity(Intent(this,Drink2glasses::class.java))
3->startActivity(Intent(this,TodoList::class.java))
else -> IllegalArgumentException("unknown layout")
}
}
Could you tell me where I missed my point?
For android navigation try this:
val possibleDestinations = listOf(
R.id.action_mainFragment_to_firstFragment,
R.id.action_mainFragment_to_secondFragment,
R.id.action_mainFragment_to_thirdFragment)
viewBinding.apply {
randPick.setOnClickListener {
Navigation.findNavController(view).navigate(possibleDestinations.random())
}
}
To see it in action visit this working example on GitHub
Here is the definitive answer to the randomization loop of my algorithm. My main problem was that i didn't initialized the setOnclicklistener in onCreate when i made my onClick function.
I based my code on Tenfour04 answer even if his/her answer seems to be gone.
Anyways, thank you guys. I promise, next question will be much more clean and detailed ;)
There is the code for those that might want it, i created 3 classed to have 3 setContentView for 3 different XML layouts:
override fun onClick(view: View) {
val safeChallenges = listOf(
DeclutterPhone::class,
Drink2glasses::class,
TodoList::class
)
val intent = when(view.id){
R.id.category12_challenge_button ->{
Intent(this, safeChallenges.random().java)
}
else -> throw IllegalArgumentException("unknown layout selected")
}
startActivity(intent)
}
I've come across an interesting problem with trying to accomplish dynamic or conditional navigation with the Jetpack Navigation library.
The goal I have in mind is to be able to continue using the nav_graph.xml to manage the overall navigation graph, but simultaneously allow for conditional navigation based on some factors.
I have included some code below that shows where my solution is headed. The problem is that it inherently requires a lot of maintenance for future conditional logic to work.
I really want the navigateToDashboard function in the example to be able to be executed with either no parameters, or parameters that rarely change. For instance, instead of passing NavDirections, maybe passing some identifier that let's the navigateToDashboard function know which NavDirections to return.
Code for the class managing the conditional logic.
class DynamicNavImpl(private val featureFlagService: FeatureFlagService) : DynamicNav {
override fun navigateToDashboard(navDirectionsMap: Map<Int, NavDirections>): NavDirections {
val destinationIdRes = if (featureFlagService.isDashboardV2Enabled()) {
R.id.dashboardV2Fragment
} else {
R.id.dashboardFragment
}
return navDirectionsMap[destinationIdRes] ?: handleNavDirectionsException(destinationIdRes)
}
private fun handleNavDirectionsException(destinationIdRes: Int): Nothing {
throw IllegalStateException("Destination $destinationIdRes does not have an accompanying set of NavDirections. Are you sure you added NavDirections for it?")
}
}
Call site examples
navigate(
dynamicNav.navigateToDashboard(
mapOf(
Pair(R.id.dashboardFragment, PhoneVerificationFragmentDirections.phoneVerificationToDashboard()),
Pair(R.id.dashboardV2Fragment, PhoneVerificationFragmentDirections.phoneVerificationToDashboardV2())
)
)
)
navigate(
dynamicNav.navigateToDashboard(
mapOf(
Pair(R.id.dashboardFragment, EmailLoginFragmentDirections.emailLoginToDashboard()),
Pair(R.id.dashboardV2Fragment, EmailLoginFragmentDirections.emailLoginToDashboardV2())
)
)
)
Looking at the call site, you could see how this could be problematic. If I ever want to add a new potential destination, let's say dashboardV3Fragment, then I'd have to go to each call site and add another Pair.
This almost defeats the purpose of having the DynamicNavImpl class. So this is where I am stuck. I want to be able to encapsulate the various variables involved in deciding what destination to go to, but it seems with how NavDirections are implemented, I'm not able to.
I went between a few different approaches, and I landed on something that still doesn't feel ideal, but works for my use case.
I completely abandoned the idea of using a central dynamic navigation manager. Instead, I decided on having a "redirect" or "container" Fragment that decides what Fragment to show.
So here's the new code inside of the DashboardRedirectFragment
childFragmentManager.beginTransaction().replace(
R.id.dashboard_placeholder,
if (featureFlagService.isDashboardV2Enabled()) {
DashboardV2Fragment.newInstance()
} else {
DashboardFragment.newInstance()
}
).commit()
The way I'm using this is by registering a new destination in my nav graph called dashboardRedirectFragment, and anything in the graph that needs access to the dashboard use the dashboardRedirectFragment destination.
This fully encapsulates the dynamic navigation logic in the redirect Fragment, and allows me to continue using my nav graph as expected.